大文件分片上传
文件上传到本地
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
})
}
上传效果
文件下载
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()
},
使用这种下载方式会触发浏览器的默认下载行为,不占用浏览器内存