MinIO使用記錄

Comfortable發表於2024-07-05

探索MinIO:高效能、分散式物件儲存解決方案

注:本文除程式碼外多數為AI生成

最近因為有專案需要換成Amazon S3的雲端儲存,所以把之前做過的minio部分做一個記錄,後面也會把基於這版改造的S3方法發出來記錄。

MinIO簡介

MinIO是一款高效能、分散式物件儲存伺服器,設計用於在大規模環境中儲存和檢索非結構化資料集,如影像、影片和日誌檔案。它完全開源,遵循Apache License v2.0,並且與Amazon S3 API相容,這使得從現有的S3環境遷移變得簡單。MinIO支援多租戶,確保了資料的安全性和隔離性,同時提供了多種資料永續性和一致性保障機制。由於其出色的效能特性,如高吞吐量和低延遲,MinIO成為了眾多企業和組織在構建現代雲基礎設施時的首選物件儲存解決方案。

MinIO架構與技術

去中心化架構
  • 無共享架構:MinIO採用了一種去中心化的無共享架構,這意味著沒有單一的瓶頸點或中心化的後設資料伺服器。資料和後設資料分佈在叢集內的所有節點上,提高了系統的整體效能和可靠性。
  • 統一名稱空間:儘管資料分佈在多個節點上,但MinIO對外提供了一個統一的名稱空間,使用者無需關心資料的具體位置。
分散式特性
  • 水平擴充套件:MinIO可以很容易地透過新增更多的節點來水平擴充套件,每個節點都是對等的,可以獨立執行和儲存資料。
  • 糾刪編碼:為了提高資料的可靠性和容錯性,MinIO使用糾刪編碼技術(Erasure Coding)來儲存資料。這允許資料在多個節點上以冗餘的形式存在,即使部分節點發生故障,資料仍然可讀。
高效能
  • 併發處理:MinIO的設計考慮到了高併發性,能夠同時處理大量的讀寫請求,提供低延遲和高吞吐量。
  • 網路最佳化:MinIO透過高效的網路協議和資料傳輸最佳化,確保資料在網路中的快速流動。
相容性
  • S3 API相容:MinIO完全相容Amazon S3 API,這使得它可以無縫地與許多已有的應用程式和服務整合,降低了遷移成本。
安全性
  • 加密:MinIO支援靜態資料加密,確保資料在儲存期間的安全。
  • 訪問控制:透過IAM(Identity and Access Management)策略,MinIO提供了細粒度的訪問控制,保護資料不被未授權訪問。
開源與跨平臺
  • 開源許可證:MinIO遵循Apache License v2.0開源協議,允許自由使用、修改和分發。
  • 跨平臺:MinIO可以在多種作業系統上執行,包括Linux、Windows和macOS,增強了其部署靈活性。
叢集部署
  • 多節點叢集:MinIO可以部署為一個叢集,其中包含多個節點,這些節點共同維護資料的一致性和可用性。
  • 負載均衡:在叢集中,可以透過DNS輪詢或負載均衡器來分配請求到不同的節點,確保負載均勻分佈。
技術棧
  • Go語言:MinIO使用Go語言編寫,這提供了良好的效能和簡潔的程式碼基礎。

Minio的安裝部署

單節點部署

  • 參考https://www.cnblogs.com/ComfortableM/p/17384523.html

叢集部署概念

  1. 節點:MinIO叢集由多個節點組成,每個節點都是一個獨立執行的MinIO伺服器例項。
  2. 糾刪編碼:為了實現資料冗餘和容錯,MinIO使用糾刪編碼(Erasure Coding)。這種技術允許資料被分割成多個片段,並且每個片段都有額外的校驗資訊。如果叢集中有節點失效,資料仍然可以從剩餘的節點中重構出來。
  3. 資料分佈:資料均勻分佈在所有節點上,以達到負載均衡和最大化使用所有儲存資源的目的。

部署步驟

  1. 環境準備
    • 確保有足夠的物理伺服器或虛擬機器。
    • 準備好足夠的磁碟空間,每臺伺服器可以使用單個磁碟或多個磁碟。
    • 配置網路,確保所有節點之間可以互相通訊。
  2. 軟體安裝
    • 在每臺伺服器上安裝MinIO軟體。可以透過二進位制包、Docker容器或軟體包管理系統完成。
    • 配置MinIO伺服器,指定資料儲存目錄和叢集相關的配置。
  3. 啟動MinIO服務
    • 在每個節點上啟動MinIO服務,使用叢集模式的啟動引數。
    • 指定叢集中的其他節點,以便它們可以相互發現並形成叢集。
  4. 驗證叢集狀態
    • 使用MinIO的管理命令或Web UI檢查叢集狀態,確認所有節點是否已成功加入叢集。
    • 測試讀寫操作,確保資料可以正確地在叢集中分佈和訪問。
  5. 監控和維護
    • 設定監控,持續監控叢集的健康狀況和效能指標。
    • 定期進行維護,如資料平衡、硬體升級或替換故障節點。

