java大文件分片上传

流程图

在这里插入图片描述

前端代码

<template>
  <view class="container">
    <button @click="startRecord">录制视频</button>
    <button @click="chooseVideo">选择视频</button>
    <video v-if="videoSrc" :src="videoSrc" controls></video>
  </view>
</template>

<script>
export default {
  data() {
    return {
      videoSrc: '', // 存储录制或选择的视频源
      recordedVideoPath: '', // 存储录制视频的临时路径
	  
	  
	  
	    status: 'Ready',
	  
	      previewSrc: '',
	 intervalId:null,
    };
  },
	//
	beforeCreate() {
					
				},
  methods: {
	 
    // 开始录制视频
    startRecord() {
		
      uni.chooseVideo({
        sourceType: ['camera'],
        maxDuration: 60000, // 视频最长录制时间,单位为毫秒
        camera: 'back', // 使用后置摄像头
        success: (res) => {
          this.recordedVideoPath = res.tempFilePath;
          //this.compressVideo(res.tempFilePath);
		  console.log(res);
		 // this.download(res.tempFilePath);
		 this.calculateFileHash(res.tempFile);
		 
        },
        fail: (err) => {
          uni.showToast({
            title: '录制失败',
            icon: 'none'
          });
        }
      });
    },
	 download(tempFilePath) {
	      
	      const a = document.createElement("a");
	      a.style.display = "none";
	      a.href = tempFilePath;
	      a.download = "recorded-video.webm";
	      document.body.appendChild(a);
	      a.click();
	      window.URL.revokeObjectURL(tempFilePath);
	    },
	
	 sliceFile(file,hashHex) {
		 
		 
		 
	  const chunkSize = 10 * 1024 * 1024; // 10MB,以字节为单位
	  const chunks = [];
	  let start = 0;
	
	  while (start < file.size) {
	    const end = Math.min(start + chunkSize, file.size);
	    const chunk = file.slice(start, end,file.type);
	    chunks.push(chunk);
	    start = end;
	  }
	    console.log(`文件被切片成 ${chunks.length} 个部分。`);
		//发起post请求
	  uni.request({
	      url: 'http://localhost:8080/shardUpload/init', // API地址
	      method: 'POST',
	      data: {
			  fileName:file.name,
			  partNum:chunks.length,
			  hash:hashHex
		  },
	      success: (res) => {
						console.log('请求成功:', res.data);
						let shardUploadId = res.data.data;
						  // 现在可以对每个切片进行操作,例如上传
						  chunks.forEach((chunk, index) => {
							var fileData = new File([chunk], index, {
							  type: file.type,
							  lastModified: new Date().getTime()
							});
							  var fileUrl = URL.createObjectURL(fileData);
							  // 上传分片的代码可以在这里实现
							   uni.uploadFile({
								   url: 'http://localhost:8080/shardUpload/uploadPart', // API地址
								   filePath:fileUrl,
								   formData: {
									  partOrder:index+1,
									  shardUploadId:shardUploadId
								   },
								   success: (res) => {
									   console.log('请求成功:', res.data);
									  
								   },
								   fail: (err) => {
									   console.error('请求失败:', err);
								   }
							   });
						  });
						  //定时监控上传进度
						  setTimeout(() =>{
							  
							   this.intervalId = setInterval(() => {
							  							   //查看上传进度
							         uni.request({
							         	url: 'http://localhost:8080/shardUpload/detail', // API地址
							         	method: 'POST',
							         	data: shardUploadId,
							         	success: (res) => {
							         		console.log('查看进度成功:', res.data);
							  										
							  										if(res.data.data.partNum === res.data.data.partOrderList.length){
							  											 clearInterval(this.intervalId);
																		//合成文件
							  											uni.request({
							  												url: 'http://localhost:8080/shardUpload/complete', // API地址
							  												method: 'POST',
							  												data: shardUploadId,
							  												success: (res) => {
							  													console.log('合成成功:', res.data);
							  						
							  												},
							  												fail: (err) => {
							  													console.error('请求失败:', err);
							  												}
							  											});
							  										}
							         	},
							         	fail: (err) => {
							         		console.error('请求失败:', err);
							         	}
							         });
							        }, 2000);
						  },3000)
						  
				
	      },
	      fail: (err) => {
	          console.error('请求失败:', err);
	      }
	  });
	  
	  
	  
	     
	},
	
	// 定义一个函数,用于计算文件的SHA-256哈希值
	calculateFileHash(file) {
	  // 创建一个FileReader对象,用于读取文件内容
	  const reader = new FileReader();
	  var that = this;
	  
	  // 为FileReader对象设置onload事件处理函数
	  // 当文件读取完成时,此函数将被调用
	  reader.onload = (e) => {
	    // 获取文件内容,存储在e.target.result中
	    const buffer = e.target.result;
	    
	    // 使用Web Crypto API的crypto.subtle.digest方法计算SHA-256哈希值
	    // 'SHA-256'是指定的哈希算法,buffer是要计算哈希的文件数据
	    const digestOp = crypto.subtle.digest('SHA-256', buffer);
	    
	    // 当哈希计算完成时,处理Promise对象
	    digestOp.then((hashBuffer) => {
	      // 将哈希值的ArrayBuffer转换为Uint8Array
	      const hashArray = Array.from(new Uint8Array(hashBuffer));
	      
	      // 将每个字节转换为十六进制字符串,并确保每两个字符为一组,不足两位前面补零
	      const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
	      
	      // 打印计算出的哈希值
	      console.log("SHA-256"+hashHex);
		  that.sliceFile(file,hashHex)
	    }).catch((error) => {
	      // 如果在计算哈希值过程中出现错误,打印错误信息
	      console.error('Error calculating hash:', error);
	    });
	  };
	  
	  // 以ArrayBuffer格式读取文件内容
	  // 这将触发onload事件,当读取操作完成时
	  reader.readAsArrayBuffer(file);
	},
    // 选择视频
    chooseVideo() {
      uni.chooseVideo({
        sourceType: ['album'],
        maxDuration: 60000, // 视频最长录制时间,单位为毫秒
        success: (res) => {
		   this.compressVideo(res.tempFilePath);
        },
        fail: (err) => {
          uni.showToast({
            title: '选择失败',
            icon: 'none'
          });
        }
      });
    },
    // 压缩视频
    compressVideo(tempFilePath) {
		 // 初始化 ffmpeg.js
		 
		 let ffmpeg = new FFmpeg({ logger: console });
		 this.setData({ status: 'Compressing...' });
		 
		     const that = this;
		 
		     // 创建一个 Blob 对象
		     fetch(tempFilePath)
		       .then(res => res.arrayBuffer())
		       .then(buffer => {
		         // 将视频文件加载到 FFmpeg 的文件系统中
		         ffmpeg.FS('writeFile', 'input.mp4', buffer);
		 
		         // 压缩视频
		         ffmpeg.run('-i', 'input.mp4', '-vcodec', 'libx264', '-crf', '28', '-preset', 'ultrafast', 'output.mp4')
		           .on('progress', progress => {
		             console.log(`Progress: ${progress.percent}%`);
		             that.setData({ status: `Progress: ${progress.percent}%` });
		           })
		           .on('end', () => {
		             console.log('Compression finished.');
		 
		             // 读取压缩后的视频
		             const outputBuffer = ffmpeg.FS('readFile', 'output.mp4');
		             const outputBlob = new Blob([outputBuffer], { type: 'video/mp4' });
		 
		             // 将压缩后的视频显示为预览
		             const outputUrl = URL.createObjectURL(outputBlob);
		             that.setData({ previewSrc: outputUrl, status: 'Compression complete.' });
		           })
		           .on('error', err => {
		             console.error('Error during compression:', err);
		             that.setData({ status: 'Error during compression.' });
		           });
		 
		         // 运行 FFmpeg
		         ffmpeg.run();
		       })
		       .catch(err => {
		         console.error('Failed to load video file:', err);
		         this.setData({ status: 'Failed to load video file.' });
		       });
     
    }
  }
};
</script>

