第一步 作MD5檢查,若是服務端已經有相同的MD5值文件,則直接提示上傳成功;若是已經上傳一部分了則直接返回尚未上傳部分的列表。javascript
第二步 提交分片,服務端按照分片文件向文件中按照offset位置寫入。css
@RestController @RequestMapping(value = "/file") public class UploadController { private Logger logger = LoggerFactory.getLogger(UploadController.class); // @Autowired // private StringRedisTemplate stringRedisTemplate; @Autowired private StorageServiceImpl storageService; /** * 秒傳判斷,斷點判斷 * * @return */ @RequestMapping(value = "checkFileMd5", method = RequestMethod.POST) @ResponseBody public Object checkFileMd5(String md5) throws IOException { Map<String, HashMap<String, Object>> mapState = StorageServiceImpl.mapState; HashMap<String, Object> hMap = mapState.get(Constants.FILE_UPLOAD_STATUS); Object processingObj = null; if (hMap != null) { processingObj = hMap.get(md5); } if (processingObj == null) { return new ResultVo(ResultStatus.NO_HAVE); } String processingStr = processingObj.toString(); boolean processing = Boolean.parseBoolean(processingStr); // String value = stringRedisTemplate.opsForValue().get(Constants.FILE_MD5_KEY + md5); String value = (String) StorageServiceImpl.mapMD5.get(Constants.FILE_MD5_KEY + md5); if (processing) { return new ResultVo(ResultStatus.IS_HAVE, value); } else { File confFile = new File(value); byte[] completeList = FileUtils.readFileToByteArray(confFile); List<String> missChunkList = new LinkedList<>(); for (int i = 0; i < completeList.length; i++) { if (completeList[i] != Byte.MAX_VALUE) { missChunkList.add(i + ""); } } return new ResultVo<>(ResultStatus.ING_HAVE, missChunkList); } } /** * 上傳文件 * * @param param * @param request * @return * @throws Exception */ @RequestMapping(value = "/upload", method = RequestMethod.POST) @ResponseBody public ResultJson fileUpload(MultipartFileParam param, HttpServletRequest request) { boolean isMultipart = ServletFileUpload.isMultipartContent(request); if (isMultipart) { logger.info("上傳文件start。"); try { String uri = storageService.uploadFileByMappedByteBuffer(param); return ResultJson.ok(uri); } catch (IOException e) { e.printStackTrace(); logger.error("文件上傳失敗。{}", param.toString()); } logger.info("上傳文件end。"); } return ResultJson.ok(""); } }
public class MultipartFileParam { // 用戶id private String uid; //任務ID private String id; //總分片數量 private int chunks; //當前爲第幾塊分片 private int chunk; //當前分片大小 private long size = 0L; //文件名 private String name; //分片對象 private MultipartFile file; // MD5 private String md5; public String getUid() { return uid; } public void setUid(String uid) { this.uid = uid; } public String getId() { return id; } public void setId(String id) { this.id = id; } public int getChunks() { return chunks; } public void setChunks(int chunks) { this.chunks = chunks; } public int getChunk() { return chunk; } public void setChunk(int chunk) { this.chunk = chunk; } public long getSize() { return size; } public void setSize(long size) { this.size = size; } public String getName() { return name; } public void setName(String name) { this.name = name; } public MultipartFile getFile() { return file; } public void setFile(MultipartFile file) { this.file = file; } public String getMd5() { return md5; } public void setMd5(String md5) { this.md5 = md5; } @Override public String toString() { return "MultipartFileParam{" + "uid='" + uid + '\'' + ", id='" + id + '\'' + ", chunks=" + chunks + ", chunk=" + chunk + ", size=" + size + ", name='" + name + '\'' + ", file=" + file + ", md5='" + md5 + '\'' + '}'; } }
@Service public class StorageServiceImpl { public static final Map<String, HashMap<String, Object>> mapState = new ConcurrentHashMap(); public static final Map<String, Object> mapMD5 = new ConcurrentHashMap(); private final Logger logger = LoggerFactory.getLogger(StorageServiceImpl.class); public static String path_ = null; // 保存文件的根目錄 private static Path rootPaht; static { path_ = FileManageImpl.class.getClassLoader().getResource("").getPath().replace("WEB-INF/classes/", "files/big/"); File m = new File(path_); m.mkdirs(); path_ = path_.substring(1); rootPaht = Paths.get(path_); } //這個必須與前端設定的值一致 private long CHUNK_SIZE = 5242880l; private String finalDirPath = path_; public void deleteAll() { logger.info("開發初始化清理數據,start"); mapState.remove(Constants.FILE_UPLOAD_STATUS); mapState.remove(Constants.FILE_MD5_KEY); logger.info("開發初始化清理數據,end"); } public void init() { try { Files.createDirectory(rootPaht); mapState.put(Constants.FILE_UPLOAD_STATUS, new HashMap<>()); mapState.put(Constants.FILE_MD5_KEY, new HashMap<>()); } catch (FileAlreadyExistsException e) { logger.error("文件夾已經存在了,不用再建立。"); } catch (IOException e) { logger.error("初始化root文件夾失敗。", e); } } public String uploadFileByMappedByteBuffer(MultipartFileParam param) throws IOException { String fileName = param.getName(); String md5Val = param.getMd5(); String uploadDirPath = finalDirPath + md5Val; String tempFileName = fileName + "_tmp"; File tmpDir = new File(uploadDirPath); File tmpFile = new File(uploadDirPath, tempFileName); if (!tmpDir.exists()) { tmpDir.mkdirs(); } RandomAccessFile tempRaf = new RandomAccessFile(tmpFile, "rw"); FileChannel fileChannel = tempRaf.getChannel(); //寫入該分片數據 long offset = CHUNK_SIZE * param.getChunk(); byte[] fileData = param.getFile().getBytes(); MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, offset, fileData.length); mappedByteBuffer.put(fileData); // 釋放 FileMD5Util.freedMappedByteBuffer(mappedByteBuffer); fileChannel.close(); boolean isOk = checkAndSetUploadProgress(param, uploadDirPath); if (isOk) { boolean flag = renameFile(tmpFile, fileName); System.out.println("upload complete !!" + flag + " name=" + fileName); return "/files/big/" + md5Val + "/" + fileName; } return null; } /** * 檢查並修改文件上傳進度 * * @param param * @param uploadDirPath * @return * @throws IOException */ private boolean checkAndSetUploadProgress(MultipartFileParam param, String uploadDirPath) throws IOException { String fileName = param.getName(); File confFile = new File(uploadDirPath, fileName + ".conf"); RandomAccessFile accessConfFile = new RandomAccessFile(confFile, "rw"); accessConfFile.setLength(param.getChunks()); accessConfFile.seek(param.getChunk()); accessConfFile.write(Byte.MAX_VALUE); byte[] completeList = FileUtils.readFileToByteArray(confFile); byte isComplete = Byte.MAX_VALUE; for (int i = 0; i < completeList.length && isComplete == Byte.MAX_VALUE; i++) { isComplete = (byte) (isComplete & completeList[i]); System.out.println("shard-id" + completeList[i]); } accessConfFile.close(); if (isComplete == Byte.MAX_VALUE) { HashMap<String, Object> md5 = mapState.get(Constants.FILE_UPLOAD_STATUS); if (md5 == null) { md5 = new HashMap<>(); mapState.put(Constants.FILE_UPLOAD_STATUS, md5); } md5.put(param.getMd5(), "true"); mapMD5.put(Constants.FILE_MD5_KEY + param.getMd5(), "/files/big/" + param.getMd5() + "/" + fileName); return true; } else { HashMap<String, Object> md5 = mapState.get(Constants.FILE_UPLOAD_STATUS); if (md5 == null) { md5 = new HashMap<>(); mapState.put(Constants.FILE_UPLOAD_STATUS, md5); } md5.put(param.getMd5(), "false"); mapMD5.put(Constants.FILE_MD5_KEY + param.getMd5(), path_ + param.getMd5() + "/" + fileName + ".conf"); return false; } } /** * 文件重命名 * * @param toBeRenamed 將要修更名字的文件 * @param toFileNewName 新的名字 * @return */ public boolean renameFile(File toBeRenamed, String toFileNewName) { //檢查要重命名的文件是否存在,是不是文件 if (!toBeRenamed.exists() || toBeRenamed.isDirectory()) { logger.info("File does not exist: " + toBeRenamed.getName()); return false; } String p = toBeRenamed.getParent(); File newFile = new File(p + File.separatorChar + toFileNewName); //修改文件名 return toBeRenamed.renameTo(newFile); } }
@JsonFormat(shape = JsonFormat.Shape.OBJECT) public enum ResultStatus { /** * 1 開頭爲判斷文件在系統的狀態 */ IS_HAVE(100, "文件已存在!"), NO_HAVE(101, "該文件沒有上傳過。"), ING_HAVE(102, "該文件上傳了一部分。"); private final int value; private final String reasonPhrase; ResultStatus(int value, String reasonPhrase) { this.value = value; this.reasonPhrase = reasonPhrase; } public int getValue() { return value; } public String getReasonPhrase() { return reasonPhrase; } }
public class ResultVo<T> { private ResultStatus status; private String msg; private T data; public ResultVo(ResultStatus status) { this(status, status.getReasonPhrase(), null); } public ResultVo(ResultStatus status, T data) { this(status, status.getReasonPhrase(), data); } public ResultVo(ResultStatus status, String msg, T data) { this.status = status; this.msg = msg; this.data = data; } public ResultStatus getStatus() { return status; } public void setStatus(ResultStatus status) { this.status = status; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } public T getData() { return data; } public void setData(T data) { this.data = data; } @Override public String toString() { return "ResultVo{" + "status=" + status + ", msg='" + msg + '\'' + ", data=" + data + '}'; } }
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>斷點上傳</title> <link rel="stylesheet" type="text/css" href="css/webuploader.css"> <link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> </head> <body> <div id="uploader" class="wu-example"> <!--用來存放文件信息--> <div id="thelist" class="uploader-list"></div> <div class="btns"> <div id="picker">選擇大文件</div> <button id="ctlBtn" class="btn btn-default">開始上傳</button> </div> </div> <!--引入JS--> <script src="http://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script> <script type="text/javascript" src="js/webuploader.min.js"></script> <script> var $btn = $('#ctlBtn'); var $thelist = $('#thelist'); var chunkSize = 5 * 1024 * 1024; // HOOK 這個必需要再uploader實例化前面 WebUploader.Uploader.register({ 'before-send-file': 'beforeSendFile', 'before-send': 'beforeSend' }, { beforeSendFile: function (file) { console.log("beforeSendFile"); // Deferred對象在鉤子回掉函數中常常要用到,用來處理須要等待的異步操做。 var task = new $.Deferred(); // 根據文件內容來查詢MD5 uploader.md5File(file).progress(function (percentage) { // 及時顯示進度 console.log('計算md5進度:', percentage); getProgressBar(file, percentage, "MD5", "MD5"); }).then(function (val) { // 完成 console.log('md5 result:', val); file.md5 = val; // 模擬用戶id // file.uid = new Date().getTime() + "_" + Math.random() * 100; file.uid = WebUploader.Base.guid(); // 進行md5判斷 $.post("file/checkFileMd5", {uid: file.uid, md5: file.md5}, function (data) { console.log(data.status); var status = data.status.value; task.resolve(); if (status == 101) { // 文件不存在,那就正常流程 } else if (status == 100) { // 忽略上傳過程,直接標識上傳成功; uploader.skipFile(file); file.pass = true; } else if (status == 102) { // 部分已經上傳到服務器了,可是差幾個模塊。 file.missChunks = data.data; } }); }); return $.when(task); }, beforeSend: function (block) { console.log("block") var task = new $.Deferred(); var file = block.file; var missChunks = file.missChunks; var blockChunk = block.chunk; console.log("當前分塊:" + blockChunk); console.log("missChunks:" + missChunks); if (missChunks !== null && missChunks !== undefined && missChunks !== '') { var flag = true; for (var i = 0; i < missChunks.length; i++) { if (blockChunk == missChunks[i]) { console.log(file.name + ":" + blockChunk + ":還沒上傳,如今上傳去吧。"); flag = false; break; } } if (flag) { task.reject(); } else { task.resolve(); } } else { task.resolve(); } return $.when(task); } }); // 實例化 var uploader = WebUploader.create({ pick: { id: '#picker', label: '點擊選擇文件' }, formData: { uid: 0, md5: '', chunkSize: chunkSize }, //dnd: '#dndArea', //paste: '#uploader', swf: 'js/Uploader.swf', chunked: true, chunkSize: chunkSize, // 字節 1M分塊 threads: 3, server: 'file/upload', auto: false, // 禁掉全局的拖拽功能。這樣不會出現圖片拖進頁面的時候,把圖片打開。 disableGlobalDnd: true, fileNumLimit: 1024, fileSizeLimit: 1024 * 1024 * 1024, // 200 M fileSingleSizeLimit: 1024 * 1024 * 1024 // 50 M }); // 當有文件被添加進隊列的時候 uploader.on('fileQueued', function (file) { console.log("fileQueued"); $thelist.append('<div id="' + file.id + '" class="item">' + '<h4 class="info">' + file.name + '</h4>' + '<p class="state">等待上傳...</p>' + '</div>'); }); //當某個文件的分塊在發送前觸發,主要用來詢問是否要添加附帶參數,大文件在開起分片上傳的前提下此事件可能會觸發屢次。 uploader.onUploadBeforeSend = function (obj, data) { console.log("onUploadBeforeSend"); var file = obj.file; data.md5 = file.md5 || ''; data.uid = file.uid; }; // 上傳中 uploader.on('uploadProgress', function (file, percentage) { getProgressBar(file, percentage, "FILE", "上傳進度"); }); // 上傳返回結果 uploader.on('uploadSuccess', function (file) { var text = '已上傳'; if (file.pass) { text = "文件妙傳功能,文件已上傳。" } $('#' + file.id).find('p.state').text(text); }); uploader.on('uploadError', function (file) { $('#' + file.id).find('p.state').text('上傳出錯'); }); uploader.on('uploadComplete', function (file) { // 隱藏進度條 // fadeOutProgress(file, 'MD5'); // fadeOutProgress(file, 'FILE'); }); // 文件上傳 $btn.on('click', function () { console.log("上傳..."); uploader.upload(); console.log("上傳成功"); }); /** * 生成進度條封裝方法 * @param file 文件 * @param percentage 進度值 * @param id_Prefix id前綴 * @param titleName 標題名 */ function getProgressBar(file, percentage, id_Prefix, titleName) { var $li = $('#' + file.id), $percent = $li.find('#' + id_Prefix + '-progress-bar'); // 避免重複建立 if (!$percent.length) { $percent = $('<div id="' + id_Prefix + '-progress" class="progress progress-striped active">' + '<div id="' + id_Prefix + '-progress-bar" class="progress-bar" role="progressbar" style="width: 0%">' + '</div>' + '</div>' ).appendTo($li).find('#' + id_Prefix + '-progress-bar'); } var progressPercentage = percentage * 100 + '%'; $percent.css('width', progressPercentage); $percent.html(titleName + ':' + progressPercentage); } /** * 隱藏進度條 * @param file 文件對象 * @param id_Prefix id前綴 */ function fadeOutProgress(file, id_Prefix) { $('#' + file.id).find('#' + id_Prefix + '-progress').fadeOut(); } </script> </body> </html>