Optimised UploadFileAsync() #35

Merged
khwezi merged 1 commits from s3service into master 2026-05-20 15:33:48 +02:00
5 changed files with 80 additions and 6 deletions
Showing only changes of commit ccf30ac36b - Show all commits
@@ -7,7 +7,7 @@ public class S3ServiceFeatureTests(CommonFixture fixture, ITestOutputHelper outp
[Fact]
public async Task BookshopS3Service_MustReturnUrl()
{
var service = fixture.Services.GetKeyedService<IS3Service>(S3.Constants.BookshopQuotesBucketName);
var service = fixture.Services.GetKeyedService<IS3Service>(S3.Constants.BookshopBucketName);
var fileName = "appsettings.json";
@@ -25,4 +25,30 @@ public class S3ServiceFeatureTests(CommonFixture fixture, ITestOutputHelper outp
output.WriteLine(result.Value);
}
[Fact]
public async Task BookshopS3Service_MustDeleteFile()
{
var service = fixture.Services.GetKeyedService<IS3Service>(S3.Constants.BookshopBucketName);
var fileName = "appsettings.json";
string path = Path.Combine(Directory.GetCurrentDirectory(), fileName);
Assert.True(File.Exists(path));
var stream = File.OpenRead(path);
var uploadResult = await service!.UploadFileAsync(fileName, stream, MimeKit.MimeTypes.GetMimeType(fileName));
Assert.True(uploadResult.IsSuccess);
Assert.NotNull(uploadResult.Value);
Assert.NotEmpty(uploadResult.Value);
var fileKey = uploadResult.Value.Split('/').Last();
var deleteResult = await service!.DeleteFileAsync(fileKey);
Assert.True(deleteResult.IsSuccess);
}
}
+7 -1
View File
@@ -2,6 +2,12 @@
public static class Hash
{
public static Func<string?, string?> GenerateSha256HashString = (input) =>
public static Func<string?, string?> StringToSha256Hash = (input) =>
Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(input!)));
public static Func<Stream, string?> StreamToSha256Hash = (stream) =>
Convert.ToHexString(SHA256.HashData(stream));
public static Func<byte[], string?> BytesToSha256Hash = (bytes) =>
Convert.ToHexString(SHA256.HashData(bytes));
}
@@ -3,4 +3,5 @@
public interface IS3Service
{
Task<Result<string>> UploadFileAsync(string fileName, Stream fileStream, string contentType, CancellationToken cancellationToken = default);
Task<Result> DeleteFileAsync(string fileKey, CancellationToken cancellationToken = default);
}
@@ -1,4 +1,6 @@
namespace LiteCharms.Features.S3.Abstractions;
using static LiteCharms.Features.Extensions.Hash;
namespace LiteCharms.Features.S3.Abstractions;
public abstract class S3ServiceBase(IAmazonS3 amazonS3)
{
@@ -17,17 +19,31 @@ public abstract class S3ServiceBase(IAmazonS3 amazonS3)
if (string.IsNullOrWhiteSpace(CdnBaseUrl))
return Result.Fail<string>("CDN base URL is not configured.");
var fileKey = $"{Guid.NewGuid():N}{Path.GetExtension(fileName)}";
using var stream = new MemoryStream();
await fileStream.CopyToAsync(stream, cancellationToken);
await fileStream.DisposeAsync();
stream.Seek(0, SeekOrigin.Begin);
var fileHash = StreamToSha256Hash(stream);
if(string.IsNullOrWhiteSpace(fileHash))
return Result.Fail<string>("Failed to compute file hash.");
var fileKey = $"{fileHash.ToLower()}{Path.GetExtension(fileName)}";
var putRequest = new PutObjectRequest
{
BucketName = BucketName,
Key = fileKey,
InputStream = fileStream,
InputStream = stream,
ContentType = contentType,
UseChunkEncoding = false
};
stream.Seek(0, SeekOrigin.Begin);
var response = await Client.PutObjectAsync(putRequest, cancellationToken);
return response.HttpStatusCode != System.Net.HttpStatusCode.OK
@@ -39,4 +55,29 @@ public abstract class S3ServiceBase(IAmazonS3 amazonS3)
return Result.Fail<string>(new Error($"Error uploading {fileName} to S3: {ex.Message}").CausedBy(ex));
}
}
public virtual async Task<Result> DeleteFileAsync(string fileKey, CancellationToken cancellationToken = default)
{
try
{
if (string.IsNullOrWhiteSpace(BucketName))
return Result.Fail("Bucket name is not configured.");
var deleteRequest = new DeleteObjectRequest
{
BucketName = BucketName,
Key = fileKey
};
var response = await Client.DeleteObjectAsync(deleteRequest, cancellationToken);
return response.HttpStatusCode != System.Net.HttpStatusCode.NoContent
? Result.Fail($"Failed to delete {fileKey} from S3.")
: Result.Ok();
}
catch (Exception ex)
{
return Result.Fail(new Error($"Error deleting {fileKey} from S3: {ex.Message}").CausedBy(ex));
}
}
}
@@ -28,7 +28,7 @@ public class LeadService(IDbContextFactory<ShopDbContext> contextFactory)
FeedItemId = request.FeedItemId,
Status = LeadStatus.New,
TargetId = request.TargetId,
AttributionHash = GenerateSha256HashString.Invoke($"{request.ClickId}{request.AppClickId}{request.WebClickId}")
AttributionHash = StringToSha256Hash.Invoke($"{request.ClickId}{request.AppClickId}{request.WebClickId}")
});
return await context.SaveChangesAsync(cancellationToken) > 0