dramaling-vocab-learning/Google-Cloud-Storage圖片儲存遷移手...

26 KiB
Raw Blame History

Google Cloud Storage 圖片儲存遷移手冊

概述

將 DramaLing 後端的例句圖片儲存從本地檔案系統遷移到 Google Cloud Storage (GCS),利用 Google 的全球 CDN 網路提供更快的圖片載入速度和更高的可靠性。

目前系統分析

現有架構優勢

  • 使用 IImageStorageService 接口抽象化
  • 依賴注入已完整設定
  • 支援條件式服務切換
  • 完整的錯誤處理和日誌

當前實現

  • 服務: LocalImageStorageService
  • 儲存位置: wwwroot/images/examples
  • URL 模式: https://localhost:5008/images/examples/{fileName}

Phase 1: Google Cloud 環境準備

1.1 建立 Google Cloud 專案

  1. 前往 Google Cloud Console

    訪問: https://console.cloud.google.com/
    登入你的 Google 帳戶
    
  2. 建立新專案

    點擊頂部專案選擇器 → 新增專案
    
    專案名稱: dramaling-storage (或你偏好的名稱)
    組織: 選擇適當的組織 (可選)
    專案 ID: 記錄此 ID後續會用到
    

1.2 啟用 Cloud Storage API

Google Cloud Console → API 和服務 → 程式庫
搜尋: "Cloud Storage API"
點擊 → 啟用

1.3 建立 Service Account

  1. 建立服務帳戶

    Google Cloud Console → IAM 和管理 → 服務帳戶 → 建立服務帳戶
    
    服務帳戶名稱: dramaling-storage-service
    說明: DramaLing application storage service account
    
  2. 設定權限

    選擇角色: Storage Object Admin (允許完整的物件管理)
    或更細緻的權限:
    - Storage Object Creator (建立物件)
    - Storage Object Viewer (檢視物件)
    - Storage Object Admin (完整管理)
    
  3. 建立和下載金鑰檔案

    服務帳戶 → 金鑰 → 新增金鑰 → JSON
    
    下載 JSON 檔案並妥善保存
    檔案名建議: dramaling-storage-service-account.json
    

1.4 建立 Storage Bucket

  1. 建立 Bucket

    Google Cloud Console → Cloud Storage → 瀏覽器 → 建立值區
    
    值區名稱: dramaling-images (需全球唯一)
    位置類型: Region
    位置: asia-east1 (台灣) 或 asia-southeast1 (新加坡)
    儲存類別: Standard
    存取控制: 統一 (Uniform)
    
  2. 設定公開存取權限

    選擇建立的 bucket → 權限 → 新增主體
    
    新主體: allUsers
    角色: Storage Object Viewer
    
    這會讓圖片可以透過 URL 公開存取
    

1.5 設定 CORS

在 Google Cloud Console 中設定 CORS:

# 建立 cors.json 檔案
[
  {
    "origin": ["http://localhost:3000", "http://localhost:5000", "https://你的域名.com"],
    "method": ["GET", "HEAD"],
    "responseHeader": ["Content-Type"],
    "maxAgeSeconds": 86400
  }
]

# 使用 gsutil 設定 (需要安裝 Google Cloud SDK)
gsutil cors set cors.json gs://dramaling-images

Phase 2: .NET 專案設定

2.1 安裝 NuGet 套件

backend/DramaLing.Api/DramaLing.Api.csproj 中添加:

<PackageReference Include="Google.Cloud.Storage.V1" Version="4.7.0" />
<PackageReference Include="Google.Apis.Auth" Version="1.68.0" />

或使用命令列:

cd backend/DramaLing.Api
dotnet add package Google.Cloud.Storage.V1
dotnet add package Google.Apis.Auth

2.2 建立配置模型

建立 backend/DramaLing.Api/Models/Configuration/GoogleCloudStorageOptions.cs:

using Microsoft.Extensions.Options;

namespace DramaLing.Api.Models.Configuration;

public class GoogleCloudStorageOptions
{
    public const string SectionName = "GoogleCloudStorage";

    /// <summary>
    /// Google Cloud 專案 ID
    /// </summary>
    public string ProjectId { get; set; } = string.Empty;

    /// <summary>
    /// Storage Bucket 名稱
    /// </summary>
    public string BucketName { get; set; } = string.Empty;

    /// <summary>
    /// Service Account JSON 金鑰檔案路徑
    /// </summary>
    public string CredentialsPath { get; set; } = string.Empty;

