900字范文,内容丰富有趣,生活中的好帮手!
900字范文 > SpringBoot+Vue.js实现大文件分片上传 断点续传与极速秒传

SpringBoot+Vue.js实现大文件分片上传 断点续传与极速秒传

时间:2022-07-24 22:28:56

相关推荐

SpringBoot+Vue.js实现大文件分片上传 断点续传与极速秒传

亲测好用,这里就直接上代码了,代码有详细的解释。

0. 建表语句

SET NAMES utf8mb4;SET FOREIGN_KEY_CHECKS = 0;-- ------------------------------ Table structure for file_info-- ----------------------------DROP TABLE IF EXISTS `file_info`;CREATE TABLE `file_info` (`id` char(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'id',`file_path` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '相对路径',`file_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '文件名',`suffix` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '后缀',`file_size` int(11) NULL DEFAULT NULL COMMENT '大小|字节B',`file_use` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用途|枚举[FileUseEnum]:COURSE(\'C\', \'讲师\'), TEACHER(\'T\', \'课程\')',`created_at` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',`updated_at` datetime(0) NULL DEFAULT NULL COMMENT '修改时间',`shard_index` int(11) NULL DEFAULT NULL COMMENT '已上传分片',`shard_total` int(11) NULL DEFAULT NULL COMMENT '分片总数',`shard_size` int(11) NULL DEFAULT NULL COMMENT '分片大小|B',`file_key` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '文件标识',`vod` char(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'vod|阿里云vod',PRIMARY KEY (`id`) USING BTREE,UNIQUE INDEX `path_unique`(`file_path`) USING BTREE,UNIQUE INDEX `key_unique`(`file_key`) USING BTREE) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '文件' ROW_FORMAT = Dynamic;SET FOREIGN_KEY_CHECKS = 1;

1. vue代码

<template><div><el-card class="box-card"><el-row><el-col :span="6"><el-uploadclass="upload-vhr"action="no"list-type="text"ref="uploadFile"accept="no":auto-upload="false":on-exceed="handleExceed":http-request="customUpload":on-change="handleChange":on-remove="handleRemove":limit="1":file-list="fileList"><el-input placeholder="请输入内容" v-model="fileName"><template slot="append"><el-button type="primary" icon="el-icon-folder-opened">选择文件</el-button></template></el-input><!--<div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过500kb</div>--></el-upload></el-col><el-col :span="6"><el-button type="primary" icon="el-icon-folder-opened" @click="submitUpload">提交</el-button></el-col></el-row></el-card></div></template><script>import {hex_md5} from "@/utils/md5.js";export default {name: "EmpAdv",/*props: {afterUpload: {type: Function,default: null},},*/data() {return {file: "",fileList: [],fileName: "",url: {upload: "/file/upload",check: "/file/check"}};},methods: {submitUpload() {if (this.fileList == '') {this.$message.warning("请选择需要上传的文件!")} else {// 调用文件上传的钩子函数this.$refs.uploadFile.submit();this.fileList = []}},//自定义上传文件钩子,发送上传文件请求customUpload() {let file = this.file;let key = hex_md5(file.name + file.size + file.type);let suffix = file.name.substr(file.name.lastIndexOf(".") + 1).toLowerCase();// 文件分片let shardSize = 20 * 1024 * 1024; // 以20M为一个分片let shardIndex = 1; //分片索引, 1表示第一个分片let size = file.size;let shardTotal = Math.ceil(size / shardSize);let param = {"shardIndex": shardIndex,"shardSize": shardSize,"shardTotal": shardTotal,"fileUse": "C","fileName": file.name,"suffix": suffix,"fileSize": size,"fileKey": key}this.check(param);},/*** 检查文件状态,是否已上传过?传到第几个分片?*/check(param) {this.getRequest(this.url.check, {"fileKey": param.fileKey}).then(resp => {if (resp && resp.status) {let obj = resp.data;if (!obj) {param.shardIndex = 1;console.log("没有找到文件记录,从分片1开始上传");this.upload(param);} else if (obj.shardIndex === obj.shardTotal) {// 已上传分片 = 分片总数,说明已全部上传完,不需要再上传this.$message.success("文件极速秒传成功!");} else {param.shardIndex = obj.shardIndex + 1;console.log("找到文件记录,从分片" + param.shardIndex + "开始上传");this.upload(param);}} else {this.$message.error("文件上传失败");}})},upload(param) {let shardIndex = param.shardIndex;let shardTotal = param.shardTotal;let shardSize = param.shardSize;let fileShard = this.getFileShard(shardIndex, shardSize);// 将图片转为 base64 进行传输let fileReader = new FileReader();fileReader.onload = (e => {let base64 = e.target.result;param.shard = base64;this.postRequest(this.url.upload, param).then(resp => {if (resp && resp.status) {this.fileName = "";this.fileList = []} else {this.$message.error(resp.msg)}let respData = resp.dataif (shardIndex < shardTotal) {// 上传下一个分片param.shardIndex = param.shardIndex + 1;this.upload(param);} else {this.$message.success("上传成功")}})})fileReader.readAsDataURL(fileShard);},getFileShard(shardIndex, shardSize) {let file = this.file;// 当前分片起始位置let start = (shardIndex - 1) * shardSize;//当前分片结束位置let end = Math.min(file.size, start + shardSize);let fileShard = file.slice(start, end);return fileShard;},handleRemove(file, fileList) {// 删除上传文件this.fileName = "";this.fileList = []},handleChange(file, fileList) {// 文件状态钩子,选择文件时触发this.fileList = fileList;this.fileName = file.name;this.file = this.fileList[0].raw;},handleExceed(files, fileList) {this.$message.warning(`当前限制选择 1 个文件,本次选择了 ${files.length} 个文件,共选择了 ${files.length + fileList.length} 个文件`);}}}</script><style scoped></style>

2. md5加密工具类

var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode*//** These are the functions you'll usually want to call* They take string arguments and return either hex or base-64 encoded strings*/export function hex_md5(s) {return binl2hex(core_md5(str2binl(s), s.length * chrsz));}function str2binl(str) {var bin = Array();var mask = (1 << chrsz) - 1;for (var i = 0; i < str.length * chrsz; i += chrsz)bin[i >> 5] |= (str.charCodeAt(i / chrsz) & mask) << (i % 32);return bin;}/** Calculate the MD5 of an array of little-endian words, and a bit length*/function core_md5(x, len) {/* append padding */x[len >> 5] |= 0x80 << ((len) % 32);x[(((len + 64) >>> 9) << 4) + 14] = len;var a = 1732584193;var b = -271733879;var c = -1732584194;var d = 271733878;for (var i = 0; i < x.length; i += 16) {var olda = a;var oldb = b;var oldc = c;var oldd = d;a = md5_ff(a, b, c, d, x[i + 0], 7, -680876936);d = md5_ff(d, a, b, c, x[i + 1], 12, -389564586);c = md5_ff(c, d, a, b, x[i + 2], 17, 606105819);b = md5_ff(b, c, d, a, x[i + 3], 22, -1044525330);a = md5_ff(a, b, c, d, x[i + 4], 7, -176418897);d = md5_ff(d, a, b, c, x[i + 5], 12, 1200080426);c = md5_ff(c, d, a, b, x[i + 6], 17, -1473231341);b = md5_ff(b, c, d, a, x[i + 7], 22, -45705983);a = md5_ff(a, b, c, d, x[i + 8], 7, 1770035416);d = md5_ff(d, a, b, c, x[i + 9], 12, -1958414417);c = md5_ff(c, d, a, b, x[i + 10], 17, -42063);b = md5_ff(b, c, d, a, x[i + 11], 22, -1990404162);a = md5_ff(a, b, c, d, x[i + 12], 7, 1804603682);d = md5_ff(d, a, b, c, x[i + 13], 12, -40341101);c = md5_ff(c, d, a, b, x[i + 14], 17, -1502002290);b = md5_ff(b, c, d, a, x[i + 15], 22, 1236535329);a = md5_gg(a, b, c, d, x[i + 1], 5, -165796510);d = md5_gg(d, a, b, c, x[i + 6], 9, -1069501632);c = md5_gg(c, d, a, b, x[i + 11], 14, 643717713);b = md5_gg(b, c, d, a, x[i + 0], 20, -373897302);a = md5_gg(a, b, c, d, x[i + 5], 5, -701558691);d = md5_gg(d, a, b, c, x[i + 10], 9, 38016083);c = md5_gg(c, d, a, b, x[i + 15], 14, -660478335);b = md5_gg(b, c, d, a, x[i + 4], 20, -405537848);a = md5_gg(a, b, c, d, x[i + 9], 5, 568446438);d = md5_gg(d, a, b, c, x[i + 14], 9, -1019803690);c = md5_gg(c, d, a, b, x[i + 3], 14, -187363961);b = md5_gg(b, c, d, a, x[i + 8], 20, 1163531501);a = md5_gg(a, b, c, d, x[i + 13], 5, -1444681467);d = md5_gg(d, a, b, c, x[i + 2], 9, -51403784);c = md5_gg(c, d, a, b, x[i + 7], 14, 1735328473);b = md5_gg(b, c, d, a, x[i + 12], 20, -1926607734);a = md5_hh(a, b, c, d, x[i + 5], 4, -378558);d = md5_hh(d, a, b, c, x[i + 8], 11, -574463);c = md5_hh(c, d, a, b, x[i + 11], 16, 1839030562);b = md5_hh(b, c, d, a, x[i + 14], 23, -35309556);a = md5_hh(a, b, c, d, x[i + 1], 4, -1530992060);d = md5_hh(d, a, b, c, x[i + 4], 11, 1272893353);c = md5_hh(c, d, a, b, x[i + 7], 16, -155497632);b = md5_hh(b, c, d, a, x[i + 10], 23, -1094730640);a = md5_hh(a, b, c, d, x[i + 13], 4, 681279174);d = md5_hh(d, a, b, c, x[i + 0], 11, -358537222);c = md5_hh(c, d, a, b, x[i + 3], 16, -722521979);b = md5_hh(b, c, d, a, x[i + 6], 23, 76029189);a = md5_hh(a, b, c, d, x[i + 9], 4, -640364487);d = md5_hh(d, a, b, c, x[i + 12], 11, -421815835);c = md5_hh(c, d, a, b, x[i + 15], 16, 530742520);b = md5_hh(b, c, d, a, x[i + 2], 23, -995338651);a = md5_ii(a, b, c, d, x[i + 0], 6, -198630844);d = md5_ii(d, a, b, c, x[i + 7], 10, 1126891415);c = md5_ii(c, d, a, b, x[i + 14], 15, -1416354905);b = md5_ii(b, c, d, a, x[i + 5], 21, -57434055);a = md5_ii(a, b, c, d, x[i + 12], 6, 1700485571);d = md5_ii(d, a, b, c, x[i + 3], 10, -1894986606);c = md5_ii(c, d, a, b, x[i + 10], 15, -1051523);b = md5_ii(b, c, d, a, x[i + 1], 21, -2054922799);a = md5_ii(a, b, c, d, x[i + 8], 6, 1873313359);d = md5_ii(d, a, b, c, x[i + 15], 10, -30611744);c = md5_ii(c, d, a, b, x[i + 6], 15, -1560198380);b = md5_ii(b, c, d, a, x[i + 13], 21, 1309151649);a = md5_ii(a, b, c, d, x[i + 4], 6, -145523070);d = md5_ii(d, a, b, c, x[i + 11], 10, -110379);c = md5_ii(c, d, a, b, x[i + 2], 15, 718787259);b = md5_ii(b, c, d, a, x[i + 9], 21, -343485551);a = safe_add(a, olda);b = safe_add(b, oldb);c = safe_add(c, oldc);d = safe_add(d, oldd);}return Array(a, b, c, d);}/** Convert an array of little-endian words to a hex string.*/function binl2hex(binarray) {var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";var str = "";for (var i = 0; i < binarray.length * 4; i++) {str += hex_tab.charAt((binarray[i >> 2] >> ((i % 4) * 8 + 4)) & 0xF) +hex_tab.charAt((binarray[i >> 2] >> ((i % 4) * 8)) & 0xF);}return str;}

3. 配置文件

我这里配置了一些基础配置:druid、log4j2、mybatis等。

集成log4j2可以看这里: /weixin_4280/article/details/111028263

要是不想配置log4j2可以注释掉:

server:port: 8090servlet:context-path: /vhraddress:spring:profiles:active: devapplication:name: vhrservlet:multipart:maxFileSize: 100MBmaxRequestSize: 100MB# 数据源配置datasource:driver-class-name: com.mysql.jdbc.Drivertype: com.alibaba.druid.pool.DruidDataSourceurl: jdbc:mysql://127.0.0.1:3306/vhr?useUnicode=true&characterEncoding=utf-8&useSSL=false&autoReconnect=true&serverTimezone=UTCdata-username: rootdata-password: rootdruid:# 初始化时建立物理连接的个数,initial-size: 5# 最小连接池数量min-idle: 5# 最大连接池数量max-active: 20# 获取连接时最大等待时间,单位毫秒max-wait: 60000# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位毫秒time-between-eviction-runs-millis: 60000# 配置一个连接在池中最小生存的时间,单位毫秒min-evictable-idle-time-millis: 300000validation-query: SELECT 1 FROM DUAL# 申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。test-while-idle: true# 申请连接时会执行validationQuery检测连接是否有效,开启会降低性能,默认为truetest-on-borrow: false# 归还连接时会执行validationQuery检测连接是否有效,开启会降低性能,默认为truetest-on-return: false# 是否缓存preparedStatement,也就是PSCache,PSCache对支持游标的数据库性能提升巨大,比如说oracle,在mysql下建议关闭。mysql5.5+建议开启pool-prepared-statements: true# 当值大于0时poolPreparedStatements会自动修改为truemax-pool-prepared-statement-per-connection-size: 20# 通过别名的方式配置扩展插件: stat:监控统计,wall:防sql注入,log4j:日志filters: stat,wall,slf4j# 合并多个DruidDataSource的监控数据use-global-data-source-stat: true# 通过connectProperties属性来打开mergeSql功能;慢SQL记录connect-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000mybatis:# 注意:一定要对应mapper映射xml文件的所在路径mapper-locations: classpath:/mapper/*Mapper.xml# 注意:对应实体类的路径type-aliases-package: com.javaboy.vhr.entityconfiguration:map-underscore-to-camel-case: true# 日志配置logging:level:com.javaboy.vhr.mapper: DEBUGconfig: classpath:log4j2.yml # 指定log4j配置文件的位置localUploadFilePath: D:/vhr/localUploadFilePath/

4. 实体类

package com.javaboy.vhr.entity;import io.swagger.annotations.ApiModel;import io.swagger.annotations.ApiModelProperty;import lombok.Getter;import lombok.Setter;import java.io.Serializable;import java.util.Date;/*** @author: gaoyang* @date: -03-23 10:46:31* @description: 文件(FileInfo)实体类*/@Getter@Setter@ApiModel("文件实体类")public class FileInfo implements Serializable {private static final long serialVersionUID = 694649584012557460L;@ApiModelProperty("id")private String id;@ApiModelProperty("相对路径")private String filePath;@ApiModelProperty("文件名")private String fileName;@ApiModelProperty("后缀")private String suffix;@ApiModelProperty("大小|字节B")private Integer fileSize;@ApiModelProperty("用途|枚举[FileUseEnum]:COURSE('C', '讲师'), TEACHER('T', '课程')")private String fileUse;@ApiModelProperty("创建时间")private Date createdAt;@ApiModelProperty("修改时间")private Date updatedAt;@ApiModelProperty("已上传分片")private Integer shardIndex;@ApiModelProperty("分片大小|B")private Integer shardSize;@ApiModelProperty("分片总数")private Integer shardTotal;@ApiModelProperty("文件标识")private String fileKey;@ApiModelProperty("base64")private String shard;@ApiModelProperty("vod|阿里云vod")private String vod;}

5. 后端接口-controller

package com.javaboy.vhr.controller;import com.github.pagehelper.PageInfo;import com.javaboy.vhr.entity.FileInfo;import com.javaboy.vhr.enums.FileUseEnum;import com.javaboy.vhr.service.FileInfoService;import com.javaboy.vhr.utils.Base64ToMultipartFile;import com.javaboy.vhr.utils.result.ResultDTO;import io.swagger.annotations.Api;import io.swagger.annotations.ApiOperation;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Value;import org.springframework.web.bind.annotation.*;import org.springframework.web.multipart.MultipartFile;import javax.annotation.Resource;import java.io.*;/*** @author: gaoyang* @date: -03-23 10:46:33* @description: 文件(FileInfo)表控制层*/@Slf4j@Api(tags = "文件API")@RestController@RequestMapping("/file")public class FileInfoController {@Value("${localUploadFilePath}")private String FILE_PATH;@Resourceprivate FileInfoService fileInfoService;@ApiOperation(value = "文件上传")@PostMapping("/upload")public ResultDTO<FileInfo> upload(@RequestBody FileInfo fileInfo) throws InterruptedException {String use = fileInfo.getFileUse();String key = fileInfo.getFileKey();String suffix = fileInfo.getSuffix();String shardBase64 = fileInfo.getShard();MultipartFile shard = Base64ToMultipartFile.base64ToMultipart(shardBase64);// 保存文件到本地FileUseEnum useEnum = FileUseEnum.getByCode(use);// 如果目录不存在则创建String dir = useEnum.name().toLowerCase();File fullDir = new File(FILE_PATH + dir);if (!fullDir.exists()) {fullDir.mkdirs();}// course\6sfSqfOwzmik4A4icMYuUe.mp4String path = new StringBuffer(dir).append(File.separator).append(key).append(".").append(suffix).toString();// course\6sfSqfOwzmik4A4icMYuUe.mp4.1String localPath = new StringBuffer(path).append(".").append(fileInfo.getShardIndex()).toString();String fullPath = FILE_PATH + localPath;File dest = new File(fullPath);try {// 保存文件shard.transferTo(dest);} catch (IOException e) {log.error(e.getMessage());return ResultDTO.error("上传失败-" + e.getMessage(), null);}// 保存文件记录fileInfo.setFilePath(path);FileInfo model = this.fileInfoService.queryByKey(fileInfo.getFileKey());if (model == null) {this.fileInfoService.insert(fileInfo);} else {model.setShardIndex(fileInfo.getShardIndex());this.fileInfoService.update(model);}if (fileInfo.getShardIndex().equals(fileInfo.getShardTotal())) {this.merge(fileInfo);}return ResultDTO.success("上传成功", fileInfo);}/*** 文件合并*/public void merge(FileInfo fileInfo) throws InterruptedException {log.info("合并分片开始");// course\6sfSqfOwzmik4A4icMYuUe.mp4String path = fileInfo.getFilePath();Integer shardTotal = fileInfo.getShardTotal();File newFile = new File(FILE_PATH, path);// 文件追加写入FileOutputStream outputStream = null;try {outputStream = new FileOutputStream(newFile, true);} catch (FileNotFoundException e) {log.error(e.getMessage());}// 分片文件FileInputStream fileInputStream = null;byte[] bytes = new byte[10 * 1024 * 1024];int len;try {for (Integer i = 0; i < shardTotal; i++) {// 读取第 i 个分片fileInputStream = new FileInputStream(new File(FILE_PATH + path + "." + (i + 1)));while ((len = fileInputStream.read(bytes)) != -1) {outputStream.write(bytes, 0, len);}}} catch (IOException e) {log.error("合并分片异常-" + e.getMessage());} finally {try {if (fileInputStream != null) {fileInputStream.close();}outputStream.close();log.info("IO流关闭");} catch (IOException e) {log.error("IO流关闭失败-", e.getMessage());}}log.info("合并分片结束");// 释放虚拟机对文件的占用System.gc();Thread.sleep(100);log.info("删除分片开始");for (Integer i = 0; i < shardTotal; i++) {String filePath = FILE_PATH + path + "." + (i + 1);File file = new File(filePath);boolean result = file.delete();log.info("删除{},{}", filePath, result ? "成功" : "失败");}log.info("删除分片结束");}@ApiOperation(value = "文件分片检查")@GetMapping("/check")public ResultDTO<FileInfo> check(@RequestParam(name = "fileKey") String fileKey) {FileInfo fileInfo = this.fileInfoService.queryByKey(fileKey);return ResultDTO.success(fileInfo);}}

6. 接口实现类serviceImpl

这里就不贴service代码了,大家自动生成即可。

package com.javaboy.vhr.service.impl;import com.github.pagehelper.PageHelper;import com.github.pagehelper.PageInfo;import com.javaboy.vhr.entity.FileInfo;import com.javaboy.vhr.mapper.FileInfoMapper;import com.javaboy.vhr.service.FileInfoService;import com.javaboy.vhr.utils.UuidUtil;import org.springframework.stereotype.Service;import javax.annotation.Resource;import java.util.Date;import java.util.List;/*** @author: gaoyang* @date: -03-23 10:46:35* @description: 文件(FileInfo)表服务实现类*/@Service("fileInfoService")public class FileInfoServiceImpl implements FileInfoService {@Resourceprivate FileInfoMapper fileInfoMapper;/*** 通过ID查询单条数据** @param id 主键* @return 实例对象*/@Overridepublic FileInfo queryById(String id) {return this.fileInfoMapper.queryById(id);}/*** 新增数据** @param fileInfo 实例对象* @return 实例对象*/@Overridepublic FileInfo insert(FileInfo fileInfo) {fileInfo.setId(UuidUtil.getShortUuid());fileInfo.setCreatedAt(new Date());fileInfo.setUpdatedAt(new Date());this.fileInfoMapper.insert(fileInfo);return fileInfo;}/*** 修改数据** @param fileInfo 实例对象* @return 实例对象*/@Overridepublic FileInfo update(FileInfo fileInfo) {fileInfo.setUpdatedAt(new Date());this.fileInfoMapper.update(fileInfo);return this.queryById(fileInfo.getId());}/*** 通过文件标识查询* @param fileKey* @return*/@Overridepublic FileInfo queryByKey(String fileKey) {return this.fileInfoMapper.queryByKey(fileKey);}}

mybatis语句这里也不贴了,就是简单的增删改查。

源码地址:/king-high/vhr-master.git

技术交流+微:JavaBoy_1024

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。