using Amazon.Runtime; using Amazon.S3; using Amazon.S3.Model; namespace TheCr3w.Shared.Storage; public class R2StorageOptions { public string Endpoint { get; set; } = "https://bf705752e38169fedb83efd5b747390e.r2.cloudflarestorage.com"; public string BucketName { get; set; } = "thecr3w-app"; public string Region { get; set; } = "auto"; public string AccessKeyId { get; set; } = string.Empty; public string SecretAccessKey { get; set; } = string.Empty; } public interface IR2StorageService { string GeneratePreSignedDownloadUrl(string objectKey, TimeSpan? duration = null); string GeneratePreSignedUploadUrl(string objectKey, string contentType, TimeSpan? duration = null); Task ObjectExistsAsync(string objectKey, CancellationToken cancellationToken = default); } public class R2StorageService : IR2StorageService { private readonly R2StorageOptions _options; private readonly IAmazonS3 _s3Client; public R2StorageService(R2StorageOptions options) { _options = options ?? throw new ArgumentNullException(nameof(options)); var credentials = new BasicAWSCredentials(_options.AccessKeyId, _options.SecretAccessKey); var config = new AmazonS3Config { ServiceURL = _options.Endpoint, AuthenticationRegion = _options.Region, ForcePathStyle = true }; _s3Client = new AmazonS3Client(credentials, config); } /// /// Generuje krótko żyjący (domyślnie 15 minut) podpisany adres URL (Pre-signed S3 GET) /// pozwalający na bezpośrednie pobranie artefaktu z Cloudflare R2 bez obciążania serwera. /// public string GeneratePreSignedDownloadUrl(string objectKey, TimeSpan? duration = null) { var expires = DateTime.UtcNow.Add(duration ?? TimeSpan.FromMinutes(15)); var request = new GetPreSignedUrlRequest { BucketName = _options.BucketName, Key = objectKey, Verb = HttpVerb.GET, Expires = expires }; return _s3Client.GetPreSignedURL(request); } /// /// Generuje krótko żyjący (domyślnie 30 minut) podpisany adres URL (Pre-signed S3 PUT) /// pozwalający na bezpieczny upload pliku do Cloudflare R2. /// public string GeneratePreSignedUploadUrl(string objectKey, string contentType, TimeSpan? duration = null) { var expires = DateTime.UtcNow.Add(duration ?? TimeSpan.FromMinutes(30)); var request = new GetPreSignedUrlRequest { BucketName = _options.BucketName, Key = objectKey, Verb = HttpVerb.PUT, Expires = expires, ContentType = contentType }; return _s3Client.GetPreSignedURL(request); } public async Task ObjectExistsAsync(string objectKey, CancellationToken cancellationToken = default) { try { var response = await _s3Client.GetObjectMetadataAsync(new GetObjectMetadataRequest { BucketName = _options.BucketName, Key = objectKey }, cancellationToken); return response.HttpStatusCode == System.Net.HttpStatusCode.OK; } catch (AmazonS3Exception ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound) { return false; } } }