探索Amazon S3:儲存解決方案的基石(Amazon S3使用記錄)

Comfortable發表於2024-07-31

探索Amazon S3:儲存解決方案的基石

本文為上一篇minio使用的衍生版

相關連結:1.https://www.cnblogs.com/ComfortableM/p/18286363

​ 2.https://blog.csdn.net/zizai_a/article/details/140796186?spm=1001.2014.3001.5501

目錄
  • 探索Amazon S3:儲存解決方案的基石
    • 引言
    • Amazon S3簡介
    • 快速使用
    • Spring boot整合
      • 新增依賴
      • 建立配置類
      • 基本操作介紹
        • 建立桶
        • 上傳
        • 分片上傳
        • 下載
        • 獲取物件預簽名url
      • 部分方法集
          • S3Service
          • S3ServiceImpl

引言

雲端儲存已經成為現代資訊科技不可或缺的一部分,它為企業和個人提供了諸多便利。以下是幾個關鍵點,說明雲端儲存為何如此重要:

1. 資料安全與備份

  • 資料加密:雲端儲存服務商通常提供高階加密技術,確保資料在傳輸過程中和儲存時的安全。
  • 備份與恢復:雲端儲存能夠自動備份資料,並且在發生災難性事件時,可以迅速恢復資料,保證業務連續性不受影響。

2. 成本效益

  • 按需付費:使用者可以根據實際使用的儲存空間支付費用,避免了傳統儲存方式中預購大量儲存空間的成本浪費。
  • 運維成本降低:雲端儲存減少了企業在硬體採購、維護和升級方面的開銷,同時也降低了電力和冷卻成本。

3. 靈活性與可擴充套件性

  • 無限擴充套件:隨著資料量的增長,雲端儲存可以輕鬆地擴充套件儲存容量,無需使用者手動增加硬體資源。
  • 多租戶模型:使用者可以輕鬆地管理不同的專案或部門的資料隔離,而不會受到物理限制的影響。

4. 訪問與協作

  • 遠端訪問:無論使用者身處何處,只要有網際網路連線,就可以訪問儲存在雲端的資料。
  • 檔案共享:透過簡單的連結分享機制,團隊成員之間可以輕鬆共享檔案,促進協作。

5. 災難恢復

  • 多地域複製:雲端儲存服務通常提供資料的多地域複製功能,確保即使某個資料中心發生故障,資料依然可用。
  • 快速恢復:當遇到意外情況時,雲端儲存可以迅速恢復資料,減少資料丟失的風險。

6. 技術創新與支援

  • 最新技術:雲端儲存服務商會持續更新技術棧,確保使用者能夠獲得最新的儲存技術和安全措施。
  • 技術支援:專業的技術支援團隊可以及時響應使用者的疑問和技術難題。

7. 法規遵從

  • 合規性:許多雲端儲存服務提供商遵循嚴格的法規標準,確保資料儲存符合地區法律法規要求。

8. 對智慧城市的貢獻

  • 城市管理:雲端儲存對於收集、處理和分析智慧城市中產生的大量資料至關重要,有助於提高城市管理效率和服務質量。

綜上所述,雲端儲存不僅改變了企業和個人管理資料的方式,還推動了整個社會向著更加高效、可持續的方向發展。隨著技術的進步,我們可以期待雲端儲存在未來扮演更加重要的角色。

Amazon Simple Storage Service (S3) 是 Amazon Web Services (AWS) 中最成熟且廣泛使用的物件儲存服務之一。自從2006年推出以來,S3 已經成為雲端計算領域的一個標誌性產品,它不僅為AWS奠定了基礎,而且在全球範圍內成為了雲端儲存的標準之一。

關鍵點:

  • 成熟度與可靠性:經過多年的運營,S3 已經證明了其極高的可靠性和穩定性。它能夠處理大量的資料儲存需求,並保持高可用性和永續性,平均故障時間(MTTF)達到了數百年。
  • 廣泛的採用率:S3 被成千上萬的企業所使用,包括初創公司、大型企業和政府機構,這些組織依賴於 S3 來儲存各種型別的資料,從小檔案到PB級別的資料集。
  • 豐富的功能集:S3 提供了一系列強大的功能,如版本控制、生命週期管理、資料加密、訪問控制等,這些功能使得 S3 成為了一個靈活且全面的儲存解決方案。
  • 整合與相容性:S3 與其他 AWS 服務緊密整合,比如 Amazon Elastic Compute Cloud (EC2)、Amazon Redshift 和 AWS Lambda,使得使用者可以在 AWS 生態系統內部構建複雜的應用程式和服務。此外,S3 還支援多種第三方工具和服務,使其成為資料處理管道的核心組成部分。
  • 成本效益:S3 提供了多種儲存類別的選擇,允許使用者根據資料訪問頻率和儲存需求來最佳化成本。例如,Standard 類別適用於頻繁訪問的資料,而 Infrequent Access (IA) 或 Glacier 類別則適用於長期存檔資料。
  • 技術創新:S3 不斷推出新的特性和改進,以滿足不斷變化的市場需求。例如,Intelligent-Tiering 儲存類別能夠自動將資料移動到最合適的儲存層,從而幫助使用者節省成本。
  • 行業認可:S3 獲得了多個行業獎項和認證,這反映了它在雲端儲存領域的領導地位。

Amazon S3 作為 AWS 的旗艦級服務之一,在過去十幾年裡已經成為了全球雲端儲存領域的標杆。無論是從成熟度、可靠性還是功能豐富性來看,S3 都是企業和開發者信賴的選擇。隨著 AWS 繼續在其基礎上進行創新和發展,我們有理由相信 S3 將繼續引領雲端儲存的發展趨勢。

Amazon S3簡介

