大文件分片上传

实现思路

         如标题所说,大文件分片上传,大文件上传可以使用分片的解决办法。

具体操作如下:

1.文件分片

2.创建分片上传任务

3.分片上传

4.合并分片

前端代码示例如下

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>

    <style>
        .container {
            width: 300px;
            margin: 100px auto;
        }
    </style>
</head>

<body>
    <div class="container">
        <input type="file" id="fileInput">
        <button id="uploadButton">Upload</button>
    </div>
</body>
<script>

    const uploadButton = document.getElementById('uploadButton').addEventListener('click', async () => {
        const fileInput = document.getElementById('fileInput');
        const file = fileInput.files[0];
        if (!file) {
            alert('Please select a file first.');
            return;
        }
        // 分割文件
        const chunkSize = 1024 * 1024 * 10; // 10MB
        const chunks = splitFile(file, chunkSize);
        console.log('chunks', chunks)

        // 预处理文件
        let preHandleResponse = await fetch("/largeFile/preHandle", {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({
                fileName: file.name,
                chunkSize: chunks.length
            })
        }
        )
        //
        if (!preHandleResponse.ok) {
            alert('文件预处理失败');
            return;
        }
        let preHandleResult = await preHandleResponse.json()

        const doChunks = chunks.map( (chunk, i) => {
            const formData = new FormData();
            formData.append('file', chunks[i]);
            formData.append('taskId', preHandleResult.id);
            formData.append('taskTag', preHandleResult.taskTag);
            formData.append('chunkNum', i);

            // 发送分片到服务器
            return fetch('/largeFile/uploadChunk', {
                method: 'POST',
                body: formData
            })
        })
        Promise.all(doChunks)
            .then(async () => {
                //发送合并请求
                let mergeResult = await fetch("/largeFile/merge", {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify({
                        taskId: preHandleResult.id,
                        taskTag: preHandleResult.taskTag
                    })
                }
                )
                if (mergeResult.ok) {
                    alert('合并成功')
                    return;
                }
            })
    })

    //分割文件
    function splitFile(file, chunkSize) {
        const chunks = [];
        let start = 0;
        while (start < file.size) {
            const end = Math.min(file.size, start + chunkSize);
            const chunk = file.slice(start, end);
            chunks.push(chunk);
            start = end;
        }
        return chunks;
    }

</script>

</html>

上面代码提供了一个文件上传的按钮,具体操作过程如下

1.在上传前将文件按10m每个切割,切割成多个

2.上传之前调用预上传接口(/largeFile/preHandle),获取本次文件处理的taskid(taskId),以上ID用以标记同一个大文件的不同小文件,和追踪整个上传任务

3.并行上传各个文件分片,调用上传文件接口(/largeFile/uploadChunk)

4.分片文件全部上传完成之后,调用合并分片接口(/largeFile/merge),完成操作

预处理接口

//DTO
@Data
public class LargeFilePreHandleDto {

    private Integer chunkSize;

    private String fileName;

    public LargeFileMergeTask toSave(){
      return  LargeFileMergeTask.builder()
                .taskTag(UUID.randomUUID().toString())
                .chunkSize(this.chunkSize)
              .fileName(this.fileName)
                .createTime(LocalDateTime.now())
                .done(false)
                .build();
    }
}

//Controller
  @PostMapping("/preHandle")
    public LargeFileMergeTask preHandle(@RequestBody LargeFilePreHandleDto preHandleDto) {
        if (preHandleDto.getChunkSize() == null || preHandleDto.getChunkSize() <= 0) {
            throw new RuntimeException("参数错误");
        }

        return largeFileHandleService.preHandle(preHandleDto);
    }

//Service
    @Transactional
    public LargeFileMergeTask preHandle(LargeFilePreHandleDto preHandleDto) {

        LargeFileMergeTask toSave = preHandleDto.toSave();
        largeFileMergeTaskService.save(toSave);


        return toSave;
    }

上传分片接口

//DTO
@Data
public class LargeFileUploadChunkDto {

    private Long taskId;

    private String taskTag;

    private int chunkNum;

    public LargeFileMergeSubFile toSave(){
        return LargeFileMergeSubFile
                .builder()
                .taskId(taskId)
                .taskTag(taskTag)
                .chunkNum(chunkNum)
                .createTime(LocalDateTime.now())
                .build();
    }
}


