分片上传、断点续传和分片下载

在这里插入图片描述

<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,
    };
  },
  methods: {
    handleFileChange(event) {
      this.file = event.target.files[0];
      this.totalChunks = Math.ceil(this.file.size / this.chunkSize);
      this.uploadChunks();
    },
    async uploadChunks() {
      for (let index = 0; index < this.totalChunks; 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);

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

在这里插入图片描述

@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 uploadDir = new File(UPLOAD_DIR);
            if (!uploadDir.exists()) {
                uploadDir.mkdirs();
            }

            // 保存分片文件
            File chunkFile = new File(UPLOAD_DIR + fileName + "_" + index);
            chunk.transferTo(chunkFile);

            return ResponseEntity.ok("Chunk uploaded successfully");
        } catch (IOException e) {
            e.printStackTrace();
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body("Chunk upload failed");
        }
    }

    @PostMapping("/mergeChunks")
    public ResponseEntity<String> mergeChunks(@RequestParam("fileName") String fileName) {
        try {
            File mergedFile = new File(UPLOAD_DIR + fileName);
            try (FileOutputStream outputStream = new FileOutputStream(mergedFile, true)) {
                int index = 0;
                while (true) {
                    File chunkFile = new File(UPLOAD_DIR + fileName + "_" + index);
                    if (!chunkFile.exists()) {
                        break;
                    }
                    Files.copy(chunkFile.toPath(), outputStream);
                    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");
        }
    }


    // 如果传了分片数量(totalChunks)的话,上面俩接口可以写成一个
    @PostMapping("/uploadChunk")
    public ResponseEntity<String> uploadChunk(
            @RequestParam("chunk") MultipartFile chunk,
            @RequestParam("index") int index,  // 从0开始
            @RequestParam("totalChunks") int totalChunks,
            @RequestParam("fileName") String fileName) {

        try {
            File uploadDir = new File(UPLOAD_DIR);
            if (!uploadDir.exists()) {
                uploadDir.mkdirs();
            }

            // 保存分片文件
            File chunkFile = new File(UPLOAD_DIR + fileName + "_" + index);
            chunk.transferTo(chunkFile);  // 如果前端传的是Base64的文件加密字符串(而不是MultipartFile),那么这里就用流的方式把图片存到指定的位置,Base64的前缀要去掉,逗号后面才是实际的Base64编码数据(看另一篇博客)

            if (index == totalChunks - 1) {  // 必须在上面保存完分片文件后再判断,确保分片全部保存
                try (FileOutputStream fos = new FileOutputStream(UPLOAD_DIR + fileName)) {
                     for (int i = 0; i < totalChunks; i++) {
                          File tempFile = new File(UPLOAD_DIR + fileName + "_" + i);
                          Files.copy(tempFile.toPath(), fos);
                          tempFile.delete(); // Clean up temp file
                     }
                }
            }

            return ResponseEntity.ok("Chunk uploaded successfully");
        } catch (IOException e) {
            e.printStackTrace();
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body("Chunk upload failed");
        }
    }

}

在这里插入图片描述
在这里插入图片描述

<template>
  <div>
    <button @click="downloadFile">Download File</button>
  </div>
</template>

<script>
export default {
  methods: {
    async downloadFile() {
      let fileName = "largefile.zip"; // 需要下载的文件名
      let totalChunks = 10; // 文件分片数
      let chunkSize = 1024 * 1024; // 1MB 分片大小
      let blobParts = [];

      for (let index = 0; index < totalChunks; index++) {
        const response = await this.$axios.get(`/downloadChunk`, {
          params: { fileName, index, chunkSize },
          responseType: 'blob',
        });
        blobParts.push(response.data);
      }

      const blob = new Blob(blobParts);
      const link = document.createElement('a');
      link.href = URL.createObjectURL(blob);
      link.download = fileName;
      link.click();
    },
  },
};
</script>

// 也可以并发的调用接口
<template>
  <div>
    <button @click="downloadFile">Download File</button>
  </div>
</template>

<script>
export default {
  methods: {
    async downloadFile() {
      const fileName = "largefile.zip"; // 需要下载的文件名
      const totalChunks = 10; // 文件分片数
      const chunkSize = 1024 * 1024; // 1MB 分片大小
      const requests = [];

      // 发起所有文件块的请求
      for (let index = 0; index < totalChunks; index++) {
        const request = this.$axios.get(`/download/downloadChunk`, {
          params: { fileName, index, chunkSize },
          responseType: 'blob',
        });
        requests.push(request);
      }

      try {
        // 并行请求所有文件块
        const responses = await Promise.all(requests);

        // 处理所有文件块的响应
        const blobParts = responses.map(response => response.data);
        const blob = new Blob(blobParts);

        // 创建一个下载链接并触发下载
        const link = document.createElement('a');
        link.href = URL.createObjectURL(blob);
        link.download = fileName;
        link.click();
      } catch (error) {
        console.error('Error downloading file:', error);
        // 处理错误情况,例如显示错误消息给用户
      }
    },
  },
};
</script>

在这里插入图片描述

import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;

@RestController
@RequestMapping("/download")
public class FileDownloadController {

    private static final String UPLOAD_DIR = "uploads/";

    @GetMapping("/downloadChunk")
    public ResponseEntity<Resource> downloadChunk(
            @RequestParam("fileName") String fileName,
            @RequestParam("index") int index,
            @RequestParam("chunkSize") int chunkSize) {

        File file = new File(UPLOAD_DIR + fileName);
        if (!file.exists()) {
            return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
        }

        try (RandomAccessFile raf = new RandomAccessFile(file, "r")) {
            long start = index * chunkSize;
            long fileSize = file.length();
            long end = Math.min(start + chunkSize, fileSize);

            if (start >= fileSize) {
                return ResponseEntity.status(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE).build();
            }

            // Move file pointer to the start position
            raf.seek(start);

            // Calculate the length of the chunk to read
            int length = (int) (end - start);

            byte[] buffer = new byte[length];
            raf.readFully(buffer); // Read the exact number of bytes into the buffer

            InputStreamResource resource = new InputStreamResource(new ByteArrayInputStream(buffer));

            HttpHeaders headers = new HttpHeaders();
            headers.set(HttpHeaders.CONTENT_RANGE, "bytes " + start + "-" + (end - 1) + "/" + fileSize);
            headers.set(HttpHeaders.CONTENT_LENGTH, String.valueOf(length));
            headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);

            return ResponseEntity.ok()
                    .headers(headers)
                    .body(resource);

        } catch (IOException e) {
            e.printStackTrace();
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }
}

在这里插入图片描述
在这里插入图片描述

import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.ByteArrayInputStream;
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;

@RestController
@RequestMapping("/download")
public class FileDownloadController {

    private static final String UPLOAD_DIR = "uploads/";

    @GetMapping("/downloadChunk")
    public ResponseEntity<Resource> downloadChunk(
            @RequestParam("fileName") String fileName,
            @RequestParam("index") int index,
            @RequestParam("chunkSize") int chunkSize) {

        File file = new File(UPLOAD_DIR + fileName);
        if (!file.exists()) {
            return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
        }

        try (RandomAccessFile raf = new RandomAccessFile(file, "r");
             FileChannel fileChannel = raf.getChannel()) {

            long start = index * chunkSize;
            long fileSize = file.length();
            long end = Math.min(start + chunkSize, fileSize);

            if (start >= fileSize) {
                return ResponseEntity.status(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE).build();
            }

            // Calculate the size of the chunk to map
            long size = end - start;

            // Map the file chunk into memory
            MappedByteBuffer buffer = fileChannel.map(MapMode.READ_ONLY, start, size);

            // Create a byte array to hold the mapped content
            byte[] chunkData = new byte[(int) size];
            buffer.get(chunkData);  // Transfer the mapped content to the byte array

            // Create InputStream from byte array
            InputStreamResource resource = new InputStreamResource(new ByteArrayInputStream(chunkData));

            // Set headers
            HttpHeaders headers = new HttpHeaders();
            headers.set(HttpHeaders.CONTENT_RANGE, "bytes " + start + "-" + (end - 1) + "/" + fileSize);
            headers.set(HttpHeaders.CONTENT_LENGTH, String.valueOf(size));
            headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);

            return ResponseEntity.ok()
                    .headers(headers)
                    .body(resource);

        } catch (IOException e) {
            e.printStackTrace();
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }
}

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值