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