前言
1)、有人一定會問,為什麼不用FastDFS?眾所周知,FastDFS的原生安裝非常複雜,有過安裝經驗的人大體都明白,雖然可以利用別人做好的docker直接安裝,但真正使用過程中也可能出現許多莫名其妙的問題;
2)、還有人會問,為什麼不用oss或其他現有云產品?道理很簡單,你不能保證自己所在的公司擁有的專案一定會上雲,據我個人瞭解,大部分公司要麼依託於甲方使用內網伺服器,要麼是公司自己內部搭建的,比如我公司就是依託於醫院自己的伺服器,所有部署以安全為首,只能自己搭建內部檔案伺服器;
3)、Minio是GO語言開發的,效能很好,安裝簡單,可分散式儲存海量圖片、音訊、視訊等檔案,且擁有自己的管理後臺介面,十分友好。
4)、我有個習慣,每年會觀察流量大的培訓機構會新增什麼技術去除什麼技術,雖然Minio出來也有些時日了,但這兩年陸續有知名機構的講師開始引入Minio了,意味著這個產品的接受度正在升高,隨著培訓機構培養的人員擴散到各個IT公司,接受度只會越來越高。
所以,此時不瞭解更待何時~
minio官網地址: https://docs.min.io/docs/minio-quickstart-guide.html
minio中文網地址: http://docs.minio.org.cn/docs/
特別說明:大部分內容直接看中文網即可,但下載minio時,最好看官網,因為minio版本更新非常快,經常會出現資原始檔更換目錄的情況,同時中文網的地址可能會失效,導致下載失敗。
搭建Minio
分為下載、安裝、啟動、訪問、自定義啟動指令碼及設定永久訪問連結等幾步操作。
1. 下載minio
1)、手工下載:https://docs.min.io/docs/minio-quickstart-guide.html
找到這個位置,下載自己需要的版本,我這裡使用Linux,所以下載第一個就行。
2)、遠端拉取
建立自己的minio目錄
遠端拉取: wget http://dl.minio.org.cn/server/minio/release/darwin-amd64/minio
2. 安裝minio
1)、給minio二進位制檔案賦許可權,否則無法執行:chmod +x minio
2)、在二進位制檔案所在目錄執行 ./minio
,成功後可看到最下面的版本號,我這裡安裝的是當前最新版。
3. 啟動minio
1)、在minio安裝目錄新建data目錄,用來存放minio的資料:mkdir data
;
2)、在後臺程式啟動minio: ./minio server /data/minio/data > /data/minio/minio.log 2>&1 &
檢視後臺執行日誌: tail -f minio.log
特別說明: 這裡日誌可以看出來,新版的minio和老版是有區別的,這裡API後面的地址是9000埠,console也就是控制檯地址的埠是33587,而且最後一句WARNING有提示,控制檯埠是動態生成的,請使用命令選擇一個靜態埠,意思就是如果重啟了,那麼這個控制檯的埠又會發生改變,需要自己設定一個固定不變的靜態埠,具體的設定方法可以按照提示的命令設定。
命令如下:(注意重啟時要執行 kill -9 [程式號]
把之前後臺程式啟動的minio殺掉)
# 指定後臺埠為9999
./minio server --console-address 0.0.0.0:9999 /data/minio/data > /data/minio/minio.log 2>&1 &
4. 訪問minio
設定固定的靜態埠後,日誌提示的訪問地址是 http://127.0.0.1:9999 ,這裡我們就替換成自己伺服器的ip地址即可,我這裡用的是騰訊雲伺服器。
訪問地址:http://42.193.14.144:9999
效果如下,和老版的介面也不一樣了:
預設賬號密碼: minioadmin minioadmin
5. 自定義指令碼啟動minio
1)、新建一個shell指令碼,把啟動時需要設定的命令放進來即可。這裡新增了設定賬號密碼的命令,不再用之前的預設賬號密碼minioadmin。
新建shell指令碼:vim minio-start.sh
# 設定賬號密碼
export MINIO_ACCESS_KEY=root
export MINIO_SECRET_KEY=123456
# 後臺程式啟動minio
./minio server --console-address 0.0.0.0:9999 /data/minio/data > /data/minio/minio.log 2>&1 &
2)、給這個指令碼賦予執行許可權:chmod +x minio-start.sh
3)、執行指令碼啟動minio:./minio-start.sh
最終效果和上面一樣!
6. 使用minio
進入後臺後便可以簡單使用minio上傳檔案、預覽、分享URL等來嘗試minio帶來的美好。
許多配置使用預設的就好,不明白的就多點點很快就會了,唯一要明白的是Bucket概念,因為呼叫minio的API時經常會用到它,簡單點就可以理解為存放雞蛋的籃子(存放檔案的目錄)。
PS: 剛開始使用的同學可能會習慣點選檔案右側幾個按鈕中的share按鈕copy後臺生成的檔案連結,然後貼上到瀏覽器開啟,基本上都會遭遇打不開的情況,因為你仔細看連結就發現,這個連結地址的ip埠是錯誤的,這是一個誤區,我們一般使用minio會通過mc客戶端來執行命令進行一些配置,達到永久訪問檔案及直接下載檔案的效果。
7. 設定永久訪問連結
1)、安裝mc客戶端
可以參考官網,寫的很詳細:https://docs.min.io/docs/minio-client-complete-guide.html
也可以參考中文網: http://docs.minio.org.cn/docs/master/minio-client-complete-guide
當你開啟文件讀一會兒後,你會發現寫的很棒,但是看不懂。沒關係,有許多踩過坑的人已經把障礙掃清了。
安裝MC客戶端:
wget https://dl.min.io/client/mc/release/linux-amd64/mc
這裡就印證了我前面講的,安裝minio相關檔案最好看官網,這裡的中文網地址無法下載了,所以無法安裝成功。
官網mc安裝地址:
中文網mc安裝地址:(這個我使用時已經失效了)
同樣的,要給mc執行檔案賦予許可權,否則會提示許可權不足的錯誤。
chmod +x mc
設定永久訪問連結,這裡官網和中文網都講的不清楚,個人認為這裡就是設定了一個可訪問的字首地址,方便之後開放桶許可權後能直接訪問到圖片,方便理解你可以想象為nginx做代理。
設定配置名稱為minio,設定訪問字首為http://42.193.14.144 ,這是前面說的我的騰訊雲伺服器地址,埠設為9000,當然也可以設為別的,我這裡設為9000是因為騰訊雲安全組的規則已經存在9000埠,我不需要重新新增規則了。這裡的root和123456就是前面自定義啟動指令碼設定的賬號密碼,你改成自己的就好。其他都不需要改。
./mc config host add minio http://42.193.14.144:9000 root 123456 --api S3v4
特別說明:切記,這裡設定埠,如果用的是本地虛擬機器,要麼關閉防火牆,要麼就開啟你設定的這個埠;如果用的是和我一樣的雲伺服器,不管有沒有開啟防火牆,都要在雲伺服器後臺管理中新增規則開放這個埠,否則你依然打不開檔案。
設定某個桶(即檔案目錄)中的檔案可直接下載的許可權:./mc policy set download minio/hospitalimages
這裡的hospitalimages就是我自己建的存放網際網路醫院檔案的桶了,記得一定要加上前面的minio,是上一步命令設定的配置名。
執行命令後,這個桶下面的檔案就可以直接訪問到了。
設定永久訪問連結和下載許可權的命令執行完後,最終效果如下:
可通過 http://伺服器ip:埠/桶名稱/檔名稱 直接訪問到了!
SpringBoot整合Minio
特別說明:minio引入不同版本依賴使用過程是有較大區別的,比如7和8就區別很大,本人也踩過不少坑,搜過不少資料,雖然7版本算是用上了,但目前版本較新,就使用8版本,8的坑也很多,後來在某網站的風間影月老師那裡終於找到了能使用的方案,也已經用在了公司的專案中,在這裡直接分享給大家。
1. 引入依賴
<!-- MinIO -->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.2.1</version>
</dependency>
2. MinioUtils工具類
import io.minio.*;
import io.minio.http.Method;
import io.minio.messages.Bucket;
import io.minio.messages.DeleteObject;
import io.minio.messages.Item;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
/**
* MinIO工具類
*
* @author guoj
* @date 2021/12/14 19:30
*/
@Slf4j
public class MinIOUtils {
private static MinioClient minioClient;
private static String endpoint;
private static String bucketName;
private static String accessKey;
private static String secretKey;
private static Integer imgSize;
private static Integer fileSize;
private static final String SEPARATOR = "/";
public MinIOUtils() {
}
public MinIOUtils(String endpoint, String bucketName, String accessKey, String secretKey, Integer imgSize, Integer fileSize) {
MinIOUtils.endpoint = endpoint;
MinIOUtils.bucketName = bucketName;
MinIOUtils.accessKey = accessKey;
MinIOUtils.secretKey = secretKey;
MinIOUtils.imgSize = imgSize;
MinIOUtils.fileSize = fileSize;
createMinioClient();
}
/**
* 建立基於Java端的MinioClient
*/
public void createMinioClient() {
try {
if (null == minioClient) {
log.info("開始建立 MinioClient...");
minioClient = MinioClient
.builder()
.endpoint(endpoint)
.credentials(accessKey, secretKey)
.build();
createBucket(bucketName);
log.info("建立完畢 MinioClient...");
}
} catch (Exception e) {
log.error("[Minio工具類]>>>> MinIO伺服器異常:", e);
}
}
/**
* 獲取上傳檔案字首路徑
* @return
*/
public static String getBasisUrl() {
return endpoint + SEPARATOR + bucketName + SEPARATOR;
}
/****************************** Operate Bucket Start ******************************/
/**
* 啟動SpringBoot容器的時候初始化Bucket
* 如果沒有Bucket則建立
* @throws Exception
*/
private static void createBucket(String bucketName) throws Exception {
if (!bucketExists(bucketName)) {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
}
}
/**
* 判斷Bucket是否存在,true:存在,false:不存在
* @return
* @throws Exception
*/
public static boolean bucketExists(String bucketName) throws Exception {
return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
}
/**
* 獲得Bucket的策略
* @param bucketName
* @return
* @throws Exception
*/
public static String getBucketPolicy(String bucketName) throws Exception {
return minioClient
.getBucketPolicy(
GetBucketPolicyArgs
.builder()
.bucket(bucketName)
.build()
);
}
/**
* 獲得所有Bucket列表
* @return
* @throws Exception
*/
public static List<Bucket> getAllBuckets() throws Exception {
return minioClient.listBuckets();
}
/**
* 根據bucketName獲取其相關資訊
* @param bucketName
* @return
* @throws Exception
*/
public static Optional<Bucket> getBucket(String bucketName) throws Exception {
return getAllBuckets().stream().filter(b -> b.name().equals(bucketName)).findFirst();
}
/**
* 根據bucketName刪除Bucket,true:刪除成功; false:刪除失敗,檔案或已不存在
* @param bucketName
* @throws Exception
*/
public static void removeBucket(String bucketName) throws Exception {
minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
}
/****************************** Operate Bucket End ******************************/
/****************************** Operate Files Start ******************************/
/**
* 判斷檔案是否存在
* @param bucketName 儲存桶
* @param objectName 檔名
* @return
*/
public static boolean isObjectExist(String bucketName, String objectName) {
boolean exist = true;
try {
minioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(objectName).build());
} catch (Exception e) {
log.error("[Minio工具類]>>>> 判斷檔案是否存在, 異常:", e);
exist = false;
}
return exist;
}
/**
* 判斷資料夾是否存在
* @param bucketName 儲存桶
* @param objectName 資料夾名稱
* @return
*/
public static boolean isFolderExist(String bucketName, String objectName) {
boolean exist = false;
try {
Iterable<Result<Item>> results = minioClient.listObjects(
ListObjectsArgs.builder().bucket(bucketName).prefix(objectName).recursive(false).build());
for (Result<Item> result : results) {
Item item = result.get();
if (item.isDir() && objectName.equals(item.objectName())) {
exist = true;
}
}
} catch (Exception e) {
log.error("[Minio工具類]>>>> 判斷資料夾是否存在,異常:", e);
exist = false;
}
return exist;
}
/**
* 根據檔案前置查詢檔案
* @param bucketName 儲存桶
* @param prefix 字首
* @param recursive 是否使用遞迴查詢
* @return MinioItem 列表
* @throws Exception
*/
public static List<Item> getAllObjectsByPrefix(String bucketName,
String prefix,
boolean recursive) throws Exception {
List<Item> list = new ArrayList<>();
Iterable<Result<Item>> objectsIterator = minioClient.listObjects(
ListObjectsArgs.builder().bucket(bucketName).prefix(prefix).recursive(recursive).build());
if (objectsIterator != null) {
for (Result<Item> o : objectsIterator) {
Item item = o.get();
list.add(item);
}
}
return list;
}
/**
* 獲取檔案流
* @param bucketName 儲存桶
* @param objectName 檔名
* @return 二進位制流
*/
public static InputStream getObject(String bucketName, String objectName) throws Exception {
return minioClient.getObject(GetObjectArgs.builder().bucket(bucketName).object(objectName).build());
}
/**
* 斷點下載
* @param bucketName 儲存桶
* @param objectName 檔名稱
* @param offset 起始位元組的位置
* @param length 要讀取的長度
* @return 二進位制流
*/
public InputStream getObject(String bucketName, String objectName, long offset, long length)throws Exception {
return minioClient.getObject(
GetObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.offset(offset)
.length(length)
.build());
}
/**
* 獲取路徑下檔案列表
* @param bucketName 儲存桶
* @param prefix 檔名稱
* @param recursive 是否遞迴查詢,false:模擬資料夾結構查詢
* @return 二進位制流
*/
public static Iterable<Result<Item>> listObjects(String bucketName, String prefix,
boolean recursive) {
return minioClient.listObjects(
ListObjectsArgs.builder()
.bucket(bucketName)
.prefix(prefix)
.recursive(recursive)
.build());
}
/**
* 使用MultipartFile進行檔案上傳
* @param bucketName 儲存桶
* @param file 檔名
* @param objectName 物件名
* @param contentType 型別
* @return
* @throws Exception
*/
public static ObjectWriteResponse uploadFile(String bucketName, MultipartFile file,
String objectName, String contentType) throws Exception {
InputStream inputStream = file.getInputStream();
return minioClient.putObject(
PutObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.contentType(contentType)
.stream(inputStream, inputStream.available(), -1)
.build());
}
/**
* 上傳本地檔案
* @param bucketName 儲存桶
* @param objectName 物件名稱
* @param fileName 本地檔案路徑
*/
public static ObjectWriteResponse uploadFile(String bucketName, String objectName,
String fileName) throws Exception {
return minioClient.uploadObject(
UploadObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.filename(fileName)
.build());
}
/**
* 通過流上傳檔案
*
* @param bucketName 儲存桶
* @param objectName 檔案物件
* @param inputStream 檔案流
*/
public static ObjectWriteResponse uploadFile(String bucketName, String objectName, InputStream inputStream) throws Exception {
return minioClient.putObject(
PutObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.stream(inputStream, inputStream.available(), -1)
.build());
}
/**
* 建立資料夾或目錄
* @param bucketName 儲存桶
* @param objectName 目錄路徑
*/
public static ObjectWriteResponse createDir(String bucketName, String objectName) throws Exception {
return minioClient.putObject(
PutObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.stream(new ByteArrayInputStream(new byte[]{}), 0, -1)
.build());
}
/**
* 獲取檔案資訊, 如果丟擲異常則說明檔案不存在
*
* @param bucketName 儲存桶
* @param objectName 檔名稱
*/
public static String getFileStatusInfo(String bucketName, String objectName) throws Exception {
return minioClient.statObject(
StatObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.build()).toString();
}
/**
* 拷貝檔案
*
* @param bucketName 儲存桶
* @param objectName 檔名
* @param srcBucketName 目標儲存桶
* @param srcObjectName 目標檔名
*/
public static ObjectWriteResponse copyFile(String bucketName, String objectName,
String srcBucketName, String srcObjectName) throws Exception {
return minioClient.copyObject(
CopyObjectArgs.builder()
.source(CopySource.builder().bucket(bucketName).object(objectName).build())
.bucket(srcBucketName)
.object(srcObjectName)
.build());
}
/**
* 刪除檔案
* @param bucketName 儲存桶
* @param objectName 檔名稱
*/
public static void removeFile(String bucketName, String objectName) throws Exception {
minioClient.removeObject(
RemoveObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.build());
}
/**
* 批量刪除檔案
* @param bucketName 儲存桶
* @param keys 需要刪除的檔案列表
* @return
*/
public static void removeFiles(String bucketName, List<String> keys) {
List<DeleteObject> objects = new LinkedList<>();
keys.forEach(s -> {
objects.add(new DeleteObject(s));
try {
removeFile(bucketName, s);
} catch (Exception e) {
log.error("[Minio工具類]>>>> 批量刪除檔案,異常:", e);
}
});
}
/**
* 獲取檔案外鏈
* @param bucketName 儲存桶
* @param objectName 檔名
* @param expires 過期時間 <=7 秒 (外鏈有效時間(單位:秒))
* @return url
* @throws Exception
*/
public static String getPresignedObjectUrl(String bucketName, String objectName, Integer expires) throws Exception {
GetPresignedObjectUrlArgs args = GetPresignedObjectUrlArgs.builder().expiry(expires).bucket(bucketName).object(objectName).build();
return minioClient.getPresignedObjectUrl(args);
}
/**
* 獲得檔案外鏈
* @param bucketName
* @param objectName
* @return url
* @throws Exception
*/
public static String getPresignedObjectUrl(String bucketName, String objectName) throws Exception {
GetPresignedObjectUrlArgs args = GetPresignedObjectUrlArgs.builder()
.bucket(bucketName)
.object(objectName)
.method(Method.GET).build();
return minioClient.getPresignedObjectUrl(args);
}
/**
* 將URLDecoder編碼轉成UTF8
* @param str
* @return
* @throws UnsupportedEncodingException
*/
public static String getUtf8ByURLDecoder(String str) throws UnsupportedEncodingException {
String url = str.replaceAll("%(?![0-9a-fA-F]{2})", "%25");
return URLDecoder.decode(url, "UTF-8");
}
/****************************** Operate Files End ******************************/
}
總結
這樣其實就完成了整合,是不是So Easy?咔咔,在需要用到的地方通過MinioUtils.xxx()方法呼叫即可,比如我在公司專案中用到的就是MinioUtils.getPresignedObjectUrl()這個獲取檔案外鏈的方法,因為大多數時候不需要你對檔案本身進行修改刪除操作,正常來講只會用到上傳和查詢檔案的操作,在設計上許多產品老師也會規避這種風險問題。另外,工具類中傳遞的endpoint、bucketName、accessKey、ecretKey等引數,都是在minio後臺可以拿到的,沒有的話也可以自己設定。
啥也不說了,純手打,看在這點辛苦的份上,各位看官點個贊給個關注可好?
可惜沒有一鍵三連…… (=_=!)