Hi,大家好,我是抢老婆酸奶的小肥仔。
在日常开发中,我们常常需要文件上传,传统上实现上传是直接将文件保存到本地磁盘,然后通过磁盘路径进行下载查看。这样子会造成磁盘被大量占用,同时一不小心手欠的话就会将文件删除。这时候,我们可能会想到用一些文件存储系统,例如:我们熟悉的FastDFS等,我们今天介绍的主角不是FastDFS,而是Minio。
废话不多说,开整。
1、Minio简介及部署
Minio:一种分布式文件存储,具有高性能,轻量级,速度快,容错率高等特点,兼容亚马逊S3云存储服务接口,并可以作为一个独立的存储后端。
minio提供了纠删码策略,即将数据进行切分,同时计算校验块,采用Reed-Solomon code将对象拆分成N/2数据和N/2奇偶校验块,假如有8块盘,数据则被分成4个数据块,4个奇偶校验块。即使这个对象丢了4块盘,数据依然可以进行恢复,因此即使我们一不小心删除了一些盘,也不用担心数据会丢失。
1.1 Minio部署
我们以win10环境为例,进行Minio部署。
1.1.1 下载Minio
minio下载地址:www.minio.org.cn/download.sh...
选择windows版本进行下载,下载完成后是一个exe文件。
1.1.2 部署minio
在windows下创建一个文件夹用来放置minio执行文件,例如:我将其放在D盘下的soft/minio下。
Minio的部署可以分为:单节点单磁盘,单节点多磁盘,多节点方式。
单节点单磁盘执行执行exe文件即可,我们就说说单节点多次盘,多节点两种部署方式。
1.1.2.1 单节点多磁盘
1.1.2.1.1 创建目录
即通过一个节点进行访问,数据被保存在不同磁盘下。
我们创建4个文件夹来模拟四块不同的磁盘。如图:
1.1.2.1.2 启动minio
minio启动需要编写脚本,在存放minio.exe文件夹下创建一个minio.bat文件,文件内容如下:
bat
@echo off
set MINIO_ROOT_USER=admin
set MINIO_ROOT_PASSWORD=12345678
cd "D:\soft\minio"
start minio.exe server --console-address ":9000" D:\soft\minio\dataOne D:\soft\minio\dataTwo D:\soft\minio\dataThree D:\soft\minio\dataFour
--console-address ":9000"
:管理页的ip和端口,缺省时默认是127.0.0.1:9000
--address "127.0.0.1:9090"
:代表接口的ip和端口
1.1.2.1.3 访问
直接在浏览器上输入:http://localhost:9000即可进行访问。
1.1.2.2 多节点
即端口不一样,数据磁盘路径一致。
例如我们设置四个不同端口:9001,9002,9003,9004。分别执行如下脚本:
bat
set MINIO_ROOT_USER=admin
set MINIO_ROOT_PASSWORD=12345678
start minio.exe server --console-address "127.0.0.1:9001" --address "127.0.0.1:9091"
http://127.0.0.1:9091/D:\soft\minio\dataOne
http://127.0.0.1:9092/D:\soft\minio\dataTwo
http://127.0.0.1:9093/D:\soft\minio\dataThree
http://127.0.0.1:9094/D:\soft\minio\dataFour
其他端口执行,只需要更改端口就好。
启动minio
由于涉及到多个端口,访问资源时,不可能对每个节点进行访问显然不合理,因此我们可以通过nginx来进行代理。配置如下:
config
upstream minio{
server 127.0.0.1:9001;
server 127.0.0.1:9002;
server 127.0.0.1:9003;
server 127.0.0.1:9004;
}
server{
listen 8888;
server_name 127.0.0.1;
ignore_invalid_headers off;
client_max_body_size 0;
proxy_buffering off;
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Host $host:$server_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
proxy_set_header Host $http_host;
proxy_connect_timeout 300;
proxy_http_version 1.1;
chunked_transfer_encoding off;
proxy_ignore_client_abort on;
proxy_pass http://minio;
}
}
2、分片上传
在minio中实现上传会自动进行分块,然后再将分块上传到Minio服务器,最后在进行合并。但是我们在使用时必须要将整个文件上传到系统,然后再调用Minio接口进行文件上传,当文件比较大时,就会占用太多宽带,从而大致上传慢,甚至使服务挂掉。因此我们需要进行优化。
在minio中提供了三个方法completeMultipartUpload,createMultipartUpload,listMultipart通过这三个方法我们可以将文件进行分片,分片后返回分片上传连接,等所有分片上传完成后,再进行分片合并,从而完成整个文件的上传。大致流程:
1、用户在前端使用vue-uploader上传文件,判断文件的大小。
2、如果文件大小小于5M则直接通过接口上传到minio服务器,如果大于5M时,计算分片数,调用分片接口获取每个分片对应的上传地址。
3、根据分片计算每个分片的大小,将文件按大小进行分片,调用分片地址将分片进行上传。
4、分片上传完成后,将分片进行合并,在服务器上形成文件。
2.1 代码实现
2.1.1 后端实现
基于minio的分片上传主要是重写三个接口,即completeMultipartUpload,createMultipartUpload,listMultipart,这三个接口在minio包中是protected的,我们如果想要使用这三个方法,只能重写这三个方法即可。
completeMultipartUpload
: 即完成分片上传后,进行分片合并。
createMultipartUpload
: 即返回每个分片对应Id及上传的url。
listMultipart
: 即查询分片信息。
1、定义PearMinioClient
PearMinioClient主要是集成MinioClient,重写MinioClient中的三个方法。
java
/**
* @author: jiangjs
* @description: 分片上传MinioClient继承MinioClient,主要暴露:
* createMultipartUpload:创建分片请求,返回uploadId
* listMultipart:查询分片信息
* completeMultipartUpload:根据uploadId合并已上传的分片
* @date: 2023/10/24 14:08
**/
public class PearMinioClient extends MinioClient {
protected PearMinioClient(MinioClient client) {
super(client);
}
@Override
public ObjectWriteResponse completeMultipartUpload(String bucketName, String region, String objectName, String uploadId, Part[] parts, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) throws NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, ServerException, XmlParserException, ErrorResponseException, InternalException, InvalidResponseException {
return super.completeMultipartUpload(bucketName, region, objectName, uploadId, parts, extraHeaders, extraQueryParams);
}
@Override
public CreateMultipartUploadResponse createMultipartUpload(String bucketName, String region, String objectName, Multimap<String, String> headers, Multimap<String, String> extraQueryParams) throws NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, ServerException, XmlParserException, ErrorResponseException, InternalException, InvalidResponseException {
return super.createMultipartUpload(bucketName, region, objectName, headers, extraQueryParams);
}
public ListPartsResponse listMultipart(String bucketName,String region,String objectName,Integer maxParts,Integer partNumberMarker,
String uploadId, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) throws ServerException, InsufficientDataException, ErrorResponseException, NoSuchAlgorithmException, IOException, InvalidKeyException, XmlParserException, InvalidResponseException, InternalException {
return super.listParts(bucketName, region, objectName, maxParts, partNumberMarker, uploadId, extraHeaders, extraQueryParams);
}
}
2、设置minio配置
获取配置文件中配置的minio相关属性,如地址等。
java
/**
* @author: jiangjs
* @description:
* @date: 2023/10/20 10:27
**/
@Component
@Data
@ConfigurationProperties(prefix = "minio")
public class MinioEndPointInfo {
/**
* minio节点地址
*/
private String endpoint;
/**
* 登录用户名
*/
private String accessKey;
/**
* 密码
*/
private String secretKey;
}
其中accessKey,secretKey分别在创建服务用户时创建。
创建minio配置类
java
/**
* @author: jiangjs
* @description: minio相关配置
* @date: 2023/10/20 10:46
**/
@Configuration
@EnableConfigurationProperties(MinioEndPointInfo.class)
public class MinioConfig {
@Resource
private MinioEndPointInfo minioEndPointInfo;
@Bean
public PearMinioClient createPearMinioClient(){
MinioClient minioClient = MinioClient.builder()
.endpoint(minioEndPointInfo.getEndpoint())
.credentials(minioEndPointInfo.getAccessKey(), minioEndPointInfo.getSecretKey())
.build();
return new PearMinioClient(minioClient);
}
}
上述minio配置类中,创建了minioClient来初始化我们定义的PearMinioClient。
3、创建minio分片工具类
java
/**
* @author: jiangjs
* @description: minio分片上传工具
* @date: 2023/10/30 9:34
**/
@Component
@Slf4j
public class MinioPearUploadUtil {
@Resource
private PearMinioClient pearMinioClient;
/**
* 校验当前bucket是否存在,不存在则创建
* @param bucketName 桶
*/
private void existBucket(String bucketName){
try {
boolean isExist = pearMinioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
if (!isExist){
pearMinioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
}
}catch (Exception e){
e.printStackTrace();
}
}
/**
* 创建分片上传信息
* @param chunkNum 分片数量
* @param fileName 文件名称
* @param contentType 文件类型
* @param bucketEnum 桶
* @return 返回分片信息
*/
@SneakyThrows
public MinioPearVo createMultipartUploadUrl(Integer chunkNum, String fileName, String contentType,MinioBucketEnum bucketEnum){
//设置分片文件类型
Multimap<String, String> headerMap = HashMultimap.create();
headerMap.put("Content-Type",contentType);
CreateMultipartUploadResponse uploadResponse = pearMinioClient.createMultipartUpload(bucketEnum.getBucket(), null, fileName,
headerMap, null);
Map<String, String> reqParams = new HashMap<>(2);
reqParams.put("uploadId",uploadResponse.result().uploadId());
MinioPearVo pearVo = new MinioPearVo();
pearVo.setUploadId(uploadResponse.result().uploadId());
List<MinioPearVo.PearUploadData> uploads = new ArrayList<>();
for (int i = 1; i <= chunkNum; i++) {
reqParams.put("partNumber",String.valueOf(i));
String objectUrl = pearMinioClient.getPresignedObjectUrl(
GetPresignedObjectUrlArgs.builder()
.method(Method.PUT)
.bucket(MinioBucketEnum.EMAIL.getBucket())
.object(fileName)
.expiry(1, TimeUnit.DAYS)
.extraQueryParams(reqParams)
.build());
MinioPearVo.PearUploadData uploadData = new MinioPearVo.PearUploadData();
uploadData.setUploadUrl(objectUrl).setParkNum(i);
uploads.add(uploadData);
}
pearVo.setParts(uploads);
return pearVo;
}
/**
* 合并文件分片
* @param chunkNum 分片数量
* @param fileName 文件名称
* @param contentType 文件类型
* @param uploadId 分片上传时的Id
* @param bucketEnum 桶
* @return 合并结果
*/
@SneakyThrows
public Boolean completeMultipart(Integer chunkNum, String fileName, String contentType,String uploadId,MinioBucketEnum bucketEnum){
Multimap<String, String> headerMap = HashMultimap.create();
headerMap.put("Content-Type",contentType);
ListPartsResponse listMultipart = pearMinioClient.listMultipart(bucketEnum.getBucket(), null, fileName, chunkNum + 10,
0, uploadId, headerMap, null);
if (Objects.nonNull(listMultipart)){
Part[] parts = new Part[chunkNum+10];
int partNum = 0;
for (Part part : listMultipart.result().partList()) {
parts[partNum] = new Part(partNum,part.etag());
partNum++;
}
pearMinioClient.completeMultipartUpload(MinioBucketEnum.EMAIL.getBucket(), null, fileName,
uploadId, parts, headerMap, null);
return Boolean.TRUE;
}
return Boolean.FALSE;
}
/**
* 文件流上传
* @param ism 文件流
* @param bucketName 桶名称
* @param fileName 文件名称
* @return 执行结果
*/
public Boolean upLoadInputStream(InputStream ism, String bucketName, String fileName){
try {
existBucket(bucketName);
pearMinioClient.putObject(PutObjectArgs.builder()
.bucket(bucketName)
.object(fileName)
.stream(ism,ism.available(),-1)
.build());
return Boolean.TRUE;
}catch (Exception e){
log.error("文件流上传报错:"+e.getMessage());
e.printStackTrace();
return Boolean.FALSE;
}
}
/**
* 下载文件
* @param fileName 文件名称
* @param bucketName 桶名称
* @return 文件流
*/
public InputStream downLoadFile(String fileName,String bucketName){
InputStream ism = null;
try {
ism = pearMinioClient.getObject(GetObjectArgs.builder()
.bucket(bucketName)
.object(fileName).build());
} catch (Exception e) {
log.error(String.format("下载文件(%s)报错:%s",fileName,e.getMessage()));
e.printStackTrace();
}
return ism;
}
}
MinioPearVo
:创建分片返回回来的实体,包含了uploadId及分片数量、地址。
MinioBucketEnum
:创建桶的枚举类,为后期方便扩展。
MinioPearVo实体:
java
/**
* @author: jiangjs
* @description:
* @date: 2023/10/30 9:36
**/
@Data
public class MinioPearVo {
/**
* 上传Id
*/
private String uploadId;
/**
* 获取分片上传URL
*/
private List<PearUploadData> parts;
@Data
@Accessors(chain = true)
public static class PearUploadData{
/**
* 分片编号,从1开始
*/
private int parkNum;
/**
* 分片上传Url
*/
private String uploadUrl;
}
}
MinioBucketEnum桶枚举类:
java
/**
* @author: jiangjs
* @description:
* @date: 2023/10/20 10:51
**/
public enum MinioBucketEnum {
/**
* email
*/
EMAIL("email");
private final String bucket;
MinioBucketEnum(String bucket){
this.bucket = bucket;
}
public MinioBucketEnum getMinioBucket(String bucket){
return MinioBucketEnum.valueOf(bucket);
}
public String getBucket(){
return bucket;
}
}
4、重传校验
由于minio服务是不会校验当前文件是否已经上传过,因此即使是相同文件、相同名称的文件可以重复上传,为了避免资源的浪费,我们将上传的文件进行校验。
其思路:每次上传文件的时候都对文件内容进行md5加密,使用加密后的md5去数据库查询,查询当前文件在当前桶里面是否已经上传。
创建记录文件的表:
SQL
create table opu_sys_files
(
id bigint auto_increment comment '主键Id'
primary key,
bucket_name varchar(100) not null comment '桶名称',
file_name varchar(200) not null comment '文件名称',
suffix varchar(8) not null comment '文件后缀名',
md5_code varchar(32) not null comment '加密md5编码',
create_time datetime default CURRENT_TIMESTAMP null comment '创建时间'
)
comment '系统文件表';
引入Mybatis-plus,实现根据md5、桶名称查询数据,其他的mapper,service小伙伴们自行实现。
java
@Override
public Boolean isExistFile(String md5Code, String bucketName) {
Long count = opuSysFilesMapper.selectCount(Wrappers.<OpuSysFiles>lambdaQuery()
.eq(OpuSysFiles::getMd5Code, md5Code)
.eq(OpuSysFiles::getBucketName, bucketName));
return count > 0;
}
注:文件内容的md5加密,小伙伴们可以使用Spring中自带的DigestUtils进行加密
String md5Str = DigestUtils.md5DigestAsHex(file.getInputStream());
5、创建接口
万事具备,只欠东风了。上面封装的方法都有了,我们创建接口给前端调用即可。
分片上传controller代码:
java
/**
* @author: jiangjs
* @description: 分片上传文件
* @date: 2023/10/25 16:05
**/
@RestController
@RequestMapping("/multipart")
public class UploadMultipartFileController {
@Autowired
private UploadMultipartFileService multipartFileService;
@GetMapping("/create.do")
public JsonResult<?> createMultipartUploadUrl(@RequestParam(value = "chunkNum",defaultValue = "0") Integer chunkNum,
@RequestParam("fileName") String fileName,
@RequestParam("contentType") String contentType){
return multipartFileService.createMultipartUploadUrl(chunkNum,fileName,contentType);
}
@GetMapping("/complete.do")
public JsonResult<?> completeMultipart(@RequestParam(value = "chunkNum",defaultValue = "0") Integer chunkNum,
@RequestParam("fileName") String fileName,
@RequestParam("contentType") String contentType,
@RequestParam("uploadId") String uploadId,
@RequestParam("fileMd5") String fileMd5){
return multipartFileService.completeMultipart(chunkNum,fileName,contentType,uploadId,fileMd5);
}
}
系统文件对应controller代码:
java
/**
* @author: jiangjs
* @description:
* @date: 2023/10/23 11:27
**/
@RestController
@RequestMapping("/sysFile")
public class OpuSysFilesController {
@Resource
private OpuSysFilesService opuSysFilesService;
@PostMapping("/uploadSingleFile.do")
public JsonResult<String> uploadSingleFile(@RequestBody MultipartFile file){
return opuSysFilesService.uploadSingleFile(file,MinioBucketEnum.EMAIL);
}
@GetMapping("/downFile.do/{md5Code}")
public void downFile(@PathVariable("md5Code") String md5Code, HttpServletResponse response){
opuSysFilesService.downFile(md5Code,MinioBucketEnum.EMAIL,response);
}
@GetMapping("/existFile.do/{md5Code}")
public JsonResult<Boolean> judgeExistFile(@PathVariable("md5Code") String md5Code){
Boolean existFile = opuSysFilesService.isExistFile(md5Code, MinioBucketEnum.EMAIL.getBucket());
return JsonResult.success(existFile);
}
}
分片上传service代码:
java
/**
* @author: jiangjs
* @description:
* @date: 2023/10/25 16:47
**/
@Service
public class UploadMultipartFileServiceImpl implements UploadMultipartFileService {
@Resource
private MinioPearUploadUtil minioPearUploadUtil;
@Resource
private OpuSysFilesMapper opuSysFilesMapper;
@Override
public JsonResult<?> createMultipartUploadUrl(Integer chunkNum, String fileName, String contentType) {
return JsonResult.success(minioPearUploadUtil.createMultipartUploadUrl(chunkNum,fileName,contentType,MinioBucketEnum.EMAIL));
}
@Override
public JsonResult<?> completeMultipart(Integer chunkNum, String fileName, String contentType,String uploadId,String fileMd5) {
if (minioPearUploadUtil.completeMultipart(chunkNum,fileName,contentType,uploadId,MinioBucketEnum.EMAIL)){
OpuSysFiles sysFiles = new OpuSysFiles();
String suffix = StringUtils.isNoneBlank(fileName) ? fileName.substring(fileName.indexOf(".")+1) : "" ;
sysFiles.setFileName(fileName)
.setBucketName(MinioBucketEnum.EMAIL.getBucket())
.setMd5Code(fileMd5).setSuffix(suffix);
opuSysFilesMapper.insert(sysFiles);
return JsonResult.success("上传成功");
}
return JsonResult.fails("上传失败");
}
}
系统文件对应service代码:
java
/**
* @author: jiangjs
* @description:
* @date: 2023/10/20 15:19
**/
@Service
@AllArgsConstructor(onConstructor = @__(@Autowired))
@Slf4j
public class OpuSysFilesServiceImpl implements OpuSysFilesService {
private final OpuSysFilesMapper opuSysFilesMapper;
private final MinioPearUploadUtil minioPearUploadUtil;
@Override
public Boolean isExistFile(String md5Code, String bucketName) {
Long count = opuSysFilesMapper.selectCount(Wrappers.<OpuSysFiles>lambdaQuery()
.eq(OpuSysFiles::getMd5Code, md5Code)
.eq(OpuSysFiles::getBucketName, bucketName));
return count > 0;
}
@Override
public JsonResult<String> uploadSingleFile(MultipartFile file, MinioBucketEnum bucketEnum) {
try (InputStream ism = file.getInputStream()){
String md5Str = DigestUtils.md5DigestAsHex(ism);
Boolean existFile = this.isExistFile(md5Str, bucketEnum.getBucket());
if (!existFile){
String fileName = md5Str + "_" + file.getOriginalFilename();
Boolean uploadFlag = minioPearUploadUtil.upLoadInputStream(ism, bucketEnum.getBucket(),fileName);
if (uploadFlag){
OpuSysFiles sysFiles = new OpuSysFiles();
String suffix = StringUtils.isNoneBlank(fileName) ? fileName.substring(fileName.indexOf(".")+1) : "" ;
sysFiles.setFileName(fileName)
.setBucketName(bucketEnum.getBucket())
.setMd5Code(md5Str).setSuffix(suffix);
opuSysFilesMapper.insert(sysFiles);
}
}
return JsonResult.success(md5Str);
} catch (Exception e) {
log.error("单个文件上传报错:"+e.getMessage());
e.printStackTrace();
return JsonResult.fails("上传文件失败");
}
}
@Override
public void downFile(String md5Code, MinioBucketEnum bucketEnum,HttpServletResponse response) {
OpuSysFiles opuSysFiles = opuSysFilesMapper.selectOne(Wrappers.<OpuSysFiles>lambdaQuery()
.eq(OpuSysFiles::getMd5Code, md5Code)
.eq(OpuSysFiles::getBucketName, bucketEnum.getBucket()));
String fileName = Objects.nonNull(opuSysFiles) ? opuSysFiles.getFileName() : "";
try (InputStream ism = minioPearUploadUtil.downLoadFile(fileName, bucketEnum.getBucket())){
String contentType = this.getFileContentType(opuSysFiles.getSuffix());
response.setContentType(contentType);
response.setHeader("Content-disposition", "inline; filename="+URLEncoder.encode(fileName, "UTF-8"));
byte[] bytes = new byte[1024];
OutputStream osm = response.getOutputStream();
int count;
while((count = ism.read(bytes)) !=-1){
osm.write(bytes, 0, count);
}
osm.flush();
osm.close();
} catch (Exception e){
log.error("下载文件报错:"+e.getMessage());
e.printStackTrace();
}
}
private String getFileContentType(String suffix){
switch (suffix){
case "jpg":
case "jpeg":
return "image/jpeg";
case "png":
return "image/png";
case "gif":
return "image/gif";
case "xml":
return "application/xml";
case "pdf":
return "application/pdf";
case "xls":
return "application/vnd.ms-excel";
case "xlsx":
return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
case "doc":
return "application/msword";
case "docx":
return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
case "ppt":
return "application/vnd.ms-powerpoint";
case "pptx":
return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
default:
return "";
}
}
}
至此后端的代码基本已经完成,接下来我们看看前端的代码。
2.1.2 前端实现
前端主要采用的是vuejs,elementui。至于创建vue项目,引入相关的依赖在这就不说了,相信小伙伴们也知道怎么弄。我们直接贴代码吧。
VU
<template>
<div>
<el-form ref="form" label-width="80px">
<el-upload
class="upload-demo"
v-loading="loading"
drag
action=""
:auto-upload="false"
:show-file-list="true"
:on-change="changeFile"
multiple>
<i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
<div class="el-upload__tip" slot="tip">文件大小不超过50MB</div>
</el-upload>
<el-progress :text-inside="true" :stroke-width="20" :percentage="percent" status="success"></el-progress>
</el-form>
</div>
</template>
<script>
import SparkMD5 from "spark-md5";
export default {
name: 'Home',
data(){
return {
chunkSize: 5 * 1024 *1024,
chunkUploadParam: {
"data":null,
"contentType":true,
"processData":false
},
uploadId:"",
loading: false,
percent:0
}
},
methods: {
async changeFile(file){
this.loading = true;
debugger;
if (!file) return;
const fileSize = file.size;
const isLt2M = fileSize / 1024 / 1024 < 50;
if (!isLt2M) {
this.$message.error('上传文件大小不能超过 50MB!');
this.loading = false;
return;
}
if (fileSize <= this.chunkSize){
this.$postReq("/sysFile/uploadSingleFile.do",{"file":file.raw},"multipart/form-data").then(resp => {
if (resp.data.code === 200){
this.$message.success("文件已上传");
} else {
this.$message.error(resp.msg.data);
}
}).catch(error => {
this.$message.error("文件失败");
})
this.loading = false;
} else {
//分片上传
//1、计算分片数量 剩余
const chunkNum = Math.floor(fileSize / this.chunkSize);
//获取文件内容MD5
const fileMd5 = await this.getFileMd5(file);
let isExist = false;
//判断当前文件在桶中是否已存在
await this.$req.get("/sysFile/existFile.do/" + fileMd5).then(resp => {
if (resp.code === 200 && resp.data){
isExist = true;
}
});
if (isExist){
this.$message.success("文件已上传");
this.loading = false;
return;
}
//向后端请求获取本次分片上传初始化
await this.$req.get("/multipart/create.do?chunkNum="+chunkNum+"&fileName="+file.name+"&contentType="+file.raw.type)
.then(resp => {
if(resp.code === 200){
const parts = resp.data.parts;
this.uploadId = resp.data.uploadId;
let item;
for (item of parts){
//分片开始位置
let startSize = (item.parkNum - 1) * this.chunkSize;
//分片结束位置
let endSize = item.parkNum === chunkNum ? fileSize : startSize + this.chunkSize;
//获取当前分片的byte信息
let chunkFile = file.raw instanceof File ? file.raw.slice(startSize,endSize) : null;
this.uploadFilePear(item.uploadUrl,chunkFile,file.raw.type,item.parkNum);
}
} else {
this.$message.error("文件调用后端进行分片失败");
}
});
await this.completeChunkFile(chunkNum,file.name,file.raw.type,this.uploadId,fileMd5);
this.loading = false;
}
},
completeChunkFile(chunkNum,fileName,contentType,uploadId,fileMd5){
this.$req.get("/multipart/complete.do?chunkNum="+chunkNum+"&fileName="+fileName+"&contentType="
+contentType+"&uploadId="+uploadId+"&fileMd5="+fileMd5)
.then(resp => {
if (resp.code === 200){
this.$message.success("文件上传成功");
console.log("文件合并成功");
}else {
this.$message.error("文件上传失败");
}
});
},
uploadFilePear(uploadUrl,chunkFile,contentType,partNum){
this.$putReq(uploadUrl,chunkFile,contentType).then(resp => {
if (resp.data.status === 200){
console.log("第" + partNum + "个分片上传完成");
}
}).catch(error => {
console.log('分片:' + partNum + ' 上传失败,' + error)
});
},
getFileMd5(file){
const reader = new FileReader();
reader.readAsBinaryString(file.raw);
const sparkMD5 = new SparkMD5();
return new Promise((resolve) => {
reader.onload = (e) => {
sparkMD5.append(e.target.result);
resolve(sparkMD5.end());
}
})
}
}
}
</script>
前端实现跟其他分片上传一样,还是比较简单的,主要是:
1、设置分片的大小,然后根据大小进行分片;
2、得到分片的数量,再调用后端的创建分片连接的接口,获取对应分片编号与上传的url;
3、然后将文件按照分片进行拆分,上传到minio;
4、上传完成后进行合并即可。
具体步骤小伙伴们可参考代码。
注:在进行分片时,最后一个分片的大小不能小于设置分片大小,否则minio会报错
至此,SpringBoot基于minio进行分片上传的功能基本已经实现。
【总结】
minio的分片主要是依赖completeMultipartUpload,createMultipartUpload,listMultipart三个方法,只需要弄懂这三个方法,就可以实现minio的分片上传。
至于前端,则跟正常的分片一样,按照设置的分片大小进行分片,调用分片相关接口即可。
上述功能,已经经过测试,小伙伴们可以拿来即用,语言组织能力有限,说的比较泛泛,有说不到位的请见谅。
我是抢老婆酸奶的小肥仔,我们下次见。
如果对你有用的话记得点赞、收藏哦。