赞
踩
前端页面使用的elment-plus的el-upload组件。
- <el-upload action="#" :multiple="true" :auto-upload="false" :on-change="handleChange" :show-file-list="false">
- <FileButton content="上传文件" type="primary" class="file-button" />
- </el-upload>
当上传文件后,会调用handleChange 方法,可以在这里进行文件相关的操作。
- //处理文件上传
- const handleChange = async (uploadFile) => {
-
- //文件名字
- let fileName = uploadFile.name
-
- //文件的大小
- const fileSize = uploadFile.size || 0
-
-
- //当前的文件对象
- let fileItem = {}
- fileItem.fileName = fileName
- fileItem.fileSize = fileSize
- fileItem.state = 1 //解码中
- fileItem.progress = 0 //进度是0
- fileItem.filePid = 102903232
- fileItem.fileMd5 = ""
- fileItem.uploadSize = 0
-
- fileUploadList.value.addFile(fileItem)
-
- //弹框显示
- isVisible.value = true
-
- //获得文件的md5
- if (uploadFile.raw) {
- await generateMD5OfFile(uploadFile.raw).then(
- res => {
- fileItem.fileMd5 = res
- }
- )
- }
-
-
-
- fileUploadList.value.addMd5(fileItem.fileName, fileItem.fileMd5)
-
- fileUploadList.value.changeFileState(fileItem.fileName, 2)
-
- //分片上传
- let chunkTotals = Math.ceil(fileSize / chunkSize);
-
- //分片上传
- if (chunkTotals > 0) {
-
- for (let chunkNumber = 0, start = 0; chunkNumber < chunkTotals; chunkNumber++, start += chunkSize) {
- //文件最后的end
- let end = Math.min(fileSize, start + chunkSize);
- // el-mement - plus中,上传的文件就在raw里面
- const files = uploadFile.raw?.slice(start, end)
-
- //上传的结果
- const result = await uploadFileToServer(files, chunkNumber + 1, chunkTotals, fileName , getCurrentId(), fileItem.fileMd5,userId)
- console.log(result.data)
- console.log(result.data.data)
- if (result.data.data.status === 1) {
- // console.log("上传中")
- //上传的进度
- fileUploadList.value.changeProgress(fileItem.fileName, ((end / fileSize) * 100).toFixed(1))
- //修改已经上传完成的文件大小
- fileUploadList.value.changeUploadSize(fileItem.fileName, end)
-
- } else if (result.data.data.status === 3) {
-
- // console.log("上传成功!"),这里是弹窗显示的文件上传进度,可以适当修改
- fileUploadList.value.changeFileState(fileItem.fileName, 3) //上传完成
- fileUploadList.value.changeProgress(fileItem.fileName, 100) // 进度100%
-
- //通过main,进行刷新
- $emit("addChangeNum")
-
- return ; //结束
- } else {
- message("上传失败", 'error')
-
- return; //结束
- }
- }
- }
- }

计算文件的MD5值
- //计算文件的md5
- function generateMD5OfFile(file) {
- return new Promise((resolve, reject) => {
- let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice, // Read in chunks of 2MB
- chunks = Math.ceil(file.size / chunkSize),
- currentChunk = 0,
- spark = new SparkMD5.ArrayBuffer(),
- fileReader = new FileReader();
-
- fileReader.onload = function (e) {
- console.log('read chunk nr', currentChunk + 1, 'of', chunks);
- spark.append(e.target.result); // Append array buffer
- currentChunk++;
-
- if (currentChunk < chunks) {
- loadNext();
- } else {
- resolve(spark.end())
- }
- };
-
- fileReader.onerror = function () {
-
- reject('MD5 calc error')
- };
-
- function loadNext() {
- let start = currentChunk * chunkSize,
- end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;
-
- fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
- }
-
- loadNext();
- })
- }