關鍵考慮因素

  • 節點數量:叢集至少需要4個節點來實現資料的冗餘和分佈。通常,節點數量越多,資料的可用性和效能就越好。
  • 資料冗餘:根據糾刪編碼策略,叢集可以容忍一定數量的節點故障而不會丟失資料。
  • 網路配置:確保網路穩定和高速,因為叢集中的節點需要頻繁地相互通訊。
  • 故障恢復:設計故障恢復計劃,包括節點替換、資料重建和災難恢復策略。
  • 效能調優:根據實際工作負載,可能需要調整網路頻寬、磁碟I/O或其他系統引數來最佳化效能。

擴充套件和縮放

  • 擴充套件:透過新增更多節點可以輕鬆擴充套件MinIO叢集的容量和效能。
  • 縮放:移除節點時需要小心,確保資料冗餘不會降低到不可接受的水平。

注意事項

  • 使用hosts檔案或DNS服務來解決叢集內部的域名或IP地址,避免直接在配置檔案中硬編碼IP地址,這有助於在節點變化時更容易地維護叢集。
  • 在部署多節點多磁碟的叢集時,確保遵循最佳實踐,如避免在同一伺服器上使用過多的磁碟,以防伺服器故障導致大量資料不可用。

下面為例:

  • 節點啟動方式,每個節點都需要啟動
export MINIO_ACCESS_KEY=admin
export MINIO_SECRET_KEY=minioadmin
#console-address引數可設可不設
./minio server --address ":9001" --console-address ":9011" 
"http://ipA:9001/arcfile/minioData/data" 
"http://ipB:9001/arcfile/minioData/data"
"http://ipC:9001/arcfile/minioData/data"
"http://ipD:9001/arcfile/minioData/data" >/arcfile/minioData/logs/start.txt 2>&1 &
  • 如果要用來測試,可以一個伺服器部署四個節點。以上面為例修改埠啟動,或者參考下面的啟動指令碼
RUNNING_USER=root
MINIO_HOME=/opt/minio
MINIO_HOST=192.168.222.10
#accesskey and secretkey
ACCESS_KEY=admin 
SECRET_KEY=minioadmin

for i in {01..04}; do
    START_CMD="MINIO_ACCESS_KEY=${ACCESS_KEY} MINIO_SECRET_KEY=${SECRET_KEY} nohup ${MINIO_HOME}/minio  server --address "${MINIO_HOST}:90${i}" http://${MINIO_HOST}:9001/opt/min-data1 http://${MINIO_HOST}:9002/opt/min-data2 http://${MINIO_HOST}:9003/opt/min-data3 http://${MINIO_HOST}:9004/opt/min-data4 > ${MINIO_HOME}/minio-90${i}.log 2>&1 &"
    su - ${RUNNING_USER} -c "${START_CMD}"
done

整合Spring boot專案

新增依賴
        <dependency>
            <groupId>io.minio</groupId>
            <artifactId>minio</artifactId>
            <version>8.3.3</version>
        </dependency>

        <!--如果出現okhttp依賴衝突,新增下面依賴-->
<!--        <dependency>-->
<!--            <groupId>com.squareup.okhttp3</groupId>-->
<!--            <artifactId>okhttp</artifactId>-->
<!--            <version>4.9.0</version>-->
<!--        </dependency>-->
建立配置類
import io.minio.MinioClient;
import io.minio.errors.*;
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.nio.client.HttpAsyncClientBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.logging.Logger;

@Configuration
public class MinioClientConfig {

    private final Logger log = Logger.getLogger(this.getClass().getName());

    @Value("${config.minio.url}")
    private String MINIO_URL;
    @Value("${config.minio.accessKey}")
    private String MINIO_ACCESS_KEY;
    @Value("${config.minio.secretKey}")
    private String MINIO_SECRET_KEY;

    @Bean
    public MinioClient getMinioClient() {
        if (MINIO_URL.length() == 0 || MINIO_URL == null) {
            throw new IllegalArgumentException("\n請正確配置Minio伺服器的 URL 連線引數");
        }
        if (MINIO_ACCESS_KEY.length() == 0 || MINIO_ACCESS_KEY == null) {
            throw new IllegalArgumentException("\n請正確配置Minio伺服器的 ACCESS_KEY 連線引數");
        }
        if (MINIO_SECRET_KEY.length() == 0 || MINIO_SECRET_KEY == null) {
            throw new IllegalArgumentException("\n請正確配置Minio伺服器的 SECRET_KEY 連線引數");
        }
        MinioClient minioClient = MinioClient.builder()
                .endpoint(MINIO_URL)
                .credentials(MINIO_ACCESS_KEY, MINIO_SECRET_KEY)
                .build();
        try {
            minioClient.listBuckets();
        } catch (ErrorResponseException e) {
            e.printStackTrace();
            throw new RuntimeException("\nMinio伺服器連線異常\n請檢查所配置的Minio連線資訊Access-key和Secret-Key是否正確");
        } catch (InsufficientDataException e) {
            e.printStackTrace();
        } catch (InternalException e) {
            e.printStackTrace();
        } catch (InvalidKeyException e) {
            e.printStackTrace();
        } catch (InvalidResponseException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException("\nMinio伺服器連線異常\n請檢查Minio伺服器是否已開啟或所配置的Minio_url連線資訊是否正確");
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (ServerException e) {
            e.printStackTrace();
        } catch (XmlParserException e) {
            e.printStackTrace();
        }
        log.info("Minio伺服器連線成功,URL = " + MINIO_URL);
        return minioClient;
    }
}

基本操作介紹

