mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2025-03-01 01:20:49 +03:00
Merge pull request #347 from hamid-gh98/main
[Feature] import/export database in the panel
This commit is contained in:
commit
ac31d6d9fb
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,4 +1,5 @@
|
|||||||
.idea
|
.idea
|
||||||
|
.vscode
|
||||||
tmp
|
tmp
|
||||||
backup/
|
backup/
|
||||||
bin/
|
bin/
|
||||||
|
@ -33,8 +33,8 @@ apt-get install certbot -y
|
|||||||
certbot certonly --standalone --agree-tos --register-unsafely-without-email -d yourdomain.com
|
certbot certonly --standalone --agree-tos --register-unsafely-without-email -d yourdomain.com
|
||||||
certbot renew --dry-run
|
certbot renew --dry-run
|
||||||
```
|
```
|
||||||
or you can use x-ui menu then number '16' (Apply for an SSL Certificate)
|
|
||||||
|
|
||||||
|
or you can use x-ui menu then number '16' (Apply for an SSL Certificate)
|
||||||
|
|
||||||
# Default settings
|
# Default settings
|
||||||
|
|
||||||
@ -116,6 +116,7 @@ If you want to use routing to WARP follow steps as below:
|
|||||||
- For more advanced configuration items, please refer to the panel
|
- For more advanced configuration items, please refer to the panel
|
||||||
- Fix api routes (user setting will create with api)
|
- Fix api routes (user setting will create with api)
|
||||||
- Support to change configs by different items provided in panel
|
- Support to change configs by different items provided in panel
|
||||||
|
- Support export/import database from panel
|
||||||
|
|
||||||
# Tg robot use
|
# Tg robot use
|
||||||
|
|
||||||
@ -194,7 +195,6 @@ Reference syntax:
|
|||||||
|
|
||||||
- Tron USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
|
- Tron USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
|
||||||
|
|
||||||
|
|
||||||
# Pictures
|
# Pictures
|
||||||
|
|
||||||

|

