admin管理员组

文章数量:1794759

JAVA+VUE3.0+MINIO 大文件上传(极速上传,分片上传)

JAVA+VUE3.0+MINIO 大文件上传(极速上传,分片上传)

文章目录

    • 定义分片大小
    • 急速上传
      • 文件MD5
    • 分片上传
      • 创建分片上传任务
        • 前端计算文件分片数量
        • 自定义MINIO CLIENT
        • 调用后台接口创建上传任务
        • 创建单文件上传任务
        • 创建分块上传任务
        • 校验桶是否存在(不存在则创建)
      • 分片文件合并
        • 获取指定 uploadId 下已上传的分块信
        • MINIO 文件合并

记录一下自己在实现大文件上传时的简单思路和核心代码。

大体思路如下: 1、数据库中存放文件路径,所有文件保存在 MINIO 中,文件名即是文件的 MD5。 2、当用户上传文件时,首先判断该文件信是否存在在数据库中,如果存在则直接显示上传成功(急速上传),若不存在则执行上传操作。 3、文件在真正上传之前先判断文件大小,太小的不需要创建分片上传任务,一次性上传即可。 4、后台调用 MINIO 的 API 创建分片上传任务(得到一个任务 ID ),并为该任务生成分片上传链接(上传地址列表)后返回给前端,前端将对应分片按照到对应的连接传递到 MINIO 中。 5、分片上传成功后更新进度信。 6、所有分片上传结束后,调用 MINIO 的 API 将当前任务的分片全部合并形成整个文件。

定义分片大小 const chunkSize = 5 * 1024 * 1024; // 切片大小为5M 急速上传 文件MD5

前端使用 SparkMD5 获取文件的 MD5 信,当该 MD5 信已经存在在数据库中时,即上传完成(急速上传) 下面是获取文件 MD5 的方法

import SparkMD5 from 'spark-md5'; //获取文件的MD5信 分片获取 const ReadFileMD5 = (param) => { return new Promise((resolve, reject) => { const file = param.file; const fileReader = new FileReader(); const md5 = new SparkMD5(); let index = 0; const loadFile = () => { const slice = file.slice(index, index + chunkSize); fileReader.readAsBinaryString(slice); }; loadFile(); fileReader.onload = (e) => { md5.appendBinary(e.target.result); if (index < file.size) { index += chunkSize; loadFile(); } else { // md5.end() 就是文件md5码 var md5Str = md5.end(); return resolve(md5Str); } }; fileReader.onerror = () => { reject('文件MD5获取失败'); }; }); };

当确认该文件的 MD5 在数据库中不存在时,开始触发我们的上传操作

分片上传 创建分片上传任务 前端计算文件分片数量 let chunks = Math.ceil(file.file.size / chunkSize); 自定义MINIO CLIENT

我们需要重新写一个 MINIO 客户端来实现我们的分片上传。