Amazon Simple Storage Service (S3) 是亞馬遜 Web Services (AWS) 提供的一種物件儲存服務。自2006年推出以來,S3 已經成為了雲端儲存領域的一個標誌性產品,它為開發者和企業提供了一種簡單、高效的方式來儲存和檢索任意數量的資料。

核心特點:

  • 高可用性和永續性:S3 被設計為能夠承受嚴重的系統故障,確保資料的高度永續性。其設計目標是每年的資料丟失率為0.000000000001(11個9),這意味著資料幾乎不會丟失。S3 使用多副本冗餘儲存來保護資料免受元件故障的影響,並且資料被自動分佈在多個設施中,以防止區域性的災難影響資料的可用性。
  • 無限的可擴充套件性:S3 的架構允許無縫擴充套件,無需預先規劃儲存容量。企業可以根據需要儲存從GB到EB級別的資料,而無需擔心儲存限制。這種自動擴充套件能力意味著企業不必擔心在資料量快速增長時需要手動調整基礎設施。
  • 成本效益:S3 提供了多種儲存類別,使企業可以根據資料的訪問頻率選擇最適合的選項,從而最佳化成本。例如,S3 Standard 適合頻繁訪問的資料,而 S3 Standard-Infrequent Access (S3 Standard-IA) 和 S3 One Zone-Infrequent Access (S3 One Zone-IA) 適用於不經常訪問的資料。S3 智慧分層能夠自動檢測資料的訪問模式,並將資料移動到最經濟的儲存層,以進一步降低成本。
  • 安全性和合規性:S3 支援多種安全功能,如伺服器端加密、客戶端加密、訪問控制策略等,以保護資料免受未經授權的訪問。它還支援多種合規標準,如 HIPAA、FedRAMP、PCI DSS 等,幫助企業遵守行業法規要求。
  • 易於管理和整合:S3 提供了一個直觀的管理控制檯,使使用者能夠輕鬆地上傳、下載和管理資料。它還提供了豐富的 API 和 SDK,以便開發者可以輕鬆地將 S3 整合到他們的應用程式和服務中。
  • 資料生命週期管理:透過 S3 生命週期策略,企業可以自動將資料從一個儲存類別遷移到另一個儲存類別,或者在指定的時間後刪除資料。這種自動化的過程有助於減少管理負擔,並確保資料始終處於最經濟的儲存層。
  • 高效能和全球覆蓋:S3 的全球邊緣位置網路確保了低延遲的資料訪問,無論使用者位於世界哪個角落。這種分散式的架構有助於提高資料的可用性和響應速度。

使用場景:

  • 網站託管:S3 可以用來託管靜態網站,包括HTML頁面、CSS樣式表、JavaScript指令碼和圖片。
  • 資料備份與恢復:企業可以使用 S3 作為資料備份和災難恢復策略的一部分,確保資料的安全性和可恢復性。
  • 大資料處理:S3 可以作為大資料分析的資料湖,儲存原始資料,供後續處理和分析使用。
  • 媒體儲存與分發:S3 適用於儲存和分發影片、音訊和其他多媒體檔案。
  • 應用程式資料儲存:S3 可以儲存應用程式生成的日誌檔案、快取資料、資料庫備份等。

快速使用

  • 註冊賬戶https://signin.aws.amazon.com/signup?request_type=register

    S3具有很多免費使用的套餐,可以為開發前搭建一個小型測試環境。

    註冊成功後登入進控制檯,點選右上角使用者有一個安全憑證:

    下滑到訪問金鑰這,建立一個訪問金鑰,這個是連線S3的憑證。也可以建立一個IAM賬戶,為其分配許可權,用IAM賬戶登入到控制檯來建立訪問金鑰,S3也建議這樣做。

    接下來可以透過左上角服務找到S3自由發揮了。

Spring boot整合

新增依賴

        <dependency>
            <groupId>software.amazon.awssdk</groupId>
            <artifactId>s3</artifactId>
        </dependency>

建立配置類

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.S3Configuration;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;

import java.net.URI;

@Configuration
public class AmazonS3Config {

    @Value("${aws.s3.accessKey}")
    private String accessKeyId;
    @Value("${aws.s3.secretKey}")
    private String secretKey;
    @Value("${aws.s3.endPoint}")
    private String endPoint;//接入點,未設定可以註釋

    @Bean
    public S3Client s3Client() {
        AwsBasicCredentials credentials = AwsBasicCredentials.create(accessKeyId, secretKey);

        return S3Client.builder()
                .credentialsProvider(StaticCredentialsProvider.create(credentials))
                .region(Region.US_EAST_1)//區域
                .endpointOverride(URI.create(endPoint))//接入點
                .serviceConfiguration(S3Configuration.builder()
                        .pathStyleAccessEnabled(true)
                        .chunkedEncodingEnabled(false)
                        .build())
                .build();
    }

    //S3Presigner是用來獲取檔案物件預簽名url的
    @Bean
    public S3Presigner s3Presigner() {
        AwsBasicCredentials credentials = AwsBasicCredentials.create(accessKeyId, secretKey);

        return S3Presigner.builder()
                .credentialsProvider(StaticCredentialsProvider.create(credentials))
                .region(Region.US_EAST_1)
                .endpointOverride(URI.create(endPoint))
                .build();
    }
}

基本操作介紹

