Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@

namespace ByteSync.Functions.Timer;

public class CleanupBlobStorageSnippetsFunction
public class CleanupAzureBlobStorageSnippetsFunction
{
private readonly ILogger<CleanupBlobStorageSnippetsFunction> _logger;
private readonly ILogger<CleanupAzureBlobStorageSnippetsFunction> _logger;
private readonly IMediator _mediator;

public CleanupBlobStorageSnippetsFunction(IConfiguration configuration, IMediator mediator,
ILogger<CleanupBlobStorageSnippetsFunction> logger)
public CleanupAzureBlobStorageSnippetsFunction(IConfiguration configuration, IMediator mediator,
ILogger<CleanupAzureBlobStorageSnippetsFunction> logger)
{
_mediator = mediator;
_logger = logger;
Expand All @@ -25,9 +25,9 @@ public async Task<int> RunAsync([TimerTrigger("0 0 0 * * *"
#endif
)] TimerInfo myTimer)
{
_logger.LogInformation("Cleanup BlobStorage Function started at: {Now}", DateTime.Now);
var deletedBlobsCount = await _mediator.Send(new CleanupBlobStorageSnippetsRequest());
_logger.LogInformation("Cleanup BlobStorage Function - Deletion complete, {Deleted} element(s)", deletedBlobsCount);
_logger.LogInformation("Cleanup Azure BlobStorage Function started at: {Now}", DateTime.Now);
var deletedBlobsCount = await _mediator.Send(new CleanupAzureBlobStorageSnippetsRequest());
_logger.LogInformation("Cleanup Azure BlobStorage Function - Deletion complete, {Deleted} element(s)", deletedBlobsCount);
return deletedBlobsCount;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using MediatR;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using ByteSync.ServerCommon.Commands.Storage;
using Microsoft.Azure.Functions.Worker;

namespace ByteSync.Functions.Timer;

public class CleanupCloudflareR2SnippetsFunction
{
private readonly ILogger<CleanupCloudflareR2SnippetsFunction> _logger;
private readonly IMediator _mediator;

public CleanupCloudflareR2SnippetsFunction(IConfiguration configuration, IMediator mediator,
ILogger<CleanupCloudflareR2SnippetsFunction> logger)
{
_mediator = mediator;
_logger = logger;
}

[Function("CleanupCloudflareR2FilesFunction")]
public async Task<int> RunAsync([TimerTrigger("0 0 0 * * *"
#if DEBUG
, RunOnStartup= true
#endif
)] TimerInfo myTimer)
{
_logger.LogInformation("Cleanup Cloudflare R2 Function started at: {Now}", DateTime.Now);
var deletedObjectsCount = await _mediator.Send(new CleanupCloudflareR2SnippetsRequest());
_logger.LogInformation("Cleanup Cloudflare R2 Function - Deletion complete, {Deleted} element(s)", deletedObjectsCount);
return deletedObjectsCount;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

namespace ByteSync.ServerCommon.Commands.Storage;

public class CleanupAzureBlobStorageSnippetsCommandHandler : IRequestHandler<CleanupBlobStorageSnippetsRequest, int>
public class CleanupAzureBlobStorageSnippetsCommandHandler : IRequestHandler<CleanupAzureBlobStorageSnippetsRequest, int>
{
private readonly IBlobStorageContainerService _blobStorageContainerService;
private readonly ILogger<CleanupAzureBlobStorageSnippetsCommandHandler> _logger;
Expand All @@ -23,7 +23,7 @@ public CleanupAzureBlobStorageSnippetsCommandHandler(
_logger = logger;
}

public async Task<int> Handle(CleanupBlobStorageSnippetsRequest request, CancellationToken cancellationToken)
public async Task<int> Handle(CleanupAzureBlobStorageSnippetsRequest request, CancellationToken cancellationToken)
{
if (_blobStorageSettings.RetentionDurationInDays < 1)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using MediatR;

namespace ByteSync.ServerCommon.Commands.Storage;

public class CleanupAzureBlobStorageSnippetsRequest : IRequest<int>
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using Amazon.S3;
using Amazon.S3.Model;
using ByteSync.ServerCommon.Business.Settings;
using ByteSync.ServerCommon.Interfaces.Services;
using MediatR;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace ByteSync.ServerCommon.Commands.Storage;

public class CleanupCloudflareR2SnippetsCommandHandler : IRequestHandler<CleanupCloudflareR2SnippetsRequest, int>
{
private readonly ICloudflareR2Service _cloudflareR2Service;
private readonly ILogger<CleanupCloudflareR2SnippetsCommandHandler> _logger;
private readonly CloudflareR2Settings _cloudflareR2Settings;

public CleanupCloudflareR2SnippetsCommandHandler(
ICloudflareR2Service cloudflareR2Service,
IOptions<CloudflareR2Settings> cloudflareR2Settings,
ILogger<CleanupCloudflareR2SnippetsCommandHandler> logger)
{
_cloudflareR2Service = cloudflareR2Service;
_cloudflareR2Settings = cloudflareR2Settings.Value;
_logger = logger;
}

public async Task<int> Handle(CleanupCloudflareR2SnippetsRequest request, CancellationToken cancellationToken)
{
if (_cloudflareR2Settings.RetentionDurationInDays < 1)
{
_logger.LogWarning("RetentionDurationInDays is less than 1, no element deleted");
return 0;
}

var listObjectsRequest = new ListObjectsV2Request
{
BucketName = _cloudflareR2Settings.BucketName
};

var deletedObjectsCount = 0;
var cutoffDate = DateTime.UtcNow.AddDays(-_cloudflareR2Settings.RetentionDurationInDays);

try
{
var listObjectsResponse = await _cloudflareR2Service.ListObjectsAsync(listObjectsRequest, cancellationToken);

foreach (var s3Object in listObjectsResponse.S3Objects)
{
if (s3Object.LastModified <= cutoffDate)
{
_logger.LogInformation("Deleting obsolete R2 object {ObjectKey} (LastModified:{LastModified})",
s3Object.Key, s3Object.LastModified);

var deleteRequest = new DeleteObjectRequest
{
BucketName = _cloudflareR2Settings.BucketName,
Key = s3Object.Key
};

await _cloudflareR2Service.DeleteObjectAsync(deleteRequest, cancellationToken);
deletedObjectsCount += 1;
}
}
}
catch (AmazonS3Exception ex)
{
_logger.LogError(ex, "Error listing or deleting objects from R2 bucket {BucketName}", _cloudflareR2Settings.BucketName);
return 0;
}

return deletedObjectsCount;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

namespace ByteSync.ServerCommon.Commands.Storage;

public class CleanupBlobStorageSnippetsRequest : IRequest<int>
public class CleanupCloudflareR2SnippetsRequest : IRequest<int>
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Amazon.S3.Model;

namespace ByteSync.ServerCommon.Interfaces.Services;

public interface ICloudflareR2Service
{
Task<ListObjectsV2Response> ListObjectsAsync(ListObjectsV2Request request, CancellationToken cancellationToken);

Task<DeleteObjectResponse> DeleteObjectAsync(DeleteObjectRequest request, CancellationToken cancellationToken);
}
47 changes: 47 additions & 0 deletions src/ByteSync.ServerCommon/Services/CloudflareR2Service.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using Amazon.S3;
using Amazon.S3.Model;
using ByteSync.ServerCommon.Business.Settings;
using ByteSync.ServerCommon.Interfaces.Services;
using Microsoft.Extensions.Options;

namespace ByteSync.ServerCommon.Services;

public class CloudflareR2Service : ICloudflareR2Service
{
private readonly CloudflareR2Settings _cloudflareR2Settings;
private AmazonS3Client? _s3Client;

public CloudflareR2Service(IOptions<CloudflareR2Settings> cloudflareR2Settings)
{
_cloudflareR2Settings = cloudflareR2Settings.Value;
}

public async Task<ListObjectsV2Response> ListObjectsAsync(ListObjectsV2Request request, CancellationToken cancellationToken)
{
var s3Client = GetS3Client();
return await s3Client.ListObjectsV2Async(request, cancellationToken);
}

public async Task<DeleteObjectResponse> DeleteObjectAsync(DeleteObjectRequest request, CancellationToken cancellationToken)
{
var s3Client = GetS3Client();
return await s3Client.DeleteObjectAsync(request, cancellationToken);
}

private AmazonS3Client GetS3Client()
{
if (_s3Client == null)
{
_s3Client = new AmazonS3Client(
_cloudflareR2Settings.AccessKeyId,
_cloudflareR2Settings.SecretAccessKey,
new AmazonS3Config
{
ServiceURL = _cloudflareR2Settings.Endpoint,
ForcePathStyle = true
});
}

return _s3Client;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using ByteSync.ServerCommon.Commands.Storage;
using ByteSync.Functions.Timer;
using Moq;
using MediatR;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Azure.Functions.Worker;

namespace ByteSync.Functions.UnitTests.Timer;

[TestFixture]
public class CleanupCloudflareR2SnippetsFunctionTests
{
private Mock<IMediator> _mediator = null!;
private Mock<ILogger<CleanupCloudflareR2SnippetsFunction>> _logger = null!;
private Mock<IConfiguration> _configuration = null!;
private CleanupCloudflareR2SnippetsFunction _function = null!;

[SetUp]
public void Setup()
{
_mediator = new Mock<IMediator>();
_logger = new Mock<ILogger<CleanupCloudflareR2SnippetsFunction>>();
_configuration = new Mock<IConfiguration>();

_function = new CleanupCloudflareR2SnippetsFunction(_configuration.Object, _mediator.Object, _logger.Object);
}

[Test]
public async Task RunAsync_ShouldSendCleanupRequest()
{
// Arrange
var expectedDeletedCount = 5;
_mediator.Setup(m => m.Send(It.IsAny<CleanupCloudflareR2SnippetsRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(expectedDeletedCount);

// Act
var result = await _function.RunAsync(It.IsAny<TimerInfo>());

// Assert
Assert.That(result, Is.EqualTo(expectedDeletedCount));
_mediator.Verify(m => m.Send(It.IsAny<CleanupCloudflareR2SnippetsRequest>(), It.IsAny<CancellationToken>()), Times.Once);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public async Task Handle_DeletesBlobsOlderThanRetention()
.Returns(Task.FromResult(container));

// Act
var result = await _handler.Handle(new CleanupBlobStorageSnippetsRequest(), CancellationToken.None);
var result = await _handler.Handle(new CleanupAzureBlobStorageSnippetsRequest(), CancellationToken.None);

// Assert
result.Should().Be(2);
Expand All @@ -76,7 +76,7 @@ public async Task Handle_DoesNothingIfRetentionIsTooLow()
_settings.RetentionDurationInDays = 0;

// Act
var result = await _handler.Handle(new CleanupBlobStorageSnippetsRequest(), CancellationToken.None);
var result = await _handler.Handle(new CleanupAzureBlobStorageSnippetsRequest(), CancellationToken.None);

// Assert
result.Should().Be(0);
Expand All @@ -94,7 +94,7 @@ public async Task Handle_DoesNothingIfContainerDoesNotExist()
.Returns(Task.FromResult(container));

// Act
var result = await _handler.Handle(new CleanupBlobStorageSnippetsRequest(), CancellationToken.None);
var result = await _handler.Handle(new CleanupAzureBlobStorageSnippetsRequest(), CancellationToken.None);

// Assert
result.Should().Be(0);
Expand All @@ -120,7 +120,7 @@ public async Task Handle_ReturnsZeroIfNoBlobsToDelete()
.Returns(Task.FromResult(container));

// Act
var result = await _handler.Handle(new CleanupBlobStorageSnippetsRequest(), CancellationToken.None);
var result = await _handler.Handle(new CleanupAzureBlobStorageSnippetsRequest(), CancellationToken.None);

// Assert
result.Should().Be(0);
Expand Down
Loading
Loading