generated from fahricansecer/boilerplate-be
173 lines
5.3 KiB
C#
173 lines
5.3 KiB
C#
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);
|
||
}
|
||
}
|