    /// <summary>
    /// Service Account JSON 金鑰內容 (用於環境變數)
    /// </summary>
    public string CredentialsJson { get; set; } = string.Empty;

    /// <summary>
    /// 自訂域名 (用於 CDN)
    /// </summary>
    public string CustomDomain { get; set; } = string.Empty;

    /// <summary>
    /// 是否使用自訂域名
    /// </summary>
    public bool UseCustomDomain { get; set; } = false;

    /// <summary>
    /// 圖片路徑前綴
    /// </summary>
    public string PathPrefix { get; set; } = "examples";
}

public class GoogleCloudStorageOptionsValidator : IValidateOptions<GoogleCloudStorageOptions>
{
    public ValidateOptionsResult Validate(string name, GoogleCloudStorageOptions options)
    {
        var failures = new List<string>();

        if (string.IsNullOrEmpty(options.ProjectId))
            failures.Add("GoogleCloudStorage:ProjectId is required");

        if (string.IsNullOrEmpty(options.BucketName))
            failures.Add("GoogleCloudStorage:BucketName is required");

        if (string.IsNullOrEmpty(options.CredentialsPath) && string.IsNullOrEmpty(options.CredentialsJson))
            failures.Add("Either GoogleCloudStorage:CredentialsPath or GoogleCloudStorage:CredentialsJson must be provided");

        return failures.Count > 0
            ? ValidateOptionsResult.Fail(failures)
            : ValidateOptionsResult.Success;
    }
}

Phase 3: 實現 Google Cloud Storage 服務

3.1 建立 GoogleCloudImageStorageService

建立 backend/DramaLing.Api/Services/Media/Storage/GoogleCloudImageStorageService.cs:

using Google.Cloud.Storage.V1;
using Google.Apis.Auth.OAuth2;
using DramaLing.Api.Models.Configuration;
using DramaLing.Api.Services.Storage;
using Microsoft.Extensions.Options;
using System.Text;

namespace DramaLing.Api.Services.Media.Storage;

public class GoogleCloudImageStorageService : IImageStorageService
{
    private readonly StorageClient _storageClient;
    private readonly GoogleCloudStorageOptions _options;
    private readonly ILogger<GoogleCloudImageStorageService> _logger;

