Java大文件上传方案(vue+饿了么):秒传、断点续传、分片上传!

前言

本篇文章是基于其他文章的基础上结合自己的理解写出来的,如果哪里有问题请指出!

详细教程

秒传

1、什么是秒传

通俗的说,你把要上传的东西上传,服务器会先做MD5校验,如果服务器上有它就会进入秒传,想要不秒传,其实只要让MD5改变,就是对文件本身做一下修改(改名字不行),例如一个文本文件,你多加几个字,MD5就变了,就不会秒传了.

2、本文实现的秒传核心逻辑

a、利用redis的set方法存放文件上传状态,其中key为文件上传的md5,value为是否上传完成的标志位,

b、当标志位true为上传已经完成,此时如果有相同文件上传,则进入秒传逻辑。如果标志位为false,则表示当前分片没有上传,则进入上传逻辑.

分片上传

1.什么是分片上传

分片上传,就是将所要上传的文件,按照一定的大小,将整个文件分隔成多个数据块(我们称之为Part)来进行分别上传,上传完之后再由服务端对所有上传的文件进行汇总整合成原始的文件。

2.分片上传的场景

1.大文件上传

2.网络环境环境不好,存在需要重传风险的场景.

断点续传

1、什么是断点续传

断点续传是在下载或上传时,将下载或上传任务(一个文件或一个压缩包)人为的划分为几个部分,每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传或者下载未完成的部分,而没有必要从头开始上传或者下载。本文的断点续传主要是针对断点上传场景。

2、应用场景

断点续传可以看成是分片上传的一个衍生,因此可以使用分片上传的场景,都可以使用断点续传。

3、实现断点续传的核心逻辑

在分片上传的过程中,如果因为系统崩溃或者网络中断等异常因素导致上传中断,这时候服务端需要记录上传的进度。在之后支持再次上传时,可以继续从上次上传中断的地方进行继续上传。(秒传逻辑已经记录在了redis,就不用记录了.)

为了避免客户端在上传之后的进度数据被删除而导致重新开始从头上传的问题,服务端提供相应的接口便于客户端对分片数据进行查询,从而使客户端知道已经上传的分片数据,从而从下一个没有上传的分片数据开始继续上传。

4、实现流程步骤

1.本文实现的步骤
  • 前端(客户端)需要根据固定大小对文件进行分片,请求后端(服务端)时要带上分片序号和大小

  • 服务端创建conf文件用来记录分块位置,conf文件长度为总分片数,每上传一个分块即向conf文件中写入一个127,那么没上传的位置就是默认的0,已上传的就是Byte.MAX_VALUE 127(这步是实现断点续传和秒传的核心步骤)

  • 服务器按照请求数据中给的分片序号和每片分块大小(分片大小是固定且一样的)算出开始位置,与读取到的文件片段数据,写入文件。

2.分片上传/断点上传代码实现

a、前端框架使用的是vue+饿了么UI,进行分片。

b、后端实现文件写入,是用RandomAccessFile,和MappedByteBuffer实现。

前端(客户端)大概流程:

对文件进行指定大小分片->循环调用分片次数->计算文件内容的MD5值->调用服务器接口将MD5值传递查询服务器是否有相同文件->否?->进入上传逻辑->是?->什么也不干,进入下个分片检测

前端代码片段(代码垃圾,大佬勿怪,有哪里需要改的帮忙指出下谢谢)

