2026-02-19 11:29:44 +00:00
|
|
|
package publisher
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
2026-03-03 11:36:24 +00:00
|
|
|
"io"
|
2026-03-21 23:28:33 +00:00
|
|
|
"io/fs"
|
2026-03-05 11:04:24 +00:00
|
|
|
"iter"
|
2026-02-19 11:29:44 +00:00
|
|
|
"log"
|
2026-02-20 06:39:58 +00:00
|
|
|
"os"
|
2026-03-09 10:47:02 +00:00
|
|
|
"time"
|
2026-02-19 11:29:44 +00:00
|
|
|
|
2026-02-20 06:39:58 +00:00
|
|
|
"emperror.dev/errors"
|
|
|
|
|
"github.com/go-openapi/runtime"
|
|
|
|
|
"github.com/go-openapi/strfmt"
|
|
|
|
|
"github.com/netlify/open-api/v2/go/porcelain"
|
|
|
|
|
netlify_ctx "github.com/netlify/open-api/v2/go/porcelain/context"
|
2026-02-19 11:29:44 +00:00
|
|
|
"lmika.dev/lmika/weiro/layouts/simplecss"
|
|
|
|
|
"lmika.dev/lmika/weiro/models"
|
|
|
|
|
"lmika.dev/lmika/weiro/models/pubmodel"
|
|
|
|
|
"lmika.dev/lmika/weiro/providers/db"
|
|
|
|
|
"lmika.dev/lmika/weiro/providers/sitebuilder"
|
2026-02-20 23:22:10 +00:00
|
|
|
"lmika.dev/lmika/weiro/providers/siteexporter"
|
2026-03-03 11:36:24 +00:00
|
|
|
"lmika.dev/lmika/weiro/providers/uploadfiles"
|
2026-02-19 11:29:44 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type Publisher struct {
|
|
|
|
|
db *db.Provider
|
2026-03-03 11:36:24 +00:00
|
|
|
up *uploadfiles.Provider
|
2026-02-19 11:29:44 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-03 11:36:24 +00:00
|
|
|
func New(db *db.Provider, up *uploadfiles.Provider) *Publisher {
|
2026-02-19 11:29:44 +00:00
|
|
|
return &Publisher{
|
|
|
|
|
db: db,
|
2026-03-03 11:36:24 +00:00
|
|
|
up: up,
|
2026-02-19 11:29:44 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-20 23:22:10 +00:00
|
|
|
func (p *Publisher) Publish(ctx context.Context, site models.Site) error {
|
|
|
|
|
targets, err := p.db.SelectPublishTargetsOfSite(ctx, site.ID)
|
2026-02-19 11:29:44 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-03 11:36:24 +00:00
|
|
|
// Fetch all uploads of site
|
|
|
|
|
uploads, err := p.db.SelectUploadsOfSite(ctx, site.ID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 10:51:19 +00:00
|
|
|
// Fetch categories with counts
|
|
|
|
|
cats, err := p.db.SelectCategoriesOfSite(ctx, site.ID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
var catsWithCounts []models.CategoryWithCount
|
|
|
|
|
for _, cat := range cats {
|
|
|
|
|
count, err := p.db.CountPostsOfCategory(ctx, cat.ID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
catsWithCounts = append(catsWithCounts, models.CategoryWithCount{
|
|
|
|
|
Category: *cat,
|
|
|
|
|
PostCount: int(count),
|
|
|
|
|
DescriptionBrief: models.BriefDescription(cat.Description),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-22 08:09:01 +00:00
|
|
|
// Fetch pages
|
|
|
|
|
sitePages, err := p.db.SelectPagesOfSite(ctx, site.ID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-19 11:29:44 +00:00
|
|
|
for _, target := range targets {
|
2026-02-21 23:09:34 +00:00
|
|
|
if !target.Enabled {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-19 11:29:44 +00:00
|
|
|
pubSite := pubmodel.Site{
|
2026-03-05 11:04:24 +00:00
|
|
|
Site: site,
|
|
|
|
|
PostIter: func(ctx context.Context) iter.Seq[models.Maybe[*models.Post]] {
|
|
|
|
|
return p.postIter(ctx, site.ID)
|
|
|
|
|
},
|
2026-03-18 10:51:19 +00:00
|
|
|
BaseURL: target.BaseURL,
|
|
|
|
|
Uploads: uploads,
|
|
|
|
|
Categories: catsWithCounts,
|
|
|
|
|
PostIterByCategory: func(ctx context.Context, categoryID int64) iter.Seq[models.Maybe[*models.Post]] {
|
|
|
|
|
return p.postIterByCategory(ctx, categoryID)
|
|
|
|
|
},
|
|
|
|
|
CategoriesOfPost: func(ctx context.Context, postID int64) ([]*models.Category, error) {
|
|
|
|
|
return p.db.SelectCategoriesOfPost(ctx, postID)
|
|
|
|
|
},
|
2026-03-22 08:09:01 +00:00
|
|
|
Pages: sitePages,
|
2026-03-03 11:36:24 +00:00
|
|
|
OpenUpload: func(u models.Upload) (io.ReadCloser, error) {
|
|
|
|
|
return p.up.OpenUpload(site, u)
|
|
|
|
|
},
|
2026-02-19 11:29:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := p.publishSite(ctx, pubSite, target); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *Publisher) publishSite(ctx context.Context, pubSite pubmodel.Site, target models.SitePublishTarget) error {
|
2026-03-09 10:47:02 +00:00
|
|
|
renderTZ, err := time.LoadLocation(pubSite.Timezone)
|
|
|
|
|
if err != nil {
|
|
|
|
|
renderTZ = time.UTC
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-21 23:28:33 +00:00
|
|
|
templateFS, err := fs.Sub(simplecss.FS, "templates")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
staticFS, err := fs.Sub(simplecss.FS, "static")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-19 11:29:44 +00:00
|
|
|
sb, err := sitebuilder.New(pubSite, sitebuilder.Options{
|
|
|
|
|
BasePosts: "/posts",
|
2026-03-21 23:28:33 +00:00
|
|
|
BaseUploads: "/uploads",
|
|
|
|
|
BaseStatic: "/static",
|
|
|
|
|
TemplatesFS: templateFS,
|
|
|
|
|
StaticFS: staticFS,
|
2026-03-05 11:04:24 +00:00
|
|
|
FeedItems: 30,
|
2026-03-09 10:47:02 +00:00
|
|
|
RenderTZ: renderTZ,
|
2026-02-19 11:29:44 +00:00
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch target.TargetType {
|
2026-02-20 23:22:10 +00:00
|
|
|
case "export":
|
|
|
|
|
exporter := siteexporter.New(pubSite.Site, target.BaseURL, target.TargetRef)
|
|
|
|
|
if err := exporter.WriteSiteYAML(); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2026-03-05 11:04:24 +00:00
|
|
|
for mp := range pubSite.PostIter(ctx) {
|
|
|
|
|
p, err := mp.Get()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2026-02-20 23:22:10 +00:00
|
|
|
if err := exporter.WritePost(p); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
2026-02-20 06:39:58 +00:00
|
|
|
case "localfs":
|
2026-02-19 11:29:44 +00:00
|
|
|
log.Printf("Building site at %s", target.TargetRef)
|
|
|
|
|
return sb.BuildSite(target.TargetRef)
|
2026-02-20 06:39:58 +00:00
|
|
|
case "netlify":
|
|
|
|
|
return func() error {
|
|
|
|
|
tmpDir, err := os.MkdirTemp("", "weiro-publish")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
|
|
|
|
|
|
if err := sb.BuildSite(tmpDir); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx = netlify_ctx.WithAuthInfo(ctx, runtime.ClientAuthInfoWriterFunc(func(r runtime.ClientRequest, _ strfmt.Registry) error {
|
|
|
|
|
return r.SetHeaderParam("Authorization", "Bearer "+target.TargetKey)
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
client := porcelain.Default
|
|
|
|
|
_, err = client.DeploySite(ctx, porcelain.DeployOptions{
|
|
|
|
|
SiteID: target.TargetRef,
|
|
|
|
|
Dir: tmpDir,
|
|
|
|
|
})
|
|
|
|
|
return err
|
|
|
|
|
}()
|
2026-02-19 11:29:44 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-20 06:39:58 +00:00
|
|
|
return errors.New("unknown target type")
|
2026-02-19 11:29:44 +00:00
|
|
|
}
|