173 lines
5.9 KiB
C#
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"
|
|
};
|
|
}
|
|
} |