/** * MINIO 遵循 AmazonS3 规则,S3 有的方法他都有实现 * 关于其他方法 * 参考 MINIO 网站 * minio-JAVA.min.io/ * 结合 亚马逊官方文档 * docs.aws.amazon/AmazonS3/latest/API * 查看方法使用和效果 */ public class CustomMinioClient extends MinioClient { protected CustomMinioClient(MinioClient client) { super(client); } //创建分块上传任务 public String initMultiPartUpload(String bucket, String region, String object, Multimap<String, String> headers, Multimap<String, String> extraQueryParams) throws IOException, InvalidKeyException, NoSuchAlgorithmException, InsufficientDataException, ServerException, InternalException, XmlParserException, InvalidResponseException, ErrorResponseException { CreateMultipartUploadResponse response = this.createMultipartUpload(bucket, region, object, headers, extraQueryParams); return response.result().uploadId(); } //合并指定上传任务的分块文件 public ObjectWriteResponse mergeMultipartUpload(String bucketName, String region, String objectName, String uploadId, Part[] parts, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) throws IOException, InvalidKeyException, NoSuchAlgorithmException, InsufficientDataException, ServerException, InternalException, XmlParserException, InvalidResponseException, ErrorResponseException { return thispleteMultipartUpload(bucketName, region, objectName, uploadId, parts, extraHeaders, 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 NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, ServerException, XmlParserException, ErrorResponseException, InternalException, InvalidResponseException { return this.listParts(bucketName, region, objectName, maxParts, partNumberMarker, uploadId, extraHeaders, extraQueryParams); } } 调用后台接口创建上传任务 ... if (partCount == 1) { //只有一个分片的情况下 直接返回上传地址 String uploadObjectUrl = MinioUtils.getUploadObjectUrl(MinioUtils.FILE_WAREHOUSE,objectName); result.setUploadUrl(new ArrayList<String>(){{add(uploadObjectUrl);}}); }else { Map<String, Object> initRsl = MinioUtils.initMultiPartUpload(MinioUtils.FILE_WAREHOUSE, objectName, partCount, contentType); result.setFinished(false); result.setUploadId(initRsl.get("uploadId").toString()); result.setUploadUrl((List<String>)initRsl.get("uploadUrls")); } ...

其中比较重要的创建 MINIO 上传任务的方法如下

创建单文件上传任务 /** * 单文件上传 * * @param objectName 文件全路径名称 * @return / */ public static String getUploadObjectUrl(String bucketName, String objectName) { try { //创建 MINIO 连接 CustomMinioClient customMinioClient = new CustomMinioClient(MinioClient.builder() .endpoint(properties.getUrl())//MINIO 服务地址 .credentials(properties.getAccessKey(), properties.getSecureKey())//用户名和密码 .build()); return customMinioClient.getPresignedObjectUrl( GetPresignedObjectUrlArgs.builder() .method(Method.PUT)//GET方式请求 .bucket(bucketName)//存储桶的名字 .object(objectName)//文件的名字 .expiry(1, TimeUnit.DAYS)//上传地址有效时长 .build() ); } catch (Exception e) { return null; } } 创建分块上传任务 /** * 创建分块任务 * * @param bucketName 存储桶名称 * @param objectName 文件全路径名称 * @param partCount 分片数量 * @return / */ public static Map<String, Object> initMultiPartUpload(String bucketName,String objectName, int partCount,String contentType) { Map<String, Object> result = new HashMap<>(); try { //如果类型使用默认流会导致无法预览 contentType = "application/octet-stream"; HashMultimap<String, String> headers = HashMultimap.create(); headers.put("Content-Type", contentType); CustomMinioClient customMinioClient = new CustomMinioClient(MinioClient.builder() .endpoint(properties.getUrl()) .credentials(properties.getAccessKey(), properties.getSecureKey()) .build()); checkBucket(customMinioClient,false,bucketName); //初始化分块上传任务 String uploadId = customMinioClient.initMultiPartUpload(bucketName, null, objectName, headers, null); result.put("uploadId", uploadId); List<String> partList = new ArrayList<>(); Map<String, String> reqParams = new HashMap<>(); reqParams.put("uploadId", uploadId); for (int i = 1; i <= partCount; i++) { reqParams.put("partNumber", String.valueOf(i)) //返回带签名URL String uploadUrl = customMinioClient.getPresignedObjectUrl( GetPresignedObjectUrlArgs.builder() .method(Method.PUT)//GET方式请求 .bucket(bucketName)//存储桶的名字 .object(objectName)//文件的名字 .expiry(1, TimeUnit.DAYS)//上传地址有效时长 .extraQueryParams(reqParams)//指定任务ID和当前是第几个分块,生成上传链接 .build()); partList.add(uploadUrl); } //返回任务ID 和 分块任务列表 result.put("uploadUrls", partList); } catch (Exception e) { return null; } return result; } 校验桶是否存在(不存在则创建) /** * 检查是否存在指定桶 不存在则先创建 * @param minioClient * @param versioning * @param bucket * @throws Exception */ private static void checkBucket(MinioClient minioClient ,boolean versioning, String bucket) throws Exception { boolean exists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucket).build()); if (!exists) { minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucket).build()); //设置Procy属性 默认所有文件都能读取 遵循 AmazonS3 规则 String config = "{ " + " "Id": "Policy1", " + " "Version": "2012-10-17", " + " "Statement": [ " + " { " + " "Sid": "Statement1", " + " "Effect": "Allow", " + " "Action": [ " + " "s3:ListBucket", " + " "s3:GetObject" " + " ], " + " "Resource": [ " + " "arn:aws:s3:::"+bucket+"", " + " "arn:aws:s3:::"+bucket+"/*" " + " ]," + " "Principal": "*"" + " } " + " ] " + "}"; minioClient.setBucketPolicy( SetBucketPolicyArgs.builder().bucket(bucket).config(config).build()); } // 版本控制 VersioningConfiguration configuration = minioClient.getBucketVersioning(GetBucketVersioningArgs.builder().bucket(bucket).build()); boolean enabled = configuration.status() == VersioningConfiguration.Status.ENABLED; if (versioning && !enabled) { minioClient.setBucketVersioning(SetBucketVersioningArgs.builder() .config(new VersioningConfiguration(VersioningConfiguration.Status.ENABLED, null)).bucket(bucket).build()); } else if (!versioning && enabled) { minioClient.setBucketVersioning(SetBucketVersioningArgs.builder() .config(new VersioningConfiguration(VersioningConfiguration.Status.SUSPENDED, null)).bucket(bucket).build()); } } 分片文件合并

前端全部上传完毕之后,通知后台进行文件合并操作

... //先判断文件列表是否完整 List<String> partList = MinioUtils.getExsitParts(MinioUtils.FILE_WAREHOUSE, md5, uploadId); if (CollectionUtils.isNotEmpty(partList)) { //上传列表不是空 判断上传列表是否完整 if (chunckspareTo(partList.size()) < 0) { //缺少分片 return R.failure("文件分片缺失,请重新上传"); } else { //分片完整 整合并返回 boolean success = MinioUtils.mergeMultipartUpload(MinioUtils.FILE_WAREHOUSE, md5, uploadId); if (!success) { //合并失败 return R.failure("合并文件异常"); } } } else { return R.failure("文件分片缺失,请重新上传"); } ... 获取指定 uploadId 下已上传的分块信 public static List<String> getExsitParts(String bucketName, String objectName, String uploadId) { List<String> parts = new ArrayList<>(); try { /** * 最大分片1000 */ customMinioClient = new CustomMinioClient(MinioClient.builder() .endpoint(properties.getUrl()) .credentials(properties.getAccessKey(), properties.getSecureKey()) .build()); ListPartsResponse partResult = customMinioClient.listMultipart(bucketName, null, objectName, 1024, 0, uploadId, null, null); for (Part part : partResult.result().partList()) { parts.add(part.etag()); } //合并分片 } catch (Exception e) { // log.error("查询任务分片错误"); } return parts; } MINIO 文件合并 /** * 文件合并 * @param bucketName * @param objectName * @param uploadId * @return */ public static boolean mergeMultipartUpload(String bucketName, String objectName, String uploadId) { try { Part[] parts = new Part[1000]; /** * 默认最大分片1000 因为AmazonS规则里面默认最大分片就是1000,可以通过修改max-parts更改 */ CustomMinioClient customMinioClient = new CustomMinioClient(MinioClient.builder() .endpoint(properties.getUrl()) .credentials(properties.getAccessKey(), properties.getSecureKey()) .build()); ListPartsResponse partResult = customMinioClient.listMultipart(bucketName, null, objectName, 1000, 0, uploadId, null, null); int partNumber = 1; for (Part part : partResult.result().partList()) { parts[partNumber - 1] = new Part(partNumber, part.etag()); partNumber++; } //合并分片 customMinioClient.mergeMultipartUpload(bucketName, null, objectName, uploadId, parts, null, null); } catch (Exception e) { return false; } return true; }

先自我介绍一下,小编13年上师交大毕业,曾经在小公司待过,去过华为OPPO等大厂,18年进入阿里,直到现在。深知大多数初中级java工程师,想要升技能,往往是需要自己摸索成长或是报班学习,但对于培训机构动则近万元的学费,着实压力不小。自己不成体系的自学效率很低又漫长,而且容易碰到天花板技术停止不前。因此我收集了一份《java开发全套学习资料》送给大家,初衷也很简单,就是希望帮助到想自学又不知道该从何学起的朋友,同时减轻大家的负担。添加下方名片,即可获取全套学习资料哦

本文标签: 上传极速分片大文件java