<style>
.container {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 20px;
}

video {
  width: 100%;
  margin-top: 20px;
}
</style>

java代码

一共四个接口,对应流程图的四个过程,创建分片任务接口、上传分片文件接口、分片任务详情接口、合成文件接口,代码如下

  1. controller
package com.gwb.template.controller;

import cn.hutool.core.util.IdUtil;
import com.gwb.template.entity.ShardUpload;
import com.gwb.template.service.ShardUploadService;
import com.gwb.template.util.AjaxJson;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;

/**
 * 分片上传文件
 */
@RestController
@RequestMapping("/shardUpload")
public class ShardUploadController {
    @Autowired
    private ShardUploadService shardUploadService;

    /**
     * 创建分片上传任务
     *
     * @return 分片任务id
     */
    @PostMapping("/init")
    public AjaxJson init(@RequestBody ShardUpload shardUpload) {
        return shardUploadService.init(shardUpload);
    }

    /**
     * 上传分片(客户端需遍历上传分片文件)
     *
     * @return
     */
    @PostMapping("/uploadPart")
    public AjaxJson uploadPart(@RequestParam("file")MultipartFile file,@RequestParam("partOrder")String partOrder,
                               @RequestParam("shardUploadId")String shardUploadId) throws IOException {

        return shardUploadService.uploadPart(file,partOrder,shardUploadId);
    }

