mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-02-08 01:20:07 +08:00
feat: 文件列表增加批量上传功能 (#1168)
This commit is contained in:
parent
7cbaa4f63d
commit
2d6925ac4f
@ -575,6 +575,7 @@ func mergeChunks(fileName string, fileDir string, dstDir string, chunkCount int)
|
|||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Router /files/chunkupload [post]
|
// @Router /files/chunkupload [post]
|
||||||
func (b *BaseApi) UploadChunkFiles(c *gin.Context) {
|
func (b *BaseApi) UploadChunkFiles(c *gin.Context) {
|
||||||
|
var err error
|
||||||
fileForm, err := c.FormFile("chunk")
|
fileForm, err := c.FormFile("chunk")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||||
@ -585,19 +586,16 @@ func (b *BaseApi) UploadChunkFiles(c *gin.Context) {
|
|||||||
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
chunkIndex, err := strconv.Atoi(c.PostForm("chunkIndex"))
|
chunkIndex, err := strconv.Atoi(c.PostForm("chunkIndex"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
chunkCount, err := strconv.Atoi(c.PostForm("chunkCount"))
|
chunkCount, err := strconv.Atoi(c.PostForm("chunkCount"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fileOp := files.NewFileOp()
|
fileOp := files.NewFileOp()
|
||||||
tmpDir := path.Join(global.CONF.System.TmpDir, "upload")
|
tmpDir := path.Join(global.CONF.System.TmpDir, "upload")
|
||||||
if !fileOp.Stat(tmpDir) {
|
if !fileOp.Stat(tmpDir) {
|
||||||
@ -606,37 +604,45 @@ func (b *BaseApi) UploadChunkFiles(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
filename := c.PostForm("filename")
|
filename := c.PostForm("filename")
|
||||||
fileDir := filepath.Join(tmpDir, filename)
|
fileDir := filepath.Join(tmpDir, filename)
|
||||||
|
|
||||||
_ = os.MkdirAll(fileDir, 0755)
|
_ = os.MkdirAll(fileDir, 0755)
|
||||||
filePath := filepath.Join(fileDir, filename)
|
filePath := filepath.Join(fileDir, filename)
|
||||||
|
|
||||||
emptyFile, err := os.Create(filePath)
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
_ = os.Remove(fileDir)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
var (
|
||||||
|
emptyFile *os.File
|
||||||
|
chunkData []byte
|
||||||
|
)
|
||||||
|
|
||||||
|
emptyFile, err = os.Create(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer emptyFile.Close()
|
defer emptyFile.Close()
|
||||||
|
|
||||||
chunkData, err := io.ReadAll(uploadFile)
|
chunkData, err = io.ReadAll(uploadFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrFileUpload, err)
|
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, buserr.WithMap(constant.ErrFileUpload, map[string]interface{}{"name": filename, "detail": err.Error()}, err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
chunkPath := filepath.Join(fileDir, fmt.Sprintf("%s.%d", filename, chunkIndex))
|
chunkPath := filepath.Join(fileDir, fmt.Sprintf("%s.%d", filename, chunkIndex))
|
||||||
err = os.WriteFile(chunkPath, chunkData, 0644)
|
err = os.WriteFile(chunkPath, chunkData, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrFileUpload, err)
|
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, buserr.WithMap(constant.ErrFileUpload, map[string]interface{}{"name": filename, "detail": err.Error()}, err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if chunkIndex+1 == chunkCount {
|
if chunkIndex+1 == chunkCount {
|
||||||
err = mergeChunks(filename, fileDir, c.PostForm("path"), chunkCount)
|
err = mergeChunks(filename, fileDir, c.PostForm("path"), chunkCount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrFileUpload, err)
|
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, buserr.WithMap(constant.ErrFileUpload, map[string]interface{}{"name": filename, "detail": err.Error()}, err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
helper.SuccessWithData(c, true)
|
helper.SuccessWithData(c, true)
|
||||||
|
@ -39,7 +39,7 @@ ErrPathNotFound: "Path is not found"
|
|||||||
ErrMovePathFailed: "The target path cannot contain the original path!"
|
ErrMovePathFailed: "The target path cannot contain the original path!"
|
||||||
ErrLinkPathNotFound: "Target path does not exist!"
|
ErrLinkPathNotFound: "Target path does not exist!"
|
||||||
ErrFileIsExit: "File already exists!"
|
ErrFileIsExit: "File already exists!"
|
||||||
ErrFileUpload: "Failed to upload file"
|
ErrFileUpload: "Failed to upload file {{.name}} {{.detail}}"
|
||||||
|
|
||||||
#website
|
#website
|
||||||
ErrDomainIsExist: "Domain is already exist"
|
ErrDomainIsExist: "Domain is already exist"
|
||||||
|
@ -39,7 +39,7 @@ ErrPathNotFound: "目录不存在"
|
|||||||
ErrMovePathFailed: "目标路径不能包含原路径!"
|
ErrMovePathFailed: "目标路径不能包含原路径!"
|
||||||
ErrLinkPathNotFound: "目标路径不存在!"
|
ErrLinkPathNotFound: "目标路径不存在!"
|
||||||
ErrFileIsExit: "文件已存在!"
|
ErrFileIsExit: "文件已存在!"
|
||||||
ErrFileUpload: "上传文件失败"
|
ErrFileUpload: "{{ .name }} 上传文件失败 {{ .detail}}"
|
||||||
|
|
||||||
#website
|
#website
|
||||||
ErrDomainIsExist: "域名已存在"
|
ErrDomainIsExist: "域名已存在"
|
||||||
|
@ -849,6 +849,8 @@ const message = {
|
|||||||
ownerHelper:
|
ownerHelper:
|
||||||
'The default user of the PHP operating environment: the user group is 1000:1000, it is normal that the users inside and outside the container show inconsistencies',
|
'The default user of the PHP operating environment: the user group is 1000:1000, it is normal that the users inside and outside the container show inconsistencies',
|
||||||
searchHelper: 'Support wildcards such as *',
|
searchHelper: 'Support wildcards such as *',
|
||||||
|
uploadFailed: '[{0}] File Upload file',
|
||||||
|
fileUploadStart: 'Uploading [{0}]....',
|
||||||
},
|
},
|
||||||
ssh: {
|
ssh: {
|
||||||
sshOperate: 'Operation [{0}] on the SSH service is performed. Do you want to continue?',
|
sshOperate: 'Operation [{0}] on the SSH service is performed. Do you want to continue?',
|
||||||
|
@ -852,6 +852,8 @@ const message = {
|
|||||||
containSub: '同时修改子文件属性',
|
containSub: '同时修改子文件属性',
|
||||||
ownerHelper: 'PHP 运行环境默认用户:用户组为 1000:1000, 容器内外用户显示不一致为正常现象',
|
ownerHelper: 'PHP 运行环境默认用户:用户组为 1000:1000, 容器内外用户显示不一致为正常现象',
|
||||||
searchHelper: '支持 * 等通配符',
|
searchHelper: '支持 * 等通配符',
|
||||||
|
uploadFailed: '[{0}] 文件上传失败',
|
||||||
|
fileUploadStart: '正在上传[{0}]....',
|
||||||
},
|
},
|
||||||
ssh: {
|
ssh: {
|
||||||
sshOperate: '对 SSH 服务进行 [{0}] 操作,是否继续?',
|
sshOperate: '对 SSH 服务进行 [{0}] 操作,是否继续?',
|
||||||
|
@ -15,8 +15,10 @@
|
|||||||
:auto-upload="false"
|
:auto-upload="false"
|
||||||
ref="uploadRef"
|
ref="uploadRef"
|
||||||
:on-change="fileOnChange"
|
:on-change="fileOnChange"
|
||||||
:limit="1"
|
|
||||||
:on-exceed="handleExceed"
|
:on-exceed="handleExceed"
|
||||||
|
:on-success="hadleSuccess"
|
||||||
|
show-file-list
|
||||||
|
multiple
|
||||||
>
|
>
|
||||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||||
<div class="el-upload__text">
|
<div class="el-upload__text">
|
||||||
@ -24,7 +26,8 @@
|
|||||||
<em>{{ $t('database.clickHelper') }}</em>
|
<em>{{ $t('database.clickHelper') }}</em>
|
||||||
</div>
|
</div>
|
||||||
<template #tip>
|
<template #tip>
|
||||||
<el-progress v-if="loading" text-inside :stroke-width="12" :percentage="uploadPrecent"></el-progress>
|
<el-text>{{ uploadHelper }}</el-text>
|
||||||
|
<el-progress v-if="loading" text-inside :stroke-width="20" :percentage="uploadPrecent"></el-progress>
|
||||||
</template>
|
</template>
|
||||||
</el-upload>
|
</el-upload>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@ -41,7 +44,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { UploadFile, UploadFiles, UploadInstance, UploadProps, UploadRawFile } from 'element-plus';
|
import { UploadFile, UploadFiles, UploadInstance, UploadProps, UploadRawFile } from 'element-plus';
|
||||||
import { ChunkUploadFileData } from '@/api/modules/files';
|
import { ChunkUploadFileData, UploadFileData } from '@/api/modules/files';
|
||||||
import i18n from '@/lang';
|
import i18n from '@/lang';
|
||||||
import DrawerHeader from '@/components/drawer-header/index.vue';
|
import DrawerHeader from '@/components/drawer-header/index.vue';
|
||||||
import { MsgSuccess } from '@/utils/message';
|
import { MsgSuccess } from '@/utils/message';
|
||||||
@ -51,11 +54,11 @@ interface UploadFileProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const uploadRef = ref<UploadInstance>();
|
const uploadRef = ref<UploadInstance>();
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
let uploadPrecent = ref(0);
|
let uploadPrecent = ref(0);
|
||||||
const open = ref(false);
|
const open = ref(false);
|
||||||
const path = ref();
|
const path = ref();
|
||||||
|
let uploadHelper = ref('');
|
||||||
|
|
||||||
const em = defineEmits(['close']);
|
const em = defineEmits(['close']);
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
@ -72,51 +75,76 @@ const fileOnChange = (_uploadFile: UploadFile, uploadFiles: UploadFiles) => {
|
|||||||
|
|
||||||
const handleExceed: UploadProps['onExceed'] = (files) => {
|
const handleExceed: UploadProps['onExceed'] = (files) => {
|
||||||
uploadRef.value!.clearFiles();
|
uploadRef.value!.clearFiles();
|
||||||
const file = files[0] as UploadRawFile;
|
for (let i = 0; i < files.length; i++) {
|
||||||
uploadRef.value!.handleStart(file);
|
const file = files[i] as UploadRawFile;
|
||||||
|
uploadRef.value!.handleStart(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const hadleSuccess: UploadProps['onSuccess'] = (res, file) => {
|
||||||
|
console.log(file.name);
|
||||||
|
file.status = 'success';
|
||||||
};
|
};
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
const file = uploaderFiles.value[0];
|
let success = 0;
|
||||||
|
const files = uploaderFiles.value.slice();
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
const file = files[i];
|
||||||
|
const CHUNK_SIZE = 1024 * 1024; // 1MB
|
||||||
|
const fileSize = file.size;
|
||||||
|
|
||||||
const CHUNK_SIZE = 1024 * 1024; // 1MB
|
uploadHelper.value = i18n.global.t('file.fileUploadStart', [file.name]);
|
||||||
const fileSize = file.size;
|
if (fileSize == 0) {
|
||||||
const chunkCount = Math.ceil(fileSize / CHUNK_SIZE);
|
const formData = new FormData();
|
||||||
let uploadedChunkCount = 0;
|
formData.append('file', file.raw);
|
||||||
|
formData.append('path', path.value);
|
||||||
for (let i = 0; i < chunkCount; i++) {
|
await UploadFileData(formData, {});
|
||||||
const start = i * CHUNK_SIZE;
|
|
||||||
const end = Math.min(start + CHUNK_SIZE, fileSize);
|
|
||||||
const chunk = file.raw.slice(start, end);
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
|
|
||||||
formData.append('filename', file.name);
|
|
||||||
formData.append('path', path.value);
|
|
||||||
formData.append('chunk', chunk);
|
|
||||||
formData.append('chunkIndex', i.toString());
|
|
||||||
formData.append('chunkCount', chunkCount.toString());
|
|
||||||
|
|
||||||
try {
|
|
||||||
await ChunkUploadFileData(formData, {
|
|
||||||
onUploadProgress: (progressEvent) => {
|
|
||||||
const progress = Math.round(
|
|
||||||
((uploadedChunkCount + progressEvent.loaded / progressEvent.total) * 100) / chunkCount,
|
|
||||||
);
|
|
||||||
uploadPrecent.value = progress;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
uploadedChunkCount++;
|
|
||||||
} catch (error) {
|
|
||||||
loading.value = false;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
if (uploadedChunkCount == chunkCount) {
|
|
||||||
|
const chunkCount = Math.ceil(fileSize / CHUNK_SIZE);
|
||||||
|
let uploadedChunkCount = 0;
|
||||||
|
for (let c = 0; c < chunkCount; c++) {
|
||||||
|
const start = c * CHUNK_SIZE;
|
||||||
|
const end = Math.min(start + CHUNK_SIZE, fileSize);
|
||||||
|
const chunk = file.raw.slice(start, end);
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
formData.append('filename', file.name);
|
||||||
|
formData.append('path', path.value);
|
||||||
|
formData.append('chunk', chunk);
|
||||||
|
formData.append('chunkIndex', c.toString());
|
||||||
|
formData.append('chunkCount', chunkCount.toString());
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ChunkUploadFileData(formData, {
|
||||||
|
onUploadProgress: (progressEvent) => {
|
||||||
|
const progress = Math.round(
|
||||||
|
((uploadedChunkCount + progressEvent.loaded / progressEvent.total) * 100) / chunkCount,
|
||||||
|
);
|
||||||
|
uploadPrecent.value = progress;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
uploadedChunkCount++;
|
||||||
|
} catch (error) {
|
||||||
|
uploaderFiles.value[i].status = 'fail';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (uploadedChunkCount == chunkCount) {
|
||||||
|
success++;
|
||||||
|
uploaderFiles.value[i].status = 'success';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (i == files.length - 1) {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
uploadRef.value!.clearFiles();
|
uploadHelper.value = '';
|
||||||
uploaderFiles.value = [];
|
if (success == files.length) {
|
||||||
MsgSuccess(i18n.global.t('file.uploadSuccess'));
|
uploadRef.value!.clearFiles();
|
||||||
|
uploaderFiles.value = [];
|
||||||
|
MsgSuccess(i18n.global.t('file.uploadSuccess'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -125,6 +153,7 @@ const acceptParams = (props: UploadFileProps) => {
|
|||||||
path.value = props.path;
|
path.value = props.path;
|
||||||
open.value = true;
|
open.value = true;
|
||||||
uploadPrecent.value = 0;
|
uploadPrecent.value = 0;
|
||||||
|
uploadHelper.value = '';
|
||||||
};
|
};
|
||||||
|
|
||||||
defineExpose({ acceptParams });
|
defineExpose({ acceptParams });
|
||||||
|
Loading…
x
Reference in New Issue
Block a user