Content is user-generated and unverified.

.NET Core: MinIO + AWS S3 + GCP Cloud Storage 完整實作

1. NuGet 套件安裝

xml
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
  </PropertyGroup>
  
  <ItemGroup>
    <!-- MinIO -->
    <PackageReference Include="Minio" Version="6.0.3" />
    
    <!-- AWS S3 -->
    <PackageReference Include="AWSSDK.S3" Version="3.7.310" />
    
    <!-- GCP Cloud Storage -->
    <PackageReference Include="Google.Cloud.Storage.V1" Version="4.6.0" />
    
    <!-- 共用套件 -->
    <PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
  </ItemGroup>
</Project>

2. 設定檔 (appsettings.json)

json
{
  "CloudStorage": {
    "Provider": "MinIO", // "MinIO" | "S3" | "GCS"
    "MinIO": {
      "Endpoint": "localhost:9000",
      "AccessKey": "minioadmin",
      "SecretKey": "minioadmin",
      "BucketName": "test-bucket",
      "UseSSL": false
    },
    "AWS": {
      "BucketName": "your-s3-bucket",
      "Region": "ap-northeast-1",
      "AccessKey": "your-access-key",
      "SecretKey": "your-secret-key"
    },
    "GCP": {
      "ProjectId": "your-project-id",
      "BucketName": "your-gcs-bucket",
      "ServiceAccountKeyPath": "path/to/service-account-key.json"
    }
  }
}

3. 統一介面定義

csharp
public interface ICloudStorageService
{
    Task<string> UploadFileAsync(Stream fileStream, string fileName, string contentType = "application/octet-stream");
    Task<Stream> DownloadFileAsync(string fileName);
    Task<byte[]> DownloadFileBytesAsync(string fileName);
    Task<List<string>> ListFilesAsync(string prefix = "");
    Task<bool> DeleteFileAsync(string fileName);
    Task<bool> FileExistsAsync(string fileName);
    Task<string> GetFileUrlAsync(string fileName, TimeSpan? expiry = null);
    Task<long> GetFileSizeAsync(string fileName);
}

public class CloudStorageResult
{
    public bool Success { get; set; }
    public string Message { get; set; }
    public string Url { get; set; }
}

4. MinIO 實作

csharp
using Minio;
using Minio.DataModel.Args;
using Minio.Exceptions;

public class MinIOStorageService : ICloudStorageService
{
    private readonly IMinioClient _minioClient;
    private readonly string _bucketName;

    public MinIOStorageService(IConfiguration configuration)
    {
        var config = configuration.GetSection("CloudStorage:MinIO");
        _bucketName = config["BucketName"];
        
        _minioClient = new MinioClient()
            .WithEndpoint(config["Endpoint"])
            .WithCredentials(config["AccessKey"], config["SecretKey"])
            .WithSSL(bool.Parse(config["UseSSL"] ?? "false"))
            .Build();
            
        // 確保 bucket 存在
        EnsureBucketExistsAsync().Wait();
    }

    private async Task EnsureBucketExistsAsync()
    {
        var bucketExistsArgs = new BucketExistsArgs()
            .WithBucket(_bucketName);
            
        bool bucketExists = await _minioClient.BucketExistsAsync(bucketExistsArgs);
        if (!bucketExists)
        {
            var makeBucketArgs = new MakeBucketArgs()
                .WithBucket(_bucketName);
            await _minioClient.MakeBucketAsync(makeBucketArgs);
        }
    }

    public async Task<string> UploadFileAsync(Stream fileStream, string fileName, string contentType = "application/octet-stream")
    {
        try
        {
            var putObjectArgs = new PutObjectArgs()
                .WithBucket(_bucketName)
                .WithObject(fileName)
                .WithStreamData(fileStream)
                .WithObjectSize(fileStream.Length)
                .WithContentType(contentType);

            await _minioClient.PutObjectAsync(putObjectArgs);
            return await GetFileUrlAsync(fileName);
        }
        catch (MinioException ex)
        {
            throw new Exception($"MinIO 上傳失敗: {ex.Message}", ex);
        }
    }