async function updateFile(file) {// 将文件切分成小块进行上传// 每个分片的大小,这里设置的全局大小const chunkSize = store.state.config.fileChunkSize;const fileSize = file.size;// 总分片数totalChunks.value = Math.ceil(fileSize / chunkSize);// 截取文件名称等参数const parts = file.name.split(".");const fileName = parts[0];const fileType = parts[parts.length - 1];//判断是不是秒传let secCount = 0;for (var i = 0; i < totalChunks.value; i++) {// 当前分片的起始位置let start = currentChunk.value * chunkSize;// 当前分片的结束位置let end = Math.min(start + chunkSize, fileSize);console.log("当前位置" + currentChunk.value + "开始位置:" + start + "===============结束位置:" + end);//获取文件片段var partFile = file.slice(start, end);//计算文件md5var md5 = await computeFileSliceMD5(partFile);//服务器验证md5是否相同var verify = true;//验证MD5await checkFileMd5({ "md5": md5 }).then(result => {console.log(typeof (result.data));if (result.code == 200) {//返回true表示上传过verify = result.data;currentChunk.value++;} else {console.log("分片md5验证失败!");progressData.progressBar = false;}})//没上传过在调用上传接口if (verify) {secCount++;console.log("已经上传过进入秒传模式!");if (secCount == totalChunks.value) {console.log("全部都是秒传");progressData.percentage = 100;ElMessage.success("上传成功");progressData.progressBar = false;}else {//计算进度条进度progressData.percentage = Math.floor((100 / totalChunks.value)) * secCount + Math.floor((100 / totalChunks.value));}} else {//组装数据const formData = new FormData();formData.append('md5', md5);formData.append('file', partFile);formData.append('chunk', i);formData.append('chunkSize', chunkSize);formData.append('totalChunks', totalChunks.value);formData.append('fileType', fileType);formData.append('fileName', fileName);formData.append('fileSize', fileSize);//开始上传await fileUpload(formData).then(result => {console.log(result);if (result.code == 200) {if (i == totalChunks.value - 1) {progressData.progressBar = false;progressData.percentage = 100;}console.log("分片上传成功!");} else {console.log("分片上传失败!");}});}}}/**计算分片文件MD5* @param {Object} fileSlice*/async function computeFileSliceMD5(fileSlice) {return new Promise((resolve, reject) => {const reader = new FileReader();reader.onload = function (e) {const arrayBuffer = e.target.result;const spark = new SparkMD5.ArrayBuffer();spark.append(arrayBuffer);const blockMD5 = spark.end();resolve(blockMD5);};reader.onerror = function (error) {console.error('Error reading file:', error);};reader.readAsArrayBuffer(fileSlice);});}

后端(服务端)大概流程:

获取到后端传递的参数->将format参数转换对象->提前将当前文件的MD5存到redis和数据库状态为为上传状态->通过偏移量计算出文件位置,将当前数据写入到服务器->检测文件是否完成

后端代码片段(代码垃圾,大佬勿怪,有哪里需要改的帮忙指出下谢谢)

    public boolean fileShardingUpload(MultipartFile file, MultiValueMap<String, Object> formData) {//对象转换FileUploadRequest param = converData(formData);//上传文件逻辑MappedByteBuffer mappedByteBuffer = null;try (RandomAccessFile tempRaf = new RandomAccessFile(FileUploadUtil.createTmpFile(param), "rw"); FileChannel fileChannel = tempRaf.getChannel()) {//新增分片记录saveFileShardingRecord(param);//设置默认值long chunkSize = Objects.isNull(param.getChunkSize()) ? 5 * 1024 * 1024 : param.getChunkSize();//写入该分片数据//计算该分片数据的偏移量long offset = chunkSize * param.getChunk();//读取文件数据byte[] fileData = file.getBytes();//映射文件mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, offset, fileData.length);//写数据mappedByteBuffer.put(fileData);//判断是否上传完成boolean isok = FileUploadUtil.checkAndSetUploadProgress(param);if (isok) {//上传成功,记录文件的完整记录,并且将文件丢到minio(后面看看有没有直接存到minio然后合并的)saveFileInfo(param);}//保存数据库操作(分片信息也要记录)updateFileShardingRecord(param);return isok;} catch (IOException e) {log.error(e.getMessage(), e);throw new RuntimeException("文件上传失败!");} finally {//这是一个坑不关闭,会一直占用try {Method m = FileChannelImpl.class.getDeclaredMethod("unmap", MappedByteBuffer.class);m.setAccessible(true);m.invoke(FileChannelImpl.class, mappedByteBuffer);} catch (Exception e) {log.error(e.getMessage(), e);}}}/*** 转换数据* @param formData* @return*/private FileUploadRequest converData(MultiValueMap<String, Object> formData) {FileUploadRequest req = new FileUploadRequest();req.setMd5(formData.getFirst("md5").toString());req.setChunk(Integer.parseInt(formData.getFirst("chunk").toString()));req.setChunkSize(Long.valueOf((formData.getFirst("chunkSize").toString())));req.setTotalChunks(Integer.parseInt(formData.getFirst("totalChunks").toString()));req.setFileType(formData.getFirst("fileType").toString());req.setFileName(formData.getFirst("fileName").toString());req.setFileSize(formData.getFirst("fileSize").toString());req.setFileTepPath(fileTemPath);return req;}/*** 新增分片记录** @param param 文件对象*/private void saveFileShardingRecord(FileUploadRequest param) {//提前设置文件状态FileUploadUtil.beforeSetUploadProgressRedis(param);//保存数据库FileShardingRecord fileShardingRecord = new FileShardingRecord();fileShardingRecord.setMd5(param.getMd5());fileShardingRecordMapper.insert(fileShardingRecord);}/*** 修改分片记录状态** @param param 文件对象*/private void updateFileShardingRecord(FileUploadRequest param) {FileShardingRecord fileShardingRecord = new FileShardingRecord();fileShardingRecord.setMd5(param.getMd5());fileShardingRecord.setStatus(1);fileShardingRecordMapper.updateByMd5(fileShardingRecord);}
FileUploadUtil类
public class FileUploadUtil {/*** 创建临时文件** @param param 前端传递的对象* @return* @throws IOException*/public static File createTmpFile(FileUploadRequest param) throws IOException {String fileName = param.getFileName();String tempFile = param.getFileTepPath() + fileName + "_tmp." + param.getFileType();// 创建 Path 对象Path path = Paths.get(tempFile);// 检查文件是否存在boolean exists = Files.exists(path);if (!exists) {Files.createFile(path);}return path.toFile();}/*** 创建conf记录文件** @param param* @return* @throws IOException*/public static File createConfFile(FileUploadRequest param) throws IOException {String fileName = param.getFileName();String uploadDirPath = param.getFileTepPath();String tempConfFile = uploadDirPath + fileName + "_tmp.conf";param.setFileTepConfPath(tempConfFile);// 创建 Path 对象Path path = Paths.get(tempConfFile);// 检查文件是否存在boolean exists = Files.exists(path);if (!exists) {Files.createFile(path);}return path.toFile();}/*** 检查文件的md5值** @param md5 md5* @return 是否上传完成*/public static boolean checkFileMd5(String md5) {//和Redis存储得做比较看看有没有上传...RedisUtil redisUtil = (RedisUtil) GlobalCache.getStaticCache().getIfPresent("redisUtil");//获取redis记录Object hget = redisUtil.hget(RedisEnum.UploadFileStatus.getContent(), md5);if (Objects.nonNull(hget)) {if (hget.equals("true")) {log.info("文件已经上传");return true;}}return false;}/*** 检查并修改文件上传进度*/public static boolean checkAndSetUploadProgress(FileUploadRequest param) throws IOException {//创建conf记录文File confFile = createConfFile(param);//是否完成byte isComplete = Byte.MAX_VALUE;try (RandomAccessFile accessConfFile = new RandomAccessFile(confFile, "rw");) {//把该分段标记为 true 表示完成System.out.println("set part " + param.getChunk() + " complete");//创建conf文件文件长度为总分片数,每上传一个分块即向conf文件中写入一个127,那么没上传的位置就是默认0,已上传的就是Byte.MAX_VALUE 127accessConfFile.setLength(param.getTotalChunks());accessConfFile.seek(param.getChunk());accessConfFile.write(Byte.MAX_VALUE);//completeList 检查是否全部完成,如果数组里是否全部都是127(全部分片都成功上传)byte[] completeList = FileUtil.readAsByteArray(confFile);for (int i = 0; i < completeList.length && isComplete == Byte.MAX_VALUE; i++) {//与运算, 如果有部分没有完成则 isComplete 不是 Byte.MAX_VALUEisComplete = (byte) (isComplete & completeList[i]);System.out.println("check part " + i + " complete?:" + completeList[i]);}} catch (IOException e) {log.error(e.getMessage(), e);}//把上传进度信息存进redisboolean isOk = setUploadProgressRedis(param, confFile, isComplete);return isOk;}/*** 把上传进度信息存进redis*/private static boolean setUploadProgressRedis(FileUploadRequest param, File confFile, byte isComplete) {//获取redisRedisUtil redisUtil = (RedisUtil) GlobalCache.getStaticCache().getIfPresent("redisUtil");//127表示全部完成了if (isComplete == Byte.MAX_VALUE) {redisUtil.hset(RedisEnum.UploadFileStatus.getContent(), param.getMd5(), "true");//删除保存的配置文件confFile.delete();//这里表示当前文件全部完成return true;}//修改位tureredisUtil.hset(RedisEnum.UploadFileStatus.getContent(), param.getMd5(), "true");return false;}/*** 提前设置状态** @param param*/public static void beforeSetUploadProgressRedis(FileUploadRequest param) {//获取redisRedisUtil redisUtil = (RedisUtil) GlobalCache.getStaticCache().getIfPresent("redisUtil");redisUtil.hset(RedisEnum.UploadFileStatus.getContent(), param.getMd5(), "false");}}

fileUploadRequest类

@Data
public class FileUploadRequest {/*** 文件名称*/private String fileName;/*** 文件临时路径*/private String fileTepPath;/*** 文件临时配置路径*/private String fileTepConfPath;/*** 文件类型*/private String fileType;/*** 文件大小*/private String fileSize;/*** 文件md5*/private String md5;/*** 当前块数*/private Integer  chunk;/*** 分片得大小*/private Long chunkSize;/*** 总块数*/private Integer totalChunks;}

总结

本文只提供分片上传思路,代码具体以项目逻辑为主.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/52798.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

properties文件提示未引用

问题描述 以前用的好好的项目,今天突然打开就发现idea不识别spring配置信息显示未引用,如果config代码中引入的配置却可以高亮显示,然后输入spring相关的配置,文件是没有提示的。经过研究发现是spring相关的插件被关闭了。效果如下 解决方法 启用三个插件spring Boot,Sp…

看完这100道软件测试面试题,拿不到offer,算我输

掌握此套面试题&#xff0c;人手至少2份offer&#xff0c;绝不瞎吹&#xff01;分享给大家。 一、自我介绍 二、灵活问题 1、大概说说之前公司的测试流程 2、测试报告有哪些内容? 3、如何保证用例的覆盖度&#xff1f; 4、什么是测试用例&#xff0c;什么是测试脚本&…

知识社区的小程序源码系统 界面支持万能DIY装修 带源代码包以及搭建部署教程

系统概述 知识社区的小程序源码系统是一款专为构建知识分享和交流社区而设计的强大工具。它提供了完整的源代码包&#xff0c;使开发者能够根据自己的需求进行定制和扩展&#xff0c;打造出个性化的小程序应用。 该系统的界面设计简洁大方&#xff0c;易于操作&#xff0c;同…

【JavaEE】线程安全性问题,线程不安全是怎么产生的,该如何应对

产生线程不安全的原因 在Java多线程编程中&#xff0c;线程不安全通常是由于多个线程同时访问共享资源而引发的竞争条件。以下是一些导致线程不安全的常见原因&#xff1a; 共享可变状态&#xff1a;当多个线程对共享的可变数据进行读写时&#xff0c;如果没有适当的同步机制&…

鸿蒙Next 单元测试框架——hypium

一 框架概述 单元测试框架(hypium)是HarmonyOS上的测试框架&#xff0c;提供测试用例编写、执行、结果显示能力&#xff0c;用于测试系统或应用接口。 表1 单元测试框架功能特性 二 安装使用 目前hypium以npm包的形式发布, 因此需要在Deveco Studio 工程级package.json内配…

CSS-常用属性【看这一篇就够了】

目录 前言文章 常用属性 cursor鼠标样式 outline外轮廓 border与outline的区别 overflow超出部分隐藏 overflow属性值 overflow-x和overflow-y vertical-align属性 应用案例 常用的a标签布局按钮 水平居中的轮播图按钮 产品展示效果&#xff1a; 小米商城菜单 前…

【C#】属性的声明

在面向对象程序设计中,属性是访问对象存储数据的首选方式。 一般不要直接公开类的变量成员,即便是get访问器和set访问器并无数据访问规则。 属性的声明 1. 完整声明 在代码中输入propfull &#xff0c;并连续按两下tab键 高亮的部分是可以修改的部分&#xff0c;按tab键可以…

FPGA上板项目(四)——FIFO测试

目录 实验内容实验原理FIFO IP 核时序绘制HDL 代码仿真综合实现上板测试 实验内容 理解 FIFO 原理调用 FIFO IP 核完成数据读写 实验原理 FIFO&#xff1a;First In First Out&#xff0c;先入先出式数据缓冲器&#xff0c;用来实现数据先入先出的读写方式。可分类为同步 FI…

一个php快速项目搭建框架源码,带一键CURD等功能

介绍&#xff1a; 框架易于功能扩展&#xff0c;代码维护&#xff0c;方便二次开发&#xff0c;帮助开发者简单高效降低二次开发成本&#xff0c;满足专注业务深度开发的需求。 百度网盘下载 图片&#xff1a;

Redis 入门到精通1

一、String&#xff08;字符串&#xff09; 特点&#xff1a; 最基本的数据类型&#xff0c;二进制安全&#xff0c;可以存储任何数据&#xff0c;比如图片或者序列化的对象。一个 key 对应一个 value。 常用命令及示例&#xff1a; SET key value&#xff1a;设置一个键值对。…

科研绘图系列:R语言多组极坐标图(grouped polar plot)

介绍 Polar plot(极坐标图)是一种二维图表,它使用极坐标系统来表示数据,而不是像笛卡尔坐标系(直角坐标系)那样使用x和y坐标。在极坐标图中,每个数据点由一个角度(极角)和一个半径(极径)来确定。角度通常从水平线(或图表的某个固定参考方向)开始测量,而半径则是…

CannotCreateTransactionException产生原因及解决方案

CannotCreateTransactionException 是 Spring 框架中的一个异常&#xff0c;通常出现在使用 Spring 的事务管理器时。该异常表明事务无法创建&#xff0c;可能是由于与底层资源&#xff08;如数据库连接&#xff09;相关的问题导致的。这是一个运行时异常&#xff0c;通常与 Da…

MySQL 函数、约束、多表查询与事务详解

在 MySQL 数据库中&#xff0c;函数、约束、多表查询和事务是非常重要的概念&#xff0c;它们可以帮助我们更好地管理和操作数据。本文将详细介绍这些概念&#xff0c;并通过代码演示来帮助你更好地理解。 一、函数 MySQL 提供了许多内置函数&#xff0c;可以用于处理字符串、数…

【网络安全】服务基础第一阶段——第六节:Windows系统管理基础---- DNS部署与安全

计算机智能识别并用IP地址定位&#xff0c;例如我们想要访问一个网页&#xff0c;其实是只能使用这个网页的IP地址&#xff0c;即四位的0&#xff5e;255来访问&#xff0c;但这一串数字难以记忆&#xff0c;于是就有了DNS&#xff0c;将难以记忆的数字转化为容易记忆的域名&am…

odbc连接达梦数据库DM8

odbc连接达梦数据库DM8 1 环境介绍2 达梦数据库安装3 odbc安装3.1 查询yum 匹配的odbc安装包3.2 安装 unixODBC.x86_64 4 配置odbc4.1 查看odbc 环境信息 5 odbc连接dm8成功5.1 配置 odbcinst.ini5.2 配置 odbc.ini5.3 odbc 连接达梦数据库5.3.1 dmdba 用户使用isql5.3.2 root …

AI模型:追求全能还是专精?

OpenAI计划在秋季推出的代号为“草莓”的新AI模型&#xff0c;展现了从数学问题到主观营销策略等多样化处理能力&#xff0c;这确实是一个引人注目的全能型AI发展的里程碑。关于全能型AI是否代表未来趋势&#xff0c;以及相比专业型AI产品是否具有更广阔的经济市场和用户吸引力…

合宙LuatOS产品规格书——Air700EAQ

Luat Air700EAQ是合宙的LTE Cat.1bis通信模块&#xff0c;采用移芯EC716E平台&#xff0c;支持LTE 3GPP Rel.13技术。 该模块专为满足小型化、低成本需求而设计&#xff0c;具备超小封装和极致成本优势。 Air700EAQ支持移动双模&#xff0c;内置丰富的网络协议&#xff0c;集…

获取Word、PPT、Excel、PDF文件页数及加密校验

想要获取一个pdf文件的页数&#xff0c;有多种实现方式。可以利用pdfjs&#xff0c;也可以利用PDFDocument&#xff1a; // 方法一&#xff1a;利用文件的arrayBuffer let arrayBuffer await file.arrayBuffer(); const pdfDoc await PDFDocument.load(arrayBuffer, { ignor…

基于AI大模型开发上层应用常见的技术栈

基于AI大模型的上层应用开发&#xff0c;技术栈要求通常包括以下几个方面&#xff1a; 编程语言&#xff1a;Python是AI领域的主要编程语言&#xff0c;具有大量的库和框架支持&#xff0c;是大模型开发的首选语言 。TypeScript也是不错的选择&#xff0c;很多模型对外提供类似…

LuaJit分析(六)luajit -bl 命令分析

Luajit -bl命令用于将luajit字节码文件或者lua脚本文件反汇编&#xff0c;输出汇编指令&#xff0c;很好奇怎么将字节码文件和lua脚本文件放在一块处理的&#xff0c;下面一步步分析&#xff1a; luajit虚拟机由luajit.c文件生成&#xff0c;首先定位到main函数&#xff0c;代…