    /**
     * 合并分片,完成上传
     *
     * @return
     */
    @PostMapping("/complete")
    public AjaxJson complete(@RequestBody String shardUploadId) throws IOException {



        return shardUploadService.complete(shardUploadId);
    }

    /**
     * 获取分片任务详细信息
     *
     * @param shardUploadId 分片任务id
     * @return
     */
    @PostMapping("/detail")
    public AjaxJson detail(@RequestBody String shardUploadId) {
        return shardUploadService.detail(shardUploadId);
    }
}

  1. service
package com.gwb.template.service.impl;

import cn.hutool.core.util.IdUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.gwb.template.entity.ShardUpload;

import com.gwb.template.entity.ShardUploadDetail;
import com.gwb.template.entity.ShardUploadPart;
import com.gwb.template.exception.ServiceException;
import com.gwb.template.mapper.ShardUploadMapper;
import com.gwb.template.mapper.ShardUploadPartMapper;
import com.gwb.template.service.ShardUploadService;
import com.gwb.template.util.AjaxJson;
import com.gwb.template.util.FileHashCalculator;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;

@Service
public class ShardUploadServiceImpl extends ServiceImpl<ShardUploadMapper, ShardUpload> implements ShardUploadService {
   public static final String path = "D:/luren/sharduploadGwb/";
   @Autowired
   ShardUploadPartMapper shardUploadPartMapper;

   @Override
   public AjaxJson init(ShardUpload shardUpload) {
       String uuid = IdUtil.fastSimpleUUID();
       shardUpload.setId(uuid);
       int insert = this.baseMapper.insert(shardUpload);
       if (insert >0){
           Path storagePath = Paths.get(path+uuid);
           // 确保存储路径存在
           if (!Files.exists(storagePath)) {
               try {
                   Files.createDirectories(storagePath);
               } catch (IOException e) {
                   e.printStackTrace();
               }
           }
           return AjaxJson.getSuccess("创建分片任务成功",uuid);
       }
       return AjaxJson.getError("创建分片任务失败");
   }

   @Override
   public AjaxJson uploadPart(MultipartFile file, String partOrder, String shardUploadId) {

       // 指定存储路径,例如D盘的uploads文件夹
       Path storagePath = Paths.get(path+shardUploadId);
       // 保存文件
       try {
           // 构建文件存储路径
           Path filePath = storagePath.resolve(Paths.get(partOrder ));
           file.transferTo(filePath);
           ShardUploadPart shardUploadPart = new ShardUploadPart();
           String uuid = IdUtil.fastSimpleUUID();
           shardUploadPart.setId(uuid);
           shardUploadPart.setFileFullPath(filePath.toString());
           shardUploadPart.setPartOrder(Integer.valueOf(partOrder));
           shardUploadPart.setShardUploadId(shardUploadId);
           shardUploadPartMapper.insert(shardUploadPart);
           System.out.println("File uploaded: " + filePath.toString());
       } catch (IOException e) {
           e.printStackTrace();
       }
       return AjaxJson.getSuccess("上传分片成功");
   }

