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 @@
-
-
-
- 备份网站
-
-
-
- 备份 MySQL
-
-
-
- 备份 PostgreSQL
-
-
@@ -104,37 +44,4 @@ onMounted(() => {
-
-
-
-
-
-
-
-
-
-
-
-
- 提交
-
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 @@
+
+
+
+
+
+
+
+
+
+
+ 点击或者拖动文件到该区域来上传
+ 大文件建议使用 SFTP 等方式上传
+
+
+
+
+
+
+
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"
>
-
+