diff --git a/backend/services/setup/db.go b/backend/services/setup/db.go
index 4efac5c3..1a785f4a 100644
--- a/backend/services/setup/db.go
+++ b/backend/services/setup/db.go
@@ -434,10 +434,16 @@ func prepareSQLiteDatabase(dsn string) error {
return nil
}
directory := filepath.Dir(path)
- if directory == "." || directory == "" {
- return nil
+ if directory != "." && directory != "" {
+ if err := os.MkdirAll(directory, 0750); err != nil {
+ return err
+ }
}
- return os.MkdirAll(directory, 0750)
+ // 全新部署自动清空 SQLite 历史数据库文件及临时文件
+ for _, ext := range []string{"", "-wal", "-shm"} {
+ _ = os.Remove(path + ext)
+ }
+ return nil
}
func buildMySQLDSN(settings DatabaseSettings, previousDSN string) (string, error) {
@@ -582,6 +588,26 @@ func (database *sqlSetupDatabase) TableCountContext(ctx stdcontext.Context, quer
return count, nil
}
+func (database *sqlSetupDatabase) TableNamesContext(ctx stdcontext.Context, query string) ([]string, error) {
+ rows, err := database.QueryContext(ctx, query)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ var names []string
+ for rows.Next() {
+ var name string
+ if err := rows.Scan(&name); err != nil {
+ return nil, err
+ }
+ names = append(names, name)
+ }
+ if err := rows.Err(); err != nil {
+ return nil, err
+ }
+ return names, nil
+}
+
var openMySQLSetupDatabase = func(dsn string) (setupDatabase, error) {
database, err := sql.Open("mysql", dsn)
if err != nil {
@@ -616,7 +642,7 @@ func ensurePostgresDatabase(dsn string) error {
}
defer func() { _ = target.Close() }()
if err = target.PingContext(requestContext); err == nil {
- return requireEmptyExternalDatabase(target, requestContext, "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema NOT IN ('pg_catalog', 'information_schema')")
+ return clearPostgresDatabase(target, requestContext)
} else {
var postgresErr *pq.Error
if !stderrors.As(err, &postgresErr) || string(postgresErr.Code) != "3D000" {
@@ -652,16 +678,45 @@ type setupDatabase interface {
PingContext(stdcontext.Context) error
ExecContext(stdcontext.Context, string, ...any) (sql.Result, error)
TableCountContext(stdcontext.Context, string) (int, error)
+ TableNamesContext(stdcontext.Context, string) ([]string, error)
Close() error
}
-func requireEmptyExternalDatabase(database setupDatabase, ctx stdcontext.Context, query string) error {
- count, err := database.TableCountContext(ctx, query)
+func clearPostgresDatabase(database setupDatabase, ctx stdcontext.Context) error {
+ count, err := database.TableCountContext(ctx, "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema NOT IN ('pg_catalog', 'information_schema')")
if err != nil {
- return databaseError("检查外部数据库是否为空失败", err)
+ return databaseError("检查 PostgreSQL 数据库表失败", err)
}
- if count > 0 {
- return databaseError("全新部署要求外部数据库为空,请使用新数据库或先清理 Smail 专属数据库", nil)
+ if count == 0 {
+ return nil
+ }
+ if _, err := database.ExecContext(ctx, "DROP SCHEMA public CASCADE; CREATE SCHEMA public;"); err != nil {
+ return databaseError("清空 PostgreSQL 数据库失败", err)
+ }
+ return nil
+}
+
+func clearMySQLDatabase(database setupDatabase, ctx stdcontext.Context) error {
+ tableNames, err := database.TableNamesContext(ctx, "SELECT table_name FROM information_schema.tables WHERE table_schema = DATABASE() AND table_type = 'BASE TABLE'")
+ if err != nil {
+ return databaseError("检查 MySQL 数据库表失败", err)
+ }
+ if len(tableNames) == 0 {
+ return nil
+ }
+ if _, err := database.ExecContext(ctx, "SET FOREIGN_KEY_CHECKS = 0;"); err != nil {
+ return databaseError("清空 MySQL 数据库表失败", err)
+ }
+ defer func() {
+ _, _ = database.ExecContext(ctx, "SET FOREIGN_KEY_CHECKS = 1;")
+ }()
+ for _, tableName := range tableNames {
+ if !validMySQLIdentifier(tableName) {
+ continue
+ }
+ if _, err := database.ExecContext(ctx, fmt.Sprintf("DROP TABLE IF EXISTS `%s` CASCADE;", tableName)); err != nil {
+ return databaseError("清空 MySQL 数据库表失败", err)
+ }
}
return nil
}
@@ -684,7 +739,7 @@ func ensureMySQLDatabase(dsn string) error {
}
defer func() { _ = target.Close() }()
if err = target.PingContext(requestContext); err == nil {
- return requireEmptyExternalDatabase(target, requestContext, "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE()")
+ return clearMySQLDatabase(target, requestContext)
} else {
var mysqlErr *mysql.MySQLError
if !errors.As(err, &mysqlErr) || mysqlErr.Number != 1049 {
diff --git a/backend/services/setup/db_database_test.go b/backend/services/setup/db_database_test.go
index 65bbdda5..edeb2348 100644
--- a/backend/services/setup/db_database_test.go
+++ b/backend/services/setup/db_database_test.go
@@ -50,6 +50,8 @@ type fakeSetupDB struct {
tableCount int
tableCountErr error
tableQueries []string
+ tableNames []string
+ tableNamesErr error
closed bool
}
@@ -75,6 +77,14 @@ func (database *fakeSetupDB) TableCountContext(_ context.Context, query string)
return database.tableCount, nil
}
+func (database *fakeSetupDB) TableNamesContext(_ context.Context, query string) ([]string, error) {
+ if database.tableNamesErr != nil {
+ return nil, database.tableNamesErr
+ }
+ database.tableQueries = append(database.tableQueries, query)
+ return database.tableNames, nil
+}
+
func (database *fakeSetupDB) Close() error {
database.closed = true
return nil
@@ -293,29 +303,31 @@ func TestEnsureMySQLDatabaseSkipsCreationWhenDatabaseExists(t *testing.T) {
}
}
-func TestEnsureMySQLDatabaseRejectsExistingNonEmptyDatabase(t *testing.T) {
- target := &fakeSetupDB{tableCount: 1}
+func TestEnsureMySQLDatabaseClearsExistingNonEmptyDatabase(t *testing.T) {
+ target := &fakeSetupDB{tableNames: []string{"user", "mail"}}
withMySQLSetupOpener(t, func(string) (setupDatabase, error) {
return target, nil
})
- if err := ensureMySQLDatabase(testMySQLDSN(t, "secret")); err == nil {
- t.Fatal("non-empty MySQL database was accepted for a fresh deployment")
- } else if message := DatabaseSetupErrorMessage(err); message == "" {
- t.Fatal("fresh deployment error message was empty")
+ if err := ensureMySQLDatabase(testMySQLDSN(t, "secret")); err != nil {
+ t.Fatalf("expected non-empty MySQL database to be cleared successfully, got: %v", err)
+ }
+ if len(target.execQueries) == 0 {
+ t.Fatal("expected DROP TABLE queries to be executed on MySQL database")
}
}
-func TestEnsurePostgresDatabaseRejectsExistingNonEmptyDatabase(t *testing.T) {
- target := &fakeSetupDB{tableCount: 1}
+func TestEnsurePostgresDatabaseClearsExistingNonEmptyDatabase(t *testing.T) {
+ target := &fakeSetupDB{tableCount: 2}
withPostgresSetupOpener(t, func(string) (setupDatabase, error) {
return target, nil
})
- if err := ensurePostgresDatabase("postgres://smail:secret@127.0.0.1:5432/smail?sslmode=disable"); err == nil {
- t.Fatal("non-empty PostgreSQL database was accepted for a fresh deployment")
- } else if message := DatabaseSetupErrorMessage(err); message == "" {
- t.Fatal("fresh deployment error message was empty")
+ if err := ensurePostgresDatabase("postgres://smail:secret@127.0.0.1:5432/smail?sslmode=disable"); err != nil {
+ t.Fatalf("expected non-empty PostgreSQL database to be cleared successfully, got: %v", err)
+ }
+ if len(target.execQueries) == 0 {
+ t.Fatal("expected DROP SCHEMA queries to be executed on PostgreSQL database")
}
}
@@ -399,13 +411,27 @@ func testMySQLDSN(t *testing.T, password string) string {
}
func TestPrepareSQLiteDatabaseCreatesParentDirectory(t *testing.T) {
- databasePath := filepath.Join(t.TempDir(), "nested", "config", "smail.db")
+ tempDir := t.TempDir()
+ databasePath := filepath.Join(tempDir, "nested", "config", "smail.db")
if err := prepareSQLiteDatabase(databasePath); err != nil {
t.Fatal(err)
}
if _, err := os.Stat(filepath.Dir(databasePath)); err != nil {
t.Fatalf("SQLite parent directory was not prepared: %v", err)
}
+
+ // 测试全新部署自动清空已存在的数据文件和 wal/shm 临时文件
+ _ = os.WriteFile(databasePath, []byte("legacy data"), 0644)
+ _ = os.WriteFile(databasePath+"-wal", []byte("wal data"), 0644)
+ _ = os.WriteFile(databasePath+"-shm", []byte("shm data"), 0644)
+ if err := prepareSQLiteDatabase(databasePath); err != nil {
+ t.Fatal(err)
+ }
+ for _, ext := range []string{"", "-wal", "-shm"} {
+ if _, err := os.Stat(databasePath + ext); !os.IsNotExist(err) {
+ t.Fatalf("expected SQLite file %s to be removed, but still exists", databasePath+ext)
+ }
+ }
}
func TestDatabaseInitializationErrorReturnsMigrationGuidance(t *testing.T) {
diff --git a/frontend/src/views/components/HomeAside/HomeAside.vue b/frontend/src/views/components/HomeAside/HomeAside.vue
index 866a949d..ce18f38b 100644
--- a/frontend/src/views/components/HomeAside/HomeAside.vue
+++ b/frontend/src/views/components/HomeAside/HomeAside.vue
@@ -57,7 +57,7 @@
{{ lang.settings }}
- V2.10.6
+ V2.10.7