1
0
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:
zhengkunwang223 2023-05-27 23:15:30 +08:00 committed by GitHub
parent 7cbaa4f63d
commit 2d6925ac4f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 93 additions and 54 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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}] 操作是否继续',

View File

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