MappedByteBuffer实现分片上传和断点续传

在这里插入图片描述

<template>
  <div>
    <input type="file" @change="handleFileChange" />
    <progress :value="progress" max="100">{{ progress }}%</progress>
  </div>
</template>

<script>
export default {
  data() {
    return {
      file: null,
      chunkSize: 1024 * 1024, // 1MB 分片大小
      totalChunks: 0,
      uploadedChunks: 0,
      progress: 0,
      uploadedChunkIndices: [], // 记录已上传的分片索引
    };
  },
  methods: {
    handleFileChange(event) {
      this.file = event.target.files[0];
      this.totalChunks = Math.ceil(this.file.size / this.chunkSize);
      this.getUploadedChunkIndices();
    },
    async getUploadedChunkIndices() {
      try {
        const response = await this.$axios.get('/getUploadedChunks', {
          params: { fileName: this.file.name },
        });
        this.uploadedChunkIndices = response.data;
        this.uploadChunks();
      } catch (error) {
        console.error('Error fetching uploaded chunks', error);
      }
    },
    async uploadChunks() {
      for (let index = 0; index < this.totalChunks; index++) {
        if (!this.uploadedChunkIndices.includes(index)) {
          const chunk = this.file.slice(
            index * this.chunkSize,
            (index + 1) * this.chunkSize
          );
          const formData = new FormData();
          formData.append('chunk', chunk);
          formData.append('index', index);
          formData.append('fileName', this.file.name);

          try {
            await this.$axios.post('/uploadChunk', formData);
            this.uploadedChunkIndices.push(index);
            this.uploadedChunks++;
            this.progress = Math.floor(
              (this.uploadedChunks / this.totalChunks) * 100
            );
          } catch (error) {
            console.error('Error uploading chunk', error);
          }
        }
      }
    },
  },
};
</script>

在这里插入图片描述

import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;
import java.util.HashSet;
import java.util.Set;

@RestController
@RequestMapping("/upload")
public class FileUploadController {

    private static final String UPLOAD_DIR = "uploads/";

    @PostMapping("/uploadChunk")
    public ResponseEntity<String> uploadChunk(
            @RequestParam("chunk") MultipartFile chunk,
            @RequestParam("index") int index,
            @RequestParam("fileName") String fileName) {

        try {
            File file = new File(UPLOAD_DIR + fileName);
            if (!file.exists()) {
                file.createNewFile();
            }

            try (RandomAccessFile raf = new RandomAccessFile(file, "rw");
                 FileChannel channel = raf.getChannel()) {

                long position = index * chunk.getSize();  // index从0开始
                //channel.position(position);

                try (MappedByteBuffer buffer = channel.map(MapMode.READ_WRITE, position, chunk.getSize())) {
                    buffer.put(chunk.getBytes());
                }
            }
            return ResponseEntity.ok("Chunk uploaded successfully");
        } catch (IOException e) {
            e.printStackTrace();
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body("Chunk upload failed");
        }
    }

    @GetMapping("/getUploadedChunks")
    public ResponseEntity<Set<Integer>> getUploadedChunks(@RequestParam("fileName") String fileName) {
        File file = new File(UPLOAD_DIR + fileName);
        Set<Integer> uploadedChunks = new HashSet<>();
        if (file.exists()) {
            long fileSize = file.length();
            int chunkSize = 1024 * 1024; // 1MB
            int totalChunks = (int) Math.ceil((double) fileSize / chunkSize);
            for (int i = 0; i < totalChunks; i++) {
                uploadedChunks.add(i);
            }
        }
        return ResponseEntity.ok(uploadedChunks);
    }
}

使用了 MappedByteBuffer 直接在目标文件的指定位置写入每个分块(chunk)。这种方式的确可以避免在上传文件的过程中额外进行文件合并操作。因为每个分块(chunk)都是在文件的正确位置上写入,所以上传结束后,整个文件已经是完整的,不需要再额外进行合并。

优化:
1、同步方法: uploadChunk() 方法被标记为 synchronized,确保同一时间只能有一个线程写入同一个文件的同一位置,防止并发上传同一文件的不同分片时造成写入冲突。

public synchronized ResponseEntity<String> uploadChunk()

2、更精确的上传进度:当前通过文件大小来判断已上传的分片,可以进一步改进,在上传成功后将已上传的分片索引存储在持久化存储中(redis中),避免重复上传。
(1)getUploadedChunks() 接口,直接返回redis的数据;
(2)uploadChunk()接口,判断传过来的index在redis中是否存在,存在直接return,不存在加入redis。

不需要进行合并!!!!!,以下代码只是演示下怎么合并
在这里插入图片描述

/*import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

@RestController
@RequestMapping("/merge")
public class FileMergeController {

    private static final String UPLOAD_DIR = "uploads/";

    @PostMapping("/mergeChunks")
    public ResponseEntity<String> mergeChunks(@RequestParam("fileName") String fileName) {
        File file = new File(UPLOAD_DIR + fileName);
        if (file.exists()) {
            try (FileOutputStream fos = new FileOutputStream(file)) {
                int index = 0;
                File chunkFile;
                while ((chunkFile = new File(UPLOAD_DIR + fileName + "_" + index)).exists()) {
                    Files.copy(chunkFile.toPath(), fos);
                    chunkFile.delete();
                    index++;
                }
                return ResponseEntity.ok("File merged successfully");
            } catch (IOException e) {
                e.printStackTrace();
                return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                        .body("File merge failed");
            }
        }
        return ResponseEntity.badRequest().body("File does not exist");
    }
}*/

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值