SpringBoot+Minio搭建不再爆肝禿頭的分散式檔案伺服器

福隆苑居士發表於2022-02-01

前言

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,所以下載第一個就行。

1643606976(1).jpg


  2)、遠端拉取


  建立自己的minio目錄

  遠端拉取: wget http://dl.minio.org.cn/server/minio/release/darwin-amd64/minio
微信圖片_20220131171328.png


2. 安裝minio

  1)、給minio二進位制檔案賦許可權,否則無法執行:chmod +x minio

  2)、在二進位制檔案所在目錄執行 ./minio ,成功後可看到最下面的版本號,我這裡安裝的是當前最新版。

微信圖片_20220131172535.png


3. 啟動minio

  1)、在minio安裝目錄新建data目錄,用來存放minio的資料:mkdir data ;

微信圖片_20220131173728.png


  2)、在後臺程式啟動minio: ./minio server /data/minio/data > /data/minio/minio.log 2>&1 &

    檢視後臺執行日誌: tail -f minio.log

微信圖片_20220131173737.png

特別說明: 這裡日誌可以看出來,新版的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

微信圖片_20220131173751.png


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時經常會用到它,簡單點就可以理解為存放雞蛋的籃子(存放檔案的目錄)。

影像_2022-01-31_183516.png

  PS: 剛開始使用的同學可能會習慣點選檔案右側幾個按鈕中的share按鈕copy後臺生成的檔案連結,然後貼上到瀏覽器開啟,基本上都會遭遇打不開的情況,因為你仔細看連結就發現,這個連結地址的ip埠是錯誤的,這是一個誤區,我們一般使用minio會通過mc客戶端來執行命令進行一些配置,達到永久訪問檔案及直接下載檔案的效果。

影像_2022-01-31_200351.png


7. 設定永久訪問連結

  1)、安裝mc客戶端

  可以參考官網,寫的很詳細:https://docs.min.io/docs/minio-client-complete-guide.html

  也可以參考中文網: http://docs.minio.org.cn/docs/master/minio-client-complete-guide

  當你開啟文件讀一會兒後,你會發現寫的很棒,但是看不懂。沒關係,有許多踩過坑的人已經把障礙掃清了。
影像_2022-01-31_184636.png


安裝MC客戶端:
wget https://dl.min.io/client/mc/release/linux-amd64/mc

微信圖片_20220131185049.png

這裡就印證了我前面講的,安裝minio相關檔案最好看官網,這裡的中文網地址無法下載了,所以無法安裝成功。

  官網mc安裝地址:

gw.png
  中文網mc安裝地址:(這個我使用時已經失效了)

zww.png


  同樣的,要給mc執行檔案賦予許可權,否則會提示許可權不足的錯誤。

  chmod +x mc

  設定永久訪問連結,這裡官網和中文網都講的不清楚,個人認為這裡就是設定了一個可訪問的字首地址,方便之後開放桶許可權後能直接訪問到圖片,方便理解你可以想象為nginx做代理。

微信圖片_20220131185636.png

  設定配置名稱為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,是上一步命令設定的配置名。
微信圖片_20220131190759.png

  執行命令後,這個桶下面的檔案就可以直接訪問到了。

  設定永久訪問連結和下載許可權的命令執行完後,最終效果如下:

可通過 http://伺服器ip:埠/桶名稱/檔名稱 直接訪問到了!
微信圖片_20220131190807.png



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後臺可以拿到的,沒有的話也可以自己設定。



啥也不說了,純手打,看在這點辛苦的份上,各位看官點個贊給個關注可好?


可惜沒有一鍵三連…… (=_=!)




相關文章