5 Commity 44f2f426d8 ... 4e928a1ce0

Autor SHA1 Wiadomość Data
  MHSanaei 4e928a1ce0 v3.5.0 1 dzień temu
  MHSanaei e211a5cc47 feat(frontend): hide redundant migration download on sqlite panels 1 dzień temu
  MHSanaei 77dffe9a85 feat(server): sniff sqlite panel restore uploads and keep the fallback on failure 1 dzień temu
  MHSanaei 54fc0fd47c fix(database): make cross-db migration lossless, transactional, and pre-checked 1 dzień temu
  MHSanaei 30b611614b feat: import SQLite migration dumps through the PostgreSQL panel restore 1 dzień temu

+ 2 - 2
frontend/public/openapi.json

@@ -4100,7 +4100,7 @@
         "tags": [
           "Server"
         ],
-        "summary": "Stream the SQLite database file as an attachment. Use as a manual backup.",
+        "summary": "Stream a full database backup as an attachment: the SQLite .db file on SQLite panels, or a pg_dump custom-format archive (.dump) on PostgreSQL panels. Use as a manual backup.",
         "operationId": "get_panel_api_server_getDb",
         "responses": {
           "200": {
@@ -4844,7 +4844,7 @@
         "tags": [
           "Server"
         ],
-        "summary": "Restore the panel DB from an uploaded SQLite file (multipart form, field name \"db\"). The panel restarts after restore. Destructive.",
+        "summary": "Restore the panel DB from an uploaded backup (multipart form, field name \"db\"). SQLite panels accept a SQLite database (.db) or a SQLite migration dump (.dump); PostgreSQL panels accept a pg_dump archive (.dump), a SQLite database (.db), or a SQLite migration dump. The panel restarts after restore. Destructive.",
         "operationId": "post_panel_api_server_importDB",
         "responses": {
           "200": {

+ 3 - 3
frontend/src/pages/api-docs/endpoints.ts

@@ -338,7 +338,7 @@ export const sections: readonly Section[] = [
       {
         method: 'GET',
         path: '/panel/api/server/getDb',
-        summary: 'Stream the SQLite database file as an attachment. Use as a manual backup.',
+        summary: 'Stream a full database backup as an attachment: the SQLite .db file on SQLite panels, or a pg_dump custom-format archive (.dump) on PostgreSQL panels. Use as a manual backup.',
       },
       {
         method: 'GET',
@@ -468,9 +468,9 @@ export const sections: readonly Section[] = [
       {
         method: 'POST',
         path: '/panel/api/server/importDB',
-        summary: 'Restore the panel DB from an uploaded SQLite file (multipart form, field name "db"). The panel restarts after restore. Destructive.',
+        summary: 'Restore the panel DB from an uploaded backup (multipart form, field name "db"). SQLite panels accept a SQLite database (.db) or a SQLite migration dump (.dump); PostgreSQL panels accept a pg_dump archive (.dump), a SQLite database (.db), or a SQLite migration dump. The panel restarts after restore. Destructive.',
         params: [
-          { name: 'db', in: 'body (multipart)', type: 'file', desc: 'SQLite database file to upload.' },
+          { name: 'db', in: 'body (multipart)', type: 'file', desc: 'Database backup or migration file to upload.' },
         ],
       },
       {

+ 8 - 8
frontend/src/pages/index/BackupModal.tsx

@@ -32,7 +32,7 @@ export default function BackupModal({ open, basePath: _basePath, onClose, onBusy
   function importDb() {
     const fileInput = document.createElement('input');
     fileInput.type = 'file';
-    fileInput.accept = isPostgres ? '.dump' : '.db';
+    fileInput.accept = '.dump,.db';
     fileInput.addEventListener('change', async (e) => {
       const dbFile = (e.target as HTMLInputElement).files?.[0];
       if (!dbFile) return;
@@ -86,15 +86,15 @@ export default function BackupModal({ open, basePath: _basePath, onClose, onBusy
           <Button type="primary" aria-label={t('pages.index.exportDatabase')} onClick={exportDb} icon={<DownloadOutlined />} />
         </div>
 
-        <div className="backup-item">
-          <div className="backup-meta">
-            <div className="backup-title">{t('pages.index.migrationDownload')}</div>
-            <div className="backup-description">
-              {isPostgres ? t('pages.index.migrationDownloadPgDesc') : t('pages.index.migrationDownloadDesc')}
+        {isPostgres && (
+          <div className="backup-item">
+            <div className="backup-meta">
+              <div className="backup-title">{t('pages.index.migrationDownload')}</div>
+              <div className="backup-description">{t('pages.index.migrationDownloadPgDesc')}</div>
             </div>
+            <Button type="primary" aria-label={t('pages.index.migrationDownload')} onClick={exportMigration} icon={<DownloadOutlined />} />
           </div>
-          <Button type="primary" aria-label={t('pages.index.migrationDownload')} onClick={exportMigration} icon={<DownloadOutlined />} />
-        </div>
+        )}
 
         <div className="backup-item">
           <div className="backup-meta">

+ 1 - 1
internal/config/version

@@ -1 +1 @@
-3.4.2
+3.5.0

+ 10 - 6
internal/database/db.go

@@ -56,8 +56,8 @@ const (
 	defaultPassword = "admin"
 )
 
-func initModels() error {
-	models := []any{
+func allModels() []any {
+	return []any{
 		&model.User{},
 		&model.Inbound{},
 		&model.OutboundTraffics{},
@@ -78,12 +78,16 @@ func initModels() error {
 		&model.ClientGlobalTraffic{},
 		&model.OutboundSubscription{},
 	}
+}
+
+func initModels() error {
+	models := allModels()
 	for _, mdl := range models {
 		if IsPostgres() && postgresModelSettled(mdl) {
 			continue
 		}
 		if err := db.AutoMigrate(mdl); err != nil {
-			if isIgnorableDuplicateColumnErr(err, mdl) {
+			if isIgnorableDuplicateColumnErr(db, err, mdl) {
 				log.Printf("Ignoring duplicate column during auto migration for %T: %v", mdl, err)
 				continue
 			}
@@ -999,7 +1003,7 @@ func dedupeInboundSettingsClients() error {
 	return nil
 }
 
-func isIgnorableDuplicateColumnErr(err error, mdl any) bool {
+func isIgnorableDuplicateColumnErr(gdb *gorm.DB, err error, mdl any) bool {
 	if err == nil {
 		return false
 	}
@@ -1009,14 +1013,14 @@ func isIgnorableDuplicateColumnErr(err error, mdl any) bool {
 	if _, after, ok := strings.Cut(errMsg, sqlitePrefix); ok {
 		col := strings.TrimSpace(after)
 		col = strings.Trim(col, "`\"[]")
-		return col != "" && db != nil && db.Migrator().HasColumn(mdl, col)
+		return col != "" && gdb != nil && gdb.Migrator().HasColumn(mdl, col)
 	}
 	if strings.Contains(errMsg, "already exists") && strings.Contains(errMsg, "column ") {
 		if _, after, ok := strings.Cut(errMsg, "column \""); ok {
 			rest := after
 			if e := strings.Index(rest, "\""); e > 0 {
 				col := rest[:e]
-				return col != "" && db != nil && db.Migrator().HasColumn(mdl, col)
+				return col != "" && gdb != nil && gdb.Migrator().HasColumn(mdl, col)
 			}
 		}
 	}

+ 66 - 24
internal/database/migrate_data.go

@@ -25,7 +25,8 @@ import (
 // related tests.
 //
 // Important: When adding a new top-level model (like OutboundSubscription),
-// you must add it here **in addition to** the list in internal/database/db.go:initModels().
+// you must add it here **in addition to** allModels() in internal/database/db.go;
+// TestMigrationModelsMatchPanelModels fails when the two lists drift apart.
 // This list is used for:
 //   - Creating the destination schema during cross-DB migration
 //   - Truncating tables
@@ -48,17 +49,21 @@ func migrationModels() []any {
 		&model.ClientRecord{},
 		&model.ClientInbound{},
 		&model.ClientExternalLink{},
+		&model.ClientGroup{},
 		&model.InboundFallback{},
 		&model.Host{},
 		&model.NodeClientTraffic{},
 		&model.NodeClientIp{},
+		&model.ClientGlobalTraffic{},
 		&model.OutboundSubscription{},
 	}
 }
 
 // MigrateData copies every row from the configured SQLite file at srcPath into
 // a fresh PostgreSQL database described by dstDSN. The destination tables are
-// (re)created with AutoMigrate before the copy. Source data is left untouched.
+// (re)created with AutoMigrate; truncate and copy then run in one transaction,
+// so a failed migration leaves the destination data unchanged. Source data is
+// left untouched.
 func MigrateData(srcPath, dstDSN string) error {
 	if _, err := os.Stat(srcPath); err != nil {
 		return fmt.Errorf("source sqlite not found at %s: %w", srcPath, err)
@@ -100,33 +105,41 @@ func MigrateData(srcPath, dstDSN string) error {
 		}
 	}
 
-	// AutoMigrate re-creates the legacy client_traffics -> inbounds foreign key,
-	// but the running panel drops it (see dropLegacyForeignKeys) and tolerates
-	// client_traffics rows whose inbound was deleted. Drop it here too so copying
-	// such orphaned rows can't fail with an fk_inbounds_client_stats violation.
-	if err := dst.Exec("ALTER TABLE client_traffics DROP CONSTRAINT IF EXISTS fk_inbounds_client_stats").Error; err != nil {
-		return fmt.Errorf("drop legacy foreign key: %w", err)
-	}
+	totalRows := 0
+	txErr := dst.Transaction(func(tx *gorm.DB) error {
+		// AutoMigrate re-creates the legacy client_traffics -> inbounds foreign key,
+		// but the running panel drops it (see dropLegacyForeignKeys) and tolerates
+		// client_traffics rows whose inbound was deleted. Drop it here too so copying
+		// such orphaned rows can't fail with an fk_inbounds_client_stats violation.
+		if err := tx.Exec("ALTER TABLE client_traffics DROP CONSTRAINT IF EXISTS fk_inbounds_client_stats").Error; err != nil {
+			return fmt.Errorf("drop legacy foreign key: %w", err)
+		}
 
-	// Empty the destination tables so the migration is idempotent: a fresh
-	// PostgreSQL DB already holds an auto-seeded admin (id=1) from any prior
-	// panel start, and a partially-failed earlier run leaves rows behind. Either
-	// way a plain INSERT with explicit ids would collide on users_pkey, so clear
-	// our tables (only) before copying.
-	if err := truncatePostgresTables(dst, migrationModels()); err != nil {
-		return fmt.Errorf("clear destination tables: %w", err)
-	}
+		// Empty the destination tables before copying: a fresh PostgreSQL DB
+		// already holds an auto-seeded admin (id=1) from any prior panel start,
+		// so a plain INSERT with explicit ids would collide on users_pkey. Only
+		// the panel's own tables are cleared, and a failure anywhere in this
+		// transaction rolls the clear back with everything else.
+		if err := truncatePostgresTables(tx, migrationModels()); err != nil {
+			return fmt.Errorf("clear destination tables: %w", err)
+		}
 
-	totalRows := 0
-	for _, m := range migrationModels() {
-		n, err := copyTable(src, dst, m)
-		if err != nil {
-			return fmt.Errorf("copy %T: %w", m, err)
+		for _, m := range migrationModels() {
+			n, err := copyTable(src, tx, m)
+			if err != nil {
+				return fmt.Errorf("copy %T: %w", m, err)
+			}
+			totalRows += n
+			log.Printf("  %-32s %d rows", reflect.TypeOf(m).Elem().Name(), n)
 		}
-		totalRows += n
-		log.Printf("  %-32s %d rows", reflect.TypeOf(m).Elem().Name(), n)
+		return nil
+	})
+	if txErr != nil {
+		return txErr
 	}
 
+	// setval is never rolled back by PostgreSQL, so sequences are resynced only
+	// after the transaction has committed.
 	if err := resetPostgresSequences(dst); err != nil {
 		log.Printf("warning: failed to reset some postgres sequences: %v", err)
 	}
@@ -301,3 +314,32 @@ func tableWithIdColumn(db *gorm.DB, m any) (string, bool) {
 	}
 	return stmt.Table, true
 }
+
+// PrepareSQLiteForMigration rejects SQLite files that are not a panel database
+// before the caller causes any downtime, then AutoMigrates the panel schema
+// onto the file so backups from older versions gain the newer tables and
+// columns the row copy reads. Data-level upgrades are not needed here: they
+// run dialect-agnostically on the destination via InitDB after the import.
+func PrepareSQLiteForMigration(dbPath string) error {
+	gdb, err := gorm.Open(sqlite.Open(dbPath+"?_busy_timeout=10000"), &gorm.Config{Logger: logger.Discard})
+	if err != nil {
+		return err
+	}
+	sqlDB, err := gdb.DB()
+	if err != nil {
+		return err
+	}
+	defer sqlDB.Close()
+
+	for _, table := range []string{"users", "settings", "inbounds"} {
+		if !sqliteTableExists(sqlDB, table) {
+			return fmt.Errorf("not a 3x-ui panel database: required table %q is missing", table)
+		}
+	}
+	for _, m := range migrationModels() {
+		if err := gdb.AutoMigrate(m); err != nil && !isIgnorableDuplicateColumnErr(gdb, err, m) {
+			return fmt.Errorf("upgrade panel schema for %T: %w", m, err)
+		}
+	}
+	return nil
+}

+ 70 - 0
internal/database/migrate_data_test.go

@@ -137,3 +137,73 @@ func TestMigrateData_PreservesFalseDefaultedColumns(t *testing.T) {
 		t.Fatalf("disabled node re-enabled after migration")
 	}
 }
+
+func TestMigrateData_FailedCopyLeavesDestinationUntouched(t *testing.T) {
+	dsn := os.Getenv("XUI_TEST_PG_DSN")
+	if dsn == "" {
+		t.Skip("set XUI_TEST_PG_DSN to a reachable Postgres to run this test")
+	}
+
+	seedSource := func(username string) string {
+		t.Helper()
+		srcPath := t.TempDir() + "/x-ui.db"
+		src, err := gorm.Open(sqlite.Open(srcPath), &gorm.Config{Logger: logger.Discard})
+		if err != nil {
+			t.Fatalf("open sqlite: %v", err)
+		}
+		for _, m := range migrationModels() {
+			if err := src.AutoMigrate(m); err != nil {
+				t.Fatalf("automigrate %T: %v", m, err)
+			}
+		}
+		if err := src.Create(&model.User{Username: username, Password: "pw"}).Error; err != nil {
+			t.Fatalf("seed user: %v", err)
+		}
+		if sqlDB, err := src.DB(); err == nil {
+			sqlDB.Close()
+		}
+		return srcPath
+	}
+
+	dst, err := gorm.Open(postgres.Open(dsn), &gorm.Config{Logger: logger.Discard})
+	if err != nil {
+		t.Fatalf("open postgres: %v", err)
+	}
+	if err := dst.Migrator().DropTable(migrationModels()...); err != nil {
+		t.Fatalf("drop tables: %v", err)
+	}
+
+	if err := MigrateData(seedSource("keep-me"), dsn); err != nil {
+		t.Fatalf("seed destination via MigrateData: %v", err)
+	}
+
+	brokenSrc := seedSource("evil")
+	breaker, err := gorm.Open(sqlite.Open(brokenSrc), &gorm.Config{Logger: logger.Discard})
+	if err != nil {
+		t.Fatalf("reopen broken source: %v", err)
+	}
+	if err := breaker.Exec("DROP TABLE outbound_subscriptions").Error; err != nil {
+		t.Fatalf("drop table from broken source: %v", err)
+	}
+	if sqlDB, err := breaker.DB(); err == nil {
+		sqlDB.Close()
+	}
+
+	if err := MigrateData(brokenSrc, dsn); err == nil {
+		t.Fatal("MigrateData succeeded on a source missing outbound_subscriptions, want error")
+	}
+
+	var keepMe, evil int64
+	if err := dst.Model(&model.User{}).Where("username = ?", "keep-me").Count(&keepMe).Error; err != nil {
+		t.Fatalf("count keep-me: %v", err)
+	}
+	if err := dst.Model(&model.User{}).Where("username = ?", "evil").Count(&evil).Error; err != nil {
+		t.Fatalf("count evil: %v", err)
+	}
+	if keepMe != 1 {
+		t.Fatalf("previous destination data lost after failed migration: keep-me count = %d, want 1", keepMe)
+	}
+	if evil != 0 {
+		t.Fatalf("failed migration leaked partial rows: evil count = %d, want 0", evil)
+	}
+}

+ 29 - 0
internal/database/migration_models_test.go

@@ -0,0 +1,29 @@
+package database
+
+import (
+	"reflect"
+	"testing"
+)
+
+func TestMigrationModelsMatchPanelModels(t *testing.T) {
+	names := func(models []any) map[string]bool {
+		set := make(map[string]bool, len(models))
+		for _, m := range models {
+			set[reflect.TypeOf(m).Elem().Name()] = true
+		}
+		return set
+	}
+	panel := names(allModels())
+	migration := names(migrationModels())
+
+	for name := range panel {
+		if !migration[name] {
+			t.Errorf("model %s is in allModels but missing from migrationModels: cross-db migration silently drops its rows", name)
+		}
+	}
+	for name := range migration {
+		if !panel[name] {
+			t.Errorf("model %s is in migrationModels but missing from allModels: its table never exists on a live panel", name)
+		}
+	}
+}

+ 66 - 0
internal/database/prepare_sqlite_test.go

@@ -0,0 +1,66 @@
+package database
+
+import (
+	"path/filepath"
+	"strings"
+	"testing"
+
+	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
+
+	"gorm.io/driver/sqlite"
+	"gorm.io/gorm"
+	"gorm.io/gorm/logger"
+)
+
+func TestPrepareSQLiteForMigration(t *testing.T) {
+	t.Run("rejects non-panel database", func(t *testing.T) {
+		dbPath := filepath.Join(t.TempDir(), "random.db")
+		gdb, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{Logger: logger.Discard})
+		if err != nil {
+			t.Fatalf("open sqlite: %v", err)
+		}
+		if err := gdb.Exec("CREATE TABLE notes(id integer primary key, body text)").Error; err != nil {
+			t.Fatalf("create table: %v", err)
+		}
+		closeGorm(gdb)
+
+		err = PrepareSQLiteForMigration(dbPath)
+		if err == nil {
+			t.Fatal("PrepareSQLiteForMigration accepted a non-panel database, want error")
+		}
+		if !strings.Contains(err.Error(), "not a 3x-ui panel database") {
+			t.Fatalf("error = %q, want it to contain %q", err.Error(), "not a 3x-ui panel database")
+		}
+	})
+
+	t.Run("upgrades old panel schema", func(t *testing.T) {
+		dbPath := filepath.Join(t.TempDir(), "old.db")
+		gdb, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{Logger: logger.Discard})
+		if err != nil {
+			t.Fatalf("open sqlite: %v", err)
+		}
+		if err := gdb.AutoMigrate(&model.User{}, &model.Setting{}, &model.Inbound{}); err != nil {
+			t.Fatalf("automigrate legacy subset: %v", err)
+		}
+		closeGorm(gdb)
+
+		if err := PrepareSQLiteForMigration(dbPath); err != nil {
+			t.Fatalf("PrepareSQLiteForMigration rejected an old panel database: %v", err)
+		}
+
+		check, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{Logger: logger.Discard})
+		if err != nil {
+			t.Fatalf("reopen sqlite: %v", err)
+		}
+		defer closeGorm(check)
+		sqlDB, err := check.DB()
+		if err != nil {
+			t.Fatalf("sql db: %v", err)
+		}
+		for _, table := range []string{"client_groups", "client_global_traffics", "outbound_subscriptions"} {
+			if !sqliteTableExists(sqlDB, table) {
+				t.Errorf("table %s was not created by the schema upgrade", table)
+			}
+		}
+	})
+}

+ 180 - 53
internal/web/service/server.go

@@ -10,6 +10,7 @@ import (
 	"encoding/hex"
 	"encoding/json"
 	"encoding/pem"
+	"errors"
 	"fmt"
 	"io"
 	"mime/multipart"
@@ -1425,44 +1426,26 @@ func (s *ServerService) ImportDB(file multipart.File) error {
 	if database.IsPostgres() {
 		return s.importPostgresDB(file)
 	}
-	// Check if the file is a SQLite database
-	isValidDb, err := database.IsSQLiteDB(file)
+	kind, err := sniffUploadKind(file)
 	if err != nil {
-		return common.NewErrorf("Error checking db file format: %v", err)
+		return common.NewErrorf("Error reading uploaded file: %v", err)
 	}
-	if !isValidDb {
-		return common.NewError("Invalid db file format")
-	}
-
-	// Reset the file reader to the beginning
-	_, err = file.Seek(0, 0)
-	if err != nil {
-		return common.NewErrorf("Error resetting file reader: %v", err)
+	switch kind {
+	case importKindSQLiteDB, importKindSQLiteDump:
+	case importKindPgDump:
+		return common.NewError("This file is a PostgreSQL backup; it can only be restored on a panel running PostgreSQL")
+	default:
+		return common.NewError("Invalid file: expected a SQLite database (.db) from Back Up or a SQLite migration dump (.dump)")
 	}
 
-	// Save the file as a temporary file
 	tempPath := fmt.Sprintf("%s.temp", config.GetDBPath())
 
-	// Remove the existing temporary file (if any)
 	if _, err := os.Stat(tempPath); err == nil {
 		if errRemove := os.Remove(tempPath); errRemove != nil {
 			return common.NewErrorf("Error removing existing temporary db file: %v", errRemove)
 		}
 	}
-
-	// Create the temporary file
-	tempFile, err := os.Create(tempPath)
-	if err != nil {
-		return common.NewErrorf("Error creating temporary db file: %v", err)
-	}
-
-	// Robust deferred cleanup for the temporary file
 	defer func() {
-		if tempFile != nil {
-			if cerr := tempFile.Close(); cerr != nil {
-				logger.Warningf("Warning: failed to close temp file: %v", cerr)
-			}
-		}
 		if _, err := os.Stat(tempPath); err == nil {
 			if rerr := os.Remove(tempPath); rerr != nil {
 				logger.Warningf("Warning: failed to remove temp file: %v", rerr)
@@ -1470,21 +1453,16 @@ func (s *ServerService) ImportDB(file multipart.File) error {
 		}
 	}()
 
-	// Save uploaded file to temporary file
-	if _, err = io.Copy(tempFile, file); err != nil {
-		return common.NewErrorf("Error saving db: %v", err)
-	}
-
-	// Close temp file before opening via sqlite
-	if err = tempFile.Close(); err != nil {
-		return common.NewErrorf("Error closing temporary db file: %v", err)
+	if err := stageSQLiteUpload(file, kind, tempPath); err != nil {
+		return err
 	}
-	tempFile = nil
 
-	// Validate integrity (no migrations / side effects)
 	if err = database.ValidateSQLiteDB(tempPath); err != nil {
 		return common.NewErrorf("Invalid or corrupt db file: %v", err)
 	}
+	if err = database.PrepareSQLiteForMigration(tempPath); err != nil {
+		return common.NewErrorf("This file cannot be imported: %v", err)
+	}
 
 	xrayStopped := true
 	defer func() {
@@ -1502,6 +1480,19 @@ func (s *ServerService) ImportDB(file multipart.File) error {
 		logger.Warningf("Failed to close existing DB before replacement: %v", errClose)
 	}
 
+	// Registered after the xray-restart defer so it runs first (LIFO): every
+	// error return below leaves a database file at the configured path, and the
+	// restart needs an open pool to build the xray config from it.
+	dbReopened := false
+	defer func() {
+		if dbReopened {
+			return
+		}
+		if errReopen := database.InitDB(config.GetDBPath()); errReopen != nil {
+			logger.Warningf("Failed to reopen the database after import error: %v", errReopen)
+		}
+	}()
+
 	// Backup the current database for fallback
 	fallbackPath := fmt.Sprintf("%s.backup", config.GetDBPath())
 
@@ -1517,15 +1508,6 @@ func (s *ServerService) ImportDB(file multipart.File) error {
 		return common.NewErrorf("Error backing up current db file: %v", err)
 	}
 
-	// Defer fallback cleanup ONLY if everything goes well
-	defer func() {
-		if _, err := os.Stat(fallbackPath); err == nil {
-			if rerr := os.Remove(fallbackPath); rerr != nil {
-				logger.Warningf("Warning: failed to remove fallback file: %v", rerr)
-			}
-		}
-	}()
-
 	// Move temp to DB path
 	if err = os.Rename(tempPath, config.GetDBPath()); err != nil {
 		// Restore from fallback
@@ -1537,19 +1519,30 @@ func (s *ServerService) ImportDB(file multipart.File) error {
 
 	// Open & migrate new DB
 	if err = database.InitDB(config.GetDBPath()); err != nil {
+		// A failed InitDB still holds the imported file open; close before the
+		// rename or Windows refuses to replace it.
+		if errClose := database.CloseDB(); errClose != nil {
+			logger.Warningf("Failed to close the imported DB before restoring fallback: %v", errClose)
+		}
 		if errRename := os.Rename(fallbackPath, config.GetDBPath()); errRename != nil {
 			return common.NewErrorf("Error migrating db and restoring fallback: %v", errRename)
 		}
 		return common.NewErrorf("Error migrating db: %v", err)
 	}
+	dbReopened = true
 
 	s.inboundService.MigrateDB()
 
 	xrayStopped = false
 	if err = s.RestartXrayService(); err != nil {
-		return common.NewErrorf("Imported DB but failed to start Xray: %v", err)
+		return common.NewErrorf("Imported DB but failed to start Xray: %v; the previous database was kept at %s", err, fallbackPath)
 	}
 
+	if _, err := os.Stat(fallbackPath); err == nil {
+		if rerr := os.Remove(fallbackPath); rerr != nil {
+			logger.Warningf("Warning: failed to remove fallback file: %v", rerr)
+		}
+	}
 	return nil
 }
 
@@ -1657,18 +1650,59 @@ func parsePgToolVersion(versionOutput string) string {
 	return pgToolVersionPattern.FindString(versionOutput)
 }
 
-func (s *ServerService) importPostgresDB(file multipart.File) error {
-	header := make([]byte, 5)
-	if _, err := file.ReadAt(header, 0); err != nil {
-		return common.NewErrorf("Error reading dump file: %v", err)
+const (
+	importKindUnknown = iota
+	importKindPgDump
+	importKindSQLiteDB
+	importKindSQLiteDump
+)
+
+// sniffImportKind classifies an uploaded restore file by its leading bytes:
+// a pg_dump custom archive, a raw SQLite database, or a SQLite SQL text dump.
+func sniffImportKind(header []byte) int {
+	if bytes.HasPrefix(header, []byte("PGDMP")) {
+		return importKindPgDump
 	}
-	if string(header) != "PGDMP" {
-		return common.NewError("Invalid file: expected a PostgreSQL custom-format dump (.dump) created by this panel's Back Up")
+	if bytes.HasPrefix(header, []byte("SQLite format 3\x00")) {
+		return importKindSQLiteDB
+	}
+	text := bytes.TrimLeft(bytes.TrimPrefix(header, []byte("\xef\xbb\xbf")), " \t\r\n")
+	if bytes.HasPrefix(text, []byte("PRAGMA")) || bytes.HasPrefix(text, []byte("BEGIN TRANSACTION")) {
+		return importKindSQLiteDump
+	}
+	return importKindUnknown
+}
+
+func sniffUploadKind(file multipart.File) (int, error) {
+	header := make([]byte, 64)
+	n, err := file.ReadAt(header, 0)
+	if err != nil && !errors.Is(err, io.EOF) {
+		return importKindUnknown, err
 	}
 	if _, err := file.Seek(0, 0); err != nil {
-		return common.NewErrorf("Error resetting file reader: %v", err)
+		return importKindUnknown, err
+	}
+	return sniffImportKind(header[:n]), nil
+}
+
+func (s *ServerService) importPostgresDB(file multipart.File) error {
+	kind, err := sniffUploadKind(file)
+	if err != nil {
+		return common.NewErrorf("Error reading uploaded file: %v", err)
+	}
+	switch kind {
+	case importKindPgDump:
+		return s.restorePostgresDump(file)
+	case importKindSQLiteDB:
+		return s.migrateSQLiteIntoPostgres(file, false)
+	case importKindSQLiteDump:
+		return s.migrateSQLiteIntoPostgres(file, true)
+	default:
+		return common.NewError("Invalid file: expected a PostgreSQL custom-format dump (.dump) from this panel's Back Up, a SQLite database (.db), or a SQLite migration dump")
 	}
+}
 
+func (s *ServerService) restorePostgresDump(file multipart.File) error {
 	bin, err := exec.LookPath("pg_restore")
 	if err != nil {
 		return common.NewError("pg_restore not found on the server; install the postgresql-client package to restore a PostgreSQL database")
@@ -1737,6 +1771,99 @@ func (s *ServerService) importPostgresDB(file multipart.File) error {
 	return nil
 }
 
+func (s *ServerService) migrateSQLiteIntoPostgres(file multipart.File, isSQLDump bool) error {
+	tempDir, err := os.MkdirTemp("", "x-ui-pg-migrate-*")
+	if err != nil {
+		return common.NewErrorf("Error creating temporary folder: %v", err)
+	}
+	defer os.RemoveAll(tempDir)
+
+	uploadPath := filepath.Join(tempDir, "upload.db")
+	if isSQLDump {
+		uploadPath = filepath.Join(tempDir, "upload.dump")
+	}
+	if err := saveUploadedFile(file, uploadPath); err != nil {
+		return common.NewErrorf("Error saving uploaded file: %v", err)
+	}
+
+	dbPath := uploadPath
+	if isSQLDump {
+		dbPath = filepath.Join(tempDir, "restored.db")
+		if err := database.RestoreSQLite(uploadPath, dbPath); err != nil {
+			return common.NewErrorf("Error rebuilding a SQLite database from the migration dump: %v", err)
+		}
+	}
+	if err := database.ValidateSQLiteDB(dbPath); err != nil {
+		return common.NewErrorf("Invalid or corrupt db file: %v", err)
+	}
+	if err := database.PrepareSQLiteForMigration(dbPath); err != nil {
+		return common.NewErrorf("This file cannot be imported: %v", err)
+	}
+
+	xrayStopped := true
+	defer func() {
+		if xrayStopped {
+			if errR := s.RestartXrayService(); errR != nil {
+				logger.Warningf("Failed to restart Xray after DB restore error: %v", errR)
+			}
+		}
+	}()
+	if errStop := s.StopXrayService(); errStop != nil {
+		logger.Warningf("Failed to stop Xray before DB restore: %v", errStop)
+	}
+
+	if errClose := database.CloseDB(); errClose != nil {
+		logger.Warningf("Failed to close existing DB before restore: %v", errClose)
+	}
+
+	migrateErr := database.MigrateData(dbPath, config.GetDBDSN())
+
+	if errInit := database.InitDB(config.GetDBPath()); errInit != nil {
+		return common.NewErrorf("Restore finished but reopening the database failed: %v", errInit)
+	}
+	s.inboundService.MigrateDB()
+
+	if migrateErr != nil {
+		return common.NewErrorf("Importing the SQLite data into PostgreSQL failed: %v; the import runs in a single transaction, so the database was left unchanged", migrateErr)
+	}
+
+	xrayStopped = false
+	if err := s.RestartXrayService(); err != nil {
+		return common.NewErrorf("Restored DB but failed to start Xray: %v", err)
+	}
+	return nil
+}
+
+func saveUploadedFile(file multipart.File, dstPath string) error {
+	dst, err := os.Create(dstPath)
+	if err != nil {
+		return err
+	}
+	if _, err := io.Copy(dst, file); err != nil {
+		dst.Close()
+		return err
+	}
+	return dst.Close()
+}
+
+func stageSQLiteUpload(file multipart.File, kind int, tempPath string) error {
+	if kind == importKindSQLiteDump {
+		dumpPath := tempPath + ".dump"
+		defer os.Remove(dumpPath)
+		if err := saveUploadedFile(file, dumpPath); err != nil {
+			return common.NewErrorf("Error saving migration dump: %v", err)
+		}
+		if err := database.RestoreSQLite(dumpPath, tempPath); err != nil {
+			return common.NewErrorf("Error rebuilding a SQLite database from the migration dump: %v", err)
+		}
+		return nil
+	}
+	if err := saveUploadedFile(file, tempPath); err != nil {
+		return common.NewErrorf("Error saving db: %v", err)
+	}
+	return nil
+}
+
 // IsValidGeofileName validates that the filename is safe for geofile operations.
 // It checks for path traversal attempts and ensures the filename contains only safe characters.
 func (s *ServerService) IsValidGeofileName(filename string) bool {

+ 45 - 0
internal/web/service/server_import_sniff_test.go

@@ -0,0 +1,45 @@
+package service
+
+import (
+	"path/filepath"
+	"testing"
+
+	"github.com/mhsanaei/3x-ui/v3/internal/database"
+)
+
+func TestSniffImportKind(t *testing.T) {
+	cases := []struct {
+		name   string
+		header []byte
+		want   int
+	}{
+		{"pg custom archive", []byte("PGDMP\x01\x10\x04"), importKindPgDump},
+		{"raw sqlite database", []byte("SQLite format 3\x00rest of header"), importKindSQLiteDB},
+		{"sqlite cli dump without pragma", []byte("BEGIN TRANSACTION;\nCREATE TABLE t(i);"), importKindSQLiteDump},
+		{"bom and whitespace before pragma", []byte("\xef\xbb\xbf\r\n PRAGMA foreign_keys=OFF;"), importKindSQLiteDump},
+		{"plain-format postgres dump", []byte("--\n-- PostgreSQL database dump\n--"), importKindUnknown},
+		{"empty file", nil, importKindUnknown},
+	}
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			if got := sniffImportKind(tc.header); got != tc.want {
+				t.Errorf("sniffImportKind(%q) = %d, want %d", tc.header, got, tc.want)
+			}
+		})
+	}
+
+	t.Run("panel migration dump", func(t *testing.T) {
+		dbPath := filepath.Join(t.TempDir(), "x-ui.db")
+		if err := database.InitDB(dbPath); err != nil {
+			t.Fatalf("InitDB: %v", err)
+		}
+		t.Cleanup(func() { _ = database.CloseDB() })
+		dump, err := database.DumpSQLiteToBytes(dbPath)
+		if err != nil {
+			t.Fatalf("DumpSQLiteToBytes: %v", err)
+		}
+		if got := sniffImportKind(dump[:64]); got != importKindSQLiteDump {
+			t.Errorf("sniffImportKind(real migration dump) = %d, want %d", got, importKindSQLiteDump)
+		}
+	})
+}

+ 46 - 0
internal/web/service/server_import_stage_test.go

@@ -0,0 +1,46 @@
+package service
+
+import (
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/mhsanaei/3x-ui/v3/internal/database"
+)
+
+func TestStageSQLiteUploadRebuildsFromDump(t *testing.T) {
+	dir := t.TempDir()
+	dbPath := filepath.Join(dir, "x-ui.db")
+	if err := database.InitDB(dbPath); err != nil {
+		t.Fatalf("InitDB: %v", err)
+	}
+	t.Cleanup(func() { _ = database.CloseDB() })
+	dump, err := database.DumpSQLiteToBytes(dbPath)
+	if err != nil {
+		t.Fatalf("DumpSQLiteToBytes: %v", err)
+	}
+
+	uploadPath := filepath.Join(dir, "upload.dump")
+	if err := os.WriteFile(uploadPath, dump, 0o644); err != nil {
+		t.Fatalf("write upload: %v", err)
+	}
+	upload, err := os.Open(uploadPath)
+	if err != nil {
+		t.Fatalf("open upload: %v", err)
+	}
+	defer upload.Close()
+
+	staged := filepath.Join(dir, "x-ui.db.temp")
+	if err := stageSQLiteUpload(upload, importKindSQLiteDump, staged); err != nil {
+		t.Fatalf("stageSQLiteUpload: %v", err)
+	}
+	if _, err := os.Stat(staged + ".dump"); !os.IsNotExist(err) {
+		t.Errorf("intermediate dump file %s.dump was not cleaned up", staged)
+	}
+	if err := database.ValidateSQLiteDB(staged); err != nil {
+		t.Errorf("staged database fails integrity check: %v", err)
+	}
+	if err := database.PrepareSQLiteForMigration(staged); err != nil {
+		t.Errorf("staged database fails the panel pre-flight: %v", err)
+	}
+}

+ 4 - 5
internal/web/translation/ar-EG.json

@@ -261,19 +261,18 @@
       "backup": "نسخ احتياطي",
       "backupTitle": "نسخ احتياطي واستعادة",
       "exportDatabase": "اخزن نسخة",
-      "exportDatabaseDesc": "اضغط عشان تحمل ملف .db يحتوي على نسخة احتياطية لقاعدة البيانات الحالية على جهازك.",
+      "exportDatabaseDesc": "اضغط عشان تحمل ملف .db يحتوي على نسخة احتياطية لقاعدة البيانات الحالية على جهازك. نفس الملف ممكن كمان يترجع على لوحة شغالة بـ PostgreSQL.",
       "importDatabase": "استرجاع",
-      "importDatabaseDesc": "اضغط عشان تختار وتحمل ملف .db من جهازك لاسترجاع قاعدة البيانات من نسخة احتياطية.",
+      "importDatabaseDesc": "اضغط عشان تختار وتحمل نسخة احتياطية .db أو ملف ترحيل (.dump) من جهازك لاسترجاع قاعدة البيانات.",
       "importDatabaseSuccess": "تم استيراد قاعدة البيانات بنجاح",
       "importDatabaseError": "حدث خطأ أثناء استيراد قاعدة البيانات",
       "readDatabaseError": "حدث خطأ أثناء قراءة قاعدة البيانات",
       "getDatabaseError": "حدث خطأ أثناء استرجاع قاعدة البيانات",
       "getConfigError": "حدث خطأ أثناء استرجاع ملف الإعدادات",
-      "backupPostgresNote": "تعمل هذه اللوحة على PostgreSQL. يقوم «النسخ الاحتياطي» بتنزيل أرشيف pg_dump (.dump)، و«الاستعادة» تعيد تحميله عبر pg_restore. يجب أن تكون أدوات عميل PostgreSQL (pg_dump و pg_restore) مثبَّتة على الخادم.",
+      "backupPostgresNote": "تعمل هذه اللوحة على PostgreSQL. يقوم «النسخ الاحتياطي» بتنزيل أرشيف pg_dump (.dump)، و«الاستعادة» تعيد تحميله عبر pg_restore. تقبل «الاستعادة» أيضًا قاعدة بيانات SQLite (.db) أو ملف ترحيل SQLite وتستورد بياناتها إلى PostgreSQL. يجب أن تكون أدوات عميل PostgreSQL (pg_dump و pg_restore) مثبَّتة على الخادم.",
       "exportDatabasePgDesc": "انقر لتنزيل نسخة PostgreSQL (.dump) من قاعدة بياناتك الحالية إلى جهازك.",
-      "importDatabasePgDesc": "انقر لاختيار ورفع ملف .dump لاستعادة قاعدة بيانات PostgreSQL. سيؤدي هذا إلى استبدال جميع البيانات الحالية.",
+      "importDatabasePgDesc": "انقر لاختيار ورفع نسخة احتياطية من PostgreSQL (.dump) أو قاعدة بيانات SQLite (.db) أو ملف ترحيل SQLite لاستعادة قاعدة البيانات. سيؤدي هذا إلى استبدال جميع البيانات الحالية.",
       "migrationDownload": "تنزيل ملف الترحيل",
-      "migrationDownloadDesc": "انقر لتنزيل تصدير .dump محمول (نص SQL) لقاعدة بيانات SQLite الخاصة بك.",
       "migrationDownloadPgDesc": "انقر لتنزيل قاعدة بيانات SQLite بامتداد .db مبنية من بيانات PostgreSQL الخاصة بك، جاهزة لتشغيل هذه اللوحة على SQLite."
     },
     "inbounds": {

+ 4 - 5
internal/web/translation/en-US.json

@@ -261,19 +261,18 @@
       "backup": "Backup",
       "backupTitle": "Backup & Restore",
       "exportDatabase": "Back Up",
-      "exportDatabaseDesc": "Click to download a .db file containing a backup of your current database to your device.",
+      "exportDatabaseDesc": "Click to download a .db file containing a backup of your current database to your device. The same file can also be restored into a panel running on PostgreSQL.",
       "importDatabase": "Restore",
-      "importDatabaseDesc": "Click to select and upload a .db file from your device to restore your database from a backup.",
+      "importDatabaseDesc": "Click to select and upload a .db backup or a migration dump (.dump) from your device to restore your database.",
       "importDatabaseSuccess": "The database has been successfully imported.",
       "importDatabaseError": "An error occurred while importing the database.",
       "readDatabaseError": "An error occurred while reading the database.",
       "getDatabaseError": "An error occurred while retrieving the database.",
       "getConfigError": "An error occurred while retrieving the config file.",
-      "backupPostgresNote": "This panel runs on PostgreSQL. Back Up downloads a pg_dump archive (.dump) and Restore loads it back with pg_restore. The server needs the PostgreSQL client tools (pg_dump and pg_restore) installed.",
+      "backupPostgresNote": "This panel runs on PostgreSQL. Back Up downloads a pg_dump archive (.dump) and Restore loads it back with pg_restore. Restore also accepts a SQLite database (.db) or a SQLite migration dump and imports its data into PostgreSQL. The server needs the PostgreSQL client tools (pg_dump and pg_restore) installed.",
       "exportDatabasePgDesc": "Click to download a PostgreSQL dump (.dump) of your current database to your device.",
-      "importDatabasePgDesc": "Click to select and upload a .dump file to restore your PostgreSQL database. This replaces all current data.",
+      "importDatabasePgDesc": "Click to select and upload a PostgreSQL backup (.dump), a SQLite database (.db), or a SQLite migration dump to restore your database. This replaces all current data.",
       "migrationDownload": "Download Migration",
-      "migrationDownloadDesc": "Click to download a portable .dump (SQL text) export of your SQLite database.",
       "migrationDownloadPgDesc": "Click to download a .db SQLite database built from your PostgreSQL data, ready to run this panel on SQLite."
     },
     "inbounds": {

+ 4 - 5
internal/web/translation/es-ES.json

@@ -261,19 +261,18 @@
       "backup": "Copia de seguridad",
       "backupTitle": "Copia & Restauración",
       "exportDatabase": "Copia de seguridad",
-      "exportDatabaseDesc": "Haz clic para descargar un archivo .db que contiene una copia de seguridad de tu base de datos actual en tu dispositivo.",
+      "exportDatabaseDesc": "Haz clic para descargar un archivo .db que contiene una copia de seguridad de tu base de datos actual en tu dispositivo. El mismo archivo también puede restaurarse en un panel que funcione con PostgreSQL.",
       "importDatabase": "Restaurar",
-      "importDatabaseDesc": "Haz clic para seleccionar y cargar un archivo .db desde tu dispositivo para restaurar tu base de datos desde una copia de seguridad.",
+      "importDatabaseDesc": "Haz clic para seleccionar y cargar una copia de seguridad .db o un volcado de migración (.dump) desde tu dispositivo para restaurar tu base de datos.",
       "importDatabaseSuccess": "La base de datos se ha importado correctamente",
       "importDatabaseError": "Ocurrió un error al importar la base de datos",
       "readDatabaseError": "Ocurrió un error al leer la base de datos",
       "getDatabaseError": "Ocurrió un error al obtener la base de datos",
       "getConfigError": "Ocurrió un error al obtener el archivo de configuración",
-      "backupPostgresNote": "Este panel funciona con PostgreSQL. «Copia de seguridad» descarga un archivo pg_dump (.dump) y «Restaurar» lo vuelve a cargar con pg_restore. El servidor necesita tener instaladas las herramientas cliente de PostgreSQL (pg_dump y pg_restore).",
+      "backupPostgresNote": "Este panel funciona con PostgreSQL. «Copia de seguridad» descarga un archivo pg_dump (.dump) y «Restaurar» lo vuelve a cargar con pg_restore. «Restaurar» también acepta una base de datos SQLite (.db) o un volcado de migración de SQLite e importa sus datos a PostgreSQL. El servidor necesita tener instaladas las herramientas cliente de PostgreSQL (pg_dump y pg_restore).",
       "exportDatabasePgDesc": "Haz clic para descargar un volcado de PostgreSQL (.dump) de tu base de datos actual en tu dispositivo.",
-      "importDatabasePgDesc": "Haz clic para seleccionar y subir un archivo .dump y restaurar tu base de datos PostgreSQL. Esto reemplaza todos los datos actuales.",
+      "importDatabasePgDesc": "Haz clic para seleccionar y subir una copia de seguridad de PostgreSQL (.dump), una base de datos SQLite (.db) o un volcado de migración de SQLite para restaurar tu base de datos. Esto reemplaza todos los datos actuales.",
       "migrationDownload": "Descargar migración",
-      "migrationDownloadDesc": "Haz clic para descargar una exportación portable .dump (texto SQL) de tu base de datos SQLite.",
       "migrationDownloadPgDesc": "Haz clic para descargar una base de datos SQLite .db creada a partir de tus datos de PostgreSQL, lista para ejecutar este panel en SQLite."
     },
     "inbounds": {

+ 4 - 5
internal/web/translation/fa-IR.json

@@ -261,19 +261,18 @@
       "backup": "پشتیبان‌گیری",
       "backupTitle": "پشتیبان‌گیری و بازیابی",
       "exportDatabase": "پشتیبان‌گیری",
-      "exportDatabaseDesc": "برای دانلود یک فایل .db حاوی پشتیبان از پایگاه داده فعلی خود به دستگاهتان کلیک کنید.",
+      "exportDatabaseDesc": "برای دانلود یک فایل .db حاوی پشتیبان از پایگاه داده فعلی خود به دستگاهتان کلیک کنید. همین فایل در پنلی که روی PostgreSQL اجرا می‌شود نیز قابل بازیابی است.",
       "importDatabase": "بازیابی",
-      "importDatabaseDesc": "برای انتخاب و آپلود یک فایل .db از دستگاهتان و بازیابی پایگاه داده از یک پشتیبان کلیک کنید.",
+      "importDatabaseDesc": "برای انتخاب و آپلود یک پشتیبان .db یا فایل مهاجرت ‎.dump از دستگاهتان جهت بازیابی پایگاه داده کلیک کنید.",
       "importDatabaseSuccess": "پایگاه داده با موفقیت وارد شد",
       "importDatabaseError": "خطا در وارد کردن پایگاه داده",
       "readDatabaseError": "خطا در خواندن پایگاه داده",
       "getDatabaseError": "خطا در دریافت پایگاه داده",
       "getConfigError": "خطا در دریافت فایل پیکربندی",
-      "backupPostgresNote": "این پنل روی PostgreSQL اجرا می‌شود. «پشتیبان‌گیری» یک آرشیو pg_dump (.dump) دانلود می‌کند و «بازیابی» آن را با pg_restore بازمی‌گرداند. سرور باید ابزارهای کلاینت PostgreSQL (pg_dump و pg_restore) را نصب داشته باشد.",
+      "backupPostgresNote": "این پنل روی PostgreSQL اجرا می‌شود. «پشتیبان‌گیری» یک آرشیو pg_dump (.dump) دانلود می‌کند و «بازیابی» آن را با pg_restore بازمی‌گرداند. «بازیابی» همچنین پایگاه‌دادهٔ SQLite (.db) یا فایل مهاجرت SQLite را می‌پذیرد و داده‌های آن را به PostgreSQL وارد می‌کند. سرور باید ابزارهای کلاینت PostgreSQL (pg_dump و pg_restore) را نصب داشته باشد.",
       "exportDatabasePgDesc": "برای دانلود یک دامپ PostgreSQL (.dump) از پایگاه داده فعلی روی دستگاهتان کلیک کنید.",
-      "importDatabasePgDesc": "برای انتخاب و بارگذاری یک فایل .dump جهت بازیابی پایگاه داده PostgreSQL کلیک کنید. این کار همه داده‌های فعلی را جایگزین می‌کند.",
+      "importDatabasePgDesc": "برای انتخاب و بارگذاری یک پشتیبان PostgreSQL (.dump)، پایگاه‌دادهٔ SQLite (.db) یا فایل مهاجرت SQLite جهت بازیابی پایگاه داده کلیک کنید. این کار همه داده‌های فعلی را جایگزین می‌کند.",
       "migrationDownload": "دانلود فایل مهاجرت",
-      "migrationDownloadDesc": "برای دانلود یک خروجی قابل‌حمل ‎.dump (متن SQL) از پایگاه‌دادهٔ SQLite خود کلیک کنید.",
       "migrationDownloadPgDesc": "برای دانلود یک پایگاه‌دادهٔ SQLite با پسوند ‎.db که از داده‌های PostgreSQL شما ساخته می‌شود کلیک کنید؛ آمادهٔ اجرای این پنل روی SQLite."
     },
     "inbounds": {

+ 4 - 5
internal/web/translation/id-ID.json

@@ -261,19 +261,18 @@
       "backup": "Cadangan",
       "backupTitle": "Cadangan & Pulihkan",
       "exportDatabase": "Cadangkan",
-      "exportDatabaseDesc": "Klik untuk mengunduh file .db yang berisi cadangan dari database Anda saat ini ke perangkat Anda.",
+      "exportDatabaseDesc": "Klik untuk mengunduh file .db yang berisi cadangan dari database Anda saat ini ke perangkat Anda. Berkas yang sama juga dapat dipulihkan ke panel yang berjalan di PostgreSQL.",
       "importDatabase": "Pulihkan",
-      "importDatabaseDesc": "Klik untuk memilih dan mengunggah file .db dari perangkat Anda untuk memulihkan database dari cadangan.",
+      "importDatabaseDesc": "Klik untuk memilih dan mengunggah cadangan .db atau dump migrasi (.dump) dari perangkat Anda untuk memulihkan database.",
       "importDatabaseSuccess": "Database berhasil diimpor",
       "importDatabaseError": "Terjadi kesalahan saat mengimpor database",
       "readDatabaseError": "Terjadi kesalahan saat membaca database",
       "getDatabaseError": "Terjadi kesalahan saat mengambil database",
       "getConfigError": "Terjadi kesalahan saat mengambil file konfigurasi",
-      "backupPostgresNote": "Panel ini berjalan di PostgreSQL. «Cadangkan» mengunduh arsip pg_dump (.dump) dan «Pulihkan» memuatnya kembali dengan pg_restore. Server memerlukan alat klien PostgreSQL (pg_dump dan pg_restore) terpasang.",
+      "backupPostgresNote": "Panel ini berjalan di PostgreSQL. «Cadangkan» mengunduh arsip pg_dump (.dump) dan «Pulihkan» memuatnya kembali dengan pg_restore. «Pulihkan» juga menerima basis data SQLite (.db) atau dump migrasi SQLite dan mengimpor datanya ke PostgreSQL. Server memerlukan alat klien PostgreSQL (pg_dump dan pg_restore) terpasang.",
       "exportDatabasePgDesc": "Klik untuk mengunduh dump PostgreSQL (.dump) dari basis data Anda saat ini ke perangkat Anda.",
-      "importDatabasePgDesc": "Klik untuk memilih dan mengunggah berkas .dump guna memulihkan basis data PostgreSQL Anda. Ini menggantikan semua data saat ini.",
+      "importDatabasePgDesc": "Klik untuk memilih dan mengunggah cadangan PostgreSQL (.dump), basis data SQLite (.db), atau dump migrasi SQLite guna memulihkan basis data Anda. Ini menggantikan semua data saat ini.",
       "migrationDownload": "Unduh migrasi",
-      "migrationDownloadDesc": "Klik untuk mengunduh ekspor .dump (teks SQL) portabel dari basis data SQLite Anda.",
       "migrationDownloadPgDesc": "Klik untuk mengunduh basis data SQLite .db yang dibuat dari data PostgreSQL Anda, siap menjalankan panel ini di SQLite."
     },
     "inbounds": {

+ 4 - 5
internal/web/translation/ja-JP.json

@@ -261,19 +261,18 @@
       "backup": "バックアップ",
       "backupTitle": "バックアップと復元",
       "exportDatabase": "バックアップ",
-      "exportDatabaseDesc": "クリックして、現在のデータベースのバックアップを含む .db ファイルをデバイスにダウンロードします。",
+      "exportDatabaseDesc": "クリックして、現在のデータベースのバックアップを含む .db ファイルをデバイスにダウンロードします。同じファイルは PostgreSQL で動作するパネルにも復元できます。",
       "importDatabase": "復元",
-      "importDatabaseDesc": "クリックして、デバイスから .db ファイルを選択し、アップロードしてバックアップからデータベースを復元します。",
+      "importDatabaseDesc": "クリックして、デバイスから .db バックアップまたは移行ダンプ (.dump) を選択し、アップロードしてデータベースを復元します。",
       "importDatabaseSuccess": "データベースのインポートに成功しました",
       "importDatabaseError": "データベースのインポート中にエラーが発生しました",
       "readDatabaseError": "データベースの読み取り中にエラーが発生しました",
       "getDatabaseError": "データベースの取得中にエラーが発生しました",
       "getConfigError": "設定ファイルの取得中にエラーが発生しました",
-      "backupPostgresNote": "このパネルは PostgreSQL で動作しています。「バックアップ」は pg_dump アーカイブ (.dump) をダウンロードし、「復元」は pg_restore で読み込み直します。サーバーに PostgreSQL クライアントツール (pg_dump と pg_restore) がインストールされている必要があります。",
+      "backupPostgresNote": "このパネルは PostgreSQL で動作しています。「バックアップ」は pg_dump アーカイブ (.dump) をダウンロードし、「復元」は pg_restore で読み込み直します。「復元」は SQLite データベース (.db) や SQLite 移行ダンプも受け付け、そのデータを PostgreSQL に取り込みます。サーバーに PostgreSQL クライアントツール (pg_dump と pg_restore) がインストールされている必要があります。",
       "exportDatabasePgDesc": "現在のデータベースの PostgreSQL ダンプ (.dump) を端末にダウンロードするにはクリックしてください。",
-      "importDatabasePgDesc": "PostgreSQL データベースを復元するために .dump ファイルを選択してアップロードするにはクリックしてください。現在のすべてのデータが置き換えられます。",
+      "importDatabasePgDesc": "データベースを復元するために PostgreSQL バックアップ (.dump)、SQLite データベース (.db)、または SQLite 移行ダンプを選択してアップロードするにはクリックしてください。現在のすべてのデータが置き換えられます。",
       "migrationDownload": "移行ファイルをダウンロード",
-      "migrationDownloadDesc": "SQLite データベースのポータブルな .dump(SQL テキスト)エクスポートをダウンロードするにはクリックします。",
       "migrationDownloadPgDesc": "PostgreSQL のデータから作成した .db SQLite データベースをダウンロードします。このパネルを SQLite で実行する準備が整います。"
     },
     "inbounds": {

+ 4 - 5
internal/web/translation/pt-BR.json

@@ -261,19 +261,18 @@
       "backup": "Backup",
       "backupTitle": "Backup & Restauração",
       "exportDatabase": "Backup",
-      "exportDatabaseDesc": "Clique para baixar um arquivo .db contendo um backup do seu banco de dados atual para o seu dispositivo.",
+      "exportDatabaseDesc": "Clique para baixar um arquivo .db contendo um backup do seu banco de dados atual para o seu dispositivo. O mesmo arquivo também pode ser restaurado em um painel executando PostgreSQL.",
       "importDatabase": "Restaurar",
-      "importDatabaseDesc": "Clique para selecionar e enviar um arquivo .db do seu dispositivo para restaurar seu banco de dados a partir de um backup.",
+      "importDatabaseDesc": "Clique para selecionar e enviar um backup .db ou um dump de migração (.dump) do seu dispositivo para restaurar seu banco de dados.",
       "importDatabaseSuccess": "O banco de dados foi importado com sucesso",
       "importDatabaseError": "Ocorreu um erro ao importar o banco de dados",
       "readDatabaseError": "Ocorreu um erro ao ler o banco de dados",
       "getDatabaseError": "Ocorreu um erro ao recuperar o banco de dados",
       "getConfigError": "Ocorreu um erro ao recuperar o arquivo de configuração",
-      "backupPostgresNote": "Este painel é executado em PostgreSQL. «Backup» baixa um arquivo pg_dump (.dump) e «Restaurar» o recarrega com pg_restore. O servidor precisa ter as ferramentas cliente do PostgreSQL (pg_dump e pg_restore) instaladas.",
+      "backupPostgresNote": "Este painel é executado em PostgreSQL. «Backup» baixa um arquivo pg_dump (.dump) e «Restaurar» o recarrega com pg_restore. «Restaurar» também aceita um banco de dados SQLite (.db) ou um dump de migração do SQLite e importa seus dados para o PostgreSQL. O servidor precisa ter as ferramentas cliente do PostgreSQL (pg_dump e pg_restore) instaladas.",
       "exportDatabasePgDesc": "Clique para baixar um dump do PostgreSQL (.dump) do seu banco de dados atual para o seu dispositivo.",
-      "importDatabasePgDesc": "Clique para selecionar e enviar um arquivo .dump para restaurar seu banco de dados PostgreSQL. Isso substitui todos os dados atuais.",
+      "importDatabasePgDesc": "Clique para selecionar e enviar um backup do PostgreSQL (.dump), um banco de dados SQLite (.db) ou um dump de migração do SQLite para restaurar seu banco de dados. Isso substitui todos os dados atuais.",
       "migrationDownload": "Baixar migração",
-      "migrationDownloadDesc": "Clique para baixar uma exportação portátil .dump (texto SQL) do seu banco de dados SQLite.",
       "migrationDownloadPgDesc": "Clique para baixar um banco de dados SQLite .db criado a partir dos seus dados do PostgreSQL, pronto para executar este painel no SQLite."
     },
     "inbounds": {

+ 4 - 5
internal/web/translation/ru-RU.json

@@ -261,19 +261,18 @@
       "backup": "Резервная копия",
       "backupTitle": "Бэкап и восстановление",
       "exportDatabase": "Экспорт базы данных",
-      "exportDatabaseDesc": "Нажмите, чтобы скачать файл .db, содержащий резервную копию вашей текущей базы данных на ваше устройство.",
+      "exportDatabaseDesc": "Нажмите, чтобы скачать файл .db, содержащий резервную копию вашей текущей базы данных на ваше устройство. Этот же файл можно восстановить на панели, работающей на PostgreSQL.",
       "importDatabase": "Импорт базы данных",
-      "importDatabaseDesc": "Нажмите, чтобы выбрать и загрузить файл .db с вашего устройства для восстановления базы данных из резервной копии.",
+      "importDatabaseDesc": "Нажмите, чтобы выбрать и загрузить резервную копию .db или миграционный дамп (.dump) с вашего устройства для восстановления базы данных.",
       "importDatabaseSuccess": "База данных успешно импортирована",
       "importDatabaseError": "Произошла ошибка при импорте базы данных",
       "readDatabaseError": "Произошла ошибка при чтении базы данных",
       "getDatabaseError": "Произошла ошибка при получении базы данных",
       "getConfigError": "Произошла ошибка при получении конфигурационного файла",
-      "backupPostgresNote": "Эта панель работает на PostgreSQL. «Резервная копия» скачивает архив pg_dump (.dump), а «Восстановление» загружает его обратно через pg_restore. На сервере должны быть установлены клиентские инструменты PostgreSQL (pg_dump и pg_restore).",
+      "backupPostgresNote": "Эта панель работает на PostgreSQL. «Резервная копия» скачивает архив pg_dump (.dump), а «Восстановление» загружает его обратно через pg_restore. «Восстановление» также принимает базу данных SQLite (.db) или миграционный дамп SQLite и импортирует их данные в PostgreSQL. На сервере должны быть установлены клиентские инструменты PostgreSQL (pg_dump и pg_restore).",
       "exportDatabasePgDesc": "Нажмите, чтобы скачать дамп PostgreSQL (.dump) текущей базы данных на ваше устройство.",
-      "importDatabasePgDesc": "Нажмите, чтобы выбрать и загрузить файл .dump для восстановления базы данных PostgreSQL. Это заменит все текущие данные.",
+      "importDatabasePgDesc": "Нажмите, чтобы выбрать и загрузить резервную копию PostgreSQL (.dump), базу данных SQLite (.db) или миграционный дамп SQLite для восстановления базы данных. Это заменит все текущие данные.",
       "migrationDownload": "Скачать файл миграции",
-      "migrationDownloadDesc": "Нажмите, чтобы скачать переносимый экспорт .dump (текст SQL) вашей базы данных SQLite.",
       "migrationDownloadPgDesc": "Нажмите, чтобы скачать базу данных SQLite (.db), собранную из ваших данных PostgreSQL и готовую для запуска панели на SQLite."
     },
     "inbounds": {

+ 4 - 5
internal/web/translation/tr-TR.json

@@ -261,19 +261,18 @@
       "backup": "Yedek",
       "backupTitle": "Yedekleme ve Geri Yükleme",
       "exportDatabase": "Yedekle",
-      "exportDatabaseDesc": "Mevcut veritabanınızın yedeğini içeren bir .db dosyasını cihazınıza indirmek için tıklayın.",
+      "exportDatabaseDesc": "Mevcut veritabanınızın yedeğini içeren bir .db dosyasını cihazınıza indirmek için tıklayın. Aynı dosya PostgreSQL üzerinde çalışan bir panele de geri yüklenebilir.",
       "importDatabase": "Geri Yükle",
-      "importDatabaseDesc": "Cihazınızdan bir .db dosyası seçip yükleyerek veritabanınızı yedekten geri yüklemek için tıklayın.",
+      "importDatabaseDesc": "Cihazınızdan bir .db yedeği veya taşıma dökümü (.dump) seçip yükleyerek veritabanınızı geri yüklemek için tıklayın.",
       "importDatabaseSuccess": "Veritabanı başarıyla içe aktarıldı.",
       "importDatabaseError": "Veritabanı içe aktarılırken bir hata oluştu.",
       "readDatabaseError": "Veritabanı okunurken bir hata oluştu.",
       "getDatabaseError": "Veritabanı alınırken bir hata oluştu.",
       "getConfigError": "Yapılandırma dosyası alınırken bir hata oluştu.",
-      "backupPostgresNote": "Bu panel PostgreSQL üzerinde çalışıyor. 'Yedekle' bir pg_dump arşivi (.dump) indirir, 'Geri Yükle' ise onu pg_restore ile geri yükler. Sunucuda PostgreSQL istemci araçlarının (pg_dump ve pg_restore) kurulu olması gerekir.",
+      "backupPostgresNote": "Bu panel PostgreSQL üzerinde çalışıyor. 'Yedekle' bir pg_dump arşivi (.dump) indirir, 'Geri Yükle' ise onu pg_restore ile geri yükler. 'Geri Yükle' ayrıca bir SQLite veritabanını (.db) veya SQLite taşıma dökümünü kabul eder ve verilerini PostgreSQL'e aktarır. Sunucuda PostgreSQL istemci araçlarının (pg_dump ve pg_restore) kurulu olması gerekir.",
       "exportDatabasePgDesc": "Mevcut veritabanınızın PostgreSQL dökümünü (.dump) cihazınıza indirmek için tıklayın.",
-      "importDatabasePgDesc": "PostgreSQL veritabanınızı geri yüklemek için bir .dump dosyası seçip yüklemek üzere tıklayın. Bu, tüm mevcut verilerin yerini alır.",
+      "importDatabasePgDesc": "Veritabanınızı geri yüklemek için bir PostgreSQL yedeği (.dump), SQLite veritabanı (.db) veya SQLite taşıma dökümü seçip yüklemek üzere tıklayın. Bu, tüm mevcut verilerin yerini alır.",
       "migrationDownload": "Geçiş Dosyasını İndir",
-      "migrationDownloadDesc": "SQLite veritabanınızın taşınabilir .dump (SQL metni) yedeğini indirmek için tıklayın.",
       "migrationDownloadPgDesc": "PostgreSQL verilerinizden oluşturulan ve bu paneli SQLite üzerinde çalıştırmaya hazır bir .db SQLite veritabanı indirmek için tıklayın."
     },
     "inbounds": {

+ 4 - 5
internal/web/translation/uk-UA.json

@@ -261,19 +261,18 @@
       "backup": "Резервна копія",
       "backupTitle": "Резервне копіювання та відновлення",
       "exportDatabase": "Резервна копія",
-      "exportDatabaseDesc": "Натисніть, щоб завантажити файл .db, що містить резервну копію вашої поточної бази даних на ваш пристрій.",
+      "exportDatabaseDesc": "Натисніть, щоб завантажити файл .db, що містить резервну копію вашої поточної бази даних на ваш пристрій. Цей самий файл можна відновити на панелі, що працює на PostgreSQL.",
       "importDatabase": "Відновити",
-      "importDatabaseDesc": "Натисніть, щоб вибрати та завантажити файл .db з вашого пристрою для відновлення бази даних з резервної копії.",
+      "importDatabaseDesc": "Натисніть, щоб вибрати та завантажити резервну копію .db або міграційний дамп (.dump) з вашого пристрою для відновлення бази даних.",
       "importDatabaseSuccess": "Базу даних успішно імпортовано",
       "importDatabaseError": "Виникла помилка під час імпорту бази даних",
       "readDatabaseError": "Виникла помилка під час читання бази даних",
       "getDatabaseError": "Виникла помилка під час отримання бази даних",
       "getConfigError": "Виникла помилка під час отримання файлу конфігурації",
-      "backupPostgresNote": "Ця панель працює на PostgreSQL. «Резервна копія» завантажує архів pg_dump (.dump), а «Відновлення» завантажує його назад через pg_restore. На сервері мають бути встановлені клієнтські інструменти PostgreSQL (pg_dump і pg_restore).",
+      "backupPostgresNote": "Ця панель працює на PostgreSQL. «Резервна копія» завантажує архів pg_dump (.dump), а «Відновлення» завантажує його назад через pg_restore. «Відновлення» також приймає базу даних SQLite (.db) або міграційний дамп SQLite та імпортує їхні дані в PostgreSQL. На сервері мають бути встановлені клієнтські інструменти PostgreSQL (pg_dump і pg_restore).",
       "exportDatabasePgDesc": "Натисніть, щоб завантажити дамп PostgreSQL (.dump) вашої поточної бази даних на ваш пристрій.",
-      "importDatabasePgDesc": "Натисніть, щоб вибрати та завантажити файл .dump для відновлення бази даних PostgreSQL. Це замінить усі поточні дані.",
+      "importDatabasePgDesc": "Натисніть, щоб вибрати та завантажити резервну копію PostgreSQL (.dump), базу даних SQLite (.db) або міграційний дамп SQLite для відновлення бази даних. Це замінить усі поточні дані.",
       "migrationDownload": "Завантажити файл міграції",
-      "migrationDownloadDesc": "Натисніть, щоб завантажити переносний експорт .dump (текст SQL) вашої бази даних SQLite.",
       "migrationDownloadPgDesc": "Натисніть, щоб завантажити базу даних SQLite (.db), створену з ваших даних PostgreSQL і готову для запуску панелі на SQLite."
     },
     "inbounds": {

+ 4 - 5
internal/web/translation/vi-VN.json

@@ -261,19 +261,18 @@
       "backup": "Sao lưu",
       "backupTitle": "Sao lưu & Khôi phục",
       "exportDatabase": "Sao lưu",
-      "exportDatabaseDesc": "Nhấp để tải xuống tệp .db chứa bản sao lưu cơ sở dữ liệu hiện tại của bạn vào thiết bị.",
+      "exportDatabaseDesc": "Nhấp để tải xuống tệp .db chứa bản sao lưu cơ sở dữ liệu hiện tại của bạn vào thiết bị. Tệp này cũng có thể được khôi phục vào bảng điều khiển chạy PostgreSQL.",
       "importDatabase": "Khôi phục",
-      "importDatabaseDesc": "Nhấp để chọn và tải lên tệp .db từ thiết bị của bạn để khôi phục cơ sở dữ liệu từ bản sao lưu.",
+      "importDatabaseDesc": "Nhấp để chọn và tải lên bản sao lưu .db hoặc tệp kết xuất di trú (.dump) từ thiết bị của bạn để khôi phục cơ sở dữ liệu.",
       "importDatabaseSuccess": "Đã nhập cơ sở dữ liệu thành công",
       "importDatabaseError": "Lỗi xảy ra khi nhập cơ sở dữ liệu",
       "readDatabaseError": "Lỗi xảy ra khi đọc cơ sở dữ liệu",
       "getDatabaseError": "Lỗi xảy ra khi truy xuất cơ sở dữ liệu",
       "getConfigError": "Lỗi xảy ra khi truy xuất tệp cấu hình",
-      "backupPostgresNote": "Bảng điều khiển này chạy trên PostgreSQL. «Sao lưu» tải xuống một tệp lưu trữ pg_dump (.dump) và «Khôi phục» nạp lại bằng pg_restore. Máy chủ cần cài đặt các công cụ máy khách PostgreSQL (pg_dump và pg_restore).",
+      "backupPostgresNote": "Bảng điều khiển này chạy trên PostgreSQL. «Sao lưu» tải xuống một tệp lưu trữ pg_dump (.dump) và «Khôi phục» nạp lại bằng pg_restore. «Khôi phục» cũng chấp nhận cơ sở dữ liệu SQLite (.db) hoặc tệp kết xuất di trú SQLite và nhập dữ liệu của chúng vào PostgreSQL. Máy chủ cần cài đặt các công cụ máy khách PostgreSQL (pg_dump và pg_restore).",
       "exportDatabasePgDesc": "Nhấn để tải xuống bản kết xuất PostgreSQL (.dump) của cơ sở dữ liệu hiện tại về thiết bị của bạn.",
-      "importDatabasePgDesc": "Nhấn để chọn và tải lên một tệp .dump nhằm khôi phục cơ sở dữ liệu PostgreSQL của bạn. Thao tác này sẽ thay thế toàn bộ dữ liệu hiện tại.",
+      "importDatabasePgDesc": "Nhấn để chọn và tải lên bản sao lưu PostgreSQL (.dump), cơ sở dữ liệu SQLite (.db) hoặc tệp kết xuất di trú SQLite nhằm khôi phục cơ sở dữ liệu của bạn. Thao tác này sẽ thay thế toàn bộ dữ liệu hiện tại.",
       "migrationDownload": "Tải tệp di trú",
-      "migrationDownloadDesc": "Nhấp để tải xuống bản xuất .dump (văn bản SQL) di động của cơ sở dữ liệu SQLite của bạn.",
       "migrationDownloadPgDesc": "Nhấp để tải xuống cơ sở dữ liệu SQLite .db được tạo từ dữ liệu PostgreSQL của bạn, sẵn sàng chạy bảng điều khiển này trên SQLite."
     },
     "inbounds": {

+ 4 - 5
internal/web/translation/zh-CN.json

@@ -261,19 +261,18 @@
       "backup": "备份",
       "backupTitle": "备份和恢复",
       "exportDatabase": "备份",
-      "exportDatabaseDesc": "点击下载包含当前数据库备份的 .db 文件到您的设备。",
+      "exportDatabaseDesc": "点击下载包含当前数据库备份的 .db 文件到您的设备。同一文件也可以恢复到运行 PostgreSQL 的面板中。",
       "importDatabase": "恢复",
-      "importDatabaseDesc": "点击选择并上传设备中的 .db 文件以从备份恢复数据库。",
+      "importDatabaseDesc": "点击选择并上传设备中的 .db 备份或迁移导出文件(.dump)以恢复数据库。",
       "importDatabaseSuccess": "数据库导入成功",
       "importDatabaseError": "导入数据库时出错",
       "readDatabaseError": "读取数据库时出错",
       "getDatabaseError": "检索数据库时出错",
       "getConfigError": "检索配置文件时出错",
-      "backupPostgresNote": "此面板运行在 PostgreSQL 上。「备份」会下载一个 pg_dump 归档(.dump),「恢复」会通过 pg_restore 重新载入。服务器需要安装 PostgreSQL 客户端工具(pg_dump 和 pg_restore)。",
+      "backupPostgresNote": "此面板运行在 PostgreSQL 上。「备份」会下载一个 pg_dump 归档(.dump),「恢复」会通过 pg_restore 重新载入。「恢复」也接受 SQLite 数据库(.db)或 SQLite 迁移导出文件,并将其数据导入 PostgreSQL。服务器需要安装 PostgreSQL 客户端工具(pg_dump 和 pg_restore)。",
       "exportDatabasePgDesc": "点击将当前数据库的 PostgreSQL 转储(.dump)下载到您的设备。",
-      "importDatabasePgDesc": "点击选择并上传 .dump 文件以恢复您的 PostgreSQL 数据库。此操作将替换所有当前数据。",
+      "importDatabasePgDesc": "点击选择并上传 PostgreSQL 备份(.dump)、SQLite 数据库(.db)或 SQLite 迁移导出文件以恢复您的数据库。此操作将替换所有当前数据。",
       "migrationDownload": "下载迁移文件",
-      "migrationDownloadDesc": "点击下载当前 SQLite 数据库的可移植 .dump(SQL 文本)导出文件。",
       "migrationDownloadPgDesc": "点击下载由 PostgreSQL 数据构建的 .db SQLite 数据库,可用于在 SQLite 上运行本面板。"
     },
     "inbounds": {

+ 4 - 5
internal/web/translation/zh-TW.json

@@ -261,19 +261,18 @@
       "backup": "備份",
       "backupTitle": "備份和恢復",
       "exportDatabase": "備份",
-      "exportDatabaseDesc": "點擊下載包含當前資料庫備份的 .db 文件到您的設備。",
+      "exportDatabaseDesc": "點擊下載包含當前資料庫備份的 .db 文件到您的設備。同一檔案也可以還原到執行 PostgreSQL 的面板中。",
       "importDatabase": "恢復",
-      "importDatabaseDesc": "點擊選擇並上傳設備中的 .db 文件以從備份恢復資料庫。",
+      "importDatabaseDesc": "點擊選擇並上傳設備中的 .db 備份或遷移匯出檔(.dump)以還原資料庫。",
       "importDatabaseSuccess": "資料庫匯入成功",
       "importDatabaseError": "匯入資料庫時發生錯誤",
       "readDatabaseError": "讀取資料庫時發生錯誤",
       "getDatabaseError": "檢索資料庫時發生錯誤",
       "getConfigError": "檢索設定檔時發生錯誤",
-      "backupPostgresNote": "此面板執行於 PostgreSQL 上。「備份」會下載一個 pg_dump 封存檔(.dump),「還原」會透過 pg_restore 重新載入。伺服器需要安裝 PostgreSQL 用戶端工具(pg_dump 與 pg_restore)。",
+      "backupPostgresNote": "此面板執行於 PostgreSQL 上。「備份」會下載一個 pg_dump 封存檔(.dump),「還原」會透過 pg_restore 重新載入。「還原」也接受 SQLite 資料庫(.db)或 SQLite 遷移匯出檔,並將其資料匯入 PostgreSQL。伺服器需要安裝 PostgreSQL 用戶端工具(pg_dump 與 pg_restore)。",
       "exportDatabasePgDesc": "點擊將目前資料庫的 PostgreSQL 傾印(.dump)下載到您的裝置。",
-      "importDatabasePgDesc": "點擊選擇並上傳 .dump 檔以還原您的 PostgreSQL 資料庫。此操作將取代所有目前的資料。",
+      "importDatabasePgDesc": "點擊選擇並上傳 PostgreSQL 備份(.dump)、SQLite 資料庫(.db)或 SQLite 遷移匯出檔以還原您的資料庫。此操作將取代所有目前的資料。",
       "migrationDownload": "下載遷移檔案",
-      "migrationDownloadDesc": "點擊下載目前 SQLite 資料庫的可攜式 .dump(SQL 文字)匯出檔。",
       "migrationDownloadPgDesc": "點擊下載由 PostgreSQL 資料建立的 .db SQLite 資料庫,可用於在 SQLite 上執行本面板。"
     },
     "inbounds": {