SpringBoot集成Minio实现文件存储管理

前言

文章使用springboot项目集成minio实现文件存储管理,举例使用文件分片上传和文件分块的合并。

1.minio安装

本篇文章简单介绍一下docker安装minio的方式。

# 搜索minio镜像并拉取
docker search minio
docker pull
# 创建对应的目录,conf,data
# 启动容器
docker run -p 9000:9000 -p 9001:9001 --name minio1 -d --restart=always  -e MINIO_ACCESS_KEY=minio -e MINIO_SECRET_KEY=password  -v /usr/docker/minio/data:/data  -v /usr/docker/minio/config:/root/.minio  minio/minio server /data  --console-address ":9001"
# 这里设置的控制台端口为9001,控制台账号/密码:minio/password
# springboot中配置就使用9000端口

老规矩,云服务器的话记得去安全组配置端口出入规则。

2.springboot集成minio

(1)maven依赖


<dependency><groupId>io.miniogroupId><artifactId>minioartifactId><version>8.3.0version><exclusions><exclusion><groupId>com.squareup.okhttp3groupId><artifactId>okhttpartifactId>exclusion>exclusions>
dependency>
<dependency><groupId>com.squareup.okhttp3groupId><artifactId>okhttpartifactId><version>4.8.1version>
dependency>

(2)配置文件

这里yml没有要求配置,为了使用方便,将几个重要参数配置到yml中,如下:

minio:endpoint: http://ip:port/accessKey: miniosecretKey: passworddataPath: /data/minio/data/

获取yml中配置的属性文件:

@Data
@Component
@ConfigurationProperties(prefix = "minio")
public class MinioProp {/** 连接url */private String endpoint;/** 用户名 */private String accessKey;/** 密码 */private String secretKey;/** 文件路径 */private String dataPath;
}

注入minioClient配置文件:

@Configuration
public class MinioConf {@Autowiredprivate MinioProp minioProp;@Beanpublic MinioClient getMinioClient() {return MinioClient.builder().endpoint(minioProp.getEndpoint()).credentials(minioProp.getAccessKey(), minioProp.getSecretKey()).build();}
}

到此minio集成就完成了,可以直接操作使用了,minio基本原理就是创建桶(bucket),上传文件到指定桶,其实桶就相当于一个文件夹,在挂载的data目录下可以找到。简单了解后,下面举例使用minio上传视频分片,然后合并视频分片为原视频。

3.视频分片上传和分片合并

对某些大文件尤其是大视频进行上传的时候,如果直接上传可能会导致效率低下,系统报错(springboot有文件上传大小限制,当文件过大时,jvm配置内存过小也会导致报错)等,这个时候就需要使用分片处理提高效率和功能稳定性。

(1)介绍

分片上传将大文件切割成较小的片段,在上传过程中逐个上传这些片段,可以更好地处理网络不稳定或断网等情况。
对文件的分片可以前端处理也可以后端处理,二者各有不同:
前端处理:
减轻服务器负担:前端直接将文件分片上传到服务器,减轻了后端服务器的负担,特别是在高并发上传的情况下,可以更好地分散服务器压力。
较低的延迟:前端直接与服务器通信,减少了上传片段的延迟,因为没有必要将分片先上传到前端服务器再转发到后端。
即时错误处理:前端可以更快地检测和处理上传错误,例如网络中断、超时等,并及时提供反馈给用户。
后端处理:
安全性:后端更容易实施严格的权限控制,确保只有授权用户可以上传文件和访问文件。避免前端直接上传对文件管理的滥用。
稳定性:后端可以更好地处理复杂的上传逻辑和错误处理,例如处理分片的完整性校验,确保所有分片上传成功后再进行合并。
灵活性:后端可以根据服务器资源和网络情况进行动态调整,例如设置合适的分片大小(这一点对minio很适用,后面会提到),优化上传速度。
通常,最佳实践是前后端共同处理分片上传。前端处理文件切割和上传,后端处理分片的接收、存储和合并。这样可以最大程度地发挥各自的优势,提高上传的效率和稳定性。当然,在实际中,根据需求和应用场景选择合适的方案。

(2)分片上传与合并