    public async Task<Stream> DownloadFileAsync(string fileName)
    {
        try
        {
            var memoryStream = new MemoryStream();
            var getObjectArgs = new GetObjectArgs()
                .WithBucket(_bucketName)
                .WithObject(fileName)
                .WithCallbackStream(stream => stream.CopyTo(memoryStream));

            await _minioClient.GetObjectAsync(getObjectArgs);
            memoryStream.Position = 0;
            return memoryStream;
        }
        catch (MinioException ex)
        {
            throw new Exception($"MinIO 下載失敗: {ex.Message}", ex);
        }
    }

    public async Task<byte[]> DownloadFileBytesAsync(string fileName)
    {
        using var stream = await DownloadFileAsync(fileName);
        using var memoryStream = new MemoryStream();
        await stream.CopyToAsync(memoryStream);
        return memoryStream.ToArray();
    }

    public async Task<List<string>> ListFilesAsync(string prefix = "")
    {
        try
        {
            var listObjectsArgs = new ListObjectsArgs()
                .WithBucket(_bucketName)
                .WithPrefix(prefix);

            var objects = new List<string>();
            await foreach (var item in _minioClient.ListObjectsEnumAsync(listObjectsArgs))
            {
                objects.Add(item.Key);
            }
            return objects;
        }
        catch (MinioException ex)
        {
            throw new Exception($"MinIO 列表失敗: {ex.Message}", ex);
        }
    }

    public async Task<bool> DeleteFileAsync(string fileName)
    {
        try
        {
            var removeObjectArgs = new RemoveObjectArgs()
                .WithBucket(_bucketName)
                .WithObject(fileName);

            await _minioClient.RemoveObjectAsync(removeObjectArgs);
            return true;
        }
        catch (MinioException ex)
        {
            throw new Exception($"MinIO 刪除失敗: {ex.Message}", ex);
        }
    }

    public async Task<bool> FileExistsAsync(string fileName)
    {
        try
        {
            var statObjectArgs = new StatObjectArgs()
                .WithBucket(_bucketName)
                .WithObject(fileName);

            await _minioClient.StatObjectAsync(statObjectArgs);
            return true;
        }
        catch (ObjectNotFoundException)
        {
            return false;
        }
    }

    public async Task<string> GetFileUrlAsync(string fileName, TimeSpan? expiry = null)
    {
        try
        {
            var presignedGetObjectArgs = new PresignedGetObjectArgs()
                .WithBucket(_bucketName)
                .WithObject(fileName)
                .WithExpiry((int)(expiry?.TotalSeconds ?? 3600)); // 預設 1 小時

            return await _minioClient.PresignedGetObjectAsync(presignedGetObjectArgs);
        }
        catch (MinioException ex)
        {
            throw new Exception($"MinIO 產生 URL 失敗: {ex.Message}", ex);
        }
    }

    public async Task<long> GetFileSizeAsync(string fileName)
    {
        try
        {
            var statObjectArgs = new StatObjectArgs()
                .WithBucket(_bucketName)
                .WithObject(fileName);

            var objectStat = await _minioClient.StatObjectAsync(statObjectArgs);
            return objectStat.Size;
        }
        catch (MinioException ex)
        {
            throw new Exception($"MinIO 取得檔案大小失敗: {ex.Message}", ex);
        }
    }
}

5. AWS S3 實作

csharp
using Amazon.S3;
using Amazon.S3.Model;
using Amazon;

public class S3StorageService : ICloudStorageService
{
    private readonly IAmazonS3 _s3Client;
    private readonly string _bucketName;

    public S3StorageService(IConfiguration configuration)
    {
        var config = configuration.GetSection("CloudStorage:AWS");
        _bucketName = config["BucketName"];
        
        var accessKey = config["AccessKey"];
        var secretKey = config["SecretKey"];
        var region = RegionEndpoint.GetBySystemName(config["Region"]);

        if (!string.IsNullOrEmpty(accessKey) && !string.IsNullOrEmpty(secretKey))
        {
            _s3Client = new AmazonS3Client(accessKey, secretKey, region);
        }
        else
        {
            // 使用 IAM Role 或 AWS Profile
            _s3Client = new AmazonS3Client(region);
        }
    }

