全新部署自动清空三种数据库数据并升级版本至V2.10.7

This commit is contained in:
smail 2026-10-17 12:06:21 +08:00
parent 33310090fe
commit 73c13cbdf5
3 changed files with 105 additions and 24 deletions

View File

@ -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 {

View File

@ -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) {

View File

@ -57,7 +57,7 @@
<el-icon><Setting /></el-icon>
<span class="settings-label">{{ lang.settings }}</span>
</div>
<span class="app-version-badge">V2.10.6</span>
<span class="app-version-badge">V2.10.7</span>
</button>
</div>
</div>