Browse Source

feat: 文件列表增加批量上传功能 (#1168)

pull/1173/head
zhengkunwang223 2 years ago committed by GitHub
parent
commit
2d6925ac4f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 26
      backend/app/api/v1/file.go
  2. 2
      backend/i18n/lang/en.yaml
  3. 2
      backend/i18n/lang/zh.yaml
  4. 2
      frontend/src/lang/modules/en.ts
  5. 2
      frontend/src/lang/modules/zh.ts
  6. 55
      frontend/src/views/host/file-management/upload/index.vue

26
backend/app/api/v1/file.go

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

2
backend/i18n/lang/en.yaml

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

2
backend/i18n/lang/zh.yaml

@ -39,7 +39,7 @@ ErrPathNotFound: "目录不存在"
ErrMovePathFailed: "目标路径不能包含原路径!"
ErrLinkPathNotFound: "目标路径不存在!"
ErrFileIsExit: "文件已存在!"
ErrFileUpload: "上传文件失败"
ErrFileUpload: "{{ .name }} 上传文件失败 {{ .detail}}"
#website
ErrDomainIsExist: "域名已存在"

2
frontend/src/lang/modules/en.ts

@ -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?',

2
frontend/src/lang/modules/zh.ts

@ -852,6 +852,8 @@ const message = {
containSub: '同时修改子文件属性',
ownerHelper: 'PHP 运行环境默认用户:用户组为 1000:1000, 容器内外用户显示不一致为正常现象',
searchHelper: '支持 * 等通配符',
uploadFailed: '[{0}] 文件上传失败',
fileUploadStart: '正在上传[{0}]....',
},
ssh: {
sshOperate: ' SSH 服务进行 [{0}] 操作是否继续',

55
frontend/src/views/host/file-management/upload/index.vue

@ -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,30 +75,46 @@ const fileOnChange = (_uploadFile: UploadFile, uploadFiles: UploadFiles) => {
const handleExceed: UploadProps['onExceed'] = (files) => {
uploadRef.value!.clearFiles();
const file = files[0] as UploadRawFile;
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;
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, {});
}
const chunkCount = Math.ceil(fileSize / CHUNK_SIZE);
let uploadedChunkCount = 0;
for (let i = 0; i < chunkCount; i++) {
const start = i * CHUNK_SIZE;
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', i.toString());
formData.append('chunkIndex', c.toString());
formData.append('chunkCount', chunkCount.toString());
try {
@ -109,22 +128,32 @@ const submit = async () => {
});
uploadedChunkCount++;
} catch (error) {
loading.value = false;
uploaderFiles.value[i].status = 'fail';
break;
}
if (uploadedChunkCount == chunkCount) {
success++;
uploaderFiles.value[i].status = 'success';
break;
}
}
if (i == files.length - 1) {
loading.value = false;
uploadHelper.value = '';
if (success == files.length) {
uploadRef.value!.clearFiles();
uploaderFiles.value = [];
MsgSuccess(i18n.global.t('file.uploadSuccess'));
}
}
}
};
const acceptParams = (props: UploadFileProps) => {
path.value = props.path;
open.value = true;
uploadPrecent.value = 0;
uploadHelper.value = '';
};
defineExpose({ acceptParams });

Loading…
Cancel
Save