docker-compose搭建minio对象存储服务器
最近想使用oss对象存储进行用户图片上传的管理,了解了一下例如aliyun或者腾讯云的oss对象存储服务,但是呢涉及到对象存储以及经费有限的缘故,决定自己手动搭建一个oss对象存储服务器;
首先大致了解一下对象存储:
对象存储OSS(Object Storage Service)是一种云存储服务,它提供了海量、安全、低成本、高可靠的存储解决方案
然后在经过大致了解后,选择了MiNiO,进行oss对象服务器的搭建工作,MinIO是一个开源的对象存储服务器,它兼容Amazon S3 API,并提供高性能、高可用性的存储解决方案。在本文中,我们将介绍如何使用Docker Compose快速部署MinIO。
一、docker-compose中的minio对象服务部署
准备工作:
1、服务器必须安装docker
2、服务器必须安装docker对应版本的docker-compose
1.1获取镜像:
首先,如果你的docker还能连上网,能够通过docker pull相关的镜像(咳咳,最近docker不对劲,拉取不到镜像),如果可以拉取镜像,可以执行下述命令:
docker pull minio/minio:latest
如果不可以,建议在往上下载一个minio的tar包,上传至服务器后,可以执行以下命令:
docker load -i minio.tar ## minio 是你自己tar包的名字。
通过上述操作后,可以使用
docker images
进行查看获取到的镜像
1.2 docker-compose.yml文件制作
vim docker-compose.yml
先贴一个代码叭,一会儿挨个儿解释:
version:'3'services:minio:image: minio/minio
container_name: minio
ports:- 9010:9000- 9011:9011environment:TZ: Asia/Shanghai
MINIO_ACCESS_KEY: minio
MINIO_SECRET_KEY: minio123
volumes:- ./data:/data
command: server /data --console-address ":9011"
大概配置如上所示,有几点注意
注:
1、minio容器默认使用两个端口,9000和9001 9000端口主要适用于数据传输,9001端口主要是用于管理界面,上述文件中我为了好记且避免端口冲突,将9000端口映射到了服务器的9010端口,将9001端口改成了9011并映射到了服务器的9011端口
2、数据卷映射: 默认将数据卷映射到了docker-compose.yml同文件目录下的data文件夹
3、command: server --console-address ‘:9011’ /data 这行一定要加,否则端口号是随机的,你压根映射不出去
4、新版本中用户名和密码改用成了 “MINIO_ROOT_USER” 和 “MINIO_ROOT_PASSWORD” 旧版本是 “MINIO_ACCESS_KEY” 和 “MINIO_SECRET_KEY” 可以自己按照版本进行设置。
5、4中分别对应的是管理界面的用户名和密码
在编辑docker-compose.yml并保存后,通过下述命令创建并启动minio容器
#如果你的docker-compose.yml文件中有好几个容器,你并不想启动其他容器,只想启动miniodocker-compose up -d minio
#如果你的docker-compose.yml文件中只有目前的miniodocker-compose up -d
启动成功后会看到服务器显示

此时可以在浏览器输入 上面docker-compose.yml文件中的【你自己的IP+9011】 访问minio的控制面板,记得开启防火墙9010,9011端口哟~

输入用户名和密码,登录minio控制面板

至此呢 单机版本 通过docker-compose 部署minio对象存储结束
二、spring-boot 集成 minio 对象存储
1、自己创建spring-boot 工程,在这里不多赘述
2、引入pom依赖
在自己的boot项目中引入minio依赖
<!---minio cos对象存储--><dependency><groupId>io.minio</groupId><artifactId>minio</artifactId><version>8.4.0</version></dependency>
3、集成代码
在集成代码之前呢,首先了解一下minio的几个知识点
- Object:存储到Minio的基本对象,如文件、字节流、Anything…
- Bucket:用来存储Object的逻辑空间。每个Bucket之间的数据量是互相隔离的。对于客户端而言,就相当于一个存放文件的顶层文件夹。
- Drive:即存储数据的磁盘,在Minio启动时,以参数的方式传入。Minio中所有的对象数据都会存储在Drive里。
- Set:即一组Drive的集合,分布式部署根据集群规模自动划分一个或多个Set,每个Set中的Drive分布在不同位置。一个对象存储在一个Set上.(for example:{1…64} is divided into 4 sets each of size 16) - 一个对象存储在一个Set上- 一个集群划分为多个Set- 一个Set包含的Drive数量是固定的,默认由系统根据集群规模自动计算得出- 一个Set中我的Drive尽可能分布在不同的节点上
3.1 创建用户,创建桶
可以在minio控制面板进行用户的创建以及存储桶(bucket)的创建。我们创建一个test的桶以及创建一个用户并赋予读写的权限


