- deferred:purge [--days] in lagoon.RuntimeCommands (purge_days, default 5)
- lagoon.FrameworkSchedule entry at purge_at (default 03:00, empty disables)
- conga prepends framework entries as summercms.lagoon[i]:<command>
- pact.Relation{Before,After}{Create,Update,Delete} optional hooks
- lagoon, conga and pact READMEs, scheduling and setup docs
241 lines
6.8 KiB
Go
241 lines
6.8 KiB
Go
package lagoon
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/backpack"
|
|
"git.golem15.com/golem15/summercms/modules/bonfire"
|
|
"git.golem15.com/golem15/summercms/modules/lagoon/attach"
|
|
"git.golem15.com/golem15/summercms/modules/pact"
|
|
"git.golem15.com/golem15/summercms/modules/party"
|
|
"gocloud.dev/blob"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// RuntimeCommands returns migrate, migrate:rollback, migrate:status,
|
|
// deferred:purge and key:generate. Serve is registered separately via
|
|
// surf.ServeCommand.
|
|
func RuntimeCommands(app *backpack.App, plugins []party.Plugin) []bonfire.Command {
|
|
return []bonfire.Command{
|
|
{
|
|
Name: "migrate",
|
|
Description: "Run plugin migrations in dependency order",
|
|
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
|
|
return withDB(ctx, app, func(gdb *gorm.DB) error {
|
|
if err := Migrate(gdb, plugins); err != nil {
|
|
return err
|
|
}
|
|
out.Success("migrations applied")
|
|
return nil
|
|
})
|
|
},
|
|
},
|
|
{
|
|
Name: "migrate:rollback",
|
|
Description: "Roll back the last migration of a plugin",
|
|
Flags: []bonfire.Flag{{
|
|
Name: "plugin",
|
|
Description: "Plugin ID whose last migration to roll back",
|
|
}},
|
|
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
|
|
plugin, _ := in.Flag("plugin")
|
|
return withDB(ctx, app, func(gdb *gorm.DB) error {
|
|
if err := RollbackLast(gdb, plugins, plugin); err != nil {
|
|
return err
|
|
}
|
|
if plugin == "" {
|
|
plugin = lastMigrationPlugin(plugins)
|
|
}
|
|
out.Success(fmt.Sprintf("rolled back last migration of %s", plugin))
|
|
return nil
|
|
})
|
|
},
|
|
},
|
|
{
|
|
Name: "migrate:status",
|
|
Description: "Show per-plugin migration history",
|
|
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
|
|
return withDB(ctx, app, func(gdb *gorm.DB) error {
|
|
rows, err := Status(gdb, plugins)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tableRows := make([][]string, 0, len(rows))
|
|
for _, row := range rows {
|
|
ids := "(none)"
|
|
if len(row.IDs) > 0 {
|
|
ids = strings.Join(row.IDs, ", ")
|
|
}
|
|
tableRows = append(tableRows, []string{row.Plugin, row.Table, ids})
|
|
}
|
|
out.Table([]string{"plugin", "table", "applied"}, tableRows)
|
|
return nil
|
|
})
|
|
},
|
|
},
|
|
{
|
|
Name: "deferred:purge",
|
|
Description: "Delete expired deferred bindings with their pending uploads and created records",
|
|
Flags: []bonfire.Flag{{
|
|
Name: "days",
|
|
Description: "Purge bindings older than this many days",
|
|
}},
|
|
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
|
|
days, err := purgeDays(app, in)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return withDB(ctx, app, func(gdb *gorm.DB) error {
|
|
models, err := deferredModels(gdb, plugins)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
bucket, closeBucket, err := purgeBucket(ctx, app)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer closeBucket()
|
|
res, err := PurgeDeferred(ctx, gdb, bucket, PurgeOptions{
|
|
Before: time.Now().Add(-time.Duration(days) * 24 * time.Hour),
|
|
Models: func(slaveType string) (any, bool) {
|
|
m, ok := models[slaveType]
|
|
return m, ok
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
out.Success(fmt.Sprintf("purged %d deferred bindings older than %d days (%d files, %d children, %d skipped)",
|
|
res.Bindings, days, res.Files, res.Children, res.Skipped))
|
|
return nil
|
|
})
|
|
},
|
|
},
|
|
KeyGenerateCommand(),
|
|
}
|
|
}
|
|
|
|
// purgeDays is the --days flag, else database.deferred_bindings.purge_days,
|
|
// else WinterCMS's five days. A negative or non-integer value is an error.
|
|
func purgeDays(app *backpack.App, in bonfire.Input) (int, error) {
|
|
if in != nil {
|
|
if raw, ok := in.Flag("days"); ok {
|
|
n, err := strconv.Atoi(strings.TrimSpace(raw))
|
|
if err != nil || n < 0 {
|
|
return 0, fmt.Errorf("lagoon: deferred:purge --days %q must be a whole number of days, 0 or more", raw)
|
|
}
|
|
return n, nil
|
|
}
|
|
}
|
|
if app == nil || app.Config == nil {
|
|
return defaultPurgeDays, nil
|
|
}
|
|
raw, ok := app.Config.Lookup(purgeDaysKey)
|
|
if !ok || raw == nil {
|
|
return defaultPurgeDays, nil
|
|
}
|
|
var n int
|
|
switch v := raw.(type) {
|
|
case int:
|
|
n = v
|
|
case int64:
|
|
n = int(v)
|
|
case uint64:
|
|
n = int(v)
|
|
case float64:
|
|
if v != float64(int(v)) {
|
|
return 0, fmt.Errorf("lagoon: %s %v must be a whole number of days", purgeDaysKey, raw)
|
|
}
|
|
n = int(v)
|
|
case string:
|
|
parsed, err := strconv.Atoi(strings.TrimSpace(v))
|
|
if err != nil {
|
|
return 0, fmt.Errorf("lagoon: %s %q must be a whole number of days", purgeDaysKey, v)
|
|
}
|
|
n = parsed
|
|
default:
|
|
return 0, fmt.Errorf("lagoon: %s %v must be a whole number of days", purgeDaysKey, raw)
|
|
}
|
|
if n < 0 {
|
|
return 0, fmt.Errorf("lagoon: %s %d must not be negative", purgeDaysKey, n)
|
|
}
|
|
return n, nil
|
|
}
|
|
|
|
// deferredModels maps the MorphType of every plugin model to the model, so
|
|
// the purge can delete records created under deferral. Two different model
|
|
// types with the same morph type are an error.
|
|
func deferredModels(gdb *gorm.DB, plugins []party.Plugin) (map[string]any, error) {
|
|
out := map[string]any{}
|
|
for _, p := range plugins {
|
|
hm, ok := p.(pact.HasModels)
|
|
if !ok {
|
|
continue
|
|
}
|
|
for _, m := range hm.Models() {
|
|
if m == nil {
|
|
continue
|
|
}
|
|
key, err := MorphType(gdb, m)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("lagoon: deferred:purge: plugin %s: %w", p.ID(), err)
|
|
}
|
|
if prev, dup := out[key]; dup {
|
|
if modelType(prev) != modelType(m) {
|
|
return nil, fmt.Errorf("lagoon: deferred:purge: models %s and %s share the morph type %q", modelType(prev), modelType(m), key)
|
|
}
|
|
continue
|
|
}
|
|
out[key] = m
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func modelType(m any) reflect.Type {
|
|
t := reflect.TypeOf(m)
|
|
for t != nil && t.Kind() == reflect.Pointer {
|
|
t = t.Elem()
|
|
}
|
|
return t
|
|
}
|
|
|
|
// purgeBucket is the published uploads bucket, else one opened from
|
|
// storage.uploads.bucket_url and closed by the returned func. Without a
|
|
// configured bucket it returns nil; PurgeDeferred then refuses only when an
|
|
// expired binding points at a file.
|
|
func purgeBucket(ctx context.Context, app *backpack.App) (*blob.Bucket, func(), error) {
|
|
noop := func() {}
|
|
if app == nil {
|
|
return nil, noop, nil
|
|
}
|
|
if b, ok := app.Lookup[*blob.Bucket](); ok && b != nil {
|
|
return b, noop, nil
|
|
}
|
|
if app.Config == nil || strings.TrimSpace(app.Config.String("storage.uploads.bucket_url")) == "" {
|
|
return nil, noop, nil
|
|
}
|
|
b, err := attach.OpenBucket(ctx, app.Config)
|
|
if err != nil {
|
|
return nil, noop, err
|
|
}
|
|
return b, func() { _ = b.Close() }, nil
|
|
}
|
|
|
|
func withDB(ctx context.Context, app *backpack.App, fn func(*gorm.DB) error) error {
|
|
sqlDB, gdb, err := OpenFromApp(ctx, app)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer sqlDB.Close()
|
|
if err := Publish(app, sqlDB, gdb); err != nil {
|
|
return err
|
|
}
|
|
return fn(gdb)
|
|
}
|