    public async Task<string> UploadFileAsync(Stream fileStream, string fileName, string contentType = "application/octet-stream")
    {
        try
        {
            var request = new PutObjectRequest
            {
                BucketName = _bucketName,
                Key = fileName,
                InputStream = fileStream,
                ContentType = contentType,
                ServerSideEncryptionMethod = ServerSideEncryptionMethod.AES256
            };

            await _s3Client.PutObjectAsync(request);
            return await GetFileUrlAsync(fileName);
        }
        catch (AmazonS3Exception ex)
        {
            throw new Exception($"AWS S3 上傳失敗: {ex.Message}", ex);
        }
    }

    public async Task<Stream> DownloadFileAsync(string fileName)
    {
        try
        {
            var request = new GetObjectRequest
            {
                BucketName = _bucketName,
                Key = fileName
            };

            var response = await _s3Client.GetObjectAsync(request);
            return response.ResponseStream;
        }
        catch (AmazonS3Exception ex)
        {
            throw new Exception($"AWS S3 下載失敗: {ex.Message}", ex);
        }
    }

    public async Task<byte[]> DownloadFileBytesAsync(string fileName)
    {
        using var stream = await DownloadFileAsync(fileName);
        using var memoryStream = new MemoryStream();
        await stream.CopyToAsync(memoryStream);
        return memoryStream.ToArray();
    }

    public async Task<List<string>> ListFilesAsync(string prefix = "")
    {
        try
        {
            var request = new ListObjectsV2Request
            {
                BucketName = _bucketName,
                Prefix = prefix,
                MaxKeys = 1000
            };

            var response = await _s3Client.ListObjectsV2Async(request);
            return response.S3Objects.Select(obj => obj.Key).ToList();
        }
        catch (AmazonS3Exception ex)
        {
            throw new Exception($"AWS S3 列表失敗: {ex.Message}", ex);
        }
    }

    public async Task<bool> DeleteFileAsync(string fileName)
    {
        try
        {
            var request = new DeleteObjectRequest
            {
                BucketName = _bucketName,
                Key = fileName
            };

            await _s3Client.DeleteObjectAsync(request);
            return true;
        }
        catch (AmazonS3Exception ex)
        {
            throw new Exception($"AWS S3 刪除失敗: {ex.Message}", ex);
        }
    }

    public async Task<bool> FileExistsAsync(string fileName)
    {
        try
        {
            var request = new GetObjectMetadataRequest
            {
                BucketName = _bucketName,
                Key = fileName
            };

            await _s3Client.GetObjectMetadataAsync(request);
            return true;
        }
        catch (AmazonS3Exception ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
        {
            return false;
        }
    }

    public async Task<string> GetFileUrlAsync(string fileName, TimeSpan? expiry = null)
    {
        try
        {
            var request = new GetPreSignedUrlRequest
            {
                BucketName = _bucketName,
                Key = fileName,
                Verb = HttpVerb.GET,
                Expires = DateTime.UtcNow.Add(expiry ?? TimeSpan.FromHours(1))
            };

            return await Task.FromResult(_s3Client.GetPreSignedURL(request));
        }
        catch (AmazonS3Exception ex)
        {
            throw new Exception($"AWS S3 產生 URL 失敗: {ex.Message}", ex);
        }
    }

    public async Task<long> GetFileSizeAsync(string fileName)
    {
        try
        {
            var request = new GetObjectMetadataRequest
            {
                BucketName = _bucketName,
                Key = fileName
            };

            var response = await _s3Client.GetObjectMetadataAsync(request);
            return response.ContentLength;
        }
        catch (AmazonS3Exception ex)
        {
            throw new Exception($"AWS S3 取得檔案大小失敗: {ex.Message}", ex);
        }
    }
}

6. GCP Cloud Storage 實作

csharp
using Google.Cloud.Storage.V1;
using Google.Apis.Auth.OAuth2;

public class GcsStorageService : ICloudStorageService
{
    private readonly StorageClient _storageClient;
    private readonly string _bucketName;
    private readonly string _projectId;

    public GcsStorageService(IConfiguration configuration)
    {
        var config = configuration.GetSection("CloudStorage:GCP");
        _bucketName = config["BucketName"];
        _projectId = config["ProjectId"];

        var keyPath = config["ServiceAccountKeyPath"];
        if (!string.IsNullOrEmpty(keyPath) && File.Exists(keyPath))
        {
            var credential = GoogleCredential.FromFile(keyPath);
            _storageClient = StorageClient.Create(credential);
        }
        else
        {
            // 使用環境變數 GOOGLE_APPLICATION_CREDENTIALS
            _storageClient = StorageClient.Create();
        }
    }

