diff --git a/.env.example b/.env.example index 6b7ae19..cf0319d 100644 --- a/.env.example +++ b/.env.example @@ -19,6 +19,7 @@ PORT=8080 # 数据库配置,默认使用本地 SQLite。 STORAGE_DRIVER=sqlite # sqlite: DATABASE_DSN=data/infinite-canvas.db +# Docker 部署时建议使用绝对路径,避免工作目录变化后写入临时库:DATABASE_DSN=/app/data/infinite-canvas.db # mysql: DATABASE_DSN=user:password@tcp(127.0.0.1:3306)/infinite_canvas?parseTime=true # postgres: DATABASE_DSN=postgres://user:password@127.0.0.1:5432/infinite_canvas?sslmode=disable DATABASE_DSN=data/infinite-canvas.db diff --git a/config/config.go b/config/config.go index 95976b0..a4ed42c 100644 --- a/config/config.go +++ b/config/config.go @@ -3,6 +3,8 @@ package config import ( "crypto/rand" "encoding/base64" + "os" + "path/filepath" "strings" "github.com/caarlos0/env/v11" @@ -30,6 +32,7 @@ func Load() error { if err := env.Parse(&Cfg); err != nil { return err } + normalizeDockerSQLiteDSN("/app/data") if strings.TrimSpace(Cfg.JWTSecret) == "" || Cfg.JWTSecret == "infinite-canvas" { secret, err := randomSecret() if err != nil { @@ -40,6 +43,33 @@ func Load() error { return nil } +func normalizeDockerSQLiteDSN(appDataDir string) { + driver := strings.ToLower(strings.TrimSpace(Cfg.StorageDriver)) + if driver != "" && driver != "sqlite" { + return + } + dsn := strings.TrimSpace(Cfg.DatabaseDSN) + if dsn == "" || dsn == ":memory:" || strings.HasPrefix(dsn, "file:") { + return + } + pathPart, suffix := dsn, "" + if index := strings.Index(dsn, "?"); index >= 0 { + pathPart = dsn[:index] + suffix = dsn[index:] + } + if filepath.IsAbs(pathPart) { + return + } + slashPath := filepath.ToSlash(pathPart) + if slashPath != "data" && !strings.HasPrefix(slashPath, "data/") { + return + } + if _, err := os.Stat(appDataDir); err != nil { + return + } + Cfg.DatabaseDSN = filepath.Join(filepath.Dir(appDataDir), filepath.FromSlash(slashPath)) + suffix +} + func randomSecret() (string, error) { buf := make([]byte, 32) if _, err := rand.Read(buf); err != nil { diff --git a/config/config_test.go b/config/config_test.go new file mode 100644 index 0000000..2f18680 --- /dev/null +++ b/config/config_test.go @@ -0,0 +1,33 @@ +package config + +import ( + "os" + "path/filepath" + "testing" +) + +func TestNormalizeDockerSQLiteDSNUsesMountedDataDir(t *testing.T) { + root := t.TempDir() + appDataDir := filepath.Join(root, "data") + if err := os.MkdirAll(appDataDir, 0755); err != nil { + t.Fatal(err) + } + Cfg = Config{StorageDriver: "sqlite", DatabaseDSN: "data/infinite-canvas.db?_pragma=busy_timeout(5000)"} + + normalizeDockerSQLiteDSN(appDataDir) + + want := filepath.Join(root, "data", "infinite-canvas.db") + "?_pragma=busy_timeout(5000)" + if Cfg.DatabaseDSN != want { + t.Fatalf("DatabaseDSN = %q, want %q", Cfg.DatabaseDSN, want) + } +} + +func TestNormalizeDockerSQLiteDSNLeavesLocalPathWithoutMountedDataDir(t *testing.T) { + Cfg = Config{StorageDriver: "sqlite", DatabaseDSN: "data/infinite-canvas.db"} + + normalizeDockerSQLiteDSN(filepath.Join(t.TempDir(), "missing-data")) + + if Cfg.DatabaseDSN != "data/infinite-canvas.db" { + t.Fatalf("DatabaseDSN = %q, want relative local path", Cfg.DatabaseDSN) + } +} diff --git a/docs/pending-test.md b/docs/pending-test.md index 24a039d..fc2eb2b 100644 --- a/docs/pending-test.md +++ b/docs/pending-test.md @@ -1,5 +1,6 @@ # 待测试 +- Docker 部署时,`DATABASE_DSN=data/infinite-canvas.db` 会在存在 `/app/data` 挂载目录时自动归一到 `/app/data/infinite-canvas.db`,需要验证后台模型配置不会再因为工作目录变为 `/app/web` 而读到空库。 - Seedance 参考素材失败原因排查:后端会把火山上游错误摘要返回给前端;`/video` 和画布视频生成会按 `图片1/视频1/音频1` 自动编号参考素材,并在实际请求提示词中注入编号说明;参考视频会在请求前校验大小、时长、宽高、宽高比和像素总量。 - 画布项目导出改为下载 `.zip` 压缩包,包内包含 `projects.json` 和当前画布引用到的本地图片、视频文件,避免只导出 JSON 时丢失媒体内容。 - 画布库支持多选后一键导出多个画布项目,导出的压缩包可一次恢复多个项目。