前言
前端使用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 }