    public async Task<string> UploadFileAsync(Stream fileStream, string fileName, string contentType = "application/octet-stream")
    {
        try
        {
            var objectToUpload = new Google.Cloud.Storage.V1.Object
            {
                Bucket = _bucketName,
                Name = fileName,
                ContentType = contentType
            };

            await _storageClient.UploadObjectAsync(objectToUpload, fileStream);
            return await GetFileUrlAsync(fileName);
        }
        catch (GoogleApiException ex)
        {
            throw new Exception($"GCP Cloud Storage 上傳失敗: {ex.Message}", ex);
        }
    }

    public async Task<Stream> DownloadFileAsync(string fileName)
    {
        try
        {
            var memoryStream = new MemoryStream();
            await _storageClient.DownloadObjectAsync(_bucketName, fileName, memoryStream);
            memoryStream.Position = 0;
            return memoryStream;
        }
        catch (GoogleApiException ex)
        {
            throw new Exception($"GCP Cloud Storage 下載失敗: {ex.Message}", ex);
        }
    }

    public async Task<byte[]> DownloadFileBytesAsync(string fileName)
    {
        using var stream = await DownloadFileAsync(fileName);
        using var memoryStream = new MemoryStream();
        await stream.CopyToAsync(memoryStream);
        return memoryStream.ToArray();
    }

    public async Task<List<string>> ListFilesAsync(string prefix = "")
    {
        try
        {
            var objects = _storageClient.ListObjectsAsync(_bucketName, prefix);
            var fileNames = new List<string>();

            await foreach (var obj in objects)
            {
                fileNames.Add(obj.Name);
            }

            return fileNames;
        }
        catch (GoogleApiException ex)
        {
            throw new Exception($"GCP Cloud Storage 列表失敗: {ex.Message}", ex);
        }
    }

    public async Task<bool> DeleteFileAsync(string fileName)
    {
        try
        {
            await _storageClient.DeleteObjectAsync(_bucketName, fileName);
            return true;
        }
        catch (GoogleApiException ex)
        {
            throw new Exception($"GCP Cloud Storage 刪除失敗: {ex.Message}", ex);
        }
    }

    public async Task<bool> FileExistsAsync(string fileName)
    {
        try
        {
            await _storageClient.GetObjectAsync(_bucketName, fileName);
            return true;
        }
        catch (GoogleApiException ex) when (ex.HttpStatusCode == System.Net.HttpStatusCode.NotFound)
        {
            return false;
        }
    }

    public async Task<string> GetFileUrlAsync(string fileName, TimeSpan? expiry = null)
    {
        try
        {
            var urlSigner = UrlSigner.FromServiceAccountPath(_projectId);
            var signedUrl = await urlSigner.SignAsync(_bucketName, fileName, expiry ?? TimeSpan.FromHours(1));
            return signedUrl;
        }
        catch (Exception ex)
        {
            // 如果無法產生簽名 URL,回傳公開 URL
            return $"https://storage.googleapis.com/{_bucketName}/{fileName}";
        }
    }

    public async Task<long> GetFileSizeAsync(string fileName)
    {
        try
        {
            var obj = await _storageClient.GetObjectAsync(_bucketName, fileName);
            return (long)(obj.Size ?? 0);
        }
        catch (GoogleApiException ex)
        {
            throw new Exception($"GCP Cloud Storage 取得檔案大小失敗: {ex.Message}", ex);
        }
    }
}

7. 工廠模式實作

csharp
public interface ICloudStorageFactory
{
    ICloudStorageService CreateStorageService();
}

public class CloudStorageFactory : ICloudStorageFactory
{
    private readonly IConfiguration _configuration;
    private readonly IServiceProvider _serviceProvider;

    public CloudStorageFactory(IConfiguration configuration, IServiceProvider serviceProvider)
    {
        _configuration = configuration;
        _serviceProvider = serviceProvider;
    }

