前言
前端使用vue2.0,上传组件为vue-simple-loader分片上传文件。
后台使用java8接收,接收文件后,保存在项目路径下,分片上传到AWS S3存储桶。
流程:大文件通过vue-simple-loader分片上传到java后台,保存到本地项目下。再将本地项目下的文件分片上传到s3,上传成功后,删除本地文件。
现存待研究问题:
1. vue-simple-loader上传一个分片,s3接收一个分片的形式,但未实现(暂未找到s3接收此种形式的方法)。
2.通过js直连s3进行上传,js版本2方式。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-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
}