全新部署自动清空三种数据库数据并升级版本至V2.10.7
This commit is contained in:
parent
33310090fe
commit
73c13cbdf5
@ -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 {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user