   @Override
   public AjaxJson complete(String shardUploadId) {


       ShardUpload shardUpload = this.baseMapper.selectById(shardUploadId);
       String fileName = shardUpload.getFileName();

       String resultFilePath = path+shardUploadId+"/"+fileName;
       // 创建最终的文件对象,如果不存在则创建它
       File file = new File(resultFilePath);
       FileOutputStream fileOutputStream = null;
           // 打开输出流以写入文件,设置为追加模式
           try {
               fileOutputStream = FileUtils.openOutputStream(file, true);
               //获取所有分片文件
               List<ShardUploadPart> list = shardUploadPartMapper.selectList(Wrappers.lambdaQuery(ShardUploadPart.class).eq(ShardUploadPart::getShardUploadId, shardUploadId).orderByAsc(ShardUploadPart::getPartOrder));
               for (ShardUploadPart item : list) {
                   String fileFullPath = item.getFileFullPath();
                   File partFile = new File(fileFullPath);
                   FileInputStream partFileInputStream = null;
                   try {
                       // 打开输入流读取当前分片部分的内容
                       partFileInputStream = FileUtils.openInputStream(partFile);
                       // 将当前分片部分的内容复制到输出流中
                       IOUtils.copyLarge(partFileInputStream, fileOutputStream);
                   }finally {
                       // 确保关闭输入流
                       IOUtils.closeQuietly(partFileInputStream);
                   }
                   System.out.println("已经和成过的文件:"+fileFullPath);
                   // 删除已经处理过的分片部分文件
                   partFile.delete();
               };

           } catch (IOException e) {
               e.printStackTrace();
           }finally {
               // 确保关闭输出流
               IOUtils.closeQuietly(fileOutputStream);
           }
       //对比文件hash值
       try {
           if (!FileHashCalculator.calculateSHA256(file).equals(shardUpload.getHash())){
               throw new ServiceException("最终文件和分片文件hash值不一样");
           }
       }catch (Exception e){
           e.printStackTrace();
       }
       shardUpload.setFileFullPath(resultFilePath);
       this.baseMapper.updateById(shardUpload);
       return AjaxJson.getSuccess("分片文件合成成功");
   }

   @Override
   public AjaxJson detail(String shardUploadId) {
       ShardUpload shardUpload = this.baseMapper.selectById(shardUploadId);
       List<Integer> list =  shardUploadPartMapper.getPartDetail(shardUploadId);
       ShardUploadDetail shardUploadDetail = new ShardUploadDetail();
       BeanUtils.copyProperties(shardUpload,shardUploadDetail);
       shardUploadDetail.setPartOrderList(list);
       return AjaxJson.getSuccess("获取任务详情",shardUploadDetail);
   }
}

  1. mapper
@Mapper
public interface ShardUploadMapper extends BaseMapper<ShardUpload> {
   // 这里可以添加自定义的方法
}

@Mapper
public interface ShardUploadPartMapper extends BaseMapper<ShardUploadPart> {
   List<Integer> getPartDetail(@Param("shardUploadId") String shardUploadId);
   // 这里可以添加自定义的方法
}
  1. mapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.gwb.template.mapper.ShardUploadPartMapper">

   <!-- 这里可以定义自定义的SQL语句 -->

   <select id="getPartDetail" resultType="java.lang.Integer">
       select part_order from shard_upload_part where shard_upload_id = #{shardUploadId}
   </select>
</mapper>
  1. utils
