Skip to content

Commit

Permalink
option backup restore #238
Browse files Browse the repository at this point in the history
  • Loading branch information
alireza0 committed Jan 20, 2025
1 parent 049cfc5 commit f3432b1
Show file tree
Hide file tree
Showing 11 changed files with 442 additions and 33 deletions.
21 changes: 21 additions & 0 deletions backend/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ package api

import (
"encoding/json"
"s-ui/database"
"s-ui/logger"
"s-ui/service"
"s-ui/util"
"s-ui/util/common"
"strconv"
"strings"
"time"

"github.com/gin-gonic/gin"
)
Expand Down Expand Up @@ -109,6 +111,15 @@ func (a *APIHandler) postHandler(c *gin.Context) {
link := c.Request.FormValue("link")
result, _, err := util.GetOutbound(link, 0)
jsonObj(c, result, err)
case "importdb":
file, _, err := c.Request.FormFile("db")
if err != nil {
jsonMsg(c, "", err)
return
}
defer file.Close()
err = database.ImportDB(file)
jsonMsg(c, "", err)
default:
jsonMsg(c, "failed", common.NewError("unknown action: ", action))
}
Expand Down Expand Up @@ -188,6 +199,16 @@ func (a *APIHandler) getHandler(c *gin.Context) {
options := c.Query("o")
keypair := a.ServerService.GenKeypair(kType, options)
jsonObj(c, keypair, nil)
case "getdb":
exclude := c.Query("exclude")
db, err := database.GetDb(exclude)
if err != nil {
jsonMsg(c, "", err)
return
}
c.Header("Content-Type", "application/octet-stream")
c.Header("Content-Disposition", "attachment; filename=s-ui_"+time.Now().Format("20060102-150405")+".db")
c.Writer.Write(db)
default:
jsonMsg(c, "failed", common.NewError("unknown action: ", action))
}
Expand Down
271 changes: 271 additions & 0 deletions backend/database/backup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
package database

import (
"bytes"
"fmt"
"io"
"mime/multipart"
"os"
"path/filepath"
"s-ui/cmd/migration"
"s-ui/config"
"s-ui/database/model"
"s-ui/logger"
"s-ui/util/common"
"strings"
"syscall"
"time"

"gorm.io/driver/sqlite"
"gorm.io/gorm"
)

func GetDb(exclude string) ([]byte, error) {
exclude_changes, exclude_stats := false, false
for _, table := range strings.Split(exclude, ",") {
if table == "changes" {
exclude_changes = true
} else if table == "stats" {
exclude_stats = true
}
}

dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
if err != nil {
return nil, err
}
dbPath := dir + config.GetName() + "_" + time.Now().Format("20060102-200203") + ".db"

backupDb, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
if err != nil {
return nil, err
}

err = backupDb.AutoMigrate(
&model.Setting{},
&model.Tls{},
&model.Inbound{},
&model.Outbound{},
&model.Endpoint{},
&model.User{},
&model.Stats{},
&model.Client{},
&model.Changes{},
)
if err != nil {
return nil, err
}

var settings []model.Setting
var tls []model.Tls
var inbound []model.Inbound
var outbound []model.Outbound
var endpoint []model.Endpoint
var users []model.User
var clients []model.Client
var stats []model.Stats
var changes []model.Changes

// Perform scans and handle errors
if err := db.Model(&model.Setting{}).Scan(&settings).Error; err != nil {
return nil, err
}
if err := db.Model(&model.Tls{}).Scan(&tls).Error; err != nil {
return nil, err
}
if err := db.Model(&model.Inbound{}).Scan(&inbound).Error; err != nil {
return nil, err
}
if err := db.Model(&model.Outbound{}).Scan(&outbound).Error; err != nil {
return nil, err
}
if err := db.Model(&model.Endpoint{}).Scan(&endpoint).Error; err != nil {
return nil, err
}
if err := db.Model(&model.User{}).Scan(&users).Error; err != nil {
return nil, err
}
if err := db.Model(&model.Client{}).Scan(&clients).Error; err != nil {
return nil, err
}

// Save each model
for _, mdl := range []interface{}{settings, tls, inbound, outbound, endpoint, users, clients} {
if err := backupDb.Save(mdl).Error; err != nil {
return nil, err
}
}

if !exclude_stats {
if err := db.Model(&model.Stats{}).Scan(&stats).Error; err != nil {
return nil, err
}
if err := backupDb.Save(stats).Error; err != nil {
return nil, err
}
}
if !exclude_changes {
if err := db.Model(&model.Changes{}).Scan(&changes).Error; err != nil {
return nil, err
}
if err := backupDb.Save(changes).Error; err != nil {
return nil, err
}
}

// Update WAL
err = backupDb.Exec("PRAGMA wal_checkpoint;").Error
if err != nil {
return nil, err
}

bdb, _ := backupDb.DB()
bdb.Close()

// Open the file for reading
file, err := os.Open(dbPath)
if err != nil {
return nil, err
}
defer file.Close()
defer os.Remove(dbPath)

// Read the file contents
fileContents, err := io.ReadAll(file)
if err != nil {
return nil, err
}

return fileContents, nil
}

