首页 > 其他分享 >springboot项目实现断点续传功能

springboot项目实现断点续传功能

时间:2023-10-29 10:34:12浏览次数:34  
标签:断点续传 功能 return springboot chunkDTO file identifier public String

这篇文章主要介绍了springboot项目实现断点续传,本文通过示例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下  

java代码

package com.ruoyi.web.upload.controller;
import com.ruoyi.web.upload.dto.FileChunkDTO;
import com.ruoyi.web.upload.dto.FileChunkResultDTO;
import com.ruoyi.web.upload.result.Result;
import com.ruoyi.web.upload.service.IUploadService;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
/**
* @ProjectName UploaderController
* @author Administrator
* @version 1.0.0
* @Description 附件分片上传
* @createTime 2022/4/13 0013 15:58
*/
@RestController
@RequestMapping("upload")
public class UploaderController {
@Resource
private IUploadService uploadService;
/**
* 检查分片是否存在
*
* @return
*/
@GetMapping("chunk")
public Result checkChunkExist(FileChunkDTO chunkDTO) {
FileChunkResultDTO fileChunkCheckDTO;
try {
fileChunkCheckDTO = uploadService.checkChunkExist(chunkDTO);
return Result.ok(fileChunkCheckDTO);
} catch (Exception e) {
return Result.fail(e.getMessage());
}
}
/**
* 上传文件分片
*
* @param chunkDTO
* @return
*/
@PostMapping("chunk")
public Result uploadChunk(FileChunkDTO chunkDTO) {
try {
uploadService.uploadChunk(chunkDTO);
return Result.ok(chunkDTO.getIdentifier());
} catch (Exception e) {
return Result.fail(e.getMessage());
}
}
/**
* 请求合并文件分片
*
* @param chunkDTO
* @return
*/
@PostMapping("merge")
public Result mergeChunks(@RequestBody FileChunkDTO chunkDTO) {
try {
boolean success = uploadService.mergeChunk(chunkDTO.getIdentifier(), chunkDTO.getFilename(), chunkDTO.getTotalChunks());
return Result.ok(success);
} catch (Exception e) {
return Result.fail(e.getMessage());
}
}
}

 

package com.ruoyi.web.upload.dto;
import org.springframework.web.multipart.MultipartFile;
/**
* @ProjectName FileChunkDTO
* @author Administrator
* @version 1.0.0
* @Description 附件分片上传
* @createTime 2022/4/13 0013 15:59
*/
public class FileChunkDTO {
/**
* 文件 md5
*/
private String identifier;
/**
* 分块文件
*/
MultipartFile file;
/**
* 当前分块序号
*/
private Integer chunkNumber;
/**
* 分块大小
*/
private Long chunkSize;
/**
* 当前分块大小
*/
private Long currentChunkSize;
/**
* 文件总大小
*/
private Long totalSize;
/**
* 分块总数
*/
private Integer totalChunks;
/**
* 文件名
*/
private String filename;
public String getIdentifier() {
return identifier;
}
public void setIdentifier(String identifier) {
this.identifier = identifier;
}
public MultipartFile getFile() {
return file;
}
public void setFile(MultipartFile file) {
this.file = file;
}
public Integer getChunkNumber() {
return chunkNumber;
}
public void setChunkNumber(Integer chunkNumber) {
this.chunkNumber = chunkNumber;
}
public Long getChunkSize() {
return chunkSize;
}
public void setChunkSize(Long chunkSize) {
this.chunkSize = chunkSize;
}
public Long getCurrentChunkSize() {
return currentChunkSize;
}
public void setCurrentChunkSize(Long currentChunkSize) {
this.currentChunkSize = currentChunkSize;
}
public Long getTotalSize() {
return totalSize;
}
public void setTotalSize(Long totalSize) {
this.totalSize = totalSize;
}
public Integer getTotalChunks() {
return totalChunks;
}
public void setTotalChunks(Integer totalChunks) {
this.totalChunks = totalChunks;
}
public String getFilename() {
return filename;
}
public void setFilename(String filename) {
this.filename = filename;
}
@Override
public String toString() {
return "FileChunkDTO{" +
"identifier='" + identifier + '\'' +
", file=" + file +
", chunkNumber=" + chunkNumber +
", chunkSize=" + chunkSize +
", currentChunkSize=" + currentChunkSize +
", totalSize=" + totalSize +
", totalChunks=" + totalChunks +
", filename='" + filename + '\'' +
'}';
}
}

 

