<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>{
"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"
}
}
}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; }
}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);
}
}
}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);
}
}
}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);
}
}
}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}")
};
}
}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();[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 });
}
}
}| 功能 | MinIO | AWS S3 | GCP Cloud Storage |
|---|---|---|---|
| 本地開發 | ✅ 最佳 | ❌ 需要帳戶 | ❌ 需要帳戶 |
| S3 相容性 | ✅ 完全相容 | ✅ 原生 | ❌ 有限相容 |
| 成本 | ✅ 免費 | 💰 按使用量 | 💰 按使用量 |
| 效能 | ⚡ 本地最快 | 🌐 全球網路 | 🌐 全球網路 |
| 安全性 | 🔒 自主控制 | 🛡️ 企業級 | 🛡️ 企業級 |
| 擴展性 | 📈 需自建 | 📈 無限 | 📈 無限 |
這樣的設計讓你可以輕鬆在不同的儲存後端之間切換,非常適合多雲架構或不同環境使用不同儲存方案的場景。