mirror of https://github.com/1Panel-dev/1Panel
feat: 文件复制、粘贴操作支持修改名称 (#2763)
Refs https://github.com/1Panel-dev/1Panel/issues/1570pull/2764/head^2
parent
f6b094039b
commit
c47075beeb
|
@ -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
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -32,3 +32,7 @@ type FileProcessKeys struct {
|
|||
type FileWgetRes struct {
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
type FileExist struct {
|
||||
Exist bool `json:"exist"`
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 }">
|
||||
|
|
Loading…
Reference in New Issue