backupModal.hide()" @cancel="() => backupModal.hide()">
+
+
+ [[ backupModal.description ]]
+
+
+
+ [[ backupModal.exportText ]]
+
+
+ [[ backupModal.importText ]]
+
+
+
+
{{template "js" .}}
{{template "textModal"}}
@@ -338,6 +358,29 @@
},
};
+ const backupModal = {
+ visible: false,
+ title: '',
+ description: '',
+ exportText: '',
+ importText: '',
+ show({
+ title = '{{ i18n "pages.index.backupTitle" }}',
+ description = '{{ i18n "pages.index.backupDescription" }}',
+ exportText = '{{ i18n "pages.index.exportDatabase" }}',
+ importText = '{{ i18n "pages.index.importDatabase" }}',
+ }) {
+ this.title = title;
+ this.description = description;
+ this.exportText = exportText;
+ this.importText = importText;
+ this.visible = true;
+ },
+ hide() {
+ this.visible = false;
+ },
+ };
+
const app = new Vue({
delimiters: ['[[', ']]'],
el: '#app',
@@ -346,6 +389,7 @@
status: new Status(),
versionModal,
logModal,
+ backupModal,
spinning: false,
loadingTip: '{{ i18n "loading"}}',
},
@@ -387,7 +431,6 @@
},
});
},
- //here add stop xray function
async stopXrayService() {
this.loading(true);
const msg = await HttpUtil.post('server/stopXrayService');
@@ -396,7 +439,6 @@
return;
}
},
- //here add restart xray function
async restartXrayService() {
this.loading(true);
const msg = await HttpUtil.post('server/restartXrayService');
@@ -412,20 +454,60 @@
if (!msg.success) {
return;
}
- logModal.show(msg.obj,rows);
+ logModal.show(msg.obj, rows);
},
- async openConfig(){
+ async openConfig() {
this.loading(true);
const msg = await HttpUtil.post('server/getConfigJson');
this.loading(false);
if (!msg.success) {
return;
}
- txtModal.show('config.json',JSON.stringify(msg.obj, null, 2),'config.json');
+ txtModal.show('config.json', JSON.stringify(msg.obj, null, 2), 'config.json');
},
- getBackup(){
+ openBackup() {
+ backupModal.show({
+ title: '{{ i18n "pages.index.backupTitle" }}',
+ description: '{{ i18n "pages.index.backupDescription" }}',
+ exportText: '{{ i18n "pages.index.exportDatabase" }}',
+ importText: '{{ i18n "pages.index.importDatabase" }}',
+ });
+ },
+ exportDatabase() {
window.location = basePath + 'server/getDb';
- }
+ },
+ importDatabase() {
+ const fileInput = document.createElement('input');
+ fileInput.type = 'file';
+ fileInput.accept = '.db';
+ fileInput.addEventListener('change', async (event) => {
+ const dbFile = event.target.files[0];
+ if (dbFile) {
+ const formData = new FormData();
+ formData.append('db', dbFile);
+ backupModal.hide();
+ this.loading(true);
+ const uploadMsg = await HttpUtil.post('server/importDB', formData, {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ }
+ });
+ this.loading(false);
+ if (!uploadMsg.success) {
+ return;
+ }
+ this.loading(true);
+ const restartMsg = await HttpUtil.post("/xui/setting/restartPanel");
+ this.loading(false);
+ if (restartMsg.success) {
+ this.loading(true);
+ await PromiseUtil.sleep(5000);
+ location.reload();
+ }
+ }
+ });
+ fileInput.click();
+ },
},
async mounted() {
while (true) {
diff --git a/web/service/inbound.go b/web/service/inbound.go
index bde6b1f9..119e497e 100644
--- a/web/service/inbound.go
+++ b/web/service/inbound.go
@@ -572,6 +572,7 @@ func (s *InboundService) DisableInvalidInbounds() (int64, error) {
count := result.RowsAffected
return count, err
}
+
func (s *InboundService) DisableInvalidClients() (int64, error) {
db := database.GetDB()
now := time.Now().Unix() * 1000
@@ -582,7 +583,8 @@ func (s *InboundService) DisableInvalidClients() (int64, error) {
count := result.RowsAffected
return count, err
}
-func (s *InboundService) RemoveOrphanedTraffics() {
+
+func (s *InboundService) MigrationRemoveOrphanedTraffics() {
db := database.GetDB()
db.Exec(`
DELETE FROM client_traffics
@@ -593,6 +595,7 @@ func (s *InboundService) RemoveOrphanedTraffics() {
)
`)
}
+
func (s *InboundService) AddClientStat(inboundId int, client *model.Client) error {
db := database.GetDB()
@@ -611,6 +614,7 @@ func (s *InboundService) AddClientStat(inboundId int, client *model.Client) erro
}
return nil
}
+
func (s *InboundService) UpdateClientStat(email string, client *model.Client) error {
db := database.GetDB()
@@ -917,3 +921,8 @@ func (s *InboundService) MigrationRequirements() {
// Remove orphaned traffics
db.Where("inbound_id = 0").Delete(xray.ClientTraffic{})
}
+
+func (s *InboundService) MigrateDB() {
+ s.MigrationRequirements()
+ s.MigrationRemoveOrphanedTraffics()
+}
diff --git a/web/service/server.go b/web/service/server.go
index d4048196..813498a0 100644
--- a/web/service/server.go
+++ b/web/service/server.go
@@ -7,6 +7,7 @@ import (
"fmt"
"io"
"io/fs"
+ "mime/multipart"
"net/http"
"os"
"os/exec"
@@ -14,7 +15,9 @@ import (
"strings"
"time"
"x-ui/config"
+ "x-ui/database"
"x-ui/logger"
+ "x-ui/util/common"
"x-ui/util/sys"
"x-ui/xray"
@@ -73,7 +76,8 @@ type Release struct {
}
type ServerService struct {
- xrayService XrayService
+ xrayService XrayService
+ inboundService InboundService
}
func (s *ServerService) GetStatus(lastStatus *Status) *Status {
@@ -393,6 +397,106 @@ func (s *ServerService) GetDb() ([]byte, error) {
return fileContents, nil
}
+func (s *ServerService) ImportDB(file multipart.File) error {
+ // Check if the file is a SQLite database
+ isValidDb, err := database.IsSQLiteDB(file)
+ if err != nil {
+ return common.NewErrorf("Error checking db file format: %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)
+ }
+
+ // Save the file as temporary file
+ tempPath := fmt.Sprintf("%s.temp", config.GetDBPath())
+ // Remove the existing fallback file (if any) before creating one
+ _, err = os.Stat(tempPath)
+ if err == nil {
+ errRemove := os.Remove(tempPath)
+ if 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)
+ }
+ defer tempFile.Close()
+
+ // Remove temp file before returning
+ defer os.Remove(tempPath)
+
+ // Save uploaded file to temporary file
+ _, err = io.Copy(tempFile, file)
+ if err != nil {
+ return common.NewErrorf("Error saving db: %v", err)
+ }
+
+ // Check if we can init db or not
+ err = database.InitDB(tempPath)
+ if err != nil {
+ return common.NewErrorf("Error checking db: %v", err)
+ }
+
+ // Stop Xray
+ s.StopXrayService()
+
+ // Backup the current database for fallback
+ fallbackPath := fmt.Sprintf("%s.backup", config.GetDBPath())
+ // Remove the existing fallback file (if any)
+ _, err = os.Stat(fallbackPath)
+ if err == nil {
+ errRemove := os.Remove(fallbackPath)
+ if errRemove != nil {
+ return common.NewErrorf("Error removing existing fallback db file: %v", errRemove)
+ }
+ }
+ // Move the current database to the fallback location
+ err = os.Rename(config.GetDBPath(), fallbackPath)
+ if err != nil {
+ return common.NewErrorf("Error backing up temporary db file: %v", err)
+ }
+
+ // Remove the temporary file before returning
+ defer os.Remove(fallbackPath)
+
+ // Move temp to DB path
+ err = os.Rename(tempPath, config.GetDBPath())
+ if err != nil {
+ errRename := os.Rename(fallbackPath, config.GetDBPath())
+ if errRename != nil {
+ return common.NewErrorf("Error moving db file and restoring fallback: %v", errRename)
+ }
+ return common.NewErrorf("Error moving db file: %v", err)
+ }
+
+ // Migrate DB
+ err = database.InitDB(config.GetDBPath())
+ if err != nil {
+ errRename := os.Rename(fallbackPath, config.GetDBPath())
+ if errRename != nil {
+ return common.NewErrorf("Error migrating db and restoring fallback: %v", errRename)
+ }
+ return common.NewErrorf("Error migrating db: %v", err)
+ }
+ s.inboundService.MigrateDB()
+
+ // Start Xray
+ err = s.RestartXrayService()
+ if err != nil {
+ return common.NewErrorf("Imported DB but Failed to start Xray: %v", err)
+ }
+
+ return nil
+}
+
func (s *ServerService) GetNewX25519Cert() (interface{}, error) {
// Run the command
cmd := exec.Command(xray.GetBinaryPath(), "x25519")
diff --git a/web/translation/translate.en_US.toml b/web/translation/translate.en_US.toml
index 5d27497c..7e0a869c 100644
--- a/web/translation/translate.en_US.toml
+++ b/web/translation/translate.en_US.toml
@@ -89,6 +89,13 @@
"xraySwitchVersionDialog" = "Switch xray version"
"xraySwitchVersionDialogDesc" = "Whether to switch the xray version to"
"dontRefreshh" = "Installation is in progress, please do not refresh this page"
+"logs" = "Logs"
+"config" = "Config"
+"backup" = "Backup"
+"backupTitle" = "Backup Database"
+"backupDescription" = "Remember to backup before importing a new database."
+"exportDatabase" = "Download Database"
+"importDatabase" = "Upload Database"
[pages.inbounds]
"title" = "Inbounds"
diff --git a/web/translation/translate.fa_IR.toml b/web/translation/translate.fa_IR.toml
index 277db594..ce48f647 100644
--- a/web/translation/translate.fa_IR.toml
+++ b/web/translation/translate.fa_IR.toml
@@ -89,6 +89,13 @@
"xraySwitchVersionDialog" = "تغییر ورژن Xray"
"xraySwitchVersionDialogDesc" = "آیا از تغییر ورژن مطمئن هستین"
"dontRefreshh" = "در حال نصب ، لطفا رفرش نکنید "
+"logs" = "گزارش ها"
+"config" = "تنظیمات"
+"backup" = "پشتیبان گیری"
+"backupTitle" = "پشتیبان گیری دیتابیس"
+"backupDescription" = "به یاد داشته باشید که قبل از وارد کردن یک دیتابیس جدید، نسخه پشتیبان تهیه کنید."
+"exportDatabase" = "دانلود دیتابیس"
+"importDatabase" = "آپلود دیتابیس"
[pages.inbounds]
"title" = "کاربران"
diff --git a/web/translation/translate.zh_Hans.toml b/web/translation/translate.zh_Hans.toml
index f396340f..e678fc23 100644
--- a/web/translation/translate.zh_Hans.toml
+++ b/web/translation/translate.zh_Hans.toml
@@ -89,6 +89,13 @@
"xraySwitchVersionDialog" = "切换 xray 版本"
"xraySwitchVersionDialogDesc" = "是否切换 xray 版本至"
"dontRefreshh" = "安装中,请不要刷新此页面"
+"logs" = "日志"
+"config" = "配置"
+"backup" = "备份"
+"backupTitle" = "备份数据库"
+"backupDescription" = "请记住在导入新数据库之前进行备份。"
+"exportDatabase" = "下载数据库"
+"importDatabase" = "上传数据库"
[pages.inbounds]
"title" = "入站列表"