自定义文件切片大小
- //默认分片大小
- const chunkSize = 5 * 1024 * 1024
上传文件到服务器
- // 上传文件到服务器
- const uploadFileToServer = async (file, chunkNumber, chunkTotal, fileName,filePid, fileMd5,userId) => {
- const form = new FormData();
-
- // 这里的data是文件
- form.append("file", file);
- form.append("chunkNumber", chunkNumber);
- form.append("chunkTotal", chunkTotal);
- form.append("fileName", fileName)
- form.append("fileMd5", fileMd5)
- form.append("filePid", filePid)
- form.append("userId", userId)
-
-
- var result = await axios({
- url: env_server_production + '/file/upload',
- headers: { 'Content-Type': 'multipart/form-data' },
- method: "post",
- timeout: 1000000,
- data: form
- })
-
- return result
- }

可以简单的实现对一些文件的预览,比如图片、视频、word、pdf等等。
pdf:
等等
这里使用的是vue-office
- <template>
- <div class="preview-body">
-
- <!-- word -->
- <vue-office-docx v-if="getFileType() == 1" :src="getFileUrl()" style="height: 400px;" @rendered="renderedHandler"
- @error="errorHandler" />
-
- <!-- pdf -->
- <vue-office-pdf v-else-if="getFileType() == 2" :src="getFileUrl()" style="height: 400px;"
- @rendered="renderedHandler" @error="errorHandler" />
-
- <!-- iamge -->
- <div v-else-if="getFileType() == 3">
- <el-image :src="getFileUrl()" style="height: 100px; width: 100px;" :zoom-rate="1.2" :max-scale="7"
- :min-scale="0.2" :preview-src-list="imageList" :initial-index="4" />
- <br>
- <el-text style="margin-left: 0px;" link type="primary">点击图片查看详情</el-text>
- </div>
-
- <!-- 不支持显示 -->
- <div v-else-if="getFileType() == 4">
- <br>
- 该文件不支持在线浏览,请下载后查看!
- </div>
-
- <!-- 视频 -->
- <div v-else-if="getFileType() == 5">
-
- <video autoplay width="1200px" height="400px" controls
- :src="getFileUrl()"
- id="myVideo"
- >
-
- </video>
-
- </div>
-
- <!-- 文本显示 -->
- <div v-else>
- <el-scrollbar height="400px" class="document-preview">
- <pre>{{ documentContent }}</pre>
- </el-scrollbar>
- </div>
-
- </div>
- </template>
-
- <script setup>
- //引入相关样式
- import VueOfficeDocx from '@vue-office/docx'
- import VueOfficePdf from '@vue-office/pdf'
- import '@vue-office/docx/lib/index.css'
- import { ref } from 'vue'
- import axios from 'axios';
-
-
-
- const props = defineProps(['file'])
- const video = document.getElementById("myVideo")
-
-
- const getFileUrl = () => {
- return "http://60.205.141.200:9000/" + props.file.filePath;
- }
- const getFileType = () => {
- let category = props.file.fileCategory
-
- if (category == 18 || category == 19) {
- return 1
- }
- else if (category == 13)
- return 2
-
- else if (category == 9 || category == 14 || category == 5) {
- imageList.value.push(getFileUrl())
- return 3
- }
- else if (category == 20 || category == 11 || category == 15)
- return 4
-
- else if (category == 12) {
- //视频
- return 5
- } else {
- //文本
- readDocumentContent();
- }
-
- }
-
- const readDocumentContent = async () => {
- var res = await axios.get(getFileUrl(), {
- responseType: 'text',
- })
- documentContent.value = `\n${res.data}\n`
- }
- //文件中的内容
- const documentContent = ref('')
- //图片列表
- const imageList = ref([])
-
- const renderedHandler = () => {
- console.log("渲染成功")
- }
- const errorHandler = () => {
- console.log("渲染失败")
- }
-
- </script>
-
- <style lang="scss" scoped>
- .document-preview {
- margin-right: 100px;
- background-color: #ccc;
-
- width: 1164px;
- border: 2px solid #ccc;
- height: 400px;
- border-radius: 0 0 10px 10px;
- text-align: left;
- }
- pre {
- font-family: 'Microsoft YaHei';
- }
- </style>