package com.ruoyi.web.upload.dto;
import java.util.Set;
/**
* @ProjectName FileChunkResultDTO
* @author Administrator
* @version 1.0.0
* @Description 附件分片上传
* @createTime 2022/4/13 0013 15:59
*/
public class FileChunkResultDTO {
/**
* 是否跳过上传
*/
private Boolean skipUpload;
/**
* 已上传分片的集合
*/
private Set<Integer> uploaded;
public Boolean getSkipUpload() {
return skipUpload;
}
public void setSkipUpload(Boolean skipUpload) {
this.skipUpload = skipUpload;
}
public Set<Integer> getUploaded() {
return uploaded;
}
public void setUploaded(Set<Integer> uploaded) {
this.uploaded = uploaded;
}
public FileChunkResultDTO(Boolean skipUpload, Set<Integer> uploaded) {
this.skipUpload = skipUpload;
this.uploaded = uploaded;
}
public FileChunkResultDTO(Boolean skipUpload) {
this.skipUpload = skipUpload;
}
}

 

package com.ruoyi.web.upload.dto;
import lombok.Getter;
/**
* @Author
* @Date Created in 2023/2/23 17:25
* @DESCRIPTION: 统一返回结果状态信息类
* @Version V1.0
*/
@Getter
@SuppressWarnings("all")
public enum ResultCodeEnum {
SUCCESS(200,"成功"),
FAIL(201, "失败"),
PARAM_ERROR( 202, "参数不正确"),
SERVICE_ERROR(203, "服务异常"),
DATA_ERROR(204, "数据异常"),
DATA_UPDATE_ERROR(205, "数据版本异常"),
LOGIN_AUTH(208, "未登陆"),
PERMISSION(209, "没有权限"),
CODE_ERROR(210, "验证码错误"),
LOGIN_MOBLE_ERROR(211, "账号不正确"),
LOGIN_DISABLED_ERROR(212, "改用户已被禁用"),
REGISTER_MOBLE_ERROR(213, "手机号码格式不正确"),
REGISTER_MOBLE_ERROR_NULL(214, "手机号码为空"),
LOGIN_AURH(214, "需要登录"),
LOGIN_ACL(215, "没有权限"),
URL_ENCODE_ERROR( 216, "URL编码失败"),
ILLEGAL_CALLBACK_REQUEST_ERROR( 217, "非法回调请求"),
FETCH_ACCESSTOKEN_FAILD( 218, "获取accessToken失败"),
FETCH_USERINFO_ERROR( 219, "获取用户信息失败");
private Integer code;
private String message;
private ResultCodeEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
}

 

package com.ruoyi.web.upload.service;
import com.ruoyi.web.upload.dto.FileChunkDTO;
import com.ruoyi.web.upload.dto.FileChunkResultDTO;
import java.io.IOException;
/**
* @ProjectName IUploadService
* @author Administrator
* @version 1.0.0
* @Description 附件分片上传
* @createTime 2022/4/13 0013 15:59
*/
public interface IUploadService {
/**
* 检查文件是否存在,如果存在则跳过该文件的上传,如果不存在,返回需要上传的分片集合
* @param chunkDTO
* @return
*/
FileChunkResultDTO checkChunkExist(FileChunkDTO chunkDTO);
/**
* 上传文件分片
* @param chunkDTO
*/
void uploadChunk(FileChunkDTO chunkDTO) throws IOException;
/**
* 合并文件分片
* @param identifier
* @param fileName
* @param totalChunks
* @return
* @throws IOException
*/
boolean mergeChunk(String identifier,String fileName,Integer totalChunks)throws IOException;
}

 

