前言
前端使用vue2.0,上傳組件為vue-simple-loader分片上傳文件。
后台使用java8接收,接收文件后,保存在項目路徑下,分片上傳到AWS S3存儲桶。
流程:大文件通過vue-simple-loader分片上傳到java后台,保存到本地項目下。再將本地項目下的文件分片上傳到s3,上傳成功后,刪除本地文件。
現存待研究問題:
1. vue-simple-loader上傳一個分片,s3接收一個分片的形式,但未實現(暫未找到s3接收此種形式的方法)。
2.通過js直連s3進行上傳,js版本2方式:https://www.cnblogs.com/aiyowei/p/15769695.html。js版本3方式暫未實現
參考的大佬筆記:
vue-simple-loader github鏈接:https://github.com/simple-uploader/vue-uploader/blob/master/README_zh-CN.md
vue-simple-upload options屬性 github鏈接:https://github.com/simple-uploader/Uploader#events
vue-simple-uploader筆記:https://www.cnblogs.com/xiahj/p/vue-simple-uploader.html
后台分片上傳筆記:https://blog.csdn.net/jxysgzs/article/details/107778949
aws s3分片上傳參考文檔:https://docs.aws.amazon.com/zh_cn/AmazonS3/latest/userguide/mpu-upload-object.html
重要::雖然我很菜,寫的也不夠好,但我不接受任何批評,本文僅供有需要的人參考及自己記錄用。
前端部分
安裝vue-simple-loader
npm install vue-simple-uploader --save 本文使用0.7.6版本
main.js中
import uploader from 'vue-simple-uploader'
Vue.use(uploader)
將vue-simple-uploader項目下,src文件夾中的common和components文件夾下的文件引入自己的項目
下載地址:https://github.com/simple-uploader/vue-uploader/tree/master/src
我的項目中引入位置,分別放在components/upload 和 utils/upload 文件夾下
前端代碼,將大文件分片上傳到本地,在上傳成功的回調onFileSuccess中,將本地文件上傳到S3存儲桶
<template> <div class="uploader"> <!-- autoStart 需要設置成 false --> <uploader :options="options" :autoStart="false" :fileStatusText="{ success: '上傳成功,等待后台處理...', error: '上傳失敗', uploading: '正在上傳', paused: '暫停上傳', waiting: '等待上傳' }" @file-success="onFileSuccess" @file-added="fileAdded" @file-error="onFileError" ></uploader> </div> </template> <script> import uploader from '../../components/upload/uploader.vue' import { localFileToS3 } from '@/api/file/file.js'; export default { components: { uploader }, data() { return { options: { target: '/bigFileToLocal.do', // 目標上傳 URL chunkSize: 5 * 1024 * 1024, // 分塊大小,要和后台合並的大小對應 singleFile: true, // 是否單文件 maxChunkRetries: 3, //最大自動失敗重試上傳次數 testChunks: false, //是否開啟服務器分片校驗, 默認true query: { // 參數 }, headers: { // 請求頭認證 "token": localStorage.getItem('token') }, } } }, methods: { //大文件上傳所需 fileAdded(file) { //選擇文件后暫停文件上傳,上傳時手動啟動 file.pause() }, onFileError(file) { console.log('error', file) }, onFileSuccess(rootFile, file, response, chunk) { // 文件上傳到本地成功后的回調 var res = JSON.parse(response); if (res.code == "200") { // 上傳成功,上傳本地文件到s3 var fileName = res.obj.fileName; var filePath = res.obj.filePath; let params = { fileName: fileName, filePath: filePath } localFileToS3(params).then(res => { // 底層是axios請求 // 將上傳到本地的文件上傳到AWS s3 console.log(res); }) } }, }, } </script> <style> .uploader { position: relative; } </style>
后台部分
步驟:
1. 接收vue-simple-loader分片傳過來的參數,保存到本地項目目錄下
2. 取得本地項目目錄下的文件,分片上傳到s3
3. 刪除本地保存的文件
Controller部分
import com.systron.common.controller.BaseController; import com.systron.common.utils.ResponseApi; import com.systron.models.sys.Chunk; import com.systron.service.sys.FileService; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @Controller public class UploadController { private Logger logger = LoggerFactory.getLogger(UploadController.class); @Autowired private FileService fileService; /** * 大文件分片上傳后保存到本地項目目錄 * * @param chunk * @param request * @param response */ @RequestMapping(value="/bigFileToLocal.do") public void bigFileToLocal(@ModelAttribute Chunk chunk, HttpServletRequest request, HttpServletResponse response) { ResponseApi<Object> responseApi = new ResponseApi<Object>(); // 分片上傳 responseApi = fileService.bigFileToLocal(chunk); if (null != responseApi && StringUtils.isNotEmpty(responseApi.getCode())) { response.setStatus(Integer.valueOf(responseApi.getCode())); } else { response.setStatus(201); } outObjectToJson(response, responseApi); } /** * 本地大文件分片上傳到s3存儲桶 * @param request * @param response */ @RequestMapping(value="/localFileToS3.do") public void localFileToS3(HttpServletRequest request, HttpServletResponse response) { ResponseApi<Object> responseApi = new ResponseApi<Object>(); String allFilePath = request.getParameter("filePath"); // 文件路徑 String fileName = request.getParameter("fileName"); // 文件名稱 responseApi = fileService.localFileToS3(fileName, allFilePath); outObjectToJson(response, responseApi); } }
Service部分
import com.alibaba.fastjson.JSONObject; import com.amazonaws.AmazonServiceException; import com.amazonaws.SdkClientException; import com.amazonaws.auth.profile.ProfileCredentialsProvider; import com.amazonaws.regions.Regions; import com.amazonaws.services.s3.AmazonS3; import com.amazonaws.services.s3.AmazonS3ClientBuilder; import com.amazonaws.services.s3.transfer.TransferManager; import com.amazonaws.services.s3.transfer.TransferManagerBuilder; import com.amazonaws.services.s3.transfer.Upload; import com.systron.common.utils.ResponseApi; import com.systron.common.utils.cache.CacheConfigUtil; import com.systron.dao.sys.FileDao; import com.systron.models.sys.Chunk; import com.systron.utils.HelpUtil; import com.systron.utils.PathUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.RandomAccessFile; @Service public class FileService { private Logger logger = LoggerFactory.getLogger(FileService.class); @Resource(name = "fileDao") private FileDao fileDao; // 存儲桶名稱 private static String bucketName = CacheConfigUtil.getProperty("bucket.name"); /** * 大文件分片上傳到本地項目下 * @param chunk 每個塊信息 * @return */ public ResponseApi<Object> bigFileToLocal(Chunk chunk) { ResponseApi<Object> responseApi = new ResponseApi<Object>(); /** * 每一個上傳塊都會包含如下分塊信息: * chunkNumber: 當前塊的次序,第一個塊是 1,注意不是從 0 開始的。 * totalChunks: 文件被分成塊的總數。 * chunkSize: 分塊大小,根據 totalSize 和這個值你就可以計算出總共的塊數。注意最后一塊的大小可能會比這個要大。 * currentChunkSize: 當前塊的大小,實際大小。 * totalSize: 文件總大小。 * identifier: 這個就是每個文件的唯一標示。 * filename: 文件名。 * relativePath: 文件夾上傳的時候文件的相對路徑屬性。 * 一個分塊可以被上傳多次,當然這肯定不是標准行為,但是在實際上傳過程中是可能發生這種事情的,這種重傳也是本庫的特性之一。 * * 根據響應碼認為成功或失敗的: * 200 文件上傳完成 * 201 文加快上傳成功 * 500 第一塊上傳失敗,取消整個文件上傳 * 507 服務器出錯自動重試該文件塊上傳 */ String path = PathUtils.getFileDir(); String fileName = chunk.getFilename(); String allFilePath = path + "/" + fileName; File file = new File(path, fileName); // 第一個塊,則新建文件 if (chunk.getChunkNumber() == 1 && !file.exists()) { try { file.createNewFile(); } catch (IOException e) { responseApi.setCode("500"); responseApi.setMsg("exception:createFileException"); return responseApi; } } // 進行寫文件操作 try ( //將塊文件寫入文件中 InputStream fos = chunk.getFile().getInputStream(); RandomAccessFile raf = new RandomAccessFile(file, "rw") ) { int len = -1; byte[] buffer = new byte[1024]; raf.seek((chunk.getChunkNumber() - 1) * 1024 * 1024 * 5); while ((len = fos.read(buffer)) != -1) { raf.write(buffer, 0, len); } } catch (IOException e) { e.printStackTrace(); if (chunk.getChunkNumber() == 1) { file.delete(); } responseApi.setCode("507"); responseApi.setMsg("exception:writeFileException"); return responseApi; } if (chunk.getChunkNumber().equals(chunk.getTotalChunks())) { // 保存到本地文件成功 responseApi.setCode("200"); responseApi.setMsg("over");
// 返回文件路徑和文件名稱 JSONObject json = new JSONObject(); json.put("fileName", fileName); json.put("filePath", allFilePath); responseApi.setObj(json); System.out.println(json); return responseApi; } else { responseApi.setCode("201"); responseApi.setMsg("ok"); return responseApi; } } /** * 本地大文件分片上傳到s3存儲桶 * @param fileName * @param allFilePath */ public ResponseApi<Object> localFileToS3(String fileName, String allFilePath) { ResponseApi<Object> responseApi = new ResponseApi<Object>(); // 1. 前端上傳的文件整合保存到本地成功,將本地文件分片上傳到s3存儲桶 String suffix = fileName.split("[.]")[1]; String url = ""; responseApi = awsLocalFileToS3(fileName, allFilePath); if ("200".equals(responseApi.getCode())) { // 2. 上傳到s3成功后,獲取返回url url = String.valueOf(responseApi.getObj()); // 3. 刪除本地文件 boolean fileDelFlag = HelpUtil.delete(allFilePath); if (!fileDelFlag) { logger.info("刪除本地文件失敗,文件路徑:" + allFilePath); } } return responseApi; } /** * 本地大文件分片上傳到s3存儲桶 * 具體實現 * * @param fileName 文件名稱 * @param path 文件路徑 */ public ResponseApi<Object> awsLocalFileToS3(String fileName, String path) { ResponseApi<Object> responseApi = new ResponseApi<Object>(); Regions clientRegion = Regions.CN_NORTHWEST_1; try { AmazonS3 s3Client = AmazonS3ClientBuilder.standard() .withRegion(clientRegion) .withCredentials(new ProfileCredentialsProvider()) .build(); TransferManager tm = TransferManagerBuilder.standard() .withS3Client(s3Client) .build(); String objectKey = System.currentTimeMillis() + "_" + Math.random() + "_" + fileName; Upload upload = tm.upload(bucketName, objectKey, new File(path)); logger.info("上傳開始:" + fileName); // 上傳完成 upload.waitForCompletion(); logger.info("上傳完成:" + fileName); String url = "https://" + bucketName + ".s3.cn-northwest-1.amazonaws.com.cn/" + objectKey; responseApi.setCode("200"); responseApi.setMsg("ok"); responseApi.setObj(url); return responseApi; } catch (AmazonServiceException e) { // The call was transmitted successfully, but Amazon S3 couldn't process // it, so it returned an error response. e.printStackTrace(); responseApi.setCode("508"); responseApi.setMsg("AmazonServiceException"); return responseApi; } catch (SdkClientException e) { // Amazon S3 couldn't be contacted for a response, or the client // couldn't parse the response from Amazon S3. e.printStackTrace(); responseApi.setCode("508"); responseApi.setMsg("SdkClientException"); return responseApi; } catch (InterruptedException e) { e.printStackTrace(); responseApi.setCode("508"); responseApi.setMsg("InterruptedException"); return responseApi; } } }
獲取服務器根路徑
public class PathUtils { /** * 獲取服務器存放文件的目錄路徑 * * @return 目錄路徑(String) */ public static String getFileDir() { String path = ClassUtils.getDefaultClassLoader().getResource("").getPath().substring(1) + "static/file"; File dir = new File(path); if (!dir.exists()) { dir.mkdirs(); } return path; } }
刪除本地文件HelpUtil中的delete方法
/** * 刪除文件 * * @param fileName 待刪除的完整文件名 * @return */ public static boolean delete(String fileName) { boolean result = false; File f = new File(fileName); if (f.exists()) { result = f.delete(); } else { result = true; } return result; }
其他
ResponseApi幫助類,返回結果
/** * 返回結果類 */ public class ResponseApi<T> { private String code; private String msg; private T obj; public ResponseApi() { code = "0000"; msg = "成功"; } public ResponseApi(T obj) { super(); code = "0000"; msg = "成功"; this.obj = obj; } public ResponseApi(String code,String msg, T obj) { super(); this.code = code; this.msg = msg; this.obj = obj; } // getter/setter }
Chunk幫助類
/** * 文件塊 * */ public class Chunk implements Serializable { /** * 當前文件塊,從1開始 */ private Integer chunkNumber; /** * 分塊大小 */ private Long chunkSize; /** * 當前分塊大小 */ private Long currentChunkSize; /** * 總大小 */ private Long totalSize; /** * 文件標識 */ private String identifier; /** * 文件名 */ private String filename; /** * 相對路徑 */ private String relativePath; /** * 總塊數 */ private Integer totalChunks; /** * 二進制文件 */ private MultipartFile file; // getter/setter }