建立桶
    public boolean ifExistsBucket(String bucketName) {
        // 嘗試傳送 HEAD 請求來檢查儲存桶是否存在
        try {
            HeadBucketResponse headBucketResponse = s3Client.headBucket(HeadBucketRequest.builder().bucket(bucketName).build());
        } catch (S3Exception e) {
            // 如果捕獲到 S3 異常且狀態碼為 404,則說明儲存桶不存在
            if (e.statusCode() == 404) {
                return false;
            } else {
                // 列印異常堆疊跟蹤
                e.printStackTrace();
            }
        }
        // 如果沒有丟擲異常或狀態碼不是 404,則說明儲存桶存在
        return true;
    }

    public boolean createBucket(String bucketName) throws RuntimeException {
        // 檢查儲存桶是否已經存在
        if (ifExistsBucket(bucketName)) {
            // 如果儲存桶已存在,則丟擲執行時異常
            throw new RuntimeException("桶已存在");
        }
        // 建立新的儲存桶
        S3Response bucket = s3Client.createBucket(CreateBucketRequest.builder().bucket(bucketName).build());
        // 檢查儲存桶是否建立成功
        return ifExistsBucket(bucketName);
    }

    public boolean removeBucket(String bucketName) {
        // 如果儲存桶不存在,則直接返回 true
        if (!ifExistsBucket(bucketName)) {
            return true;
        }
        // 刪除儲存桶
        s3Client.deleteBucket(DeleteBucketRequest.builder().bucket(bucketName).build());
        // 檢查儲存桶是否已被刪除
        return !ifExistsBucket(bucketName);
    }
上傳
    public boolean uploadObject(String bucketName, String targetObject, String sourcePath) {
        // 嘗試上傳本地檔案到指定的儲存桶和物件鍵
        try {
            s3Client.putObject(PutObjectRequest.builder()
                    .bucket(bucketName)
                    .key(targetObject)
                    .build(), RequestBody.fromFile(new File(sourcePath)));
        } catch (Exception e) {
            // 如果出現異常,則列印異常堆疊跟蹤並返回 false
            e.printStackTrace();
            return false;
        }
        // 如果上傳成功則返回 true
        return true;
    }

    public boolean putObject(String bucketName, String object, InputStream inputStream, long size) {
        // 嘗試從輸入流上傳資料到指定的儲存桶和物件鍵
        try {
            s3Client.putObject(PutObjectRequest.builder()
                    .bucket(bucketName)
                    .key(object)
                    .build(), RequestBody.fromInputStream(inputStream, size));
        } catch (Exception e) {
            // 如果出現異常,則列印異常堆疊跟蹤並返回 false
            e.printStackTrace();
            return false;
        }
        // 如果上傳成功則返回 true
        return true;
    }

    public boolean putObject(String bucketName, String object, InputStream inputStream, long size, Map<String, String> tags) {
        // 嘗試從輸入流上傳資料到指定的儲存桶和物件鍵,並設定標籤
        try {
            // 將標籤對映轉換為標籤集合
            Collection<Tag> tagList = tags.entrySet().stream().map(entry ->
                    Tag.builder().key(entry.getKey()).value(entry.getValue()).build())
                    .collect(Collectors.toList());
            // 上傳物件並設定標籤
            s3Client.putObject(PutObjectRequest.builder()
                    .bucket(bucketName)
                    .key(object)
                    .tagging(Tagging.builder()
                            .tagSet(tagList)
                            .build())
                    .build(), RequestBody.fromInputStream(inputStream, size));
        } catch (Exception e) {
            // 如果出現異常,則列印異常堆疊跟蹤並返回 false
            e.printStackTrace();
            return false;
        }
        // 如果上傳成功則返回 true
        return true;
    }
分片上傳
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.*;
import software.amazon.awssdk.core.sync.RequestBody;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

public class S3MultipartUploader {

    private static final S3Client s3Client = S3Client.create();

    /**
     * 開始一個新的分片上傳會話。
     *
     * @param bucketName 儲存桶名稱
     * @param objectKey 物件鍵
     * @return 返回的 UploadId 用於後續操作
     */
    public static InitiateMultipartUploadResponse initiateMultipartUpload(String bucketName, String objectKey) {
        InitiateMultipartUploadRequest request = InitiateMultipartUploadRequest.builder()
                .bucket(bucketName)
                .key(objectKey)
                .build();
        return s3Client.initiateMultipartUpload(request);
    }

