本部落格將介紹如何進行檔案的分塊上傳。如果讀者還想瞭解檔案的“分塊”下載相關內容可以去參考部落格《Java 伺服器端支援斷點續傳的原始碼【支援快車、迅雷】》。
本文側重介紹伺服器端,用戶端端請參考本篇部落格的姊妹篇《Java 檔案分塊上傳用戶端原始碼》,關於分塊上傳的思想及其流程,已在該部落格中進行了詳細說明,這裡不再贅述。
直接上代碼。接收用戶端 HTTP 分塊上傳請求的 Spring MVC 控制器原始碼如下:
@Controllerpublic class UploadController extends BaseController {private static final Log log = LogFactory.getLog(UploadController.class);private UploadService uploadService;private AuthService authService;/** * 大檔案分成小檔案塊上傳,一次傳遞一塊,最後一塊上傳成功後,將合并所有已經上傳的塊,儲存到File Server * 上相應的位置,並返回已經成功上傳的檔案的詳細屬性. 當最後一塊上傳完畢,返回上傳成功的資訊。此時用getFileList查詢該檔案, * 該檔案的uploadStatus為2。client請自行處理該狀態下檔案如何顯示。(for UPS Server) * */@RequestMapping("/core/v1/file/upload")@ResponseBodypublic Object upload(HttpServletResponse response,@RequestParam(value = "client_id", required = false) String appkey,@RequestParam(value = "sig", required = false) String appsig,@RequestParam(value = "token", required = false) String token,@RequestParam(value = "uuid", required = false) String uuid,@RequestParam(value = "block", required = false) String blockIndex,@RequestParam(value = "file", required = false) MultipartFile multipartFile,@RequestParam Map<String, String> parameters) {checkEmpty(appkey, BaseException.ERROR_CODE_16002);checkEmpty(token, BaseException.ERROR_CODE_16007);checkEmpty(uuid, BaseException.ERROR_CODE_20016);checkEmpty(blockIndex, BaseException.ERROR_CODE_20006);checkEmpty(appsig, BaseException.ERROR_CODE_10010);if (multipartFile == null) {throw new BaseException(BaseException.ERROR_CODE_20020);// 上傳檔案不存在}Long uuidL = parseLong(uuid, BaseException.ERROR_CODE_20016);Integer blockIndexI = parseInt(blockIndex, BaseException.ERROR_CODE_20006);Map<String, Object> appMap = getAuthService().validateSigature(parameters);AccessToken accessToken = CasUtil.checkAccessToken(token, appMap);Long uid = accessToken.getUid();String bucketUrl = accessToken.getBucketUrl();// 從上傳目錄拷貝檔案到工作目錄String fileAbsulutePath = null;try {fileAbsulutePath = this.copyFile(multipartFile.getInputStream(), multipartFile.getOriginalFilename());} catch (IOException ioe) {log.error(ioe.getMessage(), ioe);throw new BaseException(BaseException.ERROR_CODE_20020);// 上傳檔案不存在}File uploadedFile = new File(Global.UPLOAD_TEMP_DIR + fileAbsulutePath);checkEmptyFile(uploadedFile);// file 非空驗證Object rs = uploadService.upload(uuidL, blockIndexI, uid, uploadedFile, bucketUrl);setHttpStatusOk(response);return rs;}// TODO 查看下這裡是否有問題// 上傳檔案非空驗證private void checkEmptyFile(File file) {if (file == null || file.getAbsolutePath() == null) {throw new BaseException(BaseException.ERROR_CODE_20020);// 上傳檔案不存在}}/** * 寫檔案到本地檔案夾 * * @throws IOException * 返回產生的檔案名稱 */private String copyFile(InputStream inputStream, String fileName) {OutputStream outputStream = null;String tempFileName = null;int pointPosition = fileName.lastIndexOf(".");if (pointPosition < 0) {// myvediotempFileName = UUID.randomUUID().toString();// 94d1d2e0-9aad-4dd8-a0f6-494b0099ff26} else {// myvedio.flvtempFileName = UUID.randomUUID() + fileName.substring(pointPosition);// 94d1d2e0-9aad-4dd8-a0f6-494b0099ff26.flv}try {outputStream = new FileOutputStream(Global.UPLOAD_TEMP_DIR + tempFileName);int readBytes = 0;byte[] buffer = new byte[10000];while ((readBytes = inputStream.read(buffer, 0, 10000)) != -1) {outputStream.write(buffer, 0, readBytes);}return tempFileName;} catch (IOException ioe) {// log.error(ioe.getMessage(), ioe);throw new BaseException(BaseException.ERROR_CODE_20020);// 上傳檔案不存在} finally {if (outputStream != null) {try {outputStream.close();} catch (IOException e) {}}if (inputStream != null) {try {inputStream.close();} catch (IOException e) {}}}}/** * 測試此服務是否可用 * * @param response * @return * @author zwq7978 */@RequestMapping("/core/v1/file/testServer")@ResponseBodypublic Object testServer(HttpServletResponse response) {setHttpStatusOk(response);return Global.SUCCESS_RESPONSE;}public UploadService getUploadService() {return uploadService;}public void setUploadService(UploadService uploadService) {this.uploadService = uploadService;}public void setAuthService(AuthService authService) {this.authService = authService;}public AuthService getAuthService() {return authService;}}
比如要上傳的檔案是 test450k.mp4。對照《Java 檔案分塊上傳用戶端原始碼》中分塊上傳伺服器對區塊檔案參數定義的名字"file",upload 方法裡使用的是 MultipartFile 接收該對象。對於每次的 HTTP 要求,使用 copyFile 方法將檔案流輸出到伺服器本地的一個臨時檔案夾裡,比如作者的是 D:/defonds/syncPath/uploadTemp,該檔案下會有
50127019-b63b-4a54-8f53-14efd1e58ada.mp4 臨時檔案產生用於儲存上傳檔案流。
分塊依次上傳。當所有塊都上傳完畢之後,將這些臨時檔案都轉移到伺服器指定目錄中,比如作者的這個目錄是 D:/defonds/syncPath/file,在該檔案夾下會有/1/temp_dir_5_1 目錄產生,而 uploadTemp 的臨時檔案則被挨個轉移到這個檔案夾下,產生形如 5.part0001 的檔案。以下是檔案轉移的原始碼:
/** * 把所有塊從臨時檔案目錄移到指定本地目錄或S2/S3 * * @param preUpload */private void moveBlockFiles(BlockPreuploadFileInfo preUpload) {@SuppressWarnings("unchecked")String[] s3BlockUrl=new String[preUpload.getBlockNumber()];String[] localBlockUrl=new String[preUpload.getBlockNumber()];//本地的塊檔案路徑 以便以後刪除List<BlockUploadInfo> blocks = (List<BlockUploadInfo>) getBaseDao().queryForList("upload.getBlockUploadFileByUuid", preUpload.getUuid());String tempDirName = SyncUtil.getTempDirName(preUpload.getUuid(), preUpload.getUid());String parentPath = Global.UPLOAD_ABSOLUTE_PAHT_ + Global.PATH_SEPARATIVE_SIGN+ String.valueOf(preUpload.getUid());String dirPath = parentPath + Global.PATH_SEPARATIVE_SIGN + tempDirName;new File(dirPath).mkdirs();//建立存放塊檔案的檔案夾 (本地)int j=0;for (BlockUploadInfo info : blocks) {try {String strBlockIndex = createStrBlockIndex(info.getBlockIndex());String suffixPath = preUpload.getUuid() + ".part" + strBlockIndex;String tempFilePath = info.getTempFile();File tempFile = new File(tempFilePath);File tmpFile = new File(dirPath + suffixPath);if (tmpFile.exists()) {FileUtils.deleteQuietly(tmpFile);}FileUtils.moveFile(tempFile, tmpFile);localBlockUrl[j]=dirPath + suffixPath;j++;info.setStatus(Global.MOVED_TO_NEWDIR);getBaseDao().update("upload.updateBlockUpload", info);if (log.isInfoEnabled())log.info(preUpload.getUuid() + " " + info.getBuId() + " moveBlockFiles");} catch (IOException e) {log.error(e.getMessage(), e);throw new BaseException("file not found");}}preUpload.setLocalBlockUrl(localBlockUrl);preUpload.setDirPath(dirPath);preUpload.setStatus(Global.MOVED_TO_NEWDIR);getBaseDao().update("upload.updatePreUploadInfo", preUpload);}private String createStrBlockIndex(int blockIndex) {String strBlockIndex;if (blockIndex < 10) {strBlockIndex = "000" + blockIndex;} else if (10 <= blockIndex && blockIndex < 100) {strBlockIndex = "00" + blockIndex;} else if (100 <= blockIndex && blockIndex < 1000) {strBlockIndex = "0" + blockIndex;} else {strBlockIndex = "" + blockIndex;}return strBlockIndex;}
最後是檔案的組裝原始碼:
/** * 組裝檔案 * */private void assembleFileWithBlock(BlockPreuploadFileInfo preUpload) {String dirPath = preUpload.getDirPath();// 開始在指定目錄組裝檔案String uploadedUrl = null;String[] separatedFiles;String[][] separatedFilesAndSize;int fileNum = 0;File file = new File(dirPath);separatedFiles = file.list();separatedFilesAndSize = new String[separatedFiles.length][2];Arrays.sort(separatedFiles);fileNum = separatedFiles.length;for (int i = 0; i < fileNum; i++) {separatedFilesAndSize[i][0] = separatedFiles[i];String fileName = dirPath + separatedFiles[i];File tmpFile = new File(fileName);long fileSize = tmpFile.length();separatedFilesAndSize[i][1] = String.valueOf(fileSize);}RandomAccessFile fileReader = null;RandomAccessFile fileWrite = null;long alreadyWrite = 0;int len = 0;byte[] buf = new byte[1024];try {uploadedUrl = Global.UPLOAD_ABSOLUTE_PAHT_ + Global.PATH_SEPARATIVE_SIGN + preUpload.getUid() + Global.PATH_SEPARATIVE_SIGN + preUpload.getUuid();fileWrite = new RandomAccessFile(uploadedUrl, "rw");for (int i = 0; i < fileNum; i++) {fileWrite.seek(alreadyWrite);// 讀取fileReader = new RandomAccessFile((dirPath + separatedFilesAndSize[i][0]), "r");// 寫入while ((len = fileReader.read(buf)) != -1) {fileWrite.write(buf, 0, len);}fileReader.close();alreadyWrite += Long.parseLong(separatedFilesAndSize[i][1]);}fileWrite.close();preUpload.setStatus(Global.ASSEMBLED);preUpload.setServerPath(uploadedUrl);getBaseDao().update("upload.updatePreUploadInfo", preUpload);if(Global.BLOCK_UPLOAD_TO!=Global.BLOCK_UPLOAD_TO_LOCAL){//組裝完畢沒有問題 刪除掉S2/S3上的blockString[] path=preUpload.getS3BlockUrl();for (String string : path) {try {if(Global.BLOCK_UPLOAD_TO==Global.BLOCK_UPLOAD_TO_S2){S2Util.deleteFile(preUpload.getBucketUrl(), string);}else{S3Util.deleteFile(preUpload.getBucketUrl(), string);}} catch (Exception e) {log.error(e.getMessage(), e);}}}if (log.isInfoEnabled())log.info(preUpload.getUuid() + " assembleFileWithBlock");} catch (IOException e) {log.error(e.getMessage(), e);try {if (fileReader != null) {fileReader.close();}if (fileWrite != null) {fileWrite.close();}} catch (IOException ex) {log.error(e.getMessage(), e);}}}
BlockPreuploadFileInfo 是我們自訂的業務檔案處理 bean。
OK,分塊上傳的伺服器、用戶端原始碼及其工作流程至此已全部介紹完畢,以上原始碼全部是經過項目實踐過的,大部分現在仍運行於一些項目之中。有興趣的朋友可以自己動手,將以上代碼自行改造,看看能否運行成功。如果遇到問題可以在本部落格下跟帖留言,大家一起討論討論。