  • 建立桶
public boolean createBucket(String bucketName) throws RuntimeException {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
}
  • 配置桶許可權為公開
public void BucketAccessPublic(String bucketName) {
String config = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Action\":[\"s3:ListBucketMultipartUploads\",\"s3:GetBucketLocation\",\"s3:ListBucket\"],\"Resource\":[\"arn:aws:s3:::" + bucketName + "\"]},{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Action\":[\"s3:ListMultipartUploadParts\",\"s3:PutObject\",\"s3:AbortMultipartUpload\",\"s3:DeleteObject\",\"s3:GetObject\"],\"Resource\":[\"arn:aws:s3:::" + bucketName + "/*\"]}]}";
	minioClient.setBucketPolicy(SetBucketPolicyArgs.builder().bucket(bucketName).config(config).build();
}
  • 上傳物件

uploadObject()

//上傳本地檔案
public boolean uploadObject(String bucketName, String targetObject, String sourcePath) {
        try {
            minioClient.uploadObject(UploadObjectArgs.builder()
                    .bucket(bucketName)
                    .object(targetObject)
                    .filename(sourcePath)
                    .build());
        } catch (ErrorResponseException e) {
            e.printStackTrace();
            return false;
        } catch (InsufficientDataException e) {
            e.printStackTrace();
        } catch (InternalException e) {
            e.printStackTrace();
        } catch (InvalidKeyException e) {
            e.printStackTrace();
        } catch (InvalidResponseException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (ServerException e) {
            e.printStackTrace();
        } catch (XmlParserException e) {
            e.printStackTrace();
        }
        return true;
    }

putObject()

public boolean putObject(String bucketName, String object, InputStream inputStream) {
        try {
            minioClient.putObject(PutObjectArgs.builder()
                    .bucket(bucketName)
                    .object(object)
                    //.contentType("application/pdf")不設定的話預設是"application/stream",這就是為什麼某些檔案上傳上去無法直接預覽的問題
                    .stream(inputStream, -1, 10485760)
                    //.tags(tags)上傳可以直接攜帶標籤
                    .build());
        } catch (ErrorResponseException e) {
            e.printStackTrace();
            return false;
        } catch (InsufficientDataException e) {
            e.printStackTrace();
        } catch (InternalException e) {
            e.printStackTrace();
        } catch (InvalidKeyException e) {
            e.printStackTrace();
        } catch (InvalidResponseException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (ServerException e) {
            e.printStackTrace();
        } catch (XmlParserException e) {
            e.printStackTrace();
        }
        return true;
    }
  • 下載物件
    public GetObjectResponse getObject(String bucketName, String object) {
        GetObjectResponse object1 = null;//這個物件是整合了InputStream的FilterInputStream類,所以是可以直接用流來處理檔案的
        try {
            object1 = minioClient.getObject(GetObjectArgs.builder()
                    .bucket(bucketName)
                    .object(object)
                    .build());
        } catch (ErrorResponseException e) {
            e.printStackTrace();
        } catch (InsufficientDataException e) {
            e.printStackTrace();
        } catch (InternalException e) {
            e.printStackTrace();
        } catch (InvalidKeyException e) {
            e.printStackTrace();
        } catch (InvalidResponseException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (ServerException e) {
            e.printStackTrace();
        } catch (XmlParserException e) {
            e.printStackTrace();
        }
        return object1;
    }
  • 獲取物件預簽名Url
/**
 * 生成預簽名URL,允許外部在限定時間內訪問指定的MinIO物件。
 * 
 * @param bucketName 儲存桶名稱
 * @param object 物件名稱
 * @param expire 過期時間,單位是分鐘
 * @param map 用於自定義HTTP響應頭的對映,例如設定Content-Type
 * @return 返回預簽名的URL,可以用於直接訪問物件
 */
public String presignedURLofObject(String bucketName, String object, int expire, Map<String, String> map) {
    // 設定響應的內容型別為application/json
    map.put("response-content-type", "application/json");
    
    // 初始化預簽名URL字串
    String presignedObjectUrl = "";
    
    // 嘗試獲取預簽名的URL
    try {
        // 構建獲取預簽名URL的引數
        presignedObjectUrl = minioClient.getPresignedObjectUrl(
            GetPresignedObjectUrlArgs.builder()
                .bucket(bucketName)          // 設定儲存桶名稱
                .object(object)              // 設定物件名稱
                .method(Method.GET)          // 設定HTTP方法為GET
                .expiry(expire, TimeUnit.MINUTES) // 設定過期時間為expire分鐘後
                //.extraQueryParams(map)     // 註釋掉的程式碼,用於設定額外的查詢引數
                .build()                     // 構建引數物件
        );
    } catch (ErrorResponseException e) {
        // 處理錯誤響應異常
        e.printStackTrace();
    } catch (InsufficientDataException e) {
        // 資料不足異常
        e.printStackTrace();
    } catch (InternalException e) {
        // MinIO內部異常
        e.printStackTrace();
    } catch (InvalidKeyException e) {
        // 無效的訪問金鑰異常
        e.printStackTrace();
    } catch (InvalidResponseException e) {
        // 無效的響應異常
        e.printStackTrace();
    } catch (IOException e) {
        // 輸入輸出異常
        e.printStackTrace();
    } catch (NoSuchAlgorithmException e) {
        // 不存在演算法異常
        e.printStackTrace();
    } catch (XmlParserException e) {
        // XML解析異常
        e.printStackTrace();
    } catch (ServerException e) {
        // 伺服器異常
        e.printStackTrace();
    }
    // 返回預簽名的URL
    return presignedObjectUrl;
}

部分方法集

MinioService

import io.minio.ComposeSource;
import io.minio.GetObjectResponse;
import io.minio.Result;
import io.minio.StatObjectResponse;
import io.minio.messages.Bucket;
import io.minio.messages.Item;
import org.springframework.stereotype.Service;

import java.io.InputStream;
import java.util.List;
import java.util.Map;

/**
 * MinioService介面,定義了與MinIO物件儲存系統互動的方法集。
 */
@Service
public interface MinioService {

    /**
     * 檢查指定的儲存桶是否存在。
     *
     * @param bucketName 儲存桶名稱
     * @return 如果儲存桶存在則返回true,否則返回false。
     */
    boolean ifExistsBucket(String bucketName);

    /**
     * 建立一個新的儲存桶。
     *
     * @param bucketName 要建立的儲存桶名稱
     * @throws RuntimeException 如果建立失敗丟擲執行時異常
     * @return 如果建立成功則返回true,否則返回false。
     */
    boolean createBucket(String bucketName) throws RuntimeException;

    /**
     * 刪除指定的儲存桶。
     *
     * @param bucketName 要刪除的儲存桶名稱
     * @return 如果刪除成功則返回true,否則返回false。
     */
    boolean removeBucket(String bucketName);

    /**
     * 列出所有已存在的儲存桶。
     *
     * @return 包含所有儲存桶資訊的列表。
     */
    List<Bucket> alreadyExistBuckets();

    /**
     * 列出指定儲存桶中的物件。
     *
     * @param bucketName 要列出物件的儲存桶名稱
     * @param predir 可選字首,用於過濾物件名
     * @param recursive 是否遞迴列出子目錄的物件
     * @return 包含匹配條件的物件列表。
     */
    List<Result<Item>> listObjects(String bucketName, String predir, boolean recursive);

    /**
     * 列出指定儲存桶中的物件,用於組成物件操作。
     *
     * @param bucketName 要列出物件的儲存桶名稱
     * @param predir 可選字首,用於過濾物件名
     * @return 包含物件資訊的List<ComposeSource>物件列表。
     */
    List<ComposeSource> listObjects(String bucketName, String predir);

    /**
     * 複製一個物件到另一個位置。
     *
     * @param pastBucket 原始儲存桶名稱
     * @param pastObject 原始物件名稱
     * @param newBucket 目標儲存桶名稱
     * @param newObject 目標物件名稱
     * @return 如果複製成功則返回true,否則返回false。
     */
    boolean copyObject(String pastBucket, String pastObject, String newBucket, String newObject);

    /**
     * 下載儲存桶中的物件到本地檔案系統。
     *
     * @param bucketName 儲存桶名稱
     * @param objectName 物件名稱
     * @param targetPath 目標檔案路徑
     * @return 如果下載成功則返回true,否則返回false。
     */
    boolean downObject(String bucketName, String objectName, String targetPath);

    /**
     * 生成一個預簽名的URL,允許外部訪問指定的物件。
     *
     * @param bucketName 儲存桶名稱
     * @param object 物件名稱
     * @param expire URL的有效期(分鐘)
     * @return 預簽名的URL。
     */
    String presignedURLofObject(String bucketName, String object, int expire);

    /**
     * 生成一個預簽名的URL,允許外部訪問指定的物件,並自定義HTTP頭部。
     *
     * @param bucketName 儲存桶名稱
     * @param object 物件名稱
     * @param expire URL的有效期(分鐘)
     * @param map 自定義的HTTP頭部資訊
     * @return 預簽名的URL。
     */
    String presignedURLofObject(String bucketName, String object, int expire, Map<String, String> map);

    /**
     * 刪除儲存桶中的物件。
     *
     * @param bucketName 儲存桶名稱
     * @param object 物件名稱
     * @return 如果刪除成功則返回true,否則返回false。
     */
    boolean deleteObject(String bucketName, String object);

    /**
     * 上傳本地檔案到儲存桶。
     *
     * @param bucketName 儲存桶名稱
     * @param targetObject 目標物件名稱
     * @param sourcePath 本地檔案路徑
     * @return 如果上傳成功則返回true,否則返回false。
     */
    boolean uploadObject(String bucketName, String targetObject, String sourcePath);

    /**
     * 從InputStream上傳資料到儲存桶。
     *
     * @param bucketName 儲存桶名稱
     * @param object 物件名稱
     * @param inputStream 資料流
     * @return 如果上傳成功則返回true,否則返回false。
     */
    boolean putObject(String bucketName, String object, InputStream inputStream);

    /**
     * 從InputStream上傳資料到儲存桶,並附帶標籤。
     *
     * @param bucketName 儲存桶名稱
     * @param object 物件名稱
     * @param inputStream 資料流
     * @param tags 物件的標籤集合
     * @return 如果上傳成功則返回true,否則返回false。
     */
    boolean putObject(String bucketName, String object, InputStream inputStream, Map<String, String> tags);

    /**
     * 獲取儲存桶中的物件資訊。
     *
     * @param bucketName 儲存桶名稱
     * @param object 物件名稱
     * @return 物件的響應資訊。
     */
    GetObjectResponse getObject(String bucketName, String object);

    /**
     * 檢查儲存桶中是否包含指定檔案。
     *
     * @param bucketName 儲存桶名稱
     * @param filename 檔名稱
     * @param recursive 是否遞迴查詢
     * @return 如果檔案存在則返回true,否則返回false。
     */
    boolean fileifexist(String bucketName, String filename, boolean recursive);

    /**
     * 獲取物件的後設資料標籤。
     *
     * @param bucketName 儲存桶名稱
     * @param object 物件名稱
     * @return 物件的後設資料標籤集合。
     */
    Map<String, String> getTags(String bucketName, String object);

    /**
     * 新增或更新物件的標籤。
     *
     * @param bucketName 儲存桶名稱
     * @param object 物件名稱
     * @param addTags 要新增或更新的標籤集合
     * @return 如果操作成功則返回true,否則返回false。
     */
    boolean addTags(String bucketName, String object, Map<String, String> addTags);

    /**
     * 獲取物件的狀態資訊。
     *
     * @param bucketName 儲存桶名稱
     * @param object 物件名稱
     * @return 物件的狀態資訊。
     */
    StatObjectResponse statObject(String bucketName, String object);

    /**
     * 檢查儲存桶中物件是否存在。
     *
     * @param bucketName 儲存桶名稱
     * @param objectName 物件名稱
     * @return 如果物件存在則返回true,否則返回false。
     */
    boolean ifExistObject(String bucketName, String objectName);

    /**
     * 從其他物件名中提取元名稱。
     *
     * @param objectName 物件名稱
     * @return 提取的元名稱。
     */
    String getMetaNameFromOther(String objectName);

    /**
     * 更改物件的標籤。
     *
     * @param object 物件名稱
     * @param tag 新的標籤值
     * @return 如果更改成功則返回true,否則返回false。
     */
    boolean changeTag(String object, String tag);

    /**
     * 設定儲存桶的公共訪問許可權。
     *
     * @param bucketName 儲存桶名稱
     */
    void BucketAccessPublic(String bucketName);
}

MinioServiceImpl

import com.aspose.cad.internal.Y.S;
import com.xagxsj.erms.model.BucketName;
import com.xagxsj.erms.model.ObjectTags;
import com.xagxsj.erms.service.MinioService;
import com.xagxsj.erms.utils.FileUtil;
import io.minio.*;
import io.minio.errors.*;
import io.minio.http.Method;
import io.minio.messages.Bucket;
import io.minio.messages.Item;
import io.minio.messages.Tags;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.io.InputStream;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;

import static com.xagxsj.erms.model.BucketName.METADATA;

@Service
public class MinioServiceImpl implements MinioService {
    private final Logger log = Logger.getLogger(this.getClass().getName());
    @Qualifier("getMinioClient")
    @Autowired
    MinioClient minioClient;

    /**
     * 檢查指定的儲存桶是否存在於MinIO伺服器上。
     *
     * @param bucketName 要檢查的儲存桶名稱。
     * @return 如果儲存桶存在,則返回true;否則返回false。
     */
    @Override
    public boolean ifExistsBucket(String bucketName) {
        boolean result = false;
        try {
            result = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
        } catch (ErrorResponseException e) {
            e.printStackTrace();
            return false;
        } catch (InsufficientDataException | InternalException | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) {
            e.printStackTrace();
        }
        return result;
    }

    /**
     * 在MinIO伺服器上建立一個新的儲存桶。
     *
     * @param bucketName 要建立的儲存桶名稱。
     * @throws RuntimeException 如果嘗試建立一個已存在的儲存桶,則丟擲此異常。
     * @return 如果儲存桶建立成功,則返回true;否則返回false。
     */
    @Override
    public boolean createBucket(String bucketName) throws RuntimeException {
        if (ifExistsBucket(bucketName)) {
            throw new RuntimeException("桶已存在");
        }
        try {
            minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
        } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) {
            e.printStackTrace();
        }
        return ifExistsBucket(bucketName);
    }

    /**
     * 從MinIO伺服器上刪除一個儲存桶。
     *
     * @param bucketName 要刪除的儲存桶名稱。
     * @return 如果儲存桶成功刪除,則返回true;如果儲存桶不存在,則返回true;否則返回false。
     */
    @Override
    public boolean removeBucket(String bucketName) {
        if (!ifExistsBucket(bucketName)) {
            return true;
        }
        try {
            minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
        } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) {
            e.printStackTrace();
        }
        return !ifExistsBucket(bucketName);
    }

    /**
     * 列出MinIO伺服器上所有存在的儲存桶。
     *
     * @return 返回一個包含所有儲存桶資訊的列表。
     */
    @Override
    public List<Bucket> alreadyExistBuckets() {
        List<Bucket> buckets = null;
        try {
            buckets = minioClient.listBuckets();
        } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) {
            e.printStackTrace();
        }
        return buckets;
    }

    /**
     * 檢查指定的檔案是否存在於儲存桶中。
     *
     * @param bucketName 儲存桶名稱。
     * @param filename 要檢查的檔名。
     * @param recursive 是否遞迴搜尋子目錄。
     * @return 如果檔案存在,則返回true;否則返回false。
     */
    @Override
    public boolean fileifexist(String bucketName, String filename, boolean recursive) {
        boolean flag = false;
        Iterable<Result<Item>> results = minioClient.listObjects(ListObjectsArgs.builder()
                .bucket(bucketName)
                .prefix(filename)
                .recursive(recursive)
                .maxKeys(1000)
                .build());
        for (Result<Item> result : results) {
            try {
                Item item = result.get();
                if (item.objectName().equals(filename)) {
                    flag = true;
                    break;
                }
            } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) {
                e.printStackTrace();
            }
        }
        return flag;
    }

    /**
     * 列出儲存桶中所有物件,可指定字首和是否遞迴搜尋子目錄。
     *
     * @param bucketName 儲存桶名稱。
     * @param predir 字首過濾器。
     * @param recursive 是否遞迴搜尋子目錄。
     * @return 返回一個包含所有匹配物件的結果列表。
     */
    @Override
    public List<Result<Item>> listObjects(String bucketName, String predir, boolean recursive) {
        Iterable<Result<Item>> results = minioClient.listObjects(ListObjectsArgs.builder()
                .bucket(bucketName)
                .prefix(predir)
                .recursive(recursive)
                .maxKeys(1000)
                .build());
        List<Result<Item>> list = new ArrayList<>();
        results.forEach(list::add);
        return list;
    }

    /**
     * 構建儲存桶中物件的ComposeSource列表,用於複合物件操作。
     *
     * @param bucketName 儲存桶名稱。
     * @param predir 字首過濾器。
     * @return 返回一個包含所有匹配物件的ComposeSource列表。
     */
    @Override
    public List<ComposeSource> listObjects(String bucketName, String predir) {
        Iterable<Result<Item>> results = minioClient.listObjects(ListObjectsArgs.builder()
                .bucket(bucketName)
                .prefix(predir)
                .recursive(true)
                .maxKeys(1000)
                .build());
        List<Result<Item>> list = new ArrayList<>();
        results.forEach(list::add);

        List<ComposeSource> sources = new ArrayList<>();
        for (Result<Item> itemResult : list) {
            try {
                sources.add(ComposeSource.builder().bucket(bucketName).object(itemResult.get().objectName()).build());
            } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) {
                e.printStackTrace();
            }
        }
        return sources;
    }


