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); } }