dramaling-vocab-learning/backend/DramaLing.Api/Services/Media/Storage/GoogleCloudImageStorageServ...

173 lines
5.9 KiB
C#

using Google.Cloud.Storage.V1;
using Google.Apis.Auth.OAuth2;
using DramaLing.Api.Models.Configuration;
using DramaLing.Api.Services.Storage;
using Microsoft.Extensions.Options;
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,
IConfiguration configuration,
ILogger<GoogleCloudImageStorageService> logger)
{
_options = options.Value ?? throw new ArgumentNullException(nameof(options));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
// 從 User Secrets 取得配置
var projectId = configuration["GoogleCloudStorage:ProjectId"] ?? "dramaling-project";
var bucketName = configuration["GoogleCloudStorage:BucketName"] ?? "dramaling-images";
_options.ProjectId = projectId;
_options.BucketName = bucketName;
// 初始化 Storage Client
_storageClient = CreateStorageClient();
_logger.LogInformation("GoogleCloudImageStorageService initialized with bucket: {BucketName}", bucketName);
}
private StorageClient CreateStorageClient()
{
try
{
// 嘗試使用預設認證 (本地開發使用 gcloud auth application-default login)
return StorageClient.Create();
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to initialize Google Cloud Storage client. Please ensure you have run 'gcloud auth application-default login' or set up Service Account credentials.");
throw;
}
}
public async Task<string> SaveImageAsync(Stream imageStream, string fileName)
{
try
{
var objectName = $"{_options.PathPrefix}/{fileName}";
// 使用 UploadObjectAsync 的 簡化版本
var uploadedObject = await _storageClient.UploadObjectAsync(
_options.BucketName,
objectName,
GetContentType(fileName),
imageStream);
_logger.LogInformation("Image uploaded successfully to GCS: {ObjectName}", objectName);
return objectName;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to save image to GCS: {FileName}", fileName);
throw;
}
}
public Task<string> GetImageUrlAsync(string imagePath)
{
try
{
// 如果設定了自訂域名 (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);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to generate image URL for: {ImagePath}", imagePath);
throw;
}
}
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 (Exception ex) when (ex.Message.Contains("404") || ex.Message.Contains("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 (Exception ex) when (ex.Message.Contains("404") || ex.Message.Contains("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
{
// 簡化版本 - 只返回基本資訊
return new StorageInfo
{
Provider = "Google Cloud Storage",
TotalSizeBytes = 0, // GCS 不提供簡單的總計查詢
FileCount = 0,
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",
".bmp" => "image/bmp",
".tiff" or ".tif" => "image/tiff",
_ => "application/octet-stream"
};
}
}