    /**
     * 複製一個物件到新的儲存桶或新的物件名稱。
     *
     * @param pastBucket  原始儲存桶名稱
     * @param pastObject  原始物件名稱
     * @param newBucket   新的儲存桶名稱
     * @param newObject   新的物件名稱
     * @return 如果複製成功,則返回true;否則返回false。
     */
    @Override
    public boolean copyObject(String pastBucket, String pastObject, String newBucket, String newObject) {
        try {
            ObjectWriteResponse response = minioClient.copyObject(
                    CopyObjectArgs.builder()
                            .bucket(newBucket)
                            .object(newObject)
                            .source(
                                    CopySource.builder()
                                            .bucket(pastBucket)
                                            .object(pastObject)
                                            .build())
                            .build());
        } catch (ErrorResponseException e) {
            e.printStackTrace();
            return false;
        } catch (InsufficientDataException | InternalException | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) {
            e.printStackTrace();
        }
        return true;
    }

    /**
     * 下載儲存桶中的物件到本地檔案系統。
     *
     * @param bucketName  儲存桶名稱
     * @param objectName  物件名稱
     * @param targetPath  本地目標路徑
     * @return 如果下載成功,則返回true;否則返回false。
     */
    @Override
    public boolean downObject(String bucketName, String objectName, String targetPath) {
        try {
            if ("".equals(objectName) || null == objectName) {
                throw new RuntimeException("檢查電子檔案FilePath是否為空,下載目標位置為:" + targetPath);
            }
            minioClient.downloadObject(
                    DownloadObjectArgs.builder()
                            .bucket(bucketName)
                            .object(objectName)
                            .filename(targetPath)      //download local path
                            .build());
        } catch (ErrorResponseException e) {
            e.printStackTrace();
            return false;
        } catch (InsufficientDataException | InternalException | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) {
            e.printStackTrace();
        }
        return true;
    }