    /**
     * 上傳一個分片。
     *
     * @param bucketName 儲存桶名稱
     * @param objectKey 物件鍵
     * @param uploadId 上傳會話的 ID
     * @param partNumber 分片序號
     * @param file 檔案
     * @param offset 檔案偏移量
     * @param length 檔案長度
     * @return 返回的 Part ETag 用於完成上傳時驗證
     */
    public static UploadPartResponse uploadPart(String bucketName, String objectKey, String uploadId,
                                                int partNumber, File file, long offset, long length) {
        try (FileInputStream fis = new FileInputStream(file)) {
            UploadPartRequest request = UploadPartRequest.builder()
                    .bucket(bucketName)
                    .key(objectKey)
                    .uploadId(uploadId)
                    .partNumber(partNumber)
                    .build();
            RequestBody requestBody = RequestBody.fromInputStream(fis, length, offset);
            return s3Client.uploadPart(request, requestBody);
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 完成分片上傳。
     *
     * @param bucketName 儲存桶名稱
     * @param objectKey 物件鍵
     * @param uploadId 上傳會話的 ID
     * @param parts 已上傳的分片列表
     */
    public static CompleteMultipartUploadResponse completeMultipartUpload(String bucketName, String objectKey,
                                                                          String uploadId, List<CompletedPart> parts) {
        CompleteMultipartUploadRequest request = CompleteMultipartUploadRequest.builder()
                .bucket(bucketName)
                .key(objectKey)
                .uploadId(uploadId)
                .multipartUpload(CompletedMultipartUpload.builder()
                        .parts(parts)
                        .build())
                .build();
        return s3Client.completeMultipartUpload(request);
    }

    /**
     * 取消分片上傳會話。
     *
     * @param bucketName 儲存桶名稱
     * @param objectKey 物件鍵
     * @param uploadId 上傳會話的 ID
     */
    public static void abortMultipartUpload(String bucketName, String objectKey, String uploadId) {
        AbortMultipartUploadRequest request = AbortMultipartUploadRequest.builder()
                .bucket(bucketName)
                .key(objectKey)
                .uploadId(uploadId)
                .build();
        s3Client.abortMultipartUpload(request);
    }

    public static void main(String[] args) {
        String bucketName = "your-bucket-name";
        String objectKey = "path/to/your-object";
        File fileToUpload = new File("path/to/your/local/file");

        // 開始分片上傳會話
        InitiateMultipartUploadResponse initResponse = initiateMultipartUpload(bucketName, objectKey);
        String uploadId = initResponse.uploadId();

        // 計算檔案大小和分片數量
        long fileSize = fileToUpload.length();
        int partSize = 5 * 1024 * 1024; // 假設每個分片大小為 5MB
        int numberOfParts = (int) Math.ceil((double) fileSize / partSize);

        // 上傳每個分片
        List<CompletedPart> completedParts = new ArrayList<>();
        try (FileInputStream fis = new FileInputStream(fileToUpload)) {
            for (int i = 1; i <= numberOfParts; i++) {
                long startOffset = (i - 1) * partSize;
                long currentPartSize = Math.min(partSize, fileSize - startOffset);

                // 上傳分片
                UploadPartResponse partResponse = uploadPart(bucketName, objectKey, uploadId, i, fileToUpload, startOffset, currentPartSize);
                if (partResponse != null) {
                    // 儲存已完成分片的資訊
                    CompletedPart completedPart = CompletedPart.builder()
                            .partNumber(i)
                            .eTag(partResponse.eTag())
                            .build();
                    completedParts.add(completedPart);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 完成分片上傳
        CompleteMultipartUploadResponse completeResponse = completeMultipartUpload(bucketName, objectKey, uploadId, completedParts);
        if (completeResponse != null) {
            System.out.println("分片上傳成功!");
        } else {
            System.out.println("分片上傳失敗。");
        }
    }
}
下載
    public boolean downObject(String bucketName, String objectName, String targetPath) {
        // 嘗試下載物件到指定路徑
        try {
            s3Client.getObject(GetObjectRequest.builder()
                    .bucket(bucketName)
                    .key(objectName)
                    .build(), new File(targetPath).toPath());
        } catch (Exception e) {
            // 如果出現異常則列印異常堆疊跟蹤並返回 false
            e.printStackTrace();
            return false;
        }
        // 如果下載成功則返回 true
        return true;
    }

    public InputStream getObject(String bucketName, String object) {
        InputStream objectStream = null;
        // 嘗試獲取指定儲存桶和物件鍵的輸入流
        try {
            objectStream = s3Client.getObject(GetObjectRequest.builder()
                    .bucket(bucketName)
                    .key(object)
                    .build());
        } catch (Exception e) {
            // 如果出現異常,則列印異常堆疊跟蹤並返回 null
            e.printStackTrace();
            return null;
        }
        // 返回物件的輸入流
        return objectStream;
    }

    public ResponseBytes<GetObjectResponse> getObjectAsBytes(String bucketName, String object) {
        ResponseBytes<GetObjectResponse> objectAsBytes = null;
        // 嘗試獲取指定儲存桶和物件鍵的內容作為位元組
        try {
            objectAsBytes = s3Client.getObjectAsBytes(GetObjectRequest.builder()
                    .bucket(bucketName)
                    .key(object)
                    .build());
        } catch (Exception e) {
            // 如果出現異常,則列印異常堆疊跟蹤並返回 null
            e.printStackTrace();
            return null;
        }
        // 返回物件的內容作為位元組
        return objectAsBytes;
    }
獲取物件預簽名url
    public String presignedURLofObject(String bucketName, String object, int expire) {
        URL url = null;
        // 嘗試生成預簽名 URL
        try {
            url = s3Presigner.presignGetObject(GetObjectPresignRequest.builder()
                    .signatureDuration(Duration.ofMinutes(expire))
                    .getObjectRequest(GetObjectRequest.builder()
                            .bucket(bucketName)
                            .key(object)
                            .build())
                    .build()).url();
        } catch (Exception e) {
            // 如果出現異常則列印異常堆疊跟蹤並返回 null
            e.printStackTrace();
            return null;
        } finally {
            // 關閉預簽名客戶端
            s3Presigner.close();
        }
        // 返回預簽名 URL 的字串表示形式
        return url.toString();
    }

獲取到的url是可以直接在瀏覽器預覽的,但是要用其他外掛(如 pdf.js)預覽的話會出現跨域的錯誤,這個時候就需要給要訪問的桶新增一下cors規則。

程式碼新增cors規則:

    CORSRule corsRule = CORSRule.builder()
            .allowedOrigins("http://ip:埠")//可以設定多個
        	//.allowedOrigins("*")
            .allowedHeaders("Authorization")
            .allowedMethods("GET","HEAD")
            .exposeHeaders("Access-Control-Allow-Origin").build();

    // 設定 CORS 配置
    s3Client.putBucketCors(PutBucketCorsRequest.builder()
            .bucket(bucketName)
            .corsConfiguration(CORSConfiguration.builder()
                    .corsRules(corsRule)
                    .build())
            .build());

S3控制檯新增CORS規則:

點選你的桶選擇許可權,下拉找到這個設定,編輯新增下面的規則:

[
    {
        "AllowedHeaders": [
            "Authorization"
        ],
        "AllowedMethods": [
            "GET",
            "HEAD"
        ],
        "AllowedOrigins": [
            "http://ip:埠"
        ],
        "ExposeHeaders": [
            "Access-Control-Allow-Origin"
        ],
        "MaxAgeSeconds": 3000
    }
]

用S3 Browser新增

S3 Browser 是一款用於管理 Amazon S3 儲存服務的第三方工具。它提供了一個圖形使用者介面(GUI),讓使用者能夠更方便地上傳、下載、管理和瀏覽儲存在 Amazon S3 中的物件和儲存桶。也可以用來連線minio或者其他儲存。

新增你的配置等待掃描出桶名後,右擊桶名選擇CORS Configuration新增你的規則,點選apply使用。

<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <CORSRule>
    <AllowedOrigin>http://ip:埠</AllowedOrigin>
    <AllowedMethod>GET</AllowedMethod>
    <AllowedMethod>HEAD</AllowedMethod>
    <MaxAgeSeconds>3000</MaxAgeSeconds>
    <ExposeHeader>Access-Control-Allow-Origin</ExposeHeader>
    <AllowedHeader>Authorization</AllowedHeader>
  </CORSRule>
</CORSConfiguration>

部分方法集

因為是另一個專案的延伸版,所以有些方法反而改的更繁瑣的一些。

S3Service
import org.springframework.stereotype.Service;
import software.amazon.awssdk.core.ResponseBytes;
import software.amazon.awssdk.services.s3.model.Bucket;
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
import software.amazon.awssdk.services.s3.model.HeadObjectResponse;
import software.amazon.awssdk.services.s3.model.S3Object;

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

/**
 * 定義與 AWS S3 客戶端互動的一般功能。
 */
@Service
public interface S3Service {

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

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

    /**
     * 刪除一個儲存桶。儲存桶必須為空;否則不會被刪除。
     * 即使指定的儲存桶不存在也會返回 true。
     *
     * @param bucketName 要刪除的儲存桶名稱。
     * @return 如果儲存桶被刪除或不存在返回 true。
     */
    boolean removeBucket(String bucketName);

    /**
     * 列出當前 S3 伺服器上所有存在的儲存桶。
     *
     * @return 儲存桶列表。
     */
    List<Bucket> alreadyExistBuckets();

    /**
     * 列出儲存桶中的物件,可選地透過字首過濾並指定是否包括子目錄。
     *
     * @param bucketName 儲存桶名稱。
     * @param predir     字首過濾條件。
     * @param recursive  是否包括子目錄。
     * @return S3 物件列表。
     */
    List<S3Object> listObjects(String bucketName, String predir, boolean recursive);

    /**
     * 列出儲存桶中的物件,透過字首過濾。
     *
     * @param bucketName 儲存桶名稱。
     * @param predir     字首過濾條件。
     * @return S3 物件列表。
     */
    List<S3Object> listObjects(String bucketName, String predir);

    /**
     * 複製一個物件檔案從一個儲存桶到另一個儲存桶。
     *
     * @param pastBucket 原始檔所在的儲存桶。
     * @param pastObject 原始檔在儲存桶內的路徑。
     * @param newBucket  將要複製進的目標儲存桶。
     * @param newObject  將要複製進的目標儲存桶內的路徑。
     * @return 如果複製成功返回 true。
     */
    boolean copyObject(String pastBucket, String pastObject, String newBucket, String newObject);

    /**
     * 下載一個物件。
     *
     * @param bucketName 儲存桶名稱。
     * @param objectName 物件路徑及名稱(例如:2022/02/02/xxx.doc)。
     * @param targetPath 目標路徑及名稱(例如:/opt/xxx.doc)。
     * @return 如果下載成功返回 true。
     */
    boolean downObject(String bucketName, String objectName, String targetPath);

    /**
     * 返回物件的簽名 URL。
     *
     * @param bucketName 儲存桶名稱。
     * @param object     物件路徑及名稱。
     * @param expire     過期時間(分鐘)。
     * @return 簽名 URL。
     */
    String presignedURLofObject(String bucketName, String object, int expire);

    /**
     * 返回帶有額外引數的物件簽名 URL。
     *
     * @param bucketName 儲存桶名稱。
     * @param object     物件路徑及名稱。
     * @param expire     過期時間(分鐘)。
     * @param map        額外引數。
     * @return 簽名 URL。
     */
    String presignedURLofObject(String bucketName, String object, int expire, Map<String, String> map);

    /**
     * 刪除一個物件。
     *
     * @param bucketName 儲存桶名稱。
     * @param object     物件名稱(路徑)。
     * @return 如果刪除成功返回 true。
     */
    boolean deleteObject(String bucketName, String object);

    /**
     * 上傳一個物件,使用本地檔案作為源。
     *
     * @param bucketName 儲存桶名稱。
     * @param targetObject 目標物件的名稱。
     * @param sourcePath   本地檔案路徑(例如:/opt/1234.doc)。
     * @return 如果上傳成功返回 true。
     */
    boolean uploadObject(String bucketName, String targetObject, String sourcePath);

    /**
     * 上傳一個物件,使用輸入流作為源。
     *
     * @param bucketName 儲存桶名稱。
     * @param object     物件名稱。
     * @param inputStream 輸入流。
     * @param size       物件大小。
     * @return 如果上傳成功返回 true。
     */
    boolean putObject(String bucketName, String object, InputStream inputStream, long size);

    /**
     * 上傳一個帶有標籤的物件。
     *
     * @param bucketName 儲存桶名稱。
     * @param object     物件名稱。
     * @param inputStream 輸入流。
     * @param size       物件大小。
     * @param tags       標籤集合。
     * @return 如果上傳成功返回 true。
     */
    boolean putObject(String bucketName, String object, InputStream inputStream, long size, Map<String, String> tags);

    /**
     * 獲取一個物件。
     *
     * @param bucketName 儲存桶名稱。
     * @param object     物件名稱。
     * @return GetObjectResponse 型別的輸入流。
     */
    InputStream getObject(String bucketName, String object);

    /**
     * 獲取一個物件的內容作為位元組流。
     *
     * @param bucketName 儲存桶名稱。
     * @param object     物件名稱。
     * @return 包含物件內容的位元組流。
     */
    ResponseBytes<GetObjectResponse> getObjectAsBytes(String bucketName, String object);

    /**
     * 判斷一個檔案是否存在於儲存桶中。
     *
     * @param bucketName 儲存桶名稱。
     * @param filename   檔名稱。
     * @param recursive  是否遞迴搜尋。
     * @return 如果檔案存在返回 true。
     */
    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。
     */
    boolean addTags(String bucketName, String object, Map<String, String> addTags);

    /**
     * 獲取物件的物件資訊和後設資料。
     *
     * @param bucketName 儲存桶名稱。
     * @param object     物件名稱。
     * @return HeadObjectResponse 型別的物件資訊和後設資料。
     */
    HeadObjectResponse statObject(String bucketName, String object);

    /**
     * 判斷一個物件是否存在。
     *
     * @param bucketName 儲存桶名稱。
     * @param objectName 物件名稱。
     * @return 如果物件存在返回 true。
     */
    boolean ifExistObject(String bucketName, String objectName);

    /**
     * 從其他物件名稱中獲取後設資料名稱。
     *
     * @param objectName 物件名稱。
     * @return 後設資料名稱。
     */
    String getMetaNameFromOther(String objectName);

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

    /**
     * 設定儲存桶為公共訪問。
     *
     * @param bucketName 儲存桶名稱。
     */
    void BucketAccessPublic(String bucketName);

}
S3ServiceImpl
import com.xagxsj.erms.model.BucketName;
import com.xagxsj.erms.model.ObjectTags;
import com.xagxsj.erms.service.S3Service;
import com.xagxsj.erms.utils.FileUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import software.amazon.awssdk.core.ResponseBytes;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.*;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;

import java.io.File;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLEncoder;
import java.time.Duration;
import java.util.*;
import java.util.logging.Logger;
import java.util.stream.Collectors;

@Service
public class S3ServiceImpl implements S3Service {
    private final Logger log = Logger.getLogger(this.getClass().getName());
    @Qualifier("s3Client")
    @Autowired
    S3Client s3Client;

    @Qualifier("s3Presigner")
    @Autowired
    S3Presigner s3Presigner;
    
    @Override
    public boolean ifExistsBucket(String bucketName) {
        // 嘗試傳送 HEAD 請求來檢查儲存桶是否存在
        try {
            HeadBucketResponse headBucketResponse = s3Client.headBucket(HeadBucketRequest.builder().bucket(bucketName).build());
        } catch (S3Exception e) {
            // 如果捕獲到 S3 異常且狀態碼為 404,則說明儲存桶不存在
            if (e.statusCode() == 404) {
                return false;
            } else {
                // 列印異常堆疊跟蹤
                e.printStackTrace();
            }
        }
        // 如果沒有丟擲異常或狀態碼不是 404,則說明儲存桶存在
        return true;
    }

    @Override
    public boolean createBucket(String bucketName) throws RuntimeException {
        // 檢查儲存桶是否已經存在
        if (ifExistsBucket(bucketName)) {
            // 如果儲存桶已存在,則丟擲執行時異常
            throw new RuntimeException("桶已存在");
        }
        // 建立新的儲存桶
        S3Response bucket = s3Client.createBucket(CreateBucketRequest.builder().bucket(bucketName).build());
        // 檢查儲存桶是否建立成功
        return ifExistsBucket(bucketName);
    }

    @Override
    public boolean removeBucket(String bucketName) {
        // 如果儲存桶不存在,則直接返回 true
        if (!ifExistsBucket(bucketName)) {
            return true;
        }
        // 刪除儲存桶
        s3Client.deleteBucket(DeleteBucketRequest.builder().bucket(bucketName).build());
        // 檢查儲存桶是否已被刪除
        return !ifExistsBucket(bucketName);
    }

    @Override
    public List<Bucket> alreadyExistBuckets() {
        // 列出所有存在的儲存桶
        List<Bucket> buckets = s3Client.listBuckets().buckets();
        return buckets;
    }

    @Override
    public boolean fileifexist(String bucketName, String filename, boolean recursive) {
        // 初始化一個布林值用於標記檔案是否存在
        boolean flag = false;
        // 構建第一次的 ListObjectsV2 請求
        ListObjectsV2Request request = ListObjectsV2Request.builder().bucket(bucketName).build();
        ListObjectsV2Response response;
        // 迴圈處理直到所有分頁的資料都被檢索
        do {
            // 傳送請求並獲取響應
            response = s3Client.listObjectsV2(request);
            // 遍歷響應中的內容
            for (S3Object content : response.contents()) {
                // 如果找到匹配的檔名,則設定標誌位為真
                if (content.key().equals(filename)) {
                    flag = true;
                    break;
                }
            }
            // 構建下一次請求,如果響應被截斷,則繼續獲取下一頁的資料
            request = ListObjectsV2Request.builder()
                    .bucket(bucketName)
                    .continuationToken(response.nextContinuationToken())
                    .build();
        } while (response.isTruncated());
        // 返回檔案是否存在
        return flag;
    }

    @Override
    public List<S3Object> listObjects(String bucketName, String predir, boolean recursive) {
        // 構建 ListObjects 請求以列出具有指定字首的檔案
        List<S3Object> contents = s3Client.listObjects(ListObjectsRequest.builder()
                .bucket(bucketName)
                .prefix(predir)
                .maxKeys(1000)
                .build()).contents();
        return contents;
    }

    @Override
    public List<S3Object> listObjects(String bucketName, String predir) {
        // 構建 ListObjects 請求以列出具有指定字首的檔案
        List<S3Object> contents = s3Client.listObjects(ListObjectsRequest.builder()
                .bucket(bucketName)
                .prefix(predir)
                .maxKeys(1000)
                .build()).contents();
        return contents;
    }

    @Override
    public boolean copyObject(String pastBucket, String pastObject, String newBucket, String newObject) {
        // 嘗試複製物件
        try {
            CopyObjectResponse copyObjectResponse = s3Client.copyObject(CopyObjectRequest.builder()
                    .sourceBucket(pastBucket)
                    .sourceKey(pastObject)
                    .destinationBucket(newBucket)
                    .destinationKey(newObject)
                    .build());
        } catch (Exception e) {
            // 如果出現異常則列印異常堆疊跟蹤並返回 false
            e.printStackTrace();
            return false;
        }
        // 如果複製成功則返回 true
        return true;
    }

    @Override
    public boolean downObject(String bucketName, String objectName, String targetPath) {
        // 嘗試下載物件到指定路徑
        try {
            s3Client.getObject(GetObjectRequest.builder()
                    .bucket(bucketName)
                    .key(objectName)
                    .build(), new File(targetPath).toPath());
        } catch (Exception e) {
            // 如果出現異常則列印異常堆疊跟蹤並返回 false
            e.printStackTrace();
            return false;
        }
        // 如果下載成功則返回 true
        return true;
    }

    @Override
    public String presignedURLofObject(String bucketName, String object, int expire) {
        URL url = null;
        // 嘗試生成預簽名 URL
        try {
            url = s3Presigner.presignGetObject(GetObjectPresignRequest.builder()
                    .signatureDuration(Duration.ofMinutes(expire))
                    .getObjectRequest(GetObjectRequest.builder()
                            .bucket(bucketName)
                            .key(object)
                            .build())
                    .build()).url();
        } catch (Exception e) {
            // 如果出現異常則列印異常堆疊跟蹤並返回 null
            e.printStackTrace();
            return null;
        } finally {
            // 關閉預簽名客戶端
            s3Presigner.close();
        }
        // 返回預簽名 URL 的字串表示形式
        return url.toString();
    }

    @Override
    public String presignedURLofObject(String bucketName, String object, int expire, Map<String, String> map) {
        URL url = null;
        // 嘗試生成預簽名 URL
        try {
            url = s3Presigner.presignGetObject(GetObjectPresignRequest.builder()
                    .signatureDuration(Duration.ofMinutes(expire))
                    .getObjectRequest(GetObjectRequest.builder()
                            .bucket(bucketName)
                            .key(object)
                            .build())
                    .build()).url();
        } catch (Exception e) {
            // 如果出現異常則列印異常堆疊跟蹤並返回 null
            e.printStackTrace();
            return null;
        } finally {
            // 關閉預簽名客戶端
            s3Presigner.close();
        }
        // 返回預簽名 URL 的字串表示形式
        return url.toString();
    }

    @Override
    public boolean deleteObject(String bucketName, String object) {
        // 嘗試刪除物件
        try {
            s3Client.deleteObject(DeleteObjectRequest.builder()
                    .bucket(bucketName)
                    .key(object)
                    .build());
        } catch (Exception e) {
            // 如果出現異常則列印異常堆疊跟蹤並返回 false
            e.printStackTrace();
            return false;
        }
        // 如果刪除成功則返回 true
        return true;
    }
    
    @Override
    public boolean uploadObject(String bucketName, String targetObject, String sourcePath) {
        // 嘗試上傳本地檔案到指定的儲存桶和物件鍵
        try {
            s3Client.putObject(PutObjectRequest.builder()
                    .bucket(bucketName)
                    .key(targetObject)
                    .build(), RequestBody.fromFile(new File(sourcePath)));
        } catch (Exception e) {
            // 如果出現異常,則列印異常堆疊跟蹤並返回 false
            e.printStackTrace();
            return false;
        }
        // 如果上傳成功則返回 true
        return true;
    }

    @Override
    public boolean putObject(String bucketName, String object, InputStream inputStream, long size) {
        // 嘗試從輸入流上傳資料到指定的儲存桶和物件鍵
        try {
            s3Client.putObject(PutObjectRequest.builder()
                    .bucket(bucketName)
                    .key(object)
                    .build(), RequestBody.fromInputStream(inputStream, size));
        } catch (Exception e) {
            // 如果出現異常,則列印異常堆疊跟蹤並返回 false
            e.printStackTrace();
            return false;
        }
        // 如果上傳成功則返回 true
        return true;
    }

    @Override
    public boolean putObject(String bucketName, String object, InputStream inputStream, long size, Map<String, String> tags) {
        // 嘗試從輸入流上傳資料到指定的儲存桶和物件鍵,並設定標籤
        try {
            // 將標籤對映轉換為標籤集合
            Collection<Tag> tagList = tags.entrySet().stream().map(entry ->
                    Tag.builder().key(entry.getKey()).value(entry.getValue()).build())
                    .collect(Collectors.toList());
            // 上傳物件並設定標籤
            s3Client.putObject(PutObjectRequest.builder()
                    .bucket(bucketName)
                    .key(object)
                    .tagging(Tagging.builder()
                            .tagSet(tagList)
                            .build())
                    .build(), RequestBody.fromInputStream(inputStream, size));
        } catch (Exception e) {
            // 如果出現異常,則列印異常堆疊跟蹤並返回 false
            e.printStackTrace();
            return false;
        }
        // 如果上傳成功則返回 true
        return true;
    }

    @Override
    public InputStream getObject(String bucketName, String object) {
        InputStream objectStream = null;
        // 嘗試獲取指定儲存桶和物件鍵的輸入流
        try {
            objectStream = s3Client.getObject(GetObjectRequest.builder()
                    .bucket(bucketName)
                    .key(object)
                    .build());
        } catch (Exception e) {
            // 如果出現異常,則列印異常堆疊跟蹤並返回 null
            e.printStackTrace();
            return null;
        }
        // 返回物件的輸入流
        return objectStream;
    }

    @Override
    public ResponseBytes<GetObjectResponse> getObjectAsBytes(String bucketName, String object) {
        ResponseBytes<GetObjectResponse> objectAsBytes = null;
        // 嘗試獲取指定儲存桶和物件鍵的內容作為位元組
        try {
            objectAsBytes = s3Client.getObjectAsBytes(GetObjectRequest.builder()
                    .bucket(bucketName)
                    .key(object)
                    .build());
        } catch (Exception e) {
            // 如果出現異常,則列印異常堆疊跟蹤並返回 null
            e.printStackTrace();
            return null;
        }
        // 返回物件的內容作為位元組
        return objectAsBytes;
    }

    @Override
    public Map<String, String> getTags(String bucketName, String object) {
        Map<String, String> tags = null;
        // 嘗試獲取指定儲存桶和物件鍵的標籤
        try {
            List<Tag> tagList = s3Client.getObjectTagging(GetObjectTaggingRequest.builder()
                    .bucket(bucketName)
                    .key(object)
                    .build()).tagSet();
            // 將標籤集合轉換為標籤對映
            tags = tagList.stream().collect(Collectors.toMap(Tag::key, Tag::value));
        } catch (Exception e) {
            // 如果出現異常,則列印異常堆疊跟蹤並返回 null
            e.printStackTrace();
            return null;
        }
        // 返回標籤對映
        return tags;
    }

    @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);
            }
        } catch (Exception e) {
            // 如果出現異常,則列印異常堆疊跟蹤並提示沒有舊標籤
            e.printStackTrace();
            System.out.println("原儲存桶無老舊標籤");
        }
        // 新增新標籤
        if (addTags != null && addTags.size() > 0) {
            newTags.putAll(addTags);
        }
        // 將新標籤集合轉換為標籤列表
        Collection<Tag> tagList = newTags.entrySet().stream().map(entry ->
                Tag.builder().key(entry.getKey()).value(entry.getValue()).build())
                .collect(Collectors.toList());
        // 設定新標籤
        try {
            s3Client.putObjectTagging(PutObjectTaggingRequest.builder()
                    .bucket(bucketName)
                    .key(object)
                    .tagging(Tagging.builder()
                            .tagSet(tagList).build())
                    .build());
        } catch (Exception e) {
            // 如果出現異常,則列印異常堆疊跟蹤並返回 false
            e.printStackTrace();
            return false;
        }
        // 如果設定成功則返回 true
        return true;
    }

    @Override
    public HeadObjectResponse statObject(String bucketName, String object) {
        HeadObjectResponse headObjectResponse = null;
        // 嘗試獲取指定儲存桶和物件鍵的後設資料
        try {
            headObjectResponse = s3Client.headObject(HeadObjectRequest.builder()
                    .bucket(bucketName)
                    .key(object)
                    .build());
        } catch (Exception e) {
            // 如果出現異常,則列印異常堆疊跟蹤並返回 null
            e.printStackTrace();
            return null;
        }
        // 返回物件的後設資料
        return headObjectResponse;
    }

    @Override
    public boolean ifExistObject(String bucketName, String objectName) {
        // 檢查指定儲存桶和物件鍵的物件是否存在
        return listObjects(bucketName, objectName, true).size() >= 1;
    }

    @Override
    public String getMetaNameFromOther(String objectName) {
        String metaObject = "";
        // 獲取後設資料儲存桶中特定字首的物件列表
        List<S3Object> s3Objects = listObjects(BucketName.METADATA, FileUtil.getPreMeta(objectName), true);
        if (s3Objects.size() == 1) {
            try {
                // 獲取第一個物件的鍵並獲取其標籤
                metaObject = s3Objects.get(0).key();
                Map<String, String> tags = getTags(BucketName.METADATA, metaObject);
                // 編碼檔名標籤
                String fileName = tags.get(ObjectTags.FILENAME);
                return URLEncoder.encode(fileName, "UTF-8");
            } catch (UnsupportedEncodingException e) {
                // 如果出現異常,則列印異常堆疊跟蹤
                e.printStackTrace();
            }
        }
        // 如果未找到物件或編碼失敗,則返回原始檔名
        return FileUtil.getFileName(metaObject);
    }

    @Override
    public boolean changeTag(String object, String tag) {
        // 嘗試更改指定物件的標籤
        try {
            s3Client.putObjectTagging(PutObjectTaggingRequest.builder()
                    .bucket(BucketName.METADATA)
                    .key(object)
                    .tagging(Tagging.builder()
                            .tagSet(Tag.builder()
                                    .key(ObjectTags.FILENAME)
                                    .value(tag)
                                    .build())
                            .build())
                    .build());
        } catch (Exception e) {
            // 如果出現異常,則列印異常堆疊跟蹤並返回 false
            e.printStackTrace();
            return false;
        }
        // 如果更改成功則返回 true
        return true;
    }

    @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 {
            // 應用儲存桶策略
            s3Client.putBucketPolicy(PutBucketPolicyRequest.builder()
                    .bucket(bucketName)
                    .policy(config).build());
        } catch (Exception e) {
            // 如果出現異常,則列印異常堆疊跟蹤
            e.printStackTrace();
        }
    }
}

相關文章