这里采用的是前端分片,后端负责接收片段,并上传到minio中。

前端demo代码:

<input type="file" id="videoFileInput">
<button onclick="uploadVideo()">Upload Videobutton>
<div id="progressBar">div><script>function uploadVideo() {const fileInput = document.getElementById('videoFileInput');const file = fileInput.files[0];const chunkSize = 1024 * 1024 * 5; // 每个片段大小(5MB)let start = 0;let end = Math.min(chunkSize, file.size);let chunkNumber = 0;const progressBar = document.getElementById('progressBar');function uploadChunk() {const chunk = file.slice(start, end);const formData = new FormData();formData.append('file', chunk);formData.append('chunkNumber', chunkNumber);const xhr = new XMLHttpRequest();xhr.open('POST', 'http://localhost:9200/test/video/uploadVideoChunk', true);xhr.onload = function () {if (end < file.size) {start = end;end = Math.min(end + chunkSize, file.size);chunkNumber++;uploadChunk();} else {progressBar.innerHTML = 'Upload complete!';}};xhr.upload.onprogress = function (event) {const percent = (event.loaded / event.total) * 100;progressBar.innerHTML = 'Uploading... ' + percent.toFixed(2) + '%';};xhr.send(formData);}uploadChunk();}
script>

后端demo代码:
注意:下面使用到的Result 为封装的返回参,自己可以修改为自己的返回值类型。

a. 封装的上传工具类:

@Slf4j
@Component
public class MinioUtils {@Autowiredprivate MinioClient client;@Autowiredprivate MinioProp minioProp;/*** 创建bucket* @param bucketName bucket名称*/@SneakyThrowspublic void createBucket(String bucketName) {if (!client.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build())) {client.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());}}/*** 上传文件* @param file       文件* @param bucketName 存储桶* @return */public Result uploadFile(MultipartFile file, String bucketName, String fileName) throws Exception {// 判断上传文件是否为空if (null == file || 0 == file.getSize()) {return Result.fail("上传文件不能为空");}try {// 判断存储桶是否存在createBucket(bucketName);// 自定义上传参数,后续可以取到Map<String, String> userMetaDataMap = new HashMap<>();userMetaDataMap.put("dataPath", minioProp.getDataPath());// 开始上传client.putObject(PutObjectArgs.builder().bucket(bucketName).object(fileName).stream(file.getInputStream(), file.getSize(), -1).userMetadata(userMetaDataMap).contentType(file.getContentType()).build());return Result.success(minioProp.getEndpoint() + "/" + bucketName + "/" + fileName);}  catch (Exception e) {log.error("上传文件失败:{}", e.getMessage());return Result.fail("上传失败");}}
}

分片上传接口:

