全新部署自动清空三种数据库数据并升级版本至V2.10.7
This commit is contained in:
parent
33310090fe
commit
73c13cbdf5
@ -434,10 +434,16 @@ func prepareSQLiteDatabase(dsn string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
directory := filepath.Dir(path)
|
directory := filepath.Dir(path)
|
||||||
if directory == "." || directory == "" {
|
if directory != "." && directory != "" {
|
||||||
return nil
|
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) {
|
func buildMySQLDSN(settings DatabaseSettings, previousDSN string) (string, error) {
|
||||||
@ -582,6 +588,26 @@ func (database *sqlSetupDatabase) TableCountContext(ctx stdcontext.Context, quer
|
|||||||
return count, nil
|
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) {
|
var openMySQLSetupDatabase = func(dsn string) (setupDatabase, error) {
|
||||||
database, err := sql.Open("mysql", dsn)
|
database, err := sql.Open("mysql", dsn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -616,7 +642,7 @@ func ensurePostgresDatabase(dsn string) error {
|
|||||||
}
|
}
|
||||||
defer func() { _ = target.Close() }()
|
defer func() { _ = target.Close() }()
|
||||||
if err = target.PingContext(requestContext); err == nil {
|
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 {
|
} else {
|
||||||
var postgresErr *pq.Error
|
var postgresErr *pq.Error
|
||||||
if !stderrors.As(err, &postgresErr) || string(postgresErr.Code) != "3D000" {
|
if !stderrors.As(err, &postgresErr) || string(postgresErr.Code) != "3D000" {
|
||||||
@ -652,16 +678,45 @@ type setupDatabase interface {
|
|||||||
PingContext(stdcontext.Context) error
|
PingContext(stdcontext.Context) error
|
||||||
ExecContext(stdcontext.Context, string, ...any) (sql.Result, error)
|
ExecContext(stdcontext.Context, string, ...any) (sql.Result, error)
|
||||||
TableCountContext(stdcontext.Context, string) (int, error)
|
TableCountContext(stdcontext.Context, string) (int, error)
|
||||||
|
TableNamesContext(stdcontext.Context, string) ([]string, error)
|
||||||
Close() error
|
Close() error
|
||||||
}
|
}
|
||||||
|
|
||||||
func requireEmptyExternalDatabase(database setupDatabase, ctx stdcontext.Context, query string) error {
|
func clearPostgresDatabase(database setupDatabase, ctx stdcontext.Context) error {
|
||||||
count, err := database.TableCountContext(ctx, query)
|
count, err := database.TableCountContext(ctx, "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema NOT IN ('pg_catalog', 'information_schema')")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return databaseError("检查外部数据库是否为空失败", err)
|
return databaseError("检查 PostgreSQL 数据库表失败", err)
|
||||||
}
|
}
|
||||||
if count > 0 {
|
if count == 0 {
|
||||||
return databaseError("全新部署要求外部数据库为空,请使用新数据库或先清理 Smail 专属数据库", nil)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
@ -684,7 +739,7 @@ func ensureMySQLDatabase(dsn string) error {
|
|||||||
}
|
}
|
||||||
defer func() { _ = target.Close() }()
|
defer func() { _ = target.Close() }()
|
||||||
if err = target.PingContext(requestContext); err == nil {
|
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 {
|
} else {
|
||||||
var mysqlErr *mysql.MySQLError
|
var mysqlErr *mysql.MySQLError
|
||||||
if !errors.As(err, &mysqlErr) || mysqlErr.Number != 1049 {
|
if !errors.As(err, &mysqlErr) || mysqlErr.Number != 1049 {
|
||||||
|
|||||||
@ -50,6 +50,8 @@ type fakeSetupDB struct {
|
|||||||
tableCount int
|
tableCount int
|
||||||
tableCountErr error
|
tableCountErr error
|
||||||
tableQueries []string
|
tableQueries []string
|
||||||
|
tableNames []string
|
||||||
|
tableNamesErr error
|
||||||
closed bool
|
closed bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,6 +77,14 @@ func (database *fakeSetupDB) TableCountContext(_ context.Context, query string)
|
|||||||
return database.tableCount, nil
|
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 {
|
func (database *fakeSetupDB) Close() error {
|
||||||
database.closed = true
|
database.closed = true
|
||||||
return nil
|
return nil
|
||||||
@ -293,29 +303,31 @@ func TestEnsureMySQLDatabaseSkipsCreationWhenDatabaseExists(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEnsureMySQLDatabaseRejectsExistingNonEmptyDatabase(t *testing.T) {
|
func TestEnsureMySQLDatabaseClearsExistingNonEmptyDatabase(t *testing.T) {
|
||||||
target := &fakeSetupDB{tableCount: 1}
|
target := &fakeSetupDB{tableNames: []string{"user", "mail"}}
|
||||||
withMySQLSetupOpener(t, func(string) (setupDatabase, error) {
|
withMySQLSetupOpener(t, func(string) (setupDatabase, error) {
|
||||||
return target, nil
|
return target, nil
|
||||||
})
|
})
|
||||||
|
|
||||||
if err := ensureMySQLDatabase(testMySQLDSN(t, "secret")); err == nil {
|
if err := ensureMySQLDatabase(testMySQLDSN(t, "secret")); err != nil {
|
||||||
t.Fatal("non-empty MySQL database was accepted for a fresh deployment")
|
t.Fatalf("expected non-empty MySQL database to be cleared successfully, got: %v", err)
|
||||||
} else if message := DatabaseSetupErrorMessage(err); message == "" {
|
}
|
||||||
t.Fatal("fresh deployment error message was empty")
|
if len(target.execQueries) == 0 {
|
||||||
|
t.Fatal("expected DROP TABLE queries to be executed on MySQL database")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEnsurePostgresDatabaseRejectsExistingNonEmptyDatabase(t *testing.T) {
|
func TestEnsurePostgresDatabaseClearsExistingNonEmptyDatabase(t *testing.T) {
|
||||||
target := &fakeSetupDB{tableCount: 1}
|
target := &fakeSetupDB{tableCount: 2}
|
||||||
withPostgresSetupOpener(t, func(string) (setupDatabase, error) {
|
withPostgresSetupOpener(t, func(string) (setupDatabase, error) {
|
||||||
return target, nil
|
return target, nil
|
||||||
})
|
})
|
||||||
|
|
||||||
if err := ensurePostgresDatabase("postgres://smail:secret@127.0.0.1:5432/smail?sslmode=disable"); err == 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")
|
t.Fatalf("expected non-empty PostgreSQL database to be cleared successfully, got: %v", err)
|
||||||
} else if message := DatabaseSetupErrorMessage(err); message == "" {
|
}
|
||||||
t.Fatal("fresh deployment error message was empty")
|
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) {
|
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 {
|
if err := prepareSQLiteDatabase(databasePath); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if _, err := os.Stat(filepath.Dir(databasePath)); err != nil {
|
if _, err := os.Stat(filepath.Dir(databasePath)); err != nil {
|
||||||
t.Fatalf("SQLite parent directory was not prepared: %v", err)
|
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) {
|
func TestDatabaseInitializationErrorReturnsMigrationGuidance(t *testing.T) {
|
||||||
|
|||||||
@ -57,7 +57,7 @@
|
|||||||
<el-icon><Setting /></el-icon>
|
<el-icon><Setting /></el-icon>
|
||||||
<span class="settings-label">{{ lang.settings }}</span>
|
<span class="settings-label">{{ lang.settings }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="app-version-badge">V2.10.6</span>
|
<span class="app-version-badge">V2.10.7</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user