后端使用minio,minio先接收分片文件,上传完成所有的分片文件后,在合并分片文件,删除中间文件即可。
- /**
- * 上传文件方法。
- * 该方法负责检查文件是否已存在,如果存在,则返回已存在标志;如果不存在且是完整文件,则上传文件到MinIO并保存文件信息到数据库。
- *
- * @param fileVO 文件相关信息VO,包含文件本身、MD5、文件名等。
- * @return 如果文件已存在,返回秒传状态码;如果文件上传完成,返回上传完成状态码;否则返回null。
- * @throws GeneralException 如果文件为空,抛出通用异常。
- */
- @Override
- @Transactional(rollbackFor = Exception.class) //所有的操作都在一个事务里面。
- public HashMap<Object, Object> uploadFile(FileVO fileVO) {
-
- if(fileVO.getFile().isEmpty())
- throw new GeneralException("文件上传异常");
-
- FileInfo insertItem = new FileInfo();
- Date now = new Date();
- HashMap<Object, Object> map = new HashMap<>();
-
- //第一片文件
- if(fileVO.getChunkNumber() == 1){
- //先去数据库看看有没有这个文件
- QueryWrapper<FileInfo> queryWrapper = new QueryWrapper<>();
- queryWrapper.eq("file_md5", fileVO.getFileMd5());
-
- //通过Md5查询,别人是不是已经传过这个文件了(文件名不影响文件的MD5值)。
- List<FileInfo> fileInfoList = fileInfoMapper.selectList(queryWrapper);
- FileInfo fileInfo = null;
- if(fileInfoList.size() > 0){
- fileInfo = fileInfoList.get(0);
- }
- //别人已经上传过这个文件了,直接秒传
- if(fileInfo != null){
- log.info("服务器中有相同的文件,直接秒传");
- //说明minIO中有对应的文件
- insertItem.setUserId(fileVO.getUserId());
- insertItem.setFileMd5(fileVO.getFileMd5());
- insertItem.setFileName(fileInfo.getFileName());
- insertItem.setFileCategory(fileInfo.getFileCategory());
- insertItem.setFileId(StringUtil.getRandomString(10));
- insertItem.setDelFlag(FileDelFlagEnums.USING.getFlag());
- insertItem.setFilePid(fileVO.getFilePid());
- insertItem.setFilePath(fileInfo.getFilePath());
- insertItem.setCreateTime(now);
- insertItem.setFileSize(fileInfo.getFileSize());
- insertItem.setState(UploadStatus.UPLOAD_FINISH.getStatus());
-
-
- fileInfoMapper.insert(insertItem);
- System.err.println(insertItem);
-
- map.put("status",UploadStatus.UPLOAD_FINISH.getStatus());
- map.put("fileId",insertItem.getFileId());
- return map;
- }
- //插入 一个切片
- redisUtil.set(fileVO.getFileMd5(),0);
- }
-
- if(Integer.parseInt(redisUtil.get(fileVO.getFileMd5()).toString()) >= fileVO.getChunkNumber()){
- //说明这片文件已经上传过了。
- map.put("status",UploadStatus.UPLOADING.getStatus());
- return map;
- }
-
- //只有一段,直接放到服务器就行
- if(fileVO.getChunkTotal() == 1){
- int lastDotIndex = fileVO.getFileName().lastIndexOf(".");
- String type = fileVO.getFileName().substring(lastDotIndex + 1);
-
- String url = minioUtils.uploadFile(MessageConstant.MINIO_BUCKET,fileVO.getFileName(), fileVO.getFile());
-
- insertItem.setUserId(fileVO.getUserId());
- insertItem.setFileMd5(fileVO.getFileMd5());
- insertItem.setFileName(fileVO.getFileName());
- insertItem.setFileCategory(FileCategoryEnums.getByCode(type).getCategory());
- insertItem.setFileId(StringUtil.getRandomString(10));
- insertItem.setDelFlag(FileDelFlagEnums.USING.getFlag());
- insertItem.setFilePid(fileVO.getFilePid());
- insertItem.setFilePath(url);
- insertItem.setCreateTime(now);
- insertItem.setFileSize(fileVO.getFile().getSize());
- insertItem.setState(UploadStatus.UPLOAD_FINISH.getStatus());
-
- fileInfoMapper.insert(insertItem);
-
- //删除redis中的切片上传信息
- redisUtil.del(fileVO.getFileMd5());
- map.put("status",UploadStatus.UPLOAD_FINISH.getStatus());
- map.put("fileId",insertItem.getFileId());
-
- return map;
- }
-
- log.info("分片上传====> md5 :{} ,=====> index :{}",fileVO.getFileMd5(),fileVO.getChunkNumber());
- //不止一片,继续上传
- //放切片文件的目录是 文件的userId + md5值,这个是唯一的。
- String objectName = fileVO.getUserId() + fileVO.getFileMd5() ;
-
- try {
- minioUtils.putChunkObject(fileVO.getFile().getInputStream(), MessageConstant.MINIO_BUCKET, objectName + "/" + fileVO.getChunkNumber());
- } catch (IOException e) {
- throw new GeneralException("文件上传异常!");
- }
-
- //最后一片,进行合并
- if(Objects.equals(fileVO.getChunkNumber(), fileVO.getChunkTotal())){
-
- //获得文件类型
- int lastDotIndex = fileVO.getFileName().lastIndexOf(".");
- String type = fileVO.getFileName().substring(lastDotIndex + 1);
-
- //objectName : userId+md5
- String filePath = minioUtils.composeObject(MessageConstant.MINIO_BUCKET,MessageConstant.MINIO_BUCKET,objectName, type);
-
-
- insertItem.setUserId(fileVO.getUserId());
- insertItem.setFileMd5(fileVO.getFileMd5());
- insertItem.setFileName(fileVO.getFileName());
- insertItem.setFileCategory(FileCategoryEnums.getByCode(type).getCategory());
- insertItem.setFileId(StringUtil.getRandomString(10));
- insertItem.setDelFlag(FileDelFlagEnums.USING.getFlag());
- insertItem.setFilePid(fileVO.getFilePid());
- insertItem.setFilePath(filePath);
- insertItem.setCreateTime(now);
- Long fileSize = MessageConstant.DEFAULT_CHUNK_SIZE * (fileVO.getChunkTotal() - 1) + fileVO.getFile().getSize();
- insertItem.setFileSize(fileSize);
- insertItem.setState(UploadStatus.UPLOAD_FINISH.getStatus());
-
- //插入一条数据
- System.out.println(fileInfoMapper.insert(insertItem));
-
- //删除minio中的临时文件目录
- System.out.println(minioUtils.deleteFolder(MessageConstant.MINIO_BUCKET, objectName));
-
- //删除redis中的切片上传信息
- redisUtil.del(fileVO.getFileMd5());
- map.put("status",UploadStatus.UPLOAD_FINISH.getStatus());
- map.put("fileId",insertItem.getFileId());
-
- return map;
- }
-
- //更新redis中的切片上传信息
- redisUtil.incrby(fileVO.getFileMd5(),1);
-
- //上传中
- map.put("status",UploadStatus.UPLOADING.getStatus());
- return map;
- }

一个文件有个不重复的md5值,所谓的秒传其实就是你要上传的文件,别人已经上传过了,minio中已经有这个文件了,再解析完文件的md5值之后,后端发现数据库中md5存在了,所以就不用上传文件了,直接在数据库中创建一个信息即可,也就实现了秒传。
传统传递过程是一整个文件上传,如果中断了下次传的时候,需要重新上传;断点传递,每次传递的时候,可以把分片信息放到redis中,同时下一次传分片的时候,判断一下,redis中时候已经有了这个分片,如果有就不用上传此分片文件,即断点传递。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。