Restructure backend into services and fix security, crash and sync issues
Security - Serve character images through os.Root so names like ../ can't escape the characters folder - Sync start and /sync/reset are POST only (legacy and /api/v1) - /api/v1/token/cleanup requires a token, password compared in constant time, tokens no longer logged - Echo's rate limiter replaces the custom one; TRUSTED_PROXIES enables X-Forwarded-For for reverse proxies Crashes and races - Sync state lives in SyncService with atomics and a mutex instead of unsynchronised globals (fixes concurrent map writes during full sync) - Only one sync can run; /api/v1 sync and music routes are locked during sync like the legacy ones - No log.Fatal outside startup: errors become 4xx/5xx responses - Search uses an in-order character match instead of unescaped regexes - Empty queue / no songs return 404 instead of panicking or looping Behaviour - Sync result is computed once and stays the same between polls - Swagger and /openapi serve the embedded spec - Shutdown stops HTTP before closing the database - Expired tokens are deleted with a context that outlives the request - Files are served with Range support and correct content types; audio has no write deadline Performance - Migration 000006 adds soundtrack.fingerprint (names, sizes, mtimes) so unchanged folders are skipped without reading any files - Songs are reconciled per soundtrack with one query plus needed writes; renames keep play counts - Moved MUSIC_PATH updates soundtrack and song paths - .id cleanup only runs for changed soundtracks or a full sync - Classic random uses one SQL query; Gitea release lookups are cached with a timeout Structure - internal/config reads all environment variables once - sqlc emits a Querier interface; backend globals become services injected into handlers - One route registration shared by legacy and /api/v1 - Removed dead code (dbHelper.go, unused middleware, duplicate Gitea functions, embedded Swagger UI copy) and lib/pq - templ and swag pinned as go tools
This commit is contained in:
1 parent
75b6f0c1cf
commit
090e794460
78 files changed
+2919
-6303
No files matched your search
+69
-84
@@ -2,54 +2,57 @@ package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"music-server/internal/logging"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
"github.com/golang-migrate/migrate/v4/database/postgres"
|
||||
_ "github.com/golang-migrate/migrate/v4/database/postgres"
|
||||
pgxmigrate "github.com/golang-migrate/migrate/v4/database/pgx/v5"
|
||||
"github.com/golang-migrate/migrate/v4/source/iofs"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
_ "github.com/lib/pq"
|
||||
"github.com/jackc/pgx/v5/stdlib"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// Database holds the database connection pool and context
|
||||
//go:embed "migrations/*.sql"
|
||||
var MigrationsFs embed.FS
|
||||
|
||||
// Database holds the database connection pool
|
||||
type Database struct {
|
||||
Pool *pgxpool.Pool
|
||||
Ctx context.Context
|
||||
}
|
||||
|
||||
// ConnString builds a PostgreSQL connection URL.
|
||||
func ConnString(host, port, user, password, dbname string) string {
|
||||
u := url.URL{
|
||||
Scheme: "postgres",
|
||||
User: url.UserPassword(user, password),
|
||||
Host: net.JoinHostPort(host, port),
|
||||
Path: dbname,
|
||||
RawQuery: "sslmode=disable",
|
||||
}
|
||||
return u.String()
|
||||
}
|
||||
|
||||
// NewDatabase creates a new Database instance with connection pool
|
||||
func NewDatabase(host, port, user, password, dbname string) (*Database, error) {
|
||||
ctx := context.Background()
|
||||
psqlInfo := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
|
||||
host, port, user, password, dbname)
|
||||
|
||||
logging.GetLogger().Debug("Database connection info",
|
||||
zap.String("host", host),
|
||||
zap.String("port", port),
|
||||
zap.String("dbname", dbname))
|
||||
|
||||
pool, err := pgxpool.New(ctx, psqlInfo)
|
||||
func NewDatabase(ctx context.Context, connString string) (*Database, error) {
|
||||
pool, err := pgxpool.New(ctx, connString)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to connect to database: %w", err)
|
||||
}
|
||||
|
||||
// Test connection
|
||||
var success string
|
||||
err = pool.QueryRow(ctx, "select 'Successfully connected!'").Scan(&success)
|
||||
if err != nil {
|
||||
if err := pool.Ping(ctx); err != nil {
|
||||
pool.Close()
|
||||
return nil, fmt.Errorf("database query failed: %w", err)
|
||||
return nil, fmt.Errorf("database ping failed: %w", err)
|
||||
}
|
||||
|
||||
logging.GetLogger().Info("Database connected", zap.String("status", success))
|
||||
|
||||
return &Database{Pool: pool, Ctx: ctx}, nil
|
||||
logging.GetLogger().Info("Database connected")
|
||||
return &Database{Pool: pool}, nil
|
||||
}
|
||||
|
||||
// Close closes the database connection pool
|
||||
@@ -61,82 +64,64 @@ func (db *Database) Close() {
|
||||
}
|
||||
|
||||
// Health checks the health of the database connection by pinging the database.
|
||||
// It returns a map with keys indicating various health statistics.
|
||||
func (db *Database) Health() map[string]string {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
||||
func (db *Database) Health(ctx context.Context) map[string]string {
|
||||
ctx, cancel := context.WithTimeout(ctx, time.Second)
|
||||
defer cancel()
|
||||
|
||||
stats := make(map[string]string)
|
||||
|
||||
// Ping the database
|
||||
err := db.Pool.Ping(ctx)
|
||||
if err != nil {
|
||||
stats["status"] = "down"
|
||||
stats["error"] = err.Error()
|
||||
return stats
|
||||
if err := db.Pool.Ping(ctx); err != nil {
|
||||
return map[string]string{"status": "down", "error": err.Error()}
|
||||
}
|
||||
|
||||
stats["status"] = "up"
|
||||
return stats
|
||||
return map[string]string{"status": "up"}
|
||||
}
|
||||
|
||||
// RunMigrations runs all pending database migrations to the latest version.
|
||||
// Uses the existing pool to extract connection details.
|
||||
func (db *Database) RunMigrations() error {
|
||||
// Extract connection info from pool config
|
||||
connConfig := db.Pool.Config().ConnConfig
|
||||
migrationURL := fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=disable",
|
||||
connConfig.User,
|
||||
connConfig.Password,
|
||||
connConfig.Host,
|
||||
connConfig.Port,
|
||||
connConfig.Database)
|
||||
|
||||
logging.GetLogger().Debug("Migration info", zap.String("url", migrationURL))
|
||||
|
||||
sqlDb, err := sql.Open("postgres", migrationURL)
|
||||
m, closeFn, err := db.NewMigrator()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open database for migration: %w", err)
|
||||
return err
|
||||
}
|
||||
defer sqlDb.Close()
|
||||
defer closeFn()
|
||||
|
||||
driver, err := postgres.WithInstance(sqlDb, &postgres.Config{})
|
||||
version, _, err := m.Version()
|
||||
if err != nil && !errors.Is(err, migrate.ErrNilVersion) {
|
||||
logging.GetLogger().Error("Failed to get migration version", zap.String("error", err.Error()))
|
||||
}
|
||||
logging.GetLogger().Info("Migration version before", zap.Uint("version", version))
|
||||
|
||||
if err := m.Up(); err != nil {
|
||||
if !errors.Is(err, migrate.ErrNoChange) {
|
||||
return fmt.Errorf("migration failed: %w", err)
|
||||
}
|
||||
logging.GetLogger().Info("Database already up to date")
|
||||
return nil
|
||||
}
|
||||
|
||||
versionAfter, _, _ := m.Version()
|
||||
logging.GetLogger().Info("Migrated to version", zap.Uint("version", versionAfter))
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewMigrator returns a migrator for the embedded migrations using the pool's connection settings.
|
||||
// The returned function closes the underlying connection.
|
||||
func (db *Database) NewMigrator() (*migrate.Migrate, func(), error) {
|
||||
sqlDB := stdlib.OpenDB(*db.Pool.Config().ConnConfig)
|
||||
|
||||
driver, err := pgxmigrate.WithInstance(sqlDB, &pgxmigrate.Config{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create migration driver: %w", err)
|
||||
sqlDB.Close()
|
||||
return nil, nil, fmt.Errorf("failed to create migration driver: %w", err)
|
||||
}
|
||||
|
||||
files, err := iofs.New(MigrationsFs, "migrations")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create migration files: %w", err)
|
||||
sqlDB.Close()
|
||||
return nil, nil, fmt.Errorf("failed to read migration files: %w", err)
|
||||
}
|
||||
|
||||
m, err := migrate.NewWithInstance("iofs", files, "postgres", driver)
|
||||
m, err := migrate.NewWithInstance("iofs", files, "pgx5", driver)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create migrator: %w", err)
|
||||
sqlDB.Close()
|
||||
return nil, nil, fmt.Errorf("failed to create migrator: %w", err)
|
||||
}
|
||||
|
||||
// Get current version for logging
|
||||
version, _, err := m.Version()
|
||||
if err != nil {
|
||||
logging.GetLogger().Error("Failed to get migration version", zap.String("error", err.Error()))
|
||||
}
|
||||
|
||||
logging.GetLogger().Info("Migration version before", zap.Uint("version", version))
|
||||
|
||||
// Run all pending migrations to latest version
|
||||
err = m.Up()
|
||||
if err != nil {
|
||||
if err == migrate.ErrNoChange {
|
||||
logging.GetLogger().Info("Database already up to date")
|
||||
} else {
|
||||
return fmt.Errorf("migration failed: %w", err)
|
||||
}
|
||||
} else {
|
||||
// Get new version after migration
|
||||
versionAfter, _, _ := m.Version()
|
||||
logging.GetLogger().Info("Migrated to version", zap.Uint("version", versionAfter))
|
||||
}
|
||||
|
||||
logging.GetLogger().Info("Migration completed")
|
||||
return nil
|
||||
return m, func() { m.Close() }, nil
|
||||
}
|
||||
Reference in new issue
Block a user