@Api(tags = "视频接口")
@RequestMapping("/test/video")
@RestController
@Slf4j
public class VideoUploadController {@Autowiredprivate MinioClient minioClient;@Autowiredprivate MinioUtils minioUtils;@Autowiredprivate MinioProp minioProp;private final String bucketName = "video";@PostMapping("/uploadVideoChunk")@ApiOperation("上传视频片段")public Result uploadVideoChunk(@RequestParam("file") MultipartFile file, @RequestParam("chunkNumber") int chunkNumber) {try {minioUtils.uploadFile(file, bucketName, "video_temp_" + chunkNumber);return Result.success("上传成功");} catch (Exception e) {e.printStackTrace();return Result.fail("上传失败");}}@PostMapping("/mergeFileChunks")@ApiOperation("合并文件块")public Result mergeVideoChunks(@RequestParam String newFileName) throws Exception {List<String> fileNameList = getFileName(bucketName);// 对视频片段用最后的编号进行排序,避免顺序错误导致合并文件出问题Collections.sort(fileNameList, (o1, o2) -> {// 截取出文件编号if (Integer.parseInt(o2.substring(o2.lastIndexOf("_")+1)) > Integer.parseInt(o1.substring(o1.lastIndexOf("_")+1))){return -1;}return 1;});List<ComposeSource> composeSourceList = new ArrayList<>();for (String s : fileNameList) {ComposeSource build = ComposeSource.builder().bucket(bucketName).object(s).build();composeSourceList.add(build);}try { // 上面抛出了异常,但是这里还是使用try-catch是刚写完为了判断是否合并成功用的minioClient.composeObject(ComposeObjectArgs.builder().object(newFileName).bucket(bucketName).sources(composeSourceList).build());} catch (Exception e) {e.printStackTrace();log.info("合并文件分块失败");return Result.fail("合并失败");}// 删除原文件deleteTemporaryObjects(fileNameList);return Result.success();}/*** 获取指定桶内的所有文件*/private List<String> getFileName(String bucketName) throws Exception {Iterable<io.minio.Result<Item>> results = minioClient.listObjects(ListObjectsArgs.builder().bucket(bucketName).build());List<String> fileNameList = new ArrayList<>();for (io.minio.Result<Item> result : results) {Item item = result.get();String objectName = item.objectName();fileNameList.add(objectName);}return fileNameList;}private void deleteTemporaryObjects(List<String> tempObjects) throws Exception {for (String objectName : tempObjects) {minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(objectName).build());}}
}

(3)报错:chunk size must be greater than 5242880

刚开始执行的时候就遇到了这个报错,经过查阅资料后得知,minio要求分片大小得大于5M,最后一个分片可以小于5M,如果其余分片小于5M的时候,就会报这个错,也可以点开合并的源码查看。
合并文件块源码:
在这里插入图片描述这里可以看到有有个calculatePartCount()方法,见名知意得知,计算分片数量的,点进去查看,可以看到就是这做了大小判断,小于5M就报错。
在这里插入图片描述我最开始报错,因为我前端设置的分片大小为1M,所以直接就报错 了,得知原因后,就改为了5M,然后测试一下就合并成功了,网上查了大量资料,有的说改服务器minio的配置文件,有的说改源码,但是都只是说,没找到哪个大哥改过然后成功了,其实5M分片已经能满足需求了,所以我这也没去试验了。
前面介绍中提及的后端分片可以灵活设置分片大小,也就是这个原因了,但是其实前后端统一一下分片大小就行了。

(4)补充

当使用minio合并完成视频后,打开minio控制台,可以看到视频是合并完成的,这里的video.mp4就是我合并完成后的视频文件,点击下载,也是可以正常播放的。
在这里插入图片描述但是这里我突然想利用FFmpeg对视频做一些操作(例如,转码,裁剪,字幕添加,音频提取等),当我完成对应代码,点击执行,始终提示报错,我到服务器去执行FFmpeg命令,仍然报错,提示我video.mp4 是一个文件夹,我就去minio存储路径查找对应文件,结果如下:
在这里插入图片描述可以看到,这里的video.mp4确实是个文件夹,下面有一个文件夹和一个文件,进入到文件夹后发现了我们上传的视频片段,这里我去询问了一下ChatGPT,以下是ChatGPT的回答:


当你在 Minio 控制台看到的是一个 video.mp4 的文件,而在服务器存储路径下看到的是一个包含 配置文件和包含四个片段视频文件的文件夹,这可能是因为 Minio 在存储对象时会生成多个部分文件,并在内部维护一个 配置文件以跟踪这些部分文件。


这里我又去查看了一下minioClient有没有提供可以使用的方法,这里我找到了下载方法:minioClient.downloadObject(),直接使用这个方法本地测试一下,发现下载下来的文件是video.mp4,可以正常播放的。下载代码:

/*** 下载* @param targetPath 目标路径* @param fileName 新文件名,包含后缀* @throws Exception*/
private void downloadFile(String targetPath, String fileName) throws Exception {minioClient.downloadObject(DownloadObjectArgs.builder().bucket(bucketName).object("video.mp4") // 这里的video.mp4我写死的,可以根据需要修改,这里仅做参考.filename(targetPath + "/" + fileName).build());
}

综上所述,我想到的方法就是,通过下载方法,下载到一个临时路径,再对临时路径下的视频文件做FFmpeg操作,最后对生成的文件上传到minio,然后删除临时文件,但是考虑到这里下载速度慢和转码操作慢等因素,可以直接扔到队列里面实现相关功能。
如果有哪位大佬有其他方法的,欢迎指出,相互学习。


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部