Have got site creation working

This commit is contained in:
Leon Mika 2025-01-27 09:26:15 +11:00
parent 4ecc12f035
commit f8e7ea482b
15 changed files with 281 additions and 11 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
.idea
build/

22
config/config.go Normal file
View file

@ -0,0 +1,22 @@
package config
import "path/filepath"
type Config struct {
DatabaseURL string `env:"DATABASE_URL"`
DataDir string `env:"DATA_DIR"`
DataStagingDir string `env:"DATA_STAGING_DIR,default=staging"`
}
func Load() (Config, error) {
return Config{
DatabaseURL: "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable",
DataDir: "build/data",
DataStagingDir: "staging",
}, nil
}
func (c Config) StagingDir() string {
return filepath.Join(c.DataDir, c.DataStagingDir)
}

28
main.go
View file

@ -2,18 +2,37 @@ package main
import (
"context"
"lmika.dev/lmika/hugo-crm/models"
"lmika.dev/lmika/hugo-crm/config"
"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/themes"
"lmika.dev/lmika/hugo-crm/services/sites"
"log"
"time"
)
func main() {
dbp, err := db.New("postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable")
cfg, err := config.Load()
if err != nil {
log.Fatal(err)
}
dbp, err := db.New(cfg.DatabaseURL)
if err != nil {
log.Fatal(err)
}
defer dbp.Close()
hugoProvider, err := hugo.New(cfg.StagingDir())
if err != nil {
log.Fatal(err)
}
gitProvider := git.New()
themesProvider := themes.New()
siteService := sites.NewService(cfg, dbp, themesProvider, gitProvider, hugoProvider)
log.Println("Connected to database")
if err := dbp.Migrate(context.Background()); err != nil {
log.Fatal(err)
@ -21,10 +40,7 @@ func main() {
log.Println("Database migrated")
if err := dbp.InsertSite(context.Background(), &models.Site{
Name: "Test site",
URL: "https://www.testsite.com",
}); err != nil {
if _, err := siteService.CreateSite(context.Background(), "Test site "+time.Now().Format("2006-01-02T15:04:05")); err != nil {
log.Fatal(err)
}
}

View file

@ -1,7 +1,9 @@
package models
type Site struct {
ID int64
Name string
URL string
ID int64
Name string
Title string
URL string
Theme string
}

6
models/theme.go Normal file
View file

@ -0,0 +1,6 @@
package models
type ThemeMeta struct {
Name string `json:"name"`
URL string `json:"repo"`
}

View file

@ -10,7 +10,7 @@ func (db *DB) InsertSite(ctx context.Context, site *models.Site) error {
id, err := db.q.NewSite(ctx, dbq.NewSiteParams{
Name: site.Name,
Url: site.URL,
Theme: "default",
Theme: site.Theme,
Props: []byte("{}"),
})
if err != nil {

17
providers/git/provider.go Normal file
View file

@ -0,0 +1,17 @@
package git
import (
"context"
"os/exec"
)
type Provider struct {
}
func New() *Provider {
return &Provider{}
}
func (p *Provider) Clone(ctx context.Context, url string, targetDir string) error {
return exec.CommandContext(ctx, "git", "clone", url, targetDir).Run()
}

8
providers/hugo/dirs.go Normal file
View file

@ -0,0 +1,8 @@
package hugo
type SiteDir string
const (
BaseSiteDir SiteDir = "base"
ThemeSiteDir SiteDir = "theme"
)

View file

@ -0,0 +1,70 @@
package hugo
import (
"bytes"
"context"
"lmika.dev/lmika/hugo-crm/models"
"lmika.dev/lmika/hugo-crm/providers/hugo/tmpls"
"log"
"os"
"os/exec"
"path/filepath"
"text/template"
)
type Provider struct {
stagingDir string
tmpls *template.Template
}
func New(stagingDir string) (*Provider, error) {
ts, err := template.ParseFS(tmpls.FS, "*.tmpl")
if err != nil {
return nil, err
}
return &Provider{
stagingDir: stagingDir,
tmpls: ts,
}, nil
}
func (p *Provider) SiteStagingDir(site models.Site, what SiteDir) string {
switch what {
case BaseSiteDir:
return filepath.Join(p.stagingDir, site.Name)
case ThemeSiteDir:
return filepath.Join(p.stagingDir, site.Name, "themes", site.Theme)
}
return ""
}
func (p *Provider) NewSite(ctx context.Context, site models.Site) error {
log.Printf(" .. %v", p.SiteStagingDir(site, BaseSiteDir))
if err := os.MkdirAll(p.SiteStagingDir(site, BaseSiteDir), 0755); err != nil {
return err
}
// Create the new site
if err := exec.CommandContext(ctx, "hugo", "new", "site", p.SiteStagingDir(site, BaseSiteDir)).Run(); err != nil {
return err
}
return nil
}
func (p *Provider) ReconfigureSite(ctx context.Context, site models.Site) error {
// Reconfigure the site
var hugoCfg bytes.Buffer
if err := p.tmpls.ExecuteTemplate(&hugoCfg, "config.toml.tmpl", struct {
Site models.Site
}{
Site: site,
}); err != nil {
return err
}
if err := os.WriteFile(filepath.Join(p.SiteStagingDir(site, BaseSiteDir), "hugo.toml"), hugoCfg.Bytes(), 0644); err != nil {
return err
}
return nil
}

View file

@ -0,0 +1,5 @@
baseURL = {{.Site.URL | printf "%q"}}
languageCode = 'en-us'
title = {{.Site.Title | printf "%q"}}
theme = {{.Site.Theme | printf "%q"}}

View file

@ -0,0 +1,6 @@
package tmpls
import "embed"
//go:embed *.tmpl
var FS embed.FS

10
providers/themes/meta.go Normal file
View file

@ -0,0 +1,10 @@
package themes
import "lmika.dev/lmika/hugo-crm/models"
var themes = map[string]models.ThemeMeta{
"bear": models.ThemeMeta{
Name: "bear",
URL: "https://github.com/janraasch/hugo-bearblog",
},
}

View file

@ -0,0 +1,14 @@
package themes
import "lmika.dev/lmika/hugo-crm/models"
type Provider struct{}
func New() *Provider {
return &Provider{}
}
func (p *Provider) Lookup(name string) (models.ThemeMeta, bool) {
t, ok := themes[name]
return t, ok
}

91
services/sites/service.go Normal file
View file

@ -0,0 +1,91 @@
package sites
import (
"context"
"errors"
"lmika.dev/lmika/hugo-crm/config"
"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/themes"
"log"
"strings"
"unicode"
)
type Service struct {
cfg config.Config
db *db.DB
themes *themes.Provider
git *git.Provider
hugo *hugo.Provider
}
func NewService(
cfg config.Config,
db *db.DB,
themes *themes.Provider,
git *git.Provider,
hugo *hugo.Provider,
) *Service {
return &Service{
cfg: cfg,
db: db,
git: git,
hugo: hugo,
}
}
func (s *Service) CreateSite(ctx context.Context, name string) (models.Site, error) {
newSite := models.Site{
Name: normaliseName(name),
Title: name,
Theme: "bear",
}
themeMeta, ok := s.themes.Lookup(newSite.Theme)
if !ok {
return models.Site{}, errors.New("theme not found")
}
if err := s.db.InsertSite(ctx, &newSite); err != nil {
return models.Site{}, err
}
// Build the site
log.Printf(" .. build")
if err := s.hugo.NewSite(ctx, newSite); err != nil {
return models.Site{}, err
}
// Setup the theme
log.Printf(" .. theme")
if err := s.git.Clone(ctx, themeMeta.URL, s.hugo.SiteStagingDir(newSite, hugo.ThemeSiteDir)); err != nil {
return models.Site{}, err
}
if err := s.hugo.ReconfigureSite(ctx, newSite); err != nil {
return models.Site{}, err
}
return newSite, nil
}
func normaliseName(name string) string {
var sb strings.Builder
seenDash := false
for _, r := range strings.TrimSpace(name) {
if unicode.IsLetter(r) || unicode.IsNumber(r) {
sb.WriteRune(unicode.ToLower(r))
seenDash = false
} else if r == '_' || r == '-' || r == '.' || unicode.IsSpace(r) {
if !seenDash {
sb.WriteRune('-')
}
seenDash = true
}
}
return sb.String()
}

View file

@ -4,8 +4,9 @@ SELECT * FROM site;
-- name: NewSite :one
INSERT INTO site (
name,
title,
url,
theme,
props
) VALUES ($1, $2, $3, $4)
) VALUES ($1, $2, $3, $4, $5)
RETURNING id;