在这里插入代码片
  1. maven
 <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-web</artifactId>
       </dependency>

       <!-- Mysql驱动包 -->
       <dependency>
           <groupId>mysql</groupId>
           <artifactId>mysql-connector-java</artifactId>
       </dependency>
       <!-- 阿里数据库连接池 -->
       <dependency>
           <groupId>com.alibaba</groupId>
           <artifactId>druid-spring-boot-starter</artifactId>
           <version>${druid.version}</version>
       </dependency>
       <!-- hutool工具包 -->
       <dependency>
           <groupId>cn.hutool</groupId>
           <artifactId>hutool-all</artifactId>
           <version>5.8.9</version>
       </dependency>
       <!--常用工具类 -->
       <dependency>
           <groupId>org.apache.commons</groupId>
           <artifactId>commons-lang3</artifactId>
       </dependency>
       <!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-boot-starter -->
       <dependency>
           <groupId>com.baomidou</groupId>
           <artifactId>mybatis-plus-boot-starter</artifactId>
           <version>3.5.7</version>
       </dependency>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-test</artifactId>
           <scope>test</scope>
       </dependency>

       <!-- Sa-Token 权限认证, 在线文档:https://sa-token.cc/ -->
       <dependency>
           <groupId>cn.dev33</groupId>
           <artifactId>sa-token-spring-boot-starter</artifactId>
           <version>${sa-token.version}</version>
       </dependency>
       <!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
       <dependency>
           <groupId>cn.dev33</groupId>
           <artifactId>sa-token-redis-jackson</artifactId>
           <version>1.38.0</version>
       </dependency>
       <!-- 提供Redis连接池 -->
       <dependency>
           <groupId>org.apache.commons</groupId>
           <artifactId>commons-pool2</artifactId>
       </dependency>
       <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
       <dependency>
           <groupId>org.projectlombok</groupId>
           <artifactId>lombok</artifactId>
           <version>1.18.24</version>
           <scope>provided</scope>
       </dependency>
       <!-- ui界面 -->
       <!--引入Knife4j的官方start包,该指南选择Spring Boot版本<3.0,开发者需要注意-->
       <dependency>
           <groupId>com.github.xiaoymin</groupId>
           <artifactId>knife4j-openapi3-spring-boot-starter</artifactId>
           <version>4.4.0</version>
       </dependency>


       <!-- fastjson2 -->
       <dependency>
           <groupId>com.alibaba.fastjson2</groupId>
           <artifactId>fastjson2</artifactId>
           <version>2.0.52</version>
       </dependency>
       <!-- 本地线程变量  可在子线程之间传输 -->
       <dependency>
           <groupId>com.alibaba</groupId>
           <artifactId>transmittable-thread-local</artifactId>
           <version>2.11.2</version> <!-- 请使用最新的稳定版本 -->
       </dependency>

       <!-- Spring Boot Starter AOP -->
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-aop</artifactId>
       </dependency>
       <!-- io常用工具类 -->
       <dependency>
           <groupId>commons-io</groupId>
           <artifactId>commons-io</artifactId>
           <version>2.11.0</version>
       </dependency>
  1. yaml
spring:
 servlet:
   multipart:
     max-file-size: 500MB
     max-request-size: 500MB
  1. sqlTable
-- shard_upload:分片任务表,shard_upload_part:分片文件表,两个表是1:n的关系
create table if not exists shard_upload(
   id varchar(64) primary key,
   file_name varchar(256) not null comment '文件名称',
   part_num int not null comment '分片数量',
   md5 varchar(512) comment '文件hash值',
   file_full_path varchar(512) comment '文件完整路径'
) comment = '分片上传任务表';


create table if not exists  shard_upload_part(
   id varchar(64) primary key,
   shard_upload_id varchar(64) not null comment '分片任务id(shard_upload.id)',
   part_order int not null comment '第几个分片,从1开始',
   file_full_path varchar(512) comment '文件完整路径',
   UNIQUE KEY `uq_part_order` (`shard_upload_id`,`part_order`)
) comment = '分片文件表,每个分片文件对应一条记录';
  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

诸葛博仌

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值