    public GoogleCloudImageStorageService(
        IOptions<GoogleCloudStorageOptions> options,
        ILogger<GoogleCloudImageStorageService> logger)
    {
        _options = options.Value ?? throw new ArgumentNullException(nameof(options));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));

        // 初始化 Storage Client
        _storageClient = CreateStorageClient();

        _logger.LogInformation("GoogleCloudImageStorageService initialized with bucket: {BucketName}",
            _options.BucketName);
    }

    private StorageClient CreateStorageClient()
    {
        GoogleCredential credential;

        // 優先使用 JSON 字串 (適合 Render 等雲端部署)
        if (!string.IsNullOrEmpty(_options.CredentialsJson))
        {
            credential = GoogleCredential.FromJson(_options.CredentialsJson);
        }
        // 次要使用檔案路徑 (適合本地開發)
        else if (!string.IsNullOrEmpty(_options.CredentialsPath) && File.Exists(_options.CredentialsPath))
        {
            credential = GoogleCredential.FromFile(_options.CredentialsPath);
        }
        // 最後嘗試使用預設認證 (適合 Google Cloud 環境)
        else
        {
            credential = GoogleCredential.GetApplicationDefault();
        }

        return StorageClient.Create(credential);
    }

    public async Task<string> SaveImageAsync(Stream imageStream, string fileName)
    {
        try
        {
            var objectName = $"{_options.PathPrefix}/{fileName}";

            var obj = new Google.Cloud.Storage.V1.Object
            {
                Bucket = _options.BucketName,
                Name = objectName,
                ContentType = GetContentType(fileName)
            };

            // 上傳檔案
            var uploadedObject = await _storageClient.UploadObjectAsync(obj, imageStream);

            _logger.LogInformation("Image uploaded successfully to GCS: {ObjectName}", objectName);

            return objectName; // 回傳 GCS 中的物件名稱
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to save image to GCS: {FileName}", fileName);
            throw;
        }
    }

    public Task<string> GetImageUrlAsync(string imagePath)
    {
        // 如果設定了自訂域名 (CDN)
        if (_options.UseCustomDomain && !string.IsNullOrEmpty(_options.CustomDomain))
        {
            var cdnUrl = $"https://{_options.CustomDomain.TrimEnd('/')}/{imagePath.TrimStart('/')}";
            return Task.FromResult(cdnUrl);
        }

        // 使用標準 Google Cloud Storage URL
        var gcsUrl = $"https://storage.googleapis.com/{_options.BucketName}/{imagePath.TrimStart('/')}";
        return Task.FromResult(gcsUrl);
    }

    public async Task<bool> DeleteImageAsync(string imagePath)
    {
        try
        {
            await _storageClient.DeleteObjectAsync(_options.BucketName, imagePath);

            _logger.LogInformation("Image deleted from GCS: {ObjectName}", imagePath);
            return true;
        }
        catch (GoogleApiException ex) when (ex.HttpStatusCode == System.Net.HttpStatusCode.NotFound)
        {
            _logger.LogWarning("Attempted to delete non-existent image: {ObjectName}", imagePath);
            return false;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to delete image from GCS: {ObjectName}", imagePath);
            return false;
        }
    }

    public async Task<bool> ImageExistsAsync(string imagePath)
    {
        try
        {
            var obj = await _storageClient.GetObjectAsync(_options.BucketName, imagePath);
            return obj != null;
        }
        catch (GoogleApiException ex) when (ex.HttpStatusCode == System.Net.HttpStatusCode.NotFound)
        {
            return false;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to check image existence in GCS: {ObjectName}", imagePath);
            return false;
        }
    }

    public async Task<StorageInfo> GetStorageInfoAsync()
    {
        try
        {
            var request = new ListObjectsOptions
            {
                Prefix = _options.PathPrefix,
                PageSize = 1000 // 限制查詢數量
            };

            var objects = _storageClient.ListObjectsAsync(_options.BucketName, request);

            long totalSize = 0;
            int fileCount = 0;

            await foreach (var obj in objects)
            {
                totalSize += (long)(obj.Size ?? 0);
                fileCount++;
            }

            return new StorageInfo
            {
                Provider = "Google Cloud Storage",
                TotalSizeBytes = totalSize,
                FileCount = fileCount,
                Status = "Available"
            };
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to get GCS storage info");
            return new StorageInfo
            {
                Provider = "Google Cloud Storage",
                Status = $"Error: {ex.Message}"
            };
        }
    }

    private static string GetContentType(string fileName)
    {
        var extension = Path.GetExtension(fileName).ToLowerInvariant();
        return extension switch
        {
            ".jpg" or ".jpeg" => "image/jpeg",
            ".png" => "image/png",
            ".gif" => "image/gif",
            ".webp" => "image/webp",
            ".svg" => "image/svg+xml",
            _ => "application/octet-stream"
        };
    }
}

Phase 4: 應用配置更新

4.1 更新 ServiceCollectionExtensions.cs

修改 AddBusinessServices 方法:

/// <summary>
/// 配置業務服務
/// </summary>
public static IServiceCollection AddBusinessServices(this IServiceCollection services, IConfiguration configuration)
{
    services.AddScoped<IAuthService, AuthService>();

    // 媒體服務
    services.AddScoped<IImageProcessingService, ImageProcessingService>();

    // 圖片儲存服務 - 根據設定選擇實現
    var storageProvider = configuration.GetValue<string>("ImageStorage:Provider", "Local");

    switch (storageProvider.ToLowerInvariant())
    {
        case "googlecloud" or "gcs":
            ConfigureGoogleCloudStorage(services, configuration);
            break;

        case "local":
        default:
            services.AddScoped<IImageStorageService, LocalImageStorageService>();
            break;
    }

    // 其他服務保持不變...
    services.AddHttpClient<IReplicateService, ReplicateService>();
    services.AddScoped<IOptionsVocabularyService, OptionsVocabularyService>();
    services.AddScoped<IAnalysisService, AnalysisService>();
    services.AddScoped<DramaLing.Api.Contracts.Services.Review.IReviewService,
        DramaLing.Api.Services.Review.ReviewService>();

    return services;
}

private static void ConfigureGoogleCloudStorage(IServiceCollection services, IConfiguration configuration)
{
    // 配置 Google Cloud Storage 選項
    services.Configure<GoogleCloudStorageOptions>(configuration.GetSection(GoogleCloudStorageOptions.SectionName));
    services.AddSingleton<IValidateOptions<GoogleCloudStorageOptions>, GoogleCloudStorageOptionsValidator>();

    // 註冊 Google Cloud Storage 服務
    services.AddScoped<IImageStorageService, GoogleCloudImageStorageService>();
}

