2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 06:47:20 +08:00

fix: 文件管理bug

This commit is contained in:
耗子
2024-10-31 21:49:58 +08:00
parent ea3bf37687
commit 336a9bc675
10 changed files with 246 additions and 246 deletions

View File

@@ -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"`

View File

@@ -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)

View File

@@ -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

View File

@@ -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<AxiosResponse<any>> => request.post('/file/exist', paths),
// 移动文件
move: (source: string, target: string, force: boolean): Promise<AxiosResponse<any>> =>
request.post('/file/move', { source, target, force }, { noNeedTip: true } as RequestConfig),
move: (paths: any[]): Promise<AxiosResponse<any>> => request.post('/file/move', paths),
// 复制文件
copy: (source: string, target: string, force: boolean): Promise<AxiosResponse<any>> =>
request.post('/file/copy', { source, target, force }, { noNeedTip: true } as RequestConfig),
copy: (paths: any[]): Promise<AxiosResponse<any>> => request.post('/file/copy', paths),
// 远程下载
remoteDownload: (path: string, url: string): Promise<AxiosResponse<any>> =>
request.post('/file/remoteDownload', { path, url }),

View File

@@ -13,6 +13,7 @@ import type { Marked } from '@/views/file/types'
const path = ref('/www')
const selected = ref<string[]>([])
const marked = ref<Marked[]>([])
const markedType = ref<string>('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"
/>

View File

@@ -15,6 +15,7 @@ const sort = ref<string>('')
const path = defineModel<string>('path', { type: String, required: true })
const selected = defineModel<any[]>('selected', { type: Array, default: () => [] })
const marked = defineModel<Marked[]>('marked', { type: Array, default: () => [] })
const markedType = defineModel<string>('markedType', { type: String, required: true })
const compress = defineModel<boolean>('compress', { type: Boolean, required: true })
const permission = defineModel<boolean>('permission', { type: Boolean, required: true })
const editorModal = ref(false)
@@ -275,21 +276,23 @@ const columns: DataTableColumns<RowData> = [
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('标记成功,请前往目标路径粘贴')

View File

@@ -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')
}

View File

@@ -7,6 +7,7 @@ import type { Marked } from '@/views/file/types'
const path = defineModel<string>('path', { type: String, required: true })
const selected = defineModel<string[]>('selected', { type: Array, default: () => [] })
const marked = defineModel<Marked[]>('marked', { type: Array, default: () => [] })
const markedType = defineModel<string>('markedType', { type: String, required: true })
const compress = defineModel<boolean>('compress', { type: Boolean, required: true })
const permission = defineModel<boolean>('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 = []
}
// 自动填充下载文件名

View File

@@ -33,21 +33,24 @@ const uploadRequest = ({ file, onFinish, onError, onProgress }: UploadCustomRequ
:bordered="false"
:segmented="false"
>
<n-upload
ref="upload"
directory-dnd
multiple
action="/api/panel/file/upload"
:custom-request="uploadRequest"
>
<n-upload-dragger>
<div style="margin-bottom: 12px">
<the-icon :size="48" icon="bi:arrow-up-square" />
</div>
<NText style="font-size: 16px"> 点击或者拖动文件到该区域来上传</NText>
<NP depth="3" style="margin: 8px 0 0 0"> 不支持断点续传大文件建议使用 FTP 上传 </NP>
</n-upload-dragger>
</n-upload>
<n-flex vertical>
<n-alert type="info">若上传报网络错误请开启面板 HTTPS 后重试</n-alert>
<n-upload
ref="upload"
directory-dnd
multiple
action="/api/panel/file/upload"
:custom-request="uploadRequest"
>
<n-upload-dragger>
<div style="margin-bottom: 12px">
<the-icon :size="48" icon="bi:arrow-up-square" />
</div>
<NText text-18> 点击或者拖动文件到该区域来上传</NText>
<NP depth="3" m-10> 大文件建议使用 SFTP 上传 </NP>
</n-upload-dragger>
</n-upload>
</n-flex>
</n-modal>
</template>

View File

@@ -1,5 +1,5 @@
export interface Marked {
name: string
source: string
type: string
force: boolean
}