Easy云盘总结篇-文件上传01
说在前面:此项目是跟着B站一位大佬做的,不分享源码,支持项目付费
文章目录
- 文件列表
- 文件上传
- 文件秒传
- 分片上传
- 文件合并
- 视频切割
- 缩略图
文件列表
这段的逻辑
前端传过来query,包含用户上传文件的相关内容,和类型category
然后通过枚举类FileCategoryEnums进行文件类型识别,如果找到对应枚举,就设置好文件类型
然后就是设置好用户id,和排序顺序,文件使用情况(使用中)
将查询到的用户文件以PaginationResultVO返回,其中PaginationResultVO的list装FileInfo
然后需要返回给前端FileInfoVo类型而并非实体类类型FileInfo,所以这里编写了一个类型转换方法:convert2PaginationVo
protected <S,T> PaginationResultVO<T> convert2PaginationVo(PaginationResultVO<S> result,Class<T> classz ) {PaginationResultVO<T> resultVO=new PaginationResultVO<>();resultVO.setList(CopyTools.copyList(result.getList(),classz));resultVO.setPageNo(result.getPageNo());resultVO.setPageSize(result.getPageSize());resultVO.setPageTotal(result.getPageTotal());resultVO.setTotalCount(result.getTotalCount());return resultVO;}
这个逻辑就是将PaginationResultVO里的list类型进行s换为t,其他数据直接复制过去的过程。
文件上传
这个逻辑蛮复杂,文件上传分普通上传(第一次传)和秒传(上传相同文件)。其中还需要对文件进行分片处理,过大的文件就需要拆分成小的分片逐步上传。
文件秒传
这段的逻辑:
当分片为0,也就是传过来第一个分片时,进行文件是否在库里的判断,如果有,就秒传。
这里判断秒传:根据md5值查询到第一页使用中的文件
如果不为空,则实现秒传:
先判断当前使用空间+文件大小是否>总空间,抛出对应异常
最后就是将文件进行属性赋值,再将文件插入到表中更新用户空间。
当时有个疑惑点:为什么秒传文件是insert而不是update,现在一想,明白如果update那么意味着该用户始终只有一个文件,而他上传的相同文件相当于覆盖了之前的文件,不符合逻辑。
然后就是说明文件的重命名问题:
分片上传
在前面如果不实现秒传,则进行分片上传
这段逻辑:
当第一个分片来时,先判断是否需要秒传,不是的话执行后面分片上传。当第2第3等分片来时,不再判断秒传,直接分片上传。
分片上传,先判断空间大小,这里,因为是分片,所有就创建了临时文件大小,临时空间(已经上传了的分片大小)+用户已存文件空间+文件大小(当前分片文件大小)>总空间,就抛出异常。
然后分片文件没上传完时,dto要设置状态为“上传中”,并且保存当前分片的临时大小。
假如5个分片,当前分片如果是4时,就已经上传完成了,不走if。
最后,当最后一个分片也上传后:
文件合并
因为我们上传的是一个一个的分片,临时存储在temp文件夹中,还需要将这些分片合并,才是真正实现上传了文件。
这段逻辑:
在最后一个分片上传之前的分片,只是设置了上传状态和保存临时分片大小。在最后一个分片上传后,创建FileInfo,将整个文件的信息进行属性填充,插入到数据库文件表中。
然后更新用户当前空间。
最后如果没上传成功或者临时存放文件夹都没有,就上传失败,删除文件夹。
在事务提交后,执行异步合并文件。
异步
异步通常指的是非阻塞的操作,也就是说在合并分片的过程中,不需要等待所有分片都准备好才开始处理,而是可以一边接收分片一边处理,或者在后台进行合并操作,不影响主线程或其他任务的执行。
转码过程:
转码
转码(Transcoding)是指将数据从一种编码格式转换为另一种编码格式的过程,通常用于适配不同设备、平台或需求。其核心目标是解决兼容性问题或优化数据存储/传输效率
这段逻辑:
通过传过来的源路径,找到需要合并的文件,然后listFiles()获取到这个路径下的这个文件的所有分片。然后就是就是io不断的读取所有的分片文件,把所有的数据都存储到目标文件中。
listFiles()
获取目录下的文件和子目录
RandomAccessFile
RandomAccessFile 是Java中用于访问文件的类,它允许随机访问文件,即可以跳转到文件的任意位置进行读写操作。这一特性使得 RandomAccessFile 在需要访问文件的部分内容时非常有用,而不是从头到尾完整地读取文件
视频切割
为什么要视频切割:
可以按需加载和播放视频,而不需要一次性下载完整的视频文件,从而提高视频的加载速度和播放效率
这段的逻辑:
将视频先转换成一个大的index.ts文件,然后再将它切割成几个小的ts文件和一个.m3u8索引文件(类似目录),最后删除index.ts文件。
缩略图
一般图片类型和视频需要做缩略图:
无论转码成功与否,将整个文件的文件大小,文件缩略图,文件状态(使用中/转码失败),进行文件更新,
这里通过对老状态(转码中)的限制,只有当数据库中文件的当前状态等于 oldStatus 时,才会执行状态更新。若此时状态已被其他线程修改,则更新失败(不会影响数据)。避免了转码流程中的状态覆盖和顺序错乱问题。这就是乐观锁。
生成缩略图这里主要是FFmpeg命令行知识
/*** 为视频生成封面图(通过截取视频的一帧画面)* @param sourceFile 原始视频文件(输入文件)* @param width 生成封面图的目标宽度(高度会根据原始视频宽高比自动计算)* @param targetFile 生成的封面图文件(输出文件)*/public static void createCover4Video(File sourceFile, Integer width, File targetFile) {try {// FFmpeg 命令模板说明:// -i: 指定输入文件(原始视频)// -y: 覆盖已存在的输出文件(避免冲突)// -vframes 1: 仅提取1帧画面(作为封面)// -vf scale=%d:%d/a: 视频过滤(Video Filter)参数,设置缩放规则// 第一个%d: 目标宽度(传入的width)// 第二个%d/a: 高度按原始宽高比自动计算(保持比例避免画面变形)// %s: 输出文件路径(生成的封面图)String cmd = "ffmpeg -i %s -y -vframes 1 -vf scale=%d:%d/a %s";// 格式化命令并执行(通过ProcessUtils工具类调用系统命令)// 第二个参数false表示不等待命令执行完成(异步执行)ProcessUtils.executeCommand(String.format(cmd,sourceFile.getAbsoluteFile(), // 原始视频路径width, // 目标宽度width, // 用于计算高度的基准值(与宽度相同以保持比例)targetFile.getAbsoluteFile() // 封面输出路径),false);} catch (Exception e) {// 记录生成封面失败的异常信息(包括堆栈跟踪)logger.error("生成视频封面失败", e);}}
/*** 使用 FFmpeg 为图片生成缩略图(仅当原始图片宽度超过指定阈值时触发压缩)* @param file 原始图片文件(输入文件)* @param thumbnailWidth 缩略图的目标宽度(高度自动按比例计算)* @param targetFile 生成的缩略图文件(输出文件)* @param delSource 是否删除原始图片文件(true: 删除;false: 保留)* @return 布尔值,表示是否执行了压缩操作(true: 执行了压缩;false: 未压缩,因原始宽度未超过阈值)*/public static Boolean createThumbnailWithFfmpeg(File file, int thumbnailWidth, File targetFile, Boolean delSource) {try {// 读取原始图片的宽高信息(通过ImageIO读取图片元数据)BufferedImage src = ImageIO.read(file);// 注意:若ImageIO无法读取该图片格式(如WebP、HEIC等非标准格式),src会为null,后续操作会抛出空指针异常// 获取原始图片的宽度和高度int sorceW = src.getWidth(); // 原始宽度int sorceH = src.getHeight(); // 原始高度// 关键逻辑:若原始宽度 <= 目标缩略图宽度,无需压缩,直接返回falseif (sorceW <= thumbnailWidth) {return false;}// 调用压缩方法(实际通过FFmpeg执行缩放操作)compressImage(file, thumbnailWidth, targetFile, delSource);return true; // 标记已执行压缩} catch (Exception e) {// 打印异常堆栈(实际生产环境建议替换为日志记录)e.printStackTrace();}return false; // 异常时返回未压缩}/*** 使用 FFmpeg 压缩图片(按指定宽度缩放,高度自动保持比例)* @param sourceFile 原始图片文件(输入文件)* @param width 目标宽度(高度自动按原始宽高比计算)* @param targetFile 压缩后的图片文件(输出文件)* @param delSource 是否删除原始文件(true: 删除;false: 保留)*/public static void compressImage(File sourceFile, Integer width, File targetFile, Boolean delSource) {try {// FFmpeg 命令模板说明:// -i: 指定输入文件(原始图片)// -vf scale=%d:-1: 视频过滤参数,设置缩放规则// %d: 目标宽度(传入的width)// -1: 高度自动计算(保持原始宽高比例)// %s: 输出文件路径(压缩后的图片)// -y: 覆盖已存在的输出文件String cmd = "ffmpeg -i %s -vf scale=%d:-1 %s -y";// 格式化并执行命令ProcessUtils.executeCommand(String.format(cmd,sourceFile.getAbsoluteFile(), // 原始图片路径width, // 目标宽度targetFile.getAbsoluteFile() // 压缩后输出路径),false);// 若需要删除原始文件,则通过FileUtils强制删除(注意:需确保文件未被其他进程占用)if (delSource) {FileUtils.forceDelete(sourceFile);}} catch (Exception e) {// 记录压缩失败的异常信息(不记录堆栈,仅记录错误信息)logger.error("压缩图片失败");}}