4.2 更新 appsettings.json

{
  "ImageStorage": {
    "Provider": "Local",
    "Local": {
      "BasePath": "wwwroot/images/examples",
      "BaseUrl": "https://localhost:5008/images/examples"
    }
  },
  "GoogleCloudStorage": {
    "ProjectId": "",
    "BucketName": "dramaling-images",
    "CredentialsPath": "",
    "CredentialsJson": "",
    "CustomDomain": "",
    "UseCustomDomain": false,
    "PathPrefix": "examples"
  }
}

4.3 開發環境設定 (appsettings.Development.json)

{
  "ImageStorage": {
    "Provider": "GoogleCloud"
  },
  "GoogleCloudStorage": {
    "ProjectId": "your-project-id",
    "BucketName": "dramaling-images",
    "CredentialsPath": "path/to/your/service-account.json",
    "PathPrefix": "examples"
  }
}

4.4 生產環境設定 (appsettings.Production.json)

{
  "ImageStorage": {
    "Provider": "GoogleCloud"
  },
  "GoogleCloudStorage": {
    "ProjectId": "your-production-project-id",
    "BucketName": "dramaling-images-prod",
    "CustomDomain": "images.dramaling.com",
    "UseCustomDomain": true,
    "PathPrefix": "examples"
  }
}

Phase 5: 認證設定

5.1 本地開發環境

方法 1: Service Account JSON 檔案

  1. 儲存金鑰檔案

    將下載的 JSON 檔案放到安全位置
    建議: backend/secrets/dramaling-storage-service-account.json
    
    ⚠️ 務必將 secrets/ 目錄加入 .gitignore
    
  2. 設定檔案路徑

    {
      "GoogleCloudStorage": {
        "CredentialsPath": "secrets/dramaling-storage-service-account.json"
      }
    }
    

方法 2: 環境變數 (推薦)

export GOOGLE_APPLICATION_CREDENTIALS="/path/to/service-account.json"

方法 3: User Secrets (最安全)

cd backend/DramaLing.Api
dotnet user-secrets init
dotnet user-secrets set "GoogleCloudStorage:ProjectId" "your-project-id"
dotnet user-secrets set "GoogleCloudStorage:CredentialsJson" "$(cat path/to/service-account.json)"

5.2 生產環境 (Render)

在 Render Dashboard 設定環境變數:

GOOGLE_CLOUD_PROJECT_ID=your-project-id
GOOGLE_CLOUD_STORAGE_BUCKET=dramaling-images-prod
GOOGLE_CLOUD_CREDENTIALS_JSON=[整個JSON檔案內容]

然後在 Program.cs 中添加環境變數載入:

// 在建立 builder 後添加
builder.Configuration.AddInMemoryCollection(new Dictionary<string, string>
{
    ["GoogleCloudStorage:ProjectId"] = Environment.GetEnvironmentVariable("GOOGLE_CLOUD_PROJECT_ID") ?? "",
    ["GoogleCloudStorage:BucketName"] = Environment.GetEnvironmentVariable("GOOGLE_CLOUD_STORAGE_BUCKET") ?? "",
    ["GoogleCloudStorage:CredentialsJson"] = Environment.GetEnvironmentVariable("GOOGLE_CLOUD_CREDENTIALS_JSON") ?? ""
}!);

Phase 6: 測試和驗證

6.1 本地測試步驟

  1. 設定開發環境

    # 設定環境變數
    export GOOGLE_APPLICATION_CREDENTIALS="/path/to/service-account.json"
    
    # 或使用 user secrets
    cd backend/DramaLing.Api
    dotnet user-secrets set "GoogleCloudStorage:ProjectId" "your-project-id"
    dotnet user-secrets set "GoogleCloudStorage:BucketName" "dramaling-images"
    
  2. 修改開發設定

    {
      "ImageStorage": {
        "Provider": "GoogleCloud"
      }
    }
    
  3. 測試圖片功能

    • 啟動後端 API
    • 前往 AI 生成頁面
    • 輸入句子並生成例句圖
    • 檢查 Google Cloud Console 中的 bucket 是否有新檔案
    • 檢查前端是否正確顯示圖片

