SpringBoot+Minio搭建不再爆肝禿頭的分布式文件服務器



前言

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后台可以拿到的,沒有的話也可以自己設置。



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


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





免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM