package com.ruoyi.web.upload.controller;
import com.ruoyi.web.upload.dto.FileChunkDTO;
import com.ruoyi.web.upload.dto.FileChunkResultDTO;
import com.ruoyi.web.upload.result.Result;
import com.ruoyi.web.upload.service.IUploadService;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
@RestController
@RequestMapping("upload")
public class UploaderController {@Resourceprivate IUploadService uploadService;@GetMapping("chunk")public Result checkChunkExist(FileChunkDTO chunkDTO) {FileChunkResultDTO fileChunkCheckDTO;try {fileChunkCheckDTO = uploadService.checkChunkExist(chunkDTO);return Result.ok(fileChunkCheckDTO);} catch (Exception e) {return Result.fail(e.getMessage());}}@PostMapping("chunk")public Result uploadChunk(FileChunkDTO chunkDTO) {try {uploadService.uploadChunk(chunkDTO);return Result.ok(chunkDTO.getIdentifier());} catch (Exception e) {return Result.fail(e.getMessage());}}@PostMapping("merge")public Result mergeChunks(@RequestBody FileChunkDTO chunkDTO) {try {boolean success = uploadService.mergeChunk(chunkDTO.getIdentifier(), chunkDTO.getFilename(), chunkDTO.getTotalChunks());return Result.ok(success);} catch (Exception e) {return Result.fail(e.getMessage());}}
}
package com.ruoyi.web.upload.dto;
import org.springframework.web.multipart.MultipartFile;
public class FileChunkDTO {private String identifier;MultipartFile file;private Integer chunkNumber;private Long chunkSize;private Long currentChunkSize;private Long totalSize;private Integer totalChunks;private String filename;public String getIdentifier() {return identifier;}public void setIdentifier(String identifier) {this.identifier = identifier;}public MultipartFile getFile() {return file;}public void setFile(MultipartFile file) {this.file = file;}public Integer getChunkNumber() {return chunkNumber;}public void setChunkNumber(Integer chunkNumber) {this.chunkNumber = chunkNumber;}public Long getChunkSize() {return chunkSize;}public void setChunkSize(Long chunkSize) {this.chunkSize = chunkSize;}public Long getCurrentChunkSize() {return currentChunkSize;}public void setCurrentChunkSize(Long currentChunkSize) {this.currentChunkSize = currentChunkSize;}public Long getTotalSize() {return totalSize;}public void setTotalSize(Long totalSize) {this.totalSize = totalSize;}public Integer getTotalChunks() {return totalChunks;}public void setTotalChunks(Integer totalChunks) {this.totalChunks = totalChunks;}public String getFilename() {return filename;}public void setFilename(String filename) {this.filename = filename;}@Overridepublic String toString() {return "FileChunkDTO{" +"identifier='" + identifier + '\'' +", file=" + file +", chunkNumber=" + chunkNumber +", chunkSize=" + chunkSize +", currentChunkSize=" + currentChunkSize +", totalSize=" + totalSize +", totalChunks=" + totalChunks +", filename='" + filename + '\'' +'}';}
}
package com.ruoyi.web.upload.dto;
import java.util.Set;
public class FileChunkResultDTO {private Boolean skipUpload;private Set<Integer> uploaded;public Boolean getSkipUpload() {return skipUpload;}public void setSkipUpload(Boolean skipUpload) {this.skipUpload = skipUpload;}public Set<Integer> getUploaded() {return uploaded;}public void setUploaded(Set<Integer> uploaded) {this.uploaded = uploaded;}public FileChunkResultDTO(Boolean skipUpload, Set<Integer> uploaded) {this.skipUpload = skipUpload;this.uploaded = uploaded;}public FileChunkResultDTO(Boolean skipUpload) {this.skipUpload = skipUpload;}
}
package com.ruoyi.web.upload.dto;
import lombok.Getter;
@Getter
@SuppressWarnings("all")
public enum ResultCodeEnum {SUCCESS(200,"成功"),FAIL(201, "失败"),PARAM_ERROR( 202, "参数不正确"),SERVICE_ERROR(203, "服务异常"),DATA_ERROR(204, "数据异常"),DATA_UPDATE_ERROR(205, "数据版本异常"),LOGIN_AUTH(208, "未登陆"),PERMISSION(209, "没有权限"),CODE_ERROR(210, "验证码错误"),LOGIN_MOBLE_ERROR(211, "账号不正确"),LOGIN_DISABLED_ERROR(212, "改用户已被禁用"),REGISTER_MOBLE_ERROR(213, "手机号码格式不正确"),REGISTER_MOBLE_ERROR_NULL(214, "手机号码为空"),LOGIN_AURH(214, "需要登录"),LOGIN_ACL(215, "没有权限"),URL_ENCODE_ERROR( 216, "URL编码失败"),ILLEGAL_CALLBACK_REQUEST_ERROR( 217, "非法回调请求"),FETCH_ACCESSTOKEN_FAILD( 218, "获取accessToken失败"),FETCH_USERINFO_ERROR( 219, "获取用户信息失败");private Integer code;private String message;private ResultCodeEnum(Integer code, String message) {this.code = code;this.message = message;}
}
package com.ruoyi.web.upload.service;
import com.ruoyi.web.upload.dto.FileChunkDTO;
import com.ruoyi.web.upload.dto.FileChunkResultDTO;
import java.io.IOException;
public interface IUploadService {FileChunkResultDTO checkChunkExist(FileChunkDTO chunkDTO);void uploadChunk(FileChunkDTO chunkDTO) throws IOException;boolean mergeChunk(String identifier,String fileName,Integer totalChunks)throws IOException;
}
package com.ruoyi.web.upload.service.impl;
import com.ruoyi.web.upload.dto.FileChunkDTO;
import com.ruoyi.web.upload.dto.FileChunkResultDTO;
import com.ruoyi.web.upload.service.IUploadService;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.io.*;
import java.util.*;
@Service
@SuppressWarnings("all")
public class UploadServiceImpl implements IUploadService {private Logger logger = LoggerFactory.getLogger(UploadServiceImpl.class);@Autowiredprivate RedisTemplate redisTemplate;@Value("${ruoyi.profile}")private String uploadFolder;@Overridepublic FileChunkResultDTO checkChunkExist(FileChunkDTO chunkDTO) {String fileFolderPath = getFileFolderPath(chunkDTO.getIdentifier());logger.info("fileFolderPath-->{}", fileFolderPath);String filePath = getFilePath(chunkDTO.getIdentifier(), chunkDTO.getFilename());File file = new File(filePath);boolean exists = file.exists();Set<Integer> uploaded = (Set<Integer>) redisTemplate.opsForHash().get(chunkDTO.getIdentifier(), "uploaded");if (uploaded != null && uploaded.size() == chunkDTO.getTotalChunks() && exists) {return new FileChunkResultDTO(true);}File fileFolder = new File(fileFolderPath);if (!fileFolder.exists()) {boolean mkdirs = fileFolder.mkdirs();logger.info("准备工作,创建文件夹,fileFolderPath:{},mkdirs:{}", fileFolderPath, mkdirs);}return new FileChunkResultDTO(false, uploaded);}@Overridepublic void uploadChunk(FileChunkDTO chunkDTO) {String chunkFileFolderPath = getChunkFileFolderPath(chunkDTO.getIdentifier());logger.info("分块的目录 -> {}", chunkFileFolderPath);File chunkFileFolder = new File(chunkFileFolderPath);if (!chunkFileFolder.exists()) {boolean mkdirs = chunkFileFolder.mkdirs();logger.info("创建分片文件夹:{}", mkdirs);}try (InputStream inputStream = chunkDTO.getFile().getInputStream();FileOutputStream outputStream = new FileOutputStream(new File(chunkFileFolderPath + chunkDTO.getChunkNumber()))) {IOUtils.copy(inputStream, outputStream);logger.info("文件标识:{},chunkNumber:{}", chunkDTO.getIdentifier(), chunkDTO.getChunkNumber());long size = saveToRedis(chunkDTO);} catch (Exception e) {e.printStackTrace();}}@Overridepublic boolean mergeChunk(String identifier, String fileName, Integer totalChunks) throws IOException {return mergeChunks(identifier, fileName, totalChunks);}private boolean mergeChunks(String identifier, String filename, Integer totalChunks) {String chunkFileFolderPath = getChunkFileFolderPath(identifier);String filePath = getFilePath(identifier, filename);if (checkChunks(chunkFileFolderPath, totalChunks)) {File chunkFileFolder = new File(chunkFileFolderPath);File mergeFile = new File(filePath);File[] chunks = chunkFileFolder.listFiles();List fileList = Arrays.asList(chunks);Collections.sort(fileList, (Comparator<File>) (o1, o2) -> {return Integer.parseInt(o1.getName()) - (Integer.parseInt(o2.getName()));});try {RandomAccessFile randomAccessFileWriter = new RandomAccessFile(mergeFile, "rw");byte[] bytes = new byte[1024];for (File chunk : chunks) {RandomAccessFile randomAccessFileReader = new RandomAccessFile(chunk, "r");int len;while ((len = randomAccessFileReader.read(bytes)) != -1) {randomAccessFileWriter.write(bytes, 0, len);}randomAccessFileReader.close();}randomAccessFileWriter.close();} catch (Exception e) {return false;}return true;}return false;}private boolean checkChunks(String chunkFileFolderPath, Integer totalChunks) {try {for (int i = 1; i <= totalChunks + 1; i++) {File file = new File(chunkFileFolderPath + File.separator + i);if (file.exists()) {continue;} else {return false;}}} catch (Exception e) {return false;}return true;}private synchronized long saveToRedis(FileChunkDTO chunkDTO) {Set<Integer> uploaded = (Set<Integer>) redisTemplate.opsForHash().get(chunkDTO.getIdentifier(), "uploaded");if (uploaded == null) {uploaded = new HashSet<>(Arrays.asList(chunkDTO.getChunkNumber()));HashMap<String, Object> objectObjectHashMap = new HashMap<>();objectObjectHashMap.put("uploaded", uploaded);objectObjectHashMap.put("totalChunks", chunkDTO.getTotalChunks());objectObjectHashMap.put("totalSize", chunkDTO.getTotalSize());
objectObjectHashMap.put("path", chunkDTO.getFilename());redisTemplate.opsForHash().putAll(chunkDTO.getIdentifier(), objectObjectHashMap);} else {uploaded.add(chunkDTO.getChunkNumber());redisTemplate.opsForHash().put(chunkDTO.getIdentifier(), "uploaded", uploaded);}return uploaded.size();}private String getFilePath(String identifier, String filename) {String ext = filename.substring(filename.lastIndexOf("."));
return uploadFolder + filename;}private String getFileRelativelyPath(String identifier, String filename) {String ext = filename.substring(filename.lastIndexOf("."));return "/" + identifier.substring(0, 1) + "/" +identifier.substring(1, 2) + "/" +identifier + "/" + identifier+ ext;}private String getChunkFileFolderPath(String identifier) {return getFileFolderPath(identifier) + "chunks" + File.separator;}private String getFileFolderPath(String identifier) {return uploadFolder + identifier.substring(0, 1) + File.separator +identifier.substring(1, 2) + File.separator +identifier + File.separator;
}
}
package com.ruoyi.web.upload.result;
import com.ruoyi.web.upload.dto.ResultCodeEnum;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel(value = "全局统一返回结果")
@SuppressWarnings("all")
public class Result<T> {@ApiModelProperty(value = "返回码")private Integer code;@ApiModelProperty(value = "返回消息")private String message;@ApiModelProperty(value = "返回数据")private T data;private Long total;public Result(){}protected static <T> Result<T> build(T data) {Result<T> result = new Result<T>();if (data != null)result.setData(data);return result;}public static <T> Result<T> build(T body, ResultCodeEnum resultCodeEnum) {Result<T> result = build(body);result.setCode(resultCodeEnum.getCode());result.setMessage(resultCodeEnum.getMessage());return result;}public static <T> Result<T> build(Integer code, String message) {Result<T> result = build(null);result.setCode(code);result.setMessage(message);return result;}public static<T> Result<T> ok(){return Result.ok(null);}public static<T> Result<T> ok(T data){Result<T> result = build(data);return build(data, ResultCodeEnum.SUCCESS);}public static<T> Result<T> fail(){return Result.fail(null);}public static<T> Result<T> fail(T data){Result<T> result = build(data);return build(data, ResultCodeEnum.FAIL);}public Result<T> message(String msg){this.setMessage(msg);return this;}public Result<T> code(Integer code){this.setCode(code);return this;}public boolean isOk() {if(this.getCode().intValue() == ResultCodeEnum.SUCCESS.getCode().intValue()) {return true;}return false;}
}
import uploader from 'vue-simple-uploader'
Vue.use(uploader)
npm install --save vue-simple-uploader
npm install --save spark-md5
<template><div><uploader:autoStart="false":options="options":file-status-text="statusText"class="uploader-example"@file-complete="fileComplete"@complete="complete"@file-success="fileSuccess"@files-added="filesAdded"><uploader-unsupport></uploader-unsupport><uploader-drop><p>将文件拖放到此处以上传</p><uploader-btn>选择文件</uploader-btn><uploader-btn :attrs="attrs">选择图片</uploader-btn><uploader-btn :directory="true">选择文件夹</uploader-btn></uploader-drop><!-- <uploader-list></uploader-list> --><uploader-files> </uploader-files></uploader><br /><el-button @click="allStart()" :disabled="disabled">全部开始</el-button><el-button @click="allStop()" style="margin-left: 4px">全部暂停</el-button><el-button @click="allRemove()" style="margin-left: 4px">全部移除</el-button></div>
</template><script>
import axios from "axios";
import SparkMD5 from "spark-md5";
import {upload} from "@/api/user";
export default {name: "Home",data() {return {skip: false,options: {target: "//localhost:9999/upload/chunk",testChunks: true,parseTimeRemaining: function (timeRemaining, parsedTimeRemaining) {return parsedTimeRemaining.replace(/\syears?/, "年").replace(/\days?/, "天").replace(/\shours?/, "小时").replace(/\sminutes?/, "分钟").replace(/\sseconds?/, "秒");},checkChunkUploadedByResponse: (chunk, message) => {const result = JSON.parse(message);if (result.data.skipUpload) {this.skip = true;return true;}return (result.data.uploaded || []).indexOf(chunk.offset + 1) >= 0;},},attrs: {accept: "image/*",},statusText: {success: "上传成功",error: "上传出错了",uploading: "上传中...",paused: "暂停中...",waiting: "等待中...",cmd5: "计算文件MD5中...",},fileList: [],disabled: true,};},watch: {fileList(o, n) {this.disabled = false;},},methods: {fileSuccess(rootFile, file, response, chunk) {const result = JSON.parse(response);console.log(result.success, this.skip);const user = {identifier: file.uniqueIdentifier,filename: file.name,totalChunks: chunk.offset,}if (result.success && !this.skip) {upload(user).then((res) => {if (res.code == 200) {console.log("上传成功");} else {console.log(res);}}).catch(function (error) {console.log(error);});} else {console.log("上传成功,不需要合并");}if (this.skip) {this.skip = false;}},fileComplete(rootFile) {},complete() {},filesAdded(file, fileList, event) {file.forEach((e) => {this.fileList.push(e);this.computeMD5(e);});},computeMD5(file) {let fileReader = new FileReader();let time = new Date().getTime();let blobSlice =File.prototype.slice ||File.prototype.mozSlice ||File.prototype.webkitSlice;let currentChunk = 0;const chunkSize = 1024 * 1024;let chunks = Math.ceil(file.size / chunkSize);let spark = new SparkMD5.ArrayBuffer();file.cmd5 = true; file.pause();loadNext();fileReader.onload = (e) => {spark.append(e.target.result);if (currentChunk < chunks) {currentChunk++;loadNext();console.log(`第${currentChunk}分片解析完成, 开始第${currentChunk + 1} / ${chunks}分片解析`);} else {let md5 = spark.end();console.log(`MD5计算完毕:${file.name} \nMD5:${md5} \n分片:${chunks} 大小:${file.size} 用时:${new Date().getTime() - time} ms`);spark.destroy(); file.uniqueIdentifier = md5; file.cmd5 = false; file.resume(); }};fileReader.onerror = function () {this.error(`文件${file.name}读取出错,请检查该文件`);file.cancel();};function loadNext() {let start = currentChunk * chunkSize;let end =start + chunkSize >= file.size ? file.size : start + chunkSize;fileReader.readAsArrayBuffer(blobSlice.call(file.file, start, end));}},allStart() {console.log(this.fileList);this.fileList.map((e) => {if (e.paused) {e.resume();}});},allStop() {console.log(this.fileList);this.fileList.map((e) => {if (!e.paused) {e.pause();}});},allRemove() {this.fileList.map((e) => {e.cancel();});this.fileList = [];},},
};
</script><style>
.uploader-example {width: 100%;padding: 15px;margin: 0px auto 0;font-size: 12px;box-shadow: 0 0 10px rgba(0, 0, 0, 0.4);
}
.uploader-example .uploader-btn {margin-right: 4px;
}
.uploader-example .uploader-list {max-height: 440px;overflow: auto;overflow-x: hidden;overflow-y: auto;
}
</style>