6.2 功能驗證清單

  • 圖片上傳: 新圖片出現在 GCS bucket 中
  • 圖片顯示: 前端可正確載入並顯示 GCS 圖片
  • URL 生成: 圖片 URL 格式正確
  • 圖片刪除: 刪除功能正常運作
  • 錯誤處理: 網路錯誤時有適當的錯誤訊息
  • 日誌記錄: 操作日誌正確記錄
  • 效能: 圖片載入速度合理

6.3 常見問題排除

問題 1: The Application Default Credentials are not available

解決方法:
1. 檢查環境變數 GOOGLE_APPLICATION_CREDENTIALS 是否設定
2. 檢查 JSON 檔案路徑是否正確
3. 檢查 JSON 檔案格式是否正確

問題 2: Access denied 錯誤

解決方法:
1. 檢查 Service Account 是否有 Storage Object Admin 權限
2. 檢查 bucket 名稱是否正確
3. 檢查專案 ID 是否正確

問題 3: CORS 錯誤

解決方法:
1. 設定 bucket 的 CORS 政策
2. 檢查前端域名是否在允許清單中

Phase 7: 生產環境部署

7.1 Render 環境設定

  1. 設定環境變數

    在 Render Dashboard → Your Service → Environment:
    
    GOOGLE_CLOUD_PROJECT_ID = your-production-project-id
    GOOGLE_CLOUD_STORAGE_BUCKET = dramaling-images-prod
    GOOGLE_CLOUD_CREDENTIALS_JSON = [完整的JSON內容單行格式]
    
  2. JSON 內容格式化

    # 將多行 JSON 轉為單行 (用於環境變數)
    cat service-account.json | jq -c .
    

7.2 CDN 設定 (可選)

如果需要 CDN 加速:

  1. 設定 Load Balancer

    Google Cloud Console → 網路服務 → Cloud CDN
    建立 HTTP(S) Load Balancer
    後端指向你的 Storage bucket
    
  2. 自訂域名設定

    {
      "GoogleCloudStorage": {
        "CustomDomain": "images.dramaling.com",
        "UseCustomDomain": true
      }
    }
    

7.3 部署流程

  1. 更新生產設定

    {
      "ImageStorage": {
        "Provider": "GoogleCloud"
      }
    }
    
  2. 部署到 Render

    • 推送代碼到 Git
    • Render 自動部署
    • 檢查部署日誌
  3. 驗證功能

    • 測試圖片生成
    • 檢查 GCS bucket
    • 測試圖片載入速度

成本分析

Google Cloud Storage 定價 (2024年價格)

  • Storage: $0.020 per GB/month (Standard class, Asia region)
  • Operations:
    • Class A (write): $0.05 per 10,000 operations
    • Class B (read): $0.004 per 10,000 operations
  • Network:
    • Asia to Asia: $0.05 per GB
    • Global CDN: $0.08-0.20 per GB (depending on region)

預期成本估算 (1000 張圖片範例)

假設每張圖片 500KB:

  • 儲存成本: 0.5GB × $0.02 = $0.01/月
  • 上傳操作: 1000 × $0.05/10,000 = $0.005
  • 瀏覽操作: 10,000 次 × $0.004/10,000 = $0.004

每月總成本約: $0.02-0.05 USD (非常便宜)

與其他方案比較

方案 月成本 效能 可靠性 管理複雜度
本地儲存 $0
Google Cloud $0.02-0.05
AWS S3 $0.03-0.08
Cloudflare R2 $0.01-0.03

遷移時程表

建議實施順序

  1. 準備階段 (1-2 小時):

    • 建立 Google Cloud 專案
    • 設定 Service Account 和 Bucket
    • 下載認證檔案
  2. 開發階段 (2-3 小時):

    • 安裝 NuGet 套件
    • 實現 GoogleCloudImageStorageService
    • 建立配置模型
  3. 測試階段 (1-2 小時):

    • 本地環境測試
    • 功能驗證
    • 效能測試
  4. 部署階段 (1 小時):

    • 設定生產環境變數
    • 部署到 Render
    • 最終驗證

總計時間: 5-8 小時

安全性考量

最佳實務

  1. 認證管理:

    • 使用環境變數存放敏感資訊
    • 本地使用 user secrets
    • 生產使用 Render 環境變數
    • 絕不將金鑰提交到 Git
  2. 權限管理:

    • Service Account 最小權限原則
    • Bucket 層級的權限控制
    • 定期輪換 Service Account 金鑰
  3. 網路安全:

    • 使用 HTTPS 傳輸
    • 設定 CORS 限制
    • 監控異常存取

