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 _logger; public GoogleCloudImageStorageService( IOptions options, IConfiguration configuration, ILogger 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 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 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 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 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 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" }; } }