|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package database
|
package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
@ -104,3 +106,13 @@ func GetDB() *gorm.DB {
|
|||||||
func IsNotFound(err error) bool {
|
func IsNotFound(err error) bool {
|
||||||
return err == gorm.ErrRecordNotFound
|
return err == gorm.ErrRecordNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsSQLiteDB(file io.Reader) (bool, error) {
|
||||||
|
signature := []byte("SQLite format 3\x00")
|
||||||
|
buf := make([]byte, len(signature))
|
||||||
|
_, err := file.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return bytes.Equal(buf, signature), nil
|
||||||
|
}
|
||||||
|
3
main.go
3
main.go
@ -212,8 +212,7 @@ func migrateDb() {
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
fmt.Println("Start migrating database...")
|
fmt.Println("Start migrating database...")
|
||||||
inboundService.MigrationRequirements()
|
inboundService.MigrateDB()
|
||||||
inboundService.RemoveOrphanedTraffics()
|
|
||||||
fmt.Println("Migration done!")
|
fmt.Println("Migration done!")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#app {
|
#app {
|
||||||
height: 100%;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-space {
|
.ant-space {
|
||||||
|
@ -3,10 +3,14 @@ axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
|||||||
|
|
||||||
axios.interceptors.request.use(
|
axios.interceptors.request.use(
|
||||||
config => {
|
config => {
|
||||||
config.data = Qs.stringify(config.data, {
|
if (config.data instanceof FormData) {
|
||||||
arrayFormat: 'repeat'
|
config.headers['Content-Type'] = 'multipart/form-data';
|
||||||
});
|
} else {
|
||||||
|
config.data = Qs.stringify(config.data, {
|
||||||
|
arrayFormat: 'repeat',
|
||||||
|
});
|
||||||
|
}
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
error => Promise.reject(error)
|
error => Promise.reject(error)
|
||||||
);
|
);
|
||||||
|
@ -41,6 +41,7 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) {
|
|||||||
g.POST("/logs/:count", a.getLogs)
|
g.POST("/logs/:count", a.getLogs)
|
||||||
g.POST("/getConfigJson", a.getConfigJson)
|
g.POST("/getConfigJson", a.getConfigJson)
|
||||||
g.GET("/getDb", a.getDb)
|
g.GET("/getDb", a.getDb)
|
||||||
|
g.POST("/importDB", a.importDB)
|
||||||
g.POST("/getNewX25519Cert", a.getNewX25519Cert)
|
g.POST("/getNewX25519Cert", a.getNewX25519Cert)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,8 +100,8 @@ func (a *ServerController) stopXrayService(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
jsonMsg(c, "Xray stoped", err)
|
jsonMsg(c, "Xray stoped", err)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ServerController) restartXrayService(c *gin.Context) {
|
func (a *ServerController) restartXrayService(c *gin.Context) {
|
||||||
err := a.serverService.RestartXrayService()
|
err := a.serverService.RestartXrayService()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -108,7 +109,6 @@ func (a *ServerController) restartXrayService(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
jsonMsg(c, "Xray restarted", err)
|
jsonMsg(c, "Xray restarted", err)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ServerController) getLogs(c *gin.Context) {
|
func (a *ServerController) getLogs(c *gin.Context) {
|
||||||
@ -144,6 +144,28 @@ func (a *ServerController) getDb(c *gin.Context) {
|
|||||||
c.Writer.Write(db)
|
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) {
|
func (a *ServerController) getNewX25519Cert(c *gin.Context) {
|
||||||
cert, err := a.serverService.GetNewX25519Cert()
|
cert, err := a.serverService.GetNewX25519Cert()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -4,7 +4,8 @@
|
|||||||
:class="siderDrawer.isDarkTheme ? darkClass : ''"
|
:class="siderDrawer.isDarkTheme ? darkClass : ''"
|
||||||
:ok-button-props="{attrs:{id:'txt-modal-ok-btn'}}">
|
:ok-button-props="{attrs:{id:'txt-modal-ok-btn'}}">
|
||||||
<a-button v-if="!ObjectUtil.isEmpty(txtModal.fileName)" type="primary" style="margin-bottom: 10px;"
|
<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 ]]
|
{{ i18n "download" }} [[ txtModal.fileName ]]
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-input type="textarea" v-model="txtModal.content"
|
<a-input type="textarea" v-model="txtModal.content"
|
||||||
|
@ -111,9 +111,9 @@
|
|||||||
<a-col :sm="24" :md="12">
|
<a-col :sm="24" :md="12">
|
||||||
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
|
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
|
||||||
{{ i18n "menu.link" }}:
|
{{ 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="openLogs(20)">{{ i18n "pages.index.logs" }}</a-tag>
|
||||||
<a-tag color="blue" style="cursor: pointer;" @click="openConfig">Config</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="getBackup">Backup</a-tag>
|
<a-tag color="blue" style="cursor: pointer;" @click="openBackup">{{ i18n "pages.index.backup" }}</a-tag>
|
||||||
</a-card>
|
</a-card>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :sm="24" :md="12">
|
<a-col :sm="24" :md="12">
|
||||||
@ -188,6 +188,7 @@
|
|||||||
</transition>
|
</transition>
|
||||||
</a-layout-content>
|
</a-layout-content>
|
||||||
</a-layout>
|
</a-layout>
|
||||||
|
|
||||||
<a-modal id="version-modal" v-model="versionModal.visible" title='{{ i18n "pages.index.xraySwitch" }}'
|
<a-modal id="version-modal" v-model="versionModal.visible" title='{{ i18n "pages.index.xraySwitch" }}'
|
||||||
:closable="true" @ok="() => versionModal.visible = false"
|
:closable="true" @ok="() => versionModal.visible = false"
|
||||||
:class="siderDrawer.isDarkTheme ? darkClass : ''"
|
:class="siderDrawer.isDarkTheme ? darkClass : ''"
|
||||||
@ -201,6 +202,7 @@
|
|||||||
</a-tag>
|
</a-tag>
|
||||||
</template>
|
</template>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
|
|
||||||
<a-modal id="log-modal" v-model="logModal.visible" title="X-UI logs"
|
<a-modal id="log-modal" v-model="logModal.visible" title="X-UI logs"
|
||||||
:closable="true" @ok="() => logModal.visible = false" @cancel="() => logModal.visible = false"
|
:closable="true" @ok="() => logModal.visible = false" @cancel="() => logModal.visible = false"
|
||||||
:class="siderDrawer.isDarkTheme ? darkClass : ''"
|
:class="siderDrawer.isDarkTheme ? darkClass : ''"
|
||||||
@ -227,10 +229,28 @@
|
|||||||
{{ i18n "download" }} x-ui.log
|
{{ i18n "download" }} x-ui.log
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
<a-input type="textarea" v-model="logModal.logs" disabled="true"
|
<a-input type="textarea" v-model="logModal.logs" disabled="true"
|
||||||
:autosize="{ minRows: 10, maxRows: 22}"></a-input>
|
:autosize="{ minRows: 10, maxRows: 22}"></a-input>
|
||||||
</a-modal>
|
</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>
|
</a-layout>
|
||||||
{{template "js" .}}
|
{{template "js" .}}
|
||||||
{{template "textModal"}}
|
{{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({
|
const app = new Vue({
|
||||||
delimiters: ['[[', ']]'],
|
delimiters: ['[[', ']]'],
|
||||||
el: '#app',
|
el: '#app',
|
||||||
@ -347,6 +390,7 @@
|
|||||||
status: new Status(),
|
status: new Status(),
|
||||||
versionModal,
|
versionModal,
|
||||||
logModal,
|
logModal,
|
||||||
|
backupModal,
|
||||||
spinning: false,
|
spinning: false,
|
||||||
loadingTip: '{{ i18n "loading"}}',
|
loadingTip: '{{ i18n "loading"}}',
|
||||||
},
|
},
|
||||||
@ -388,7 +432,6 @@
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
//here add stop xray function
|
|
||||||
async stopXrayService() {
|
async stopXrayService() {
|
||||||
this.loading(true);
|
this.loading(true);
|
||||||
const msg = await HttpUtil.post('server/stopXrayService');
|
const msg = await HttpUtil.post('server/stopXrayService');
|
||||||
@ -397,7 +440,6 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
//here add restart xray function
|
|
||||||
async restartXrayService() {
|
async restartXrayService() {
|
||||||
this.loading(true);
|
this.loading(true);
|
||||||
const msg = await HttpUtil.post('server/restartXrayService');
|
const msg = await HttpUtil.post('server/restartXrayService');
|
||||||
@ -413,20 +455,60 @@
|
|||||||
if (!msg.success) {
|
if (!msg.success) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
logModal.show(msg.obj,rows);
|
logModal.show(msg.obj, rows);
|
||||||
},
|
},
|
||||||
async openConfig(){
|
async openConfig() {
|
||||||
this.loading(true);
|
this.loading(true);
|
||||||
const msg = await HttpUtil.post('server/getConfigJson');
|
const msg = await HttpUtil.post('server/getConfigJson');
|
||||||
this.loading(false);
|
this.loading(false);
|
||||||
if (!msg.success) {
|
if (!msg.success) {
|
||||||
return;
|
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';
|
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() {
|
async mounted() {
|
||||||
while (true) {
|
while (true) {
|
||||||
|
@ -74,7 +74,7 @@
|
|||||||
</a-list>
|
</a-list>
|
||||||
</a-tab-pane>
|
</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-tabs default-active-key="sec-1" :class="siderDrawer.isDarkTheme ? darkClass : ''">
|
||||||
<a-tab-pane key="sec-1" tab='{{ i18n "pages.settings.security.admin"}}'>
|
<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;'">
|
<a-form :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65); padding: 20px;': 'background: white; padding: 20px;'">
|
||||||
|
@ -595,6 +595,7 @@ func (s *InboundService) DisableInvalidInbounds() (int64, error) {
|
|||||||
count := result.RowsAffected
|
count := result.RowsAffected
|
||||||
return count, err
|
return count, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *InboundService) DisableInvalidClients() (int64, error) {
|
func (s *InboundService) DisableInvalidClients() (int64, error) {
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
now := time.Now().Unix() * 1000
|
now := time.Now().Unix() * 1000
|
||||||
@ -605,7 +606,8 @@ func (s *InboundService) DisableInvalidClients() (int64, error) {
|
|||||||
count := result.RowsAffected
|
count := result.RowsAffected
|
||||||
return count, err
|
return count, err
|
||||||
}
|
}
|
||||||
func (s *InboundService) RemoveOrphanedTraffics() {
|
|
||||||
|
func (s *InboundService) MigrationRemoveOrphanedTraffics() {
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
db.Exec(`
|
db.Exec(`
|
||||||
DELETE FROM client_traffics
|
DELETE FROM client_traffics
|
||||||
@ -616,6 +618,7 @@ func (s *InboundService) RemoveOrphanedTraffics() {
|
|||||||
)
|
)
|
||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *InboundService) AddClientStat(inboundId int, client *model.Client) error {
|
func (s *InboundService) AddClientStat(inboundId int, client *model.Client) error {
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
|
|
||||||
@ -634,6 +637,7 @@ func (s *InboundService) AddClientStat(inboundId int, client *model.Client) erro
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *InboundService) UpdateClientStat(email string, client *model.Client) error {
|
func (s *InboundService) UpdateClientStat(email string, client *model.Client) error {
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
|
|
||||||
@ -1166,3 +1170,8 @@ func (s *InboundService) MigrationRequirements() {
|
|||||||
// Remove orphaned traffics
|
// Remove orphaned traffics
|
||||||
db.Where("inbound_id = 0").Delete(xray.ClientTraffic{})
|
db.Where("inbound_id = 0").Delete(xray.ClientTraffic{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *InboundService) MigrateDB() {
|
||||||
|
s.MigrationRequirements()
|
||||||
|
s.MigrationRemoveOrphanedTraffics()
|
||||||
|
}
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@ -14,7 +15,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"x-ui/config"
|
"x-ui/config"
|
||||||
|
"x-ui/database"
|
||||||
"x-ui/logger"
|
"x-ui/logger"
|
||||||
|
"x-ui/util/common"
|
||||||
"x-ui/util/sys"
|
"x-ui/util/sys"
|
||||||
"x-ui/xray"
|
"x-ui/xray"
|
||||||
|
|
||||||
@ -73,7 +76,8 @@ type Release struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ServerService struct {
|
type ServerService struct {
|
||||||
xrayService XrayService
|
xrayService XrayService
|
||||||
|
inboundService InboundService
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ServerService) GetStatus(lastStatus *Status) *Status {
|
func (s *ServerService) GetStatus(lastStatus *Status) *Status {
|
||||||
@ -395,6 +399,106 @@ func (s *ServerService) GetDb() ([]byte, error) {
|
|||||||
return fileContents, nil
|
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) {
|
func (s *ServerService) GetNewX25519Cert() (interface{}, error) {
|
||||||
// Run the command
|
// Run the command
|
||||||
cmd := exec.Command(xray.GetBinaryPath(), "x25519")
|
cmd := exec.Command(xray.GetBinaryPath(), "x25519")
|
||||||
|
@ -90,6 +90,13 @@
|
|||||||
"xraySwitchVersionDialog" = "Switch Xray Version"
|
"xraySwitchVersionDialog" = "Switch Xray Version"
|
||||||
"xraySwitchVersionDialogDesc" = "Are you sure you want to switch the Xray version to"
|
"xraySwitchVersionDialogDesc" = "Are you sure you want to switch the Xray version to"
|
||||||
"dontRefresh" = "Installation is in progress, please do not refresh this page."
|
"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]
|
[pages.inbounds]
|
||||||
"title" = "Inbounds"
|
"title" = "Inbounds"
|
||||||
|
@ -90,6 +90,13 @@
|
|||||||
"xraySwitchVersionDialog" = "تغییر ورژن"
|
"xraySwitchVersionDialog" = "تغییر ورژن"
|
||||||
"xraySwitchVersionDialogDesc" = "آیا از تغییر ورژن مطمئن هستین"
|
"xraySwitchVersionDialogDesc" = "آیا از تغییر ورژن مطمئن هستین"
|
||||||
"dontRefresh" = "در حال نصب ، لطفا رفرش نکنید "
|
"dontRefresh" = "در حال نصب ، لطفا رفرش نکنید "
|
||||||
|
"logs" = "گزارش ها"
|
||||||
|
"config" = "تنظیمات"
|
||||||
|
"backup" = "پشتیبان گیری"
|
||||||
|
"backupTitle" = "پشتیبان گیری دیتابیس"
|
||||||
|
"backupDescription" = "به یاد داشته باشید که قبل از وارد کردن یک دیتابیس جدید، نسخه پشتیبان تهیه کنید."
|
||||||
|
"exportDatabase" = "دانلود دیتابیس"
|
||||||
|
"importDatabase" = "آپلود دیتابیس"
|
||||||
|
|
||||||
[pages.inbounds]
|
[pages.inbounds]
|
||||||
"title" = "کاربران"
|
"title" = "کاربران"
|
||||||
|
@ -90,6 +90,13 @@
|
|||||||
"xraySwitchVersionDialog" = "切换 xray 版本"
|
"xraySwitchVersionDialog" = "切换 xray 版本"
|
||||||
"xraySwitchVersionDialogDesc" = "是否切换 xray 版本至"
|
"xraySwitchVersionDialogDesc" = "是否切换 xray 版本至"
|
||||||
"dontRefresh" = "安装中,请不要刷新此页面"
|
"dontRefresh" = "安装中,请不要刷新此页面"
|
||||||
|
"logs" = "日志"
|
||||||
|
"config" = "配置"
|
||||||
|
"backup" = "备份"
|
||||||
|
"backupTitle" = "备份数据库"
|
||||||
|
"backupDescription" = "请记住在导入新数据库之前进行备份。"
|
||||||
|
"exportDatabase" = "下载数据库"
|
||||||
|
"importDatabase" = "上传数据库"
|
||||||
|
|
||||||
[pages.inbounds]
|
[pages.inbounds]
|
||||||
"title" = "入站列表"
|
"title" = "入站列表"
|
||||||
|
Loading…
Reference in New Issue
Block a user