3.2 添加application.yml配置文件
minio:url: http://xxxxxxx #Minio服务所在地址bucketName: xxxxxx #存储桶名称accessKey: testUser #创建用户访问的key secretKey:000000000#创建用户 访问的秘钥
3.3 引入配置
创建MinioConfig 配置文件,将MinioClient 注入容器
@Data@Configuration@ConfigurationProperties(prefix ="minio")publicclassMinioConfig{/**
* 服务地址
*/privateString url;/**
* 用户名
*/privateString accessKey;/**
* 密码
*/privateString secretKey;/**
* 存储桶名称
*/privateString bucketName;/**
* 预览到期时间(小时)
*/privateInteger previewExpiry;@BeanpublicMinioClientgetMinIOClient(){returnMinioClient.builder().endpoint(url).credentials(accessKey, secretKey).build();}}
3.4 引入相关操作
创建MinioCosManger文件 封装了minio客户端的一些操作
@Component@Slf4jpublicclassMinioCosManger{@AutowiredprivateMinioConfig prop;@ResourceprivateMinioClient minioClient;/**
* 查看存储bucket是否存在
*
* @return boolean
*/publicBooleanbucketExists(String bucketName){Boolean found;try{
found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());}catch(Exception e){
e.printStackTrace();returnfalse;}return found;}/**
* 创建存储bucket
*
* @return Boolean
*/publicBooleanmakeBucket(String bucketName){try{
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());}catch(Exception e){
e.printStackTrace();returnfalse;}returntrue;}/**
* 删除存储bucket
*
* @return Boolean
*/publicBooleanremoveBucket(String bucketName){try{
minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());}catch(Exception e){
e.printStackTrace();returnfalse;}returntrue;}/**
* 获取全部bucket
*/publicList<Bucket>getAllBuckets(){try{List<Bucket> buckets = minioClient.listBuckets();return buckets;}catch(Exception e){
e.printStackTrace();}returnnull;}/**
* 文件上传
*
* @param file 文件
* @return Boolean
*/publicStringupload(MultipartFile file){String originalFilename = file.getOriginalFilename();if(StringUtils.isBlank(originalFilename)){thrownewRuntimeException();}String fileName =UUID.randomUUID()+ originalFilename.substring(originalFilename.lastIndexOf("."));String dateFormat ="yyyy-MM/dd";DateTimeFormatter formatter =DateTimeFormatter.ofPattern(dateFormat);LocalDate nowDate =LocalDate.now();String format = nowDate.format(formatter);String objectName = format +"/"+ fileName;try{PutObjectArgs objectArgs =PutObjectArgs.builder().bucket(prop.getBucketName()).object(objectName).stream(file.getInputStream(), file.getSize(),-1).contentType(file.getContentType()).build();//文件名称相同会覆盖
minioClient.putObject(objectArgs);}catch(Exception e){
e.printStackTrace();returnnull;}return objectName;}/**
* 预览图片
*
* @param fileName
* @return
*/publicStringpreview(String fileName){// 查看文件地址GetPresignedObjectUrlArgs build =newGetPresignedObjectUrlArgs().builder().bucket(prop.getBucketName()).object(fileName).method(Method.GET).build();try{String url = minioClient.getPresignedObjectUrl(build);return url;}catch(Exception e){
e.printStackTrace();}returnnull;}/**
* 文件下载
*
* @param fileName 文件名称
* @param res response
* @return Boolean
*/publicvoiddownload(String fileName,HttpServletResponse res){GetObjectArgs objectArgs =GetObjectArgs.builder().bucket(prop.getBucketName()).object(fileName).build();try(GetObjectResponse response = minioClient.getObject(objectArgs)){byte[] buf =newbyte[1024];int len;try(FastByteArrayOutputStream os =newFastByteArrayOutputStream()){while((len = response.read(buf))!=-1){
os.write(buf,0, len);}
os.flush();byte[] bytes = os.toByteArray();
res.setCharacterEncoding("utf-8");// 设置强制下载不打开// res.setContentType("application/force-download");
res.addHeader("Content-Disposition","attachment;fileName="+ fileName);try(ServletOutputStream stream = res.getOutputStream()){
stream.write(bytes);
stream.flush();}}}catch(Exception e){
e.printStackTrace();}}/**
* 查看文件对象
*
* @return 存储bucket内文件对象信息
*/publicList<Item>listObjects(){Iterable<Result<Item>> results = minioClient.listObjects(ListObjectsArgs.builder().bucket(prop.getBucketName()).build());List<Item> items =newArrayList<>();try{for(Result<Item> result : results){
items.add(result.get());}}catch(Exception e){
e.printStackTrace();returnnull;}return items;}/**
* 删除
*
* @param fileName
* @return
* @throws Exception
*/publicbooleanremove(String fileName){try{
minioClient.removeObject(RemoveObjectArgs.builder().bucket(prop.getBucketName()).object(fileName).build());}catch(Exception e){returnfalse;}returntrue;}}
3.5 创建controller进行测试
/**
* @version 1.0
* @Author jerryLau
* @Date 2024/7/1 11:23
* @注释
*/@Api(tags ="文件相关接口")@Slf4j@RestController@RequestMapping(value ="product/file")publicclassFileController2{@AutowiredprivateMinioCosManger minioUtil;@AutowiredprivateMinioConfig prop;@ApiOperation(value ="查看存储bucket是否存在")@GetMapping("/bucketExists")publicBaseResponse<String>bucketExists(@RequestParam("bucketName")String bucketName){if(minioUtil.bucketExists(bucketName)){returnResultUtils.success("bucketName is exit!");}elsereturnResultUtils.error(ErrorCode.NOT_FOUND_ERROR,"bucketName is not exit!");}@ApiOperation(value ="创建存储bucket")@GetMapping("/makeBucket")publicBaseResponse<String>makeBucket(String bucketName){if(minioUtil.makeBucket(bucketName)){returnResultUtils.success("create bucket success!");}elsereturnResultUtils.error(ErrorCode.OPERATION_ERROR,"create bucket error!");}@ApiOperation(value ="删除存储bucket")@GetMapping("/removeBucket")publicBaseResponse<String>removeBucket(String bucketName){if(minioUtil.removeBucket(bucketName)){returnResultUtils.success("removeBucket success!");}elsereturnResultUtils.error(ErrorCode.OPERATION_ERROR,"removeBucket error!");}@ApiOperation(value ="获取全部bucket")@GetMapping("/getAllBuckets")publicBaseResponse<List<Bucket>>getAllBuckets(){List<Bucket> allBuckets = minioUtil.getAllBuckets();returnResultUtils.success(allBuckets);}@ApiOperation(value ="文件上传返回url")@PostMapping("/upload")publicBaseResponse<String>upload(@RequestParam("file")MultipartFile file){String objectName = minioUtil.upload(file);if(null!= objectName){String url =(prop.getUrl()+"/"+ prop.getBucketName()+"/"+ objectName);returnResultUtils.success(url);}returnResultUtils.error(ErrorCode.OPERATION_ERROR,"upload error!");}@ApiOperation(value ="图片/视频预览")@GetMapping("/preview")publicBaseResponse<String>preview(@RequestParam("fileName")String fileName){String preview = minioUtil.preview(fileName);returnResultUtils.success(preview);}@ApiOperation(value ="文件下载")@GetMapping("/download")publicvoiddownload(@RequestParam("fileName")String fileName,HttpServletResponse res){
minioUtil.download(fileName, res);}@ApiOperation(value ="删除文件", notes ="根据url地址删除文件")@PostMapping("/delete")publicBaseResponse<String>remove(String url){String objName = url.substring(url.lastIndexOf(prop.getBucketName()+"/")+ prop.getBucketName().length()+1);boolean remove = minioUtil.remove(objName);if(remove){returnResultUtils.success(objName +"delete success!");}elsereturnResultUtils.error(ErrorCode.OPERATION_ERROR, objName +"delete error!");}}
3.6 接口测试以及存储验证
通过knife4j或者其他请求测试工具(postman、apifox等),测试接口

注意:
1、按照理论来说在上传结束后返回的这个文件的url应该没有办法直接访问,应该在访问该存储对象的时候,去调用
preview方法,但是对本人而言,调用preview返回的地址太长了,并且存在一定的时效性,在超过一段时间后将不会在被访问到,所以本人通过给bucket设置access prefix为 readandwrite,这样一来,上传接口的返回url便可直接被访问到了。
2、如果想去调用preview 或者download 方法时,所传入的文件名一定是bucket后面的全部文件名称,比如上面测试图片中,如果调用,传入文件名应为
2024-07/01/0011c366-f2a4-4b26-adbc-931d444d7205.png而不是简单的
0011c366-f2a4-4b26-adbc-931d444d7205.png,否则即使返回了preview的url ,这个url也无法被访问到。

至此,通过docker-compose手动搭建minio 对象存储服务器已全部完结,喜欢的观众老爷,请一键三连 🎉🎉🎉,感谢大家~
版权归原作者 Jerry Lau 所有, 如有侵权,请联系我们删除。