java
复制代码
package org.example.controller;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
@RestController
@RequestMapping("/api/test")
public class TestController {
private Map<String, AtomicInteger> CHUNK_TOTAL_MAP = new ConcurrentHashMap<>();
/**
* 大文件分片上传
* 思路: 1. 将大文件拆分成多个小文件多次上传, 全部上传完成后, 将这些分片合并成原始文件
* 2. 合并过程中需要按顺序合并, 所以需要 chunkIndex 定位顺序
* 3. 需要知道什么时候合并, 所以需要 totalChunks 确定分片总数, 后端判断 上传数量(后端记录) == 分片总数
* 4. fileHash作为唯一key来标识文件
* 5. 合并完成后删除分片, 清空缓存
* @param multipartFile 文件片段
* @param chunkIndex 分片顺序编号, 从0开始
* @param totalChunks 分片总数
* @param fileHash 原文件hash值, 作为唯一key
* @param fileName 合并后的文件名
* @param endWith 合并后的文件后缀
* @return
*/
@PostMapping("/upload")
public ResponseEntity<Object> upload(@RequestPart MultipartFile multipartFile,
@RequestParam int chunkIndex,
@RequestParam int totalChunks,
@RequestParam String fileHash,
@RequestParam String fileName,
@RequestParam String endWith){
//临时存储分片路径
String chunkDir = "D:/file/upload/"+fileHash;
String chunkPath = "/chunk_" + chunkIndex;
//创建目录
try {
Files.createDirectories(Path.of(chunkDir));
} catch (IOException e) {
throw new RuntimeException(e);
}
//保存分片
writeToChunk(multipartFile, chunkPath);
//更新上传分片数量
CHUNK_TOTAL_MAP.putIfAbsent(fileHash, new AtomicInteger(0));
int uploadChunks = CHUNK_TOTAL_MAP.get(fileHash).incrementAndGet();
//判断是否开始合并
if(uploadChunks == totalChunks){
mergeChunk(chunkDir, fileName, totalChunks, endWith);
CHUNK_TOTAL_MAP.remove(fileHash); //清空缓存
}
return ResponseEntity.ok(CHUNK_TOTAL_MAP);
}
/**
* 合并分片
* @param chunkDir 文件保存路径
* @param fileName 文件名
* @param totalChunks 分片总数
* @param endWith 合并后的文件后缀
*/
private void mergeChunk(String chunkDir, String fileName, int totalChunks, String endWith) {
String filePath = chunkDir + "/" +fileName + "." + endWith;
try (FileChannel outChannel = (FileChannel) Files.newByteChannel(Path.of(filePath), StandardOpenOption.CREATE, StandardOpenOption.APPEND)){
for (int i = 0; i < totalChunks; i++) {
//读取分片
String chunkPath = "/chunk_" + i;
try(FileChannel inChannel = (FileChannel) Files.newByteChannel(Path.of(chunkPath), StandardOpenOption.READ)){
//合并分片到主文件
outChannel.transferFrom(inChannel,outChannel.size(), inChannel.size());
//删除分片
Files.deleteIfExists(Path.of(chunkPath));
}catch (IOException e) {
throw new RuntimeException(e);
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 保存分片
* @param multipartFile
* @param chunkPath
*/
private void writeToChunk(MultipartFile multipartFile, String chunkPath) {
try (FileChannel outChannel = (FileChannel) Files.newByteChannel(Path.of(chunkPath), StandardOpenOption.CREATE, StandardOpenOption.WRITE);
InputStream inputStream = multipartFile.getInputStream();
ReadableByteChannel inChannel = Channels.newChannel(inputStream);){
long chunkSize = multipartFile.getSize();
outChannel.transferFrom(inChannel,0, chunkSize);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}