大文件分片上传和下载

大文件分片上传

文件上传到本地

Java代码

配置文件修改文件上传大小限制,和指定文件上传目录

server:
  port: 8001

spring:
  application:
    name: service-edu 
  servlet:
    multipart:
      enabled: true #默认支持文件上传
      max-file-size: -1 #不做限制
      max-request-size: -1 #不做限制

upload:
  directory: /apps/files

添加请求接口地址

@Autowired
FileServiceImpl fileService;

@ApiOperation("分片上传")
@PostMapping("/splitUpload")
public Response<Boolean> uploadFileByCondition(MultipartFile file, int chunkNumber, int totalChunks){
    return Response.rspData(fileService.uploadFileByCondition(file,chunkNumber,totalChunks));
}

实现uploadFileByCondition方法

@Value("${upload.directory}")
private String uploadDirectory;

/**
     * 文件上传
     * @param file
     * @param chunkNumber
     * @param totalChunks
     * @return
     */
public Boolean uploadFileByCondition(MultipartFile file, int chunkNumber, int totalChunks) {
    // 创建目录
    File directory = new File(uploadDirectory);
    if (!directory.exists()) {
        directory.mkdirs();
    }
    String fileName = file.getOriginalFilename();
    try {
        // 将分片保存到磁盘
        String filePath = uploadDirectory + fileName + "_part_" + chunkNumber;
        Path path = Paths.get(filePath);
        Files.copy(file.getInputStream(), path, StandardCopyOption.REPLACE_EXISTING);
        // 判断是否已经接收到所有分片
        if (chunkNumber == totalChunks) {
            // 所有分片已接收,开始合并文件
            mergeFile(uploadDirectory, fileName, totalChunks);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    return true;
}

/**
     * 合并文件
     * @param filename
     * @param totalChunks
     */
private void mergeFile(String samplePath, String filename, int totalChunks) {
    String mergedFilePath = samplePath + filename;
    Path mergedPath = Paths.get(mergedFilePath);
    // 逐个合并分片
    for (int i = 1; i <= totalChunks; i++) {
        String chunkFilePath = samplePath + filename + "_part_" + i;
        Path chunkPath = Paths.get(chunkFilePath);
        try {
            Files.write(mergedPath, Files.readAllBytes(chunkPath), StandardOpenOption.CREATE, StandardOpenOption.APPEND);
            Files.delete(chunkPath); // 删除已合并的分片
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
前端代码

发起请求

<template>
  <div>
    <el-button type="primary" @click="selectFile">上传</el-button>
  </div>
</template>

<script>
import {splitUploadFile} from "@/api/file";

export default {
  data() {
    return {}
  },
  methods: {
    selectFile() {
      let fileInput = document.createElement('input')
      fileInput.type = 'file'
      fileInput.removeAttribute('value')
      // 添加change事件监听器
      fileInput.addEventListener('change', (event) => {
        if (event) {
          const selectedFile = event.target.files[0]
          // 处理选中的文件
          this.handleSelectedFile(selectedFile)
          // 上传完成后删除元素
          fileInput.remove()
        }
      })
      // 触发文件选择对话框
      fileInput.click()
    },
    handleSelectedFile(file) {
      splitUploadFile(file).then(() => {
        this.$message.success('上传成功')
      })
    },
  }
}
</script>

js逻辑

/**
 * 接收文件,进行分片上传
 * @param file
 */
export function splitUploadFile(file) {
    // eslint-disable-next-line no-async-promise-executor
    return new Promise(async (resolve, reject) => {
        const chunkSize = 1024 * 1024 * 2 // 分片大小为2MB
        const endpoint = '/splitUpload' // 替换为你的上传接口地址
        let progress = 0 // 上传进度
        const chunks = createChunks(file, chunkSize) // 存放文件每片信息
        const totalChunks = chunks.length // 总块数
        for (let i = 1; i <= totalChunks; i++) {
            const formData = new FormData()
            formData.append('file', chunks[i - 1], file.name)
            formData.append('chunkNumber', i)
            formData.append('totalChunks', totalChunks)
            try {
                await request.post(endpoint, formData) // 发送分片到服务器
                progress = Math.round(((i + 1) / totalChunks) * 100)
                console.log(progress)
            } catch (error) {
                // 失败后退出循环
                reject(false)
                break
            }
        }
        resolve(true)
    })
}

/**
 * 对文件进行分片,并放在一个数组中返回
 * @param file
 * @param chunkSize
 * @returns {[]}
 */
function createChunks(file, chunkSize = 2 * 1024 * 1024) {
    const chunks = []
    const fileSize = file.size
    let currentByte = 0
    while (currentByte < fileSize) {
        const chunk = file.slice(currentByte, currentByte + chunkSize)
        chunks.push(chunk)
        currentByte += chunkSize
    }
    return chunks
}
上传效果

在这里插入图片描述

分片上传到阿里云并添加进度条

java代码

安装依赖

<!--阿里云OSS依赖-->
<dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-sdk-oss</artifactId>
    <version>3.15.1</version>
</dependency>
<!--websocket-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

添加配置文件

oss:
  endpoint: oss-cn-hangzhou.aliyuncs.com
  accessKeyId: 这里换成你自己的
  accessKeySecret: 这里换成你自己的
  bucketName: szx-bucket1
  prefix: filttest/

添加请求方法

@ApiOperation("分片上传到OSS")
@PostMapping("/ossUpload")
public Response<String> ossUpload(MultipartFile file){
    return Response.rspData(fileService.ossUpload(file));
}

实现ossUpload方法

@Value("${oss.endpoint}")
private String endpoint;

@Value("${oss.accessKeyId}")
private String accessKeyId;

@Value("${oss.accessKeySecret}")
private String accessKeySecret;

@Value("${oss.bucketName}")
private String bucketName;

@Value("${oss.prefix}")
private String prefix;

/**
 * 创建OssClient
 * @return
 */
public OSS createOssClient() {
    // 创建OSSClient实例。
    return new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
}

/**
 * 文件上传
 * @param file
 * @return
 */
public String ossUpload(MultipartFile file) {
    try {
        OSS ossClient = createOssClient();
        // 创建文件名,例如 filttest/exampleobject.txt";
        String objectName = prefix + file.getOriginalFilename();
        // 创建InitiateMultipartUploadRequest对象。
        InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest(bucketName, objectName);
        // 初始化分片。
        InitiateMultipartUploadResult upresult = ossClient.initiateMultipartUpload(request);
        // 返回uploadId,它是分片上传事件的唯一标识。您可以根据该uploadId发起相关的操作,例如取消分片上传、查询分片上传等。
        String uploadId = upresult.getUploadId();
        // partETags是PartETag的集合。PartETag由分片的ETag和分片号组成。
        List<PartETag> partETags = new ArrayList<PartETag>();
        // 每个分片的大小,用于计算文件有多少个分片。单位为字节。
        final long partSize = 2 * 1024 * 1024L;   //2 MB。
        // 根据上传的数据大小计算分片数。
        long fileLength = file.getSize();
        int partCount = (int) (fileLength / partSize);
        if (fileLength % partSize != 0) {
            partCount++;
        }

        // 添加上传进度器
        UploadProgressListener uploadProgressListener = new UploadProgressListener(fileLength);

        // 遍历分片上传。
        for (int i = 0; i < partCount; i++) {
            long startPos = i * partSize;
            long curPartSize = (i + 1 == partCount) ? (fileLength - startPos) : partSize;
            UploadPartRequest uploadPartRequest = new UploadPartRequest();
            uploadPartRequest.setBucketName(bucketName);
            uploadPartRequest.setKey(objectName);
            uploadPartRequest.setUploadId(uploadId);

            // 设置上传的分片流。并通过InputStream.skip()方法跳过指定数据。
            final InputStream instream = file.getInputStream();
            instream.skip(startPos);
            uploadPartRequest.setInputStream(instream);
            // 设置分片大小。除了最后一个分片没有大小限制,其他的分片最小为100 KB。
            uploadPartRequest.setPartSize(curPartSize);
            // 设置分片号。每一个上传的分片都有一个分片号,取值范围是1~10000,如果超出此范围,OSS将返回InvalidArgument错误码。
            uploadPartRequest.setPartNumber(i + 1);
            // 添加上传进度监听器
            uploadPartRequest.setProgressListener(uploadProgressListener);
            // 每个分片不需要按顺序上传,甚至可以在不同客户端上传,OSS会按照分片号排序组成完整的文件。
            UploadPartResult uploadPartResult = ossClient.uploadPart(uploadPartRequest);
            // 每次上传分片之后,OSS的返回结果包含PartETag。PartETag将被保存在partETags中。
            partETags.add(uploadPartResult.getPartETag());
        }
        // 创建CompleteMultipartUploadRequest对象。
        // 在执行完成分片上传操作时,需要提供所有有效的partETags。OSS收到提交的partETags后,会逐一验证每个分片的有效性。当所有的数据分片验证通过后,OSS将把这些分片组合成一个完整的文件。
        CompleteMultipartUploadRequest completeMultipartUploadRequest =
                new CompleteMultipartUploadRequest(bucketName, objectName, uploadId, partETags);
        // 完成分片上传。
        ossClient.completeMultipartUpload(completeMultipartUploadRequest);
        // 关闭连接
        ossClient.shutdown();
        // 拼接文件线上地址返回给前端
        return "http://" + bucketName + "." + endpoint + "/" + objectName;
    } catch (IOException e) {
        e.printStackTrace();
    }
    return "";
}

上面代码中用到的 UploadProgressListener

package com.szx.java.listener;

import com.aliyun.oss.event.ProgressEvent;
import com.aliyun.oss.event.ProgressEventType;
import com.aliyun.oss.event.ProgressListener;
import com.szx.java.handler.MyWebSocketHandler;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;

import java.io.IOException;

/**
 * 上载进度侦听器
 * @author songzx
 * @create 2023-06-09 8:50
 */
public class UploadProgressListener implements ProgressListener {
    private long bytesWritten = 0;
    private long totalBytes;
    private boolean succeed = false;
    // 添加WebSocket实时告诉前端上传进度
    WebSocketSession session = null;

    public UploadProgressListener(long totalBytes) {
        this.totalBytes = totalBytes;
        this.session = MyWebSocketHandler.getSession();
    }

    @Override
    public void progressChanged(ProgressEvent progressEvent) {
        long bytes = progressEvent.getBytes();
        ProgressEventType eventType = progressEvent.getEventType();

        switch (eventType) {
            case REQUEST_BYTE_TRANSFER_EVENT:
                this.bytesWritten += bytes;
                int percent = (int) (this.bytesWritten * 100.0 / this.totalBytes);
                if (session != null && session.isOpen()) {
                    try {
                        // 实时返回上传进度
                        session.sendMessage(new TextMessage( percent + "%"));
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                break;
            case TRANSFER_COMPLETED_EVENT:
                try {
                    this.succeed = true;
                    this.session.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                break;
            case TRANSFER_FAILED_EVENT:
                try {
                    this.succeed = false;
                    this.session.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                break;
            default:
                break;
        }
    }

    public boolean isSucceed() {
        return succeed;
    }
}

上传进度监听器中用到的 MyWebSocketHandler 代码

package com.szx.java.handler;

import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

/**
 * @author songzx
 * @create 2023-06-08 18:33
 */
public class MyWebSocketHandler extends TextWebSocketHandler{
    // 静态变量,用于存储WebSocketSession对象
    private static WebSocketSession session;

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        // 当WebSocket连接建立时,将WebSocketSession对象存储起来,以便后续使用
        // 这里可以使用自己的方式将session对象存储,例如将其放入Map中,或者存储到用户的会话中
        // 示例中使用静态变量存储WebSocketSession对象
        MyWebSocketHandler.session = session;
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        // 处理WebSocket文本消息
    }
    // 获取WebSocketSession对象的静态方法
    public static WebSocketSession getSession() {
        return session;
    }
}

添加配置类,启用 WebSocket

package com.szx.java.config;

import com.szx.java.handler.MyWebSocketHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

/**
 * @author songzx
 * @create 2023-06-09 8:22
 */
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) {
        webSocketHandlerRegistry.addHandler(new MyWebSocketHandler(), "/websocket")
                .setAllowedOrigins("*");
    }
}
前端代码
<template>
  <div>
    <el-button type="primary" @click="selectFile">分片上传到OSS</el-button>

    <div>
      <p>上传进度:{{ progressMessage }}</p>
      <p v-if="fileUrl">文件地址:{{ fileUrl }}</p>
    </div>
  </div>
</template>

<script>
import {fileUploadOssFun} from "@/api/file";

export default {
  data() {
    return {
      progressMessage: "",
      fileUrl: ""
    }
  },
  methods: {
    // 选择文件
    selectFile() {
      let fileInput = document.createElement('input')
      fileInput.type = 'file'
      fileInput.removeAttribute('value')
      // 添加change事件监听器
      fileInput.addEventListener('change', (event) => {
        if (event) {
          const selectedFile = event.target.files[0]
          // 处理选中的文件
          this.fileUploadOss(selectedFile)
          // 上传完成后删除元素
          fileInput.remove()
        }
      })
      // 触发文件选择对话框
      fileInput.click()
    },
    // 上传到阿里云
    fileUploadOss(file) {
      const that = this;
      // 连接一个WebSocket,实时监听上传进度
      const socket = new WebSocket('ws://127.0.0.1:8001/websocket');
      socket.onmessage = function (event) {
        that.progressMessage = event.data;
      };
      fileUploadOssFun(file).then((res) => {
        that.fileUrl = res.data
        that.$message.success('上传成功')
      })
    }
  }
}
</script>

fileUploadOssFun

/**
 * 上传文件到OSS
 * @param file
 * @returns {*}
 */
export function fileUploadOssFun(file) {
    let data = new FormData();
    data.append("file", file)
    return request({
        url: '/ossUpload',
        method: 'post',
        data
    })
}
上传效果

VeryCapture_20230609102657

文件下载

Java代码

配置文件

这里配置的地址为绝对地址,以 / 开头,表示绝对地址,指向当前项目运行地址的根目录

例如项目是在 D:/test/项目地址,那么这里directory就会指向 D:/apps/files

upload:
  directory: /apps/files/

下载方法实现

@Value("${upload.directory}")
private String uploadDirectory;

/**
 * 根据文名下载文件
 *
 * @param path
 * @param response
 */
public void downloadFileByPath(String name, HttpServletResponse response) {
  try {
    File file = new File(uploadDirectory + name);
    String filename = file.getName();
    FileInputStream fileInputStream = new FileInputStream(file);
    InputStream fis = new BufferedInputStream(fileInputStream);
    byte[] buffer = new byte[fis.available()];
    fis.read(buffer);
    fis.close();
    response.reset();
    response.setCharacterEncoding("UTF-8");
    response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8"));
    response.addHeader("Content-Length", "" + file.length());
    OutputStream outputStream = new BufferedOutputStream(response.getOutputStream());
    response.setContentType("application/octet-stream");
    outputStream.write(buffer);
    outputStream.flush();
  } catch (IOException e) {
    e.printStackTrace();
  }
}
前端代码
testDown() {
  const link = document.createElement('a')
  link.href = buildRequestURL('szxtest/downloadFileByPath', {
    path: '1.rar', // 要下载的文件名
  })
  link.download = '1.rar'
  link.click()
},

使用这种下载方式会触发浏览器的默认下载行为,不占用浏览器内存

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值