//Controller
    @PostMapping("/uploadChunk")
    public LargeFileMergeSubFile uploadChunk(LargeFileUploadChunkDto dto, @RequestPart("file") MultipartFile file) throws IOException {

        if(dto.getTaskId()==null|| dto.getTaskTag()==null|| StringUtils.isEmpty(dto.getTaskTag())|| dto.getChunkNum()<0){
            throw new RuntimeException("参数错误");
        }

        LargeFileMergeSubFile largeFileMergeSubFile = largeFileHandleService.uploadChunk(dto, file);
        return largeFileMergeSubFile;
    }

//Service
@Transactional
    public LargeFileMergeSubFile uploadChunk(LargeFileUploadChunkDto dto, MultipartFile file) throws IOException {
        InputStream inputStream = file.getInputStream();

        //TODO 保存磁盘,根据实际存储方式替换
        String filePath = dto.getTaskId() + dto.getTaskTag() + dto.getChunkNum();

        File tempFile = new File(fileConfig.getTempPath()+File.separator+filePath);
        OutputStream os = new FileOutputStream(tempFile);

        byte[] buffer = new byte[1024*8];
        int len = 0;
        while ((len = inputStream.read(buffer)) != -1) {
            os.write(buffer, 0, len);
        }
        os.close();
        //
        LargeFileMergeSubFile save = dto.toSave();
        save.setFilePath(filePath);
        largeFileMergeSubFileService.save(save);
        return save;
    }

合并分片逻辑

//DTO
@Data
public class LargeFileMergeDto {

    private Long taskId;

    private String taskTag;
}

//Controller
    @PostMapping("/merge")
    public String merge(@RequestBody LargeFileMergeDto largeFileMergeDto) throws IOException {

        if(largeFileMergeDto.getTaskId()==null|| StringUtils.isEmpty(largeFileMergeDto.getTaskTag())){
            throw new RuntimeException("参数错误");
        }

        largeFileHandleService.merge(largeFileMergeDto);
        return "merge success";
    }


//Service
@Transactional
    public void merge(LargeFileMergeDto dto) throws IOException {
        // TODO merge可能耗时较长,需要考虑和前端进行异步 交互

        LargeFileMergeTask task = largeFileMergeTaskService.checkExist(dto.getTaskId(), dto.getTaskTag());

        List<LargeFileMergeSubFile> subFiles = largeFileMergeSubFileService.listByTaskId(dto.getTaskId());
        logger.info("task chunk size={},subFilesSize={}",task.getChunkSize(),subFiles.size());
        if(task.getChunkSize()!=subFiles.size()){
            throw new RuntimeException("文件分片数量不匹配");
        }
        //TODO 从磁盘读取,根据实际存储方式替换

        //此次为内存处理,文件极大容易导致内存溢出,可以考虑利用磁盘处理
        ByteArrayOutputStream os=new ByteArrayOutputStream();
        for (LargeFileMergeSubFile subFile : subFiles) {

            File tempFile = new File(fileConfig.getTempPath()+File.separator+subFile.getFilePath());

            try (InputStream inputStream = new FileInputStream(tempFile)) {
                byte[] buffer = new byte[1024*8];
                int len = 0;
                while ((len = inputStream.read(buffer)) != -1) {
                    os.write(buffer, 0, len);

                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        //
        String filePath =fileConfig.getUploadPath()+File.separator+ UUID.randomUUID().toString()+task.getFileName();
        OutputStream outputStream = new FileOutputStream(filePath);
        os.writeTo(outputStream);
        outputStream.close();

        FileInfo fileInfo = FileInfo
                .builder()
                .fileName(task.getFileName())
                .filePath(filePath)
                .createTime(LocalDateTime.now())
                .build();
        fileService.save(fileInfo);

        largeFileMergeTaskService.done(task.getId());
        //TODO 可考虑定时清理
        largeFileMergeSubFileService.deleteByTaskId(task.getId());
        logger.info("merge success");
    }


    //TODO 定期处理无效任务记录。
}

完整的代码示例

        完整的代码示例地址在gitee,地址如下,觉得不错的麻烦给个star

        https://gitee.com/pushaftercommit/tool-set

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值