分片上传
前端代码
<progress class="progress" :value="fileObj.progress" ></progress>
<span class="desc">{{fileObj.progressText ? fileObj.progressText : '正在解析文件'}}</span>
<input type="file" @change="handleFileInputChange"/>
methods: {
handleFileInputChange(e) {
let _this = this;
if (e.target.files) {
let file = e.target.files[0];
let sha1 = hex_sha1(file.name + '-' + file.lastModified);//用于前端判断该文件是否已上传
let item = {
progressText: '等待上传', //上传提示
progress: 0, //进度
file: file, // 文件本身
retry: 0,// 重试次数
del: false, // 删除标志
sha1: sha1, // 前端判断标识
uuid: UUID.generate().replace(/-/g, ''), //作为该文件的唯一标识
idx: 0, //当前分片
chunkNum: Math.ceil(file.size / bufferLength), // 计算分片数量, 向上取整
finishIdx: 0//完成上传的分片数
}
_this.upload(item);
e.target.value = '';
}
},
/*上传*/
async upload(item) {
let _this = this;
item.progressText = '正在上传(' + Math.round(item.finishIdx / item.chunkNum * 100) + '%)';
_this.uploadSlice(item);
},
/*上传*/
uploadSlice(item) {
if (item.del) return;
let _this = this;
let file = item.file;
// 每片分片大小
let bufferLength = 1024 * 1024;
//开始切割位置
let start = item.idx * bufferLength;
//全部上传完毕或重试次数用完则退出
if (start >= file.size) return;
//计算分割的位置
let end = start + bufferLength;
//如果分割点超出文件大小,回退分割点
if (end > file.size) {
end = file.size;
}
//切割文件
let chunk = file.slice(start, end);
let fileReader = new FileReader();
//该方法用于将File对象转化为二进制文件
fileReader.readAsBinaryString(chunk);
fileReader.onload = function (e) {
//e.target.result为读取到的分片的二进制
//创建 formData 对象并添加数据
let formData = new FormData();
formData.set("uuid", item.uuid);
formData.set('sha1', hex_sha1(e.target.result));//每个分片的sha1
formData.set("file", chunk, file.name);
formData.set("idx", item.idx);
formData.set("totalIdx", item.chunkNum);
formData.set("start", start);
formData.set("end", end);
formData.set("bufferLength", bufferLength);
formData.set("totalSize", file.size);
_this.$http.post(_this.src, formData).then((res) => {
item.finishIdx += 1;
let p = item.finishIdx / item.chunkNum;
item.progress = p;
if (p === 1) {
item.progressText = '上传完成(100%)';
if (res.code === 1 && res.data) {
_this.$emit('complete', res.data)
}
} else {
item.progressText = '正在上传(' + Math.round(p * 100) + '%)';
}
if (item.idx === item.chunkNum) return;
item.idx += 1;
_this.uploadSlice(item);
}).catch(err => {
console.log(err);
//失败后可以重试上传对应分片
})
}
}
}
后端代码
重点:只要使用了RandomAccessFile对文件进行处理(好像还有FileChannel可以实现)
//FileSliceVo 实体类
public class FileSliceVo implements Serializable {
private static final long SerialVersionUID = 1L;
private Integer idx;
private Long start;
private Long end;
private Long bufferLength;
private Integer totalSize;
private String sha1;
private Integer totalIdx;
private long idxSize;
private String uuid;
private String path;
private String filename;
}
public AjaxResult<?> upload(MultipartFile file, FileSliceVo sliceVo) {
if (file == null || file.isEmpty()) {
return AjaxResult.fail("请选择要上传的文件");
}
log.info("sha1:{} - totalSize:{} - bufferLength: {} - totalIdx: {} - idx:{} - idxSize: {} - start: {} - end: {}", sliceVo.getSha1(), sliceVo.getTotalSize(), sliceVo.getBufferLength(), sliceVo.getTotalIdx(), sliceVo.getIdx(), file.getSize(), sliceVo.getStart(), sliceVo.getEnd());
//检测对应分片是否已上传
List<FileSliceVo> sliceVos = RedisStorage.get(RedisKeys.FILE_KEY + sliceVo.getUuid(), () -> ListUtil.list(false), 3600L);
if (CollectionUtils.isNotEmpty(sliceVos.stream().filter(f -> f.getSha1().equals(sliceVo.getSha1())).collect(Collectors.toList()))) {
return AjaxResult.ok();
}
}
// 用uuid的后两位作为文件夹的名称
String format = sliceVo.getUuid().substring(sliceVo.getUuid().length() - 2);
// 文件夹路径
String baseUrl = appConfiguration.getFilePath() + File.separator + format;
// 用前端传过来的uuid作为文件名称
String filename = sliceVo.getUuid() + "." + FileUtils.getExtension(file.getOriginalFilename());
// 文件夹路径 文件对象
File serviceFile = new File(baseUrl);
// 文件路劲 文件对象
File tmpFile = new File(baseUrl, filename);
// 检测文件夹是否已存在,不存在就创建
if (!serviceFile.exists()) {
serviceFile.mkdirs();
}
sliceVo.setPath(format + File.separator + filename);
sliceVo.setFilename(file.getOriginalFilename());
sliceVo.setIdxSize(file.getSize()); // 该分片的文件大小
try (RandomAccessFile accessFile = new RandomAccessFile(tmpFile, "rw")) {
accessFile.seek(sliceVo.getStart());
accessFile.write(file.getBytes());
sliceVos.add(sliceVo);
JedisUtil.setObjectValue(RedisKeys.FILE_KEY + sliceVo.getUuid(), sliceVos, 3600L);
long sum = sliceVos.stream().mapToLong(FileSliceVo::getIdxSize).sum();
// 检测是否文件上传完成
if (sliceVos.size() == sliceVo.getTotalIdx() && (sum + "").equals(sliceVo.getTotalSize().toString())) {
//文件上传完成后,保存文件信息
String sha1 = SecureUtil.sha1(tmpFile);
ItFiles itFiles = filesService.selectBySha1(sha1);
if (itFiles == null) {
itFiles = new ItFiles();
itFiles.setFilename(file.getOriginalFilename());
itFiles.setCreateAt(LocalDateTime.now());
itFiles.setPath(format + File.separator + filename);
itFiles.setFilesize(sliceVo.getTotalSize());
itFiles.setSha1(sha1);
filesService.save(itFiles);
}
// 删除对应的redis数据
JedisUtil.del(RedisKeys.FILE_KEY + sliceVo.getUuid());
}
}
分片上传参考的是tus协议
视频文件分片下载(其他大文件亦可参考)
前端代码(vue)
/*
avSrc: 文件获取地址
crossorigin="anonymous" 允许跨域
autoplay 自动播放
*/
<video :src="avSrc" controls="controls" width="100%" crossorigin="anonymous" autoplay></video>
后端代码
/**
range: video 会在请求头中自动添加Range字段,Range: bytes=1048576-
使用RandomAccessFile 读取对应的文件分片
*/
public void fileSlice(HttpServletResponse response, @RequestHeader String range, @RequestParam String params, @RequestParam String sign) throws IOException {
log.info("range -> {}", range);
if (MD5Utils.md5(params + FileUtils.KEY).equals(sign)) {
File file = new File(appConfiguration.getFilePath() + File.separator + params);
if (file.exists()) {
try (OutputStream out = response.getOutputStream();RandomAccessFile accessFile = new RandomAccessFile(file, "r")) {
long fileLength = file.length();
long start = Long.parseLong(range.substring(range.indexOf("=") + 1, range.indexOf("-")));
long end = Math.min(start + 1024 * 1024 - 1, fileLength - 1);
//设定文件读取开始位置(以字节为单位)
accessFile.seek(start);
int contentLength = (int) (end - start + 1);
//返回码需要为206,代表只处理了部分请求,响应了部分数据
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
response.setHeader("Content-Type", "video/mp4");
//设置此次相应返回的数据长度
response.setContentLength(contentLength);
//设置此次相应返回的数据范围, range的end最大为fileLen-1,最后一段需设置为 xxx-fileLen-1/fileLen
response.setHeader("Content-Range", "bytes " + start + "-" + end + "/" + fileLength);
response.setHeader("Accept-Ranges", "bytes");
byte[] cache = new byte[1024];
int len;
while ((len = accessFile.read(cache)) != -1) {
out.write(cache, 0, len);
}
} catch (Exception e) {
log.error("file down", e);
}
} else {
response.setContentType(ContentType.TEXT_PLAIN.getValue());
response.setCharacterEncoding("UTF-8");
response.getWriter().write("文件不存在");
response.getWriter().close();
}
}
}
分片播放参考地址:Spring boot实现视频播放断点续传 - BuptWade - 博客园 (cnblogs.com)
网友评论