赞
踩
在日常生活中,文件上传相关的操作随处可见,大到处理大数据量的文件,小到头像上传,都离不开文件上传操作,但是当一个文件的大小超过了某个阈值时,这个文件的上传过程就会变得及其的慢,且会消耗大量网络资源,这是我们不愿意看到的,所以,文件分片上传孕育而生。
文件分片上传就是将一整个文件分为几个小块,然后将这几个小块分别传送给服务器,从而实现分片上传。
上图为文件分片的图解,在本图中,我们假定每一个分片都为67MB。(只是演示,实际文件分片需要考虑更多细节)
如果当我们分片到最后一片的时候,我们就会直接将剩余所有空间存放到一个切片中,不管大小是否足够我们指定的大小。
注意:这里的最后一片是指剩余的文件大小小于等于我们分片指定大小的情况。
在进行文件分片时,我们需要按照实际情况下文件大小来指定每一个切片的大小。并且需要在切片后将所有切片数量做记录,具体流程将以列表形式呈现:
以上是文件分片上传时前后端的基础流程(可能有些地方写的不够严谨,希望各位大佬指教)
特别注意:在文件合并时要注意分片文件合并的顺序问题,如果顺序颠倒,那文件自然无法正常显示。
个人建议所有分片文件命名后面跟上一个索引.
声明:此代码没有考虑过多细节,只是作为一个基础展示的案例。
- <!doctype html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport"
- content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
- <meta http-equiv="X-UA-Compatible" content="ie=edge">
- <title>Document</title>
- <style>
- .msg{
- font-size: 20px;
- font-weight: bold;
- }
- </style>
- </head>
- <body>
- <input type="file">
- <p class="msg"></p>
- <script src="js/axios.js"></script>
- <script src="js/spark-md5.js"></script>
- <script>
- const statusCode = {
- UPLOAD_SUCCESS: 200,
- NOT_UPLOAD: 202,
- ALREADY_UPLOAD: 1000,
- UPLOAD_FAILED: 1004
- }
- let chunkSize = 2 * 1024 * 1024
- let msg = document.querySelector(".msg")
- let file = document.querySelector("input[type='file']")
- file.addEventListener("change", async (e) => {
- let fileList = e.target.files
- let file = fileList[0]
- let chunkArr = chunk(file, chunkSize)
- let fileHash = await hash(chunkArr)
- let filename = file.name
- //false:没上传 true:上传过了
- let hasUpload = await check(fileHash, filename)
- if (!hasUpload) {
- let promises = []
- for (let i = 0; i < chunkArr.length; i++) {
- //将最后的返回结果添加到数组中
- let res = await upload(fileHash, chunkArr, i, filename)
- promises.push(res)
- }
- Promise.all(promises).then(res => {
- mergeNotify(fileHash, filename, chunkArr.length)
- msg.innerHTML="文件上传成功"
- msg.style.color="green"
- }).catch(err => {
- console.error(err)
- })
- } else {
- //文件上传过了,无需再次上传
- msg.innerHTML="文件已经上传!!"
- msg.style.color="red"
- }
-
- })
- /**
- *
- * @param file 文件File对象
- * @param chunkSize 每一个切片的大小
- * @return {[]} 返回切片数组
- */
- const chunk = (file, chunkSize) => {
- let res = []
- for (let i = 0; i < file.size; i += chunkSize) {
- res.push(file.slice(i, i + chunkSize))
- }
- return res
- }
- /**
- *
- * @param chunks 切片数组
- * @return string 返回文件hash
- */
- const hash = async (chunks) => {
- let sparkMD5 = new SparkMD5.ArrayBuffer()
- //存储每个切片加密的任务状态,全部完成后,才会返回最终hash
- let promises = []
- //将切片数组所有切片转为二进制,并将其合并为一个完整文件
- for (let i = 0; i < chunks.length; i++) {
- //由于hash加密耗时,所以我们采用异步
- let promise = new Promise((resolve, reject) => {
- let fileReader = new FileReader()//使用fileReader对象将文件切片转为二进制
- fileReader.readAsArrayBuffer(chunks[i])
- fileReader.onload = (e) => {
- //添加到SparkMD5中,等所有切片添加完毕后,获取最终哈希
- sparkMD5.append(e.target.result)
- //每次添加成功后返回一个成功状态
- resolve()
- }
- fileReader.onerror = (e) => {
- reject(e.target.error)
- }
- })
- //将该promise任务添加到promise数组中
- promises.push(promise)
- }
- //当所有加密任务全都完成后,返回加密后的完整文件hash
- return await Promise.all(promises).then(res => {
- return sparkMD5.end()
- }).catch(err => {
- console.error("Hash加密出现问题")
- })
- }
- /***
- *
- * @param hash 文件hash
- * @param chunks 切片数组
- * @param currentIndex 当前切片索引
- * @param filename 文件名
- * @return 返回Promise,用于检测当前切片是否上传成功
- */
- const upload = (hash, chunks, currentIndex, filename) => {
- return new Promise((resolve, reject) => {
- let formData = new FormData()
- formData.append("hash", hash)
- formData.append("chunkIndex", currentIndex)
- formData.append("filename", filename)
- formData.append("chunkBody", chunks[currentIndex])
- axios.post("http://localhost:8080/upload", formData).then(res => {
- //出现无法判断是否成功的问题,推荐判断是否成功在Promise.all中判断
- resolve("")
- }).catch(err => {
- reject(err)
- })
- })
- }
- /***
- * 通知后端接口:可以开始合并任务了
- * @param hash 文件hash
- * @param filename 文件名
- */
- const mergeNotify = (hash, filename, chunksLen) => {
- let formData = new FormData()
- formData.append("filename", filename)
- formData.append("fileHash", hash)
- formData.append("totalChunk", chunksLen)
- axios.post("http://localhost:8080/merge", formData).then(res => {})
- }
- /**
- * 检查文件是否上传
- * @param hash 文件hash
- * @param filename 文件名
- * @return {Promise<Boolean>} 返回一个Promise对象
- */
- const check = async (hash, filename) => {
- let formData = new FormData()
- formData.append("filename", filename)
- formData.append("fileHash", hash)
- let hasUpload = axios.post("http://localhost:8080/check", formData).then(res => {
- let result;
- //判断是否上传过该文件
- if (res.data.code === statusCode.NOT_UPLOAD) {
- result = false
- } else {
- result = true
- }
- //返回promise对象
- return Promise.resolve(result)
- })
- return hasUpload
- }
- </script>
- </body>
- </html>
- package com.cc.fileupload.entity;
-
- /**
- * @author CC
- * @date Created in 2024/2/7 12:15
- */
- public class BaseFile {
- /**
- * 文件hash
- */
- private String fileHash;
-
- public BaseFile() {
- }
-
- public BaseFile(String fileHash, String filename) {
- this.fileHash = fileHash;
- this.filename = filename;
- }
-
- /**
- * 文件名
- */
- private String filename;
-
- @Override
- public String toString() {
- return "BaseFile{" +
- "fileHash='" + fileHash + '\'' +
- ", filename='" + filename + '\'' +
- '}';
- }
-
- public String getFileHash() {
- return fileHash;
- }
-
- public void setFileHash(String fileHash) {
- this.fileHash = fileHash;
- }
-
- public String getFilename() {
- return filename;
- }
-
- public void setFilename(String filename) {
- this.filename = filename;
- }
- }
- package com.cc.fileupload.entity;
-
- /**
- * @author CC
- * @date Created in 2024/2/7 11:27
- */
- public class MergeFile {
- /**
- * 文件名
- */
- private String filename;
- /**
- * 文件hash
- */
- private String fileHash;
- /**
- * 切片总数
- */
- private Integer totalChunk;
-
- public String getFilename() {
- return filename;
- }
-
- public void setFilename(String filename) {
- this.filename = filename;
- }
-
- public String getFileHash() {
- return fileHash;
- }
-
- public void setFileHash(String fileHash) {
- this.fileHash = fileHash;
- }
-
- public Integer getTotalChunk() {
- return totalChunk;
- }
-
- @Override
- public String toString() {
- return "MergeFile{" +
- "filename='" + filename + '\'' +
- ", fileHash='" + fileHash + '\'' +
- ", totalChunk=" + totalChunk +
- '}';
- }
-
- public void setTotalChunk(Integer totalChunk) {
- this.totalChunk = totalChunk;
- }
-
- public MergeFile() {
- }
-
- public MergeFile(String filename, String fileHash, Integer totalChunk) {
- this.filename = filename;
- this.fileHash = fileHash;
- this.totalChunk = totalChunk;
- }
- }
- package com.cc.fileupload.entity;
-
- import org.springframework.web.bind.annotation.RequestParam;
- import org.springframework.web.multipart.MultipartFile;
-
- /**
- * @author CC
- * @date Created in 2024/2/7 10:33
- */
- public class UploadFile {
- /**
- * 传入的切片文件
- */
- private MultipartFile chunkBody;
- /**
- * 文件hash
- */
- private String hash;
- /**
- * 文件名
- */
- private String filename;
- /**
- * 当前切片的索引号
- */
- private Integer chunkIndex;
-
-
- public MultipartFile getChunkBody() {
- return chunkBody;
- }
-
- public void setChunkBody(MultipartFile chunkBody) {
- this.chunkBody = chunkBody;
- }
-
- public String getHash() {
- return hash;
- }
-
- public void setHash(String hash) {
- this.hash = hash;
- }
-
- public String getFilename() {
- return filename;
- }
-
- public void setFilename(String filename) {
- this.filename = filename;
- }
-
- public Integer getChunkIndex() {
- return chunkIndex;
- }
-
- public void setChunkIndex(Integer chunkIndex) {
- this.chunkIndex = chunkIndex;
- }
-
-
- @Override
- public String toString() {
- return "UploadFile{" +
- "chunkBody=" + chunkBody +
- ", hash='" + hash + '\'' +
- ", filename='" + filename + '\'' +
- ", chunkIndex=" + chunkIndex +
- '}';
- }
- }
- package com.cc.fileupload.util;
-
- /**
- * @author CC
- * @date Created in 2024/2/7 10:49
- */
- public class Helper {
- /**
- * 构建切片文件名
- *
- * @param baseName 基础文件名
- * @param index 文件索引
- * @return 返回切片文件名
- */
- public static String buildChunkName(String baseName, Integer index) {
- int i = baseName.lastIndexOf(".");
- String prefix = baseName.substring(0, i).replaceAll("\\.", "_");
- return prefix + "_part_" + index;
- }
-
- public static <T> ResultFormat<T> getReturnMsg(Integer code, T data, String msg) {
- return new ResultFormat<T>(data, msg, code);
- }
-
- public static <T> ResultFormat<T> getReturnMsg(Integer code, T data) {
- return new ResultFormat<T>(data, code);
- }
-
- public static ResultFormat<String> getReturnMsg(Integer code, String msg) {
- return new ResultFormat<>(msg, code);
- }
- public static ResultFormat<Integer> getReturnMsg(Integer code){
- return new ResultFormat<>(code);
- }
- //
- // public static void main(String[] args) {
- // String s = buildChunkName("test.xx.txt", 1);
- // System.out.println(s);
- // }
- }
- package com.cc.fileupload.util;
-
- /**
- * @author CC
- * @date Created in 2024/2/7 11:46
- */
- public class ResultFormat<T> {
- private T data;
- private String msg;
- private Integer code;
-
- @Override
- public String toString() {
- return "{" +
- "data=" + data +
- ", msg='" + msg + '\'' +
- ", code=" + code +
- '}';
- }
-
- public T getData() {
- return data;
- }
-
- public void setData(T data) {
- this.data = data;
- }
-
- public String getMsg() {
- return msg;
- }
-
- public void setMsg(String msg) {
- this.msg = msg;
- }
-
- public Integer getCode() {
- return code;
- }
-
- public void setCode(Integer code) {
- this.code = code;
- }
-
- public ResultFormat(String msg, Integer code) {
- this.msg = msg;
- this.code = code;
- }
-
- public ResultFormat(Integer code) {
- this.code = code;
- }
-
- public ResultFormat(T data, Integer code) {
- this.data = data;
- this.code = code;
- }
-
- public ResultFormat(T data, String msg, Integer code) {
- this.data = data;
- this.msg = msg;
- this.code = code;
- }
- }
- package com.cc.fileupload.util;
-
- /**
- * @author CC
- * @date Created in 2024/2/7 11:46
- */
- public enum StatusCode {
- UPLOAD_SUCCESS(200),
- NOT_UPLOAD(202),
- ALREADY_UPLOAD(1000),
- UPLOAD_FAILED(1004);
- private java.lang.Integer code;
-
- StatusCode(java.lang.Integer code) {
- this.code = code;
- }
-
- public java.lang.Integer getCode() {
- return code;
- }
-
- public void setCode(java.lang.Integer code) {
- this.code = code;
- }
-
-
- }
- package com.cc.fileupload.service;
-
- import com.cc.fileupload.entity.BaseFile;
- import com.cc.fileupload.entity.MergeFile;
- import com.cc.fileupload.entity.UploadFile;
- import com.cc.fileupload.util.ResultFormat;
-
- import java.io.File;
-
- /**
- * @author CC
- * @date Created in 2024/2/7 10:46
- */
- public interface UploadService {
- /**
- * 上传文件并保存切片的操作
- *
- * @param uploadFile 文件上传实体类
- * @return 返回状态信息
- */
- ResultFormat upload(UploadFile uploadFile);
-
- /**
- * 合并文件切片
- *
- * @param mergeFile 合并文件实体类
- */
- void merge(MergeFile mergeFile);
-
- /**
- * 对文件的切片做删除操作
- * @param mergeFile 合并文件实体类
- */
- void deleteChunks(MergeFile mergeFile);
-
- /**
- *
- * @param baseFile 检查文件是否已经上传
- * @return 返回状态信息
- */
- ResultFormat<Integer> checkHasUpload(BaseFile baseFile);
- }
- package com.cc.fileupload.service.impl;
-
- import com.cc.fileupload.entity.BaseFile;
- import com.cc.fileupload.entity.MergeFile;
- import com.cc.fileupload.entity.UploadFile;
- import com.cc.fileupload.service.UploadService;
- import com.cc.fileupload.util.Helper;
- import com.cc.fileupload.util.ResultFormat;
- import com.cc.fileupload.util.StatusCode;
- import org.springframework.stereotype.Service;
- import org.springframework.web.multipart.MultipartFile;
-
- import java.io.File;
- import java.io.FileOutputStream;
- import java.io.IOException;
- import java.io.OutputStream;
- import java.nio.file.Files;
-
- /**
- * @author CC
- * @date Created in 2024/2/7 10:46
- */
- @Service
- public class IUploadService implements UploadService {
- private static final String BASE_PATH = "D:\\桌面\\图片";
-
- @Override
- public ResultFormat<java.lang.Integer> checkHasUpload(BaseFile mergeFile) {
- String fileHash = mergeFile.getFileHash();
- String filename = mergeFile.getFilename();
- File folder = new File(BASE_PATH, fileHash);
- if (folder.exists()) {
- File file = new File(folder, filename);
- if (file.exists()) {
- return Helper.getReturnMsg(StatusCode.ALREADY_UPLOAD.getCode());
- }
- }
- return Helper.getReturnMsg(StatusCode.NOT_UPLOAD.getCode());
- }
-
- @Override
- public ResultFormat upload(UploadFile uploadFile) {
- String filename = uploadFile.getFilename();
- String hash = uploadFile.getHash();
- java.lang.Integer currentChunkIndex = uploadFile.getChunkIndex();
- MultipartFile chunkBody = uploadFile.getChunkBody();
- //根据hash来创建文件夹,有助于检测是否上传
- File folder = new File(BASE_PATH, hash);
- if (!folder.exists()) {
- folder.mkdirs();
- }
- //这里获取需要写入的文件路径和文件名
- File file1 = new File(folder, Helper.buildChunkName(filename, currentChunkIndex));
- try {
- //文件写入
- chunkBody.transferTo(file1);
- return Helper.getReturnMsg(StatusCode.UPLOAD_SUCCESS.getCode(), "上传成功");
- } catch (IOException e) {
- System.out.println("出现错误");
- e.printStackTrace();
- }
- //对文件进行写入
- return Helper.getReturnMsg(StatusCode.UPLOAD_FAILED.getCode(), "上传失败");
- }
-
- @Override
- public void deleteChunks(MergeFile mergeFile) {
- File hashFolder = new File(BASE_PATH, mergeFile.getFileHash());
- java.lang.Integer totalChunk = mergeFile.getTotalChunk();
- String filename = mergeFile.getFilename();
- for (int i = 0; i < totalChunk; i++) {
- //获取切片
- File tmpChunkFile = new File(hashFolder, Helper.buildChunkName(filename, i));
- tmpChunkFile.delete();
- }
- }
-
- @Override
- public void merge(MergeFile mergeFile) {
- String hash = mergeFile.getFileHash();
- String filename = mergeFile.getFilename();
- java.lang.Integer totalChunk = mergeFile.getTotalChunk();
- //文件hash的Folder
- File hashFolder = new File(BASE_PATH, hash);
- OutputStream os = null;
- //检查是否有该hash目录
- try {
- if (hashFolder.exists()) {
- //指定最后输出的文件名
- os = new FileOutputStream(new File(hashFolder, filename));
- for (int i = 0; i < totalChunk; i++) {
- //获取切片
- File tmpChunkFile = new File(hashFolder, Helper.buildChunkName(filename, i));
- //数据读取并写入缓存区
- byte[] bytes = Files.readAllBytes(tmpChunkFile.toPath());
- //将每一个切片数据读取写入缓存区
- os.write(bytes);
- }
- //在将每一个切片的字节全都写入缓冲区后,最后合并输出文件
- os.flush();
- //输出后清理临时文件
- deleteChunks(mergeFile);
- }
- } catch (IOException e) {
- e.printStackTrace();
- } finally {
- //资源关闭
- if (os != null) {
- try {
- os.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
- }
-
- }
- package com.cc.fileupload.controller;
-
- import com.cc.fileupload.entity.BaseFile;
- import com.cc.fileupload.entity.MergeFile;
- import com.cc.fileupload.entity.UploadFile;
- import com.cc.fileupload.service.UploadService;
- import com.cc.fileupload.util.ResultFormat;
- import org.springframework.web.bind.annotation.*;
-
- import javax.annotation.Resource;
-
- /**
- * @author CC
- * @date Created in 2024/2/7 9:46
- */
- @RestController
- @CrossOrigin
- public class UploadController {
- @Resource
- private UploadService uploadService;
-
- @RequestMapping("/upload")
- public ResultFormat upload(@ModelAttribute UploadFile uploadFile) {
- System.out.println("上传");
- return uploadService.upload(uploadFile);
- }
-
- @RequestMapping("/merge")
- public void merge(@ModelAttribute MergeFile mergeFile) {
- uploadService.merge(mergeFile);
- }
-
- @RequestMapping("/check")
- public ResultFormat check(@ModelAttribute BaseFile file) {
- System.out.println("检查");
- return uploadService.checkHasUpload(file);
- }
- }
后端:https://github.com/wewCc/fileUploadhttps://github.com/wewCc/fileUpload
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。