MinIO学习笔记
分布式文件系统:MinIO 是一个非常轻量的服务,可以很简单的和其他应用的结合使用,它兼容亚马逊 S3 云存储服务接口,非常适合于存储大容量非结构化的数据,例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等。
它一大特点就是轻量,使用简单,功能强大,支持各种平台,单个文件最大5TB,兼容 Amazon S3接口,提供了 Java、Python、GO等多版本SDK支持。
Java Quickstart Guide — MinIO Object Storage for Linux
一、导入
1、安装MinIO
在这里可以指定用户名密码,以及控制台地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| mkdir -p ~/minio/data
docker run \ -d -p 9000:9000 \ -p 9001:9001 \ --name minio \ -v ~/minio/data:/data \ -e "MINIO_ROOT_USER=minioadmin" \ -e "MINIO_ROOT_PASSWORD=minioadmin" \ quay.io/minio/minio:RELEASE.2022-09-07T22-25-02Z /data --console-address ":9001" docker run \ -d \ -p 9000:9000 \ -p 9200:9200 \ --name minio \ -v ~/minio/data:/data \ -e "MINIO_ROOT_USER=minioadmin" \ -e "MINIO_ROOT_PASSWORD=minioadmin" \ quay.io/minio/minio server /data --console-address ":9200"
|
2、使用MinIO
1 2 3 4 5 6 7 8 9 10 11 12 13
| <dependency> <groupId>io.minio</groupId> <artifactId>minio</artifactId> <version>8.5.14</version> </dependency>
<dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> <version>4.8.1</version> </dependency>
|
3、创建bucket
在此之前要先创建一个bucket,以存储我们上传的文件。
-
通过控制台创建
-
通过代码创建
1 2 3 4 5 6 7
| boolean found =minioClient.bucketExists(BucketExistsArgs.builder().bucket("testbucket").build()); if (!found) { minioClient.makeBucket(MakeBucketArgs.builder().bucket("testbucket").build()); } else { System.out.println("Bucket 'testbucket' already exists."); }
|
4、上传文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| public class MinioTest {
static MinioClient minioClient = MinioClient.builder() .endpoint("http://192.168.101.129:9000/") .credentials("minioadmin", "minioadmin") .build(); @Test public void upload() { try { UploadObjectArgs testbucket = UploadObjectArgs.builder() .bucket("testbucket") .object("001/test001.mp4") .filename("C:\\Users\\Lenovo\\Desktop\\upload\\1mp4.temp") .contentType("video/mp4") .build(); minioClient.uploadObject(testbucket); System.out.println("上传成功"); } catch (Exception e) { e.printStackTrace(); System.out.println("上传失败"); } } }
|
5、删除文件
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Test public void delete(){ try { minioClient.removeObject( RemoveObjectArgs.builder().bucket("testbucket").object("001/test001.mp4").build()); System.out.println("删除成功"); } catch (Exception e) { e.printStackTrace(); System.out.println("删除失败"); } }
|
6、查询并获取文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @Test public void getFile() { GetObjectArgs getObjectArgs = GetObjectArgs.builder().bucket("testbucket").object("001/test001.mp4").build(); try( FilterInputStream inputStream = minioClient.getObject(getObjectArgs); FileOutputStream outputStream = new FileOutputStream(new File("C:\\Users\\Lenovo\\Desktop\\upload\\1_2.mp4")); ) { IOUtils.copy(inputStream,outputStream); } catch (Exception e) { e.printStackTrace(); } }
|
7、验证文件完整性。
这里实际上会将上传成功的文件再次下载下来。在本地比较文件的md5是否相同。然后返回结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| @Test public void Integrity() { try { UploadObjectArgs testbucket = UploadObjectArgs.builder() .bucket("testbucket") .object("testIntegrity/001.mp4") .filename("C:\\Users\\Lenovo\\Desktop\\upload\\1mp4.temp") .contentType("video/mp4") .build(); minioClient.uploadObject(testbucket); System.out.println("上传成功"); } catch (Exception e) { e.printStackTrace(); System.out.println("上传失败"); } GetObjectArgs getObjectArgs = GetObjectArgs.builder().bucket("testbucket").object("testIntegrity/001.mp4").build(); try( FilterInputStream inputStream = minioClient.getObject(getObjectArgs); FileOutputStream outputStream = new FileOutputStream(new File("C:\\Users\\Lenovo\\Desktop\\upload\\test1.mp4")); ) { IOUtils.copy(inputStream,outputStream); } catch (Exception e) { e.printStackTrace(); } try{ FileInputStream fileInputStream1 = new FileInputStream(new File("C:\\Users\\Lenovo\\Desktop\\upload\\1mp4.temp")); String source_md5 = DigestUtils.md5Hex(fileInputStream1); FileInputStream fileInputStream = new FileInputStream(new File("C:\\Users\\Lenovo\\Desktop\\upload\\test1.mp4")); String local_md5 = DigestUtils.md5Hex(fileInputStream); if(source_md5.equals(local_md5)){ System.out.println("文件md5值相同"); }else{ System.out.println("文件md5值不同,疑似上传失败"); } System.out.println("source_md5 = " + source_md5); System.out.println("local_md5 = " + local_md5); }catch (Exception e){ e.printStackTrace(); }
}
|
二、前置工作
1、前置工作
-
创建一个mediafiles桶
-
将创建的桶设为public
2、MinioConfig
application.yaml
1 2 3 4 5 6 7
| minio: endpoint: http://192.168.101.129:9000 accessKey: minioadmin secretKey: minioadmin bucket: files: mediafiles videofiles: video
|
配置类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| @Configuration public class MinioConfig {
@Value("${minio.endpoint}") private String endpoint; @Value("${minio.accessKey}") private String accessKey; @Value("${minio.secretKey}") private String secretKey;
@Bean public MinioClient minioClient() {
MinioClient minioClient = MinioClient.builder() .endpoint(endpoint) .credentials(accessKey, secretKey) .build(); return minioClient; } }
|
然后通过MinioClient上传文件即可
三、MinIOUtil
1、前端简单的html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <!DOCTYPE html> <html lang="en">
<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>上传Demo</title> </head>
<body> <h2>文件上传</h2>
<form action="http://localhost:8080/upload/testUtil" method="POST" enctype="multipart/form-data"> <label for="filedata">选择文件:</label> <input type="file" name="filedata" id="filedata" required><br><br> <button type="submit">上传文件</button> </form> </body>
</html>
|
2、后端代码(图片上传)
先导入这3个依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <dependency> <groupId>io.minio</groupId> <artifactId>minio</artifactId> <version>8.5.14</version> </dependency>
<dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> <version>4.8.1</version> </dependency>
<dependency> <groupId>com.j256.simplemagic</groupId> <artifactId>simplemagic</artifactId> <version>1.17</version> </dependency>
|
Controller样例
1 2 3 4 5 6 7 8 9 10 11
| @PostMapping("/testUtil") public String upload2(MultipartFile filedata) throws IOException {
String upload = MinIOUtil.upload(filedata);
if(upload==null){ return "上传文件失败"; }else{ return "上传文件成功:"+upload; } }
|
使用封装好的MinIOUtil上传文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107
| public class MinIOUtil {
static private String endpoint = "http://192.168.101.129:9000"; static private String accessKey = "minioadmin"; static private String secretKey = "minioadmin"; static private String filesBucket ="mediafiles";
static private MinioClient minioClient;
static public String upload(MultipartFile filedata) throws IOException {
minioClient = MinioClient.builder() .endpoint(endpoint) .credentials(accessKey, secretKey) .build();
File tempFile = File.createTempFile("minio", ".temp"); filedata.transferTo(tempFile);
String absolutePath = tempFile.getAbsolutePath();
String originalFilename = filedata.getOriginalFilename(); String extension = originalFilename.substring(originalFilename.lastIndexOf(".")); String mimeType = getMimeType(extension);
String defaultFolderPath = getDefaultFolderPath();
String fileMd5 = getFileMd5(tempFile);
String objectName = defaultFolderPath+fileMd5+extension;
boolean b = addMediaFilesToMinIO(absolutePath, mimeType, filesBucket, objectName);
if(!b){ return null; }else{ return endpoint+"/"+filesBucket+"/"+objectName; } }
static private boolean addMediaFilesToMinIO(String localFilePath, String mimeType, String bucket, String objectName) { try { UploadObjectArgs testbucket = UploadObjectArgs.builder() .bucket(bucket) .filename(localFilePath) .object(objectName) .contentType(mimeType) .build(); minioClient.uploadObject(testbucket);
return true; } catch (Exception e) { e.printStackTrace();
} return false; }
static private String getMimeType(String extension) { if(extension==null) extension = ""; ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension); String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE; if (extensionMatch != null) { mimeType = extensionMatch.getMimeType(); } return mimeType; }
static private String getFileMd5(File file) { try (FileInputStream fileInputStream = new FileInputStream(file)) { String fileMd5 = DigestUtils.md5Hex(fileInputStream); return fileMd5; } catch (Exception e) { e.printStackTrace(); return null; } }
static private String getDefaultFolderPath() { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); String folder = sdf.format(new Date()).replace("-", "/")+"/"; return folder; }
}
|
upload方法执行成功后,会得到上传文件链接
四、断点续传
上传或下载任务中,将需要上传或者下载的文件,人为分成多个部分,分别上传和下载。如果遇到网络故障,可以从中断的地方继续上传或者下载。
1、文件本地分块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| @Test public void testChunk() throws IOException { File sourseFile = new File("C:\\Users\\Lenovo\\Desktop\\upload\\1a.mp4"); String chunkFilePath = "C:\\Users\\Lenovo\\Desktop\\upload\\chunk\\"; int chunkSize = 1024*1024 * 5; int chunkNum =(int)Math.ceil(sourseFile.length() * 1.0 / chunkSize) ;
RandomAccessFile raf_r = new RandomAccessFile(sourseFile, "r");
byte[] bytes = new byte[1024]; for (int i = 0; i < chunkNum; i++) {
File chunkFile = new File(chunkFilePath + i);
RandomAccessFile raf_rw = new RandomAccessFile(chunkFile, "rw"); int len = -1; while((len=raf_r.read(bytes))!=-1){ raf_rw.write(bytes,0,len); if(chunkFile.length()>=chunkSize){ break; } } raf_rw.close(); } raf_r.close();
}
|
2、测试本地合并
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| @Test public void testMerge() throws IOException { File sourseFile = new File("C:\\Users\\Lenovo\\Desktop\\upload\\1a.mp4");
File chunkFolder = new File("C:\\Users\\Lenovo\\Desktop\\upload\\chunk\\"); File mergeFile = new File("C:\\Users\\Lenovo\\Desktop\\upload\\1a_merge.mp4");
File[] files = chunkFolder.listFiles(); List<File> fileList = Arrays.asList(files);
Collections.sort(fileList, new Comparator<File>() { @Override public int compare(File o1, File o2) { return Integer.parseInt(o1.getName())-Integer.parseInt(o2.getName()); } });
RandomAccessFile raf_rw = new RandomAccessFile(mergeFile, "rw"); byte[] bytes = new byte[1024]; for (File file : fileList) { RandomAccessFile raf_r = new RandomAccessFile(file, "r"); int len= -1;
while ((len=raf_r.read(bytes)) !=-1){ raf_rw.write(bytes,0,len); } raf_r.close();
} raf_rw.close();
FileInputStream fileInputStream_merge = new FileInputStream(mergeFile); FileInputStream fileInputStream_source = new FileInputStream(sourseFile);
String md5_merge = DigestUtils.md5Hex(fileInputStream_merge); String md5_source = DigestUtils.md5Hex(fileInputStream_source);
if(md5_merge.equals(md5_source)){ System.out.println("文件合并成功!"); }
}
|
MinIO
3、分块文件上传MinIO
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @Test public void uploadChunk() throws Exception{ for(int i=0;i<3;i++){ UploadObjectArgs testbucket = UploadObjectArgs.builder() .bucket("testbucket") .filename("C:\\Users\\Lenovo\\Desktop\\upload\\chunk\\"+i) .object("chunk/"+i) .build(); minioClient.uploadObject(testbucket); System.out.println("上传分块"+i+"成功"); } }
|
4、通过minio的合并文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| @Test public void testMerge()throws Exception{
List<ComposeSource> sources = Stream.iterate(0, i -> ++i) .limit(3) .map(i -> ComposeSource.builder() .bucket("testbucket") .object("chunk/".concat(Integer.toString(i))) .build()) .collect(Collectors.toList());
ComposeObjectArgs testbucket = ComposeObjectArgs.builder() .bucket("testbucket") .sources(sources) .object("merge01.mp4") .build(); minioClient.composeObject(testbucket); }
|
5、删除minio中的分块文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @Test public void test_removeObjects(){ List<DeleteObject> deleteObjects = Stream.iterate(0, i -> ++i) .limit(3) .map(i -> new DeleteObject("chunk/".concat(Integer.toString(i)))) .collect(Collectors.toList());
RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs.builder().bucket("testbucket").objects(deleteObjects).build(); Iterable<Result<DeleteError>> results = minioClient.removeObjects(removeObjectsArgs); results.forEach(r->{ DeleteError deleteError = null; try { deleteError = r.get(); } catch (Exception e) { e.printStackTrace(); } }); }
|
五、项目中的用法(断点续传)
1、查询文件是否存在
一般来说我们上传到minio的文件需要一个数据表来存放。
以md5为id主键。这样查询是否上传过文件很方便。
查询文件是否已经上传过,如果上传过。就不需要再上传,也不需要再分块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| public RestResponse<Boolean> checkFile(String fileMd5) { MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5); if(mediaFiles != null){ String bucket = mediaFiles.getBucket(); String filePath = mediaFiles.getFilePath();
GetObjectArgs testbucket = GetObjectArgs.builder() .bucket(bucket) .object(filePath) .build();
try{ FilterInputStream inputStream = minioClient.getObject(testbucket); if(inputStream !=null){ inputStream.close(); return RestResponse.success(true); } }catch (Exception e){ e.printStackTrace(); } } return RestResponse.success(false); }
|
2、上传分块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| private String getChunkFileFolderPath(String fileMd5) { return fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/" + "chunk" + "/"; }
@Override public RestResponse uploadChunk(String fileMd5, int chunk, String localChunkFilePath) { String chunkFilePath = getChunkFileFolderPath(fileMd5)+chunk;
String mimeType = getMimeType(null); boolean b = addMediaFilesToMinIO(localChunkFilePath, mimeType, bucket_videofiles, chunkFilePath);
if(!b){ return RestResponse.validfail(false,"上传分块文件失败"); } return RestResponse.success(true); }
|
3、查询分块文件是否存在
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| @Override public RestResponse<Boolean> checkChunk(String fileMd5, int chunkIndex) { String chunkFileFolderPath = getChunkFileFolderPath(fileMd5); GetObjectArgs testbucket = GetObjectArgs.builder() .bucket(bucket_videofiles) .object(chunkFileFolderPath+chunkIndex) .build(); try{ FilterInputStream inputStream = minioClient.getObject(testbucket); if(inputStream !=null){ inputStream.close(); return RestResponse.success(true); } }catch (Exception e){ e.printStackTrace(); } return RestResponse.success(false);
}
|
4、合并文件
-
构建好sources然后合并分块
-
下载合并好的文件,检验md5,验证完整性
-
文件信息入数据库
-
清除分块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| @Override public RestResponse mergechunks(Long companyId, String fileMd5, int chunkTotal, UploadFileParamsDto uploadFileParamsDto) {
String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
List<ComposeSource> sources = Stream.iterate(0, i -> ++i) .limit(chunkTotal) .map(i -> ComposeSource.builder() .bucket(bucket_videofiles) .object(chunkFileFolderPath+i) .build()) .collect(Collectors.toList());
String filename = uploadFileParamsDto.getFilename(); String extension = filename.substring(filename.lastIndexOf(".")); String objectName = getFilePathByMd5(fileMd5, extension); ComposeObjectArgs testbucket = ComposeObjectArgs.builder() .bucket(bucket_videofiles) .sources(sources) .object(objectName) .build(); try{ minioClient.composeObject(testbucket); }catch (Exception e){ e.printStackTrace(); log.error("合并文件出错,bucket:{},objectName:{},错误信息:{}",bucket_videofiles,objectName,e.getMessage()); return RestResponse.validfail(false,"合并文件异常"); }
File file = downloadFileFromMinIO(bucket_videofiles, objectName); try { FileInputStream inputStream = new FileInputStream(file); String mergeFile_md5 = DigestUtils.md5Hex(inputStream);
if(!fileMd5.equals(mergeFile_md5)){ log.error("校验合并文件MD5值不一致,原始文件:{},合并文件:{}",fileMd5,mergeFile_md5); return RestResponse.validfail(false,"文件校验失败"); } uploadFileParamsDto.setFileSize(file.length()); } catch (IOException e) { return RestResponse.validfail(false,"文件校验失败"); }
MediaFiles mediaFiles = ProxyService.addMediaFilesToDb(companyId, fileMd5, uploadFileParamsDto, bucket_videofiles, objectName); if(mediaFiles == null){ return RestResponse.validfail(false,"文件入库失败"); } clearChunkFiles(chunkFileFolderPath,chunkTotal); return RestResponse.success(true); }
|
5、清除分块文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| private void clearChunkFiles(String chunkFileFolderPath,int chunkTotal){
try { List<DeleteObject> deleteObjects = Stream.iterate(0, i -> ++i) .limit(chunkTotal) .map(i -> new DeleteObject(chunkFileFolderPath.concat(Integer.toString(i)))) .collect(Collectors.toList());
RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs.builder().bucket("video").objects(deleteObjects).build(); Iterable<Result<DeleteError>> results = minioClient.removeObjects(removeObjectsArgs); results.forEach(r->{ DeleteError deleteError = null; try { deleteError = r.get(); } catch (Exception e) { e.printStackTrace(); log.error("清楚分块文件失败,objectname:{}",deleteError.objectName(),e); } }); } catch (Exception e) { e.printStackTrace(); log.error("清楚分块文件失败,chunkFileFolderPath:{}",chunkFileFolderPath,e); } }
|