Introduces the Go photo-api service, nginx/systemd deploy wiring, and Next.js gallery/lightbox pages so event photos can be managed and browsed. Co-authored-by: Cursor <cursoragent@cursor.com>
116 lines
2.9 KiB
Go
116 lines
2.9 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.azzamo.net/Michilis/Spanglish/photo-api/migrations"
|
|
)
|
|
|
|
// advisoryLockKey is an arbitrary fixed key for pg_advisory_lock so two
|
|
// instances can't run migrations concurrently. SQLite relies on its file lock.
|
|
const advisoryLockKey = 792346801
|
|
|
|
// Migrate applies embedded migrations for the active dialect in version
|
|
// order, recording applied versions in photos_schema_migrations.
|
|
func (db *DB) Migrate(ctx context.Context) error {
|
|
conn, err := db.Conn(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer conn.Close()
|
|
|
|
if db.Type == Postgres {
|
|
if _, err := conn.ExecContext(ctx, "SELECT pg_advisory_lock($1)", advisoryLockKey); err != nil {
|
|
return fmt.Errorf("advisory lock: %w", err)
|
|
}
|
|
defer conn.ExecContext(ctx, "SELECT pg_advisory_unlock($1)", advisoryLockKey)
|
|
}
|
|
|
|
createVersions := "CREATE TABLE IF NOT EXISTS photos_schema_migrations (version integer PRIMARY KEY, applied_at text NOT NULL)"
|
|
if db.Type == Postgres {
|
|
createVersions = "CREATE TABLE IF NOT EXISTS photos_schema_migrations (version integer PRIMARY KEY, applied_at timestamptz NOT NULL)"
|
|
}
|
|
if _, err := conn.ExecContext(ctx, createVersions); err != nil {
|
|
return fmt.Errorf("create photos_schema_migrations: %w", err)
|
|
}
|
|
|
|
applied := map[int]bool{}
|
|
rows, err := conn.QueryContext(ctx, "SELECT version FROM photos_schema_migrations")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for rows.Next() {
|
|
var v int
|
|
if err := rows.Scan(&v); err != nil {
|
|
rows.Close()
|
|
return err
|
|
}
|
|
applied[v] = true
|
|
}
|
|
rows.Close()
|
|
if err := rows.Err(); err != nil {
|
|
return err
|
|
}
|
|
|
|
suffix := ".sqlite.sql"
|
|
if db.Type == Postgres {
|
|
suffix = ".pg.sql"
|
|
}
|
|
entries, err := migrations.FS.ReadDir(".")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var names []string
|
|
for _, e := range entries {
|
|
if strings.HasSuffix(e.Name(), suffix) {
|
|
names = append(names, e.Name())
|
|
}
|
|
}
|
|
sort.Strings(names)
|
|
|
|
for _, name := range names {
|
|
version, err := strconv.Atoi(strings.SplitN(name, "_", 2)[0])
|
|
if err != nil {
|
|
return fmt.Errorf("migration %s: name must start with a numeric version", name)
|
|
}
|
|
if applied[version] {
|
|
continue
|
|
}
|
|
body, err := migrations.FS.ReadFile(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
tx, err := conn.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, stmt := range strings.Split(string(body), ";") {
|
|
stmt = strings.TrimSpace(stmt)
|
|
if stmt == "" {
|
|
continue
|
|
}
|
|
if _, err := tx.ExecContext(ctx, stmt); err != nil {
|
|
tx.Rollback()
|
|
return fmt.Errorf("migration %s: %w", name, err)
|
|
}
|
|
}
|
|
if _, err := tx.ExecContext(ctx, db.Rebind("INSERT INTO photos_schema_migrations (version, applied_at) VALUES (?, ?)"),
|
|
version, db.TimeArg(time.Now())); err != nil {
|
|
tx.Rollback()
|
|
return fmt.Errorf("record migration %s: %w", name, err)
|
|
}
|
|
if err := tx.Commit(); err != nil {
|
|
return fmt.Errorf("commit migration %s: %w", name, err)
|
|
}
|
|
log.Printf("applied migration %s", name)
|
|
}
|
|
return nil
|
|
}
|