package com.ruoyi.web.upload.service.impl;
import com.ruoyi.web.upload.dto.FileChunkDTO;
import com.ruoyi.web.upload.dto.FileChunkResultDTO;
import com.ruoyi.web.upload.service.IUploadService;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.io.*;
import java.util.*;
/**
* @ProjectName UploadServiceImpl
* @author Administrator
* @version 1.0.0
* @Description 附件分片上传
* @createTime 2022/4/13 0013 15:59
*/
@Service
@SuppressWarnings("all")
public class UploadServiceImpl implements IUploadService {
private Logger logger = LoggerFactory.getLogger(UploadServiceImpl.class);
@Autowired
private RedisTemplate redisTemplate;
@Value("${ruoyi.profile}")
private String uploadFolder;
/**
* 检查文件是否存在,如果存在则跳过该文件的上传,如果不存在,返回需要上传的分片集合
* 检查分片是否存在
○ 检查目录下的文件是否存在。
○ 检查redis存储的分片是否存在。
○ 判断分片数量和总分片数量是否一致。
如果文件存在并且分片上传完毕,标识已经完成附件的上传,可以进行秒传操作。
如果文件不存在或者分片为上传完毕,则返回false并返回已经上传的分片信息。
* @param chunkDTO
* @return
*/
@Override
public FileChunkResultDTO checkChunkExist(FileChunkDTO chunkDTO) {
//1.检查文件是否已上传过
//1.1)检查在磁盘中是否存在
String fileFolderPath = getFileFolderPath(chunkDTO.getIdentifier());
logger.info("fileFolderPath-->{}", fileFolderPath);
String filePath = getFilePath(chunkDTO.getIdentifier(), chunkDTO.getFilename());
File file = new File(filePath);
boolean exists = file.exists();
//1.2)检查Redis中是否存在,并且所有分片已经上传完成。
Set<Integer> uploaded = (Set<Integer>) redisTemplate.opsForHash().get(chunkDTO.getIdentifier(), "uploaded");
if (uploaded != null && uploaded.size() == chunkDTO.getTotalChunks() && exists) {
return new FileChunkResultDTO(true);
}
File fileFolder = new File(fileFolderPath);
if (!fileFolder.exists()) {
boolean mkdirs = fileFolder.mkdirs();
logger.info("准备工作,创建文件夹,fileFolderPath:{},mkdirs:{}", fileFolderPath, mkdirs);
}
// 断点续传,返回已上传的分片
return new FileChunkResultDTO(false, uploaded);
}
/**
* 上传分片
* 上传附件分片
○ 判断目录是否存在,如果不存在则创建目录。
○ 进行切片的拷贝,将切片拷贝到指定的目录。
○ 将该分片写入redis
* @param chunkDTO
*/
@Override
public void uploadChunk(FileChunkDTO chunkDTO) {
//分块的目录
String chunkFileFolderPath = getChunkFileFolderPath(chunkDTO.getIdentifier());
logger.info("分块的目录 -> {}", chunkFileFolderPath);
File chunkFileFolder = new File(chunkFileFolderPath);
if (!chunkFileFolder.exists()) {
boolean mkdirs = chunkFileFolder.mkdirs();
logger.info("创建分片文件夹:{}", mkdirs);
}
//写入分片
try (
InputStream inputStream = chunkDTO.getFile().getInputStream();
FileOutputStream outputStream = new FileOutputStream(new File(chunkFileFolderPath + chunkDTO.getChunkNumber()))
) {
IOUtils.copy(inputStream, outputStream);
logger.info("文件标识:{},chunkNumber:{}", chunkDTO.getIdentifier(), chunkDTO.getChunkNumber());
//将该分片写入redis
long size = saveToRedis(chunkDTO);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public boolean mergeChunk(String identifier, String fileName, Integer totalChunks) throws IOException {
return mergeChunks(identifier, fileName, totalChunks);
}
/**
* 合并分片
*
* @param identifier
* @param filename
*/
private boolean mergeChunks(String identifier, String filename, Integer totalChunks) {
String chunkFileFolderPath = getChunkFileFolderPath(identifier);
String filePath = getFilePath(identifier, filename);
// 检查分片是否都存在
if (checkChunks(chunkFileFolderPath, totalChunks)) {
File chunkFileFolder = new File(chunkFileFolderPath);
File mergeFile = new File(filePath);
File[] chunks = chunkFileFolder.listFiles();
// 切片排序1、2/3、---
List fileList = Arrays.asList(chunks);
Collections.sort(fileList, (Comparator<File>) (o1, o2) -> {
return Integer.parseInt(o1.getName()) - (Integer.parseInt(o2.getName()));
});
try {
RandomAccessFile randomAccessFileWriter = new RandomAccessFile(mergeFile, "rw");
byte[] bytes = new byte[1024];
for (File chunk : chunks) {
RandomAccessFile randomAccessFileReader = new RandomAccessFile(chunk, "r");
int len;
while ((len = randomAccessFileReader.read(bytes)) != -1) {
randomAccessFileWriter.write(bytes, 0, len);
}
randomAccessFileReader.close();
}
randomAccessFileWriter.close();
} catch (Exception e) {
return false;
}
return true;
}
return false;
}
/**
* 检查分片是否都存在
* @param chunkFileFolderPath
* @param totalChunks
* @return
*/
private boolean checkChunks(String chunkFileFolderPath, Integer totalChunks) {
try {
for (int i = 1; i <= totalChunks + 1; i++) {
File file = new File(chunkFileFolderPath + File.separator + i);
if (file.exists()) {
continue;
} else {
return false;
}
}
} catch (Exception e) {
return false;
}
return true;
}
/**
* 分片写入Redis
* 判断切片是否已存在,如果未存在,则创建基础信息,并保存。
* @param chunkDTO
*/
private synchronized long saveToRedis(FileChunkDTO chunkDTO) {
Set<Integer> uploaded = (Set<Integer>) redisTemplate.opsForHash().get(chunkDTO.getIdentifier(), "uploaded");
if (uploaded == null) {
uploaded = new HashSet<>(Arrays.asList(chunkDTO.getChunkNumber()));
HashMap<String, Object> objectObjectHashMap = new HashMap<>();
objectObjectHashMap.put("uploaded", uploaded);
objectObjectHashMap.put("totalChunks", chunkDTO.getTotalChunks());
objectObjectHashMap.put("totalSize", chunkDTO.getTotalSize());
// objectObjectHashMap.put("path", getFileRelativelyPath(chunkDTO.getIdentifier(), chunkDTO.getFilename()));
objectObjectHashMap.put("path", chunkDTO.getFilename());
redisTemplate.opsForHash().putAll(chunkDTO.getIdentifier(), objectObjectHashMap);
} else {
uploaded.add(chunkDTO.getChunkNumber());
redisTemplate.opsForHash().put(chunkDTO.getIdentifier(), "uploaded", uploaded);
}
return uploaded.size();
}
/**
* 得到文件的绝对路径
*
* @param identifier
* @param filename
* @return
*/
private String getFilePath(String identifier, String filename) {
String ext = filename.substring(filename.lastIndexOf("."));
// return getFileFolderPath(identifier) + identifier + ext;
return uploadFolder + filename;
}
/**
* 得到文件的相对路径
*
* @param identifier
* @param filename
* @return
*/
private String getFileRelativelyPath(String identifier, String filename) {
String ext = filename.substring(filename.lastIndexOf("."));
return "/" + identifier.substring(0, 1) + "/" +
identifier.substring(1, 2) + "/" +
identifier + "/" + identifier
+ ext;
}
/**
* 得到分块文件所属的目录
*
* @param identifier
* @return
*/
private String getChunkFileFolderPath(String identifier) {
return getFileFolderPath(identifier) + "chunks" + File.separator;
}
/**
* 得到文件所属的目录
*
* @param identifier
* @return
*/
private String getFileFolderPath(String identifier) {
return uploadFolder + identifier.substring(0, 1) + File.separator +
identifier.substring(1, 2) + File.separator +
identifier + File.separator;
// return uploadFolder;
}
}

 

package com.ruoyi.web.upload.result;
import com.ruoyi.web.upload.dto.ResultCodeEnum;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* @Author
* @Date Created in 2023/2/23 17:25
* @DESCRIPTION: 全局统一返回结果
* @Version V1.0
*/
@Data
@ApiModel(value = "全局统一返回结果")
@SuppressWarnings("all")
public class Result<T> {
@ApiModelProperty(value = "返回码")
private Integer code;
@ApiModelProperty(value = "返回消息")
private String message;
@ApiModelProperty(value = "返回数据")
private T data;
private Long total;
public Result(){}
protected static <T> Result<T> build(T data) {
Result<T> result = new Result<T>();
if (data != null)
result.setData(data);
return result;
}
public static <T> Result<T> build(T body, ResultCodeEnum resultCodeEnum) {
Result<T> result = build(body);
result.setCode(resultCodeEnum.getCode());
result.setMessage(resultCodeEnum.getMessage());
return result;
}
public static <T> Result<T> build(Integer code, String message) {
Result<T> result = build(null);
result.setCode(code);
result.setMessage(message);
return result;
}
public static<T> Result<T> ok(){
return Result.ok(null);
}
/**
* 操作成功
* @param data
* @param <T>
* @return
*/
public static<T> Result<T> ok(T data){
Result<T> result = build(data);
return build(data, ResultCodeEnum.SUCCESS);
}
public static<T> Result<T> fail(){
return Result.fail(null);
}
/**
* 操作失败
* @param data
* @param <T>
* @return
*/
public static<T> Result<T> fail(T data){
Result<T> result = build(data);
return build(data, ResultCodeEnum.FAIL);
}
public Result<T> message(String msg){
this.setMessage(msg);
return this;
}
public Result<T> code(Integer code){
this.setCode(code);
return this;
}
public boolean isOk() {
if(this.getCode().intValue() == ResultCodeEnum.SUCCESS.getCode().intValue()) {
return true;
}
return false;
}
}

 

  • 前端代码
  • mainjs导入uploader

import uploader from 'vue-simple-uploader'
Vue.use(uploader)

 

安装uploader和spark-md5的依赖

npm install --save vue-simple-uploader
npm install --save spark-md5

创建uploader组件

<template>
<div>
<uploader
:autoStart="false"
:options="options"
:file-status-text="statusText"
class="uploader-example"
@file-complete="fileComplete"
@complete="complete"
@file-success="fileSuccess"
@files-added="filesAdded"
>
<uploader-unsupport></uploader-unsupport>
<uploader-drop>
<p>将文件拖放到此处以上传</p>
<uploader-btn>选择文件</uploader-btn>
<uploader-btn :attrs="attrs">选择图片</uploader-btn>
<uploader-btn :directory="true">选择文件夹</uploader-btn>
</uploader-drop>
<!-- <uploader-list></uploader-list> -->
<uploader-files> </uploader-files>
</uploader>
<br />
<el-button @click="allStart()" :disabled="disabled">全部开始</el-button>
<el-button @click="allStop()" style="margin-left: 4px">全部暂停</el-button>
<el-button @click="allRemove()" style="margin-left: 4px">全部移除</el-button>
</div>
</template>
<script>
import axios from "axios";
import SparkMD5 from "spark-md5";
import {upload} from "@/api/user";
// import storage from "store";
// import { ACCESS_TOKEN } from '@/store/mutation-types'
export default {
name: "Home",
data() {
return {
skip: false,
options: {
target: "//localhost:9999/upload/chunk",
// 开启服务端分片校验功能
testChunks: true,
parseTimeRemaining: function (timeRemaining, parsedTimeRemaining) {
return parsedTimeRemaining
.replace(/\syears?/, "年")
.replace(/\days?/, "天")
.replace(/\shours?/, "小时")
.replace(/\sminutes?/, "分钟")
.replace(/\sseconds?/, "秒");
},
// 服务器分片校验函数
checkChunkUploadedByResponse: (chunk, message) => {
const result = JSON.parse(message);
if (result.data.skipUpload) {
this.skip = true;
return true;
}
return (result.data.uploaded || []).indexOf(chunk.offset + 1) >= 0;
},
// headers: {
// // 在header中添加的验证,请根据实际业务来
// "Access-Token": storage.get(ACCESS_TOKEN),
// },
},
attrs: {
accept: "image/*",
},
statusText: {
success: "上传成功",
error: "上传出错了",
uploading: "上传中...",
paused: "暂停中...",
waiting: "等待中...",
cmd5: "计算文件MD5中...",
},
fileList: [],
disabled: true,
};
},
watch: {
fileList(o, n) {
this.disabled = false;
},
},
methods: {
// fileSuccess(rootFile, file, response, chunk) {
// // console.log(rootFile);
// // console.log(file);
// // console.log(message);
// // console.log(chunk);
// const result = JSON.parse(response);
// console.log(result.success, this.skip);
//
// if (result.success && !this.skip) {
// axios
// .post(
// "http://127.0.0.1:9999/upload/merge",
// {
// identifier: file.uniqueIdentifier,
// filename: file.name,
// totalChunks: chunk.offset,
// },
// // {
// // headers: { "Access-Token": storage.get(ACCESS_TOKEN) }
// // }
// )
// .then((res) => {
// if (res.data.success) {
// console.log("上传成功");
// } else {
// console.log(res);
// }
// })
// .catch(function (error) {
// console.log(error);
// });
// } else {
// console.log("上传成功,不需要合并");
// }
// if (this.skip) {
// this.skip = false;
// }
// },
fileSuccess(rootFile, file, response, chunk) {
// console.log(rootFile);
// console.log(file);
// console.log(message);
// console.log(chunk);
const result = JSON.parse(response);
console.log(result.success, this.skip);
const user = {
identifier: file.uniqueIdentifier,
filename: file.name,
totalChunks: chunk.offset,
}
if (result.success && !this.skip) {
upload(user).then((res) => {
if (res.code == 200) {
console.log("上传成功");
} else {
console.log(res);
}
})
.catch(function (error) {
console.log(error);
});
} else {
console.log("上传成功,不需要合并");
}
if (this.skip) {
this.skip = false;
}
},
fileComplete(rootFile) {
// 一个根文件(文件夹)成功上传完成。
// console.log("fileComplete", rootFile);
// console.log("一个根文件(文件夹)成功上传完成。");
},
complete() {
// 上传完毕。
// console.log("complete");
},
filesAdded(file, fileList, event) {
// console.log(file);
file.forEach((e) => {
this.fileList.push(e);
this.computeMD5(e);
});
},
computeMD5(file) {
let fileReader = new FileReader();
let time = new Date().getTime();
let blobSlice =
File.prototype.slice ||
File.prototype.mozSlice ||
File.prototype.webkitSlice;
let currentChunk = 0;
const chunkSize = 1024 * 1024;
let chunks = Math.ceil(file.size / chunkSize);
let spark = new SparkMD5.ArrayBuffer();
// 文件状态设为"计算MD5"
file.cmd5 = true; //文件状态为“计算md5...”
file.pause();
loadNext();
fileReader.onload = (e) => {
spark.append(e.target.result);
if (currentChunk < chunks) {
currentChunk++;
loadNext();
// 实时展示MD5的计算进度
console.log(
`第${currentChunk}分片解析完成, 开始第${
currentChunk + 1
} / ${chunks}分片解析`
);
} else {
let md5 = spark.end();
console.log(
`MD5计算完毕:${file.name} \nMD5:${md5} \n分片:${chunks} 大小:${
file.size
} 用时:${new Date().getTime() - time} ms`
);
spark.destroy(); //释放缓存
file.uniqueIdentifier = md5; //将文件md5赋值给文件唯一标识
file.cmd5 = false; //取消计算md5状态
file.resume(); //开始上传
}
};
fileReader.onerror = function () {
this.error(`文件${file.name}读取出错,请检查该文件`);
file.cancel();
};
function loadNext() {
let start = currentChunk * chunkSize;
let end =
start + chunkSize >= file.size ? file.size : start + chunkSize;
fileReader.readAsArrayBuffer(blobSlice.call(file.file, start, end));
}
},
allStart() {
console.log(this.fileList);
this.fileList.map((e) => {
if (e.paused) {
e.resume();
}
});
},
allStop() {
console.log(this.fileList);
this.fileList.map((e) => {
if (!e.paused) {
e.pause();
}
});
},
allRemove() {
this.fileList.map((e) => {
e.cancel();
});
this.fileList = [];
},
},
};
</script>
<style>
.uploader-example {
width: 100%;
padding: 15px;
margin: 0px auto 0;
font-size: 12px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.4);
}
.uploader-example .uploader-btn {
margin-right: 4px;
}
.uploader-example .uploader-list {
max-height: 440px;
overflow: auto;
overflow-x: hidden;
overflow-y: auto;
}
</style>

到此这篇关于springboot项目实现断点续传功能的文章就介绍到这了

 

参考文章:http://blog.ncmem.com/wordpress/2023/10/29/springboot%e9%a1%b9%e7%9b%ae%e5%ae%9e%e7%8e%b0%e6%96%ad%e7%82%b9%e7%bb%ad%e4%bc%a0%e5%8a%9f%e8%83%bd/

欢迎入群一起讨论

 

 

标签:断点续传,功能,return,springboot,chunkDTO,file,identifier,public,String
From: https://www.cnblogs.com/songsu/p/17795561.html

相关文章