func ImportDB(file multipart.File) error {
// Check if the file is a SQLite database
isValidDb, err := 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)

// Close old DB
old_db, _ := db.DB()
old_db.Close()

// 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
newDb, err := gorm.Open(sqlite.Open(tempPath), &gorm.Config{})
if err != nil {
return common.NewErrorf("Error checking db: %v", err)
}
newDb_db, _ := newDb.DB()
newDb_db.Close()

// 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
migration.MigrateDb()
err = 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)
}

// Restart app
err = SendSighup()
if err != nil {
return common.NewErrorf("Error restarting app: %v", err)
}

return nil
}

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
}

func SendSighup() error {
// Get the current process
process, err := os.FindProcess(os.Getpid())
if err != nil {
return err
}

// Send SIGHUP to the current process
go func() {
time.Sleep(3 * time.Second)
err := process.Signal(syscall.SIGHUP)
if err != nil {
logger.Error("send signal SIGHUP failed:", err)
}
}()
return nil
}
32 changes: 7 additions & 25 deletions frontend/src/components/Main.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
<template>
<LogVue
v-model="logModal.visible"
:visible="logModal.visible"
@close="closeLogs"
/>
<LogVue v-model="logModal.visible" :control="logModal" :visible="logModal.visible" />
<Backup v-model="backupModal.visible" :control="backupModal" :visible="backupModal.visible" />
<v-container class="fill-height" :loading="loading">
<v-responsive :class="reloadItems.length>0 ? 'fill-height text-center' : 'align-center'" >
<v-row class="d-flex align-center justify-center">
Expand Down Expand Up @@ -46,6 +43,8 @@
</v-row>
</v-card>
</v-dialog>
<v-btn variant="tonal" hide-details style="margin-inline-start: 10px;" @click="backupModal.visible = true">{{ $t('main.backup.title') }} <v-icon icon="mdi-backup-restore" /></v-btn>
<v-btn variant="tonal" hide-details style="margin-inline-start: 10px;" @click="logModal.visible = true">{{ $t('basic.log.title') }} <v-icon icon="mdi-list-box-outline" /></v-btn>
</v-col>
</v-row>
<v-row>
Expand Down Expand Up @@ -86,18 +85,8 @@
<v-col cols="3">S-UI</v-col>
<v-col cols="9">
<v-chip density="compact" color="blue">
<v-tooltip activator="parent" location="top">
{{ $t('main.info.threads') }}: {{ tilesData.sys?.appThreads }}<br />
{{ $t('main.info.memory') }}: {{ HumanReadable.sizeFormat(tilesData.sys?.appMem) }}
</v-tooltip>
v{{ tilesData.sys?.appVersion }}
</v-chip>
<v-chip density="compact" color="transparent" style="cursor: pointer;" @click="openLogs()">
<v-tooltip activator="parent" location="top">
{{ $t('basic.log.title') + " - S-UI" }}
</v-tooltip>
<v-icon icon="mdi-list-box-outline" color="blue" />
</v-chip>
</v-col>
<v-col cols="3">{{ $t('main.info.uptime') }}</v-col>
<v-col cols="9">{{ HumanReadable.formatSecond(tilesData.uptime) }}</v-col>
Expand Down Expand Up @@ -166,6 +155,7 @@ import History from '@/components/tiles/History.vue'
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { i18n } from '@/locales'
import LogVue from '@/layouts/modals/Logs.vue'
import Backup from '@/layouts/modals/Backup.vue'
const loading = ref(false)
const menu = ref(false)
Expand Down Expand Up @@ -235,17 +225,9 @@ onBeforeUnmount(() => {
stopTimer()
})
const logModal = ref({
visible: false,
})
const openLogs = () => {
logModal.value.visible = true
}
const logModal = ref({ visible: false })
const closeLogs = () => {
logModal.value.visible = false
}
const backupModal = ref({ visible: false })
const restartSingbox = async () => {
loading.value = true
Expand Down
Loading

0 comments on commit f3432b1

Please sign in to comment.