From 1c2a96c6f2f0599c2f1fd1b406850e5eab2db4b0 Mon Sep 17 00:00:00 2001 From: Aaron Po Date: Fri, 28 Aug 2026 00:33:11 -0400 Subject: [PATCH] Wire SeaweedFS credentials through to the api.core dev container api.core now waits on seaweedfs and receives its service URL and credentials so the file storage provider can connect. --- web/.env.example | 1 + .../Commands/UploadPhotoHandlerTests.cs | 64 +++++++++++++------ .../UploadPhoto/UploadPhotoHandler.cs | 18 ++---- .../UploadPhoto/UploadPhotoValidator.cs | 17 ++--- .../Features.PhotoUpload.csproj | 3 - .../Repository/PhotoUploadRepository.cs | 8 +-- web/docker-compose.dev.yaml | 6 ++ 7 files changed, 64 insertions(+), 53 deletions(-) diff --git a/web/.env.example b/web/.env.example index 43f3a72b..8bcfbc07 100644 --- a/web/.env.example +++ b/web/.env.example @@ -61,6 +61,7 @@ WEBSITE_BASE_URL=http://localhost:3000 # SeaweedFS (S3) Configuration # ====================== # Credentials and default bucket for the local SeaweedFS S3-compatible storage +SEAWEEDFS_SERVICE_URL=http://seaweedfs:8333 SEAWEEDFS_ACCESS_KEY_ID=admin SEAWEEDFS_SECRET_ACCESS_KEY=secret SEAWEEDFS_BUCKET=my-bucket diff --git a/web/backend/Features/Features.PhotoUpload.Tests/Commands/UploadPhotoHandlerTests.cs b/web/backend/Features/Features.PhotoUpload.Tests/Commands/UploadPhotoHandlerTests.cs index 09f5179b..856fe5ab 100644 --- a/web/backend/Features/Features.PhotoUpload.Tests/Commands/UploadPhotoHandlerTests.cs +++ b/web/backend/Features/Features.PhotoUpload.Tests/Commands/UploadPhotoHandlerTests.cs @@ -19,7 +19,10 @@ public class UploadPhotoHandlerTests _handler = new UploadPhotoHandler(_fileStorageProviderMock.Object, _repoMock.Object); } - private static Mock ValidFile(string content = "fake-image-bytes", string contentType = "image/png") + private static Mock ValidFile( + string content = "fake-image-bytes", + string contentType = "image/png" + ) { Mock file = new(); byte[] bytes = System.Text.Encoding.UTF8.GetBytes(content); @@ -38,16 +41,21 @@ public class UploadPhotoHandlerTests string? uploadedKey = null; string? uploadedContentType = null; _fileStorageProviderMock - .Setup(p => p.UploadAsync( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .Callback((key, _, contentType, _) => - { - uploadedKey = key; - uploadedContentType = contentType; - }) + .Setup(p => + p.UploadAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ) + ) + .Callback( + (key, _, contentType, _) => + { + uploadedKey = key; + uploadedContentType = contentType; + } + ) .Returns(Task.CompletedTask); Photo? persisted = null; @@ -68,9 +76,19 @@ public class UploadPhotoHandlerTests persisted.UploadedById.Should().Be(command.UploadedById); _fileStorageProviderMock.Verify( - p => p.UploadAsync(uploadedKey!, It.IsAny(), "image/png", It.IsAny()), - Times.Once); - _repoMock.Verify(r => r.CreateAsync(It.IsAny(), It.IsAny()), Times.Once); + p => + p.UploadAsync( + uploadedKey!, + It.IsAny(), + "image/png", + It.IsAny() + ), + Times.Once + ); + _repoMock.Verify( + r => r.CreateAsync(It.IsAny(), It.IsAny()), + Times.Once + ); } [Fact] @@ -80,16 +98,22 @@ public class UploadPhotoHandlerTests UploadPhotoCommand command = new(Guid.NewGuid(), "avatars", file.Object); _fileStorageProviderMock - .Setup(p => p.UploadAsync( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) + .Setup(p => + p.UploadAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ) + ) .ThrowsAsync(new InvalidOperationException("storage unavailable")); Func act = () => _handler.Handle(command, CancellationToken.None); await act.Should().ThrowAsync(); - _repoMock.Verify(r => r.CreateAsync(It.IsAny(), It.IsAny()), Times.Never); + _repoMock.Verify( + r => r.CreateAsync(It.IsAny(), It.IsAny()), + Times.Never + ); } } diff --git a/web/backend/Features/Features.PhotoUpload/Commands/UploadPhoto/UploadPhotoHandler.cs b/web/backend/Features/Features.PhotoUpload/Commands/UploadPhoto/UploadPhotoHandler.cs index 73c2d742..959a85fe 100644 --- a/web/backend/Features/Features.PhotoUpload/Commands/UploadPhoto/UploadPhotoHandler.cs +++ b/web/backend/Features/Features.PhotoUpload/Commands/UploadPhoto/UploadPhotoHandler.cs @@ -6,8 +6,10 @@ using MediatR; namespace Features.ImageUploads.Commands.UploadPhoto; /// Handles . -public class UploadPhotoHandler(IFileStorageProvider fileStorageProvider, IPhotoUploadRepository photoUploadRepository) - : IRequestHandler +public class UploadPhotoHandler( + IFileStorageProvider fileStorageProvider, + IPhotoUploadRepository photoUploadRepository +) : IRequestHandler { public async Task Handle(UploadPhotoCommand request, CancellationToken cancellationToken) { @@ -16,7 +18,6 @@ public class UploadPhotoHandler(IFileStorageProvider fileStorageProvider, IPhoto await using Stream stream = request.File.OpenReadStream(); - await fileStorageProvider.UploadAsync( key, stream, @@ -24,16 +25,9 @@ public class UploadPhotoHandler(IFileStorageProvider fileStorageProvider, IPhoto cancellationToken ); + Photo photo = new() { Hyperlink = key, UploadedById = request.UploadedById }; - Photo photo = new() - { - Hyperlink = key, - UploadedById = request.UploadedById, - }; - - - await photoUploadRepository.CreateAsync(photo,cancellationToken); - + await photoUploadRepository.CreateAsync(photo, cancellationToken); return photoId; } diff --git a/web/backend/Features/Features.PhotoUpload/Commands/UploadPhoto/UploadPhotoValidator.cs b/web/backend/Features/Features.PhotoUpload/Commands/UploadPhoto/UploadPhotoValidator.cs index 4d1549e8..98236168 100644 --- a/web/backend/Features/Features.PhotoUpload/Commands/UploadPhoto/UploadPhotoValidator.cs +++ b/web/backend/Features/Features.PhotoUpload/Commands/UploadPhoto/UploadPhotoValidator.cs @@ -7,18 +7,11 @@ public class UploadPhotoValidator : AbstractValidator { public UploadPhotoValidator() { - RuleFor(x => x.UploadedById) - .NotEmpty() - .WithMessage("UploadedById is required."); + RuleFor(x => x.UploadedById).NotEmpty().WithMessage("UploadedById is required."); - RuleFor(x => x.File) - .NotNull() - .WithMessage("File is required."); - - RuleFor(x => x.Key) - .NotEmpty() - .WithMessage("Key is required."); + RuleFor(x => x.File).NotNull().WithMessage("File is required."); + RuleFor(x => x.Key).NotEmpty().WithMessage("Key is required."); RuleFor(x => x.File.Length) .GreaterThan(0) @@ -26,7 +19,9 @@ public class UploadPhotoValidator : AbstractValidator .WithMessage("File cannot be empty."); RuleFor(x => x.File.ContentType) - .Must(contentType => contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase)) + .Must(contentType => + contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase) + ) .When(x => x.File is not null) .WithMessage("File must be an image."); } diff --git a/web/backend/Features/Features.PhotoUpload/Features.PhotoUpload.csproj b/web/backend/Features/Features.PhotoUpload/Features.PhotoUpload.csproj index 7fdca1f2..64b3c805 100644 --- a/web/backend/Features/Features.PhotoUpload/Features.PhotoUpload.csproj +++ b/web/backend/Features/Features.PhotoUpload/Features.PhotoUpload.csproj @@ -4,7 +4,6 @@ enable enable Features.ImageUploads - @@ -19,6 +18,4 @@ - - diff --git a/web/backend/Features/Features.PhotoUpload/Repository/PhotoUploadRepository.cs b/web/backend/Features/Features.PhotoUpload/Repository/PhotoUploadRepository.cs index 01ce66b1..76a0d475 100644 --- a/web/backend/Features/Features.PhotoUpload/Repository/PhotoUploadRepository.cs +++ b/web/backend/Features/Features.PhotoUpload/Repository/PhotoUploadRepository.cs @@ -17,7 +17,6 @@ public class PhotoUploadRepository(ISqlConnectionFactory connectionFactory) { public async Task CreateAsync(Photo photo, CancellationToken cancellationToken) { - await using DbConnection connection = await CreateConnection(); await using DbTransaction transaction = await connection.BeginTransactionAsync(); @@ -48,11 +47,7 @@ public class PhotoUploadRepository(ISqlConnectionFactory connectionFactory) INSERT INTO dbo.Photo (Hyperlink, UploadedByID) VALUES (@Hyperlink, @UploadedById) """, - new - { - photo.Hyperlink, - photo.UploadedById - }, + new { photo.Hyperlink, photo.UploadedById }, transaction ) ); @@ -65,5 +60,4 @@ public class PhotoUploadRepository(ISqlConnectionFactory connectionFactory) throw; } } - } diff --git a/web/docker-compose.dev.yaml b/web/docker-compose.dev.yaml index 3b4b353c..aa8a56a9 100644 --- a/web/docker-compose.dev.yaml +++ b/web/docker-compose.dev.yaml @@ -80,6 +80,8 @@ services: depends_on: database.seed: condition: service_completed_successfully + seaweedfs: + condition: service_started build: context: ./backend dockerfile: API/API.Core/Dockerfile @@ -101,6 +103,10 @@ services: REFRESH_TOKEN_SECRET: "${REFRESH_TOKEN_SECRET}" CONFIRMATION_TOKEN_SECRET: "${CONFIRMATION_TOKEN_SECRET}" WEBSITE_BASE_URL: "${WEBSITE_BASE_URL}" + SEAWEEDFS_SERVICE_URL: "${SEAWEEDFS_SERVICE_URL}" + SEAWEEDFS_ACCESS_KEY_ID: "${SEAWEEDFS_ACCESS_KEY_ID}" + SEAWEEDFS_SECRET_ACCESS_KEY: "${SEAWEEDFS_SECRET_ACCESS_KEY}" + SEAWEEDFS_BUCKET: "${SEAWEEDFS_BUCKET}" restart: unless-stopped networks: - devnet