using Amazon.S3;
using Amazon.S3.Model;
using Amazon.S3.Transfer;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using SaasMediaWorker.Configuration;
using SaasMediaWorker.Models;
namespace SaasMediaWorker.Services;
///
/// S3/Cloudflare R2 depolama servisi.
/// Üretilen medya dosyalarını nesne depolamaya yükler.
///
/// Neden Cloudflare R2?
/// - Egress (çıkış) ücretsiz — global SaaS için büyük maliyet avantajı
/// - S3 uyumlu API — AWS SDK ile doğrudan çalışır
/// - ARM64 uyumlu — AWS SDK .NET tamamen cross-platform
///
public class S3StorageService
{
private readonly ILogger _logger;
private readonly S3Settings _settings;
private readonly AmazonS3Client _s3Client;
public S3StorageService(
ILogger logger,
IOptions settings)
{
_logger = logger;
_settings = settings.Value;
var config = new AmazonS3Config
{
ServiceURL = _settings.Endpoint,
ForcePathStyle = true,
RequestChecksumCalculation = RequestChecksumCalculation.WHEN_REQUIRED,
ResponseChecksumValidation = ResponseChecksumValidation.WHEN_REQUIRED,
};
_s3Client = new AmazonS3Client(
_settings.AccessKey,
_settings.SecretKey,
config);
}
///
/// Tek bir dosyayı S3/R2'ye yükler.
/// Büyük dosyalar (>5MB) otomatik multipart upload kullanır.
///
public async Task<(string url, string key)> UploadFileAsync(
string localPath,
string projectId,
string fileName,
string contentType,
CancellationToken ct)
{
var key = $"projects/{projectId}/{DateTime.UtcNow:yyyy/MM/dd}/{fileName}";
_logger.LogInformation(
"☁️ S3 yükleme başlıyor — Key: {Key}, Dosya: {File}",
key, Path.GetFileName(localPath));
var fileInfo = new FileInfo(localPath);
if (fileInfo.Length > 5 * 1024 * 1024) // 5MB üstü → multipart
{
await UploadMultipartAsync(localPath, key, contentType, ct);
}
else
{
var request = new PutObjectRequest
{
BucketName = _settings.BucketName,
Key = key,
FilePath = localPath,
ContentType = contentType,
DisablePayloadSigning = true,
};
await _s3Client.PutObjectAsync(request, ct);
}
var publicUrl = string.IsNullOrEmpty(_settings.PublicBaseUrl)
? $"{_settings.Endpoint}/{_settings.BucketName}/{key}"
: $"{_settings.PublicBaseUrl.TrimEnd('/')}/{key}";
_logger.LogInformation(
"✅ S3 yükleme tamamlandı — {Key} ({Size} bytes)",
key, fileInfo.Length);
return (publicUrl, key);
}
///
/// Render pipeline sonucu üretilen tüm medya dosyalarını toplu yükler.
///
public async Task> UploadAllMediaAsync(
string projectId,
List mediaFiles,
CancellationToken ct)
{
var uploadedFiles = new List();
foreach (var media in mediaFiles)
{
if (!File.Exists(media.LocalPath))
{
_logger.LogWarning("Dosya bulunamadı, atlanıyor: {Path}", media.LocalPath);
continue;
}
var fileName = Path.GetFileName(media.LocalPath);
var (url, key) = await UploadFileAsync(
media.LocalPath, projectId, fileName, media.MimeType, ct);
media.S3Url = url;
media.S3Key = key;
media.FileSizeBytes = new FileInfo(media.LocalPath).Length;
uploadedFiles.Add(media);
}
_logger.LogInformation(
"Toplu yükleme tamamlandı — {Count}/{Total} dosya yüklendi",
uploadedFiles.Count, mediaFiles.Count);
return uploadedFiles;
}
///
/// Final videoyu yükler ve public URL döner.
///
public async Task UploadFinalVideoAsync(
string localPath,
string projectId,
CancellationToken ct)
{
var fileName = $"final_video_{DateTime.UtcNow:yyyyMMdd_HHmmss}.mp4";
var (url, _) = await UploadFileAsync(
localPath, projectId, fileName, "video/mp4", ct);
return url;
}
private async Task UploadMultipartAsync(
string localPath, string key, string contentType, CancellationToken ct)
{
using var transferUtility = new TransferUtility(_s3Client);
var request = new TransferUtilityUploadRequest
{
BucketName = _settings.BucketName,
Key = key,
FilePath = localPath,
ContentType = contentType,
PartSize = 5 * 1024 * 1024, // 5MB parçalar
DisablePayloadSigning = true,
};
request.UploadProgressEvent += (_, args) =>
{
if (args.PercentDone % 25 == 0)
{
_logger.LogInformation(
"Multipart upload: {Key} — %{Percent}",
key, args.PercentDone);
}
};
await transferUtility.UploadAsync(request, ct);
}
}