    /**
     * 獲取儲存桶中物件的預簽名URL(GET方法)。
     *
     * @param bucketName  儲存桶名稱
     * @param object      物件名稱
     * @param expire      過期時間(分鐘)
     * @return 預簽名URL,如果發生錯誤則返回null。
     */
    @Override
    public String presignedURLofObject(String bucketName, String object, int expire) {
        String url = null;
        try {
            url = minioClient.getPresignedObjectUrl(
                    GetPresignedObjectUrlArgs.builder()
                            .method(Method.GET)
                            .bucket(bucketName)
                            .object(object)
                            .expiry(expire, TimeUnit.MINUTES)
                            .build());
        } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException | XmlParserException | ServerException e) {
            e.printStackTrace();
        }
        return url;
    }

    /**
     * 獲取儲存桶中物件的預簽名URL,允許設定額外的查詢引數。
     *
     * @param bucketName  儲存桶名稱
     * @param object      物件名稱
     * @param expire      過期時間(分鐘)
     * @param map         額外的查詢引數
     * @return 預簽名URL,如果發生錯誤則返回空字串。
     */
    @Override
    public String presignedURLofObject(String bucketName, String object, int expire, Map<String, String> map) {
        map.put("response-content-type", "application/json");
        String presignedObjectUrl = "";
        try {
            presignedObjectUrl = minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
                    .bucket(bucketName)
                    .object(object)
                    .method(Method.GET)
                    .expiry(expire, TimeUnit.MINUTES)
                    .extraQueryParams(map)
                    .build());
        } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException | XmlParserException | ServerException e) {
            e.printStackTrace();
        }
        return presignedObjectUrl;
    }

    /**
     * 刪除儲存桶中的物件。
     *
     * @param bucketName  儲存桶名稱
     * @param object      物件名稱
     * @return 如果刪除成功,則返回true;否則返回false。
     */
    @Override
    public boolean deleteObject(String bucketName, String object) {
        try {
            minioClient.removeObject(RemoveObjectArgs.builder()
                    .bucket(bucketName)
                    .object(object)
                    .build());
        } catch (ErrorResponseException e) {
            e.printStackTrace();
            return false;
        } catch (InsufficientDataException | InternalException | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) {
            e.printStackTrace();
        }
        return true;
    }

    /**
     * 將本地檔案上傳至MinIO儲存桶。
     *
     * @param bucketName     儲存桶名稱
     * @param targetObject   目標物件名稱
     * @param sourcePath     原始檔路徑
     * @return 如果上傳成功,則返回true;否則返回false。
     */
    @Override
    public boolean uploadObject(String bucketName, String targetObject, String sourcePath) {
        try {
            minioClient.uploadObject(UploadObjectArgs.builder()
                    .bucket(bucketName)
                    .object(targetObject)
                    .filename(sourcePath)
                    .build());
        } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 將輸入流資料寫入MinIO儲存桶。
     *
     * @param bucketName 儲存桶名稱
     * @param object     物件名稱
     * @param inputStream 輸入流
     * @return 如果寫入成功,則返回true;否則返回false。
     */
    @Override
    public boolean putObject(String bucketName, String object, InputStream inputStream) {
        try {
            minioClient.putObject(PutObjectArgs.builder()
                    .bucket(bucketName)
                    .object(object)
                    .stream(inputStream, -1, 10485760)
                    .build());
        } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 將帶有標籤的輸入流資料寫入MinIO儲存桶。
     *
     * @param bucketName 儲存桶名稱
     * @param object     物件名稱
     * @param inputStream 輸入流
     * @param tags       物件標籤
     * @return 如果寫入成功,則返回true;否則返回false。
     */
    @Override
    public boolean putObject(String bucketName, String object, InputStream inputStream, Map<String, String> tags) {
        try {
            minioClient.putObject(PutObjectArgs.builder()
                    .bucket(bucketName)
                    .object(object)
                    .stream(inputStream, -1, 10485760)
                    .tags(tags)
                    .build());
        } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 從MinIO儲存桶獲取物件。
     *
     * @param bucketName 儲存桶名稱
     * @param object     物件名稱
     * @return 返回物件響應資訊,如果發生錯誤則返回null。
     */
    @Override
    public GetObjectResponse getObject(String bucketName, String object) {
        GetObjectResponse object1 = null;
        try {
            object1 = minioClient.getObject(GetObjectArgs.builder()
                    .bucket(bucketName)
                    .object(object)
                    .build());
        } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) {
            e.printStackTrace();
        }
        return object1;
    }

    /**
     * 獲取物件的標籤資訊。
     *
     * @param bucketName 儲存桶名稱
     * @param object     物件名稱
     * @return 返回物件的標籤對映,如果發生錯誤則返回null。
     */
    @Override
    public Map<String, String> getTags(String bucketName, String object) {
        Map<String, String> map = new HashMap<>();
        try {
            Tags objectTags = minioClient.getObjectTags(GetObjectTagsArgs.builder()
                    .bucket(bucketName)
                    .object(object)
                    .build());
            map = objectTags.get();
        } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) {
            e.printStackTrace();
            return null;
        }
        return map;
    }

    /**
     * 向物件新增或更新標籤。
     *
     * @param bucketName 儲存桶名稱
     * @param object     物件名稱
     * @param addTags    要新增或更新的標籤對映
     * @return 如果操作成功,則返回true;否則返回false。
     */
    @Override
    public boolean addTags(String bucketName, String object, Map<String, String> addTags) {
        Map<String, String> oldtags = new HashMap<>();
        Map<String, String> newTags = new HashMap<>();
        try {
            oldtags = getTags(bucketName, object);
            if (oldtags.size() > 0) {
                newTags.putAll(oldtags);
            }
            if (addTags != null && addTags.size() > 0) {
                newTags.putAll(addTags);
            }
            minioClient.setObjectTags(SetObjectTagsArgs.builder()
                    .bucket(bucketName)
                    .object(object)
                    .tags(newTags)
                    .build());
            return true;
        } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 獲取物件的狀態資訊。
     *
     * @param bucketName 儲存桶名稱
     * @param object     物件名稱
     * @return 返回物件狀態資訊,如果發生錯誤則返回null。
     */
    @Override
    public StatObjectResponse statObject(String bucketName, String object) {
        StatObjectResponse statObject = null;
        try {
            statObject =
                    minioClient.statObject(
                            StatObjectArgs.builder()
                                    .bucket(bucketName)
                                    .object(object)
                                    .build());
        } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) {
            e.printStackTrace();
        }
        return statObject;
    }

    /**
     * 判斷儲存桶中是否存在指定物件。
     *
     * @param bucketName   儲存桶名稱
     * @param objectName   物件名稱
     * @return 如果存在,則返回true;否則返回false。
     */
    @Override
    public boolean ifExistObject(String bucketName, String objectName) {
        return listObjects(bucketName, objectName, true).size() >= 1;
    }

    /**
     * 從後設資料儲存桶中獲取與特定物件相關的後設資料物件名。
     *
     * @param objectName 原始物件名稱
     * @return 返回編碼後的後設資料物件名,如果沒有找到對應的後設資料則返回其檔名。
     */
    @Override
    public String getMetaNameFromOther(String objectName) {
        String metaobject = "";
        List<Result<Item>> results = listObjects(BucketName.METADATA, FileUtil.getPreMeta(objectName), true);
        if (results.size() == 1) {
            try {
                metaobject = results.get(0).get().objectName();
                Map<String, String> tags = getTags(BucketName.METADATA, metaobject);
                String s = tags.get(ObjectTags.FILENAME);
                // 解碼後再編碼以確保正確處理特殊字元
                // URLDecoder.decode(s,"UTF-8");
                return URLEncoder.encode(s, "UTF-8");
            } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) {
                e.printStackTrace();
            }
        }
        return FileUtil.getFileName(metaobject);
    }

    /**
     * 修改物件的標籤資訊。
     *
     * @param object 物件名稱
     * @param tag 新的標籤值
     * @return 如果修改成功,則返回true;否則返回false。
     */
    @Override
    public boolean changeTag(String object, String tag) {
        try {
            Map<String, String> map = minioClient.getObjectTags(GetObjectTagsArgs.builder()
                    .bucket(BucketName.METADATA)
                    .object(object)
                    .build()).get();

            Map<String, String> map1 = new HashMap<>();
            tag = tag + FileUtil.getSuffix(object);
            map1.put(ObjectTags.FILENAME, tag);
            map1.put(ObjectTags.OPERATOR, map.get(ObjectTags.OPERATOR));

            minioClient.setObjectTags(SetObjectTagsArgs.builder()
                    .bucket(BucketName.METADATA)
                    .object(object)
                    .tags(map1)
                    .build());
            return true;
        } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 設定儲存桶的訪問策略為公開訪問。
     *
     * @param bucketName 儲存桶名稱
     */
    @Override
    public void BucketAccessPublic(String bucketName) {
        String config = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Action\":[\"s3:ListBucketMultipartUploads\",\"s3:GetBucketLocation\",\"s3:ListBucket\"],\"Resource\":[\"arn:aws:s3:::" + bucketName + "\"]},{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Action\":[\"s3:ListMultipartUploadParts\",\"s3:PutObject\",\"s3:AbortMultipartUpload\",\"s3:DeleteObject\",\"s3:GetObject\"],\"Resource\":[\"arn:aws:s3:::" + bucketName + "/*\"]}]}";
        try {
            minioClient.setBucketPolicy(SetBucketPolicyArgs.builder().bucket(bucketName).config(config).build());
        } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) {
            e.printStackTrace();
        }
    }

}

相關文章