Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] import/export database in the panel #347

Merged
merged 13 commits into from
May 6, 2023
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.idea
.vscode
tmp
backup/
bin/
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ apt-get install certbot -y
certbot certonly --standalone --agree-tos --register-unsafely-without-email -d yourdomain.com
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

Expand Down Expand Up @@ -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
- Fix api routes (user setting will create with api)
- Support to change configs by different items provided in panel
- Support export/import database from panel

# Tg robot use

Expand Down Expand Up @@ -194,7 +195,6 @@ Reference syntax:

- Tron USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`


# Pictures

![1](./media/1.png)
Expand Down
12 changes: 12 additions & 0 deletions database/db.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package database

import (
"bytes"
"io"
"io/fs"
"os"
"path"
Expand Down Expand Up @@ -104,3 +106,13 @@ func GetDB() *gorm.DB {
func IsNotFound(err error) bool {
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 changes: 1 addition & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,8 +212,7 @@ func migrateDb() {
log.Fatal(err)
}
fmt.Println("Start migrating database...")
inboundService.MigrationRequirements()
inboundService.RemoveOrphanedTraffics()
inboundService.MigrateDB()
fmt.Println("Migration done!")
}

Expand Down
2 changes: 1 addition & 1 deletion web/assets/css/custom.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#app {
height: 100%;
height: 100vh;
}

.ant-space {
Expand Down
12 changes: 8 additions & 4 deletions web/assets/js/axios-init.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
);
);
26 changes: 24 additions & 2 deletions web/controller/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down Expand Up @@ -99,16 +100,15 @@ 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 {
jsonMsg(c, "", err)
return
}
jsonMsg(c, "Xray restarted", err)

}

func (a *ServerController) getLogs(c *gin.Context) {
Expand Down Expand Up @@ -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 {
Expand Down
3 changes: 2 additions & 1 deletion web/html/common/text_modal.html
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
104 changes: 93 additions & 11 deletions web/html/xui/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand Down Expand Up @@ -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 : ''"
Expand All @@ -201,6 +202,7 @@ <h2>{{ i18n "pages.index.xraySwitchClickDesk"}}</h2>
</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 : ''"
Expand All @@ -227,10 +229,28 @@ <h2>{{ i18n "pages.index.xraySwitchClickDesk"}}</h2>
{{ 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"}}
Expand Down Expand Up @@ -339,6 +359,29 @@ <h2>{{ i18n "pages.index.xraySwitchClickDesk"}}</h2>
},
};

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',
Expand All @@ -347,6 +390,7 @@ <h2>{{ i18n "pages.index.xraySwitchClickDesk"}}</h2>
status: new Status(),
versionModal,
logModal,
backupModal,
spinning: false,
loadingTip: '{{ i18n "loading"}}',
},
Expand Down Expand Up @@ -388,7 +432,6 @@ <h2>{{ i18n "pages.index.xraySwitchClickDesk"}}</h2>
},
});
},
//here add stop xray function
async stopXrayService() {
this.loading(true);
const msg = await HttpUtil.post('server/stopXrayService');
Expand All @@ -397,7 +440,6 @@ <h2>{{ i18n "pages.index.xraySwitchClickDesk"}}</h2>
return;
}
},
//here add restart xray function
async restartXrayService() {
this.loading(true);
const msg = await HttpUtil.post('server/restartXrayService');
Expand All @@ -413,20 +455,60 @@ <h2>{{ i18n "pages.index.xraySwitchClickDesk"}}</h2>
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) {
Expand Down
2 changes: 1 addition & 1 deletion web/html/xui/settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -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;'">
Expand Down
11 changes: 10 additions & 1 deletion web/service/inbound.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -616,6 +618,7 @@ func (s *InboundService) RemoveOrphanedTraffics() {
)
`)
}

func (s *InboundService) AddClientStat(inboundId int, client *model.Client) error {
db := database.GetDB()

Expand All @@ -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()

Expand Down Expand Up @@ -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()
}
Loading