mirror of
https://github.com/basketikun/infinite-canvas.git
synced 2026-07-24 06:54:06 +08:00
feat(database): add automatic database creation for MySQL and PostgreSQL
This commit is contained in:
+5
-2
@@ -6,8 +6,9 @@ ADMIN_PASSWORD=infinite-canvas
|
||||
JWT_SECRET=infinite-canvas
|
||||
JWT_EXPIRE_HOURS=168
|
||||
|
||||
# 后端监听端口
|
||||
PORT=8080
|
||||
# 后端默认监听 8080,如需本地开发修改端口再取消注释。
|
||||
# Docker 镜像内前端固定监听 3000,避免覆盖前端 PORT。
|
||||
# PORT=8080
|
||||
|
||||
# 公开访问地址,用于把本地上传的 Seedance 参考图/视频暴露给火山方舟拉取。
|
||||
# 线上部署时填写站点根地址,例如:https://your-domain.example.com
|
||||
@@ -21,6 +22,8 @@ NEXT_PUBLIC_DOC_URL=https://docs.canvas.best
|
||||
STORAGE_DRIVER=sqlite
|
||||
# sqlite: DATABASE_DSN=data/infinite-canvas.db
|
||||
# Docker 部署时建议使用绝对路径,避免工作目录变化后写入临时库:DATABASE_DSN=/app/data/infinite-canvas.db
|
||||
# mysql 目标库不存在时会尝试自动创建,账号需有 CREATE 权限。
|
||||
# mysql: DATABASE_DSN=user:password@tcp(127.0.0.1:3306)/infinite_canvas?parseTime=true
|
||||
# postgres 目标库不存在时会尝试自动创建,账号需有 CREATEDB 权限。
|
||||
# postgres: DATABASE_DSN=postgres://user:password@127.0.0.1:5432/infinite_canvas?sslmode=disable
|
||||
DATABASE_DSN=data/infinite-canvas.db
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
+ [新增] 新增图片放大工具。
|
||||
+ [优化] 优化图片工具条,增加自定义功能。
|
||||
+ [修复] 修复端口冲突问题、pg/mysql未初始化问题。
|
||||
|
||||
## v0.2.1 - 2026-06-03
|
||||
|
||||
+ [新增] 新增文档站点页面。
|
||||
|
||||
+1
-1
@@ -43,4 +43,4 @@ RUN mkdir -p /app/data/prompts
|
||||
|
||||
EXPOSE 3000
|
||||
# 先启动内部 Go API,再由 Next.js 提供页面并代理 /api/*。
|
||||
CMD ["sh", "-c", "PORT=8080 /app/server & cd /app/web && node server.js"]
|
||||
CMD ["sh", "-c", "PORT=8080 /app/server & cd /app/web && PORT=3000 node server.js"]
|
||||
|
||||
@@ -7,6 +7,8 @@ require (
|
||||
github.com/gin-gonic/gin v1.11.0
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||
github.com/go-sql-driver/mysql v1.8.1
|
||||
github.com/jackc/pgx/v5 v5.6.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
golang.org/x/crypto v0.48.0
|
||||
@@ -27,13 +29,11 @@ require (
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.6.0 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
|
||||
+113
-2
@@ -1,6 +1,9 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -9,7 +12,10 @@ import (
|
||||
"github.com/basketikun/infinite-canvas/config"
|
||||
"github.com/basketikun/infinite-canvas/model"
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/driver/mysql"
|
||||
mysqldriver "github.com/go-sql-driver/mysql"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
gormmysql "gorm.io/driver/mysql"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -41,6 +47,18 @@ func DB() (*gorm.DB, error) {
|
||||
if driver == "sqlite" && dsn != ":memory:" {
|
||||
_ = os.MkdirAll(filepath.Dir(dsn), 0755)
|
||||
}
|
||||
if isPostgresDriver(driver) {
|
||||
dbErr = ensurePostgresDatabase(dsn)
|
||||
if dbErr != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
if driver == "mysql" {
|
||||
dbErr = ensureMySQLDatabase(dsn)
|
||||
if dbErr != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
db, dbErr = gorm.Open(dialector(driver, dsn), &gorm.Config{})
|
||||
if dbErr != nil {
|
||||
return
|
||||
@@ -59,10 +77,103 @@ func DB() (*gorm.DB, error) {
|
||||
func dialector(driver string, dsn string) gorm.Dialector {
|
||||
switch driver {
|
||||
case "mysql":
|
||||
return mysql.Open(dsn)
|
||||
return gormmysql.Open(dsn)
|
||||
case "postgres", "postgresql":
|
||||
return postgres.Open(dsn)
|
||||
default:
|
||||
return sqlite.Open(dsn)
|
||||
}
|
||||
}
|
||||
|
||||
func isPostgresDriver(driver string) bool {
|
||||
return driver == "postgres" || driver == "postgresql"
|
||||
}
|
||||
|
||||
func ensureMySQLDatabase(dsn string) error {
|
||||
cfg, err := mysqldriver.ParseDSN(dsn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
target := strings.TrimSpace(cfg.DBName)
|
||||
if target == "" {
|
||||
return nil
|
||||
}
|
||||
ctx := context.Background()
|
||||
targetDB, err := sql.Open("mysql", dsn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = targetDB.PingContext(ctx)
|
||||
_ = targetDB.Close()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if !isMySQLError(err, 1049) {
|
||||
return err
|
||||
}
|
||||
|
||||
maintenance := cfg.Clone()
|
||||
maintenance.DBName = ""
|
||||
serverDB, err := sql.Open("mysql", maintenance.FormatDSN())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer serverDB.Close()
|
||||
|
||||
_, err = serverDB.ExecContext(ctx, "CREATE DATABASE "+quoteMySQLIdentifier(target)+" CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci")
|
||||
if isMySQLError(err, 1007) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func ensurePostgresDatabase(dsn string) error {
|
||||
cfg, err := pgx.ParseConfig(dsn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
target := strings.TrimSpace(cfg.Database)
|
||||
if target == "" {
|
||||
return nil
|
||||
}
|
||||
ctx := context.Background()
|
||||
conn, err := pgx.ConnectConfig(ctx, cfg)
|
||||
if err == nil {
|
||||
_ = conn.Close(ctx)
|
||||
return nil
|
||||
}
|
||||
if !isPostgresError(err, "3D000") {
|
||||
return err
|
||||
}
|
||||
|
||||
maintenance := cfg.Copy()
|
||||
maintenance.Database = "postgres"
|
||||
if strings.EqualFold(target, "postgres") {
|
||||
maintenance.Database = "template1"
|
||||
}
|
||||
conn, err = pgx.ConnectConfig(ctx, maintenance)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close(ctx)
|
||||
|
||||
_, err = conn.Exec(ctx, "CREATE DATABASE "+pgx.Identifier{target}.Sanitize(), pgx.QueryExecModeExec)
|
||||
if isPostgresError(err, "42P04") {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func isMySQLError(err error, number uint16) bool {
|
||||
var mysqlErr *mysqldriver.MySQLError
|
||||
return errors.As(err, &mysqlErr) && mysqlErr.Number == number
|
||||
}
|
||||
|
||||
func isPostgresError(err error, code string) bool {
|
||||
var pgErr *pgconn.PgError
|
||||
return errors.As(err, &pgErr) && pgErr.Code == code
|
||||
}
|
||||
|
||||
func quoteMySQLIdentifier(name string) string {
|
||||
return "`" + strings.ReplaceAll(name, "`", "``") + "`"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user