mirror of
https://github.com/napnap75/multiarch-docker-images.git
synced 2026-11-12 00:52:11 +01:00
US-22: the PocketBase UI edits one field at a time with no preview of the final name, so a field edit now persists the values with no template evaluation, no re-filing and no status change; the corrected event carries the field diff alone. The full correction (all fields, dry-run, re-filing, validation) moves to the custom dashboard (US-3x). US-24: deleting a documents record deletes the S3 object at file_key first; the cascade delete removes the history. Spec, hooks, E2E updated together.
152 lines
5.0 KiB
Go
152 lines
5.0 KiB
Go
package main
|
|
|
|
import (
|
|
"log"
|
|
"os"
|
|
|
|
"ganigeorgiev.com/pocketbase/pocketbase"
|
|
"ganigeorgiev.com/pocketbase/pocketbase/core"
|
|
"github.com/napnap75/archivist/internal/config"
|
|
"github.com/napnap75/archivist/internal/pbapp"
|
|
"github.com/napnap75/archivist/internal/pipeline"
|
|
"github.com/napnap75/archivist/internal/storage"
|
|
"github.com/spf13/pflag"
|
|
)
|
|
|
|
// EnvConfig is the technical configuration, from environment variables (US-01).
|
|
type EnvConfig struct {
|
|
S3Endpoint string
|
|
S3AccessKey string
|
|
S3SecretKey string
|
|
S3Bucket string
|
|
S3Region string
|
|
S3UseSSL bool
|
|
AdminEmail string
|
|
AdminPassword string
|
|
PollCron string
|
|
HTTPAddr string
|
|
ConfigPath string
|
|
}
|
|
|
|
func env(key, def string) string {
|
|
if v := os.Getenv(key); v != "" {
|
|
return v
|
|
}
|
|
return def
|
|
}
|
|
|
|
// LoadEnvConfig reads and validates the mandatory environment variables (US-01).
|
|
func LoadEnvConfig() (*EnvConfig, error) {
|
|
c := &EnvConfig{
|
|
S3Endpoint: os.Getenv("ARCHIVIST_S3_ENDPOINT"),
|
|
S3AccessKey: os.Getenv("ARCHIVIST_S3_ACCESS_KEY"),
|
|
S3SecretKey: os.Getenv("ARCHIVIST_S3_SECRET_KEY"),
|
|
S3Bucket: os.Getenv("ARCHIVIST_S3_BUCKET"),
|
|
AdminEmail: os.Getenv("ARCHIVIST_ADMIN_EMAIL"),
|
|
AdminPassword: os.Getenv("ARCHIVIST_ADMIN_PASSWORD"),
|
|
}
|
|
for k, v := range map[string]string{
|
|
"ARCHIVIST_S3_ENDPOINT": c.S3Endpoint,
|
|
"ARCHIVIST_S3_ACCESS_KEY": c.S3AccessKey,
|
|
"ARCHIVIST_S3_SECRET_KEY": c.S3SecretKey,
|
|
"ARCHIVIST_S3_BUCKET": c.S3Bucket,
|
|
"ARCHIVIST_ADMIN_EMAIL": c.AdminEmail,
|
|
"ARCHIVIST_ADMIN_PASSWORD": c.AdminPassword,
|
|
} {
|
|
if v == "" {
|
|
log.Fatalf("missing mandatory environment variable %s", k)
|
|
}
|
|
}
|
|
c.S3Region = env("ARCHIVIST_S3_REGION", "garage")
|
|
c.S3UseSSL = env("ARCHIVIST_S3_USE_SSL", "true") != "false"
|
|
c.PollCron = env("ARCHIVIST_POLL_INTERVAL", "* * * * *")
|
|
c.HTTPAddr = env("ARCHIVIST_HTTP_ADDR", "0.0.0.0:8090")
|
|
c.ConfigPath = env("ARCHIVIST_CONFIG", "config.jsonc")
|
|
return c, nil
|
|
}
|
|
|
|
func configPathFromArgs(args []string, defaultPath string) (string, error) {
|
|
var configPath string
|
|
flags := pflag.NewFlagSet("archivist", pflag.ContinueOnError)
|
|
flags.SetInterspersed(true)
|
|
flags.ParseErrorsWhitelist.UnknownFlags = true
|
|
flags.StringVar(&configPath, "config", defaultPath, "Path to the functional JSONC configuration")
|
|
if err := flags.Parse(args); err != nil {
|
|
return "", err
|
|
}
|
|
return configPath, nil
|
|
}
|
|
|
|
func main() {
|
|
envCfg, err := LoadEnvConfig()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
envCfg.ConfigPath, err = configPathFromArgs(os.Args[1:], envCfg.ConfigPath)
|
|
if err != nil {
|
|
log.Fatalf("invalid command line: %v", err)
|
|
}
|
|
|
|
// US-02: load and validate the functional configuration. Full rejection,
|
|
// the process does not continue with a partial configuration (US-05).
|
|
cfg, err := config.Load(envCfg.ConfigPath)
|
|
if err != nil {
|
|
log.Fatalf("invalid functional configuration: %v", err)
|
|
}
|
|
log.Printf("configuration loaded: %d fields, %d top-level rules, %d templates",
|
|
len(cfg.Fields), len(cfg.Rules), len(cfg.Templates))
|
|
|
|
// US-01: verify S3 reachability and versioning before anything else.
|
|
s3, err := storage.New(envCfg.S3Endpoint, envCfg.S3AccessKey, envCfg.S3SecretKey, envCfg.S3Bucket, envCfg.S3Region, envCfg.S3UseSSL)
|
|
if err != nil {
|
|
log.Fatalf("S3 client: %v", err)
|
|
}
|
|
|
|
app := pocketbase.New()
|
|
app.RootCmd.PersistentFlags().StringVar(&envCfg.ConfigPath, "config", envCfg.ConfigPath, "Path to the functional JSONC configuration")
|
|
// The PocketBase logger and database are available only after bootstrap;
|
|
// everything logged before this point goes to stderr (arch.md section 9).
|
|
if err := app.Bootstrap(); err != nil {
|
|
log.Fatalf("pocketbase bootstrap: %v", err)
|
|
}
|
|
|
|
// US-03 / US-04 / US-01: collections migration and superuser account.
|
|
if err := pbapp.Setup(app, envCfg.AdminEmail, envCfg.AdminPassword, cfg); err != nil {
|
|
log.Fatalf("pocketbase setup: %v", err)
|
|
}
|
|
|
|
// US-01: versioning recommendation, via the PocketBase logger
|
|
// (visible in the admin dashboard, Dashboard > Logs).
|
|
if s3.StartupWarning != "" {
|
|
app.Logger().Warn(s3.StartupWarning, "bucket", envCfg.S3Bucket)
|
|
}
|
|
|
|
// US-04 / US-22 / US-23 / US-24: request hooks on documents and document_events.
|
|
pbapp.BindHooks(app, cfg, s3)
|
|
|
|
// Manual poll pass: a superuser-only route used by the E2E gate and the
|
|
// operator to trigger a pass without waiting for the cron (arch.md 10).
|
|
app.OnServe().BindFunc(func(e *core.ServeEvent) error {
|
|
e.Router.POST("/api/archivist/poll", func(re *core.RequestEvent) error {
|
|
if !re.HasSuperuserAuth() {
|
|
return re.ForbiddenError("superusers only", nil)
|
|
}
|
|
pipeline.RunPoll(re.App, cfg, s3)
|
|
return re.JSON(200, map[string]any{"status": "ok"})
|
|
})
|
|
return e.Next()
|
|
})
|
|
// US-10..US-17: the poller, on the configured cron.
|
|
if err := app.Cron().Add("archivist-poller", envCfg.PollCron, func() {
|
|
pipeline.RunPoll(app, cfg, s3)
|
|
}); err != nil {
|
|
log.Fatalf("register archivist poller: %v", err)
|
|
}
|
|
|
|
args := append([]string(nil), os.Args[1:]...)
|
|
app.RootCmd.SetArgs(append(args, "--http="+envCfg.HTTPAddr))
|
|
if err := app.Start(); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|