Files
weichen 3145550a59 feat(security): complete P0 release blocking remediation
Unify auth middleware and remove X-User-ID trust, add project_members
authorization, sandbox workspaces via resolver, and admin-only reconcile
by run_id. Introduce goose migrations with startup schema gate, fail closed
when DB unavailable outside explicit ALLOW_IN_MEMORY, bundle web/dist in
app Dockerfile, and remove frontend pseudo-login fallback.
2026-09-13 02:50:22 +00:00

115 lines
2.6 KiB
Go

package db
import (
"context"
"database/sql"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/jackc/pgx/v5/stdlib"
"github.com/pressly/goose/v3"
)
const schemaTable = "goose_db_version"
// RunMigrations 执行 goose 迁移链。
func RunMigrations(ctx context.Context, pool *pgxpool.Pool, migrationsDir string) error {
if pool == nil {
return fmt.Errorf("db migrate: pool required")
}
absDir, err := filepath.Abs(migrationsDir)
if err != nil {
return err
}
if _, err := os.Stat(absDir); err != nil {
return fmt.Errorf("db migrate: directory %s: %w", absDir, err)
}
db := stdlib.OpenDB(*pool.Config().ConnConfig)
defer db.Close()
if err := goose.SetDialect("postgres"); err != nil {
return err
}
goose.SetTableName(schemaTable)
if err := goose.UpContext(ctx, db, absDir); err != nil {
return fmt.Errorf("db migrate up: %w", err)
}
return nil
}
// CurrentVersion 返回当前 schema 版本号。
func CurrentVersion(ctx context.Context, pool *pgxpool.Pool) (int64, error) {
if pool == nil {
return 0, fmt.Errorf("db migrate: pool required")
}
var version int64
err := pool.QueryRow(ctx, fmt.Sprintf(`
SELECT COALESCE(MAX(version_id), 0)::bigint FROM %s
`, schemaTable)).Scan(&version)
if err != nil {
// 表不存在时视为未迁移
if strings.Contains(err.Error(), "does not exist") {
return 0, nil
}
return 0, err
}
return version, nil
}
// ExpectedMigrationCount 统计 migrations 目录下 .sql 文件数量。
func ExpectedMigrationCount(migrationsDir string) (int, error) {
entries, err := os.ReadDir(migrationsDir)
if err != nil {
return 0, err
}
count := 0
for _, e := range entries {
if !e.IsDir() && strings.HasSuffix(e.Name(), ".sql") {
count++
}
}
return count, nil
}
// ResolveMigrationsDir 定位 goose 迁移目录。
func ResolveMigrationsDir() string {
candidates := []string{
"deploy/postgres/goose",
"../deploy/postgres/goose",
"../../deploy/postgres/goose",
}
for _, c := range candidates {
if fi, err := os.Stat(c); err == nil && fi.IsDir() {
return c
}
}
return "deploy/postgres/goose"
}
// OpenSQLDB 从 pgx pool 打开 database/sql(测试辅助)。
func OpenSQLDB(pool *pgxpool.Pool) *sql.DB {
return stdlib.OpenDB(*pool.Config().ConnConfig)
}
// ListMigrationFiles 按名称排序列出迁移文件(测试辅助)。
func ListMigrationFiles(dir string) ([]string, error) {
entries, err := os.ReadDir(dir)
if err != nil {
return nil, err
}
var files []string
for _, e := range entries {
if !e.IsDir() && strings.HasSuffix(e.Name(), ".sql") {
files = append(files, e.Name())
}
}
sort.Strings(files)
return files, nil
}