一、为什么选择MinIO?
在选择文件存储方案时,我们需要考虑多个因素,如功能、性能、成本、扩展性等。相比其他常见的文件存储方案,MinIO 具有以下优势:
1.功能丰富
MinIO支持标准的S3协议,可以与其他支持S3协议的工具和服务无缝集成。同时,它还提供了丰富的API,包括文件上传、下载、预览、删除、版本控制等,满足各种文件管理需求。
2.性能
MinIO 专为高性能设计,采用异构架构,可以横向扩展,支持 PB 级数据存储。在读写性能方面,MinIO 表现,尤其出色适合大文件的存储和处理。
3.开源免费
MinIO 是开源项目,采用 AGPL v3 许可证,企业可以免费使用。对于中小企业来说,这无疑是一个很大的优势。
4. 易于安装和管理
MinIO 提供了简单的仪表板工具和 Web 界面,配置和管理都非常方便。可以在几分钟内完成配置,并开始使用。
5. 数据安全
MinIO支持数据加密、访问控制、多因素认证等安全功能,保障数据的安全性和隐私性。
对比其他方案
-
Nginx: 主要用于静态文件服务,不支持仓储和大规模文件管理。
-
FastDFS: 功能相对简单,缺乏统一的管理界面,扩展性有限。
-
阿里云OSS: 云服务成本较高,依赖于网络环境,不适合数据隐私要求较高的场景。
综上所述,MinIO 是一个功能强大、性能出色、易于部署和管理的文件存储方案,非常适合作为企业级文件存储系统。
二、环境准备
1.安装MinIO
可以通过 Docker 快速安装 MinIO:
docker run -p 9000:9000 -p 9001:9001 \
--name minio \
-v /data/minio/data:/data \
-v /data/minio/config:/root/.minio \
-e "MINIO_ROOT_USER=minioadmin" \
-e "MINIO_ROOT_PASSWORD=minioadmin" \
minio/minio server /data --console-address ":9001"
安装完成后,可以通过访问http://localhost:9001进入MinIO管理界面,使用用户名minioadmin和密码minioadmin登录。
2.创建SpringBoot项目
使用 Spring Initializr 创建一个 SpringBoot 项目,添加以下依赖:
-
Spring Web
-
龙目岛
-
MinIO 客户端
三、整合MinIO
1.增加依赖
在pom.xml中添加MinIO客户端依赖:
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.5.5</version>
</dependency>
2.配置MinIO连接信息
在application.yml中添加MinIO配置信息:
minio:
endpoint: http://localhost:9000
access-key: minioadmin
secret-key: minioadmin
bucket-name: test-bucket
3.创建MinIO配置类
创建一个配置类,用于创建MinIO客户端:
import io.minio.MinioClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
publicclass MinIOConfig {
@Value("${minio.endpoint}")
private String endpoint;
@Value("${minio.access-key}")
private String accessKey;
@Value("${minio.secret-key}")
private String secretKey;
@Bean
public MinioClient minioClient() {
return MinioClient.builder()
.endpoint(endpoint)
.credentials(accessKey, secretKey)
.build();
}
}
四、创建MinIO工具类
为了方便使用 MinIO 的各种功能,我们创建了一个工具类,封装了 MinIO 的常用操作:
import io.minio.*;
import io.minio.http.Method;
import io.minio.messages.Bucket;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import io.minio.messages.Item;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Component
publicclass MinioUtil {
@Autowired
private MinioClient minioClient;
@Value("${minio.bucket-name}")
private String defaultBucketName;
/**
* 检查存储桶是否存在
* @param bucketName 存储桶名称
* @return 是否存在
*/
@SneakyThrows
public boolean bucketExists(String bucketName) {
return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
}
/**
* 创建存储桶
* @param bucketName 存储桶名称
*/
@SneakyThrows
public void makeBucket(String bucketName) {
if (!bucketExists(bucketName)) {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
}
}
/**
* 获取所有存储桶
* @return 存储桶列表
*/
@SneakyThrows
public List<Bucket> listBuckets() {
return minioClient.listBuckets();
}
/**
* 删除存储桶
* @param bucketName 存储桶名称
*/
@SneakyThrows
public void removeBucket(String bucketName) {
minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
}
/**
* 简单文件上传
* @param file 文件
* @param bucketName 存储桶名称
* @return 文件信息
*/
@SneakyThrows
public Map<String, String> uploadFile(MultipartFile file, String bucketName) {
if (file == null || file.isEmpty()) {
returnnull;
}
if (!bucketExists(bucketName)) {
makeBucket(bucketName);
}
String originalFilename = file.getOriginalFilename();
String fileName = UUID.randomUUID().toString() + originalFilename.substring(originalFilename.lastIndexOf("."));
minioClient.putObject(PutObjectArgs.builder()
.bucket(bucketName)
.object(fileName)
.contentType(file.getContentType())
.stream(file.getInputStream(), file.getSize(), -1)
.build());
Map<String, String> resultMap = new HashMap<>();
resultMap.put("fileName", fileName);
resultMap.put("originalFilename", originalFilename);
resultMap.put("url", getObjectUrl(bucketName, fileName, 7));
return resultMap;
}
/**
* 简单文件上传(使用默认存储桶)
* @param file 文件
* @return 文件信息
*/
public Map<String, String> uploadFile(MultipartFile file) {
return uploadFile(file, defaultBucketName);
}
/**
* 批量文件上传
* @param files 文件列表
* @param bucketName 存储桶名称
* @return 文件信息列表
*/
public List<Map<String, String>> uploadFiles(List<MultipartFile> files, String bucketName) {
return files.stream()
.map(file -> uploadFile(file, bucketName))
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
/**
* 批量文件上传(使用默认存储桶)
* @param files 文件列表
* @return 文件信息列表
*/
public List<Map<String, String>> uploadFiles(List<MultipartFile> files) {
return uploadFiles(files, defaultBucketName);
}
/**
* 下载文件
* @param bucketName 存储桶名称
* @param objectName 对象名称
* @return 输入流
*/
@SneakyThrows
public InputStream downloadFile(String bucketName, String objectName) {
return minioClient.getObject(GetObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.build());
}
/**
* 下载文件(使用默认存储桶)
* @param objectName 对象名称
* @return 输入流
*/
public InputStream downloadFile(String objectName) {
return downloadFile(defaultBucketName, objectName);
}
/**
* 删除文件
* @param bucketName 存储桶名称
* @param objectName 对象名称
*/
@SneakyThrows
public void deleteFile(String bucketName, String objectName) {
minioClient.removeObject(RemoveObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.build());
}
/**
* 删除文件(使用默认存储桶)
* @param objectName 对象名称
*/
public void deleteFile(String objectName) {
deleteFile(defaultBucketName, objectName);
}
/**
* 批量删除文件
* @param bucketName 存储桶名称
* @param objectNames 对象名称列表
* @return 删除错误列表
*/
@SneakyThrows
public List<DeleteError> deleteFiles(String bucketName, List<String> objectNames) {
List<DeleteObject> objects = objectNames.stream()
.map(DeleteObject::new)
.collect(Collectors.toList());
Iterable<Result<DeleteError>> results = minioClient.removeObjects(RemoveObjectsArgs.builder()
.bucket(bucketName)
.objects(objects)
.build());
List<DeleteError> errors = new ArrayList<>();
for (Result<DeleteError> result : results) {
errors.add(result.get());
}
return errors;
}
/**
* 批量删除文件(使用默认存储桶)
* @param objectNames 对象名称列表
* @return 删除错误列表
*/
public List<DeleteError> deleteFiles(List<String> objectNames) {
return deleteFiles(defaultBucketName, objectNames);
}
/**
* 获取文件URL
* @param bucketName 存储桶名称
* @param objectName 对象名称
* @param expires 过期时间(天)
* @return 文件URL
*/
@SneakyThrows
public String getObjectUrl(String bucketName, String objectName, int expires) {
return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
.method(Method.GET)
.bucket(bucketName)
.object(objectName)
.expiry(expires, TimeUnit.DAYS)
.build());
}
/**
* 获取文件URL(使用默认存储桶)
* @param objectName 对象名称
* @param expires 过期时间(天)
* @return 文件URL
*/
public String getObjectUrl(String objectName, int expires) {
return getObjectUrl(defaultBucketName, objectName, expires);
}
/**
* 检查文件是否存在
* @param bucketName 存储桶名称
* @param objectName 对象名称
* @return 是否存在
*/
@SneakyThrows
public boolean objectExists(String bucketName, String objectName) {
try {
minioClient.statObject(StatObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.build());
returntrue;
} catch (Exception e) {
returnfalse;
}
}
/**
* 检查文件是否存在(使用默认存储桶)
* @param objectName 对象名称
* @return 是否存在
*/
public boolean objectExists(String objectName) {
return objectExists(defaultBucketName, objectName);
}
/**
* 列出存储桶中的所有对象
* @param bucketName 存储桶名称
* @return 对象列表
*/
@SneakyThrows
public List<Item> listObjects(String bucketName) {
Iterable<Result<Item>> results = minioClient.listObjects(ListObjectsArgs.builder()
.bucket(bucketName)
.build());
List<Item> items = new ArrayList<>();
for (Result<Item> result : results) {
items.add(result.get());
}
return items;
}
/**
* 列出存储桶中的所有对象(使用默认存储桶)
* @return 对象列表
*/
public List<Item> listObjects() {
return listObjects(defaultBucketName);
}
/**
* 创建分片上传
* @param bucketName 存储桶名称
* @param objectName 对象名称
* @return 上传ID
*/
@SneakyThrows
public String createMultipartUpload(String bucketName, String objectName) {
CreateMultipartUploadResponse response = minioClient.createMultipartUpload(CreateMultipartUploadArgs.builder()
.bucket(bucketName)
.object(objectName)
.build());
return response.result().uploadId();
}
/**
* 上传分片
* @param bucketName 存储桶名称
* @param objectName 对象名称
* @param uploadId 上传ID
* @param partNumber 分片编号
* @param stream 输入流
* @param size 大小
* @return 分片ETag
*/
@SneakyThrows
public String uploadPart(String bucketName, String objectName, String uploadId, int partNumber, InputStream stream, long size) {
UploadPartResponse response = minioClient.uploadPart(UploadPartArgs.builder()
.bucket(bucketName)
.object(objectName)
.uploadId(uploadId)
.partNumber(partNumber)
.stream(stream, size, -1)
.build());
return response.etag();
}
/**
* 完成分片上传
* @param bucketName 存储桶名称
* @param objectName 对象名称
* @param uploadId 上传ID
* @param etags 分片ETag列表
*/
@SneakyThrows
public void completeMultipartUpload(String bucketName, String objectName, String uploadId, List<String> etags) {
List<CompletePart> completeParts = new ArrayList<>();
for (int i = 0; i < etags.size(); i++) {
completeParts.add(new CompletePart(i + 1, etags.get(i)));
}
minioClient.completeMultipartUpload(CompleteMultipartUploadArgs.builder()
.bucket(bucketName)
.object(objectName)
.uploadId(uploadId)
.parts(completeParts)
.build());
}
/**
* 生成文件哈希值(用于秒传判断)
* @param file 文件
* @return 哈希值
*/
@SneakyThrows
public String generateFileHash(MultipartFile file) {
// 这里使用简单的文件大小和修改时间作为哈希值,实际应用中应使用MD5或SHA-1等算法
return file.getSize() + "-" + file.getOriginalFilename();
}
}
五、创建控制器
接下来,我们创建一个 Controller,提供各种文件操作的接口:
import io.minio.messages.Item;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/minio")
publicclass MinioController {
@Autowired
private MinioUtil minioUtil;
/**
* 简单文件上传
*/
@PostMapping("/upload")
public ResponseEntity<Map<String, Object>> uploadFile(@RequestParam("file") MultipartFile file) {
Map<String, Object> result = new HashMap<>();
try {
Map<String, String> fileInfo = minioUtil.uploadFile(file);
if (fileInfo != null) {
result.put("code", 200);
result.put("message", "上传成功");
result.put("data", fileInfo);
return ResponseEntity.ok(result);
} else {
result.put("code", 500);
result.put("message", "上传失败");
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
} catch (Exception e) {
result.put("code", 500);
result.put("message", "上传异常:" + e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
}
/**
* 批量文件上传
*/
@PostMapping("/upload/batch")
public ResponseEntity<Map<String, Object>> uploadFiles(@RequestParam("files") List<MultipartFile> files) {
Map<String, Object> result = new HashMap<>();
try {
List<Map<String, String>> fileInfos = minioUtil.uploadFiles(files);
result.put("code", 200);
result.put("message", "上传成功");
result.put("data", fileInfos);
return ResponseEntity.ok(result);
} catch (Exception e) {
result.put("code", 500);
result.put("message", "上传异常:" + e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
}
/**
* 文件下载
*/
@GetMapping("/download/{fileName}")
public ResponseEntity<byte[]> downloadFile(@PathVariable("fileName") String fileName) {
try {
InputStream inputStream = minioUtil.downloadFile(fileName);
byte[] bytes = inputStream.readAllBytes();
inputStream.close();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
headers.setContentDispositionFormData("attachment", fileName);
returnnew ResponseEntity<>(bytes, headers, HttpStatus.OK);
} catch (Exception e) {
returnnew ResponseEntity<>(null, null, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
/**
* 文件预览
*/
@GetMapping("/preview/{fileName}")
public ResponseEntity<Map<String, Object>> previewFile(@PathVariable("fileName") String fileName) {
Map<String, Object> result = new HashMap<>();
try {
String url = minioUtil.getObjectUrl(fileName, 1);
result.put("code", 200);
result.put("message", "获取成功");
result.put("url", url);
return ResponseEntity.ok(result);
} catch (Exception e) {
result.put("code", 500);
result.put("message", "获取异常:" + e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
}
/**
* 删除文件
*/
@DeleteMapping("/delete/{fileName}")
public ResponseEntity<Map<String, Object>> deleteFile(@PathVariable("fileName") String fileName) {
Map<String, Object> result = new HashMap<>();
try {
minioUtil.deleteFile(fileName);
result.put("code", 200);
result.put("message", "删除成功");
return ResponseEntity.ok(result);
} catch (Exception e) {
result.put("code", 500);
result.put("message", "删除异常:" + e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
}
/**
* 列出所有文件
*/
@GetMapping("/list")
public ResponseEntity<Map<String, Object>> listFiles() {
Map<String, Object> result = new HashMap<>();
try {
List<Item> items = minioUtil.listObjects();
result.put("code", 200);
result.put("message", "获取成功");
result.put("data", items);
return ResponseEntity.ok(result);
} catch (Exception e) {
result.put("code", 500);
result.put("message", "获取异常:" + e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
}
/**
* 初始化分片上传
*/
@PostMapping("/multipart/init")
public ResponseEntity<Map<String, Object>> initMultipartUpload(@RequestParam("fileName") String fileName) {
Map<String, Object> result = new HashMap<>();
try {
String uploadId = minioUtil.createMultipartUpload("test-bucket", fileName);
result.put("code", 200);
result.put("message", "初始化成功");
result.put("uploadId", uploadId);
return ResponseEntity.ok(result);
} catch (Exception e) {
result.put("code", 500);
result.put("message", "初始化异常:" + e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
}
/**
* 上传分片
*/
@PostMapping("/multipart/upload")
public ResponseEntity<Map<String, Object>> uploadPart(
@RequestParam("fileName") String fileName,
@RequestParam("uploadId") String uploadId,
@RequestParam("partNumber") int partNumber,
@RequestParam("file") MultipartFile file) {
Map<String, Object> result = new HashMap<>();
try {
String etag = minioUtil.uploadPart("test-bucket", fileName, uploadId, partNumber, file.getInputStream(), file.getSize());
result.put("code", 200);
result.put("message", "分片上传成功");
result.put("etag", etag);
return ResponseEntity.ok(result);
} catch (Exception e) {
result.put("code", 500);
result.put("message", "分片上传异常:" + e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
}
/**
* 完成分片上传
*/
@PostMapping("/multipart/complete")
public ResponseEntity<Map<String, Object>> completeMultipartUpload(
@RequestParam("fileName") String fileName,
@RequestParam("uploadId") String uploadId,
@RequestParam("etags") List<String> etags) {
Map<String, Object> result = new HashMap<>();
try {
minioUtil.completeMultipartUpload("test-bucket", fileName, uploadId, etags);
result.put("code", 200);
result.put("message", "分片合并成功");
return ResponseEntity.ok(result);
} catch (Exception e) {
result.put("code", 500);
result.put("message", "分片合并异常:" + e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
}
/**
* 文件秒传检查
*/
@PostMapping("/check")
public ResponseEntity<Map<String, Object>> checkFile(@RequestParam("file") MultipartFile file) {
Map<String, Object> result = new HashMap<>();
try {
String fileHash = minioUtil.generateFileHash(file);
// 这里应该查询数据库或缓存,检查是否存在相同哈希值的文件
// 为简化示例,直接返回不存在
boolean exists = false;
result.put("code", 200);
result.put("message", "检查成功");
result.put("exists", exists);
result.put("fileHash", fileHash);
return ResponseEntity.ok(result);
} catch (Exception e) {
result.put("code", 500);
result.put("message", "检查异常:" + e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
}
}
六、大文件分片上传和秒传实现原理
1.大文件分片上传
大文件分片上传一个大文件拆分多个小片段,分别上传这些片段,最后在服务器端将这些片段合并成一个完整的文件。实现步骤如下:
前端:
-
将文件限制固定大小的片段(如 1MB / 片),为每个片段生成唯一标识(如序号),按顺序上传这些片段。
联系人:
-
接收上传的片段,保存到临时目录。
-
记录已上传的片段信息(如文件名、片段序号、ETag 等)。
-
当所有片段上传完成后,按顺序合并这些片段。
2.秒传功能
秒传功能是指当用户上传一个文件时,系统首先检查该文件是否已经存在,如果存在则直接返回文件链接,不需要重新上传。实现步骤如下:
前端:
-
计算文件的哈希值(如MD5、SHA-1),放置哈希值发送给仓库。
联系人:
-
根据哈希值查询数据库或服务器,检查是否存在相同哈希值的文件。
-
如果存在,返回文件链接;如果不存在,通知前端正常上传。
七、测试与验证
1.简单文件上传测试
使用Postman或其他工具,向/api/minio/upload接口发送POST请求,上传一个文件,验证是否能成功上传并返回文件信息。
2.批量文件上传测试
向/api/minio/upload/batch接口发送POST请求,上传多个文件,验证能否成功批量上传。
3.文件下载测试
访问/api/minio/download/{fileName}接口,验证是否能成功下载文件。
4.文件预览测试
访问/api/minio/preview/{fileName}接口,验证是否能获取文件预览链接。
5.大文件分片上传测试
使用前置工具(如 webuploader、plupload 等)实现大文件分片上传功能,调用乌克兰提供的分片上传接口,验证大文件是否能成功上传。
6.秒传功能测试
上传一个文件,记录文件哈希值,再次上传相同文件,验证是否能秒传成功。
1万+

被折叠的 条评论
为什么被折叠?



