基于vue3和spring boot实现大文件上传

前端(Vue 3)

1. 文件选择与分片上传

首先,我们需要一个文件选择器来选择要上传的文件,并将其分割成多个小块(分片)进行上传。我们还需要记录每个分片的上传状态以便支持断点续传。

<template><div><!-- 文件选择输入框 --><form @submit.prevent="handleSubmit"><input type="file" @change="handleFileChange" ref="fileInput" /><!-- 文件预览信息 --><div v-if="selectedFile"><p>Selected File: {{ selectedFile.name }} ({{ formatBytes(selectedFile.size) }})</p><!-- 进度条显示 --><progress :value="uploadProgress" max="100"></progress></div><!-- 提交按钮 --><button type="submit">Submit</button></form></div>
</template><script>
import { ref, onMounted } from 'vue';
import axios from 'axios';export default {setup() {const fileInput = ref(null);const selectedFile = ref(null);const uploadProgress = ref(0);const uploadedChunks = ref(new Set());const CHUNK_SIZE = 5 * 1024 * 1024; // 每个分片大小为5MB/*** 处理文件选择事件* @param event 文件选择事件*/const handleFileChange = (event) => {selectedFile.value = event.target.files[0];};/*** 格式化字节为可读格式* @param bytes 字节数* @returns {string} 可读格式的文件大小*/const formatBytes = (bytes) => {if (bytes === 0) return '0 Bytes';const k = 1024;const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];const i = Math.floor(Math.log(bytes) / Math.log(k));return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];};/*** 检查某个分片是否已经上传* @param fileName 文件名* @param chunkNumber 分片编号* @returns {Promise<boolean>}*/const isChunkUploaded = async (fileName, chunkNumber) => {try {const response = await axios.get(`/check-chunk-uploaded?fileName=${fileName}&chunkNumber=${chunkNumber}`);return response.data.uploaded;} catch (error) {console.error('Error checking chunk upload status:', error);return false;}};/*** 上传文件分片* @param chunk 分片数据* @param fileName 文件名* @param chunkNumber 当前分片编号* @param totalChunks 总分片数* @returns {Promise<void>}*/const uploadChunk = async (chunk, fileName, chunkNumber, totalChunks) => {const formData = new FormData();formData.append('file', new Blob([chunk]), fileName);formData.append('chunkNumber', chunkNumber);formData.append('totalChunks', totalChunks);try {const response = await axios.post('/upload', formData, {onUploadProgress: (progressEvent) => {const percentage = (chunkNumber / totalChunks) * 100 + (progressEvent.loaded / progressEvent.total) * (100 / totalChunks);uploadProgress.value = Math.min(percentage, 100);}});if (response.status !== 200) {console.error('Failed to upload chunk');} else {uploadedChunks.value.add(chunkNumber); // 记录已上传的分片}} catch (error) {console.error('Error uploading chunk:', error);}};/*** 合并所有分片* @param fileName 文件名* @param totalChunks 总分片数* @returns {Promise<void>}*/const mergeChunks = async (fileName, totalChunks) => {try {const response = await axios.post('/merge', { fileName, totalChunks });if (response.status === 200) {console.log('All chunks merged successfully.');} else {console.error('Failed to merge chunks.');}} catch (error) {console.error('Error merging chunks:', error);}};/*** 表单提交处理函数* @returns {Promise<void>}*/const handleSubmit = async () => {if (!selectedFile.value) return;const file = selectedFile.value;const totalChunks = Math.ceil(file.size / CHUNK_SIZE);let start = 0;let chunkNumber = 1;while (start < file.size) {const end = Math.min(start + CHUNK_SIZE, file.size);const chunk = file.slice(start, end);if (!(await isChunkUploaded(file.name, chunkNumber))) {await uploadChunk(chunk, file.name, chunkNumber, totalChunks);}start = end;chunkNumber++;}await mergeChunks(file.name, totalChunks);uploadProgress.value = 100;};/*** 加载保存的上传进度*/const loadSavedProgress = () => {const savedProgress = JSON.parse(localStorage.getItem('uploadProgress'));if (savedProgress && savedProgress.fileName === selectedFile.value?.name) {uploadedChunks.value = new Set(savedProgress.uploadedChunks);uploadProgress.value = savedProgress.progress;}};/*** 定时保存上传进度*/const saveUploadProgress = () => {localStorage.setItem('uploadProgress', JSON.stringify({fileName: selectedFile.value?.name,uploadedChunks: Array.from(uploadedChunks.value),progress: uploadProgress.value,}));};onMounted(() => {loadSavedProgress(); // 页面加载时恢复上传进度setInterval(saveUploadProgress, 5000); // 每5秒保存一次上传进度});return {fileInput,selectedFile,uploadProgress,handleFileChange,formatBytes,handleSubmit,};},
};
</script>

后端(Spring Boot + MinIO)

1. 添加依赖

首先,在 pom.xml 中添加 MinIO 的 Maven 依赖:

<dependency><groupId>io.minio</groupId><artifactId>minio</artifactId><version>8.4.3</version>
</dependency>
2. 配置 MinIO 客户端

在 Spring Boot 应用中配置 MinIO 客户端:

import io.minio.MinioClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class MinioConfig {@Beanpublic MinioClient minioClient() {return MinioClient.builder().endpoint("http://localhost:9000") // MinIO 服务地址.credentials("YOUR-ACCESS-KEY", "YOUR-SECRET-KEY") // MinIO 凭证.build();}
}
3. 更新控制器逻辑

更新控制器逻辑以使用 MinIO 客户端上传和合并文件,并提供接口检查分片是否已上传:

import io.minio.*;
import io.minio.errors.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.ConcurrentHashMap;@RestController
public class FileUploadController {private static final String BUCKET_NAME = "your-bucket-name"; // 替换为你的桶名称private static final ConcurrentHashMap<String, Integer> chunkCountMap = new ConcurrentHashMap<>();@Autowiredprivate MinioClient minioClient;@PostMapping("/upload")public String uploadFile(@RequestParam("file") MultipartFile file,@RequestParam("chunkNumber") int chunkNumber,@RequestParam("totalChunks") int totalChunks) throws IOException, ServerException, InsufficientDataException, ErrorResponseException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {String fileName = file.getOriginalFilename();// 将分片上传到 MinIOminioClient.putObject(PutObjectArgs.builder().bucket(BUCKET_NAME).object(fileName + "_part_" + chunkNumber).stream(file.getInputStream(), file.getSize(), -1).contentType(file.getContentType()).build());// 使用ConcurrentHashMap记录每个文件已上传的分片数量chunkCountMap.merge(fileName, 1, Integer::sum);return "Uploaded chunk " + chunkNumber + " of " + totalChunks;}@GetMapping("/check-chunk-uploaded")public ResponseEntity<?> checkChunkUploaded(@RequestParam String fileName, @RequestParam int chunkNumber) throws IOException, ServerException, InsufficientDataException, ErrorResponseException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {boolean exists = minioClient.statObject(StatObjectArgs.builder().bucket(BUCKET_NAME).object(fileName + "_part_" + chunkNumber).build()) != null;return ResponseEntity.ok(new HashMap<String, Boolean>() {{put("uploaded", exists);}});}@PostMapping("/merge")public String mergeChunks(@RequestBody MergeRequest request) throws IOException, ServerException, InsufficientDataException, ErrorResponseException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {String fileName = request.getFileName();int totalChunks = request.getTotalChunks();// 创建临时文件用于合并Path tempFilePath = Paths.get(fileName);OutputStream outputStream = new FileOutputStream(tempFilePath.toFile());for (int i = 1; i <= totalChunks; i++) {InputStream inputStream = minioClient.getObject(GetObjectArgs.builder().bucket(BUCKET_NAME).object(fileName + "_part_" + i).build());byte[] buffer = new byte[8192];int bytesRead;while ((bytesRead = inputStream.read(buffer)) != -1) {outputStream.write(buffer, 0, bytesRead);}inputStream.close();// 删除临时分片文件minioClient.removeObject(RemoveObjectArgs.builder().bucket(BUCKET_NAME).object(fileName + "_part_" + i).build());}outputStream.close();// 将合并后的文件上传到 MinIOminioClient.putObject(PutObjectArgs.builder().bucket(BUCKET_NAME).object(fileName).stream(new FileInputStream(tempFilePath.toFile()), Files.size(tempFilePath), -1).contentType("application/octet-stream").build());// 删除本地临时文件Files.delete(tempFilePath);// 清除缓存中的分片计数chunkCountMap.remove(fileName);return "Merged all chunks successfully.";}static class MergeRequest {private String fileName;private int totalChunks;public String getFileName() {return fileName;}public void setFileName(String fileName) {this.fileName = fileName;}public int getTotalChunks() {return totalChunks;}public void setTotalChunks(int totalChunks) {this.totalChunks = totalChunks;}}
}

详细说明

前端部分
  1. 文件选择输入框

    • @change 事件绑定 handleFileChange 函数,用于处理文件选择。
    • ref 属性用于引用文件输入框,方便后续操作。
  2. 文件预览信息

    • 显示选中文件的名称和大小,使用 formatBytes 函数将字节数转换为可读格式。
  3. 进度条显示

    • <progress> 标签用于显示上传进度,value 属性绑定 uploadProgress 变量。
  4. 检查分片是否已上传

    • isChunkUploaded 函数通过调用 /check-chunk-uploaded 接口检查某个分片是否已经上传。
  5. 上传文件分片

    • uploadChunk 函数负责将文件分片上传到服务器。
    • 使用 FormData 对象封装分片数据和其他参数。
    • onUploadProgress 回调函数用于实时更新上传进度。
    • 成功上传后,将分片编号加入 uploadedChunks 集合中。
  6. 合并分片

    • mergeChunks 函数发送合并请求,通知服务器合并所有分片。
  7. 表单提交处理

    • handleSubmit 函数负责计算总分片数,并依次上传每个分片,最后合并分片。
  8. 加载保存的上传进度

    • loadSavedProgress 函数从 localStorage 中加载之前保存的上传进度。
    • saveUploadProgress 函数定时保存当前的上传进度。
后端部分
  1. MinIO 客户端配置

    • 在 MinioConfig 类中配置 MinIO 客户端,设置 MinIO 服务地址和凭证。
  2. 上传文件分片

    • /upload 路由处理文件分片上传请求。
    • 将分片保存到 MinIO 中,使用 PutObjectArgs 构建上传参数。
  3. 检查分片是否已上传

    • /check-chunk-uploaded 路由处理检查分片是否已上传的请求。
    • 使用 statObject 方法检查对象是否存在。
  4. 合并所有分片

    • /merge 路由处理合并请求。
    • 从 MinIO 中读取所有分片文件,按顺序写入最终文件,并删除临时分片文件。
    • 最终将合并后的文件上传到 MinIO 中。
    • 删除本地临时文件并清除缓存中的分片计数。

其他注意事项

  1. MinIO 初始化

    • 确保 MinIO 服务已经启动,并且可以通过提供的 endpoint 访问。
    • 替换 YOUR-ACCESS-KEY 和 YOUR-SECRET-KEY 为你自己的 MinIO 凭证。
  2. 错误处理

    • 在实际应用中,建议增加更多的错误处理机制,例如重试机制、超时处理等,以提高系统的健壮性。
  3. 安全性

    • 在生产环境中,请确保对 MinIO 凭证进行妥善管理,并考虑使用环境变量或配置中心来存储这些敏感信息。
  4. 用户体验优化

    • 提供用户友好的提示信息,如上传进度、成功或失败的消息等。
    • 支持暂停和恢复上传功能,进一步提升用户体验。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/25645.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

java jar包内的jar包如何打补丁

问题描述&#xff1a; 主包&#xff1a;hisca.jar&#xff0c;解压后 BOOT-INFO/lib下有其他jar包 因为一个小bug&#xff0c;需要修改这个hisca包下BOOT-INF/lib下的子jar包service-hisca-impl-1.0.0.jar中的一个service类及xml文件 操作步骤&#xff1a; 1、主包jar -xvf …

一文读懂,外贸中的invoice是什么意思?如何制作?

在外贸领域&#xff0c;invoice 这一词汇频繁出现&#xff0c;它对于国际贸易的顺利进行起着至关紧要的作用。本文将深入剖析外贸中 invoice的具体含义、与商业发票的区别&#xff0c;以及其开具流程与注意事项&#xff0c;同时向大家推荐一款高效实用的发票制作工具 ——Zoho …

【论文笔记-TPAMI 2024】FreqFusion:用于密集图像预测的频率感知特征融合

Frequency-aware Feature Fusion for Dense Image Prediction 用于密集图像预测的频率感知特征融合 Abstract&#xff1a;密集图像预测任务要求具有强类别信息和高分辨率精确空间边界细节的特征。为了实现这一点&#xff0c;现代分层模型通常利用特征融合&#xff0c;直接添加…

DeepSeek 专家级操作手册详解

✅作者简介&#xff1a;2022年博客新星 第八。热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;趣享先生的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏&…

【Django自学】Django入门:如何使用django开发一个web项目(非常详细)

测试机器&#xff1a;windows11 x64 python版本&#xff1a;3.11 一、安装Django 安装步骤非常简单&#xff0c;使用pip安装就行 pip install django安装完成之后&#xff0c;python的 Scripts 文件夹下&#xff0c;会多一个 django-admin.exe (管理创建django项目的工具)。…

优云智算:借助强大镜像社区,开启AI算力新纪元!

&#x1f381;个人主页&#xff1a;我们的五年 &#x1f50d;系列专栏&#xff1a;Linux网络编程 &#x1f337;追光的人&#xff0c;终会万丈光芒 &#x1f389;欢迎大家点赞&#x1f44d;评论&#x1f4dd;收藏⭐文章 ​ 目录 前言&#xff1a; 平台介绍&#xff1a; …

抖音生活服务加强探店内容治理,2024年达人违规率下降30%

发布 | 大力财经 2月27日&#xff0c;抖音生活服务发布《2024抖音生活服务消费者权益保护年度报告》&#xff08;以下简称“报告”&#xff09;。报告显示&#xff0c;过去一年&#xff0c;抖音生活服务针对消费者反感的虚假、夸张探店内容&#xff0c;开展了专项治理。通过一…

LeetCode 热门100题-回文链表

题目描述&#xff1a; 给你一个单链表的头节点 head &#xff0c;请你判断该链表是否为回文链表。如果是&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。 示例 1&#xff1a; 输入&#xff1a;head [1,2,2,1] 输出&#xff1a;true 逻辑&#xff1a; 首先…

SpringCloud之Eureka、Ribbon、OpenFeign

目录1. SpringCloud Eureka&#xff08;服务注册与发现组件&#xff09;2. SpringCloud Ribbon&#xff08;负载均衡与服务调用组件&#xff09;3. SpringCloud OpenFeign&#xff08;负载均衡与服务调用组件&#xff09;SpringCloud&#xff1a;用于开发高度可扩展、高性能的分…

mamba_ssm和causal-conv1d详细安装教程

1.前言 Mamba是近年来在深度学习领域出现的一种新型结构&#xff0c;特别是在处理长序列数据方面表现优异。在本文中&#xff0c;我将介绍如何在 Linux 系统上安装并配置 mamba_ssm 虚拟环境。由于官方指定mamba_ssm适用于 PyTorch 版本高于 1.12 且 CUDA 版本大于 11.6 的环境…

【MySQL】表的基本操作

??表的基本操作 文章目录&#xff1a; 表的基本操作 创建查看表 创建表 查看表结构 表的修改 表的重命名 表的添加与修改 删除表结构 总结 前言&#xff1a; 在数据库中&#xff0c;数据表是存储和组织数据的基本单位&#xff0c;对于数据表的操作是每个程序员需要烂熟…

CES Asia 2025聚焦量子计算,多领域进展引关注

作为亚洲地区极具影响力的科技盛会&#xff0c;CES Asia 2025第七届亚洲消费电子技术贸易展&#xff08;赛逸展&#xff09;将在首都北京举办。本届展会以“创新、智能、互联”为主题&#xff0c;将全方位展示全球消费科技领域的最新成果与发展趋势。其中&#xff0c;量子计算作…

wps加载项学习3-扩展

WPS扩展API &#xff08;创建一个WebShape&#xff0c;WebShape身上有一个DataSource方法&#xff0c;DataSource有一个属性CreateDataRange&#xff09;绑定数据源&#xff0c;具体应用场景未知 NativeX扩展&#xff0c;把C&#xff0c;ruby&#xff0c;python等语言实现的算法…

进程间通信 —— 共享内存

目录 1.共享内存实现通信的原理 2.如何使用共享内存实现通信 共享内存通信接口介绍 shmget shmat shmdt shmctl 使用示例 key和shmid 3.共享内存通信的优缺点 缺点&#xff1a;不提供任何同步机制&#xff0c;可能会造成数据混乱。 优点&#xff1a;共享内存是进程…

3.【基于深度学习YOLOV11的车辆类型检测系统】

文章目录 研究背景主要工作内容一、系统核心功能介绍及效果演示演示&#xff1a;软件主要功能&#xff1a;检测界面各大板块说明&#xff1a;检测区域&#xff1a;结果显示&#xff1a;主要功能说明:&#xff08;1&#xff09;图片检测说明&#xff08;2&#xff09;图片批量检…

汽车悬架系统技术演进:从被动到全主动的革新之路(主动悬架类型对比)

在汽车工业的百年发展史中&#xff0c;悬架系统始终是平衡车辆性能与舒适性的关键战场。随着消费者对驾乘体验要求的不断提升&#xff0c;传统被动悬架已难以满足中高端车型的需求&#xff0c;而半主动与全主动悬架技术的崛起&#xff0c;正在重塑行业格局。本文将深入解析三大…

跨平台文件互传工具

一款高效便捷的文件互传工具&#xff0c;支持在线快速传输各种文件格式&#xff0c;无需注册&#xff0c;直接分享文件。适用于个人和团队间的文件共享&#xff0c;跨平台支持&#xff0c;轻松解决文件传输问题。免费的文件传输服务&#xff0c;让你的工作更高效。 gotool

算法题(81):询问学号

审题&#xff1a; 需要我们根据给出的n值确定录入数据个数&#xff0c;然后根据给出的数据存储学号。再根据m值确定需要输出的学号个数&#xff0c;然后根据数组内容输出学号 思路: 我们可以利用数组进行数据顺序存储&#xff0c;以及随机读取完成本题 由于学号最大为1e9&#…

(转)Java多态`

Base是父类&#xff0c;Sub是子类。 Base b new Sub(); b.out(); b持有的是子类Sub的实例&#xff0c;调用的是子类的方法。

JavaWeb后端基础(3)

原打算把Mysql操作数据库的一些知识写进去&#xff0c;但是感觉没必要&#xff0c;要是现在会的都是简单的增删改查&#xff0c;所以&#xff0c;这一篇&#xff0c;我直接从java操作数据库开始写&#xff0c;所以这一篇大致就是记一下JDBC、MyBatis、以及SpringBoot的配置文件…