generated from fahricansecer/boilerplate-be
This commit is contained in:
172
media-worker/Services/S3StorageService.cs
Normal file
172
media-worker/Services/S3StorageService.cs
Normal file
@@ -0,0 +1,172 @@
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
public class S3StorageService
|
||||
{
|
||||
private readonly ILogger<S3StorageService> _logger;
|
||||
private readonly S3Settings _settings;
|
||||
private readonly AmazonS3Client _s3Client;
|
||||
|
||||
public S3StorageService(
|
||||
ILogger<S3StorageService> logger,
|
||||
IOptions<S3Settings> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tek bir dosyayı S3/R2'ye yükler.
|
||||
/// Büyük dosyalar (>5MB) otomatik multipart upload kullanır.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Render pipeline sonucu üretilen tüm medya dosyalarını toplu yükler.
|
||||
/// </summary>
|
||||
public async Task<List<GeneratedMediaFile>> UploadAllMediaAsync(
|
||||
string projectId,
|
||||
List<GeneratedMediaFile> mediaFiles,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var uploadedFiles = new List<GeneratedMediaFile>();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Final videoyu yükler ve public URL döner.
|
||||
/// </summary>
|
||||
public async Task<string> 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user