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
|
.idea
|
||||||
build/
|
build/
|
||||||
|
.env
|
||||||
|
# Local Netlify folder
|
||||||
|
.netlify
|
||||||
|
|
|
@ -1,22 +1,30 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import "path/filepath"
|
import (
|
||||||
|
"github.com/Netflix/go-env"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
DatabaseURL string `env:"DATABASE_URL"`
|
DatabaseURL string `env:"DATABASE_URL"`
|
||||||
|
NetlifyAuthToken string `env:"NETLIFY_AUTH_TOKEN"`
|
||||||
DataDir string `env:"DATA_DIR"`
|
DataDir string `env:"DATA_DIR"`
|
||||||
DataStagingDir string `env:"DATA_STAGING_DIR,default=staging"`
|
DataStagingDir string `env:"DATA_STAGING_DIR,default=staging"`
|
||||||
|
DataScratchDir string `env:"DATA_SCRATCH_DIR,default=scratch"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func Load() (Config, error) {
|
func Load() (cfg Config, err error) {
|
||||||
return Config{
|
_, err = env.UnmarshalFromEnviron(&cfg)
|
||||||
DatabaseURL: "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable",
|
if err != nil {
|
||||||
DataDir: "build/data",
|
return Config{}, err
|
||||||
DataStagingDir: "staging",
|
}
|
||||||
}, nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Config) StagingDir() string {
|
func (c Config) StagingDir() string {
|
||||||
return filepath.Join(c.DataDir, c.DataStagingDir)
|
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
|
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 {
|
type Post struct {
|
||||||
ID int64
|
ID int64
|
||||||
SiteID int64
|
SiteID int64
|
||||||
|
@ -64,6 +146,15 @@ type Post struct {
|
||||||
CreatedAt pgtype.Timestamp
|
CreatedAt pgtype.Timestamp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PublishTarget struct {
|
||||||
|
ID int64
|
||||||
|
SiteID int64
|
||||||
|
Role TargetRole
|
||||||
|
TargetType TargetType
|
||||||
|
Url string
|
||||||
|
TargetRef string
|
||||||
|
}
|
||||||
|
|
||||||
type Site struct {
|
type Site struct {
|
||||||
ID int64
|
ID int64
|
||||||
Name string
|
Name string
|
||||||
|
|
|
@ -81,3 +81,45 @@ func (q *Queries) ListPosts(ctx context.Context, siteID int64) ([]Post, error) {
|
||||||
}
|
}
|
||||||
return items, nil
|
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 (
|
require (
|
||||||
|
github.com/Netflix/go-env v0.1.2 // indirect
|
||||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||||
github.com/gofiber/fiber/v2 v2.52.6 // 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 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
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=
|
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 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 {
|
func (s *Site) WithSite() fiber.Handler {
|
||||||
return func(c *fiber.Ctx) error {
|
return func(c *fiber.Ctx) error {
|
||||||
id, err := c.ParamsInt("siteId")
|
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/db"
|
||||||
"lmika.dev/lmika/hugo-crm/providers/git"
|
"lmika.dev/lmika/hugo-crm/providers/git"
|
||||||
"lmika.dev/lmika/hugo-crm/providers/hugo"
|
"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/providers/themes"
|
||||||
"lmika.dev/lmika/hugo-crm/services/jobs"
|
"lmika.dev/lmika/hugo-crm/services/jobs"
|
||||||
"lmika.dev/lmika/hugo-crm/services/posts"
|
"lmika.dev/lmika/hugo-crm/services/posts"
|
||||||
|
@ -31,15 +32,16 @@ func main() {
|
||||||
}
|
}
|
||||||
defer dbp.Close()
|
defer dbp.Close()
|
||||||
|
|
||||||
hugoProvider, err := hugo.New(cfg.StagingDir())
|
hugoProvider, err := hugo.New(cfg.StagingDir(), cfg.ScratchDir())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
gitProvider := git.New()
|
gitProvider := git.New()
|
||||||
themesProvider := themes.New()
|
themesProvider := themes.New()
|
||||||
|
netlifyProvider := netlify.New(cfg.NetlifyAuthToken)
|
||||||
|
|
||||||
jobService := jobs.New()
|
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)
|
siteService := sites.NewService(cfg, dbp, themesProvider, siteBuilderService, jobService)
|
||||||
postService := posts.New(dbp, siteBuilderService, jobService)
|
postService := posts.New(dbp, siteBuilderService, jobService)
|
||||||
|
@ -69,6 +71,8 @@ func main() {
|
||||||
siteGroup := app.Group("/sites/:siteId")
|
siteGroup := app.Group("/sites/:siteId")
|
||||||
siteGroup.Use(siteHandlers.WithSite())
|
siteGroup.Use(siteHandlers.WithSite())
|
||||||
|
|
||||||
|
siteGroup.Post("/rebuild", siteHandlers.Rebuild())
|
||||||
|
|
||||||
siteGroup.Get("/posts", postHandlers.Posts())
|
siteGroup.Get("/posts", postHandlers.Posts())
|
||||||
siteGroup.Post("/posts", postHandlers.Create())
|
siteGroup.Post("/posts", postHandlers.Create())
|
||||||
|
|
||||||
|
|
|
@ -5,3 +5,14 @@ import "context"
|
||||||
type Job struct {
|
type Job struct {
|
||||||
Do func(ctx context.Context) error
|
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/gen/sqlc/dbq"
|
||||||
"lmika.dev/lmika/hugo-crm/models"
|
"lmika.dev/lmika/hugo-crm/models"
|
||||||
"lmika.dev/pkg/modash/moslice"
|
"lmika.dev/pkg/modash/moslice"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (db *DB) ListPostsOfSite(ctx context.Context, siteID int64) ([]models.Post, error) {
|
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 nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return moslice.Map(res, func(p dbq.Post) models.Post {
|
return moslice.Map(res, dbPostToPost), nil
|
||||||
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,
|
|
||||||
}
|
}
|
||||||
}), 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
|
||||||
|
}
|
||||||
|
|
||||||
|
return moslice.Map(res, dbPostToPost), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *DB) InsertPost(ctx context.Context, p *models.Post) error {
|
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
|
p.ID = res
|
||||||
return nil
|
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 {
|
type Provider struct {
|
||||||
stagingDir string
|
stagingDir string
|
||||||
|
scratchDir string
|
||||||
tmpls *template.Template
|
tmpls *template.Template
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(stagingDir string) (*Provider, error) {
|
func New(stagingDir, scratchDir string) (*Provider, error) {
|
||||||
ts, err := template.ParseFS(tmpls.FS, "*.tmpl")
|
ts, err := template.ParseFS(tmpls.FS, "*.tmpl")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -25,6 +26,7 @@ func New(stagingDir string) (*Provider, error) {
|
||||||
|
|
||||||
return &Provider{
|
return &Provider{
|
||||||
stagingDir: stagingDir,
|
stagingDir: stagingDir,
|
||||||
|
scratchDir: scratchDir,
|
||||||
tmpls: ts,
|
tmpls: ts,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
@ -52,6 +54,37 @@ func (p *Provider) NewSite(ctx context.Context, site models.Site) error {
|
||||||
return nil
|
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 {
|
func (p *Provider) ReconfigureSite(ctx context.Context, site models.Site) error {
|
||||||
// Reconfigure the site
|
// Reconfigure the site
|
||||||
var hugoCfg bytes.Buffer
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
"lmika.dev/lmika/hugo-crm/models"
|
"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/git"
|
||||||
"lmika.dev/lmika/hugo-crm/providers/hugo"
|
"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/providers/themes"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
|
db *db.DB
|
||||||
themes *themes.Provider
|
themes *themes.Provider
|
||||||
git *git.Provider
|
git *git.Provider
|
||||||
hugo *hugo.Provider
|
hugo *hugo.Provider
|
||||||
|
netlify *netlify.Provider
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(
|
func New(
|
||||||
|
db *db.DB,
|
||||||
themes *themes.Provider,
|
themes *themes.Provider,
|
||||||
git *git.Provider,
|
git *git.Provider,
|
||||||
hugo *hugo.Provider,
|
hugo *hugo.Provider,
|
||||||
|
netlify *netlify.Provider,
|
||||||
) *Service {
|
) *Service {
|
||||||
return &Service{
|
return &Service{
|
||||||
|
db: db,
|
||||||
themes: themes,
|
themes: themes,
|
||||||
git: git,
|
git: git,
|
||||||
hugo: hugo,
|
hugo: hugo,
|
||||||
|
netlify: netlify,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) CreateNewSite(site models.Site) models.Job {
|
func (s *Service) CreateNewSite(site models.Site) models.Job {
|
||||||
return models.Job{
|
return models.Job{
|
||||||
Do: func(ctx context.Context) error {
|
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)
|
themeMeta, ok := s.themes.Lookup(site.Theme)
|
||||||
if !ok {
|
if !ok {
|
||||||
return errors.New("theme not found")
|
return errors.New("theme not found")
|
||||||
|
@ -56,60 +85,4 @@ func (s *Service) CreateNewSite(site models.Site) models.Job {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
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
|
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))
|
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 {
|
func normaliseName(name string) string {
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
-- name: ListPosts :many
|
-- name: ListPosts :many
|
||||||
SELECT * FROM post WHERE site_id = $1 ORDER BY post_date DESC LIMIT 25;
|
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
|
-- name: InsertPost :one
|
||||||
INSERT INTO post (
|
INSERT INTO post (
|
||||||
site_id,
|
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'
|
'published'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TYPE target_role AS ENUM (
|
||||||
|
'production'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TYPE target_type AS ENUM (
|
||||||
|
'netlify'
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE site (
|
CREATE TABLE site (
|
||||||
id BIGSERIAL NOT NULL PRIMARY KEY,
|
id BIGSERIAL NOT NULL PRIMARY KEY,
|
||||||
name TEXT NOT NULL UNIQUE,
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
@ -24,3 +32,14 @@ CREATE TABLE post (
|
||||||
|
|
||||||
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">
|
<input type="submit" value="Post">
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<form method="post" action="/sites/{{.site.ID}}/rebuild">
|
||||||
|
<input type="submit" value="Rebuild">
|
||||||
|
</form>
|
||||||
|
|
||||||
{{range .posts}}
|
{{range .posts}}
|
||||||
<div class="post">
|
<div class="post">
|
||||||
{{.Body}}
|
{{.Body}}
|
||||||
|
|
Loading…
Reference in a new issue