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/miniocontainer_name: minioports:- 9010:9000- 9011:9011environment:TZ: Asia/ShanghaiMINIO_ACCESS_KEY: minioMINIO_SECRET_KEY: minio123volumes:- ./data:/datacommand: 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文件中有好幾個容器,你并不想啟動其他容器,只想啟動minio
docker-compose up -d minio
#如果你的docker-compose.yml文件中只有目前的minio
docker-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")
public class MinioConfig {/*** 服務地址*/private String url;/*** 用戶名*/private String accessKey;/*** 密碼*/private String secretKey;/*** 存儲桶名稱*/private String bucketName;/*** 預覽到期時間(小時)*/private Integer previewExpiry;@Beanpublic MinioClient getMinIOClient() {return MinioClient.builder().endpoint(url).credentials(accessKey, secretKey).build();}
}
3.4 引入相關操作
創建MinioCosManger文件 封裝了minio客戶端的一些操作
@Component
@Slf4j
public class MinioCosManger {@Autowiredprivate MinioConfig prop;@Resourceprivate MinioClient minioClient;/*** 查看存儲bucket是否存在** @return boolean*/public Boolean bucketExists(String bucketName) {Boolean found;try {found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());} catch (Exception e) {e.printStackTrace();return false;}return found;}/*** 創建存儲bucket** @return Boolean*/public Boolean makeBucket(String bucketName) {try {minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());} catch (Exception e) {e.printStackTrace();return false;}return true;}/*** 刪除存儲bucket** @return Boolean*/public Boolean removeBucket(String bucketName) {try {minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());} catch (Exception e) {e.printStackTrace();return false;}return true;}/*** 獲取全部bucket*/public List<Bucket> getAllBuckets() {try {List<Bucket> buckets = minioClient.listBuckets();return buckets;} catch (Exception e) {e.printStackTrace();}return null;}/*** 文件上傳** @param file 文件* @return Boolean*/public String upload(MultipartFile file) {String originalFilename = file.getOriginalFilename();if (StringUtils.isBlank(originalFilename)) {throw new RuntimeException();}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();return null;}return objectName;}/*** 預覽圖片** @param fileName* @return*/public String preview(String fileName) {// 查看文件地址GetPresignedObjectUrlArgs build = new GetPresignedObjectUrlArgs().builder().bucket(prop.getBucketName()).object(fileName).method(Method.GET).build();try {String url = minioClient.getPresignedObjectUrl(build);return url;} catch (Exception e) {e.printStackTrace();}return null;}/*** 文件下載** @param fileName 文件名稱* @param res response* @return Boolean*/public void download(String fileName, HttpServletResponse res) {GetObjectArgs objectArgs = GetObjectArgs.builder().bucket(prop.getBucketName()).object(fileName).build();try (GetObjectResponse response = minioClient.getObject(objectArgs)) {byte[] buf = new byte[1024];int len;try (FastByteArrayOutputStream os = new FastByteArrayOutputStream()) {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內文件對象信息*/public List<Item> listObjects() {Iterable<Result<Item>> results = minioClient.listObjects(ListObjectsArgs.builder().bucket(prop.getBucketName()).build());List<Item> items = new ArrayList<>();try {for (Result<Item> result : results) {items.add(result.get());}} catch (Exception e) {e.printStackTrace();return null;}return items;}/*** 刪除** @param fileName* @return* @throws Exception*/public boolean remove(String fileName) {try {minioClient.removeObject(RemoveObjectArgs.builder().bucket(prop.getBucketName()).object(fileName).build());} catch (Exception e) {return false;}return true;}}
3.5 創建controller進行測試
/*** @version 1.0* @Author jerryLau* @Date 2024/7/1 11:23* @注釋*/
@Api(tags = "文件相關接口")
@Slf4j
@RestController
@RequestMapping(value = "product/file")
public class FileController2 {@Autowiredprivate MinioCosManger minioUtil;@Autowiredprivate MinioConfig prop;@ApiOperation(value = "查看存儲bucket是否存在")@GetMapping("/bucketExists")public BaseResponse<String> bucketExists(@RequestParam("bucketName") String bucketName) {if (minioUtil.bucketExists(bucketName)) {return ResultUtils.success("bucketName is exit!");} elsereturn ResultUtils.error(ErrorCode.NOT_FOUND_ERROR, "bucketName is not exit!");}@ApiOperation(value = "創建存儲bucket")@GetMapping("/makeBucket")public BaseResponse<String> makeBucket(String bucketName) {if (minioUtil.makeBucket(bucketName)) {return ResultUtils.success("create bucket success!");} elsereturn ResultUtils.error(ErrorCode.OPERATION_ERROR, "create bucket error!");}@ApiOperation(value = "刪除存儲bucket")@GetMapping("/removeBucket")public BaseResponse<String> removeBucket(String bucketName) {if (minioUtil.removeBucket(bucketName)) {return ResultUtils.success("removeBucket success!");} elsereturn ResultUtils.error(ErrorCode.OPERATION_ERROR, "removeBucket error!");}@ApiOperation(value = "獲取全部bucket")@GetMapping("/getAllBuckets")public BaseResponse<List<Bucket>> getAllBuckets() {List<Bucket> allBuckets = minioUtil.getAllBuckets();return ResultUtils.success(allBuckets);}@ApiOperation(value = "文件上傳返回url")@PostMapping("/upload")public BaseResponse<String> upload(@RequestParam("file") MultipartFile file) {String objectName = minioUtil.upload(file);if (null != objectName) {String url = (prop.getUrl() + "/" + prop.getBucketName() + "/" + objectName);return ResultUtils.success(url);}return ResultUtils.error(ErrorCode.OPERATION_ERROR, "upload error!");}@ApiOperation(value = "圖片/視頻預覽")@GetMapping("/preview")public BaseResponse<String> preview(@RequestParam("fileName") String fileName) {String preview = minioUtil.preview(fileName);return ResultUtils.success(preview);}@ApiOperation(value = "文件下載")@GetMapping("/download")public void download(@RequestParam("fileName") String fileName, HttpServletResponse res) {minioUtil.download(fileName, res);}@ApiOperation(value = "刪除文件", notes = "根據url地址刪除文件")@PostMapping("/delete")public BaseResponse<String> remove(String url) {String objName = url.substring(url.lastIndexOf(prop.getBucketName() + "/") + prop.getBucketName().length() + 1);boolean remove = minioUtil.remove(objName);if (remove) {return ResultUtils.success(objName + "delete success!");} else return ResultUtils.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也無法被訪問到。