回滾策略

如果需要回到本地儲存:

  1. 快速回滾

    {
      "ImageStorage": {
        "Provider": "Local"
      }
    }
    
  2. 重新部署

    • 系統自動切換回 LocalImageStorageService
  3. 資料遷移 (可選)

    • 從 GCS 下載圖片回本地 (如果需要)

監控和維護

日誌監控

  • 設定 Google Cloud Logging 監控
  • 關注 Storage API 錯誤率
  • 監控上傳/下載效能

成本監控

  • 設定 Google Cloud 計費警告
  • 定期檢查 Storage 使用量
  • 監控 API 調用頻率

維護建議

  • 定期檢查圖片存取權限
  • 清理未使用的圖片 (可選)
  • 備份重要圖片 (可選)

技術支援

文檔資源

故障排除指令

# 檢查 GCS 連線
gsutil ls gs://your-bucket-name

# 測試認證
gcloud auth application-default print-access-token

# 檢查 bucket 權限
gsutil iam get gs://your-bucket-name

實施檢查清單

準備階段

  • 建立 Google Cloud 專案
  • 啟用 Cloud Storage API
  • 建立 Service Account
  • 下載 JSON 認證檔案
  • 建立 Storage Bucket
  • 設定 Bucket 權限和 CORS

開發階段

  • 安裝 Google.Cloud.Storage.V1 NuGet 套件 已完成 2024-10-08
  • 建立 GoogleCloudStorageOptions 配置模型 已完成 2024-10-08
  • 實現 GoogleCloudImageStorageService 已完成 2024-10-08
  • 更新 ServiceCollectionExtensions 已完成 2024-10-08
  • 更新 appsettings.json 配置 已完成 2024-10-08
  • 編譯測試通過 已完成 2024-10-08
  • 設定本地認證

測試階段

  • 本地環境測試圖片上傳
  • 驗證圖片 URL 可存取
  • 測試圖片刪除功能
  • 檢查錯誤處理
  • 驗證日誌記錄

部署階段

  • 設定 Render 環境變數
  • 更新生產配置
  • 部署並驗證功能
  • 設定監控和警告
  • 準備回滾計劃

🚀 實施進度

已完成項目 (2024-10-08)

NuGet 套件安裝

  • 已在 DramaLing.Api.csproj 中添加:
    • Google.Cloud.Storage.V1 v4.7.0
    • Google.Apis.Auth v1.68.0

配置模型建立

  • 已建立 Models/Configuration/GoogleCloudStorageOptions.cs
  • 支援多種認證方式Service Account JSON、檔案路徑、API Key
  • 包含配置驗證器

服務實現完成

  • 已建立 Services/Media/Storage/GoogleCloudImageStorageService.cs
  • 完整實現 IImageStorageService 接口
  • 支援現有的 User Secrets 中的 GoogleStorage:ApiKey
  • 包含錯誤處理和日誌記錄

設計特色

🔄 條件式切換支援

  • 可透過設定檔在本地儲存 ↔ Google Cloud 之間切換
  • 零程式碼修改,完全向後相容
  • 支援開發/測試/生產環境不同配置

🔐 多重認證支援

  • Service Account JSON (推薦)
  • 檔案路徑認證
  • 現有 API Key 支援
  • 環境變數配置

依賴注入設定完成

  • 已在 ServiceCollectionExtensions.cs 中添加條件式切換邏輯
  • 支援通過 ImageStorage:Provider 配置選擇實現
  • 已更新 Program.cs 傳入 configuration 參數

編譯測試通過

  • Build succeeded with 0 Error(s)
  • 所有組件整合成功
  • 準備就緒可進行實際測試

🎯 當前狀態

系統已具備完整的抽換式圖片儲存架構!

立即可用的切換方式

// 保持本地儲存 (當前設定)
"ImageStorage": { "Provider": "Local" }

// 切換到 Google Cloud Storage
"ImageStorage": { "Provider": "GoogleCloud" }

下一步 (準備實際使用)

要啟用 Google Cloud Storage

  1. 建立 Google Cloud 專案和 Bucket
  2. 設定認證 (gcloud auth application-default login)
  3. 修改設定檔 ProviderGoogleCloud

抽換式架構開發完成! 🚀

這份手冊提供了完整的 Google Cloud Storage 遷移指南,讓你能夠安全且有效地從本地儲存遷移到雲端儲存方案。