From 336a9bc675dba18b584a93ab5883e3aa57cdf862 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=97=E5=AD=90?= Date: Thu, 31 Oct 2024 21:49:58 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=96=87=E4=BB=B6=E7=AE=A1=E7=90=86bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/http/request/file.go | 8 +- internal/route/http.go | 1 + internal/service/file.go | 80 ++++++---- web/src/api/panel/file/index.ts | 9 +- web/src/views/file/IndexView.vue | 3 + web/src/views/file/ListTable.vue | 198 +++++++++++-------------- web/src/views/file/PermissionModal.vue | 15 +- web/src/views/file/ToolBar.vue | 143 +++++++++--------- web/src/views/file/UploadModal.vue | 33 +++-- web/src/views/file/types.ts | 2 +- 10 files changed, 246 insertions(+), 246 deletions(-) diff --git a/internal/http/request/file.go b/internal/http/request/file.go index e2d63388..62e8fbfe 100644 --- a/internal/http/request/file.go +++ b/internal/http/request/file.go @@ -25,13 +25,7 @@ type FileSave struct { Content string `form:"content" json:"content"` } -type FileMove struct { - Source string `form:"source" json:"source" validate:"required"` - Target string `form:"target" json:"target" validate:"required"` - Force bool `form:"force" json:"force"` -} - -type FileCopy struct { +type FileControl struct { Source string `form:"source" json:"source" validate:"required"` Target string `form:"target" json:"target" validate:"required"` Force bool `form:"force" json:"force"` diff --git a/internal/route/http.go b/internal/route/http.go index 5135a5ab..908b64d1 100644 --- a/internal/route/http.go +++ b/internal/route/http.go @@ -218,6 +218,7 @@ func Http(r chi.Router) { r.Post("/save", file.Save) r.Post("/delete", file.Delete) r.Post("/upload", file.Upload) + r.Post("/exist", file.Exist) r.Post("/move", file.Move) r.Post("/copy", file.Copy) r.Get("/download", file.Download) diff --git a/internal/service/file.go b/internal/service/file.go index be340eba..38e82559 100644 --- a/internal/service/file.go +++ b/internal/service/file.go @@ -166,51 +166,77 @@ func (s *FileService) Upload(w http.ResponseWriter, r *http.Request) { Success(w, nil) } +func (s *FileService) Exist(w http.ResponseWriter, r *http.Request) { + binder := chix.NewBind(r) + defer binder.Release() + + var paths []string + if err := binder.Body(&paths); err != nil { + Error(w, http.StatusInternalServerError, "%v", err) + return + } + + var results []bool + for item := range slices.Values(paths) { + results = append(results, io.Exists(item)) + } + + Success(w, results) +} + func (s *FileService) Move(w http.ResponseWriter, r *http.Request) { - req, err := Bind[request.FileMove](r) - if err != nil { + binder := chix.NewBind(r) + defer binder.Release() + + var req []request.FileControl + if err := binder.Body(&req); err != nil { Error(w, http.StatusInternalServerError, "%v", err) return } - if io.Exists(req.Target) && !req.Force { - Error(w, http.StatusForbidden, "target path already exists") // no translate, frontend will use it to determine whether to continue - return - } + for item := range slices.Values(req) { + if io.Exists(item.Target) && !item.Force { + continue + } - if io.IsDir(req.Source) && strings.HasPrefix(req.Target, req.Source) { - Error(w, http.StatusForbidden, "you can't do this, it will be broken") - return - } + if io.IsDir(item.Source) && strings.HasPrefix(item.Target, item.Source) { + Error(w, http.StatusForbidden, "you can't do this, it will be broken") + return + } - if err = io.Mv(req.Source, req.Target); err != nil { - Error(w, http.StatusInternalServerError, "%v", err) - return + if err := io.Mv(item.Source, item.Target); err != nil { + Error(w, http.StatusInternalServerError, "%v", err) + return + } } Success(w, nil) } func (s *FileService) Copy(w http.ResponseWriter, r *http.Request) { - req, err := Bind[request.FileCopy](r) - if err != nil { + binder := chix.NewBind(r) + defer binder.Release() + + var req []request.FileControl + if err := binder.Body(&req); err != nil { Error(w, http.StatusInternalServerError, "%v", err) return } - if io.Exists(req.Target) && !req.Force { - Error(w, http.StatusForbidden, "target path already exists") // no translate, frontend will use it to determine whether to continue - return - } + for item := range slices.Values(req) { + if io.Exists(item.Target) && !item.Force { + continue + } - if io.IsDir(req.Source) && strings.HasPrefix(req.Target, req.Source) { - Error(w, http.StatusForbidden, "you can't do this, it will be broken") - return - } + if io.IsDir(item.Source) && strings.HasPrefix(item.Target, item.Source) { + Error(w, http.StatusForbidden, "you can't do this, it will be broken") + return + } - if err = io.Cp(req.Source, req.Target); err != nil { - Error(w, http.StatusInternalServerError, "%v", err) - return + if err := io.Cp(item.Source, item.Target); err != nil { + Error(w, http.StatusInternalServerError, "%v", err) + return + } } Success(w, nil) @@ -415,7 +441,7 @@ func (s *FileService) List(w http.ResponseWriter, r *http.Request) { // formatDir 格式化目录信息 func (s *FileService) formatDir(base string, entries []stdos.DirEntry) []any { var paths []any - for _, file := range entries { + for file := range slices.Values(entries) { info, err := file.Info() if err != nil { continue diff --git a/web/src/api/panel/file/index.ts b/web/src/api/panel/file/index.ts index 3033a9bc..48caf72e 100644 --- a/web/src/api/panel/file/index.ts +++ b/web/src/api/panel/file/index.ts @@ -1,7 +1,6 @@ import type { AxiosResponse } from 'axios' import { request } from '@/utils' -import type { RequestConfig } from '~/types/axios' export default { // 创建文件/文件夹 @@ -25,12 +24,12 @@ export default { } }) }, + // 检查文件是否存在 + exist: (paths: string[]): Promise> => request.post('/file/exist', paths), // 移动文件 - move: (source: string, target: string, force: boolean): Promise> => - request.post('/file/move', { source, target, force }, { noNeedTip: true } as RequestConfig), + move: (paths: any[]): Promise> => request.post('/file/move', paths), // 复制文件 - copy: (source: string, target: string, force: boolean): Promise> => - request.post('/file/copy', { source, target, force }, { noNeedTip: true } as RequestConfig), + copy: (paths: any[]): Promise> => request.post('/file/copy', paths), // 远程下载 remoteDownload: (path: string, url: string): Promise> => request.post('/file/remoteDownload', { path, url }), diff --git a/web/src/views/file/IndexView.vue b/web/src/views/file/IndexView.vue index 011ca881..1f3f8c82 100644 --- a/web/src/views/file/IndexView.vue +++ b/web/src/views/file/IndexView.vue @@ -13,6 +13,7 @@ import type { Marked } from '@/views/file/types' const path = ref('/www') const selected = ref([]) const marked = ref([]) +const markedType = ref('copy') const compress = ref(false) const permission = ref(false) @@ -26,6 +27,7 @@ const permission = ref(false) v-model:path="path" v-model:selected="selected" v-model:marked="marked" + v-model:markedType="markedType" v-model:compress="compress" v-model:permission="permission" /> @@ -33,6 +35,7 @@ const permission = ref(false) v-model:path="path" v-model:selected="selected" v-model:marked="marked" + v-model:markedType="markedType" v-model:compress="compress" v-model:permission="permission" /> diff --git a/web/src/views/file/ListTable.vue b/web/src/views/file/ListTable.vue index e5bcb170..90525116 100644 --- a/web/src/views/file/ListTable.vue +++ b/web/src/views/file/ListTable.vue @@ -15,6 +15,7 @@ const sort = ref('') const path = defineModel('path', { type: String, required: true }) const selected = defineModel('selected', { type: Array, default: () => [] }) const marked = defineModel('marked', { type: Array, default: () => [] }) +const markedType = defineModel('markedType', { type: String, required: true }) const compress = defineModel('compress', { type: Boolean, required: true }) const permission = defineModel('permission', { type: Boolean, required: true }) const editorModal = ref(false) @@ -275,21 +276,23 @@ const columns: DataTableColumns = [ onUpdateValue: (value) => { switch (value) { case 'copy': + markedType.value = 'copy' marked.value = [ { name: row.name, source: row.full, - type: 'copy' + force: false } ] window.$message.success('标记成功,请前往目标路径粘贴') break case 'move': + markedType.value = 'move' marked.value = [ { name: row.name, source: row.full, - type: 'move' + force: false } ] window.$message.success('标记成功,请前往目标路径粘贴') @@ -385,7 +388,7 @@ const getList = async (path: string, page: number, limit: number) => { }) } -const handleRename = () => { +const handleRename = async () => { const source = path.value + '/' + renameModel.value.source const target = path.value + '/' + renameModel.value.target if (!checkName(renameModel.value.source) || !checkName(renameModel.value.target)) { @@ -393,43 +396,30 @@ const handleRename = () => { return } - file - .move(source, target, false) - .then(() => { + await file.exist([source]).then(async (res) => { + if (res.data[0]) { + window.$dialog.warning({ + title: '警告', + content: `存在同名项,是否强制覆盖?`, + positiveText: '覆盖', + negativeText: '取消', + onPositiveClick: async () => { + await file.move([{ source, target, force: true }]) + window.$message.success( + `重命名 ${renameModel.value.source} 为 ${renameModel.value.target} 成功` + ) + } + }) + } else { + await file.move([{ source, target, force: true }]) window.$message.success( `重命名 ${renameModel.value.source} 为 ${renameModel.value.target} 成功` ) - renameModal.value = false - window.$bus.emit('file:refresh') - }) - .catch((err) => { - if (err.message == 'target path already exists') { - window.$dialog.warning({ - title: '警告', - content: `存在同名项,是否强制覆盖?`, - positiveText: '覆盖', - negativeText: '取消', - onPositiveClick: () => { - file - .move(source, target, true) - .then(() => { - window.$message.success( - `重命名 ${renameModel.value.source} 为 ${renameModel.value.target} 成功` - ) - renameModal.value = false - window.$bus.emit('file:refresh') - }) - .catch((err) => { - window.$message.error(err.message) - }) - }, - onNegativeClick: () => { - renameModal.value = false - window.$bus.emit('file:refresh') - } - }) - } - }) + } + }) + + renameModal.value = false + window.$bus.emit('file:refresh') } const handleUnCompress = () => { @@ -468,76 +458,66 @@ const handlePaste = async () => { return } - for (const { name, source, type } of marked.value) { - const target = path.value + '/' + name - if (type === 'copy') { - file - .copy(source, target, false) - .then(() => { - window.$message.success(`复制 ${source} 到 ${target} 成功`) - }) - .catch((err) => { - if (err.message == 'target path already exists') { - window.$dialog.warning({ - title: '警告', - content: `目标 ${target} 已存在,是否覆盖?`, - positiveText: '覆盖', - negativeText: '取消', - onPositiveClick: () => { - file - .copy(source, target, true) - .then(() => { - window.$message.success(`复制 ${source} 到 ${target} 成功`) - }) - .catch((err) => { - window.$message.error(err.message) - }) - }, - onNegativeClick: () => { - marked.value = [] - } - }) - } - }) - .finally(() => { - window.$bus.emit('file:refresh') - }) - } else { - file - .move(source, target, false) - .then(() => { - window.$message.success(`移动 ${source} 到 ${target} 成功`) - }) - .catch((err) => { - if (err.message == 'target path already exists') { - window.$dialog.warning({ - title: '警告', - content: `目标 ${target} 已存在,是否覆盖?`, - positiveText: '覆盖', - negativeText: '取消', - onPositiveClick: () => { - file - .move(source, target, true) - .then(() => { - window.$message.success(`移动 ${source} 到 ${target} 成功`) - }) - .catch((err) => { - window.$message.error(err.message) - }) - }, - onNegativeClick: () => { - marked.value = [] - } - }) - } - }) - .finally(() => { - window.$bus.emit('file:refresh') - }) + // 查重 + let flag = false + let paths = marked.value.map((item) => { + return { + name: item.name, + source: item.source, + target: path.value + '/' + item.name, + force: false } - } - - marked.value = [] + }) + const sources = paths.map((item: any) => item.target) + await file.exist(sources).then(async (res) => { + for (let i = 0; i < res.data.length; i++) { + if (res.data[i]) { + flag = true + paths[i].force = true + } + } + if (flag) { + window.$dialog.warning({ + title: '警告', + content: `存在同名项 + ${paths + .filter((item) => item.force) + .map((item) => item.name) + .join(', ')} 是否覆盖?`, + positiveText: '覆盖', + negativeText: '取消', + onPositiveClick: async () => { + if (markedType.value == 'copy') { + await file.copy(paths).then(() => { + window.$message.success('复制成功') + }) + } else { + await file.move(paths).then(() => { + window.$message.success('移动成功') + }) + } + marked.value = [] + window.$bus.emit('file:refresh') + }, + onNegativeClick: () => { + marked.value = [] + window.$message.info('已取消') + } + }) + } else { + if (markedType.value == 'copy') { + await file.copy(paths).then(() => { + window.$message.success('复制成功') + }) + } else { + await file.move(paths).then(() => { + window.$message.success('移动成功') + }) + } + marked.value = [] + window.$bus.emit('file:refresh') + } + }) } const handleSelect = (key: string) => { @@ -553,21 +533,23 @@ const handleSelect = (key: string) => { editorModal.value = true break case 'copy': + markedType.value = 'copy' marked.value = [ { name: selectedRow.value.name, source: selectedRow.value.full, - type: 'copy' + force: false } ] window.$message.success('标记成功,请前往目标路径粘贴') break case 'move': + markedType.value = 'move' marked.value = [ { name: selectedRow.value.name, source: selectedRow.value.full, - type: 'move' + force: false } ] window.$message.success('标记成功,请前往目标路径粘贴') diff --git a/web/src/views/file/PermissionModal.vue b/web/src/views/file/PermissionModal.vue index c54d772c..dcc0062d 100644 --- a/web/src/views/file/PermissionModal.vue +++ b/web/src/views/file/PermissionModal.vue @@ -17,17 +17,12 @@ const checkbox = ref({ const handlePermission = async () => { for (const path of selected.value) { - await file - .permission(path, `0${mode.value}`, owner.value, group.value) - .then(() => { - window.$message.success(`修改 ${path} 成功`) - show.value = false - selected.value = [] - }) - .catch(() => { - window.$message.error(`修改 ${path} 失败`) - }) + await file.permission(path, `0${mode.value}`, owner.value, group.value).then(() => { + window.$message.success(`修改 ${path} 成功`) + }) } + show.value = false + selected.value = [] window.$bus.emit('file:refresh') } diff --git a/web/src/views/file/ToolBar.vue b/web/src/views/file/ToolBar.vue index 10ef0bba..23fb0f2e 100644 --- a/web/src/views/file/ToolBar.vue +++ b/web/src/views/file/ToolBar.vue @@ -7,6 +7,7 @@ import type { Marked } from '@/views/file/types' const path = defineModel('path', { type: String, required: true }) const selected = defineModel('selected', { type: Array, default: () => [] }) const marked = defineModel('marked', { type: Array, default: () => [] }) +const markedType = defineModel('markedType', { type: String, required: true }) const compress = defineModel('compress', { type: Boolean, required: true }) const permission = defineModel('permission', { type: Boolean, required: true }) @@ -62,11 +63,13 @@ const handleCopy = () => { window.$message.error('请选择要复制的文件/文件夹') return } + markedType.value = 'copy' marked.value = selected.value.map((path) => ({ name: lastDirectory(path), source: path, - type: 'copy' + force: false })) + selected.value = [] window.$message.success('标记成功,请前往目标路径粘贴') } @@ -75,11 +78,13 @@ const handleMove = () => { window.$message.error('请选择要移动的文件/文件夹') return } + markedType.value = 'move' marked.value = selected.value.map((path) => ({ name: lastDirectory(path), source: path, - type: 'move' + force: false })) + selected.value = [] window.$message.success('标记成功,请前往目标路径粘贴') } @@ -93,90 +98,82 @@ const handlePaste = async () => { return } - for (const { name, source, type } of marked.value) { - const target = path.value + '/' + name - if (type === 'copy') { - file - .copy(source, target, false) - .then(() => { - window.$message.success(`复制 ${source} 到 ${target} 成功`) - }) - .catch((err) => { - if (err.message == 'target path already exists') { - window.$dialog.warning({ - title: '警告', - content: `目标 ${target} 已存在,是否覆盖?`, - positiveText: '覆盖', - negativeText: '取消', - onPositiveClick: () => { - file - .copy(source, target, true) - .then(() => { - window.$message.success(`复制 ${source} 到 ${target} 成功`) - }) - .catch((err) => { - window.$message.error(err.message) - }) - }, - onNegativeClick: () => { - handleCancel() - } - }) - } - }) - .finally(() => { - window.$bus.emit('file:refresh') - }) - } else { - file - .move(source, target, false) - .then(() => { - window.$message.success(`移动 ${source} 到 ${target} 成功`) - }) - .catch((err) => { - if (err.message == 'target path already exists') { - window.$dialog.warning({ - title: '警告', - content: `目标 ${target} 已存在,是否覆盖?`, - positiveText: '覆盖', - negativeText: '取消', - onPositiveClick: () => { - file - .move(source, target, true) - .then(() => { - window.$message.success(`移动 ${source} 到 ${target} 成功`) - }) - .catch((err) => { - window.$message.error(err.message) - }) - }, - onNegativeClick: () => { - handleCancel() - } - }) - } - }) - .finally(() => { - window.$bus.emit('file:refresh') - }) + // 查重 + let flag = false + let paths = marked.value.map((item) => { + return { + name: item.name, + source: item.source, + target: path.value + '/' + item.name, + force: false } - } - - marked.value = [] + }) + const sources = paths.map((item: any) => item.target) + await file.exist(sources).then(async (res) => { + for (let i = 0; i < res.data.length; i++) { + if (res.data[i]) { + flag = true + paths[i].force = true + } + } + if (flag) { + window.$dialog.warning({ + title: '警告', + content: `存在同名项 + ${paths + .filter((item) => item.force) + .map((item) => item.name) + .join(', ')} 是否覆盖?`, + positiveText: '覆盖', + negativeText: '取消', + onPositiveClick: async () => { + if (markedType.value == 'copy') { + await file.copy(paths).then(() => { + window.$message.success('复制成功') + }) + } else { + await file.move(paths).then(() => { + window.$message.success('移动成功') + }) + } + marked.value = [] + window.$bus.emit('file:refresh') + }, + onNegativeClick: () => { + marked.value = [] + window.$message.info('已取消') + } + }) + } else { + if (markedType.value == 'copy') { + await file.copy(paths).then(() => { + window.$message.success('复制成功') + }) + } else { + await file.move(paths).then(() => { + window.$message.success('移动成功') + }) + } + marked.value = [] + window.$bus.emit('file:refresh') + } + }) } -const bulkDelete = () => { +const bulkDelete = async () => { if (!selected.value.length) { window.$message.error('请选择要删除的文件/文件夹') return } for (const path of selected.value) { - file.delete(path).then(() => { + await file.delete(path).then(() => { window.$message.success(`删除 ${path} 成功`) window.$bus.emit('file:refresh') }) } + + selected.value = [] } // 自动填充下载文件名 diff --git a/web/src/views/file/UploadModal.vue b/web/src/views/file/UploadModal.vue index 64ab9cfe..641d6adc 100644 --- a/web/src/views/file/UploadModal.vue +++ b/web/src/views/file/UploadModal.vue @@ -33,21 +33,24 @@ const uploadRequest = ({ file, onFinish, onError, onProgress }: UploadCustomRequ :bordered="false" :segmented="false" > - - -
- -
- 点击或者拖动文件到该区域来上传 - 不支持断点续传,大文件建议使用 FTP 上传 -
-
+ + 若上传报网络错误,请开启面板 HTTPS 后重试 + + +
+ +
+ 点击或者拖动文件到该区域来上传 + 大文件建议使用 SFTP 上传 +
+
+
diff --git a/web/src/views/file/types.ts b/web/src/views/file/types.ts index bad67e3a..21c80120 100644 --- a/web/src/views/file/types.ts +++ b/web/src/views/file/types.ts @@ -1,5 +1,5 @@ export interface Marked { name: string source: string - type: string + force: boolean }