Initial commit: Lookbook personal collection app
Pinterest-like app for saving images, videos, quotes, and embeds. Features: - Go backend with PostgreSQL, SSR templates - Console-based admin auth (login/logout via browser console) - Item types: images, videos (ffmpeg transcoding), quotes, embeds - Media stored as BLOBs in PostgreSQL - OpenGraph metadata extraction for links - Embed detection for YouTube, Vimeo, Twitter/X - Masonry grid layout, item detail pages - Tag system with filtering - Refresh metadata endpoint with change warnings - Replace media endpoint for updating item images/videos
This commit is contained in:
commit
cdcc5b5293
45 changed files with 4634 additions and 0 deletions
131
internal/migrations/migrations.go
Normal file
131
internal/migrations/migrations.go
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"embed"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
_ "github.com/jackc/pgx/v5/stdlib"
|
||||
"github.com/pressly/goose/v3"
|
||||
)
|
||||
|
||||
//go:embed sql/*.sql
|
||||
var FS embed.FS
|
||||
|
||||
const DefaultURL = "postgres:///lookbook?sslmode=disable"
|
||||
|
||||
// Up applies all available migrations using the provided database URL.
|
||||
func Up(ctx context.Context, dbURL string, logger *slog.Logger) error {
|
||||
url := dbURL
|
||||
if url == "" {
|
||||
url = DefaultURL
|
||||
}
|
||||
|
||||
db, err := openDB(url, logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
if err := goose.UpContext(ctx, db, "sql"); err != nil {
|
||||
return fmt.Errorf("apply migrations: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("database migrated")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Down rolls back migrations. If targetVersion < 0, it steps back one migration; otherwise it migrates down to the target version.
|
||||
func Down(ctx context.Context, dbURL string, targetVersion int64, logger *slog.Logger) error {
|
||||
url := dbURL
|
||||
if url == "" {
|
||||
url = DefaultURL
|
||||
}
|
||||
|
||||
db, err := openDB(url, logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
if targetVersion < 0 {
|
||||
if err := goose.DownContext(ctx, db, "sql"); err != nil {
|
||||
return fmt.Errorf("rollback one: %w", err)
|
||||
}
|
||||
logger.Info("rolled back one migration")
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := goose.DownToContext(ctx, db, "sql", targetVersion); err != nil {
|
||||
return fmt.Errorf("rollback to version %d: %w", targetVersion, err)
|
||||
}
|
||||
logger.Info("rolled back to version", slog.Int64("version", targetVersion))
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckPending returns the number of pending migrations without applying them.
|
||||
func CheckPending(ctx context.Context, dbURL string, logger *slog.Logger) (int, error) {
|
||||
url := dbURL
|
||||
if url == "" {
|
||||
url = DefaultURL
|
||||
}
|
||||
|
||||
db, err := openDB(url, logger)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
current, err := goose.GetDBVersionContext(ctx, db)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("get db version: %w", err)
|
||||
}
|
||||
|
||||
migrations, err := goose.CollectMigrations("sql", 0, goose.MaxVersion)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("collect migrations: %w", err)
|
||||
}
|
||||
|
||||
pending := 0
|
||||
for _, m := range migrations {
|
||||
if m.Version > current {
|
||||
pending++
|
||||
}
|
||||
}
|
||||
|
||||
return pending, nil
|
||||
}
|
||||
|
||||
// slogLogger adapts slog to goose's minimal logging interface.
|
||||
type slogLogger struct {
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func (l slogLogger) Printf(format string, v ...any) {
|
||||
l.logger.Info(fmt.Sprintf(format, v...))
|
||||
}
|
||||
|
||||
func (l slogLogger) Fatalf(format string, v ...any) {
|
||||
l.logger.Error(fmt.Sprintf(format, v...))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func openDB(url string, logger *slog.Logger) (*sql.DB, error) {
|
||||
goose.SetBaseFS(FS)
|
||||
goose.SetLogger(slogLogger{logger: logger})
|
||||
|
||||
db, err := sql.Open("pgx", url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open db: %w", err)
|
||||
}
|
||||
|
||||
if err := db.Ping(); err != nil {
|
||||
db.Close()
|
||||
return nil, fmt.Errorf("ping db: %w", err)
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue