diff options
| author | Ho3ein <ho3ein.sanaei@gmail.com> | 2023-05-06 12:23:41 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-05-06 12:23:41 +0300 |
| commit | ac31d6d9fb666ba8deec353d0ba783eb6a8b231f (patch) | |
| tree | ea1e1764da0208035a7000f7d884a6c36b55d830 /web | |
| parent | 78638a97373bccc761f46cbbef0fda34a8dedd6c (diff) | |
| parent | 83c853ffb6b896c8a6d1eef4e0354ba1201ebf13 (diff) | |
Merge pull request #347 from hamid-gh98/main
[Feature] import/export database in the panel
Diffstat (limited to 'web')
| -rw-r--r-- | web/assets/css/custom.css | 2 | ||||
| -rw-r--r-- | web/assets/js/axios-init.js | 12 | ||||
| -rw-r--r-- | web/controller/server.go | 26 | ||||
| -rw-r--r-- | web/html/common/text_modal.html | 3 | ||||
| -rw-r--r-- | web/html/xui/index.html | 104 | ||||
| -rw-r--r-- | web/html/xui/settings.html | 2 | ||||
| -rw-r--r-- | web/service/inbound.go | 11 | ||||
| -rw-r--r-- | web/service/server.go | 106 | ||||
| -rw-r--r-- | web/translation/translate.en_US.toml | 7 | ||||
| -rw-r--r-- | web/translation/translate.fa_IR.toml | 7 | ||||
| -rw-r--r-- | web/translation/translate.zh_Hans.toml | 7 |
11 files changed, 265 insertions, 22 deletions
diff --git a/web/assets/css/custom.css b/web/assets/css/custom.css index d13e3159..671ba24f 100644 --- a/web/assets/css/custom.css +++ b/web/assets/css/custom.css @@ -1,5 +1,5 @@ #app { - height: 100%; + height: 100vh; } .ant-space { diff --git a/web/assets/js/axios-init.js b/web/assets/js/axios-init.js index 22d14d76..bd55c3cf 100644 --- a/web/assets/js/axios-init.js +++ b/web/assets/js/axios-init.js @@ -3,10 +3,14 @@ axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; axios.interceptors.request.use( config => { - config.data = Qs.stringify(config.data, { - arrayFormat: 'repeat' - }); + if (config.data instanceof FormData) { + config.headers['Content-Type'] = 'multipart/form-data'; + } else { + config.data = Qs.stringify(config.data, { + arrayFormat: 'repeat', + }); + } return config; }, error => Promise.reject(error) -);
\ No newline at end of file +); diff --git a/web/controller/server.go b/web/controller/server.go index c365ae4b..9e649e6c 100644 --- a/web/controller/server.go +++ b/web/controller/server.go @@ -41,6 +41,7 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) { g.POST("/logs/:count", a.getLogs) g.POST("/getConfigJson", a.getConfigJson) g.GET("/getDb", a.getDb) + g.POST("/importDB", a.importDB) g.POST("/getNewX25519Cert", a.getNewX25519Cert) } @@ -99,8 +100,8 @@ func (a *ServerController) stopXrayService(c *gin.Context) { return } jsonMsg(c, "Xray stoped", err) - } + func (a *ServerController) restartXrayService(c *gin.Context) { err := a.serverService.RestartXrayService() if err != nil { @@ -108,7 +109,6 @@ func (a *ServerController) restartXrayService(c *gin.Context) { return } jsonMsg(c, "Xray restarted", err) - } func (a *ServerController) getLogs(c *gin.Context) { @@ -144,6 +144,28 @@ func (a *ServerController) getDb(c *gin.Context) { c.Writer.Write(db) } +func (a *ServerController) importDB(c *gin.Context) { + // Get the file from the request body + file, _, err := c.Request.FormFile("db") + if err != nil { + jsonMsg(c, "Error reading db file", err) + return + } + defer file.Close() + // Always restart Xray before return + defer a.serverService.RestartXrayService() + defer func() { + a.lastGetStatusTime = time.Now() + }() + // Import it + err = a.serverService.ImportDB(file) + if err != nil { + jsonMsg(c, "", err) + return + } + jsonObj(c, "Import DB", nil) +} + func (a *ServerController) getNewX25519Cert(c *gin.Context) { cert, err := a.serverService.GetNewX25519Cert() if err != nil { diff --git a/web/html/common/text_modal.html b/web/html/common/text_modal.html index b2da6160..ce77d0ca 100644 --- a/web/html/common/text_modal.html +++ b/web/html/common/text_modal.html @@ -4,7 +4,8 @@ :class="siderDrawer.isDarkTheme ? darkClass : ''" :ok-button-props="{attrs:{id:'txt-modal-ok-btn'}}"> <a-button v-if="!ObjectUtil.isEmpty(txtModal.fileName)" type="primary" style="margin-bottom: 10px;" - :href="'data:application/text;charset=utf-8,' + encodeURIComponent(txtModal.content)" :download="txtModal.fileName"> + :href="'data:application/text;charset=utf-8,' + encodeURIComponent(txtModal.content)" + :download="txtModal.fileName"> {{ i18n "download" }} [[ txtModal.fileName ]] </a-button> <a-input type="textarea" v-model="txtModal.content" diff --git a/web/html/xui/index.html b/web/html/xui/index.html index f2babcb0..c2adffde 100644 --- a/web/html/xui/index.html +++ b/web/html/xui/index.html @@ -111,9 +111,9 @@ <a-col :sm="24" :md="12"> <a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''"> {{ i18n "menu.link" }}: - <a-tag color="blue" style="cursor: pointer;" @click="openLogs(20)">Log Reports</a-tag> - <a-tag color="blue" style="cursor: pointer;" @click="openConfig">Config</a-tag> - <a-tag color="blue" style="cursor: pointer;" @click="getBackup">Backup</a-tag> + <a-tag color="blue" style="cursor: pointer;" @click="openLogs(20)">{{ i18n "pages.index.logs" }}</a-tag> + <a-tag color="blue" style="cursor: pointer;" @click="openConfig">{{ i18n "pages.index.config" }}</a-tag> + <a-tag color="blue" style="cursor: pointer;" @click="openBackup">{{ i18n "pages.index.backup" }}</a-tag> </a-card> </a-col> <a-col :sm="24" :md="12"> @@ -188,6 +188,7 @@ </transition> </a-layout-content> </a-layout> + <a-modal id="version-modal" v-model="versionModal.visible" title='{{ i18n "pages.index.xraySwitch" }}' :closable="true" @ok="() => versionModal.visible = false" :class="siderDrawer.isDarkTheme ? darkClass : ''" @@ -201,6 +202,7 @@ </a-tag> </template> </a-modal> + <a-modal id="log-modal" v-model="logModal.visible" title="X-UI logs" :closable="true" @ok="() => logModal.visible = false" @cancel="() => logModal.visible = false" :class="siderDrawer.isDarkTheme ? darkClass : ''" @@ -227,10 +229,28 @@ {{ i18n "download" }} x-ui.log </a-button> </a-form-item> - </a-form> + </a-form> <a-input type="textarea" v-model="logModal.logs" disabled="true" :autosize="{ minRows: 10, maxRows: 22}"></a-input> </a-modal> + + <a-modal id="backup-modal" v-model="backupModal.visible" :title="backupModal.title" + :closable="true" :class="siderDrawer.isDarkTheme ? darkClass : ''" + @ok="() => backupModal.hide()" @cancel="() => backupModal.hide()"> + <p style="color: inherit; font-size: 16px; padding: 4px 2px;"> + <a-icon type="warning" style="color: inherit; font-size: 20px;"></a-icon> + [[ backupModal.description ]] + </p> + <a-space direction="horizontal" align="center" style="margin-bottom: 10px;"> + <a-button type="primary" @click="exportDatabase()"> + [[ backupModal.exportText ]] + </a-button> + <a-button type="primary" @click="importDatabase()"> + [[ backupModal.importText ]] + </a-button> + </a-space> + </a-modal> + </a-layout> {{template "js" .}} {{template "textModal"}} @@ -339,6 +359,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', @@ -347,6 +390,7 @@ status: new Status(), versionModal, logModal, + backupModal, spinning: false, loadingTip: '{{ i18n "loading"}}', }, @@ -388,7 +432,6 @@ }, }); }, - //here add stop xray function async stopXrayService() { this.loading(true); const msg = await HttpUtil.post('server/stopXrayService'); @@ -397,7 +440,6 @@ return; } }, - //here add restart xray function async restartXrayService() { this.loading(true); const msg = await HttpUtil.post('server/restartXrayService'); @@ -413,20 +455,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/html/xui/settings.html b/web/html/xui/settings.html index 5b5a91dc..e1237971 100644 --- a/web/html/xui/settings.html +++ b/web/html/xui/settings.html @@ -74,7 +74,7 @@ </a-list> </a-tab-pane> - <a-tab-pane key="2" tab='{{ i18n "pages.settings.securitySettings"}}' style="padding-top: 5px;"> + <a-tab-pane key="2" tab='{{ i18n "pages.settings.securitySettings"}}' style="padding: 20px;"> <a-tabs default-active-key="sec-1" :class="siderDrawer.isDarkTheme ? darkClass : ''"> <a-tab-pane key="sec-1" tab='{{ i18n "pages.settings.security.admin"}}'> <a-form :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65); padding: 20px;': 'background: white; padding: 20px;'"> diff --git a/web/service/inbound.go b/web/service/inbound.go index dd633c4b..b14f7b4f 100644 --- a/web/service/inbound.go +++ b/web/service/inbound.go @@ -595,6 +595,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 @@ -605,7 +606,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 @@ -616,6 +618,7 @@ func (s *InboundService) RemoveOrphanedTraffics() { ) `) } + func (s *InboundService) AddClientStat(inboundId int, client *model.Client) error { db := database.GetDB() @@ -634,6 +637,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() @@ -1166,3 +1170,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 1108926b..d8a2239b 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 { @@ -395,6 +399,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 bc5bb942..42d67daf 100644 --- a/web/translation/translate.en_US.toml +++ b/web/translation/translate.en_US.toml @@ -90,6 +90,13 @@ "xraySwitchVersionDialog" = "Switch Xray Version" "xraySwitchVersionDialogDesc" = "Are you sure you want to switch the Xray version to" "dontRefresh" = "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 f2e7e93f..b0bb6a8c 100644 --- a/web/translation/translate.fa_IR.toml +++ b/web/translation/translate.fa_IR.toml @@ -90,6 +90,13 @@ "xraySwitchVersionDialog" = "تغییر ورژن" "xraySwitchVersionDialogDesc" = "آیا از تغییر ورژن مطمئن هستین" "dontRefresh" = "در حال نصب ، لطفا رفرش نکنید " +"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 c0ba24c6..6e78be09 100644 --- a/web/translation/translate.zh_Hans.toml +++ b/web/translation/translate.zh_Hans.toml @@ -90,6 +90,13 @@ "xraySwitchVersionDialog" = "切换 xray 版本" "xraySwitchVersionDialogDesc" = "是否切换 xray 版本至" "dontRefresh" = "安装中,请不要刷新此页面" +"logs" = "日志" +"config" = "配置" +"backup" = "备份" +"backupTitle" = "备份数据库" +"backupDescription" = "请记住在导入新数据库之前进行备份。" +"exportDatabase" = "下载数据库" +"importDatabase" = "上传数据库" [pages.inbounds] "title" = "入站列表" |
