Have got publishing to Netlify
This commit is contained in:
parent
8e0ffb6c24
commit
7ef6725bdb
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,2 +1,5 @@
|
|||
.idea
|
||||
build/
|
||||
.env
|
||||
# Local Netlify folder
|
||||
.netlify
|
||||
|
|
|
@ -1,22 +1,30 @@
|
|||
package config
|
||||
|
||||
import "path/filepath"
|
||||
import (
|
||||
"github.com/Netflix/go-env"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
DatabaseURL string `env:"DATABASE_URL"`
|
||||
|
||||
NetlifyAuthToken string `env:"NETLIFY_AUTH_TOKEN"`
|
||||
DataDir string `env:"DATA_DIR"`
|
||||
DataStagingDir string `env:"DATA_STAGING_DIR,default=staging"`
|
||||
DataScratchDir string `env:"DATA_SCRATCH_DIR,default=scratch"`
|
||||
}
|
||||
|
||||
func Load() (Config, error) {
|
||||
return Config{
|
||||
DatabaseURL: "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable",
|
||||
DataDir: "build/data",
|
||||
DataStagingDir: "staging",
|
||||
}, nil
|
||||
func Load() (cfg Config, err error) {
|
||||
_, err = env.UnmarshalFromEnviron(&cfg)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (c Config) StagingDir() string {
|
||||
return filepath.Join(c.DataDir, c.DataStagingDir)
|
||||
}
|
||||
|
||||
func (c Config) ScratchDir() string {
|
||||
return filepath.Join(c.DataDir, c.DataScratchDir)
|
||||
}
|
||||
|
|
|
@ -53,6 +53,88 @@ func (ns NullPostState) Value() (driver.Value, error) {
|
|||
return string(ns.PostState), nil
|
||||
}
|
||||
|
||||
type TargetRole string
|
||||
|
||||
const (
|
||||
TargetRoleProduction TargetRole = "production"
|
||||
)
|
||||
|
||||
func (e *TargetRole) Scan(src interface{}) error {
|
||||
switch s := src.(type) {
|
||||
case []byte:
|
||||
*e = TargetRole(s)
|
||||
case string:
|
||||
*e = TargetRole(s)
|
||||
default:
|
||||
return fmt.Errorf("unsupported scan type for TargetRole: %T", src)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type NullTargetRole struct {
|
||||
TargetRole TargetRole
|
||||
Valid bool // Valid is true if TargetRole is not NULL
|
||||
}
|
||||
|
||||
// Scan implements the Scanner interface.
|
||||
func (ns *NullTargetRole) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
ns.TargetRole, ns.Valid = "", false
|
||||
return nil
|
||||
}
|
||||
ns.Valid = true
|
||||
return ns.TargetRole.Scan(value)
|
||||
}
|
||||
|
||||
// Value implements the driver Valuer interface.
|
||||
func (ns NullTargetRole) Value() (driver.Value, error) {
|
||||
if !ns.Valid {
|
||||
return nil, nil
|
||||
}
|
||||
return string(ns.TargetRole), nil
|
||||
}
|
||||
|
||||
type TargetType string
|
||||
|
||||
const (
|
||||
TargetTypeNetlify TargetType = "netlify"
|
||||
)
|
||||
|
||||
func (e *TargetType) Scan(src interface{}) error {
|
||||
switch s := src.(type) {
|
||||
case []byte:
|
||||
*e = TargetType(s)
|
||||
case string:
|
||||
*e = TargetType(s)
|
||||
default:
|
||||
return fmt.Errorf("unsupported scan type for TargetType: %T", src)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type NullTargetType struct {
|
||||
TargetType TargetType
|
||||
Valid bool // Valid is true if TargetType is not NULL
|
||||
}
|
||||
|
||||
// Scan implements the Scanner interface.
|
||||
func (ns *NullTargetType) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
ns.TargetType, ns.Valid = "", false
|
||||
return nil
|
||||
}
|
||||
ns.Valid = true
|
||||
return ns.TargetType.Scan(value)
|
||||
}
|
||||
|
||||
// Value implements the driver Valuer interface.
|
||||
func (ns NullTargetType) Value() (driver.Value, error) {
|
||||
if !ns.Valid {
|
||||
return nil, nil
|
||||
}
|
||||
return string(ns.TargetType), nil
|
||||
}
|
||||
|
||||
type Post struct {
|
||||
ID int64
|
||||
SiteID int64
|
||||
|
@ -64,6 +146,15 @@ type Post struct {
|
|||
CreatedAt pgtype.Timestamp
|
||||
}
|
||||
|
||||
type PublishTarget struct {
|
||||
ID int64
|
||||
SiteID int64
|
||||
Role TargetRole
|
||||
TargetType TargetType
|
||||
Url string
|
||||
TargetRef string
|
||||
}
|
||||
|
||||
type Site struct {
|
||||
ID int64
|
||||
Name string
|
||||
|
|
|
@ -81,3 +81,45 @@ func (q *Queries) ListPosts(ctx context.Context, siteID int64) ([]Post, error) {
|
|||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listPublishablePosts = `-- name: ListPublishablePosts :many
|
||||
SELECT id, site_id, title, body, state, props, post_date, created_at
|
||||
FROM post
|
||||
WHERE id > $1 AND site_id = $2 AND state = 'published' AND post_date <= $3
|
||||
ORDER BY id LIMIT 100
|
||||
`
|
||||
|
||||
type ListPublishablePostsParams struct {
|
||||
ID int64
|
||||
SiteID int64
|
||||
PostDate pgtype.Timestamptz
|
||||
}
|
||||
|
||||
func (q *Queries) ListPublishablePosts(ctx context.Context, arg ListPublishablePostsParams) ([]Post, error) {
|
||||
rows, err := q.db.Query(ctx, listPublishablePosts, arg.ID, arg.SiteID, arg.PostDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Post
|
||||
for rows.Next() {
|
||||
var i Post
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.SiteID,
|
||||
&i.Title,
|
||||
&i.Body,
|
||||
&i.State,
|
||||
&i.Props,
|
||||
&i.PostDate,
|
||||
&i.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
|
73
gen/sqlc/dbq/targets.sql.go
Normal file
73
gen/sqlc/dbq/targets.sql.go
Normal file
|
@ -0,0 +1,73 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.28.0
|
||||
// source: targets.sql
|
||||
|
||||
package dbq
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
const insertPublishTarget = `-- name: InsertPublishTarget :one
|
||||
INSERT INTO publish_target (
|
||||
site_id,
|
||||
role,
|
||||
target_type,
|
||||
url,
|
||||
target_ref
|
||||
) VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id
|
||||
`
|
||||
|
||||
type InsertPublishTargetParams struct {
|
||||
SiteID int64
|
||||
Role TargetRole
|
||||
TargetType TargetType
|
||||
Url string
|
||||
TargetRef string
|
||||
}
|
||||
|
||||
func (q *Queries) InsertPublishTarget(ctx context.Context, arg InsertPublishTargetParams) (int64, error) {
|
||||
row := q.db.QueryRow(ctx, insertPublishTarget,
|
||||
arg.SiteID,
|
||||
arg.Role,
|
||||
arg.TargetType,
|
||||
arg.Url,
|
||||
arg.TargetRef,
|
||||
)
|
||||
var id int64
|
||||
err := row.Scan(&id)
|
||||
return id, err
|
||||
}
|
||||
|
||||
const listPublishTargetsOfRole = `-- name: ListPublishTargetsOfRole :many
|
||||
SELECT id, site_id, role, target_type, url, target_ref FROM publish_target WHERE site_id = $1 AND role = 'production'
|
||||
`
|
||||
|
||||
func (q *Queries) ListPublishTargetsOfRole(ctx context.Context, siteID int64) ([]PublishTarget, error) {
|
||||
rows, err := q.db.Query(ctx, listPublishTargetsOfRole, siteID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []PublishTarget
|
||||
for rows.Next() {
|
||||
var i PublishTarget
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.SiteID,
|
||||
&i.Role,
|
||||
&i.TargetType,
|
||||
&i.Url,
|
||||
&i.TargetRef,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
1
go.mod
1
go.mod
|
@ -8,6 +8,7 @@ require (
|
|||
)
|
||||
|
||||
require (
|
||||
github.com/Netflix/go-env v0.1.2 // indirect
|
||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||
github.com/gofiber/fiber/v2 v2.52.6 // indirect
|
||||
|
|
2
go.sum
2
go.sum
|
@ -1,3 +1,5 @@
|
|||
github.com/Netflix/go-env v0.1.2 h1:0DRoLR9lECQ9Zqvkswuebm3jJ/2enaDX6Ei8/Z+EnK0=
|
||||
github.com/Netflix/go-env v0.1.2/go.mod h1:WlIhYi++8FlKNJtrop1mjXYAJMzv1f43K4MqCoh0yGE=
|
||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
|
|
|
@ -19,7 +19,7 @@ func (s *Site) Create() fiber.Handler {
|
|||
return err
|
||||
}
|
||||
|
||||
return c.Redirect(fmt.Sprintf("/sites/%v", site.ID))
|
||||
return c.Redirect(fmt.Sprintf("/sites/%v/posts", site.ID))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -41,6 +41,16 @@ func (s *Site) Show() fiber.Handler {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *Site) Rebuild() fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
if err := s.Site.Rebuild(c.UserContext(), GetSite(c)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Redirect(fmt.Sprintf("/sites/%v/posts", GetSite(c).ID))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Site) WithSite() fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
id, err := c.ParamsInt("siteId")
|
||||
|
|
8
main.go
8
main.go
|
@ -9,6 +9,7 @@ import (
|
|||
"lmika.dev/lmika/hugo-crm/providers/db"
|
||||
"lmika.dev/lmika/hugo-crm/providers/git"
|
||||
"lmika.dev/lmika/hugo-crm/providers/hugo"
|
||||
"lmika.dev/lmika/hugo-crm/providers/netlify"
|
||||
"lmika.dev/lmika/hugo-crm/providers/themes"
|
||||
"lmika.dev/lmika/hugo-crm/services/jobs"
|
||||
"lmika.dev/lmika/hugo-crm/services/posts"
|
||||
|
@ -31,15 +32,16 @@ func main() {
|
|||
}
|
||||
defer dbp.Close()
|
||||
|
||||
hugoProvider, err := hugo.New(cfg.StagingDir())
|
||||
hugoProvider, err := hugo.New(cfg.StagingDir(), cfg.ScratchDir())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
gitProvider := git.New()
|
||||
themesProvider := themes.New()
|
||||
netlifyProvider := netlify.New(cfg.NetlifyAuthToken)
|
||||
|
||||
jobService := jobs.New()
|
||||
siteBuilderService := sitebuilder.New(themesProvider, gitProvider, hugoProvider)
|
||||
siteBuilderService := sitebuilder.New(dbp, themesProvider, gitProvider, hugoProvider, netlifyProvider)
|
||||
|
||||
siteService := sites.NewService(cfg, dbp, themesProvider, siteBuilderService, jobService)
|
||||
postService := posts.New(dbp, siteBuilderService, jobService)
|
||||
|
@ -69,6 +71,8 @@ func main() {
|
|||
siteGroup := app.Group("/sites/:siteId")
|
||||
siteGroup.Use(siteHandlers.WithSite())
|
||||
|
||||
siteGroup.Post("/rebuild", siteHandlers.Rebuild())
|
||||
|
||||
siteGroup.Get("/posts", postHandlers.Posts())
|
||||
siteGroup.Post("/posts", postHandlers.Create())
|
||||
|
||||
|
|
|
@ -5,3 +5,14 @@ import "context"
|
|||
type Job struct {
|
||||
Do func(ctx context.Context) error
|
||||
}
|
||||
|
||||
func Jobs(jobs ...Job) Job {
|
||||
return Job{Do: func(ctx context.Context) error {
|
||||
for _, job := range jobs {
|
||||
if err := job.Do(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}}
|
||||
}
|
||||
|
|
22
models/publish.go
Normal file
22
models/publish.go
Normal file
|
@ -0,0 +1,22 @@
|
|||
package models
|
||||
|
||||
type TargetRole string
|
||||
|
||||
const (
|
||||
TargetRoleProduction TargetRole = "production"
|
||||
)
|
||||
|
||||
type TargetType string
|
||||
|
||||
const (
|
||||
TargetTypeNetlify TargetType = "netlify"
|
||||
)
|
||||
|
||||
type PublishTarget struct {
|
||||
ID int64
|
||||
SiteID int64
|
||||
Role TargetRole
|
||||
Type TargetType
|
||||
URL string
|
||||
TargetRef string
|
||||
}
|
|
@ -6,6 +6,7 @@ import (
|
|||
"lmika.dev/lmika/hugo-crm/gen/sqlc/dbq"
|
||||
"lmika.dev/lmika/hugo-crm/models"
|
||||
"lmika.dev/pkg/modash/moslice"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (db *DB) ListPostsOfSite(ctx context.Context, siteID int64) ([]models.Post, error) {
|
||||
|
@ -14,17 +15,20 @@ func (db *DB) ListPostsOfSite(ctx context.Context, siteID int64) ([]models.Post,
|
|||
return nil, err
|
||||
}
|
||||
|
||||
return moslice.Map(res, func(p dbq.Post) models.Post {
|
||||
return models.Post{
|
||||
ID: p.ID,
|
||||
SiteID: p.SiteID,
|
||||
Title: p.Title.String,
|
||||
Body: p.Body,
|
||||
State: models.PostState(p.State),
|
||||
PostDate: p.PostDate.Time,
|
||||
CreatedAt: p.CreatedAt.Time,
|
||||
return moslice.Map(res, dbPostToPost), nil
|
||||
}
|
||||
|
||||
func (db *DB) ListPublishablePosts(ctx context.Context, fromID, siteID int64, now time.Time) ([]models.Post, error) {
|
||||
res, err := db.q.ListPublishablePosts(ctx, dbq.ListPublishablePostsParams{
|
||||
ID: fromID,
|
||||
SiteID: siteID,
|
||||
PostDate: pgtype.Timestamptz{Time: now, Valid: true},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}), nil
|
||||
|
||||
return moslice.Map(res, dbPostToPost), nil
|
||||
}
|
||||
|
||||
func (db *DB) InsertPost(ctx context.Context, p *models.Post) error {
|
||||
|
@ -44,3 +48,15 @@ func (db *DB) InsertPost(ctx context.Context, p *models.Post) error {
|
|||
p.ID = res
|
||||
return nil
|
||||
}
|
||||
|
||||
func dbPostToPost(p dbq.Post) models.Post {
|
||||
return models.Post{
|
||||
ID: p.ID,
|
||||
SiteID: p.SiteID,
|
||||
Title: p.Title.String,
|
||||
Body: p.Body,
|
||||
State: models.PostState(p.State),
|
||||
PostDate: p.PostDate.Time,
|
||||
CreatedAt: p.CreatedAt.Time,
|
||||
}
|
||||
}
|
||||
|
|
42
providers/db/publish.go
Normal file
42
providers/db/publish.go
Normal file
|
@ -0,0 +1,42 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"lmika.dev/lmika/hugo-crm/gen/sqlc/dbq"
|
||||
"lmika.dev/lmika/hugo-crm/models"
|
||||
"lmika.dev/pkg/modash/moslice"
|
||||
)
|
||||
|
||||
func (db *DB) InsertPublishTarget(ctx context.Context, target *models.PublishTarget) error {
|
||||
id, err := db.q.InsertPublishTarget(ctx, dbq.InsertPublishTargetParams{
|
||||
SiteID: target.SiteID,
|
||||
Role: dbq.TargetRole(target.Role),
|
||||
TargetType: dbq.TargetType(target.Type),
|
||||
Url: target.URL,
|
||||
TargetRef: target.TargetRef,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
target.ID = id
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *DB) GetPublishTargets(ctx context.Context, siteID int64) ([]models.PublishTarget, error) {
|
||||
res, err := db.q.ListPublishTargetsOfRole(ctx, siteID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return moslice.Map(res, func(m dbq.PublishTarget) models.PublishTarget {
|
||||
return models.PublishTarget{
|
||||
ID: m.ID,
|
||||
SiteID: m.SiteID,
|
||||
Role: models.TargetRole(m.Role),
|
||||
Type: models.TargetType(m.TargetType),
|
||||
URL: m.Url,
|
||||
TargetRef: m.TargetRef,
|
||||
}
|
||||
}), nil
|
||||
}
|
|
@ -14,10 +14,11 @@ import (
|
|||
|
||||
type Provider struct {
|
||||
stagingDir string
|
||||
scratchDir string
|
||||
tmpls *template.Template
|
||||
}
|
||||
|
||||
func New(stagingDir string) (*Provider, error) {
|
||||
func New(stagingDir, scratchDir string) (*Provider, error) {
|
||||
ts, err := template.ParseFS(tmpls.FS, "*.tmpl")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -25,6 +26,7 @@ func New(stagingDir string) (*Provider, error) {
|
|||
|
||||
return &Provider{
|
||||
stagingDir: stagingDir,
|
||||
scratchDir: scratchDir,
|
||||
tmpls: ts,
|
||||
}, nil
|
||||
}
|
||||
|
@ -52,6 +54,37 @@ func (p *Provider) NewSite(ctx context.Context, site models.Site) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (p *Provider) PublishSite(ctx context.Context, site models.Site, target models.PublishTarget) (outDir string, clean func(), err error) {
|
||||
if err := os.MkdirAll(p.scratchDir, 0755); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
outDir, err = os.MkdirTemp(p.scratchDir, site.Name+"-*")
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
clean = func() {
|
||||
os.RemoveAll(outDir)
|
||||
}
|
||||
|
||||
outDir, err = filepath.Abs(outDir)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, "hugo",
|
||||
"--source", p.SiteStagingDir(site, BaseSiteDir),
|
||||
"--destination", outDir,
|
||||
"--baseURL", target.URL)
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdout = os.Stdout
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return "", clean, err
|
||||
}
|
||||
return outDir, clean, nil
|
||||
}
|
||||
|
||||
func (p *Provider) ReconfigureSite(ctx context.Context, site models.Site) error {
|
||||
// Reconfigure the site
|
||||
var hugoCfg bytes.Buffer
|
||||
|
|
29
providers/netlify/provider.go
Normal file
29
providers/netlify/provider.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
package netlify
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"lmika.dev/lmika/hugo-crm/models"
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
type Provider struct {
|
||||
authToken string
|
||||
}
|
||||
|
||||
func New(authToken string) *Provider {
|
||||
return &Provider{
|
||||
authToken: authToken,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provider) Publish(ctx context.Context, target models.PublishTarget, dir string) error {
|
||||
cmd := exec.CommandContext(ctx, "netlify", "deploy", "--dir", dir, "--prod")
|
||||
cmd.Env = append(os.Environ(),
|
||||
fmt.Sprintf("NETLIFY_SITE_ID=%v", target.TargetRef),
|
||||
fmt.Sprintf("NETLIFY_AUTH_TOKEN=%v", p.authToken))
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
98
services/sitebuilder/posts.go
Normal file
98
services/sitebuilder/posts.go
Normal file
|
@ -0,0 +1,98 @@
|
|||
package sitebuilder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"gopkg.in/yaml.v3"
|
||||
"lmika.dev/lmika/hugo-crm/models"
|
||||
"lmika.dev/lmika/hugo-crm/providers/hugo"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (s *Service) WritePost(site models.Site, post models.Post) models.Job {
|
||||
return models.Jobs(
|
||||
models.Job{
|
||||
Do: func(ctx context.Context) error {
|
||||
return s.writePost(site, post)
|
||||
},
|
||||
},
|
||||
s.Publish(site),
|
||||
)
|
||||
}
|
||||
|
||||
func (s *Service) WriteAllPosts(site models.Site) models.Job {
|
||||
return models.Job{
|
||||
Do: func(ctx context.Context) error {
|
||||
var startId int64
|
||||
now := time.Now()
|
||||
for {
|
||||
posts, err := s.db.ListPublishablePosts(ctx, int64(startId), site.ID, now)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if len(posts) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, post := range posts {
|
||||
if err := s.writePost(site, post); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
startId = posts[len(posts)-1].ID
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) writePost(site models.Site, post models.Post) error {
|
||||
themeMeta, ok := s.themes.Lookup(site.Theme)
|
||||
if !ok {
|
||||
return errors.New("theme not found")
|
||||
}
|
||||
|
||||
postFilename := filepath.Join(s.hugo.SiteStagingDir(site, hugo.ContentSiteDir), themeMeta.PostDir, post.CreatedAt.Format("2006-01-02-150405.md"))
|
||||
|
||||
log.Printf(" .. post %v", postFilename)
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(postFilename), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
frontMatter := map[string]string{
|
||||
"date": post.PostDate.Format(time.RFC3339),
|
||||
}
|
||||
if post.Title != "" {
|
||||
frontMatter["title"] = post.Title
|
||||
} else if themeMeta.PreferTitle {
|
||||
frontMatter["title"] = post.PostDate.Format(time.ANSIC)
|
||||
}
|
||||
|
||||
fmBytes, err := yaml.Marshal(frontMatter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f, err := os.Create(postFilename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if _, err := f.WriteString("---\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := f.Write(fmBytes); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := f.WriteString("---\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := f.WriteString(post.Body); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
40
services/sitebuilder/publish.go
Normal file
40
services/sitebuilder/publish.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
package sitebuilder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"lmika.dev/lmika/hugo-crm/models"
|
||||
)
|
||||
|
||||
func (s *Service) Publish(site models.Site) models.Job {
|
||||
return models.Job{
|
||||
Do: func(ctx context.Context) error {
|
||||
targets, err := s.db.GetPublishTargets(ctx, site.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, target := range targets {
|
||||
if err := s.publishTarget(ctx, site, target); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) publishTarget(ctx context.Context, site models.Site, target models.PublishTarget) error {
|
||||
outDir, cleanFn, err := s.hugo.PublishSite(ctx, site, target)
|
||||
//defer func() {
|
||||
// if cleanFn != nil {
|
||||
// cleanFn()
|
||||
// }
|
||||
//}()
|
||||
_ = cleanFn
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.netlify.Publish(ctx, target, outDir)
|
||||
}
|
|
@ -3,38 +3,67 @@ package sitebuilder
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"gopkg.in/yaml.v3"
|
||||
"lmika.dev/lmika/hugo-crm/models"
|
||||
"lmika.dev/lmika/hugo-crm/providers/db"
|
||||
"lmika.dev/lmika/hugo-crm/providers/git"
|
||||
"lmika.dev/lmika/hugo-crm/providers/hugo"
|
||||
"lmika.dev/lmika/hugo-crm/providers/netlify"
|
||||
"lmika.dev/lmika/hugo-crm/providers/themes"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
db *db.DB
|
||||
themes *themes.Provider
|
||||
git *git.Provider
|
||||
hugo *hugo.Provider
|
||||
netlify *netlify.Provider
|
||||
}
|
||||
|
||||
func New(
|
||||
db *db.DB,
|
||||
themes *themes.Provider,
|
||||
git *git.Provider,
|
||||
hugo *hugo.Provider,
|
||||
netlify *netlify.Provider,
|
||||
) *Service {
|
||||
return &Service{
|
||||
db: db,
|
||||
themes: themes,
|
||||
git: git,
|
||||
hugo: hugo,
|
||||
netlify: netlify,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) CreateNewSite(site models.Site) models.Job {
|
||||
return models.Job{
|
||||
Do: func(ctx context.Context) error {
|
||||
return s.createSite(ctx, site)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) RebuildSite(site models.Site) models.Job {
|
||||
return models.Jobs(
|
||||
models.Job{
|
||||
Do: func(ctx context.Context) error {
|
||||
// Teardown the existing site
|
||||
siteDir := s.hugo.SiteStagingDir(site, hugo.BaseSiteDir)
|
||||
if err := os.RemoveAll(siteDir); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
s.CreateNewSite(site),
|
||||
s.WriteAllPosts(site),
|
||||
s.Publish(site),
|
||||
)
|
||||
}
|
||||
|
||||
func (s *Service) createSite(ctx context.Context, site models.Site) error {
|
||||
themeMeta, ok := s.themes.Lookup(site.Theme)
|
||||
if !ok {
|
||||
return errors.New("theme not found")
|
||||
|
@ -56,60 +85,4 @@ func (s *Service) CreateNewSite(site models.Site) models.Job {
|
|||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) WritePost(site models.Site, post models.Post) models.Job {
|
||||
return models.Job{
|
||||
Do: func(ctx context.Context) error {
|
||||
themeMeta, ok := s.themes.Lookup(site.Theme)
|
||||
if !ok {
|
||||
return errors.New("theme not found")
|
||||
}
|
||||
|
||||
postFilename := filepath.Join(s.hugo.SiteStagingDir(site, hugo.ContentSiteDir), themeMeta.PostDir, post.CreatedAt.Format("2006-01-02-150405.md"))
|
||||
|
||||
log.Printf(" .. post %v", postFilename)
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(postFilename), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
frontMatter := map[string]string{
|
||||
"date": post.PostDate.Format(time.RFC3339),
|
||||
}
|
||||
if post.Title != "" {
|
||||
frontMatter["title"] = post.Title
|
||||
} else if themeMeta.PreferTitle {
|
||||
frontMatter["title"] = post.PostDate.Format(time.ANSIC)
|
||||
}
|
||||
|
||||
fmBytes, err := yaml.Marshal(frontMatter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f, err := os.Create(postFilename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if _, err := f.WriteString("---\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := f.Write(fmBytes); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := f.WriteString("---\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := f.WriteString(post.Body); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,9 +57,28 @@ func (s *Service) CreateSite(ctx context.Context, name string) (models.Site, err
|
|||
return models.Site{}, err
|
||||
}
|
||||
|
||||
// TEMP
|
||||
if err := s.db.InsertPublishTarget(ctx, &models.PublishTarget{
|
||||
SiteID: newSite.ID,
|
||||
Role: models.TargetRoleProduction,
|
||||
Type: models.TargetTypeNetlify,
|
||||
URL: "https://meek-meringue-060cfc.netlify.app",
|
||||
TargetRef: "e628dc6e-e6e1-45a9-847a-982adef940a8",
|
||||
}); err != nil {
|
||||
return models.Site{}, err
|
||||
}
|
||||
|
||||
return newSite, s.jobs.Queue(ctx, s.sb.CreateNewSite(newSite))
|
||||
}
|
||||
|
||||
func (s *Service) Rebuild(ctx context.Context, site models.Site) error {
|
||||
if site.ID == 0 {
|
||||
return errors.New("site id required")
|
||||
}
|
||||
|
||||
return s.jobs.Queue(ctx, s.sb.RebuildSite(site))
|
||||
}
|
||||
|
||||
func normaliseName(name string) string {
|
||||
var sb strings.Builder
|
||||
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
-- name: ListPosts :many
|
||||
SELECT * FROM post WHERE site_id = $1 ORDER BY post_date DESC LIMIT 25;
|
||||
|
||||
-- name: ListPublishablePosts :many
|
||||
SELECT *
|
||||
FROM post
|
||||
WHERE id > $1 AND site_id = $2 AND state = 'published' AND post_date <= $3
|
||||
ORDER BY id LIMIT 100;
|
||||
|
||||
-- name: InsertPost :one
|
||||
INSERT INTO post (
|
||||
site_id,
|
||||
|
|
12
sql/queries/targets.sql
Normal file
12
sql/queries/targets.sql
Normal file
|
@ -0,0 +1,12 @@
|
|||
-- name: ListPublishTargetsOfRole :many
|
||||
SELECT * FROM publish_target WHERE site_id = $1 AND role = 'production';
|
||||
|
||||
-- name: InsertPublishTarget :one
|
||||
INSERT INTO publish_target (
|
||||
site_id,
|
||||
role,
|
||||
target_type,
|
||||
url,
|
||||
target_ref
|
||||
) VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id;
|
|
@ -3,6 +3,14 @@ CREATE TYPE post_state AS ENUM (
|
|||
'published'
|
||||
);
|
||||
|
||||
CREATE TYPE target_role AS ENUM (
|
||||
'production'
|
||||
);
|
||||
|
||||
CREATE TYPE target_type AS ENUM (
|
||||
'netlify'
|
||||
);
|
||||
|
||||
CREATE TABLE site (
|
||||
id BIGSERIAL NOT NULL PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
|
@ -22,5 +30,16 @@ CREATE TABLE post (
|
|||
post_date TIMESTAMP WITH TIME ZONE,
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
|
||||
FOREIGN KEY (site_id) REFERENCES site(id)
|
||||
FOREIGN KEY (site_id) REFERENCES site (id)
|
||||
);
|
||||
|
||||
CREATE TABLE publish_target (
|
||||
id BIGSERIAL NOT NULL PRIMARY KEY,
|
||||
site_id BIGINT NOT NULL,
|
||||
role target_role NOT NULL,
|
||||
target_type target_type NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
target_ref TEXT NOT NULL,
|
||||
|
||||
FOREIGN KEY (site_id) REFERENCES site (id)
|
||||
);
|
|
@ -5,6 +5,10 @@
|
|||
<input type="submit" value="Post">
|
||||
</form>
|
||||
|
||||
<form method="post" action="/sites/{{.site.ID}}/rebuild">
|
||||
<input type="submit" value="Rebuild">
|
||||
</form>
|
||||
|
||||
{{range .posts}}
|
||||
<div class="post">
|
||||
{{.Body}}
|
||||
|
|
Loading…
Reference in a new issue