feat: 文件复制、粘贴操作支持修改名称 (#2763)

Refs https://github.com/1Panel-dev/1Panel/issues/1570
pull/2764/head^2
zhengkunwang 2023-11-01 18:27:19 +08:00 committed by GitHub
parent f6b094039b
commit c47075beeb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 215 additions and 71 deletions

View File

@ -346,13 +346,11 @@ func (b *BaseApi) CheckFile(c *gin.Context) {
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if _, err := os.Stat(req.Path); err != nil && os.IsNotExist(err) {
helper.SuccessWithData(c, true)
if _, err := os.Stat(req.Path); err != nil {
helper.SuccessWithData(c, false)
return
}
helper.SuccessWithData(c, false)
helper.SuccessWithData(c, true)
}
// @Tags File

View File

@ -78,6 +78,8 @@ type FileMove struct {
Type string `json:"type" validate:"required"`
OldPaths []string `json:"oldPaths" validate:"required"`
NewPath string `json:"newPath" validate:"required"`
Name string `json:"name"`
Cover bool `json:"cover"`
}
type FileDownload struct {
@ -106,3 +108,8 @@ type FileRoleUpdate struct {
Group string `json:"group" validate:"required"`
Sub bool `json:"sub" validate:"required"`
}
type FileExistReq struct {
Name string `json:"name" validate:"required"`
Dir string `json:"dir" validate:"required"`
}

View File

@ -32,3 +32,7 @@ type FileProcessKeys struct {
type FileWgetRes struct {
Key string `json:"key"`
}
type FileExist struct {
Exist bool `json:"exist"`
}

View File

@ -193,7 +193,8 @@ func (f *FileService) DeCompress(c request.FileDeCompress) error {
func (f *FileService) GetContent(op request.FileContentReq) (response.FileInfo, error) {
info, err := files.NewFileInfo(files.FileOption{
Path: op.Path,
Path: op.Path,
Expand: true,
})
if err != nil {
return response.FileInfo{}, err
@ -236,12 +237,12 @@ func (f *FileService) MvFile(m request.FileMove) error {
}
}
if m.Type == "cut" {
return fo.Cut(m.OldPaths, m.NewPath)
return fo.Cut(m.OldPaths, m.NewPath, m.Name, m.Cover)
}
var errs []error
if m.Type == "copy" {
for _, src := range m.OldPaths {
if err := fo.Copy(src, m.NewPath); err != nil {
if err := fo.CopyAndReName(src, m.NewPath, m.Name, m.Cover); err != nil {
errs = append(errs, err)
global.LOG.Errorf("copy file [%s] to [%s] failed, err: %s", src, m.NewPath, err.Error())
}

View File

@ -280,11 +280,21 @@ func (f FileOp) DownloadFile(url, dst string) error {
return nil
}
func (f FileOp) Cut(oldPaths []string, dst string) error {
func (f FileOp) Cut(oldPaths []string, dst, name string, cover bool) error {
for _, p := range oldPaths {
base := filepath.Base(p)
dstPath := filepath.Join(dst, base)
if err := cmd.ExecCmd(fmt.Sprintf("mv %s %s", p, dstPath)); err != nil {
var dstPath string
if name != "" {
dstPath = filepath.Join(dst, name)
} else {
base := filepath.Base(p)
dstPath = filepath.Join(dst, base)
}
coverFlag := ""
if cover {
coverFlag = "-f"
}
cmdStr := fmt.Sprintf("mv %s %s %s", coverFlag, p, dstPath)
if err := cmd.ExecCmd(cmdStr); err != nil {
return err
}
}
@ -314,6 +324,40 @@ func (f FileOp) Copy(src, dst string) error {
return f.CopyFile(src, dst)
}
func (f FileOp) CopyAndReName(src, dst, name string, cover bool) error {
if src = path.Clean("/" + src); src == "" {
return os.ErrNotExist
}
if dst = path.Clean("/" + dst); dst == "" {
return os.ErrNotExist
}
if src == "/" || dst == "/" {
return os.ErrInvalid
}
if dst == src {
return os.ErrInvalid
}
srcInfo, err := f.Fs.Stat(src)
if err != nil {
return err
}
if srcInfo.IsDir() {
dstPath := dst
if name != "" && !cover {
dstPath = filepath.Join(dst, name)
}
return cmd.ExecCmd(fmt.Sprintf("cp -rf %s %s", src, dstPath))
} else {
dstPath := filepath.Join(dst, name)
if cover {
dstPath = dst
}
return cmd.ExecCmd(fmt.Sprintf("cp -f %s %s", src, dstPath))
}
}
func (f FileOp) CopyDir(src, dst string) error {
srcInfo, err := f.Fs.Stat(src)
if err != nil {

View File

@ -14386,12 +14386,18 @@ const docTemplate = `{
"dto.Login": {
"type": "object",
"required": [
"authMethod",
"language",
"name",
"password"
],
"properties": {
"authMethod": {
"type": "string"
"type": "string",
"enum": [
"jwt",
"session"
]
},
"captcha": {
"type": "string"
@ -14403,7 +14409,12 @@ const docTemplate = `{
"type": "boolean"
},
"language": {
"type": "string"
"type": "string",
"enum": [
"zh",
"en",
"tw"
]
},
"name": {
"type": "string"
@ -16878,6 +16889,12 @@ const docTemplate = `{
"type"
],
"properties": {
"cover": {
"type": "boolean"
},
"name": {
"type": "string"
},
"newPath": {
"type": "string"
},

View File

@ -14379,12 +14379,18 @@
"dto.Login": {
"type": "object",
"required": [
"authMethod",
"language",
"name",
"password"
],
"properties": {
"authMethod": {
"type": "string"
"type": "string",
"enum": [
"jwt",
"session"
]
},
"captcha": {
"type": "string"
@ -14396,7 +14402,12 @@
"type": "boolean"
},
"language": {
"type": "string"
"type": "string",
"enum": [
"zh",
"en",
"tw"
]
},
"name": {
"type": "string"
@ -16871,6 +16882,12 @@
"type"
],
"properties": {
"cover": {
"type": "boolean"
},
"name": {
"type": "string"
},
"newPath": {
"type": "string"
},

View File

@ -1279,6 +1279,9 @@ definitions:
dto.Login:
properties:
authMethod:
enum:
- jwt
- session
type: string
captcha:
type: string
@ -1287,12 +1290,18 @@ definitions:
ignoreCaptcha:
type: boolean
language:
enum:
- zh
- en
- tw
type: string
name:
type: string
password:
type: string
required:
- authMethod
- language
- name
- password
type: object
@ -2942,6 +2951,10 @@ definitions:
type: object
request.FileMove:
properties:
cover:
type: boolean
name:
type: string
newPath:
type: string
oldPaths:

View File

@ -225,7 +225,7 @@ const onSubmit = async () => {
return;
}
const res = await CheckFile(baseDir.value + file.raw.name);
if (!res.data) {
if (res.data) {
MsgError(i18n.global.t('commons.msg.fileExist'));
return;
}

View File

@ -383,3 +383,15 @@ html {
float: right;
margin-right: 50px;
}
.text-parent {
display: flex;
width: 100%;
margin-left: 2px;
}
.text-ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

View File

@ -329,3 +329,20 @@ export function downloadWithContent(content: string, fileName: string) {
const event = new MouseEvent('click');
a.dispatchEvent(event);
}
export function getDateStr() {
let now: Date = new Date();
let year: number = now.getFullYear();
let month: number = now.getMonth() + 1;
let date: number = now.getDate();
let hours: number = now.getHours();
let minutes: number = now.getMinutes();
let seconds: number = now.getSeconds();
let timestamp: string = `${year}-${month < 10 ? '0' + month : month}-${date < 10 ? '0' + date : date}-${
hours < 10 ? '0' + hours : hours
}-${minutes < 10 ? '0' + minutes : minutes}-${seconds < 10 ? '0' + seconds : seconds}`;
return timestamp;
}

View File

@ -3,20 +3,12 @@
<el-row>
<el-col :span="20" :offset="2">
<el-alert :title="$t('file.deleteHelper')" show-icon type="error" :closable="false"></el-alert>
<div class="resource">
<table>
<tr v-for="(row, index) in files" :key="index">
<td>
<svg-icon v-if="row.isDir" className="table-icon" iconName="p-file-folder"></svg-icon>
<svg-icon
v-else
className="table-icon"
:iconName="getIconName(row.extension)"
></svg-icon>
<span>{{ row.name }}</span>
</td>
</tr>
</table>
<div class="flx-align-center mb-1 mt-1" v-for="(row, index) in files" :key="index">
<div>
<svg-icon v-if="row.isDir" className="table-icon mr-1 " iconName="p-file-folder"></svg-icon>
<svg-icon v-else className="table-icon mr-1" :iconName="getIconName(row.extension)"></svg-icon>
</div>
<span class="sle">{{ row.name }}</span>
</div>
<div class="mt-5">
<el-checkbox v-model="forceDelete">{{ $t('file.forceDeleteHelper') }}</el-checkbox>

View File

@ -371,7 +371,7 @@ const codeReq = reactive({ path: '', expand: false, page: 1, pageSize: 100 });
const fileUpload = reactive({ path: '' });
const fileRename = reactive({ path: '', oldName: '' });
const fileWget = reactive({ path: '' });
const fileMove = reactive({ oldPaths: [''], type: '', path: '' });
const fileMove = reactive({ oldPaths: [''], type: '', path: '', name: '' });
const processPage = reactive({ open: false });
const createRef = ref();
@ -690,6 +690,9 @@ const openMove = (type: string) => {
oldpaths.push(s['path']);
}
fileMove.oldPaths = oldpaths;
if (selects.value.length == 1) {
fileMove.name = selects.value[0].name;
}
moveOpen.value = true;
};
@ -697,6 +700,7 @@ const closeMove = () => {
selects.value = [];
tableRef.value.clearSelects();
fileMove.oldPaths = [];
fileMove.name = '';
moveOpen.value = false;
};
@ -906,19 +910,6 @@ onMounted(() => {
margin-right: 10px;
}
}
.text-parent {
display: flex;
width: 100%;
margin-left: 2px;
}
.text-ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.favorite-item {
max-height: 650px;
overflow: auto;

View File

@ -18,6 +18,15 @@
<template #prepend><FileList @choose="getPath" :dir="true"></FileList></template>
</el-input>
</el-form-item>
<div v-if="changeName">
<el-form-item :label="$t('commons.table.name')" prop="name">
<el-input v-model="addForm.name"></el-input>
</el-form-item>
<el-radio-group v-model="addForm.cover" @change="changeType">
<el-radio :label="true" size="large">{{ $t('file.replace') }}</el-radio>
<el-radio :label="false" size="large">{{ $t('file.rename') }}</el-radio>
</el-radio-group>
</div>
</el-form>
</el-col>
</el-row>
@ -33,7 +42,7 @@
</template>
<script lang="ts" setup>
import { MoveFile } from '@/api/modules/files';
import { CheckFile, MoveFile } from '@/api/modules/files';
import { Rules } from '@/global/form-rules';
import i18n from '@/lang';
import { FormInstance, FormRules } from 'element-plus';
@ -41,17 +50,21 @@ import { ref, reactive, computed } from 'vue';
import FileList from '@/components/file-list/index.vue';
import DrawerHeader from '@/components/drawer-header/index.vue';
import { MsgSuccess } from '@/utils/message';
import { getDateStr } from '@/utils/util';
interface MoveProps {
oldPaths: Array<string>;
type: string;
path: string;
name: string;
}
const fileForm = ref<FormInstance>();
const loading = ref(false);
let open = ref(false);
let type = ref('cut');
const open = ref(false);
const type = ref('cut');
const changeName = ref(false);
const oldName = ref('');
const title = computed(() => {
if (type.value === 'cut') {
@ -65,10 +78,13 @@ const addForm = reactive({
oldPaths: [] as string[],
newPath: '',
type: '',
name: '',
cover: false,
});
const rules = reactive<FormRules>({
newPath: [Rules.requiredInput],
name: [Rules.requiredInput],
});
const em = defineEmits(['close']);
@ -85,6 +101,14 @@ const getPath = (path: string) => {
addForm.newPath = path;
};
const changeType = () => {
if (addForm.cover) {
addForm.name = oldName.value;
} else {
addForm.name = oldName.value + '-' + getDateStr();
}
};
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
await formEl.validate((valid) => {
@ -107,10 +131,23 @@ const submit = async (formEl: FormInstance | undefined) => {
});
};
const acceptParams = (props: MoveProps) => {
const acceptParams = async (props: MoveProps) => {
changeName.value = false;
addForm.oldPaths = props.oldPaths;
addForm.type = props.type;
addForm.newPath = props.path;
if (props.name && props.name != '') {
oldName.value = props.name;
changeName.value = true;
const res = await CheckFile(props.path + '/' + props.name);
if (res.data) {
addForm.cover = false;
addForm.name = props.name + '-' + getDateStr();
} else {
addForm.cover = true;
addForm.name = props.name;
}
}
type.value = props.type;
open.value = true;
};

View File

@ -3,21 +3,12 @@
<el-row>
<el-col :span="20" :offset="2">
<el-alert :title="$t('file.deleteRecycleHelper')" show-icon type="error" :closable="false"></el-alert>
<div class="resource">
<table aria-describedby="deleteTable">
<th></th>
<tr v-for="(row, index) in files" :key="index">
<td>
<svg-icon v-if="row.isDir" className="table-icon" iconName="p-file-folder"></svg-icon>
<svg-icon
v-else
className="table-icon"
:iconName="getIconName(row.extension)"
></svg-icon>
<span>{{ row.name }}</span>
</td>
</tr>
</table>
<div class="flx-align-center mb-1 mt-1" v-for="(row, index) in files" :key="index">
<div>
<svg-icon v-if="row.isDir" className="table-icon mr-1 " iconName="p-file-folder"></svg-icon>
<svg-icon v-else className="table-icon mr-1" :iconName="getIconName(row.extension)"></svg-icon>
</div>
<span class="sle">{{ row.name }}</span>
</div>
</el-col>
</el-row>

View File

@ -17,13 +17,16 @@
class="mt-5"
>
<el-table-column type="selection" fix />
<el-table-column
:label="$t('commons.table.name')"
min-width="100"
fix
show-overflow-tooltip
prop="name"
></el-table-column>
<el-table-column prop="name" :label="$t('commons.table.name')" show-overflow-tooltip>
<template #default="{ row }">
<span class="text-ellipsis" type="primary">
<svg-icon v-if="row.isDir" className="table-icon" iconName="p-file-folder"></svg-icon>
<svg-icon v-else className="table-icon" iconName="p-file-normal"></svg-icon>
{{ row.name }}
</span>
</template>
</el-table-column>
<el-table-column :label="$t('file.sourcePath')" show-overflow-tooltip prop="sourcePath"></el-table-column>
<el-table-column :label="$t('file.size')" prop="size" max-width="50">
<template #default="{ row }">