Compare commits

...

2 Commits

Author SHA1 Message Date
khwezi 9edb2aa4aa Merge pull request 'Optimised UploadFileAsync()' (#35) from s3service into master
Reviewed-on: #35
2026-05-20 15:33:47 +02:00
Khwezi Mngoma ccf30ac36b Optimised UploadFileAsync()
continuous-integration/drone/pr Build is passing
Implemented and tested DeleteFileAsync()
2026-05-20 15:32:54 +02:00
5 changed files with 80 additions and 6 deletions
@@ -7,7 +7,7 @@ public class S3ServiceFeatureTests(CommonFixture fixture, ITestOutputHelper outp
[Fact] [Fact]
public async Task BookshopS3Service_MustReturnUrl() 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"; var fileName = "appsettings.json";
@@ -25,4 +25,30 @@ public class S3ServiceFeatureTests(CommonFixture fixture, ITestOutputHelper outp
output.WriteLine(result.Value); 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 class Hash
{ {
public static Func<string?, string?> GenerateSha256HashString = (input) => public static Func<string?, string?> StringToSha256Hash = (input) =>
Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(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 public interface IS3Service
{ {
Task<Result<string>> UploadFileAsync(string fileName, Stream fileStream, string contentType, CancellationToken cancellationToken = default); 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) public abstract class S3ServiceBase(IAmazonS3 amazonS3)
{ {
@@ -17,17 +19,31 @@ public abstract class S3ServiceBase(IAmazonS3 amazonS3)
if (string.IsNullOrWhiteSpace(CdnBaseUrl)) if (string.IsNullOrWhiteSpace(CdnBaseUrl))
return Result.Fail<string>("CDN base URL is not configured."); 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 var putRequest = new PutObjectRequest
{ {
BucketName = BucketName, BucketName = BucketName,
Key = fileKey, Key = fileKey,
InputStream = fileStream, InputStream = stream,
ContentType = contentType, ContentType = contentType,
UseChunkEncoding = false UseChunkEncoding = false
}; };
stream.Seek(0, SeekOrigin.Begin);
var response = await Client.PutObjectAsync(putRequest, cancellationToken); var response = await Client.PutObjectAsync(putRequest, cancellationToken);
return response.HttpStatusCode != System.Net.HttpStatusCode.OK 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)); 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, FeedItemId = request.FeedItemId,
Status = LeadStatus.New, Status = LeadStatus.New,
TargetId = request.TargetId, 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 return await context.SaveChangesAsync(cancellationToken) > 0