fix: persist docker sqlite settings path
This commit is contained in:
@@ -19,6 +19,7 @@ PORT=8080
|
|||||||
# 数据库配置,默认使用本地 SQLite。
|
# 数据库配置,默认使用本地 SQLite。
|
||||||
STORAGE_DRIVER=sqlite
|
STORAGE_DRIVER=sqlite
|
||||||
# sqlite: DATABASE_DSN=data/infinite-canvas.db
|
# 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
|
# 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
|
# postgres: DATABASE_DSN=postgres://user:password@127.0.0.1:5432/infinite_canvas?sslmode=disable
|
||||||
DATABASE_DSN=data/infinite-canvas.db
|
DATABASE_DSN=data/infinite-canvas.db
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package config
|
|||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/caarlos0/env/v11"
|
"github.com/caarlos0/env/v11"
|
||||||
@@ -30,6 +32,7 @@ func Load() error {
|
|||||||
if err := env.Parse(&Cfg); err != nil {
|
if err := env.Parse(&Cfg); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
normalizeDockerSQLiteDSN("/app/data")
|
||||||
if strings.TrimSpace(Cfg.JWTSecret) == "" || Cfg.JWTSecret == "infinite-canvas" {
|
if strings.TrimSpace(Cfg.JWTSecret) == "" || Cfg.JWTSecret == "infinite-canvas" {
|
||||||
secret, err := randomSecret()
|
secret, err := randomSecret()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -40,6 +43,33 @@ func Load() error {
|
|||||||
return nil
|
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) {
|
func randomSecret() (string, error) {
|
||||||
buf := make([]byte, 32)
|
buf := make([]byte, 32)
|
||||||
if _, err := rand.Read(buf); err != nil {
|
if _, err := rand.Read(buf); err != nil {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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` 自动编号参考素材,并在实际请求提示词中注入编号说明;参考视频会在请求前校验大小、时长、宽高、宽高比和像素总量。
|
- Seedance 参考素材失败原因排查:后端会把火山上游错误摘要返回给前端;`/video` 和画布视频生成会按 `图片1/视频1/音频1` 自动编号参考素材,并在实际请求提示词中注入编号说明;参考视频会在请求前校验大小、时长、宽高、宽高比和像素总量。
|
||||||
- 画布项目导出改为下载 `.zip` 压缩包,包内包含 `projects.json` 和当前画布引用到的本地图片、视频文件,避免只导出 JSON 时丢失媒体内容。
|
- 画布项目导出改为下载 `.zip` 压缩包,包内包含 `projects.json` 和当前画布引用到的本地图片、视频文件,避免只导出 JSON 时丢失媒体内容。
|
||||||
- 画布库支持多选后一键导出多个画布项目,导出的压缩包可一次恢复多个项目。
|
- 画布库支持多选后一键导出多个画布项目,导出的压缩包可一次恢复多个项目。
|
||||||
|
|||||||
Reference in New Issue
Block a user