2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 07:57:21 +08:00

feat: 数据备份前端

This commit is contained in:
耗子
2024-10-13 22:44:42 +08:00
parent e1bcabec5d
commit f235492f8b
30 changed files with 559 additions and 73 deletions

View File

@@ -2,12 +2,23 @@ package biz
import "github.com/TheTNB/panel/pkg/types"
type BackupType string
const (
BackupTypePath BackupType = "path"
BackupTypeWebsite BackupType = "website"
BackupTypeMySQL BackupType = "mysql"
BackupTypePostgres BackupType = "postgres"
BackupTypeRedis BackupType = "redis"
BackupTypePanel BackupType = "panel"
)
type BackupRepo interface {
List(typ string) ([]*types.BackupFile, error)
Create(typ, target string, path ...string) error
Delete(typ, name string) error
Restore(typ, backup, target string) error
List(typ BackupType) ([]*types.BackupFile, error)
Create(typ BackupType, target string, path ...string) error
Delete(typ BackupType, name string) error
Restore(typ BackupType, backup, target string) error
ClearExpired(path, prefix string, save int) error
CutoffLog(path, target string) error
GetPath(typ string) (string, error)
GetPath(typ BackupType) (string, error)
}

View File

@@ -34,37 +34,38 @@ func NewBackupRepo() biz.BackupRepo {
}
// List 备份列表
func (r *backupRepo) List(typ string) ([]*types.BackupFile, error) {
backupPath, err := r.GetPath(typ)
func (r *backupRepo) List(typ biz.BackupType) ([]*types.BackupFile, error) {
path, err := r.GetPath(typ)
if err != nil {
return nil, err
}
files, err := io.ReadDir(backupPath)
files, err := io.ReadDir(path)
if err != nil {
return nil, err
}
var backupList []*types.BackupFile
list := make([]*types.BackupFile, 0)
for _, file := range files {
info, err := file.Info()
if err != nil {
continue
}
backupList = append(backupList, &types.BackupFile{
list = append(list, &types.BackupFile{
Name: file.Name(),
Path: filepath.Join(path, file.Name()),
Size: str.FormatBytes(float64(info.Size())),
})
}
return backupList, nil
return list, nil
}
// Create 创建备份
// typ 备份类型
// target 目标名称
// path 可选备份保存路径
func (r *backupRepo) Create(typ, target string, path ...string) error {
func (r *backupRepo) Create(typ biz.BackupType, target string, path ...string) error {
defPath, err := r.GetPath(typ)
if err != nil {
return err
@@ -74,13 +75,13 @@ func (r *backupRepo) Create(typ, target string, path ...string) error {
}
switch typ {
case "website":
case biz.BackupTypeWebsite:
return r.createWebsite(defPath, target)
case "mysql":
case biz.BackupTypeMySQL:
return r.createMySQL(defPath, target)
case "postgres":
case biz.BackupTypePostgres:
return r.createPostgres(defPath, target)
case "panel":
case biz.BackupTypePanel:
return r.createPanel(defPath)
}
@@ -89,7 +90,7 @@ func (r *backupRepo) Create(typ, target string, path ...string) error {
}
// Delete 删除备份
func (r *backupRepo) Delete(typ, name string) error {
func (r *backupRepo) Delete(typ biz.BackupType, name string) error {
path, err := r.GetPath(typ)
if err != nil {
return err
@@ -103,7 +104,7 @@ func (r *backupRepo) Delete(typ, name string) error {
// typ 备份类型
// backup 备份压缩包,可以是绝对路径或者相对路径
// target 目标名称
func (r *backupRepo) Restore(typ, backup, target string) error {
func (r *backupRepo) Restore(typ biz.BackupType, backup, target string) error {
if !io.Exists(backup) {
path, err := r.GetPath(typ)
if err != nil {
@@ -113,11 +114,11 @@ func (r *backupRepo) Restore(typ, backup, target string) error {
}
switch typ {
case "website":
case biz.BackupTypeWebsite:
return r.restoreWebsite(backup, target)
case "mysql":
case biz.BackupTypeMySQL:
return r.restoreMySQL(backup, target)
case "postgres":
case biz.BackupTypePostgres:
return r.restorePostgres(backup, target)
}
@@ -189,13 +190,16 @@ func (r *backupRepo) ClearExpired(path, prefix string, save int) error {
}
// GetPath 获取备份路径
func (r *backupRepo) GetPath(typ string) (string, error) {
func (r *backupRepo) GetPath(typ biz.BackupType) (string, error) {
backupPath, err := r.setting.Get(biz.SettingKeyBackupPath)
if err != nil {
return "", err
}
if !slices.Contains([]biz.BackupType{biz.BackupTypePath, biz.BackupTypeWebsite, biz.BackupTypeMySQL, biz.BackupTypePostgres, biz.BackupTypeRedis, biz.BackupTypePanel}, typ) {
return "", errors.New("未知备份类型")
}
backupPath = filepath.Join(backupPath, typ)
backupPath = filepath.Join(backupPath, string(typ))
if !io.Exists(backupPath) {
if err = io.Mkdir(backupPath, 0644); err != nil {
return "", err
@@ -216,11 +220,22 @@ func (r *backupRepo) createWebsite(to string, name string) error {
return err
}
var paths []string
dirs, err := io.ReadDir(website.Path)
if err != nil {
return err
}
for _, item := range dirs {
paths = append(paths, filepath.Join(website.Path, item.Name()))
}
start := time.Now()
backup := filepath.Join(to, fmt.Sprintf("%s_%s.zip", website.Name, time.Now().Format("20060102150405")))
if _, err = shell.Execf(`cd '%s' && zip -r '%s' .`, website.Path, backup); err != nil {
if err = io.Compress(paths, backup, io.Zip); err != nil {
return err
}
color.Greenln(fmt.Sprintf("|-备份耗时:%s", time.Since(start).String()))
color.Greenln(fmt.Sprintf("|-已备份至文件:%s", backup))
return nil
}
@@ -249,6 +264,7 @@ func (r *backupRepo) createMySQL(to string, name string) error {
if err = os.Setenv("MYSQL_PWD", rootPassword); err != nil {
return err
}
start := time.Now()
backup := filepath.Join(to, fmt.Sprintf("%s_%s.sql", name, time.Now().Format("20060102150405")))
if _, err = shell.Execf(`mysqldump -u root '%s' > '%s'`, name, backup); err != nil {
return err
@@ -264,6 +280,7 @@ func (r *backupRepo) createMySQL(to string, name string) error {
return err
}
color.Greenln(fmt.Sprintf("|-备份耗时:%s", time.Since(start).String()))
color.Greenln(fmt.Sprintf("|-已备份至文件:%s", backup+".zip"))
return nil
}
@@ -285,6 +302,7 @@ func (r *backupRepo) createPostgres(to string, name string) error {
return err
}
start := time.Now()
backup := filepath.Join(to, fmt.Sprintf("%s_%s.sql", name, time.Now().Format("20060102150405")))
if _, err = shell.Execf(`su - postgres -c "pg_dump '%s'" > '%s'`, name, backup); err != nil {
return err
@@ -297,6 +315,7 @@ func (r *backupRepo) createPostgres(to string, name string) error {
return err
}
color.Greenln(fmt.Sprintf("|-备份耗时:%s", time.Since(start).String()))
color.Greenln(fmt.Sprintf("|-已备份至文件:%s", backup+".zip"))
return nil
}
@@ -309,6 +328,7 @@ func (r *backupRepo) createPanel(to string) error {
return err
}
start := time.Now()
if err := io.Compress([]string{
filepath.Join(app.Root, "panel"),
"/usr/local/sbin/panel-cli",
@@ -317,6 +337,7 @@ func (r *backupRepo) createPanel(to string) error {
return err
}
color.Greenln(fmt.Sprintf("|-备份耗时:%s", time.Since(start).String()))
color.Greenln(fmt.Sprintf("|-已备份至文件:%s", backup))
return nil
}

View File

@@ -0,0 +1,22 @@
package request
type BackupList struct {
Type string `json:"type" form:"type" validate:"required,oneof=path website mysql postgres redis panel"`
}
type BackupCreate struct {
Type string `json:"type" form:"type" validate:"required,oneof=website mysql postgres redis panel"`
Target string `json:"target" form:"target" validate:"required"`
Path string `json:"path" form:"path"`
}
type BackupFile struct {
Type string `json:"type" form:"type" validate:"required,oneof=website mysql postgres redis panel"`
File string `json:"file" form:"file" validate:"required"`
}
type BackupRestore struct {
Type string `json:"type" form:"type" validate:"required,oneof=website mysql postgres redis panel"`
File string `json:"file" form:"file" validate:"required"`
Target string `json:"target" form:"target" validate:"required"`
}

View File

@@ -14,11 +14,6 @@ type FileSave struct {
Content string `form:"content" json:"content"`
}
type FileUpload struct {
Path string `json:"path" form:"path"`
File []byte `json:"file" form:"file"`
}
type FileMove struct {
Source string `form:"source" json:"source"`
Target string `form:"target" json:"target"`

View File

@@ -35,7 +35,7 @@ func (receiver *PanelTask) Run() {
}
// 备份面板
if err := receiver.backupRepo.Create("panel", ""); err != nil {
if err := receiver.backupRepo.Create(biz.BackupTypePanel, ""); err != nil {
app.Logger.Error("备份面板失败", zap.Error(err))
}

View File

@@ -62,16 +62,14 @@ func Http(r chi.Router) {
r.Post("/{id}/status", website.UpdateStatus)
})
// TODO
r.Route("/backup", func(r chi.Router) {
r.Use(middleware.MustLogin)
backup := service.NewBackupService()
r.Get("/backup", backup.List)
r.Post("/create", backup.Create)
r.Post("/update", backup.Update)
r.Get("/{id}", backup.Get)
r.Delete("/{id}", backup.Delete)
r.Delete("/{id}/restore", backup.Restore)
r.Get("/{type}", backup.List)
r.Post("/{type}", backup.Create)
r.Post("/{type}/upload", backup.Upload)
r.Delete("/{type}/delete", backup.Delete)
r.Post("/{type}/restore", backup.Restore)
})
r.Route("/cert", func(r chi.Router) {

View File

@@ -1,10 +1,17 @@
package service
import (
stdio "io"
"net/http"
"os"
"path/filepath"
"github.com/go-rat/chix"
"github.com/TheTNB/panel/internal/biz"
"github.com/TheTNB/panel/internal/data"
"github.com/TheTNB/panel/internal/http/request"
"github.com/TheTNB/panel/pkg/io"
)
type BackupService struct {
@@ -18,25 +25,98 @@ func NewBackupService() *BackupService {
}
func (s *BackupService) List(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.BackupList](r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
list, _ := s.backupRepo.List(biz.BackupType(req.Type))
paged, total := Paginate(r, list)
Success(w, chix.M{
"total": total,
"items": paged,
})
}
func (s *BackupService) Create(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.BackupCreate](r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
if err = s.backupRepo.Create(biz.BackupType(req.Type), req.Target, req.Path); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
Success(w, nil)
}
func (s *BackupService) Update(w http.ResponseWriter, r *http.Request) {
func (s *BackupService) Upload(w http.ResponseWriter, r *http.Request) {
if err := r.ParseMultipartForm(2 << 30); err != nil {
Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
}
_, handler, err := r.FormFile("file")
path, err := s.backupRepo.GetPath(biz.BackupType(r.FormValue("type")))
if err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
func (s *BackupService) Get(w http.ResponseWriter, r *http.Request) {
if !io.Exists(filepath.Dir(path)) {
if err = io.Mkdir(filepath.Dir(path), 0755); err != nil {
Error(w, http.StatusInternalServerError, "创建文件夹失败:%v", err)
return
}
}
src, _ := handler.Open()
out, err := os.OpenFile(filepath.Join(path, handler.Filename), os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0644)
if err != nil {
Error(w, http.StatusInternalServerError, "打开文件失败:%v", err)
return
}
if _, err = stdio.Copy(out, src); err != nil {
Error(w, http.StatusInternalServerError, "写入文件失败:%v", err)
return
}
_ = src.Close()
Success(w, nil)
}
func (s *BackupService) Delete(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.BackupFile](r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
if err = s.backupRepo.Delete(biz.BackupType(req.Type), req.File); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
Success(w, nil)
}
func (s *BackupService) Restore(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.BackupRestore](r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
if err = s.backupRepo.Restore(biz.BackupType(req.Type), req.File, req.Target); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
Success(w, nil)
}

View File

@@ -421,7 +421,7 @@ func (s *CliService) BackupWebsite(ctx context.Context, cmd *cli.Command) error
color.Greenln(s.hr)
color.Greenln("|-备份类型:网站")
color.Greenln(fmt.Sprintf("|-备份目标:%s", cmd.String("name")))
if err := s.backupRepo.Create("website", cmd.String("name"), cmd.String("path")); err != nil {
if err := s.backupRepo.Create(biz.BackupTypeWebsite, cmd.String("name"), cmd.String("path")); err != nil {
return fmt.Errorf("|-备份失败:%v", err)
}
color.Greenln(s.hr)
@@ -437,7 +437,7 @@ func (s *CliService) BackupDatabase(ctx context.Context, cmd *cli.Command) error
color.Greenln("|-备份类型:数据库")
color.Greenln(fmt.Sprintf("|-数据库:%s", cmd.String("type")))
color.Greenln(fmt.Sprintf("|-备份目标:%s", cmd.String("name")))
if err := s.backupRepo.Create(cmd.String("type"), cmd.String("name"), cmd.String("path")); err != nil {
if err := s.backupRepo.Create(biz.BackupType(cmd.String("type")), cmd.String("name"), cmd.String("path")); err != nil {
return fmt.Errorf("|-备份失败:%v", err)
}
color.Greenln(s.hr)
@@ -451,7 +451,7 @@ func (s *CliService) BackupPanel(ctx context.Context, cmd *cli.Command) error {
color.Greenln(fmt.Sprintf("★ 开始备份 [%s]", time.Now().Format(time.DateTime)))
color.Greenln(s.hr)
color.Greenln("|-备份类型:面板")
if err := s.backupRepo.Create("panel", "", cmd.String("path")); err != nil {
if err := s.backupRepo.Create(biz.BackupTypePanel, "", cmd.String("path")); err != nil {
return fmt.Errorf("|-备份失败:%v", err)
}
color.Greenln(s.hr)
@@ -461,7 +461,7 @@ func (s *CliService) BackupPanel(ctx context.Context, cmd *cli.Command) error {
}
func (s *CliService) BackupClear(ctx context.Context, cmd *cli.Command) error {
path, err := s.backupRepo.GetPath(cmd.String("type"))
path, err := s.backupRepo.GetPath(biz.BackupType(cmd.String("type")))
if err != nil {
return err
}

View File

@@ -4,6 +4,7 @@ package service
import (
"fmt"
stdio "io"
"net/http"
stdos "os"
"path/filepath"
@@ -119,18 +120,43 @@ func (s *FileService) Delete(w http.ResponseWriter, r *http.Request) {
}
func (s *FileService) Upload(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.FileUpload](r)
if err := r.ParseMultipartForm(2 << 30); err != nil {
Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
path := r.FormValue("path")
_, handler, err := r.FormFile("file")
if err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
if err = io.Write(req.Path, string(req.File), 0755); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
if io.Exists(path) {
Error(w, http.StatusForbidden, "目标路径%s已存在", path)
return
}
s.setPermission(req.Path, 0755, "www", "www")
if !io.Exists(filepath.Dir(path)) {
if err = io.Mkdir(filepath.Dir(path), 0755); err != nil {
Error(w, http.StatusInternalServerError, "创建文件夹失败:%v", err)
return
}
}
src, _ := handler.Open()
out, err := stdos.OpenFile(path, stdos.O_CREATE|stdos.O_RDWR|stdos.O_TRUNC, 0644)
if err != nil {
Error(w, http.StatusInternalServerError, "打开文件失败:%v", err)
return
}
if _, err = stdio.Copy(out, src); err != nil {
Error(w, http.StatusInternalServerError, "写入文件失败:%v", err)
return
}
_ = src.Close()
s.setPermission(path, 0755, "www", "www")
Success(w, nil)
}

View File

@@ -6,6 +6,7 @@ package service
import (
"fmt"
stdio "io"
"net/http"
stdos "os"
"path/filepath"
@@ -119,18 +120,43 @@ func (s *FileService) Delete(w http.ResponseWriter, r *http.Request) {
}
func (s *FileService) Upload(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.FileUpload](r)
if err := r.ParseMultipartForm(2 << 30); err != nil {
Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
path := r.FormValue("path")
_, handler, err := r.FormFile("file")
if err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
if err = io.Write(req.Path, string(req.File), 0755); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
if io.Exists(path) {
Error(w, http.StatusForbidden, "目标路径%s已存在", path)
return
}
s.setPermission(req.Path, 0755, "www", "www")
if !io.Exists(filepath.Dir(path)) {
if err = io.Mkdir(filepath.Dir(path), 0755); err != nil {
Error(w, http.StatusInternalServerError, "创建文件夹失败:%v", err)
return
}
}
src, _ := handler.Open()
out, err := stdos.OpenFile(path, stdos.O_CREATE|stdos.O_RDWR|stdos.O_TRUNC, 0644)
if err != nil {
Error(w, http.StatusInternalServerError, "打开文件失败:%v", err)
return
}
if _, err = stdio.Copy(out, src); err != nil {
Error(w, http.StatusInternalServerError, "写入文件失败:%v", err)
return
}
_ = src.Close()
s.setPermission(path, 0755, "www", "www")
Success(w, nil)
}

View File

@@ -2,5 +2,6 @@ package types
type BackupFile struct {
Name string `json:"name"`
Path string `json:"path"`
Size string `json:"size"`
}

View File

@@ -0,0 +1,24 @@
import type { AxiosResponse } from 'axios'
import { request } from '@/utils'
export default {
// 获取备份列表
list: (type: string, page: number, limit: number): Promise<AxiosResponse<any>> =>
request.get(`/backup/${type}`, { params: { page, limit } }),
// 创建备份
create: (type: string, target: string, path: string): Promise<AxiosResponse<any>> =>
request.post(`/backup/${type}`, { target, path }),
// 上传备份
upload: (type: string, formData: FormData): Promise<AxiosResponse<any>> => {
return request.post(`/backup/${type}/upload`, formData, {
headers: { 'Content-Type': 'multipart/form-data' }
})
},
// 删除备份
delete: (type: string, file: string): Promise<AxiosResponse<any>> =>
request.delete(`/backup/${type}/delete`, { params: { file } }),
// 恢复备份
restore: (type: string, file: string, target: string): Promise<AxiosResponse<any>> =>
request.post(`/backup/${type}/restore`, { file, target })
}

View File

@@ -7,7 +7,7 @@ export default {
path: '/app',
component: Layout,
meta: {
order: 8
order: 90
},
children: [
{
@@ -16,7 +16,7 @@ export default {
component: () => import('./IndexView.vue'),
meta: {
title: 'appIndex.title',
icon: 'mdi:puzzle-outline',
icon: 'mdi:apps',
role: ['admin'],
requireAuth: true
}

View File

@@ -300,8 +300,8 @@ onMounted(() => {
</n-tab-pane>
</n-tabs>
</common-page>
<n-modal v-model:show="addUserModal" title="建用户">
<n-card closable @close="() => (addUserModal = false)" title="建用户" style="width: 60vw">
<n-modal v-model:show="addUserModal" title="建用户">
<n-card closable @close="() => (addUserModal = false)" title="建用户" style="width: 60vw">
<n-form :model="addUserModel">
<n-form-item path="username" label="用户名">
<n-input

View File

@@ -0,0 +1,87 @@
<script setup lang="ts">
import backup from '@/api/panel/backup'
import ListView from '@/views/backup/ListView.vue'
import { NButton, NInput } from 'naive-ui'
const currentTab = ref('website')
const createModal = ref(false)
const createModel = ref({
target: '',
path: ''
})
const oldTab = ref('')
const handleCreate = () => {
backup.create(currentTab.value, createModel.value.target, createModel.value.path).then(() => {
createModal.value = false
window.$message.success('创建成功')
// 有点low但是没找到更好的办法
oldTab.value = currentTab.value
currentTab.value = ''
setTimeout(() => {
currentTab.value = oldTab.value
}, 0)
})
}
</script>
<template>
<common-page show-footer>
<template #action>
<div flex items-center>
<n-button class="ml-16" type="primary" @click="createModal = true">
<TheIcon :size="18" class="mr-5" icon="material-symbols:add" />
创建备份
</n-button>
</div>
</template>
<n-flex vertical>
<n-alert type="info">此处仅显示面板默认备份目录的文件</n-alert>
<n-tabs v-model:value="currentTab" type="line" animated>
<n-tab-pane name="website" tab="网站">
<list-view v-model:type="currentTab" />
</n-tab-pane>
<n-tab-pane name="mysql" tab="MySQL">
<list-view v-model:type="currentTab" />
</n-tab-pane>
<n-tab-pane name="postgres" tab="PostgreSQL">
<list-view v-model:type="currentTab" />
</n-tab-pane>
</n-tabs>
</n-flex>
</common-page>
<n-modal
v-model:show="createModal"
preset="card"
title="创建备份"
style="width: 60vw"
size="huge"
:bordered="false"
:segmented="false"
@close="createModal = false"
>
<n-form :model="createModel">
<n-form-item path="name" label="名称">
<n-input
v-model:value="createModel.target"
type="text"
@keydown.enter.prevent
placeholder="输入网站/数据库名称"
/>
</n-form-item>
<n-form-item path="path" label="目录">
<n-input
v-model:value="createModel.path"
type="text"
@keydown.enter.prevent
placeholder="留空使用默认路径"
/>
</n-form-item>
</n-form>
<n-row :gutter="[0, 24]">
<n-col :span="24">
<n-button type="info" block @click="handleCreate">提交</n-button>
</n-col>
</n-row>
</n-modal>
</template>

View File

@@ -0,0 +1,165 @@
<script setup lang="ts">
import backup from '@/api/panel/backup'
import { renderIcon } from '@/utils'
import type { MessageReactive } from 'naive-ui'
import { NButton, NInput, NPopconfirm } from 'naive-ui'
import type { Backup } from './types'
const type = defineModel<string>('type', { type: String, required: true })
let messageReactive: MessageReactive | null = null
const restoreModal = ref(false)
const restoreModel = ref({
file: '',
target: ''
})
const columns: any = [
{ title: '文件名', key: 'name', fixed: 'left', resizable: true, ellipsis: { tooltip: true } },
{ title: '大小', key: 'size', width: 200, ellipsis: { tooltip: true } },
{
title: '操作',
key: 'actions',
width: 200,
align: 'center',
fixed: 'right',
hideInExcel: true,
render(row: any) {
return [
h(
NButton,
{
size: 'small',
type: 'warning',
secondary: true,
onClick: () => {
restoreModel.value.file = row.path
restoreModal.value = true
}
},
{
default: () => '恢复',
icon: renderIcon('material-symbols:settings-backup-restore-rounded', { size: 14 })
}
),
h(
NPopconfirm,
{
onPositiveClick: () => handleDelete(row.name)
},
{
default: () => {
return '确定删除备份吗?'
},
trigger: () => {
return h(
NButton,
{
size: 'small',
type: 'error',
style: 'margin-left: 15px;'
},
{
default: () => '删除',
icon: renderIcon('material-symbols:delete-outline', { size: 14 })
}
)
}
}
)
]
}
}
]
const data = ref<Backup[]>([])
const pagination = reactive({
page: 1,
pageCount: 1,
pageSize: 10,
itemCount: 0,
showQuickJumper: true,
showSizePicker: true,
pageSizes: [10, 20, 50, 100]
})
const getList = async (page: number, limit: number) => {
const { data } = await backup.list(type.value, page, limit)
return data
}
const onPageChange = (page: number) => {
pagination.page = page
getList(page, pagination.pageSize).then((res) => {
data.value = res.items
pagination.itemCount = res.total
pagination.pageCount = res.total / pagination.pageSize + 1
})
}
const onPageSizeChange = (pageSize: number) => {
pagination.pageSize = pageSize
onPageChange(1)
}
const handleRestore = async () => {
messageReactive = window.$message.loading('恢复中...', {
duration: 0
})
await backup.restore(type.value, restoreModel.value.file, restoreModel.value.target).then(() => {
messageReactive?.destroy()
window.$message.success('恢复成功')
onPageChange(pagination.page)
})
}
const handleDelete = async (file: string) => {
await backup.delete(type.value, file).then(() => {
window.$message.success('删除成功')
onPageChange(pagination.page)
})
}
onMounted(() => {
onPageChange(pagination.page)
})
</script>
<template>
<n-data-table
striped
remote
:loading="false"
:columns="columns"
:data="data"
:row-key="(row: any) => row.name"
@update:page="onPageChange"
@update:page-size="onPageSizeChange"
/>
<n-modal
v-model:show="restoreModal"
preset="card"
title="恢复备份"
style="width: 60vw"
size="huge"
:bordered="false"
:segmented="false"
@close="restoreModal = false"
>
<n-form :model="restoreModel">
<n-form-item path="name" label="恢复目标">
<n-input v-model:value="restoreModel.target" type="text" @keydown.enter.prevent />
</n-form-item>
</n-form>
<n-row :gutter="[0, 24]">
<n-col :span="24">
<n-button type="info" block @click="handleRestore">提交</n-button>
</n-col>
</n-row>
</n-modal>
</template>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,25 @@
import type { RouteType } from '~/types/router'
const Layout = () => import('@/layout/IndexView.vue')
export default {
name: 'backup',
path: '/backup',
component: Layout,
meta: {
order: 60
},
children: [
{
name: 'backup-index',
path: '',
component: () => import('./IndexView.vue'),
meta: {
title: '数据备份',
icon: 'mdi:backup-outline',
role: ['admin'],
requireAuth: true
}
}
]
} as RouteType

View File

@@ -0,0 +1,5 @@
export interface Backup {
name: string
path: string
size: string
}

View File

@@ -7,7 +7,7 @@ export default {
path: '/cert',
component: Layout,
meta: {
order: 2
order: 10
},
children: [
{

View File

@@ -11,16 +11,16 @@ const currentTab = ref('container')
<common-page show-footer>
<n-tabs v-model:value="currentTab" type="line" animated size="large">
<n-tab-pane name="container" tab="容器">
<ContainerView />
<container-view />
</n-tab-pane>
<n-tab-pane name="image" tab="镜像">
<ImageView />
<image-view />
</n-tab-pane>
<n-tab-pane name="network" tab="网络">
<NetworkView />
<network-view />
</n-tab-pane>
<n-tab-pane name="volume" tab="卷">
<VolumeView />
<volume-view />
</n-tab-pane>
</n-tabs>
</common-page>

View File

@@ -7,7 +7,7 @@ export default {
path: '/container',
component: Layout,
meta: {
order: 5
order: 40
},
children: [
{

View File

@@ -329,7 +329,7 @@ onMounted(() => {
<n-radio-group v-model:value="addModel.backup_type">
<n-radio value="website">网站目录</n-radio>
<n-radio value="mysql" :disabled="!mySQLInstalled"> MySQL 数据库</n-radio>
<n-radio value="postgresql" :disabled="!postgreSQLInstalled">
<n-radio value="postgres" :disabled="!postgreSQLInstalled">
PostgreSQL 数据库
</n-radio>
</n-radio-group>

View File

@@ -7,7 +7,7 @@ export default {
path: '/cron',
component: Layout,
meta: {
order: 5
order: 70
},
children: [
{

View File

@@ -127,7 +127,7 @@ const columns: DataTableColumns<RowData> = [
selected.value = [row.full]
compress.value = true
} else {
window.open('/api/panel/file/download?path=' + encodeURIComponent(row.full))
window.open('/api/file/download?path=' + encodeURIComponent(row.full))
}
}
},

View File

@@ -7,7 +7,7 @@ export default {
path: '/file',
component: Layout,
meta: {
order: 6
order: 50
},
children: [
{

View File

@@ -7,7 +7,7 @@ export default {
path: '/monitor',
component: Layout,
meta: {
order: 3
order: 20
},
children: [
{

View File

@@ -7,7 +7,7 @@ export default {
path: '/safe',
component: Layout,
meta: {
order: 4
order: 30
},
children: [
{

View File

@@ -7,7 +7,7 @@ export default {
path: '/setting',
component: Layout,
meta: {
order: 10
order: 999
},
children: [
{

View File

@@ -7,7 +7,7 @@ export default {
path: '/ssh',
component: Layout,
meta: {
order: 7
order: 80
},
children: [
{

View File

@@ -7,7 +7,7 @@ export default {
path: '/task',
component: Layout,
meta: {
order: 9
order: 100
},
children: [
{