From 8683a85640babb8466c5ede7b6aeeb31cc40af89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=97=E5=AD=90?= Date: Thu, 27 Mar 2025 03:59:47 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A4=87=E4=BB=BD=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E4=B8=8A=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/http/request/backup.go | 15 +++- internal/service/backup.go | 37 ++++++---- web/src/api/panel/backup/index.ts | 6 +- web/src/views/backup/IndexView.vue | 93 ----------------------- web/src/views/backup/ListView.vue | 106 +++++++++++++++++++++------ web/src/views/backup/UploadModal.vue | 55 ++++++++++++++ web/src/views/file/UploadModal.vue | 8 +- 7 files changed, 172 insertions(+), 148 deletions(-) create mode 100644 web/src/views/backup/UploadModal.vue diff --git a/internal/http/request/backup.go b/internal/http/request/backup.go index 835b5433..c4091a61 100644 --- a/internal/http/request/backup.go +++ b/internal/http/request/backup.go @@ -1,22 +1,29 @@ package request +import "mime/multipart" + type BackupList struct { - Type string `json:"type" form:"type" validate:"required|in:path,website,mysql,postgres,redis,panel"` + Type string `uri:"type" form:"type" validate:"required|in:path,website,mysql,postgres,redis,panel"` } type BackupCreate struct { - Type string `json:"type" form:"type" validate:"required|in:website,mysql,postgres,redis,panel"` + Type string `uri:"type" form:"type" validate:"required|in:website,mysql,postgres,redis,panel"` Target string `json:"target" form:"target" validate:"required"` Path string `json:"path" form:"path"` } +type BackupUpload struct { + Type string `uri:"type" form:"type"` // 校验没有必要,因为根本没经过验证器 + File *multipart.FileHeader `form:"file"` +} + type BackupFile struct { - Type string `json:"type" form:"type" validate:"required|in:website,mysql,postgres,redis,panel"` + Type string `uri:"type" form:"type" validate:"required|in:website,mysql,postgres,redis,panel"` File string `json:"file" form:"file" validate:"required"` } type BackupRestore struct { - Type string `json:"type" form:"type" validate:"required|in:website,mysql,postgres,redis,panel"` + Type string `uri:"type" form:"type" validate:"required|in:website,mysql,postgres,redis,panel"` File string `json:"file" form:"file" validate:"required"` Target string `json:"target" form:"target" validate:"required"` } diff --git a/internal/service/backup.go b/internal/service/backup.go index 23388b07..ef468cb9 100644 --- a/internal/service/backup.go +++ b/internal/service/backup.go @@ -5,6 +5,7 @@ import ( "net/http" "os" "path/filepath" + "slices" "github.com/go-rat/chix" @@ -55,38 +56,44 @@ func (s *BackupService) Create(w http.ResponseWriter, r *http.Request) { } func (s *BackupService) Upload(w http.ResponseWriter, r *http.Request) { - if err := r.ParseMultipartForm(2 << 30); err != nil { + binder := chix.NewBind(r) + defer binder.Release() + + req := new(request.BackupUpload) + if err := binder.MultipartForm(req, 2<<30); err != nil { + Error(w, http.StatusUnprocessableEntity, "%v", err) + return + } + if err := binder.URI(req); err != nil { Error(w, http.StatusUnprocessableEntity, "%v", err) return } - _, handler, err := r.FormFile("file") - if err != nil { - Error(w, http.StatusInternalServerError, "上传文件失败:%v", err) + // 只允许上传 .sql .zip .tar .gz .tgz .bz2 .xz .7z + if !slices.Contains([]string{".sql", ".zip", ".tar", ".gz", ".tgz", ".bz2", ".xz", ".7z"}, filepath.Ext(req.File.Filename)) { + Error(w, http.StatusForbidden, "unsupported file type") return } - path, err := s.backupRepo.GetPath(biz.BackupType(r.FormValue("type"))) + + path, err := s.backupRepo.GetPath(biz.BackupType(req.Type)) if err != nil { Error(w, http.StatusInternalServerError, "%v", err) return } - - if !io.Exists(filepath.Dir(path)) { - if err = os.MkdirAll(filepath.Dir(path), 0755); err != nil { - Error(w, http.StatusInternalServerError, "创建文件夹失败:%v", err) - return - } + if io.Exists(filepath.Join(path, req.File.Filename)) { + Error(w, http.StatusForbidden, "target backup %s already exists", path) + return } - src, _ := handler.Open() - out, err := os.OpenFile(filepath.Join(path, handler.Filename), os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0644) + src, _ := req.File.Open() + out, err := os.OpenFile(filepath.Join(path, req.File.Filename), os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0644) if err != nil { - Error(w, http.StatusInternalServerError, "打开文件失败:%v", err) + Error(w, http.StatusInternalServerError, "open file error: %v", err) return } if _, err = stdio.Copy(out, src); err != nil { - Error(w, http.StatusInternalServerError, "写入文件失败:%v", err) + Error(w, http.StatusInternalServerError, "write file error: %v", err) return } diff --git a/web/src/api/panel/backup/index.ts b/web/src/api/panel/backup/index.ts index b9e459a6..c2eab267 100644 --- a/web/src/api/panel/backup/index.ts +++ b/web/src/api/panel/backup/index.ts @@ -8,11 +8,7 @@ export default { create: (type: string, target: string, path: string): any => http.Post(`/backup/${type}`, { target, path }), // 上传备份 - upload: (type: string, formData: FormData): any => { - return http.Post(`/backup/${type}/upload`, formData, { - headers: { 'Content-Type': 'multipart/form-data' } - }) - }, + upload: (type: string, formData: FormData): any => http.Post(`/backup/${type}/upload`, formData), // 删除备份 delete: (type: string, file: string): any => http.Delete(`/backup/${type}/delete`, { file }), // 恢复备份 diff --git a/web/src/views/backup/IndexView.vue b/web/src/views/backup/IndexView.vue index 6d247a54..fb9092ae 100644 --- a/web/src/views/backup/IndexView.vue +++ b/web/src/views/backup/IndexView.vue @@ -1,22 +1,12 @@ diff --git a/web/src/views/backup/ListView.vue b/web/src/views/backup/ListView.vue index ed0b66f6..5bf49e4b 100644 --- a/web/src/views/backup/ListView.vue +++ b/web/src/views/backup/ListView.vue @@ -2,16 +2,25 @@ import backup from '@/api/panel/backup' import { renderIcon } from '@/utils' import type { MessageReactive } from 'naive-ui' -import { NButton, NDataTable, NInput, NPopconfirm } from 'naive-ui' +import { NButton, NDataTable, NFlex, NInput, NPopconfirm } from 'naive-ui' import app from '@/api/panel/app' import website from '@/api/panel/website' import { formatDateTime } from '@/utils' +import UploadModal from '@/views/backup/UploadModal.vue' const type = defineModel('type', { type: String, required: true }) let messageReactive: MessageReactive | null = null +const uploadModal = ref(false) + +const createModal = ref(false) +const createModel = ref({ + target: '', + path: '' +}) + const restoreModal = ref(false) const restoreModel = ref({ file: '', @@ -107,6 +116,16 @@ const { loading, data, page, total, pageSize, pageCount, refresh } = usePaginati } ) +const handleCreate = () => { + useRequest(backup.create(type.value, createModel.value.target, createModel.value.path)).onSuccess( + () => { + createModal.value = false + window.$bus.emit('backup:refresh') + window.$message.success('创建成功') + } + ) +} + const handleRestore = () => { messageReactive = window.$message.loading('恢复中...', { duration: 0 @@ -140,15 +159,14 @@ onMounted(() => { }) } if (type.value === 'website') { + createModel.value.target = websites.value[0]?.value restoreModel.value.target = websites.value[0]?.value } }) } }) refresh() - window.$bus.on('backup:refresh', () => { - refresh() - }) + window.$bus.on('backup:refresh', refresh) }) onUnmounted(() => { @@ -157,26 +175,65 @@ onUnmounted(() => { diff --git a/web/src/views/backup/UploadModal.vue b/web/src/views/backup/UploadModal.vue new file mode 100644 index 00000000..0968ab4c --- /dev/null +++ b/web/src/views/backup/UploadModal.vue @@ -0,0 +1,55 @@ + + + + + diff --git a/web/src/views/file/UploadModal.vue b/web/src/views/file/UploadModal.vue index a4ab6b24..0c67a8cd 100644 --- a/web/src/views/file/UploadModal.vue +++ b/web/src/views/file/UploadModal.vue @@ -40,13 +40,7 @@ const uploadRequest = ({ file, onFinish, onError, onProgress }: UploadCustomRequ :segmented="false" > - +