    public ICloudStorageService CreateStorageService()
    {
        var provider = _configuration["CloudStorage:Provider"];
        
        return provider?.ToLower() switch
        {
            "minio" => _serviceProvider.GetRequiredService<MinIOStorageService>(),
            "s3" => _serviceProvider.GetRequiredService<S3StorageService>(),
            "gcs" => _serviceProvider.GetRequiredService<GcsStorageService>(),
            _ => throw new ArgumentException($"不支援的儲存提供者: {provider}")
        };
    }
}

8. 依賴注入設定 (Program.cs)

csharp
using Amazon.S3;

var builder = WebApplication.CreateBuilder(args);

// 註冊所有儲存服務
builder.Services.AddScoped<MinIOStorageService>();
builder.Services.AddScoped<S3StorageService>();
builder.Services.AddScoped<GcsStorageService>();

// 註冊工廠
builder.Services.AddScoped<ICloudStorageFactory, CloudStorageFactory>();

// 註冊主要服務 (根據設定檔決定使用哪個)
builder.Services.AddScoped<ICloudStorageService>(provider =>
{
    var factory = provider.GetRequiredService<ICloudStorageFactory>();
    return factory.CreateStorageService();
});

// AWS 特定設定
builder.Services.AddAWSService<IAmazonS3>();

var app = builder.Build();

app.Run();

9. 使用範例 (Controller)

csharp
[ApiController]
[Route("api/[controller]")]
public class FileController : ControllerBase
{
    private readonly ICloudStorageService _storageService;

    public FileController(ICloudStorageService storageService)
    {
        _storageService = storageService;
    }

    [HttpPost("upload")]
    public async Task<IActionResult> UploadFile(IFormFile file)
    {
        if (file == null || file.Length == 0)
            return BadRequest("沒有選擇檔案");

        try
        {
            using var stream = file.OpenReadStream();
            var url = await _storageService.UploadFileAsync(stream, file.FileName, file.ContentType);
            
            return Ok(new { Url = url, Message = "上傳成功" });
        }
        catch (Exception ex)
        {
            return StatusCode(500, new { Message = ex.Message });
        }
    }

    [HttpGet("download/{fileName}")]
    public async Task<IActionResult> DownloadFile(string fileName)
    {
        try
        {
            var fileStream = await _storageService.DownloadFileAsync(fileName);
            return File(fileStream, "application/octet-stream", fileName);
        }
        catch (Exception ex)
        {
            return StatusCode(500, new { Message = ex.Message });
        }
    }

    [HttpGet("list")]
    public async Task<IActionResult> ListFiles(string prefix = "")
    {
        try
        {
            var files = await _storageService.ListFilesAsync(prefix);
            return Ok(files);
        }
        catch (Exception ex)
        {
            return StatusCode(500, new { Message = ex.Message });
        }
    }

    [HttpDelete("{fileName}")]
    public async Task<IActionResult> DeleteFile(string fileName)
    {
        try
        {
            var result = await _storageService.DeleteFileAsync(fileName);
            return Ok(new { Success = result, Message = "刪除成功" });
        }
        catch (Exception ex)
        {
            return StatusCode(500, new { Message = ex.Message });
        }
    }
}

10. 功能對比總結

功能MinIOAWS S3GCP Cloud Storage
本地開發✅ 最佳❌ 需要帳戶❌ 需要帳戶
S3 相容性✅ 完全相容✅ 原生❌ 有限相容
成本✅ 免費💰 按使用量💰 按使用量
效能⚡ 本地最快🌐 全球網路🌐 全球網路
安全性🔒 自主控制🛡️ 企業級🛡️ 企業級
擴展性📈 需自建📈 無限📈 無限

使用建議

  1. 開發環境:使用 MinIO 進行本地開發
  2. 測試環境:使用雲端服務測試實際場景
  3. 生產環境:根據需求選擇 AWS S3 或 GCP Cloud Storage
  4. 混合架構:可以根據不同環境使用不同的儲存服務

這樣的設計讓你可以輕鬆在不同的儲存後端之間切換,非常適合多雲架構或不同環境使用不同儲存方案的場景。

Content is user-generated and unverified.
    .NET Core: MinIO + AWS S3 + GCP Cloud Storage 完整實作 | Claude