feat(12.2-01): add deferred:purge, its daily framework schedule and relation child hooks
- 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
This commit is contained in:
@@ -3,16 +3,23 @@ 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, and key:generate.
|
||||
// Serve is registered separately via surf.ServeCommand.
|
||||
// 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{
|
||||
{
|
||||
@@ -71,10 +78,155 @@ func RuntimeCommands(app *backpack.App, plugins []party.Plugin) []bonfire.Comman
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user