  • vscode 安装了clang之后 即使卸载也没有跳转功能记录
     借鉴这位博主  【精选】VSCodeC/C++无法跳转到定义、自动补全、悬停提示功能_c/c++:editconfigurations(json)-CSDN博客 (上图高亮部分)卸载clang 重启VSCode 然后IntelliSenseEngine开关设置为Default 应该就可以了 ......
  • 推荐太极神器-聚合各类资源功能,逼格满满!
    今天给大家介绍一下太极神器逼格满满的功能。观山在这个功能下有着三个图片检索接口,在这里可以找到超多美图,各种类型应有尽有。如果看到了什么喜欢的图片,也可以直接点击右下角的按钮保存到本地,用来当壁纸或者封面都是挺不错的选择听雨这个功能名为“听雨”,但听的并不是雨,而是音乐,在......
  • 一些研发工程师在Springboot注意点
    一些研发工程师在Springboot注意点1.正确设计代码目录结构虽然您有很大的自由度,但有一些基本规则值得遵循来设计您的源代码结构。避免使用默认包。确保所有内容(包括入口点)都在命名良好的包中,这样您就可以避免与组装和组件扫描相关的意外情况;将Application.java(应用程序的入口类)......
  • SpringBoot 自定义注解实现过程
    1、新建SpringBoot-Test 其中pom.xml文件如下:<?xmlversion="1.0"encoding="UTF-8"?><projectxmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schem......
  • 评论功能的选择难题:数据结构如何选定?
    尊敬的小伙伴们,大家好!我是小米,一个热爱技术、热衷分享的90后程序员。今天,我要和大家一起探讨一个在软件开发中常见,却又充满深度的话题——"面试题:评论功能采用什么数据结构?"。在这个数字化时代,几乎每个应用程序都需要实现评论功能。无论是社交媒体、电子商务网站还是新闻阅读应用,评......
  • 10G 大文件、秒传、断点续传、分片上传
    超大文件上传是一个老生常谈的话题了,在文件相对比较小的情况下,可以直接把文件转化为字节流上传到服务器,但在文件比较大的情况下,用普通的方式进行上传,这可不是一个好的办法,毕竟很少有人会忍受,当文件上传到一半中断后,继续上传却只能重头开始上传,这种让人不爽的体验。那有没有比较好......
  • 物联网告警功能设计
    物联网告警功能设计物联网中的数据告警设计1.需求平台需要具有当某个指标达到某个设定的阀值时进行通知或进行下一步操作,类似于Prometheus监控系统,但是操作要简单化,也就是说在设计触发规则时要支持图形化配置,方便用户配置.1.1数据源数据源为设备数据上传后清洗后放入K......
  • 前端大文件断点续传
    昨天整理了前端实现大文件上传通过文件切片进行处理,今天继续拓展进行断点续传原理断点续传的原理在于前端/服务端需要记住已上传的切片,这样下次上传就可以跳过之前已上传的部分,有两种方案实现记忆的功能前端使用localStorage记录已上传的切片hash服务端保存已上传的切片hash......
  • 详解SpringBoot @Conditional相关条件注解
    Springboot条件注解是@ContionalXXX相关的注解,表示当特定条件有效时,被修饰的配置类或配置方法才会生效。条件注解可以用来修饰@Configuration类或@Bean方法等。主要有以下行为:当SpringBoot检测到类加载路径包含某个框架时,会自动配置该框架的基础Bean.只有当开发者没配置某......
  • Springboot+Mybatis+Mybatisplus 框架中增加自定义分页插件和sql 占位符修改插件
    一、Springboot简介springboot是当下最流行的web框架,SpringBoot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置,让这些框架集成在一起变得更加简单,简化了我......