Files
FileShare/server/internal/database/database.go
T
SKYMirror 9686fe887a FileCodeBox Go 重写版 v2.5.6(安全审计修复版)
Go 1.27.1 (Gin+GORM) + Vue 3 文件快传服务:

- 安全审计全部修复(docs/security-audit-2026-09-05.md):
  bcrypt 密码哈希与自动升级、presign 直传服务端大小/内容校验、
  全局请求体上限、依赖升级(govulncheck 0 命中)、janitor 后台清理、
  管理端审计动作落库、/admin CORS 收紧、通知内容白名单净化、
  会话默认 7 天、限流缓存故障降级、robots.txt 端点等
- 前端:取件链接复制修复(不再重复拼接提取码)、markdown 净化器加固
- Redis 支持库号(FCB_REDIS_DB / redis://…/db URL)
- 文档:docs/api/* 与 openapi.yaml 同步最新行为(robots.txt、
  提码 5 位起、chunk 32MiB 上限、admin 审计动作等)

验证:gofmt/go vet/go test 全绿;二进制端到端冒烟通过
2026-09-05 04:22:41 +08:00

140 lines
4.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package database 负责数据库连接与迁移(需求 ⑧:双方言):
// - sqlite(默认):modernc.org/sqlite 纯 Go 驱动(GORM 封装 glebarez/sqlite),零 CGO、零外部依赖;
// - postgres:可选,配置 FCB_DB_DRIVER=postgres + FCB_DB_DSN 后启用。
//
// 两方言共用 GORM 抽象层,AutoMigrate 与全部业务查询保持方言无关;
// 唯一的原生 SQL(migrates 建表)已改为双方言分支。
package database
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/glebarez/sqlite"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"filecodebox/internal/config"
"filecodebox/internal/model"
)
// Options 连接选项(main.go 从 config.Env 装配)。
type Options struct {
Driver string // sqlite | postgres(空按 sqlite 处理)
DSN string // postgres 连接串;sqlite 为文件路径(空回退 config.DefaultSQLitePath
}
// Open 按驱动连接数据库并执行连接池设置与探活。
func Open(ctx context.Context, opts Options) (*gorm.DB, error) {
driver := strings.ToLower(strings.TrimSpace(opts.Driver))
if driver == "" {
driver = config.DBDriverSQLite
}
var dialector gorm.Dialector
switch driver {
case config.DBDriverSQLite:
path := strings.TrimSpace(opts.DSN)
if path == "" {
path = config.DefaultSQLitePath
}
// 自动创建父目录(如 ./data),对齐参考实现 data_root 语义
if dir := filepath.Dir(path); dir != "" && dir != "." {
if err := os.MkdirAll(dir, 0o755); err != nil {
return nil, fmt.Errorf("database: 创建 SQLite 目录 %s 失败: %w", dir, err)
}
}
// DSN 参数:busy_timeout 防写锁竞态;WAL 提升并发读写(Query 参数形式,驱动原生支持)
dsn := path + "?_pragma=busy_timeout(10000)&_pragma=journal_mode(WAL)&_pragma=foreign_keys(1)"
dialector = sqlite.Open(dsn)
case config.DBDriverPostgres:
if strings.TrimSpace(opts.DSN) == "" {
return nil, fmt.Errorf("database: FCB_DB_DRIVER=postgres 需要提供 FCB_DB_DSN")
}
dialector = postgres.Open(opts.DSN)
default:
return nil, fmt.Errorf("database: 不支持的数据库驱动 %q(仅支持 sqlite|postgres", driver)
}
db, err := gorm.Open(dialector, &gorm.Config{
Logger: logger.Default.LogMode(logger.Warn),
// 避免 GORM 生成方言特有子句;时间语义由应用层统一(容器本地时区)
NowFunc: time.Now,
})
if err != nil {
return nil, fmt.Errorf("database: 连接 %s 失败: %w", driver, err)
}
sqlDB, err := db.DB()
if err != nil {
return nil, err
}
// 连接池:SQLite 单文件场景保守设置;Postgres 沿用 v1 参数
switch driver {
case config.DBDriverSQLite:
sqlDB.SetMaxOpenConns(8)
sqlDB.SetMaxIdleConns(4)
sqlDB.SetConnMaxLifetime(0) // 长连接文件句柄,无需轮换
case config.DBDriverPostgres:
sqlDB.SetMaxOpenConns(32)
sqlDB.SetMaxIdleConns(8)
sqlDB.SetConnMaxLifetime(time.Hour)
}
// 连接探活(带超时)
pingCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
if err := sqlDB.PingContext(pingCtx); err != nil {
return nil, fmt.Errorf("database: %s 探活失败: %w", driver, err)
}
return db, nil
}
// Migrate 执行迁移:先建迁移台账表(双方言分支),再 AutoMigrate 全部模型。
func Migrate(ctx context.Context, db *gorm.DB) error {
if err := createMigratesTable(ctx, db); err != nil {
return err
}
if err := model.AutoMigrate(db); err != nil {
return fmt.Errorf("database: AutoMigrate 失败: %w", err)
}
return nil
}
// createMigratesTable 创建迁移台账表。
// 双方言差异:自增主键 postgres 用 BIGSERIAL、sqlite 用 INTEGER PRIMARY KEY AUTOINCREMENT
// 时间戳默认值 postgres 用 CURRENT_TIMESTAMP、sqlite 用 CURRENT_TIMESTAMP(等价)。
func createMigratesTable(ctx context.Context, db *gorm.DB) error {
ddl := `
CREATE TABLE IF NOT EXISTS migrates (
id INTEGER PRIMARY KEY AUTOINCREMENT,
migration_file VARCHAR(255) NOT NULL UNIQUE,
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)`
if db.Dialector.Name() == config.DBDriverPostgres {
ddl = `
CREATE TABLE IF NOT EXISTS migrates (
id BIGSERIAL PRIMARY KEY,
migration_file VARCHAR(255) NOT NULL UNIQUE,
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)`
}
if err := db.WithContext(ctx).Exec(ddl).Error; err != nil {
return fmt.Errorf("database: 创建 migrates 表失败: %w", err)
}
return nil
}
// Close 关闭底层连接。
func Close(db *gorm.DB) error {
sqlDB, err := db.DB()
if err != nil {
return err
}
return sqlDB.Close()
}