为啥要有断点续传
传统的文件传输方式一旦遇到网络问题或者浏览器做重刷的话,那么对于上传过程是灾难性的。那么为了解决这个问题,就出现了断点续传。在之前已传输的地方再接着传输!今天我们这里就用springboot来实现断点续传!
断点续传的大致流程图
这里的核心在于如何分片,记录分片,然后再获取分片重新从分片处接着传输!
创建工程
添加依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example.demo</groupId>
<artifactId>upload</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>upload</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--lombok依赖-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--文件上传依赖-->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.4</version>
</dependency>
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.3</version>
</dependency>
<!-- mysql的依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
<version>5.1.47</version>
</dependency>
<!-- mybatis-plus依赖 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
配置文件
#spring.resources.static-locations=classpath:/static
server.port=8000
#文件上传路径
file.basepath=E:/BaiduNetdiskDownload/
spring.servlet.multipart.max-file-size= 50MB
spring.servlet.multipart.max-request-size= 50MB
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/leotemp?characterEncoding=utf8&autoReconnect=true&useSSL=false&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=123456
# templates文件夹的路径
spring.thymeleaf.prefix=classpath:/templates/
# templates中的所有文件后缀名,如/templates/main.html
spring.thymeleaf.suffix=.html
创建数据库
create table file
(
id INTEGER primary key AUTO_INCREMENT comment 'id',
path varchar(100) not null COMMENT '相对路径',
name varchar(100) COMMENT '文件名',
suffix varchar(10) COMMENT '文件后缀',
size int COMMENT '文件大小|字节B',
created_at BIGINT(20) COMMENT '文件创建时间',
updated_at bigint(20) COMMENT '文件修改时间',
shard_index int comment '已上传分片',
shard_size int COMMENT '分片大小|B',
shard_total int COMMENT '分片总数',
file_key varchar(100) COMMENT '文件标识'
)
创建实体类
package com.example.demo.upload.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@Data
@TableName(value = "file")
public class FileDTO {
/**
* id
*/
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
/**
* 相对路径
*/
private String path;
/**
* 文件名
*/
private String name;
/**
* 后缀
*/
private String suffix;
/**
* 大小|字节B
*/
private Integer size;
/**
* 创建时间
*/
private Long createdAt;
/**
* 修改时间
*/
private Long updatedAt;
/**
* 已上传分片
*/
private Integer shardIndex;
/**
* 分片大小|B
*/
private Integer shardSize;
/**
* 分片总数
*/
private Integer shardTotal;
/**
* 文件标识
*/
private String fileKey;
}
mapper层
package com.example.demo.upload.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.demo.upload.entity.FileDTO;
import org.springframework.stereotype.Repository;
@Repository
public interface FileMapper extends BaseMapper<FileDTO> {
}
响应工具类
package com.example.demo.upload.utils;
import lombok.Data;
@Data
public class Result {
// 成功状态码
public static final int SUCCESS_CODE = 200;
// 请求失败状态码
public static final int FAIL_CODE = 500;
// 查无资源状态码
public static final int NOTF_FOUNT_CODE = 404;
// 无权访问状态码
public static final int ACCESS_DINE_CODE = 403;
/**
* 状态码
*/
private int code;
/**
* 提示信息
*/
private String msg;
/**
* 数据信息
*/
private Object data;
/**
* 请求成功
*
* @return
*/
public static Result ok() {
Result r = new Result();
r.setCode(SUCCESS_CODE);
r.setMsg("请求成功!");
r.setData(null);
return r;
}
/**
* 请求失败
*
* @return
*/
public static Result fail() {
Result r = new Result();
r.setCode(FAIL_CODE);
r.setMsg("请求失败!");
r.setData(null);
return r;
}
/**
* 请求成功,自定义信息
*
* @param msg
* @return
*/
public static Result ok(String msg) {
Result r = new Result();
r.setCode(SUCCESS_CODE);
r.setMsg(msg);
r.setData(null);
return r;
}
/**
* 请求失败,自定义信息
*
* @param msg
* @return
*/
public static Result fail(String msg) {
Result r = new Result();
r.setCode(FAIL_CODE);
r.setMsg(msg);
r.setData(null);
return r;
}
/**
* 请求成功,自定义信息,自定义数据
*
* @param msg
* @return
*/
public static Result ok(String msg, Object data) {
Result r = new Result();
r.setCode(SUCCESS_CODE);
r.setMsg(msg);
r.setData(data);
return r;
}
/**
* 请求失败,自定义信息,自定义数据
*
* @param msg
* @return
*/
public static Result fail(String msg, Object data) {
Result r = new Result();
r.setCode(FAIL_CODE);
r.setMsg(msg);
r.setData(data);
return r;
}
public Result code(Integer code){
this.setCode(code);
return this;
}
public Result data(Object data){
this.setData(data);
return this;
}
public Result msg(String msg){
this.setMsg(msg);
return this;
}
}
服务层
package com.example.demo.upload.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.demo.upload.dao.FileMapper;
import com.example.demo.upload.entity.FileDTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class FileService {
@Autowired
private FileMapper fileMapper;
//保存文件
public void save(FileDTO file1){
//根据 数据库的 文件标识来查询 当前视频 是否存在
LambdaQueryWrapper<FileDTO> lambda = new QueryWrapper<FileDTO>().lambda();
lambda.eq(FileDTO::getFileKey,file1.getFileKey());
List<FileDTO> fileDTOS = fileMapper.selectList(lambda);
//如果存在就话就修改
if(fileDTOS.size()!=0){
//根据key来修改
LambdaQueryWrapper<FileDTO> lambda1 = new QueryWrapper<FileDTO>().lambda();
lambda1.eq(FileDTO::getFileKey,file1.getFileKey());
fileMapper.update(file1,lambda1);
}else
{
//不存在就添加
fileMapper.insert(file1);
}
}
//检查文件
public List<FileDTO> check(String key){
LambdaQueryWrapper<FileDTO> lambda = new QueryWrapper<FileDTO>().lambda();
lambda.eq(FileDTO::getFileKey,key);
List<FileDTO> dtos = fileMapper.selectList(lambda);
return dtos;
}
}
控制层
package com.example.demo.upload.controller;
import com.example.demo.upload.entity.FileDTO;
import com.example.demo.upload.service.FileService;
import com.example.demo.upload.utils.Result;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FilenameUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.util.List;
import java.util.UUID;
@Controller
@RequestMapping("/file")
@Slf4j
public class FileController {
@Autowired
FileService fileService;
public static final String BUSINESS_NAME = "普通分片上传";
// 设置图片上传路径
@Value("${file.basepath}")
private String basePath;
@RequestMapping("/show")
public String show(){
return "file";
}
/**
* 上传
* @param file
* @param suffix
* @param shardIndex
* @param shardSize
* @param shardTotal
* @param size
* @param key
* @return
* @throws IOException
* @throws InterruptedException
*/
@RequestMapping("/upload")
@ResponseBody
public String upload(MultipartFile file,
String suffix,
Integer shardIndex,
Integer shardSize,
Integer shardTotal,
Integer size,
String key) throws IOException, InterruptedException {
log.info("上传文件开始");
//文件的名称
String name = UUID.randomUUID().toString().replaceAll("-", "");
// 获取文件的扩展名
String ext = FilenameUtils.getExtension(file.getOriginalFilename());
//设置图片新的名字
String fileName = new StringBuffer().append(key).append(".").append(suffix).toString(); // course\6sfSqfOwzmik4A4icMYuUe.mp4
//这个是分片的名字
String localfileName = new StringBuffer(fileName)
.append(".")
.append(shardIndex)
.toString(); // course\6sfSqfOwzmik4A4icMYuUe.mp4.1
// 以绝对路径保存重名命后的图片
File targeFile=new File(basePath,localfileName);
if(!targeFile.exists()){
targeFile.mkdirs();
}
//上传这个图片
file.transferTo(targeFile);
//数据库持久化这个数据
FileDTO file1=new FileDTO();
file1.setPath(basePath+localfileName);
file1.setSuffix(suffix);
file1.setName(name);
file1.setSuffix(ext);
file1.setSize(size);
file1.setCreatedAt(System.currentTimeMillis());
file1.setUpdatedAt(System.currentTimeMillis());
file1.setShardIndex(shardIndex);
file1.setShardSize(shardSize);
file1.setShardTotal(shardTotal);
file1.setFileKey(key);
//插入到数据库中
//保存的时候 去处理一下 这个逻辑
fileService.save(file1);
//判断当前是不是最后一个分页 如果不是就继续等待其他分页 合并分页
if(shardIndex.equals(shardTotal) ){
file1.setPath(basePath+fileName);
this.merge(file1);
}
return "上传成功";
}
@RequestMapping("/check")
@ResponseBody
public Result check(String key){
List<FileDTO> check = fileService.check(key);
//如果这个key存在的话 那么就获取上一个分片去继续上传
if(check.size()!=0){
return Result.ok("查询成功",check.get(0));
}
return Result.fail("查询失败,可以添加");
}
/**
* @author fengxinglie
* 合并分页
*/
private void merge(FileDTO fileDTO) throws FileNotFoundException, InterruptedException {
//合并分片开始
log.info("分片合并开始");
String path = fileDTO.getPath(); //获取到的路径 没有.1 .2 这样的东西
//截取视频所在的路径
path = path.replace(basePath,"");
Integer shardTotal= fileDTO.getShardTotal();
File newFile = new File(basePath + path);
FileOutputStream outputStream = new FileOutputStream(newFile,true); // 文件追加写入
FileInputStream fileInputStream = null; //分片文件
byte[] byt = new byte[10 * 1024 * 1024];
int len;
try {
for (int i = 0; i < shardTotal; i++) {
// 读取第i个分片
fileInputStream = new FileInputStream(new File(basePath + path + "." + (i + 1))); // course\6sfSqfOwzmik4A4icMYuUe.mp4.1
while ((len = fileInputStream.read(byt)) != -1) {
outputStream.write(byt, 0, len);
}
}
} catch (IOException e) {
log.error("分片合并异常", e);
} finally {
try {
if (fileInputStream != null) {
fileInputStream.close();
}
outputStream.close();
log.info("IO流关闭");
} catch (Exception e) {
log.error("IO流关闭", e);
}
}
log.info("分片结束了");
//告诉java虚拟机去回收垃圾 至于什么时候回收 这个取决于 虚拟机的决定
System.gc();
//等待100毫秒 等待垃圾回收去 回收完垃圾
Thread.sleep(100);
log.info("删除分片开始");
for (int i = 0; i < shardTotal; i++) {
String filePath = basePath + path + "." + (i + 1);
File file = new File(filePath);
boolean result = file.delete();
log.info("删除{},{}", filePath, result ? "成功" : "失败");
}
log.info("删除分片结束");
}
}
页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<!--<script src="jquery-3.3.1.min.js"></script>-->
<script type="text/javascript" src="/md5.js"></script>
<script type="text/javascript" src="/tool.js"></script>
<body>
<table border="1px solid red">
<tr>
<td>文件1</td>
<td>
<input name="file" type="file" id="inputfile"/>
</td>
</tr>
<tr>
<td></td>
<td>
<button onclick="check()">提交</button>
</td>
</tr>
</table>
</body>
<script type="text/javascript">
//上传文件的话 得 单独出来
function test1(shardIndex) {
console.log(shardIndex);
//以formData的方式提交
var fd = new FormData();
//获取表单中的file
var file = $('#inputfile').get(0).files[0];
//文件分片 以20MB为一分片
var shardSize = 20 * 1024 * 1024;
//定义分片索引
var shardIndex = shardIndex;
//定义分片的起始位置
var start = (shardIndex - 1) * shardSize;
//定义分片结束的位置
var end = Math.min(file.size, start + shardSize);
//按大小切割文件段
var fileShard = file.slice(start, end);
//分片的大小
var size = file.size;
//总片数
var shardTotal = Math.ceil(size / shardSize);
//文件的后缀名
var fileName = file.name;
var suffix = fileName.substring(fileName.lastIndexOf(".") + 1, fileName.length).toLowerCase();
//把视频的信息存储为一个字符串
var filedetails = file.name + file.size + file.type + file.lastModifiedDate;
//使用当前文件的信息用md5加密生成一个key 这个加密是根据文件的信息来加密的 如果相同的文件 加的密还是一样的
var key = hex_md5(filedetails);
var key10 = parseInt(key, 16);
//把加密的信息 转为一个64位的
var key62 = Tool._10to62(key10);
//前面的参数必须和controller层定义的一样
fd.append('file', fileShard);
fd.append('suffix', suffix);
fd.append('shardIndex', shardIndex);
fd.append('shardSize', shardSize);
fd.append('shardTotal', shardTotal);
fd.append('size', size);
fd.append("key", key62)
$.ajax({
url: "/file/upload",
type: "post",
cache: false,
data: fd,
processData: false,//很重要,告诉jquery不要对form进行处理
contentType: false,//很重要,指定为false才能形成正确的Content-Type
success: function (data) {
//这里应该是一个递归调用
if (shardIndex < shardTotal) {
var index = shardIndex + 1;
test1(index);
} else {
alert(data)
}
},
error: function () {
//请求出错处理
}
})
//发送ajax请求把参数传递到后台里面
}
//判断这个加密文件存在不存在
function check() {
var file = $('#inputfile').get(0).files[0];
//把视频的信息存储为一个字符串
var filedetails = file.name + file.size + file.type + file.lastModifiedDate;
//使用当前文件的信息用md5加密生成一个key 这个加密是根据文件的信息来加密的 如果相同的文件 加的密还是一样的
var key = hex_md5(filedetails);
var key10 = parseInt(key, 16);
//把加密的信息 转为一个64位的
var key62 = Tool._10to62(key10);
//检查这个key存在不存在
$.ajax({
url: "/file/check",
type: "post",
data: {'key': key62},
success: function (data) {
console.log(data);
if (data.code == 500) {
//这个方法必须抽离出来
test1(1);
} else {
if (data.data.shardIndex == data.data.shardTotal) {
alert("极速上传成功");
} else {
//找到这个是第几片 去重新上传
test1(parseInt(data.data.shardIndex));
}
}
}
})
}
</script>
</html>
这里面注重点就是前端分片的时候,起始片和重点片的计算问题!因为我们知道如果一个文件35M,以20M为一分片的话,他其实是两片的。简单的除法计算1余15!
码云传送门
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/16387.html