diff --git a/.dockerignore b/.dockerignore index cd967fc..fe1152b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,5 @@ -**/.dockerignore +**/.classpath +**/.dockerignore **/.env **/.git **/.gitignore @@ -7,7 +8,6 @@ **/.toolstarget **/.vs **/.vscode -**/.idea **/*.*proj.user **/*.dbmdl **/*.jfm @@ -22,4 +22,9 @@ **/secrets.dev.yaml **/values.dev.yaml LICENSE -README.md \ No newline at end of file +README.md +!**/.gitignore +!.git/HEAD +!.git/config +!.git/packed-refs +!.git/refs/heads/** \ No newline at end of file diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index 7dc84d8..f0ae742 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -6,7 +6,11 @@ on: jobs: codecov: + env: + NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages + runs-on: ubuntu-latest + steps: - name: Checkout repository uses: actions/checkout@v4 @@ -14,7 +18,8 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - global-json-file: global.json + dotnet-version: 8.0.x + cache: true - name: Restore dependencies run: dotnet restore diff --git a/.github/workflows/publish-container.yml b/.github/workflows/publish-container.yml index c2f3f6c..9b6fb73 100644 --- a/.github/workflows/publish-container.yml +++ b/.github/workflows/publish-container.yml @@ -11,8 +11,8 @@ env: jobs: publish-container: - runs-on: ubuntu-latest + permissions: contents: read packages: write diff --git a/.gitignore b/.gitignore index d14415e..3bb57f0 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ obj/ riderModule.iml /_ReSharper.Caches/ .idea/ -*.sln.DotSettings.user +*.user *.opencover.xml /recognizer +.vs/ diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index f50e891..0000000 --- a/Dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -FROM mcr.microsoft.com/dotnet/sdk:8.0 AS publish -ARG BUILD_CONFIGURATION=Release -COPY . /src -WORKDIR "/src/ImageHosting.Storage" -RUN dotnet publish "ImageHosting.Storage.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false - -FROM mcr.microsoft.com/dotnet/aspnet:8.0 -WORKDIR /app -EXPOSE 8080 -COPY --from=publish /app/publish . -ENTRYPOINT ["dotnet", "ImageHosting.Storage.dll"] diff --git a/ImageHosting.Persistence/DbContexts/ImageHostingDbContext.cs b/ImageHosting.Persistence/DbContexts/ImageHostingDbContext.cs deleted file mode 100644 index 4448fa1..0000000 --- a/ImageHosting.Persistence/DbContexts/ImageHostingDbContext.cs +++ /dev/null @@ -1,14 +0,0 @@ -using ImageHosting.Persistence.Entities; -using Microsoft.EntityFrameworkCore; - -namespace ImageHosting.Persistence.DbContexts; - -public class ImageHostingDbContext(DbContextOptions options) - : DbContext(options), IImageHostingDbContext -{ - public DbSet Images => Set(); - public DbSet ImageTags => Set(); - public DbSet ForbiddenCategories => Set(); - - public void Migrate() => Database.Migrate(); -} diff --git a/ImageHosting.Persistence/Entities/ForbiddenCategory.cs b/ImageHosting.Persistence/Entities/ForbiddenCategory.cs deleted file mode 100644 index 213419e..0000000 --- a/ImageHosting.Persistence/Entities/ForbiddenCategory.cs +++ /dev/null @@ -1,10 +0,0 @@ -using ImageHosting.Persistence.Entities.Configuration; -using Microsoft.EntityFrameworkCore; - -namespace ImageHosting.Persistence.Entities; - -[EntityTypeConfiguration(typeof(ForbiddenCategoryConfiguration))] -public class ForbiddenCategory -{ - public string Name { get; set; } = null!; -} diff --git a/ImageHosting.Persistence/Extensions/DependencyInjection/ServiceCollectionExtensions.cs b/ImageHosting.Persistence/Extensions/DependencyInjection/ServiceCollectionExtensions.cs deleted file mode 100644 index 3608f3f..0000000 --- a/ImageHosting.Persistence/Extensions/DependencyInjection/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,21 +0,0 @@ -using ImageHosting.Persistence.DbContexts; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; - -namespace ImageHosting.Persistence.Extensions.DependencyInjection; - -public static class ServiceCollectionExtensions -{ - public static IServiceCollection AddImageHostingDbContext(this IServiceCollection serviceCollection, - string connectionStringKey) - { - return serviceCollection - .AddDbContext((provider, optionsBuilder) => - { - var configuration = provider.GetRequiredService(); - var connectionString = configuration.GetConnectionString(connectionStringKey); - optionsBuilder.UseNpgsql(connectionString); - }); - } -} diff --git a/ImageHosting.Storage/AssignTagsConsumer.cs b/ImageHosting.Storage/AssignTagsConsumer.cs deleted file mode 100644 index f23271c..0000000 --- a/ImageHosting.Storage/AssignTagsConsumer.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Linq; -using System.Threading.Tasks; -using ImageHosting.Persistence.DbContexts; -using ImageHosting.Storage.Features.Images.Models; -using ImageHosting.Storage.Models; -using MassTransit; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Options; - -namespace ImageHosting.Storage; - -public class AssignTagsConsumer(IOptions kafkaOptions, IImageHostingDbContext dbContext) - : IConsumer> -{ - private readonly double _threshold = kafkaOptions.Value.CategoriesConsumer.Threshold; - - public async Task Consume(ConsumeContext> context) - { - var messages = context.Message - .GroupBy(consumeContext => consumeContext.Message.ImageId, - consumeContext => consumeContext.Message.Categories) - .ToDictionary(grouping => grouping.Key, - grouping => grouping.SelectMany(categories => categories) - .ToDictionary(category => category.Key, category => category.Value)); - - var images = await dbContext.Images - .AsTracking() - .Include(i => i.Tags) - .Where(i => messages.Keys.Contains(i.Id)) - .ToListAsync(context.CancellationToken); - - foreach (var image in images) - { - image.AddTags(messages[image.Id] - .Where(category => category.Value > _threshold) - .Select(category => category.Key)); - } - - await dbContext.SaveChangesAsync(context.CancellationToken); - } -} \ No newline at end of file diff --git a/ImageHosting.Storage/Extensions/DependencyInjection/ServiceCollectionExtensions.cs b/ImageHosting.Storage/Extensions/DependencyInjection/ServiceCollectionExtensions.cs deleted file mode 100644 index 66e3c97..0000000 --- a/ImageHosting.Storage/Extensions/DependencyInjection/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,35 +0,0 @@ -using ImageHosting.Storage.Models; -using ImageHosting.Storage.Services; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace ImageHosting.Storage.Extensions.DependencyInjection; - -public static class ServiceCollectionExtensions -{ - public static IServiceCollection AddMinioServices(this IServiceCollection services) - { - services.AddOptions() - .BindConfiguration(MinioOptions.SectionName) - .ValidateDataAnnotations(); - - services.TryAddSingleton(); - services.TryAddSingleton(sp => sp.GetRequiredService().CreateClient()); - - return services; - } - - public static IServiceCollection AddKafkaOptions(this IServiceCollection services) - { - services.AddOptions() - .BindConfiguration(KafkaOptions.SectionName) - .ValidateDataAnnotations(); - - return services; - } - - public static IServiceCollection AddInitializeUserBucket(this IServiceCollection services) - { - return services.AddScoped(); - } -} diff --git a/ImageHosting.Storage/Features/Images/Models/ImageUploadedResponse.cs b/ImageHosting.Storage/Features/Images/Models/ImageUploadedResponse.cs deleted file mode 100644 index 7df6ae8..0000000 --- a/ImageHosting.Storage/Features/Images/Models/ImageUploadedResponse.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using ImageHosting.Persistence.Entities; -using ImageHosting.Persistence.ValueTypes; - -namespace ImageHosting.Storage.Features.Images.Models; - -public class ImageUploadedResponse( - ImageId id, - UserId userId, - string objectName, - bool hidden, - DateTime uploadedAt) -{ - public ImageId Id { get; } = id; - public UserId UserId { get; } = userId; - public string ObjectName { get; } = objectName; - public bool Hidden { get; } = hidden; - public DateTime UploadedAt { get; } = uploadedAt; - - public static ImageUploadedResponse From(Image image) - { - return new ImageUploadedResponse(image.Id, image.UserId, image.ObjectName, image.Hidden, image.UploadedAt); - } -} \ No newline at end of file diff --git a/ImageHosting.Storage/Features/Images/Models/RemoveFile.cs b/ImageHosting.Storage/Features/Images/Models/RemoveFile.cs deleted file mode 100644 index 6736888..0000000 --- a/ImageHosting.Storage/Features/Images/Models/RemoveFile.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace ImageHosting.Storage.Features.Images.Models; - -public class RemoveFile(string userId, string imageId) -{ - public string UserId { get; } = userId; - public string ImageId { get; } = imageId; -} diff --git a/ImageHosting.Storage/Features/Images/Models/UploadFileDto.cs b/ImageHosting.Storage/Features/Images/Models/UploadFileDto.cs deleted file mode 100644 index 6a34660..0000000 --- a/ImageHosting.Storage/Features/Images/Models/UploadFileDto.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Minio.DataModel.Response; - -namespace ImageHosting.Storage.Features.Images.Models; - -public class UploadFileDto(string? etag, string? objectName, long size) -{ - public string? Etag { get; } = etag; - public string? ObjectName { get; } = objectName; - public long Size { get; } = size; - - public static UploadFileDto From(PutObjectResponse putObjectResponse) - { - return new UploadFileDto(putObjectResponse.Etag[1..^1], putObjectResponse.ObjectName, putObjectResponse.Size); - } -} diff --git a/ImageHosting.Storage/Features/Images/Models/WriteFile.cs b/ImageHosting.Storage/Features/Images/Models/WriteFile.cs deleted file mode 100644 index 6c82210..0000000 --- a/ImageHosting.Storage/Features/Images/Models/WriteFile.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Microsoft.AspNetCore.Http; - -namespace ImageHosting.Storage.Features.Images.Models; - -public class WriteFile(string userId, string imageId, IFormFile file) -{ - public string UserId { get; } = userId; - public string ImageId { get; } = imageId; - public IFormFile File { get; } = file; -} \ No newline at end of file diff --git a/ImageHosting.Storage/Features/Images/Services/FileService.cs b/ImageHosting.Storage/Features/Images/Services/FileService.cs deleted file mode 100644 index 3511094..0000000 --- a/ImageHosting.Storage/Features/Images/Services/FileService.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using ImageHosting.Storage.Features.Images.Exceptions; -using ImageHosting.Storage.Features.Images.Models; -using ImageHosting.Storage.Logging; -using Microsoft.Extensions.Logging; -using Minio; -using Minio.DataModel.Args; - -namespace ImageHosting.Storage.Features.Images.Services; - -public class FileService(IMinioClient minioClient, ILogger logger) : IFileService -{ - public async Task WriteFileAsync(WriteFile writeFile, - CancellationToken cancellationToken = default) - { - var bucketExistsArgs = new BucketExistsArgs().WithBucket(writeFile.UserId); - var foundBucket = - await minioClient.BucketExistsAsync(bucketExistsArgs, cancellationToken).ConfigureAwait(false); - if (!foundBucket) - { - throw new UserBucketDoesNotExistsException(writeFile.UserId); - } - - var statObjectArgs = new StatObjectArgs() - .WithBucket(writeFile.UserId) - .WithObject(writeFile.ImageId); - var objectStat = await minioClient.StatObjectAsync(statObjectArgs, cancellationToken).ConfigureAwait(false); - if (objectStat.Size > 0) - { - throw new ImageObjectAlreadyExistsException(writeFile.UserId, writeFile.ImageId); - } - - var stream = writeFile.File.OpenReadStream(); - - await using (stream.ConfigureAwait(false)) - { - var putObjectArgs = new PutObjectArgs() - .WithBucket(writeFile.UserId) - .WithObjectSize(writeFile.File.Length) - .WithContentType(writeFile.File.ContentType) - .WithStreamData(stream) - .WithObject(writeFile.ImageId); - - var putObjectResponse = - await minioClient.PutObjectAsync(putObjectArgs, cancellationToken).ConfigureAwait(false); - - logger.LogFileWritten(writeFile.ImageId, writeFile.UserId); - return UploadFileDto.From(putObjectResponse); - } - } - - public async Task RemoveFileAsync(RemoveFile removeFile, CancellationToken cancellationToken = default) - { - var bucketExistsArgs = new BucketExistsArgs().WithBucket(removeFile.UserId); - var foundBucket = - await minioClient.BucketExistsAsync(bucketExistsArgs, cancellationToken).ConfigureAwait(false); - if (!foundBucket) - { - throw new UserBucketDoesNotExistsException(removeFile.UserId); - } - - var removeObjectArgs = new RemoveObjectArgs() - .WithBucket(removeFile.UserId) - .WithObject(removeFile.ImageId); - await minioClient.RemoveObjectAsync(removeObjectArgs, cancellationToken).ConfigureAwait(false); - - logger.LogFileRemoved(removeFile.ImageId, removeFile.UserId); - } -} \ No newline at end of file diff --git a/ImageHosting.Storage/Features/Images/Services/FileUploadCommand.cs b/ImageHosting.Storage/Features/Images/Services/FileUploadCommand.cs deleted file mode 100644 index c0dd4f2..0000000 --- a/ImageHosting.Storage/Features/Images/Services/FileUploadCommand.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using ImageHosting.Persistence.ValueTypes; -using ImageHosting.Storage.Features.Images.Models; -using ImageHosting.Storage.Models; -using Microsoft.AspNetCore.Http; - -namespace ImageHosting.Storage.Features.Images.Services; - -public interface IFileUploadCommandFactory -{ - IRollbackCommand CreateCommand(UserId userId, ImageId imageId, IFormFile file); -} - -public class FileUploadCommandFactory(IFileService fileService) : IFileUploadCommandFactory -{ - public IRollbackCommand CreateCommand(UserId userId, ImageId imageId, IFormFile file) - { - return new FileUploadCommand(fileService, userId.ToString("D"), imageId.ToString("D"), file); - } -} - -public class FileUploadCommand(IFileService fileService, string userId, string imageId, IFormFile file) : IRollbackCommand -{ - public Task ExecuteAsync(CancellationToken cancellationToken = default) => - fileService.WriteFileAsync(new WriteFile(userId, imageId, file), cancellationToken); - - public Task RollbackAsync(CancellationToken cancellationToken = default) => - fileService.RemoveFileAsync(new RemoveFile(userId, imageId), cancellationToken); -} diff --git a/ImageHosting.Storage/Features/Images/Services/IFileService.cs b/ImageHosting.Storage/Features/Images/Services/IFileService.cs deleted file mode 100644 index 2dafe8c..0000000 --- a/ImageHosting.Storage/Features/Images/Services/IFileService.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using ImageHosting.Storage.Features.Images.Models; - -namespace ImageHosting.Storage.Features.Images.Services; - -public interface IFileService -{ - Task WriteFileAsync(WriteFile writeFile, CancellationToken cancellationToken = default); - Task RemoveFileAsync(RemoveFile removeFile, CancellationToken cancellationToken = default); -} diff --git a/ImageHosting.Storage/Features/Images/Services/IMetadataService.cs b/ImageHosting.Storage/Features/Images/Services/IMetadataService.cs deleted file mode 100644 index 895c077..0000000 --- a/ImageHosting.Storage/Features/Images/Services/IMetadataService.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using ImageHosting.Persistence.ValueTypes; -using ImageHosting.Storage.Features.Images.Models; - -namespace ImageHosting.Storage.Features.Images.Services; - -public interface IMetadataService -{ - /// - /// Saves metadata. - /// - /// Metadata to write. - /// A to observe while waiting for the task to complete. - /// Created entity. - Task WriteMetadataAsync(ImageMetadata imageMetadata, CancellationToken cancellationToken = default); - - /// - /// Delete metadata. - /// - /// The id of metadata to delete. - /// A to observe while waiting for the task to complete. - /// True if successful deleted. - Task DeleteMetadataAsync(ImageId id, CancellationToken cancellationToken = default); -} diff --git a/ImageHosting.Storage/Features/Images/Services/INewImageProducer.cs b/ImageHosting.Storage/Features/Images/Services/INewImageProducer.cs deleted file mode 100644 index f7ca194..0000000 --- a/ImageHosting.Storage/Features/Images/Services/INewImageProducer.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using ImageHosting.Storage.Features.Images.Models; - -namespace ImageHosting.Storage.Features.Images.Services; - -public interface INewImageProducer -{ - Task SendAsync(NewImage newImage, CancellationToken cancellationToken = default); -} \ No newline at end of file diff --git a/ImageHosting.Storage/Models/ImagesOptions.cs b/ImageHosting.Storage/Models/ImagesOptions.cs deleted file mode 100644 index 37fdc5a..0000000 --- a/ImageHosting.Storage/Models/ImagesOptions.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; - -namespace ImageHosting.Storage.Models; - -public class ImagesOptions -{ - public const string SectionName = "Images"; - - [Required(AllowEmptyStrings = false)] public required Uri BaseUri { get; init; } -} \ No newline at end of file diff --git a/ImageHosting.Storage/Models/KafkaOptions.cs b/ImageHosting.Storage/Models/KafkaOptions.cs deleted file mode 100644 index 732d30e..0000000 --- a/ImageHosting.Storage/Models/KafkaOptions.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace ImageHosting.Storage.Models; - -public class KafkaOptions -{ - public const string SectionName = "Kafka"; - - [Required] public required string[] BootstrapServers { get; init; } - [Required] public required TopicOptions NewImagesProducer { get; init; } - [Required] public required CategoriesTopicConsumerOptions CategoriesConsumer { get; init; } -} - -public class TopicOptions -{ - [Required(AllowEmptyStrings = false)] public required string TopicName { get; init; } -} - -public class CategoriesTopicConsumerOptions : TopicOptions -{ - [Required(AllowEmptyStrings = false)] public required string GroupId { get; init; } - [Required] public required double Threshold { get; init; } -} \ No newline at end of file diff --git a/ImageHosting.Storage/Properties/launchSettings.json b/ImageHosting.Storage/Properties/launchSettings.json deleted file mode 100644 index ad61805..0000000 --- a/ImageHosting.Storage/Properties/launchSettings.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/launchsettings.json", - "profiles": { - "http": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "launchUrl": "swagger", - "applicationUrl": "http://localhost:8080", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } - } -} diff --git a/ImageHosting.sln b/ImageHosting.sln index a428a34..8adf45d 100644 --- a/ImageHosting.sln +++ b/ImageHosting.sln @@ -1,23 +1,25 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageHosting.Storage", "ImageHosting.Storage\ImageHosting.Storage.csproj", "{39C30E75-601C-404C-BC00-4BAD9A926C35}" +# Visual Studio Version 17 +VisualStudioVersion = 17.11.35312.102 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{1C68230E-D545-4C68-BA8B-22C4477501AD}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{2E3ED63C-82E4-4F1D-8053-201A78C2DE9B}" - ProjectSection(SolutionItems) = preProject - docker-compose.yml = docker-compose.yml - .config\dotnet-tools.json = .config\dotnet-tools.json - .github\workflows\publish-container.yml = .github\workflows\publish-container.yml - .github\CODEOWNERS = .github\CODEOWNERS - .dockerignore = .dockerignore - Dockerfile = Dockerfile - .github\workflows\codecov.yml = .github\workflows\codecov.yml - global.json = global.json - .gitignore = .gitignore - EndProjectSection +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageHosting.Storage.Application", "src\ImageHosting.Storage.Application\ImageHosting.Storage.Application.csproj", "{005586CB-F9A6-469C-B436-2228A8B622AD}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageHosting.Storage.UnitTests", "ImageHosting.Storage.UnitTests\ImageHosting.Storage.UnitTests.csproj", "{56562A88-B4D7-43FD-948C-927A1DA6DDB9}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageHosting.Storage.Domain", "src\ImageHosting.Storage.Domain\ImageHosting.Storage.Domain.csproj", "{7255725A-9A01-4C84-B49D-FD5620802000}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageHosting.Persistence", "ImageHosting.Persistence\ImageHosting.Persistence.csproj", "{4D2A4C6C-755A-42AA-8366-C8B39564DA37}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageHosting.Storage.WebApi", "src\ImageHosting.Storage.WebApi\ImageHosting.Storage.WebApi.csproj", "{2ED37173-F78E-4BA0-8B13-BA053C6A1D8D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{A0656636-411F-44D8-A5C1-6BB3F4119A87}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageHosting.Storage.Infrastructure", "src\ImageHosting.Storage.Infrastructure\ImageHosting.Storage.Infrastructure.csproj", "{ECDE7380-A8D3-4668-92A7-20FBECC102D1}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageHosting.Storage.UnitTests", "tests\ImageHosting.Storage.UnitTests\ImageHosting.Storage.UnitTests.csproj", "{D4D379BB-8C73-47FA-87D8-44F90F05DF37}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageHosting.Storage.Tagger", "src\ImageHosting.Storage.Tagger\ImageHosting.Storage.Tagger.csproj", "{93B10A38-1C7E-4F66-8447-ED42FEAE7352}" +EndProject +Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{3E719B58-4503-4001-A20D-57698BB768DB}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -25,17 +27,47 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {39C30E75-601C-404C-BC00-4BAD9A926C35}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {39C30E75-601C-404C-BC00-4BAD9A926C35}.Debug|Any CPU.Build.0 = Debug|Any CPU - {39C30E75-601C-404C-BC00-4BAD9A926C35}.Release|Any CPU.ActiveCfg = Release|Any CPU - {39C30E75-601C-404C-BC00-4BAD9A926C35}.Release|Any CPU.Build.0 = Release|Any CPU - {56562A88-B4D7-43FD-948C-927A1DA6DDB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {56562A88-B4D7-43FD-948C-927A1DA6DDB9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {56562A88-B4D7-43FD-948C-927A1DA6DDB9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {56562A88-B4D7-43FD-948C-927A1DA6DDB9}.Release|Any CPU.Build.0 = Release|Any CPU - {4D2A4C6C-755A-42AA-8366-C8B39564DA37}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4D2A4C6C-755A-42AA-8366-C8B39564DA37}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4D2A4C6C-755A-42AA-8366-C8B39564DA37}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4D2A4C6C-755A-42AA-8366-C8B39564DA37}.Release|Any CPU.Build.0 = Release|Any CPU + {005586CB-F9A6-469C-B436-2228A8B622AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {005586CB-F9A6-469C-B436-2228A8B622AD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {005586CB-F9A6-469C-B436-2228A8B622AD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {005586CB-F9A6-469C-B436-2228A8B622AD}.Release|Any CPU.Build.0 = Release|Any CPU + {7255725A-9A01-4C84-B49D-FD5620802000}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7255725A-9A01-4C84-B49D-FD5620802000}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7255725A-9A01-4C84-B49D-FD5620802000}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7255725A-9A01-4C84-B49D-FD5620802000}.Release|Any CPU.Build.0 = Release|Any CPU + {2ED37173-F78E-4BA0-8B13-BA053C6A1D8D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2ED37173-F78E-4BA0-8B13-BA053C6A1D8D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2ED37173-F78E-4BA0-8B13-BA053C6A1D8D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2ED37173-F78E-4BA0-8B13-BA053C6A1D8D}.Release|Any CPU.Build.0 = Release|Any CPU + {ECDE7380-A8D3-4668-92A7-20FBECC102D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ECDE7380-A8D3-4668-92A7-20FBECC102D1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ECDE7380-A8D3-4668-92A7-20FBECC102D1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ECDE7380-A8D3-4668-92A7-20FBECC102D1}.Release|Any CPU.Build.0 = Release|Any CPU + {D4D379BB-8C73-47FA-87D8-44F90F05DF37}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D4D379BB-8C73-47FA-87D8-44F90F05DF37}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D4D379BB-8C73-47FA-87D8-44F90F05DF37}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D4D379BB-8C73-47FA-87D8-44F90F05DF37}.Release|Any CPU.Build.0 = Release|Any CPU + {93B10A38-1C7E-4F66-8447-ED42FEAE7352}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {93B10A38-1C7E-4F66-8447-ED42FEAE7352}.Debug|Any CPU.Build.0 = Debug|Any CPU + {93B10A38-1C7E-4F66-8447-ED42FEAE7352}.Release|Any CPU.ActiveCfg = Release|Any CPU + {93B10A38-1C7E-4F66-8447-ED42FEAE7352}.Release|Any CPU.Build.0 = Release|Any CPU + {3E719B58-4503-4001-A20D-57698BB768DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3E719B58-4503-4001-A20D-57698BB768DB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3E719B58-4503-4001-A20D-57698BB768DB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3E719B58-4503-4001-A20D-57698BB768DB}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {005586CB-F9A6-469C-B436-2228A8B622AD} = {1C68230E-D545-4C68-BA8B-22C4477501AD} + {7255725A-9A01-4C84-B49D-FD5620802000} = {1C68230E-D545-4C68-BA8B-22C4477501AD} + {2ED37173-F78E-4BA0-8B13-BA053C6A1D8D} = {1C68230E-D545-4C68-BA8B-22C4477501AD} + {ECDE7380-A8D3-4668-92A7-20FBECC102D1} = {1C68230E-D545-4C68-BA8B-22C4477501AD} + {D4D379BB-8C73-47FA-87D8-44F90F05DF37} = {A0656636-411F-44D8-A5C1-6BB3F4119A87} + {93B10A38-1C7E-4F66-8447-ED42FEAE7352} = {1C68230E-D545-4C68-BA8B-22C4477501AD} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B7A8C6A9-84B2-4C15-8C65-4C81799CC918} EndGlobalSection EndGlobal diff --git a/docker-compose.dcproj b/docker-compose.dcproj new file mode 100644 index 0000000..745dab8 --- /dev/null +++ b/docker-compose.dcproj @@ -0,0 +1,19 @@ + + + + 2.1 + Linux + False + 3e719b58-4503-4001-a20d-57698bb768db + LaunchBrowser + {Scheme}://localhost:{ServicePort} + imagehosting.storage.webapi + + + + docker-compose.yml + + + + + \ No newline at end of file diff --git a/docker-compose.override.yml b/docker-compose.override.yml new file mode 100644 index 0000000..6d03358 --- /dev/null +++ b/docker-compose.override.yml @@ -0,0 +1,45 @@ +services: + storage-webapi: + environment: + ASPNETCORE_ENVIRONMENT: Development + ASPNETCORE_HTTP_PORTS: 8080 + ConnectionStrings__ImageHosting: "Host=postgres;Port=5432;Database=image_hosting;Username=image_hosting;Password=P@ssw0rd;Include Error Detail=true" + Minio__AccessKey: minioadmin + Minio__SecretKey: minioadmin + Minio__Secure: false + ports: + - "127.0.0.1:8080:8080" + + image-tagger: + environment: + DOTNET_ENVIRONMENT: Development + ConnectionStrings__ImageHosting: "Host=postgres;Port=5432;Database=image_hosting;Username=image_hosting;Password=P@ssw0rd;Include Error Detail=true" + + minio: + environment: + MINIO_ROOT_USER: minioadmin + MINIO_ROOT_PASSWORD: minioadmin + + postgres: + environment: + POSTGRES_DB: image_hosting + POSTGRES_USER: image_hosting + POSTGRES_PASSWORD: P@ssw0rd + + kafka-ui: + ports: + - "127.0.0.1:8000:8000" + environment: + SERVER_PORT: 8000 + + resizer: + environment: + MINIO_USER: minioadmin + MINIO_PASSWORD: minioadmin + + recognizer: + environment: + MINIO_USER: minioadmin + MINIO_PASSWORD: minioadmin + DEBUG_CATEGORIES: true + diff --git a/docker-compose.yml b/docker-compose.yml index 2f5fd65..77db7b9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,30 +1,20 @@ -services: - image-hosting-storage: - container_name: image-hosting-storage - image: ghcr.io/baklanov-soft/image-hosting-storage:master +services: + storage-webapi: + container_name: storage-webapi + image: ${DOCKER_REGISTRY-}image-hosting-storage-webapi build: context: . - dockerfile: Dockerfile + dockerfile: src/ImageHosting.Storage.WebApi/Dockerfile environment: Serilog__Using__0: "Serilog.Sinks.Console" Serilog__MinimumLevel__Default: Information Serilog__MinimumLevel__Override__Microsoft.AspNetCore: Warning Serilog__MinimumLevel__Override__Microsoft.EntityFrameworkCore: Warning Serilog__WriteTo__0__Name: Console - Minio__Endpoint: minio:9000 - Minio__AccessKey: minioadmin - Minio__SecretKey: minioadmin - Minio__Secure: false - ConnectionStrings__ImageHosting: "Host=postgres;Port=5432;Database=image_hosting;Username=image_hosting;Password=P@ssw0rd;Include Error Detail=true" - ASPNETCORE_ENVIRONMENT: Development Kafka__BootstrapServers__0: kafka:9092 Kafka__NewImagesProducer__TopicName: new-images.v1 - Kafka__CategoriesConsumer__TopicName: categories.v1 - Kafka__CategoriesConsumer__GroupId: image-tagger - Kafka__CategoriesConsumer__Threshold: 0.8 - Images__BaseUri: "localhost:8080" - ports: - - "127.0.0.1:8080:8080" + Minio__Endpoint: minio:9000 + Images__BaseUri: "http://localhost:8080" depends_on: - minio - postgres @@ -32,35 +22,46 @@ - kafka-init-new-images - kafka-init-categories + image-tagger: + container_name: image-tagger + image: ${DOCKER_REGISTRY-}image-hosting-storage-tagger + build: + context: . + dockerfile: src/ImageHosting.Storage.Tagger/Dockerfile + environment: + Logging__LogLevel__Default: Information + Logging__LogLevel__Microsoft.AspNetCore: Warning + Logging__LogLevel__Microsoft.EntityFrameworkCore: Warning + AssignTags__Threshold: 0.8 + AssignTagsConsumer__BootstrapServers__0: kafka:9092 + AssignTagsConsumer__TopicName: "categories.v1" + AssignTagsConsumer__GroupId: image-tagger + depends_on: + - postgres + - kafka + - kafka-init-categories + minio: container_name: minio - image: quay.io/minio/minio:RELEASE.2024-01-31T20-20-33Z - command: server --console-address ":9001" /data + image: quay.io/minio/minio:RELEASE.2024-09-13T20-26-02Z ports: - "127.0.0.1:9000:9000" - "127.0.0.1:9001:9001" + command: server --console-address ":9001" /data volumes: - minio-data:/data - environment: - MINIO_ROOT_USER: minioadmin - MINIO_ROOT_PASSWORD: minioadmin postgres: container_name: postgres - image: postgres:16-alpine3.18 - volumes: - - db-data:/var/lib/postgresql/data - environment: - POSTGRES_DB: image_hosting - POSTGRES_USER: image_hosting - POSTGRES_PASSWORD: P@ssw0rd + image: postgres:16.4-alpine3.20 ports: - "127.0.0.1:5432:5432" - command: [ "postgres", "-c", "log_statement=all" ] + volumes: + - db-data:/var/lib/postgresql/data kafka: container_name: kafka - image: bitnami/kafka:3.6.1 + image: bitnami/kafka:3.8 ports: - "127.0.0.1:9094:9094" environment: @@ -69,7 +70,6 @@ KAFKA_CFG_ADVERTISED_LISTENERS: INTERNAL://kafka:9092,EXTERNAL://localhost:9094 KAFKA_CFG_INTER_BROKER_LISTENER_NAME: INTERNAL KAFKA_CFG_CONTROLLER_LISTENER_NAMES: CONTROLLER - # cluster config KAFKA_KRAFT_CLUSTER_ID: LelM2dIFQkiUFvXCEcqRWA KAFKA_CFG_NODE_ID: 0 KAFKA_CFG_PROCESS_ROLES: controller,broker @@ -79,7 +79,7 @@ kafka-init-new-images: container_name: kafka-init-new-images - image: bitnami/kafka:3.6.1 + image: bitnami/kafka:3.8 depends_on: - kafka entrypoint: "kafka-topics.sh" @@ -93,7 +93,7 @@ kafka-init-categories: container_name: kafka-init-categories - image: bitnami/kafka:3.6.1 + image: bitnami/kafka:3.8 depends_on: - kafka entrypoint: "kafka-topics.sh" @@ -104,22 +104,19 @@ "--partitions", "2", "--replication-factor", "1" ] - + kafka-ui: - image: provectuslabs/kafka-ui:v0.7.2 container_name: kafka-ui - ports: - - "127.0.0.1:8000:8000" + image: provectuslabs/kafka-ui:v0.7.2 + depends_on: + - kafka environment: - SERVER_PORT: 8000 KAFKA_CLUSTERS_0_NAME: image-hosting KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:9092 - depends_on: - - kafka - + resizer: - image: ghcr.io/baklanov-soft/image-hosting-processing-resizer:master container_name: resizer + image: ghcr.io/baklanov-soft/image-hosting-processing-resizer:master depends_on: - kafka-init-new-images - minio @@ -127,12 +124,10 @@ KAFKA_BOOTSTRAP_SERVERS: kafka:9092 CONSUMER_GROUP_ID: resizer-local-test MINIO_HOST: "http://minio:9000" - MINIO_USER: minioadmin - MINIO_PASSWORD: minioadmin - + recognizer: - image: ghcr.io/baklanov-soft/image-hosting-processing-recognizer:master container_name: recognizer + image: ghcr.io/baklanov-soft/image-hosting-processing-recognizer:master depends_on: - kafka-init-new-images - kafka-init-categories @@ -151,9 +146,7 @@ NSFW_SYNSET_PATH: "nsfw/synset.txt" NSFW_MODEL_PATH: "nsfw/nsfw_model.pt" MINIO_HOST: "http://minio:9000" - MINIO_USER: minioadmin - MINIO_PASSWORD: minioadmin - DEBUG_CATEGORIES: true + volumes: minio-data: diff --git a/global.json b/global.json deleted file mode 100644 index b5b37b6..0000000 --- a/global.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "sdk": { - "version": "8.0.0", - "rollForward": "latestMajor", - "allowPrerelease": false - } -} \ No newline at end of file diff --git a/launchSettings.json b/launchSettings.json new file mode 100644 index 0000000..1dfe15b --- /dev/null +++ b/launchSettings.json @@ -0,0 +1,23 @@ +{ + "profiles": { + "Docker Compose": { + "commandName": "DockerCompose", + "commandVersion": "1.0", + "composeLaunchAction": "LaunchBrowser", + "composeLaunchServiceName": "storage-webapi", + "composeLaunchUrl": "{Scheme}://localhost:{ServicePort}", + "serviceActions": { + "kafka-init-categories": "StartWithoutDebugging", + "kafka": "StartWithoutDebugging", + "kafka-init-new-images": "StartWithoutDebugging", + "kafka-ui": "StartWithoutDebugging", + "minio": "StartWithoutDebugging", + "postgres": "StartWithoutDebugging", + "resizer": "StartWithoutDebugging", + "image-tagger": "StartDebugging", + "recognizer": "StartWithoutDebugging", + "storage-webapi": "StartDebugging" + } + } + } +} \ No newline at end of file diff --git a/ImageHosting.Storage/Features/Images/Models/ImageMetadata.cs b/src/ImageHosting.Storage.Application/DTOs/ImageMetadataDTO.cs similarity index 59% rename from ImageHosting.Storage/Features/Images/Models/ImageMetadata.cs rename to src/ImageHosting.Storage.Application/DTOs/ImageMetadataDTO.cs index 0ea17d3..f7b6a16 100644 --- a/ImageHosting.Storage/Features/Images/Models/ImageMetadata.cs +++ b/src/ImageHosting.Storage.Application/DTOs/ImageMetadataDTO.cs @@ -1,17 +1,16 @@ -using System; -using ImageHosting.Persistence.Entities; -using ImageHosting.Persistence.ValueTypes; +using ImageHosting.Storage.Domain.Entities; +using ImageHosting.Storage.Domain.ValueTypes; -namespace ImageHosting.Storage.Features.Images.Models; +namespace ImageHosting.Storage.Application.DTOs; -public class ImageMetadata( +public class ImageMetadataDTO( ImageId id, string objectName, UserId userId, DateTime uploadedAt, bool hidden) { - internal Image ToEntity() + public Image ToEntity() { return new Image { diff --git a/src/ImageHosting.Storage.Application/DTOs/ImageUploadedDTO.cs b/src/ImageHosting.Storage.Application/DTOs/ImageUploadedDTO.cs new file mode 100644 index 0000000..6a511d5 --- /dev/null +++ b/src/ImageHosting.Storage.Application/DTOs/ImageUploadedDTO.cs @@ -0,0 +1,23 @@ +using ImageHosting.Storage.Domain.Entities; +using ImageHosting.Storage.Domain.ValueTypes; + +namespace ImageHosting.Storage.Application.DTOs; + +public class ImageUploadedDTO( + ImageId id, + UserId userId, + string objectName, + bool hidden, + DateTime uploadedAt) +{ + public ImageId Id { get; } = id; + public UserId UserId { get; } = userId; + public string ObjectName { get; } = objectName; + public bool Hidden { get; } = hidden; + public DateTime UploadedAt { get; } = uploadedAt; + + public static ImageUploadedDTO From(Image image) + { + return new ImageUploadedDTO(image.Id, image.UserId, image.ObjectName, image.Hidden, image.UploadedAt); + } +} \ No newline at end of file diff --git a/src/ImageHosting.Storage.Application/DTOs/RemoveFileDTO.cs b/src/ImageHosting.Storage.Application/DTOs/RemoveFileDTO.cs new file mode 100644 index 0000000..274aa3d --- /dev/null +++ b/src/ImageHosting.Storage.Application/DTOs/RemoveFileDTO.cs @@ -0,0 +1,7 @@ +namespace ImageHosting.Storage.Application.DTOs; + +public class RemoveFileDTO(string userId, string imageId) +{ + public string UserId { get; } = userId; + public string ImageId { get; } = imageId; +} diff --git a/src/ImageHosting.Storage.Application/DTOs/UploadFileDTO.cs b/src/ImageHosting.Storage.Application/DTOs/UploadFileDTO.cs new file mode 100644 index 0000000..ea46c62 --- /dev/null +++ b/src/ImageHosting.Storage.Application/DTOs/UploadFileDTO.cs @@ -0,0 +1,8 @@ +namespace ImageHosting.Storage.Application.DTOs; + +public class UploadFileDTO(string? etag, string? objectName, long size) +{ + public string? Etag { get; } = etag; + public string? ObjectName { get; } = objectName; + public long Size { get; } = size; +} diff --git a/src/ImageHosting.Storage.Application/DTOs/WriteFileDTO.cs b/src/ImageHosting.Storage.Application/DTOs/WriteFileDTO.cs new file mode 100644 index 0000000..fd952ae --- /dev/null +++ b/src/ImageHosting.Storage.Application/DTOs/WriteFileDTO.cs @@ -0,0 +1,10 @@ +namespace ImageHosting.Storage.Application.DTOs; + +public class WriteFileDTO(string userId, string imageId, long length, string contentType, Stream stream) +{ + public string UserId { get; } = userId; + public string ImageId { get; } = imageId; + public long Length { get; } = length; + public string ContentType { get; } = contentType; + public Stream Stream { get; } = stream; +} \ No newline at end of file diff --git a/ImageHosting.Storage/Features/Images/Exceptions/ImageObjectAlreadyExistsException.cs b/src/ImageHosting.Storage.Application/Exceptions/ImageObjectAlreadyExistsException.cs similarity index 77% rename from ImageHosting.Storage/Features/Images/Exceptions/ImageObjectAlreadyExistsException.cs rename to src/ImageHosting.Storage.Application/Exceptions/ImageObjectAlreadyExistsException.cs index e793818..5111355 100644 --- a/ImageHosting.Storage/Features/Images/Exceptions/ImageObjectAlreadyExistsException.cs +++ b/src/ImageHosting.Storage.Application/Exceptions/ImageObjectAlreadyExistsException.cs @@ -1,6 +1,4 @@ -using System; - -namespace ImageHosting.Storage.Features.Images.Exceptions; +namespace ImageHosting.Storage.Application.Exceptions; public class ImageObjectAlreadyExistsException(string bucketName, string imageId) : Exception($"Image id {imageId} already exists in bucket {bucketName}.") diff --git a/ImageHosting.Storage/Features/Images/Exceptions/UserBucketDoesNotExistsException.cs b/src/ImageHosting.Storage.Application/Exceptions/UserBucketDoesNotExistsException.cs similarity index 69% rename from ImageHosting.Storage/Features/Images/Exceptions/UserBucketDoesNotExistsException.cs rename to src/ImageHosting.Storage.Application/Exceptions/UserBucketDoesNotExistsException.cs index e99ca94..b9e185c 100644 --- a/ImageHosting.Storage/Features/Images/Exceptions/UserBucketDoesNotExistsException.cs +++ b/src/ImageHosting.Storage.Application/Exceptions/UserBucketDoesNotExistsException.cs @@ -1,6 +1,4 @@ -using System; - -namespace ImageHosting.Storage.Features.Images.Exceptions; +namespace ImageHosting.Storage.Application.Exceptions; public class UserBucketDoesNotExistsException(string userId) : Exception($"Bucket with id {userId} does not exists.") { diff --git a/ImageHosting.Persistence/ImageHosting.Persistence.csproj b/src/ImageHosting.Storage.Application/ImageHosting.Storage.Application.csproj similarity index 54% rename from ImageHosting.Persistence/ImageHosting.Persistence.csproj rename to src/ImageHosting.Storage.Application/ImageHosting.Storage.Application.csproj index 9563a4a..dd3d759 100644 --- a/ImageHosting.Persistence/ImageHosting.Persistence.csproj +++ b/src/ImageHosting.Storage.Application/ImageHosting.Storage.Application.csproj @@ -2,14 +2,13 @@ net8.0 + latest enable enable - default - - + diff --git a/src/ImageHosting.Storage.Application/Services/IAssignTagsService.cs b/src/ImageHosting.Storage.Application/Services/IAssignTagsService.cs new file mode 100644 index 0000000..cef3a8f --- /dev/null +++ b/src/ImageHosting.Storage.Application/Services/IAssignTagsService.cs @@ -0,0 +1,9 @@ +using ImageHosting.Storage.Domain.ValueTypes; + +namespace ImageHosting.Storage.Application.Services; + +public interface IAssignTagsService +{ + Task AssignTagsAsync(Dictionary> categories, + CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/ImageHosting.Storage.Application/Services/IFileService.cs b/src/ImageHosting.Storage.Application/Services/IFileService.cs new file mode 100644 index 0000000..eb7ef49 --- /dev/null +++ b/src/ImageHosting.Storage.Application/Services/IFileService.cs @@ -0,0 +1,9 @@ +using ImageHosting.Storage.Application.DTOs; + +namespace ImageHosting.Storage.Application.Services; + +public interface IFileService +{ + Task WriteFileAsync(WriteFileDTO writeFileDto, CancellationToken cancellationToken = default); + Task RemoveFileAsync(RemoveFileDTO removeFileDto, CancellationToken cancellationToken = default); +} diff --git a/ImageHosting.Storage/Services/IInitializeUserBucket.cs b/src/ImageHosting.Storage.Application/Services/IInitializeUserBucket.cs similarity index 55% rename from ImageHosting.Storage/Services/IInitializeUserBucket.cs rename to src/ImageHosting.Storage.Application/Services/IInitializeUserBucket.cs index 63e86b4..09ccabe 100644 --- a/ImageHosting.Storage/Services/IInitializeUserBucket.cs +++ b/src/ImageHosting.Storage.Application/Services/IInitializeUserBucket.cs @@ -1,7 +1,4 @@ -using System.Threading; -using System.Threading.Tasks; - -namespace ImageHosting.Storage.Services; +namespace ImageHosting.Storage.Application.Services; public interface IInitializeUserBucket { diff --git a/src/ImageHosting.Storage.Application/Services/IMetadataService.cs b/src/ImageHosting.Storage.Application/Services/IMetadataService.cs new file mode 100644 index 0000000..e572beb --- /dev/null +++ b/src/ImageHosting.Storage.Application/Services/IMetadataService.cs @@ -0,0 +1,12 @@ +using ImageHosting.Storage.Application.DTOs; +using ImageHosting.Storage.Domain.ValueTypes; + +namespace ImageHosting.Storage.Application.Services; + +public interface IMetadataService +{ + Task WriteMetadataAsync(ImageMetadataDTO imageMetadataDto, + CancellationToken cancellationToken = default); + + Task DeleteMetadataAsync(ImageId id, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/ImageHosting.Storage.Application/Services/INewImageProducer.cs b/src/ImageHosting.Storage.Application/Services/INewImageProducer.cs new file mode 100644 index 0000000..1e06671 --- /dev/null +++ b/src/ImageHosting.Storage.Application/Services/INewImageProducer.cs @@ -0,0 +1,8 @@ +using ImageHosting.Storage.Domain.Messages; + +namespace ImageHosting.Storage.Application.Services; + +public interface INewImageProducer +{ + Task SendAsync(NewImage newImage, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/ImageHosting.Storage/Models/IRollbackCommand.cs b/src/ImageHosting.Storage.Domain/Common/IRollbackCommand.cs similarity index 65% rename from ImageHosting.Storage/Models/IRollbackCommand.cs rename to src/ImageHosting.Storage.Domain/Common/IRollbackCommand.cs index 704d212..1796acd 100644 --- a/ImageHosting.Storage/Models/IRollbackCommand.cs +++ b/src/ImageHosting.Storage.Domain/Common/IRollbackCommand.cs @@ -1,7 +1,4 @@ -using System.Threading; -using System.Threading.Tasks; - -namespace ImageHosting.Storage.Models; +namespace ImageHosting.Storage.Domain.Common; public interface IRollbackCommand { diff --git a/ImageHosting.Storage/Models/RollbackCommands.cs b/src/ImageHosting.Storage.Domain/Common/RollbackCommands.cs similarity index 82% rename from ImageHosting.Storage/Models/RollbackCommands.cs rename to src/ImageHosting.Storage.Domain/Common/RollbackCommands.cs index 53b59d2..6c88e7f 100644 --- a/ImageHosting.Storage/Models/RollbackCommands.cs +++ b/src/ImageHosting.Storage.Domain/Common/RollbackCommands.cs @@ -1,11 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using CommunityToolkit.Diagnostics; -using ImageHosting.Storage.Exceptions; +using ImageHosting.Storage.Domain.Exceptions; -namespace ImageHosting.Storage.Models; +namespace ImageHosting.Storage.Domain.Common; public class RollbackCommands { @@ -26,8 +21,6 @@ public void AddRange(IEnumerable rollbackCommands) public async Task ExecuteAsync(CancellationToken cancellationToken = default) { - Guard.HasSizeGreaterThan(_commands, 1); - var i = 0; try { diff --git a/src/ImageHosting.Storage.Domain/Entities/ForbiddenCategory.cs b/src/ImageHosting.Storage.Domain/Entities/ForbiddenCategory.cs new file mode 100644 index 0000000..9ee1de8 --- /dev/null +++ b/src/ImageHosting.Storage.Domain/Entities/ForbiddenCategory.cs @@ -0,0 +1,6 @@ +namespace ImageHosting.Storage.Domain.Entities; + +public class ForbiddenCategory +{ + public required string Name { get; set; } +} diff --git a/ImageHosting.Persistence/Entities/Image.cs b/src/ImageHosting.Storage.Domain/Entities/Image.cs similarity index 70% rename from ImageHosting.Persistence/Entities/Image.cs rename to src/ImageHosting.Storage.Domain/Entities/Image.cs index 02b94e4..2c871af 100644 --- a/ImageHosting.Persistence/Entities/Image.cs +++ b/src/ImageHosting.Storage.Domain/Entities/Image.cs @@ -1,10 +1,7 @@ -using ImageHosting.Persistence.Entities.Configuration; -using ImageHosting.Persistence.ValueTypes; -using Microsoft.EntityFrameworkCore; +using ImageHosting.Storage.Domain.ValueTypes; -namespace ImageHosting.Persistence.Entities; +namespace ImageHosting.Storage.Domain.Entities; -[EntityTypeConfiguration(typeof(ImageConfiguration))] public class Image { public ImageId Id { get; set; } diff --git a/ImageHosting.Persistence/Entities/ImageTag.cs b/src/ImageHosting.Storage.Domain/Entities/ImageTag.cs similarity index 76% rename from ImageHosting.Persistence/Entities/ImageTag.cs rename to src/ImageHosting.Storage.Domain/Entities/ImageTag.cs index fe9f9d6..d1864c4 100644 --- a/ImageHosting.Persistence/Entities/ImageTag.cs +++ b/src/ImageHosting.Storage.Domain/Entities/ImageTag.cs @@ -1,10 +1,7 @@ -using ImageHosting.Persistence.Entities.Configuration; -using ImageHosting.Persistence.ValueTypes; -using Microsoft.EntityFrameworkCore; +using ImageHosting.Storage.Domain.ValueTypes; -namespace ImageHosting.Persistence.Entities; +namespace ImageHosting.Storage.Domain.Entities; -[EntityTypeConfiguration(typeof(ImageTagConfiguration))] public class ImageTag : IEquatable { public required Image Image { get; init; } diff --git a/ImageHosting.Storage/Exceptions/RollbackCommandsException.cs b/src/ImageHosting.Storage.Domain/Exceptions/RollbackCommandsException.cs similarity index 83% rename from ImageHosting.Storage/Exceptions/RollbackCommandsException.cs rename to src/ImageHosting.Storage.Domain/Exceptions/RollbackCommandsException.cs index 1540732..2f3c379 100644 --- a/ImageHosting.Storage/Exceptions/RollbackCommandsException.cs +++ b/src/ImageHosting.Storage.Domain/Exceptions/RollbackCommandsException.cs @@ -1,6 +1,4 @@ -using System; - -namespace ImageHosting.Storage.Exceptions; +namespace ImageHosting.Storage.Domain.Exceptions; public class RollbackCommandsException : Exception { diff --git a/src/ImageHosting.Storage.Domain/ImageHosting.Storage.Domain.csproj b/src/ImageHosting.Storage.Domain/ImageHosting.Storage.Domain.csproj new file mode 100644 index 0000000..3a63532 --- /dev/null +++ b/src/ImageHosting.Storage.Domain/ImageHosting.Storage.Domain.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/ImageHosting.Storage/Features/Images/Models/CategorizedNewImage.cs b/src/ImageHosting.Storage.Domain/Messages/CategorizedNewImage.cs similarity index 66% rename from ImageHosting.Storage/Features/Images/Models/CategorizedNewImage.cs rename to src/ImageHosting.Storage.Domain/Messages/CategorizedNewImage.cs index e1f3167..3bda9a0 100644 --- a/ImageHosting.Storage/Features/Images/Models/CategorizedNewImage.cs +++ b/src/ImageHosting.Storage.Domain/Messages/CategorizedNewImage.cs @@ -1,8 +1,7 @@ -using System.Collections.Generic; using System.Text.Json.Serialization; -using ImageHosting.Persistence.ValueTypes; +using ImageHosting.Storage.Domain.ValueTypes; -namespace ImageHosting.Storage.Features.Images.Models; +namespace ImageHosting.Storage.Domain.Messages; public class CategorizedNewImage { diff --git a/ImageHosting.Storage/Features/Images/Models/NewImage.cs b/src/ImageHosting.Storage.Domain/Messages/NewImage.cs similarity index 70% rename from ImageHosting.Storage/Features/Images/Models/NewImage.cs rename to src/ImageHosting.Storage.Domain/Messages/NewImage.cs index 2c92733..ac10b04 100644 --- a/ImageHosting.Storage/Features/Images/Models/NewImage.cs +++ b/src/ImageHosting.Storage.Domain/Messages/NewImage.cs @@ -1,7 +1,7 @@ using System.Text.Json.Serialization; -using ImageHosting.Persistence.ValueTypes; +using ImageHosting.Storage.Domain.ValueTypes; -namespace ImageHosting.Storage.Features.Images.Models; +namespace ImageHosting.Storage.Domain.Messages; public class NewImage { diff --git a/ImageHosting.Persistence/ValueTypes/ImageId.cs b/src/ImageHosting.Storage.Domain/ValueTypes/ImageId.cs similarity index 87% rename from ImageHosting.Persistence/ValueTypes/ImageId.cs rename to src/ImageHosting.Storage.Domain/ValueTypes/ImageId.cs index c1b06fb..407ec11 100644 --- a/ImageHosting.Persistence/ValueTypes/ImageId.cs +++ b/src/ImageHosting.Storage.Domain/ValueTypes/ImageId.cs @@ -1,9 +1,8 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace ImageHosting.Persistence.ValueTypes; +namespace ImageHosting.Storage.Domain.ValueTypes; [JsonConverter(typeof(JsonConverter))] public readonly record struct ImageId(Guid Id) : IParsable @@ -46,8 +45,6 @@ public static bool TryParse(string? s, IFormatProvider? provider, out ImageId re return false; } - public class ValueConverter() : ValueConverter(userId => userId.Id, guid => new ImageId(guid)); - private class JsonConverter : JsonConverter { public override ImageId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) diff --git a/ImageHosting.Persistence/ValueTypes/UserId.cs b/src/ImageHosting.Storage.Domain/ValueTypes/UserId.cs similarity index 82% rename from ImageHosting.Persistence/ValueTypes/UserId.cs rename to src/ImageHosting.Storage.Domain/ValueTypes/UserId.cs index 02ba80f..0dfe87e 100644 --- a/ImageHosting.Persistence/ValueTypes/UserId.cs +++ b/src/ImageHosting.Storage.Domain/ValueTypes/UserId.cs @@ -1,9 +1,8 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace ImageHosting.Persistence.ValueTypes; +namespace ImageHosting.Storage.Domain.ValueTypes; [JsonConverter(typeof(JsonConverter))] public readonly record struct UserId(Guid Id) @@ -26,8 +25,6 @@ public string ToString([StringSyntax("GuidFormat")] string? format) return Id.ToString(format); } - public class ValueConverter() : ValueConverter(userId => userId.Id, guid => new UserId(guid)); - private class JsonConverter : JsonConverter { public override UserId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) diff --git a/ImageHosting.Persistence/Entities/Configuration/ForbiddenCategoryConfiguration.cs b/src/ImageHosting.Storage.Infrastructure/Configuration/ForbiddenCategoryConfiguration.cs similarity index 77% rename from ImageHosting.Persistence/Entities/Configuration/ForbiddenCategoryConfiguration.cs rename to src/ImageHosting.Storage.Infrastructure/Configuration/ForbiddenCategoryConfiguration.cs index c165d53..f2ddd19 100644 --- a/ImageHosting.Persistence/Entities/Configuration/ForbiddenCategoryConfiguration.cs +++ b/src/ImageHosting.Storage.Infrastructure/Configuration/ForbiddenCategoryConfiguration.cs @@ -1,7 +1,8 @@ +using ImageHosting.Storage.Domain.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -namespace ImageHosting.Persistence.Entities.Configuration; +namespace ImageHosting.Storage.Infrastructure.Configuration; public class ForbiddenCategoryConfiguration : IEntityTypeConfiguration { diff --git a/ImageHosting.Persistence/Entities/Configuration/ImageConfiguration.cs b/src/ImageHosting.Storage.Infrastructure/Configuration/ImageConfiguration.cs similarity index 61% rename from ImageHosting.Persistence/Entities/Configuration/ImageConfiguration.cs rename to src/ImageHosting.Storage.Infrastructure/Configuration/ImageConfiguration.cs index 55a35d5..146202e 100644 --- a/ImageHosting.Persistence/Entities/Configuration/ImageConfiguration.cs +++ b/src/ImageHosting.Storage.Infrastructure/Configuration/ImageConfiguration.cs @@ -1,19 +1,21 @@ -using ImageHosting.Persistence.ValueTypes; +using ImageHosting.Storage.Domain.Entities; +using ImageHosting.Storage.Infrastructure.Converters; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -namespace ImageHosting.Persistence.Entities.Configuration; +namespace ImageHosting.Storage.Infrastructure.Configuration; public class ImageConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.HasKey(i => i.Id); - builder.Property(i => i.Id).HasConversion(); + builder.Property(i => i.Id).HasConversion(); builder.Property(i => i.ObjectName).HasMaxLength(200); builder.Property(i => i.Hidden).HasDefaultValue(false); builder.HasIndex(i => i.Hidden); - builder.Property(i => i.UserId).HasConversion(); + builder.Property(i => i.UserId).HasConversion(); + builder.Property(i => i.UploadedAt).HasDefaultValueSql("NOW()"); builder.HasMany(i => i.Tags) .WithOne(it => it.Image) diff --git a/ImageHosting.Persistence/Entities/Configuration/ImageTagConfiguration.cs b/src/ImageHosting.Storage.Infrastructure/Configuration/ImageTagConfiguration.cs similarity index 71% rename from ImageHosting.Persistence/Entities/Configuration/ImageTagConfiguration.cs rename to src/ImageHosting.Storage.Infrastructure/Configuration/ImageTagConfiguration.cs index f27a63f..d1c31aa 100644 --- a/ImageHosting.Persistence/Entities/Configuration/ImageTagConfiguration.cs +++ b/src/ImageHosting.Storage.Infrastructure/Configuration/ImageTagConfiguration.cs @@ -1,15 +1,16 @@ -using ImageHosting.Persistence.ValueTypes; +using ImageHosting.Storage.Domain.Entities; +using ImageHosting.Storage.Infrastructure.Converters; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -namespace ImageHosting.Persistence.Entities.Configuration; +namespace ImageHosting.Storage.Infrastructure.Configuration; public class ImageTagConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.Property(it => it.TagName).HasMaxLength(32); - builder.Property(it => it.ImageId).HasConversion(); + builder.Property(it => it.ImageId).HasConversion(); builder.HasKey(it => new { it.ImageId, it.TagName }); } } \ No newline at end of file diff --git a/src/ImageHosting.Storage.Infrastructure/Converters/ImageIdConverter.cs b/src/ImageHosting.Storage.Infrastructure/Converters/ImageIdConverter.cs new file mode 100644 index 0000000..fe23a24 --- /dev/null +++ b/src/ImageHosting.Storage.Infrastructure/Converters/ImageIdConverter.cs @@ -0,0 +1,7 @@ +using ImageHosting.Storage.Domain.ValueTypes; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace ImageHosting.Storage.Infrastructure.Converters; + + +public class ImageIdConverter() : ValueConverter(userId => userId.Id, guid => new ImageId(guid)); \ No newline at end of file diff --git a/src/ImageHosting.Storage.Infrastructure/Converters/UserIdConverter.cs b/src/ImageHosting.Storage.Infrastructure/Converters/UserIdConverter.cs new file mode 100644 index 0000000..8d9cf9d --- /dev/null +++ b/src/ImageHosting.Storage.Infrastructure/Converters/UserIdConverter.cs @@ -0,0 +1,6 @@ +using ImageHosting.Storage.Domain.ValueTypes; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace ImageHosting.Storage.Infrastructure.Converters; + +public class UserIdConverter() : ValueConverter(userId => userId.Id, guid => new UserId(guid)); \ No newline at end of file diff --git a/ImageHosting.Persistence/DbContexts/IImageHostingDbContext.cs b/src/ImageHosting.Storage.Infrastructure/DbContexts/IImageHostingDbContext.cs similarity index 80% rename from ImageHosting.Persistence/DbContexts/IImageHostingDbContext.cs rename to src/ImageHosting.Storage.Infrastructure/DbContexts/IImageHostingDbContext.cs index f7a6c88..ce4b3c4 100644 --- a/ImageHosting.Persistence/DbContexts/IImageHostingDbContext.cs +++ b/src/ImageHosting.Storage.Infrastructure/DbContexts/IImageHostingDbContext.cs @@ -1,7 +1,7 @@ -using ImageHosting.Persistence.Entities; +using ImageHosting.Storage.Domain.Entities; using Microsoft.EntityFrameworkCore; -namespace ImageHosting.Persistence.DbContexts; +namespace ImageHosting.Storage.Infrastructure.DbContexts; public interface IImageHostingDbContext { diff --git a/src/ImageHosting.Storage.Infrastructure/DbContexts/ImageHostingDbContext.cs b/src/ImageHosting.Storage.Infrastructure/DbContexts/ImageHostingDbContext.cs new file mode 100644 index 0000000..48e37f1 --- /dev/null +++ b/src/ImageHosting.Storage.Infrastructure/DbContexts/ImageHostingDbContext.cs @@ -0,0 +1,22 @@ +using ImageHosting.Storage.Domain.Entities; +using ImageHosting.Storage.Infrastructure.Configuration; +using Microsoft.EntityFrameworkCore; + +namespace ImageHosting.Storage.Infrastructure.DbContexts; + +public class ImageHostingDbContext(DbContextOptions options) + : DbContext(options), IImageHostingDbContext +{ + public DbSet Images => Set(); + public DbSet ImageTags => Set(); + public DbSet ForbiddenCategories => Set(); + + public void Migrate() => Database.Migrate(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.ApplyConfiguration(new ForbiddenCategoryConfiguration()); + modelBuilder.ApplyConfiguration(new ImageConfiguration()); + modelBuilder.ApplyConfiguration(new ImageTagConfiguration()); + } +} diff --git a/src/ImageHosting.Storage.Infrastructure/Extensions/DependencyInjection/ServiceCollectionExtensions.cs b/src/ImageHosting.Storage.Infrastructure/Extensions/DependencyInjection/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..d66211f --- /dev/null +++ b/src/ImageHosting.Storage.Infrastructure/Extensions/DependencyInjection/ServiceCollectionExtensions.cs @@ -0,0 +1,61 @@ +using ImageHosting.Storage.Application.Services; +using ImageHosting.Storage.Infrastructure.DbContexts; +using ImageHosting.Storage.Infrastructure.Options; +using ImageHosting.Storage.Infrastructure.Services; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace ImageHosting.Storage.Infrastructure.Extensions.DependencyInjection; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddImageHostingDbContext(this IServiceCollection serviceCollection, + string connectionStringKey) + { + return serviceCollection + .AddDbContext((provider, optionsBuilder) => + { + var configuration = provider.GetRequiredService(); + var connectionString = configuration.GetConnectionString(connectionStringKey); + optionsBuilder.UseNpgsql(connectionString); + }); + } + + public static IServiceCollection AddAssignTagsService(this IServiceCollection serviceCollection) + { + serviceCollection + .AddOptions() + .BindConfiguration(AssignTagsOptions.SectionName) + .ValidateOnStart(); + + return serviceCollection.AddScoped(); + } + + public static IServiceCollection AddMinioServices(this IServiceCollection services) + { + services.AddOptions() + .BindConfiguration(MinioOptions.SectionName) + .ValidateDataAnnotations(); + + services.TryAddSingleton(); + services.TryAddTransient(sp => sp.GetRequiredService().CreateClient()); + + return services; + } + + public static IServiceCollection AddInfrastructureServices(this IServiceCollection serviceCollection) + { + serviceCollection.AddScoped(); + + return serviceCollection + .AddTransient() + .AddTransient(); + } + + public static IServiceCollection AddInitializeUserBucket(this IServiceCollection services) + { + return services.AddScoped(); + } +} diff --git a/src/ImageHosting.Storage.Infrastructure/Extensions/PutObjectResponseExtensions.cs b/src/ImageHosting.Storage.Infrastructure/Extensions/PutObjectResponseExtensions.cs new file mode 100644 index 0000000..65ac2ea --- /dev/null +++ b/src/ImageHosting.Storage.Infrastructure/Extensions/PutObjectResponseExtensions.cs @@ -0,0 +1,12 @@ +using ImageHosting.Storage.Application.DTOs; +using Minio.DataModel.Response; + +namespace ImageHosting.Storage.Infrastructure.Extensions; + +public static class PutObjectResponseExtensions +{ + public static UploadFileDTO ToDTO(this PutObjectResponse putObjectResponse) + { + return new UploadFileDTO(putObjectResponse.Etag[1..^1], putObjectResponse.ObjectName, putObjectResponse.Size); + } +} \ No newline at end of file diff --git a/ImageHosting.Storage/Extensions/DependencyInjection/QueryableExtensions.cs b/src/ImageHosting.Storage.Infrastructure/Extensions/QueryableExtensions.cs similarity index 68% rename from ImageHosting.Storage/Extensions/DependencyInjection/QueryableExtensions.cs rename to src/ImageHosting.Storage.Infrastructure/Extensions/QueryableExtensions.cs index 181519b..16fb7f4 100644 --- a/ImageHosting.Storage/Extensions/DependencyInjection/QueryableExtensions.cs +++ b/src/ImageHosting.Storage.Infrastructure/Extensions/QueryableExtensions.cs @@ -1,15 +1,10 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; -namespace ImageHosting.Storage.Extensions.DependencyInjection; +namespace ImageHosting.Storage.Infrastructure.Extensions; public static class QueryableExtensions { - public static async Task> ToHashSetAsync( - this IQueryable source, + public static async Task> ToHashSetAsync(this IQueryable source, CancellationToken cancellationToken = default) { var set = new HashSet(); diff --git a/src/ImageHosting.Storage.Infrastructure/ImageHosting.Storage.Infrastructure.csproj b/src/ImageHosting.Storage.Infrastructure/ImageHosting.Storage.Infrastructure.csproj new file mode 100644 index 0000000..4247997 --- /dev/null +++ b/src/ImageHosting.Storage.Infrastructure/ImageHosting.Storage.Infrastructure.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + + diff --git a/ImageHosting.Storage/Logging/WebApiEvents.cs b/src/ImageHosting.Storage.Infrastructure/Logging/ImageHostingEvents.cs similarity index 69% rename from ImageHosting.Storage/Logging/WebApiEvents.cs rename to src/ImageHosting.Storage.Infrastructure/Logging/ImageHostingEvents.cs index 1572fa5..78105cf 100644 --- a/ImageHosting.Storage/Logging/WebApiEvents.cs +++ b/src/ImageHosting.Storage.Infrastructure/Logging/ImageHostingEvents.cs @@ -1,6 +1,6 @@ -namespace ImageHosting.Storage.Logging; +namespace ImageHosting.Storage.Infrastructure.Logging; -public static class WebApiEvents +public static class ImageHostingEvents { public const int WriteFile = 1000; public const int WriteMetadata = 1001; diff --git a/ImageHosting.Storage/Logging/Log.cs b/src/ImageHosting.Storage.Infrastructure/Logging/Log.cs similarity index 77% rename from ImageHosting.Storage/Logging/Log.cs rename to src/ImageHosting.Storage.Infrastructure/Logging/Log.cs index 1a3e068..295a75e 100644 --- a/ImageHosting.Storage/Logging/Log.cs +++ b/src/ImageHosting.Storage.Infrastructure/Logging/Log.cs @@ -1,36 +1,36 @@ -using ImageHosting.Persistence.ValueTypes; +using ImageHosting.Storage.Domain.ValueTypes; using Microsoft.Extensions.Logging; -namespace ImageHosting.Storage.Logging; +namespace ImageHosting.Storage.Infrastructure.Logging; public static partial class Log { [LoggerMessage( - EventId = WebApiEvents.WriteFile, + EventId = ImageHostingEvents.WriteFile, Level = LogLevel.Information, Message = "File {ImageId} was successfully written to bucket {UserId}.")] public static partial void LogFileWritten(this ILogger logger, string imageId, string userId); [LoggerMessage( - EventId = WebApiEvents.RemoveFile, + EventId = ImageHostingEvents.RemoveFile, Level = LogLevel.Information, Message = "File {ImageId} was successfully removed from bucket {UserId}.")] public static partial void LogFileRemoved(this ILogger logger, string imageId, string userId); [LoggerMessage( - EventId = WebApiEvents.WriteMetadata, + EventId = ImageHostingEvents.WriteMetadata, Level = LogLevel.Information, Message = "Metadata for image {ImageId} successfully written.")] public static partial void LogMetadataWritten(this ILogger logger, ImageId imageId); [LoggerMessage( - EventId = WebApiEvents.MetadataDeleted, + EventId = ImageHostingEvents.MetadataDeleted, Level = LogLevel.Information, Message = "Metadata for image {ImageId} successfully deleted.")] public static partial void LogMetadataDeleted(this ILogger logger, ImageId imageId); [LoggerMessage( - EventId = WebApiEvents.MessagePublished, + EventId = ImageHostingEvents.MessagePublished, Level = LogLevel.Information, Message = "Message published with imageId {ImageId} and bucketId {BucketId}.")] public static partial void LogMessagePublished(this ILogger logger, ImageId imageId, UserId bucketId); diff --git a/ImageHosting.Persistence/Migrations/20231105143451_InitDb.Designer.cs b/src/ImageHosting.Storage.Infrastructure/Migrations/20231105143451_InitDb.Designer.cs similarity index 81% rename from ImageHosting.Persistence/Migrations/20231105143451_InitDb.Designer.cs rename to src/ImageHosting.Storage.Infrastructure/Migrations/20231105143451_InitDb.Designer.cs index a773b44..a9a2fed 100644 --- a/ImageHosting.Persistence/Migrations/20231105143451_InitDb.Designer.cs +++ b/src/ImageHosting.Storage.Infrastructure/Migrations/20231105143451_InitDb.Designer.cs @@ -1,6 +1,6 @@ // using System; -using ImageHosting.Persistence.DbContexts; +using ImageHosting.Storage.Infrastructure.DbContexts; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -25,7 +25,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - modelBuilder.Entity("ImageHosting.Storage.Entities.Category", b => + modelBuilder.Entity("ImageHosting.Storage.WebApi.Entities.Category", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -46,7 +46,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("Categories"); }); - modelBuilder.Entity("ImageHosting.Storage.Entities.Image", b => + modelBuilder.Entity("ImageHosting.Storage.WebApi.Entities.Image", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -65,7 +65,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("Images"); }); - modelBuilder.Entity("ImageHosting.Storage.Entities.ImageCategory", b => + modelBuilder.Entity("ImageHosting.Storage.WebApi.Entities.ImageCategory", b => { b.Property("ImageId") .HasColumnType("uuid"); @@ -80,15 +80,15 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("ImageCategories"); }); - modelBuilder.Entity("ImageHosting.Storage.Entities.ImageCategory", b => + modelBuilder.Entity("ImageHosting.Storage.WebApi.Entities.ImageCategory", b => { - b.HasOne("ImageHosting.Storage.Entities.Category", "Category") + b.HasOne("ImageHosting.Storage.WebApi.Entities.Category", "Category") .WithMany("Categories") .HasForeignKey("CategoryId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("ImageHosting.Storage.Entities.Image", "Image") + b.HasOne("ImageHosting.Storage.WebApi.Entities.Image", "Image") .WithMany("Categories") .HasForeignKey("ImageId") .OnDelete(DeleteBehavior.Cascade) @@ -99,12 +99,12 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("Image"); }); - modelBuilder.Entity("ImageHosting.Storage.Entities.Category", b => + modelBuilder.Entity("ImageHosting.Storage.WebApi.Entities.Category", b => { b.Navigation("Categories"); }); - modelBuilder.Entity("ImageHosting.Storage.Entities.Image", b => + modelBuilder.Entity("ImageHosting.Storage.WebApi.Entities.Image", b => { b.Navigation("Categories"); }); diff --git a/ImageHosting.Persistence/Migrations/20231105143451_InitDb.cs b/src/ImageHosting.Storage.Infrastructure/Migrations/20231105143451_InitDb.cs similarity index 100% rename from ImageHosting.Persistence/Migrations/20231105143451_InitDb.cs rename to src/ImageHosting.Storage.Infrastructure/Migrations/20231105143451_InitDb.cs diff --git a/ImageHosting.Persistence/Migrations/20231105175947_AddCreatedAt.Designer.cs b/src/ImageHosting.Storage.Infrastructure/Migrations/20231105175947_AddCreatedAt.Designer.cs similarity index 81% rename from ImageHosting.Persistence/Migrations/20231105175947_AddCreatedAt.Designer.cs rename to src/ImageHosting.Storage.Infrastructure/Migrations/20231105175947_AddCreatedAt.Designer.cs index 8e1138d..c6a74cf 100644 --- a/ImageHosting.Persistence/Migrations/20231105175947_AddCreatedAt.Designer.cs +++ b/src/ImageHosting.Storage.Infrastructure/Migrations/20231105175947_AddCreatedAt.Designer.cs @@ -1,6 +1,6 @@ // using System; -using ImageHosting.Persistence.DbContexts; +using ImageHosting.Storage.Infrastructure.DbContexts; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -25,7 +25,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - modelBuilder.Entity("ImageHosting.Storage.Entities.Category", b => + modelBuilder.Entity("ImageHosting.Storage.WebApi.Entities.Category", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -46,7 +46,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("Categories"); }); - modelBuilder.Entity("ImageHosting.Storage.Entities.Image", b => + modelBuilder.Entity("ImageHosting.Storage.WebApi.Entities.Image", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -68,7 +68,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("Images"); }); - modelBuilder.Entity("ImageHosting.Storage.Entities.ImageCategory", b => + modelBuilder.Entity("ImageHosting.Storage.WebApi.Entities.ImageCategory", b => { b.Property("ImageId") .HasColumnType("uuid"); @@ -83,15 +83,15 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("ImageCategories"); }); - modelBuilder.Entity("ImageHosting.Storage.Entities.ImageCategory", b => + modelBuilder.Entity("ImageHosting.Storage.WebApi.Entities.ImageCategory", b => { - b.HasOne("ImageHosting.Storage.Entities.Category", "Category") + b.HasOne("ImageHosting.Storage.WebApi.Entities.Category", "Category") .WithMany("Categories") .HasForeignKey("CategoryId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("ImageHosting.Storage.Entities.Image", "Image") + b.HasOne("ImageHosting.Storage.WebApi.Entities.Image", "Image") .WithMany("Categories") .HasForeignKey("ImageId") .OnDelete(DeleteBehavior.Cascade) @@ -102,12 +102,12 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("Image"); }); - modelBuilder.Entity("ImageHosting.Storage.Entities.Category", b => + modelBuilder.Entity("ImageHosting.Storage.WebApi.Entities.Category", b => { b.Navigation("Categories"); }); - modelBuilder.Entity("ImageHosting.Storage.Entities.Image", b => + modelBuilder.Entity("ImageHosting.Storage.WebApi.Entities.Image", b => { b.Navigation("Categories"); }); diff --git a/ImageHosting.Persistence/Migrations/20231105175947_AddCreatedAt.cs b/src/ImageHosting.Storage.Infrastructure/Migrations/20231105175947_AddCreatedAt.cs similarity index 100% rename from ImageHosting.Persistence/Migrations/20231105175947_AddCreatedAt.cs rename to src/ImageHosting.Storage.Infrastructure/Migrations/20231105175947_AddCreatedAt.cs diff --git a/ImageHosting.Persistence/Migrations/20231216093009_DeleteCategory.Designer.cs b/src/ImageHosting.Storage.Infrastructure/Migrations/20231216093009_DeleteCategory.Designer.cs similarity index 97% rename from ImageHosting.Persistence/Migrations/20231216093009_DeleteCategory.Designer.cs rename to src/ImageHosting.Storage.Infrastructure/Migrations/20231216093009_DeleteCategory.Designer.cs index f4896c3..9bff09f 100644 --- a/ImageHosting.Persistence/Migrations/20231216093009_DeleteCategory.Designer.cs +++ b/src/ImageHosting.Storage.Infrastructure/Migrations/20231216093009_DeleteCategory.Designer.cs @@ -1,7 +1,7 @@ // using System; using System.Collections.Generic; -using ImageHosting.Persistence.DbContexts; +using ImageHosting.Storage.Infrastructure.DbContexts; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/ImageHosting.Persistence/Migrations/20231216093009_DeleteCategory.cs b/src/ImageHosting.Storage.Infrastructure/Migrations/20231216093009_DeleteCategory.cs similarity index 100% rename from ImageHosting.Persistence/Migrations/20231216093009_DeleteCategory.cs rename to src/ImageHosting.Storage.Infrastructure/Migrations/20231216093009_DeleteCategory.cs diff --git a/ImageHosting.Persistence/Migrations/20240309213631_AddImageTag.Designer.cs b/src/ImageHosting.Storage.Infrastructure/Migrations/20240309213631_AddImageTag.Designer.cs similarity index 98% rename from ImageHosting.Persistence/Migrations/20240309213631_AddImageTag.Designer.cs rename to src/ImageHosting.Storage.Infrastructure/Migrations/20240309213631_AddImageTag.Designer.cs index f3d20de..b6be490 100644 --- a/ImageHosting.Persistence/Migrations/20240309213631_AddImageTag.Designer.cs +++ b/src/ImageHosting.Storage.Infrastructure/Migrations/20240309213631_AddImageTag.Designer.cs @@ -1,6 +1,6 @@ // using System; -using ImageHosting.Persistence.DbContexts; +using ImageHosting.Storage.Infrastructure.DbContexts; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/ImageHosting.Persistence/Migrations/20240309213631_AddImageTag.cs b/src/ImageHosting.Storage.Infrastructure/Migrations/20240309213631_AddImageTag.cs similarity index 100% rename from ImageHosting.Persistence/Migrations/20240309213631_AddImageTag.cs rename to src/ImageHosting.Storage.Infrastructure/Migrations/20240309213631_AddImageTag.cs diff --git a/ImageHosting.Persistence/Migrations/ImageHostingDbContextModelSnapshot.cs b/src/ImageHosting.Storage.Infrastructure/Migrations/ImageHostingDbContextModelSnapshot.cs similarity index 98% rename from ImageHosting.Persistence/Migrations/ImageHostingDbContextModelSnapshot.cs rename to src/ImageHosting.Storage.Infrastructure/Migrations/ImageHostingDbContextModelSnapshot.cs index 0c37244..e7be763 100644 --- a/ImageHosting.Persistence/Migrations/ImageHostingDbContextModelSnapshot.cs +++ b/src/ImageHosting.Storage.Infrastructure/Migrations/ImageHostingDbContextModelSnapshot.cs @@ -1,6 +1,6 @@ // using System; -using ImageHosting.Persistence.DbContexts; +using ImageHosting.Storage.Infrastructure.DbContexts; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; diff --git a/src/ImageHosting.Storage.Infrastructure/Options/AssignTagsOptions.cs b/src/ImageHosting.Storage.Infrastructure/Options/AssignTagsOptions.cs new file mode 100644 index 0000000..64b1302 --- /dev/null +++ b/src/ImageHosting.Storage.Infrastructure/Options/AssignTagsOptions.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations; + +namespace ImageHosting.Storage.Infrastructure.Options; + +public class AssignTagsOptions +{ + public const string SectionName = "AssignTags"; + + [Required] public required double Threshold { get; init; } +} \ No newline at end of file diff --git a/ImageHosting.Storage/Models/MinioOptions.cs b/src/ImageHosting.Storage.Infrastructure/Options/MinioOptions.cs similarity index 88% rename from ImageHosting.Storage/Models/MinioOptions.cs rename to src/ImageHosting.Storage.Infrastructure/Options/MinioOptions.cs index 4eeb98c..ad9747c 100644 --- a/ImageHosting.Storage/Models/MinioOptions.cs +++ b/src/ImageHosting.Storage.Infrastructure/Options/MinioOptions.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace ImageHosting.Storage.Models; +namespace ImageHosting.Storage.Infrastructure.Options; public class MinioOptions { diff --git a/src/ImageHosting.Storage.Infrastructure/Services/AssignTagsService.cs b/src/ImageHosting.Storage.Infrastructure/Services/AssignTagsService.cs new file mode 100644 index 0000000..41029d7 --- /dev/null +++ b/src/ImageHosting.Storage.Infrastructure/Services/AssignTagsService.cs @@ -0,0 +1,33 @@ +using ImageHosting.Storage.Application.Services; +using ImageHosting.Storage.Domain.ValueTypes; +using ImageHosting.Storage.Infrastructure.DbContexts; +using ImageHosting.Storage.Infrastructure.Options; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; + +namespace ImageHosting.Storage.Infrastructure.Services; + +public class AssignTagsService(IImageHostingDbContext dbContext, IOptions options) + : IAssignTagsService +{ + private readonly double _threshold = options.Value.Threshold; + + public async Task AssignTagsAsync(Dictionary> categories, + CancellationToken cancellationToken = default) + { + var images = await dbContext.Images + .AsTracking() + .Include(i => i.Tags) + .Where(i => categories.Keys.Contains(i.Id)) + .ToListAsync(cancellationToken); + + foreach (var image in images) + { + image.AddTags(categories[image.Id] + .Where(category => category.Value > _threshold) + .Select(category => category.Key)); + } + + await dbContext.SaveChangesAsync(cancellationToken); + } +} \ No newline at end of file diff --git a/src/ImageHosting.Storage.Infrastructure/Services/FileService.cs b/src/ImageHosting.Storage.Infrastructure/Services/FileService.cs new file mode 100644 index 0000000..5882b4d --- /dev/null +++ b/src/ImageHosting.Storage.Infrastructure/Services/FileService.cs @@ -0,0 +1,81 @@ +using ImageHosting.Storage.Application.DTOs; +using ImageHosting.Storage.Application.Exceptions; +using ImageHosting.Storage.Application.Services; +using ImageHosting.Storage.Infrastructure.Extensions; +using ImageHosting.Storage.Infrastructure.Logging; +using Minio; +using Minio.DataModel.Args; +using Minio.Exceptions; +using Microsoft.Extensions.Logging; + +namespace ImageHosting.Storage.Infrastructure.Services; + +public class FileService(IMinioClient minioClient, ILogger logger) : IFileService +{ + public async Task WriteFileAsync(WriteFileDTO writeFileDto, + CancellationToken cancellationToken = default) + { + var bucketExistsArgs = new BucketExistsArgs().WithBucket(writeFileDto.UserId); + var foundBucket = + await minioClient.BucketExistsAsync(bucketExistsArgs, cancellationToken).ConfigureAwait(false); + if (!foundBucket) + { + throw new UserBucketDoesNotExistsException(writeFileDto.UserId); + } + + var isObjectExists = await IsObjectExistsAsync(writeFileDto.UserId, writeFileDto.ImageId, cancellationToken); + if (isObjectExists) + { + throw new ImageObjectAlreadyExistsException(writeFileDto.UserId, writeFileDto.ImageId); + } + + var putObjectArgs = new PutObjectArgs() + .WithBucket(writeFileDto.UserId) + .WithObjectSize(writeFileDto.Length) + .WithContentType(writeFileDto.ContentType) + .WithStreamData(writeFileDto.Stream) + .WithObject(writeFileDto.ImageId); + + var putObjectResponse = + await minioClient.PutObjectAsync(putObjectArgs, cancellationToken).ConfigureAwait(false); + + logger.LogFileWritten(writeFileDto.ImageId, writeFileDto.UserId); + return putObjectResponse.ToDTO(); + } + + private async Task IsObjectExistsAsync(string bucketId, string objectName, + CancellationToken cancellationToken = default) + { + try + { + var statObjectArgs = new StatObjectArgs() + .WithBucket(bucketId) + .WithObject(objectName); + _ = await minioClient.StatObjectAsync(statObjectArgs, cancellationToken).ConfigureAwait(false); + + return true; + } + catch (ObjectNotFoundException) + { + return false; + } + } + + public async Task RemoveFileAsync(RemoveFileDTO removeFileDto, CancellationToken cancellationToken = default) + { + var bucketExistsArgs = new BucketExistsArgs().WithBucket(removeFileDto.UserId); + var foundBucket = + await minioClient.BucketExistsAsync(bucketExistsArgs, cancellationToken).ConfigureAwait(false); + if (!foundBucket) + { + throw new UserBucketDoesNotExistsException(removeFileDto.UserId); + } + + var removeObjectArgs = new RemoveObjectArgs() + .WithBucket(removeFileDto.UserId) + .WithObject(removeFileDto.ImageId); + await minioClient.RemoveObjectAsync(removeObjectArgs, cancellationToken).ConfigureAwait(false); + + logger.LogFileRemoved(removeFileDto.ImageId, removeFileDto.UserId); + } +} \ No newline at end of file diff --git a/ImageHosting.Storage/Services/IMinioClientFactory.cs b/src/ImageHosting.Storage.Infrastructure/Services/IMinioClientFactory.cs similarity index 61% rename from ImageHosting.Storage/Services/IMinioClientFactory.cs rename to src/ImageHosting.Storage.Infrastructure/Services/IMinioClientFactory.cs index 96a5baf..84d4254 100644 --- a/ImageHosting.Storage/Services/IMinioClientFactory.cs +++ b/src/ImageHosting.Storage.Infrastructure/Services/IMinioClientFactory.cs @@ -1,6 +1,6 @@ using Minio; -namespace ImageHosting.Storage.Services; +namespace ImageHosting.Storage.Infrastructure.Services; public interface IMinioClientFactory { diff --git a/ImageHosting.Storage/Services/InitializeUserBucket.cs b/src/ImageHosting.Storage.Infrastructure/Services/InitializeUserBucket.cs similarity index 86% rename from ImageHosting.Storage/Services/InitializeUserBucket.cs rename to src/ImageHosting.Storage.Infrastructure/Services/InitializeUserBucket.cs index c460c46..e02acf7 100644 --- a/ImageHosting.Storage/Services/InitializeUserBucket.cs +++ b/src/ImageHosting.Storage.Infrastructure/Services/InitializeUserBucket.cs @@ -1,10 +1,8 @@ -using System; -using System.Threading; -using System.Threading.Tasks; +using ImageHosting.Storage.Application.Services; using Minio; using Minio.DataModel.Args; -namespace ImageHosting.Storage.Services; +namespace ImageHosting.Storage.Infrastructure.Services; public class InitializeUserBucket(IMinioClient minioClient) : IInitializeUserBucket { diff --git a/ImageHosting.Storage/Features/Images/Services/MetadataService.cs b/src/ImageHosting.Storage.Infrastructure/Services/MetadataService.cs similarity index 60% rename from ImageHosting.Storage/Features/Images/Services/MetadataService.cs rename to src/ImageHosting.Storage.Infrastructure/Services/MetadataService.cs index 18df093..065c641 100644 --- a/ImageHosting.Storage/Features/Images/Services/MetadataService.cs +++ b/src/ImageHosting.Storage.Infrastructure/Services/MetadataService.cs @@ -1,26 +1,24 @@ -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ImageHosting.Persistence.DbContexts; -using ImageHosting.Persistence.ValueTypes; -using ImageHosting.Storage.Features.Images.Models; -using ImageHosting.Storage.Logging; +using ImageHosting.Storage.Application.DTOs; +using ImageHosting.Storage.Application.Services; +using ImageHosting.Storage.Domain.ValueTypes; +using ImageHosting.Storage.Infrastructure.DbContexts; +using ImageHosting.Storage.Infrastructure.Logging; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace ImageHosting.Storage.Features.Images.Services; +namespace ImageHosting.Storage.Infrastructure.Services; public class MetadataService(IImageHostingDbContext dbContext, ILogger logger) : IMetadataService { - public async Task WriteMetadataAsync(ImageMetadata imageMetadata, + public async Task WriteMetadataAsync(ImageMetadataDTO imageMetadataDto, CancellationToken cancellationToken = default) { - var entity = imageMetadata.ToEntity(); + var entity = imageMetadataDto.ToEntity(); dbContext.Images.Add(entity); await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false); logger.LogMetadataWritten(entity.Id); - return ImageUploadedResponse.From(entity); + return ImageUploadedDTO.From(entity); } public async Task DeleteMetadataAsync(ImageId id, CancellationToken cancellationToken = default) diff --git a/ImageHosting.Storage/Services/MinioClientFactory.cs b/src/ImageHosting.Storage.Infrastructure/Services/MinioClientFactory.cs similarity index 81% rename from ImageHosting.Storage/Services/MinioClientFactory.cs rename to src/ImageHosting.Storage.Infrastructure/Services/MinioClientFactory.cs index 464326a..f4c90ab 100644 --- a/ImageHosting.Storage/Services/MinioClientFactory.cs +++ b/src/ImageHosting.Storage.Infrastructure/Services/MinioClientFactory.cs @@ -1,8 +1,8 @@ -using ImageHosting.Storage.Models; +using ImageHosting.Storage.Infrastructure.Options; using Microsoft.Extensions.Options; using Minio; -namespace ImageHosting.Storage.Services; +namespace ImageHosting.Storage.Infrastructure.Services; public class MinioClientFactory(IOptions options) : IMinioClientFactory { diff --git a/ImageHosting.Storage/Features/Images/Services/NewImageProducer.cs b/src/ImageHosting.Storage.Infrastructure/Services/NewImageProducer.cs similarity index 68% rename from ImageHosting.Storage/Features/Images/Services/NewImageProducer.cs rename to src/ImageHosting.Storage.Infrastructure/Services/NewImageProducer.cs index 0144c13..7a5beb8 100644 --- a/ImageHosting.Storage/Features/Images/Services/NewImageProducer.cs +++ b/src/ImageHosting.Storage.Infrastructure/Services/NewImageProducer.cs @@ -1,11 +1,10 @@ -using System.Threading; -using System.Threading.Tasks; -using ImageHosting.Storage.Features.Images.Models; -using ImageHosting.Storage.Logging; +using ImageHosting.Storage.Application.Services; +using ImageHosting.Storage.Domain.Messages; +using ImageHosting.Storage.Infrastructure.Logging; using MassTransit; using Microsoft.Extensions.Logging; -namespace ImageHosting.Storage.Features.Images.Services; +namespace ImageHosting.Storage.Infrastructure.Services; public class NewImageProducer(ITopicProducer messageProducer, ILogger logger) : INewImageProducer diff --git a/src/ImageHosting.Storage.Tagger/AssignTagsConsumer.cs b/src/ImageHosting.Storage.Tagger/AssignTagsConsumer.cs new file mode 100644 index 0000000..86a1ef9 --- /dev/null +++ b/src/ImageHosting.Storage.Tagger/AssignTagsConsumer.cs @@ -0,0 +1,20 @@ +using ImageHosting.Storage.Application.Services; +using ImageHosting.Storage.Domain.Messages; +using MassTransit; + +namespace ImageHosting.Storage.Tagger; + +public class AssignTagsConsumer(IAssignTagsService assignTagsService) : IConsumer> +{ + public Task Consume(ConsumeContext> context) + { + var messages = context.Message + .GroupBy(consumeContext => consumeContext.Message.ImageId, + consumeContext => consumeContext.Message.Categories) + .ToDictionary(grouping => grouping.Key, + grouping => grouping.SelectMany(categories => categories) + .ToDictionary(category => category.Key, category => category.Value)); + + return assignTagsService.AssignTagsAsync(messages, context.CancellationToken); + } +} \ No newline at end of file diff --git a/ImageHosting.Storage/AssignTagsConsumerDefinition.cs b/src/ImageHosting.Storage.Tagger/AssignTagsConsumerDefinition.cs similarity index 91% rename from ImageHosting.Storage/AssignTagsConsumerDefinition.cs rename to src/ImageHosting.Storage.Tagger/AssignTagsConsumerDefinition.cs index 2509fd2..2102586 100644 --- a/ImageHosting.Storage/AssignTagsConsumerDefinition.cs +++ b/src/ImageHosting.Storage.Tagger/AssignTagsConsumerDefinition.cs @@ -1,7 +1,6 @@ -using System; using MassTransit; -namespace ImageHosting.Storage; +namespace ImageHosting.Storage.Tagger; public class AssignTagsConsumerDefinition : ConsumerDefinition { diff --git a/src/ImageHosting.Storage.Tagger/Dockerfile b/src/ImageHosting.Storage.Tagger/Dockerfile new file mode 100644 index 0000000..7b1b07b --- /dev/null +++ b/src/ImageHosting.Storage.Tagger/Dockerfile @@ -0,0 +1,31 @@ +# See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging. + +# This stage is used when running from VS in fast mode (Default for Debug configuration) +FROM mcr.microsoft.com/dotnet/runtime:8.0 AS base +USER app +WORKDIR /app + + +# This stage is used to build the service project +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["src/ImageHosting.Storage.Tagger/ImageHosting.Storage.Tagger.csproj", "src/ImageHosting.Storage.Tagger/"] +COPY ["src/ImageHosting.Storage.Infrastructure/ImageHosting.Storage.Infrastructure.csproj", "src/ImageHosting.Storage.Infrastructure/"] +COPY ["src/ImageHosting.Storage.Application/ImageHosting.Storage.Application.csproj", "src/ImageHosting.Storage.Application/"] +COPY ["src/ImageHosting.Storage.Domain/ImageHosting.Storage.Domain.csproj", "src/ImageHosting.Storage.Domain/"] +RUN dotnet restore "./src/ImageHosting.Storage.Tagger/ImageHosting.Storage.Tagger.csproj" +COPY . . +WORKDIR "/src/src/ImageHosting.Storage.Tagger" +RUN dotnet build "./ImageHosting.Storage.Tagger.csproj" -c $BUILD_CONFIGURATION -o /app/build + +# This stage is used to publish the service project to be copied to the final stage +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "./ImageHosting.Storage.Tagger.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +# This stage is used in production or when running from VS in regular mode (Default when not using the Debug configuration) +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "ImageHosting.Storage.Tagger.dll"] \ No newline at end of file diff --git a/src/ImageHosting.Storage.Tagger/ImageHosting.Storage.Tagger.csproj b/src/ImageHosting.Storage.Tagger/ImageHosting.Storage.Tagger.csproj new file mode 100644 index 0000000..f074f6f --- /dev/null +++ b/src/ImageHosting.Storage.Tagger/ImageHosting.Storage.Tagger.csproj @@ -0,0 +1,25 @@ + + + + net8.0 + enable + enable + Linux + ..\.. + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/src/ImageHosting.Storage.Tagger/Options/AssignTagsConsumerOptions.cs b/src/ImageHosting.Storage.Tagger/Options/AssignTagsConsumerOptions.cs new file mode 100644 index 0000000..e880ac7 --- /dev/null +++ b/src/ImageHosting.Storage.Tagger/Options/AssignTagsConsumerOptions.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace ImageHosting.Storage.Tagger.Options; + +public class AssignTagsConsumerOptions +{ + public const string SectionName = "AssignTagsConsumer"; + + [Required] public required string[] BootstrapServers { get; init; } + [Required] public required string TopicName { get; init; } + [Required] public required string GroupId { get; init; } +} \ No newline at end of file diff --git a/src/ImageHosting.Storage.Tagger/Program.cs b/src/ImageHosting.Storage.Tagger/Program.cs new file mode 100644 index 0000000..79e1a50 --- /dev/null +++ b/src/ImageHosting.Storage.Tagger/Program.cs @@ -0,0 +1,40 @@ +using CommunityToolkit.Diagnostics; +using Confluent.Kafka; +using ImageHosting.Storage.Domain.Messages; +using ImageHosting.Storage.Infrastructure.Extensions.DependencyInjection; +using ImageHosting.Storage.Tagger; +using ImageHosting.Storage.Tagger.Options; +using MassTransit; + +var builder = Host.CreateApplicationBuilder(args); + +var options = builder.Configuration + .GetSection(AssignTagsConsumerOptions.SectionName) + .Get(); +Guard.IsNotNull(options); + +builder.Services.AddMassTransit(massTransit => +{ + massTransit.UsingInMemory(); + + massTransit.AddRider(rider => + { + rider.AddConsumer(consumerDefinitionType: typeof(AssignTagsConsumerDefinition)); + + rider.UsingKafka((context, kafka) => + { + kafka.Host(options.BootstrapServers); + kafka.TopicEndpoint(options.TopicName, options.GroupId, endpoint => + { + endpoint.ConfigureConsumer(context); + endpoint.AutoOffsetReset = AutoOffsetReset.Earliest; + endpoint.ConcurrentDeliveryLimit = 10; + }); + }); + }); +}); +builder.Services.AddImageHostingDbContext("ImageHosting"); +builder.Services.AddAssignTagsService(); + +var host = builder.Build(); +host.Run(); \ No newline at end of file diff --git a/src/ImageHosting.Storage.Tagger/Properties/launchSettings.json b/src/ImageHosting.Storage.Tagger/Properties/launchSettings.json new file mode 100644 index 0000000..2c30716 --- /dev/null +++ b/src/ImageHosting.Storage.Tagger/Properties/launchSettings.json @@ -0,0 +1,15 @@ +{ + "profiles": { + "ImageHosting.Storage.Tagger": { + "commandName": "Project", + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true + }, + "Container (Dockerfile)": { + "commandName": "Docker" + } + }, + "$schema": "http://json.schemastore.org/launchsettings.json" +} \ No newline at end of file diff --git a/src/ImageHosting.Storage.Tagger/appsettings.json b/src/ImageHosting.Storage.Tagger/appsettings.json new file mode 100644 index 0000000..43867f6 --- /dev/null +++ b/src/ImageHosting.Storage.Tagger/appsettings.json @@ -0,0 +1,22 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Microsoft.EntityFrameworkCore": "Warning" + } + }, + "AssignTags": { + "Threshold": 0.8 + }, + "AssignTagsConsumer": { + "BootstrapServers": [ + "localhost:9094" + ], + "TopicName": "categories.v1", + "GroupId": "image-tagger" + }, + "ConnectionStrings": { + "ImageHosting": "Host=localhost;Port=5432;Database=image_hosting;Username=image_hosting;Password=P@ssw0rd;Include Error Detail=true" + } +} diff --git a/src/ImageHosting.Storage.WebApi/Dockerfile b/src/ImageHosting.Storage.WebApi/Dockerfile new file mode 100644 index 0000000..f423beb --- /dev/null +++ b/src/ImageHosting.Storage.WebApi/Dockerfile @@ -0,0 +1,32 @@ +# See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging. + +# This stage is used when running from VS in fast mode (Default for Debug configuration) +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +USER app +WORKDIR /app +EXPOSE 8080 + + +# This stage is used to build the service project +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["src/ImageHosting.Storage.WebApi/ImageHosting.Storage.WebApi.csproj", "src/ImageHosting.Storage.WebApi/"] +COPY ["src/ImageHosting.Storage.Application/ImageHosting.Storage.Application.csproj", "src/ImageHosting.Storage.Application/"] +COPY ["src/ImageHosting.Storage.Domain/ImageHosting.Storage.Domain.csproj", "src/ImageHosting.Storage.Domain/"] +COPY ["src/ImageHosting.Storage.Infrastructure/ImageHosting.Storage.Infrastructure.csproj", "src/ImageHosting.Storage.Infrastructure/"] +RUN dotnet restore "./src/ImageHosting.Storage.WebApi/ImageHosting.Storage.WebApi.csproj" +COPY . . +WORKDIR "/src/src/ImageHosting.Storage.WebApi" +RUN dotnet build "./ImageHosting.Storage.WebApi.csproj" -c $BUILD_CONFIGURATION -o /app/build + +# This stage is used to publish the service project to be copied to the final stage +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "./ImageHosting.Storage.WebApi.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +# This stage is used in production or when running from VS in regular mode (Default when not using the Debug configuration) +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "ImageHosting.Storage.WebApi.dll"] \ No newline at end of file diff --git a/ImageHosting.Storage/Features/Images/Endpoints/Images.cs b/src/ImageHosting.Storage.WebApi/Features/Images/Endpoints/Images.cs similarity index 92% rename from ImageHosting.Storage/Features/Images/Endpoints/Images.cs rename to src/ImageHosting.Storage.WebApi/Features/Images/Endpoints/Images.cs index 3c4db62..2786948 100644 --- a/ImageHosting.Storage/Features/Images/Endpoints/Images.cs +++ b/src/ImageHosting.Storage.WebApi/Features/Images/Endpoints/Images.cs @@ -1,16 +1,11 @@ -using System; using System.Security.Claims; -using System.Threading; -using ImageHosting.Persistence.ValueTypes; -using ImageHosting.Storage.Features.Images.Handlers; -using ImageHosting.Storage.Features.Images.Models; -using ImageHosting.Storage.Http; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; +using ImageHosting.Storage.Domain.ValueTypes; +using ImageHosting.Storage.WebApi.Features.Images.Handlers; +using ImageHosting.Storage.WebApi.Features.Images.Models; +using ImageHosting.Storage.WebApi.Filters; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Routing; -namespace ImageHosting.Storage.Features.Images.Endpoints; +namespace ImageHosting.Storage.WebApi.Features.Images.Endpoints; public static class Images { diff --git a/ImageHosting.Storage/Features/Images/Exceptions/ImageMetadataNotFoundException.cs b/src/ImageHosting.Storage.WebApi/Features/Images/Exceptions/ImageMetadataNotFoundException.cs similarity index 57% rename from ImageHosting.Storage/Features/Images/Exceptions/ImageMetadataNotFoundException.cs rename to src/ImageHosting.Storage.WebApi/Features/Images/Exceptions/ImageMetadataNotFoundException.cs index af3e8a6..9462078 100644 --- a/ImageHosting.Storage/Features/Images/Exceptions/ImageMetadataNotFoundException.cs +++ b/src/ImageHosting.Storage.WebApi/Features/Images/Exceptions/ImageMetadataNotFoundException.cs @@ -1,7 +1,6 @@ -using System; -using ImageHosting.Persistence.ValueTypes; +using ImageHosting.Storage.Domain.ValueTypes; -namespace ImageHosting.Storage.Features.Images.Exceptions; +namespace ImageHosting.Storage.WebApi.Features.Images.Exceptions; public class ImageMetadataNotFoundException(ImageId imageId) : Exception($"Image '{imageId}' not found.") { diff --git a/ImageHosting.Storage/Features/Images/Exceptions/ImageNotFoundException.cs b/src/ImageHosting.Storage.WebApi/Features/Images/Exceptions/ImageNotFoundException.cs similarity index 77% rename from ImageHosting.Storage/Features/Images/Exceptions/ImageNotFoundException.cs rename to src/ImageHosting.Storage.WebApi/Features/Images/Exceptions/ImageNotFoundException.cs index dee4ebc..319aad0 100644 --- a/ImageHosting.Storage/Features/Images/Exceptions/ImageNotFoundException.cs +++ b/src/ImageHosting.Storage.WebApi/Features/Images/Exceptions/ImageNotFoundException.cs @@ -1,6 +1,4 @@ -using System; - -namespace ImageHosting.Storage.Features.Images.Exceptions; +namespace ImageHosting.Storage.WebApi.Features.Images.Exceptions; public class ImageNotFoundException(string bucketName, string imageName) : Exception($"Image '{imageName}' not found in bucket '{bucketName}'.") diff --git a/ImageHosting.Storage/Features/Images/Extensions/QuaryableExtensions.cs b/src/ImageHosting.Storage.WebApi/Features/Images/Extensions/QuaryableExtensions.cs similarity index 74% rename from ImageHosting.Storage/Features/Images/Extensions/QuaryableExtensions.cs rename to src/ImageHosting.Storage.WebApi/Features/Images/Extensions/QuaryableExtensions.cs index ff8dce1..73375f4 100644 --- a/ImageHosting.Storage/Features/Images/Extensions/QuaryableExtensions.cs +++ b/src/ImageHosting.Storage.WebApi/Features/Images/Extensions/QuaryableExtensions.cs @@ -1,10 +1,8 @@ -using System; -using System.Linq; -using ImageHosting.Persistence.Entities; -using ImageHosting.Storage.Features.Images.Models; +using ImageHosting.Storage.Domain.Entities; +using ImageHosting.Storage.WebApi.Features.Images.Models; using Microsoft.EntityFrameworkCore; -namespace ImageHosting.Storage.Features.Images.Extensions; +namespace ImageHosting.Storage.WebApi.Features.Images.Extensions; public static class QueryableExtensions { diff --git a/ImageHosting.Storage/Features/Images/Extensions/ServiceCollectionExtensions.cs b/src/ImageHosting.Storage.WebApi/Features/Images/Extensions/ServiceCollectionExtensions.cs similarity index 69% rename from ImageHosting.Storage/Features/Images/Extensions/ServiceCollectionExtensions.cs rename to src/ImageHosting.Storage.WebApi/Features/Images/Extensions/ServiceCollectionExtensions.cs index 90c8dfe..ffc55eb 100644 --- a/ImageHosting.Storage/Features/Images/Extensions/ServiceCollectionExtensions.cs +++ b/src/ImageHosting.Storage.WebApi/Features/Images/Extensions/ServiceCollectionExtensions.cs @@ -1,9 +1,8 @@ -using ImageHosting.Storage.Features.Images.Handlers; -using ImageHosting.Storage.Features.Images.Services; -using ImageHosting.Storage.Models; -using Microsoft.Extensions.DependencyInjection; +using ImageHosting.Storage.WebApi.Features.Images.Handlers; +using ImageHosting.Storage.WebApi.Features.Images.Services; +using ImageHosting.Storage.WebApi.Options; -namespace ImageHosting.Storage.Features.Images.Extensions; +namespace ImageHosting.Storage.WebApi.Features.Images.Extensions; public static class ServiceCollectionExtensions { @@ -13,12 +12,9 @@ public static IServiceCollection AddImageServices(this IServiceCollection servic .BindConfiguration(ImagesOptions.SectionName) .ValidateDataAnnotations(); - services.AddScoped(); return services - .AddTransient() .AddTransient() - .AddTransient() .AddTransient() .AddTransient() .AddTransient() diff --git a/ImageHosting.Storage/Features/Images/Handlers/AddTagsHandler.cs b/src/ImageHosting.Storage.WebApi/Features/Images/Handlers/AddTagsHandler.cs similarity index 72% rename from ImageHosting.Storage/Features/Images/Handlers/AddTagsHandler.cs rename to src/ImageHosting.Storage.WebApi/Features/Images/Handlers/AddTagsHandler.cs index abc9295..4dc6c9a 100644 --- a/ImageHosting.Storage/Features/Images/Handlers/AddTagsHandler.cs +++ b/src/ImageHosting.Storage.WebApi/Features/Images/Handlers/AddTagsHandler.cs @@ -1,14 +1,10 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ImageHosting.Persistence.DbContexts; -using ImageHosting.Persistence.ValueTypes; -using ImageHosting.Storage.Features.Images.Exceptions; -using ImageHosting.Storage.Features.Images.Models; +using ImageHosting.Storage.Domain.ValueTypes; +using ImageHosting.Storage.Infrastructure.DbContexts; +using ImageHosting.Storage.WebApi.Features.Images.Exceptions; +using ImageHosting.Storage.WebApi.Features.Images.Models; using Microsoft.EntityFrameworkCore; -namespace ImageHosting.Storage.Features.Images.Handlers; +namespace ImageHosting.Storage.WebApi.Features.Images.Handlers; public interface IAddTagsHandler { diff --git a/ImageHosting.Storage/Features/Images/Handlers/DeleteTagsHandler.cs b/src/ImageHosting.Storage.WebApi/Features/Images/Handlers/DeleteTagsHandler.cs similarity index 69% rename from ImageHosting.Storage/Features/Images/Handlers/DeleteTagsHandler.cs rename to src/ImageHosting.Storage.WebApi/Features/Images/Handlers/DeleteTagsHandler.cs index af90e54..2fc9727 100644 --- a/ImageHosting.Storage/Features/Images/Handlers/DeleteTagsHandler.cs +++ b/src/ImageHosting.Storage.WebApi/Features/Images/Handlers/DeleteTagsHandler.cs @@ -1,12 +1,8 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ImageHosting.Persistence.DbContexts; -using ImageHosting.Persistence.ValueTypes; +using ImageHosting.Storage.Domain.ValueTypes; +using ImageHosting.Storage.Infrastructure.DbContexts; using Microsoft.EntityFrameworkCore; -namespace ImageHosting.Storage.Features.Images.Handlers; +namespace ImageHosting.Storage.WebApi.Features.Images.Handlers; public interface IDeleteTagsHandler { diff --git a/ImageHosting.Storage/Features/Images/Handlers/GetImageAssetHandler.cs b/src/ImageHosting.Storage.WebApi/Features/Images/Handlers/GetImageAssetHandler.cs similarity index 85% rename from ImageHosting.Storage/Features/Images/Handlers/GetImageAssetHandler.cs rename to src/ImageHosting.Storage.WebApi/Features/Images/Handlers/GetImageAssetHandler.cs index 8d49301..160c04d 100644 --- a/ImageHosting.Storage/Features/Images/Handlers/GetImageAssetHandler.cs +++ b/src/ImageHosting.Storage.WebApi/Features/Images/Handlers/GetImageAssetHandler.cs @@ -1,13 +1,10 @@ -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using ImageHosting.Persistence.ValueTypes; -using ImageHosting.Storage.Features.Images.Exceptions; -using ImageHosting.Storage.Features.Images.Models; +using ImageHosting.Storage.Domain.ValueTypes; +using ImageHosting.Storage.WebApi.Features.Images.Exceptions; +using ImageHosting.Storage.WebApi.Features.Images.Models; using Minio; using Minio.DataModel.Args; -namespace ImageHosting.Storage.Features.Images.Handlers; +namespace ImageHosting.Storage.WebApi.Features.Images.Handlers; public interface IGetImageAssetHandler { diff --git a/ImageHosting.Storage/Features/Images/Handlers/GetImageHandler.cs b/src/ImageHosting.Storage.WebApi/Features/Images/Handlers/GetImageHandler.cs similarity index 66% rename from ImageHosting.Storage/Features/Images/Handlers/GetImageHandler.cs rename to src/ImageHosting.Storage.WebApi/Features/Images/Handlers/GetImageHandler.cs index ef5156c..ec447ee 100644 --- a/ImageHosting.Storage/Features/Images/Handlers/GetImageHandler.cs +++ b/src/ImageHosting.Storage.WebApi/Features/Images/Handlers/GetImageHandler.cs @@ -1,16 +1,13 @@ -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ImageHosting.Persistence.DbContexts; -using ImageHosting.Persistence.ValueTypes; -using ImageHosting.Storage.Features.Images.Exceptions; -using ImageHosting.Storage.Features.Images.Extensions; -using ImageHosting.Storage.Features.Images.Models; -using ImageHosting.Storage.Models; +using ImageHosting.Storage.Domain.ValueTypes; +using ImageHosting.Storage.Infrastructure.DbContexts; +using ImageHosting.Storage.WebApi.Features.Images.Exceptions; +using ImageHosting.Storage.WebApi.Features.Images.Extensions; +using ImageHosting.Storage.WebApi.Features.Images.Models; +using ImageHosting.Storage.WebApi.Options; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; -namespace ImageHosting.Storage.Features.Images.Handlers; +namespace ImageHosting.Storage.WebApi.Features.Images.Handlers; public interface IGetImageHandler { diff --git a/ImageHosting.Storage/Features/Images/Handlers/UpdateNameHandler.cs b/src/ImageHosting.Storage.WebApi/Features/Images/Handlers/UpdateNameHandler.cs similarity index 74% rename from ImageHosting.Storage/Features/Images/Handlers/UpdateNameHandler.cs rename to src/ImageHosting.Storage.WebApi/Features/Images/Handlers/UpdateNameHandler.cs index b357fa3..de5a40d 100644 --- a/ImageHosting.Storage/Features/Images/Handlers/UpdateNameHandler.cs +++ b/src/ImageHosting.Storage.WebApi/Features/Images/Handlers/UpdateNameHandler.cs @@ -1,12 +1,9 @@ -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ImageHosting.Persistence.DbContexts; -using ImageHosting.Persistence.ValueTypes; -using ImageHosting.Storage.Features.Images.Models; +using ImageHosting.Storage.Domain.ValueTypes; +using ImageHosting.Storage.Infrastructure.DbContexts; +using ImageHosting.Storage.WebApi.Features.Images.Models; using Microsoft.EntityFrameworkCore; -namespace ImageHosting.Storage.Features.Images.Handlers; +namespace ImageHosting.Storage.WebApi.Features.Images.Handlers; public interface IUpdateNameHandler { diff --git a/ImageHosting.Storage/Features/Images/Handlers/UploadFileHandler.cs b/src/ImageHosting.Storage.WebApi/Features/Images/Handlers/UploadFileHandler.cs similarity index 54% rename from ImageHosting.Storage/Features/Images/Handlers/UploadFileHandler.cs rename to src/ImageHosting.Storage.WebApi/Features/Images/Handlers/UploadFileHandler.cs index d4afa65..ccca30d 100644 --- a/ImageHosting.Storage/Features/Images/Handlers/UploadFileHandler.cs +++ b/src/ImageHosting.Storage.WebApi/Features/Images/Handlers/UploadFileHandler.cs @@ -1,17 +1,14 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using ImageHosting.Persistence.ValueTypes; -using ImageHosting.Storage.Features.Images.Models; -using ImageHosting.Storage.Features.Images.Services; -using ImageHosting.Storage.Models; -using Microsoft.AspNetCore.Http; +using ImageHosting.Storage.Application.DTOs; +using ImageHosting.Storage.Domain.Common; +using ImageHosting.Storage.Domain.ValueTypes; +using ImageHosting.Storage.WebApi.Features.Images.Services; -namespace ImageHosting.Storage.Features.Images.Handlers; +namespace ImageHosting.Storage.WebApi.Features.Images.Handlers; public interface IUploadFileHandler { - Task UploadAsync(UserId userId, ImageId imageId, IFormFile formFile, bool hidden, DateTime uploadedAt, + Task UploadAsync(UserId userId, ImageId imageId, IFormFile formFile, bool hidden, + DateTime uploadedAt, CancellationToken cancellationToken = default); } @@ -21,10 +18,13 @@ public class UploadFileHandler( IPublishNewMessageCommandFactory publishNewMessageCommandFactory) : IUploadFileHandler { - public async Task UploadAsync(UserId userId, ImageId imageId, IFormFile formFile, bool hidden, + public async Task UploadAsync(UserId userId, ImageId imageId, IFormFile formFile, bool hidden, DateTime uploadedAt, CancellationToken cancellationToken = default) { - var fileUploadCommand = fileUploadCommandFactory.CreateCommand(userId, imageId, formFile); + await using var stream = formFile.OpenReadStream(); + + var fileUploadCommand = + fileUploadCommandFactory.CreateCommand(userId, imageId, formFile.Length, formFile.ContentType, stream); var metadataUploadCommand = metadataUploadCommandFactory.CreateCommand(userId, imageId, formFile.FileName, hidden, uploadedAt); var publishNewMessageCommand = publishNewMessageCommandFactory.CreateCommand(userId, imageId); @@ -36,6 +36,6 @@ public async Task UploadAsync(UserId userId, ImageId imag await commands.ExecuteAsync(cancellationToken).ConfigureAwait(false); - return new ImageUploadedResponse(imageId, userId, formFile.FileName, hidden, uploadedAt); + return new ImageUploadedDTO(imageId, userId, formFile.FileName, hidden, uploadedAt); } } \ No newline at end of file diff --git a/ImageHosting.Storage/Features/Images/Models/AddTagsCommand.cs b/src/ImageHosting.Storage.WebApi/Features/Images/Models/AddTagsCommand.cs similarity index 50% rename from ImageHosting.Storage/Features/Images/Models/AddTagsCommand.cs rename to src/ImageHosting.Storage.WebApi/Features/Images/Models/AddTagsCommand.cs index 6fa6347..94acfa2 100644 --- a/ImageHosting.Storage/Features/Images/Models/AddTagsCommand.cs +++ b/src/ImageHosting.Storage.WebApi/Features/Images/Models/AddTagsCommand.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; - -namespace ImageHosting.Storage.Features.Images.Models; +namespace ImageHosting.Storage.WebApi.Features.Images.Models; public class AddTagsCommand { diff --git a/ImageHosting.Storage/Features/Images/Models/AddTagsResponse.cs b/src/ImageHosting.Storage.WebApi/Features/Images/Models/AddTagsResponse.cs similarity index 50% rename from ImageHosting.Storage/Features/Images/Models/AddTagsResponse.cs rename to src/ImageHosting.Storage.WebApi/Features/Images/Models/AddTagsResponse.cs index 730c285..a077f1e 100644 --- a/ImageHosting.Storage/Features/Images/Models/AddTagsResponse.cs +++ b/src/ImageHosting.Storage.WebApi/Features/Images/Models/AddTagsResponse.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; - -namespace ImageHosting.Storage.Features.Images.Models; +namespace ImageHosting.Storage.WebApi.Features.Images.Models; public class AddTagsResponse { diff --git a/ImageHosting.Storage/Features/Images/Models/GetImageAssetParams.cs b/src/ImageHosting.Storage.WebApi/Features/Images/Models/GetImageAssetParams.cs similarity index 84% rename from ImageHosting.Storage/Features/Images/Models/GetImageAssetParams.cs rename to src/ImageHosting.Storage.WebApi/Features/Images/Models/GetImageAssetParams.cs index 2355f00..4e0fba6 100644 --- a/ImageHosting.Storage/Features/Images/Models/GetImageAssetParams.cs +++ b/src/ImageHosting.Storage.WebApi/Features/Images/Models/GetImageAssetParams.cs @@ -1,10 +1,8 @@ -using System; -using System.Linq; using FluentValidation; -using ImageHosting.Persistence.ValueTypes; +using ImageHosting.Storage.Domain.ValueTypes; using Microsoft.AspNetCore.Mvc; -namespace ImageHosting.Storage.Features.Images.Models; +namespace ImageHosting.Storage.WebApi.Features.Images.Models; public class GetImageAssetParams { diff --git a/ImageHosting.Storage/Features/Images/Models/GetImageAssetResult.cs b/src/ImageHosting.Storage.WebApi/Features/Images/Models/GetImageAssetResult.cs similarity index 66% rename from ImageHosting.Storage/Features/Images/Models/GetImageAssetResult.cs rename to src/ImageHosting.Storage.WebApi/Features/Images/Models/GetImageAssetResult.cs index 84f48f3..2990d8b 100644 --- a/ImageHosting.Storage/Features/Images/Models/GetImageAssetResult.cs +++ b/src/ImageHosting.Storage.WebApi/Features/Images/Models/GetImageAssetResult.cs @@ -1,6 +1,4 @@ -using System.IO; - -namespace ImageHosting.Storage.Features.Images.Models; +namespace ImageHosting.Storage.WebApi.Features.Images.Models; public class GetImageAssetResult { diff --git a/ImageHosting.Storage/Features/Images/Models/ReadImage.cs b/src/ImageHosting.Storage.WebApi/Features/Images/Models/ReadImage.cs similarity index 69% rename from ImageHosting.Storage/Features/Images/Models/ReadImage.cs rename to src/ImageHosting.Storage.WebApi/Features/Images/Models/ReadImage.cs index 7aba299..7f54a0e 100644 --- a/ImageHosting.Storage/Features/Images/Models/ReadImage.cs +++ b/src/ImageHosting.Storage.WebApi/Features/Images/Models/ReadImage.cs @@ -1,8 +1,6 @@ -using System; -using System.Collections.Generic; -using ImageHosting.Persistence.ValueTypes; +using ImageHosting.Storage.Domain.ValueTypes; -namespace ImageHosting.Storage.Features.Images.Models; +namespace ImageHosting.Storage.WebApi.Features.Images.Models; public class ReadImage { diff --git a/ImageHosting.Storage/Features/Images/Models/SizeValues.cs b/src/ImageHosting.Storage.WebApi/Features/Images/Models/SizeValues.cs similarity index 59% rename from ImageHosting.Storage/Features/Images/Models/SizeValues.cs rename to src/ImageHosting.Storage.WebApi/Features/Images/Models/SizeValues.cs index 5ae0c1a..c4fc3a9 100644 --- a/ImageHosting.Storage/Features/Images/Models/SizeValues.cs +++ b/src/ImageHosting.Storage.WebApi/Features/Images/Models/SizeValues.cs @@ -1,4 +1,4 @@ -namespace ImageHosting.Storage.Features.Images.Models; +namespace ImageHosting.Storage.WebApi.Features.Images.Models; public enum SizeValues : short { diff --git a/ImageHosting.Storage/Features/Images/Models/UpdateNameCommand.cs b/src/ImageHosting.Storage.WebApi/Features/Images/Models/UpdateNameCommand.cs similarity index 82% rename from ImageHosting.Storage/Features/Images/Models/UpdateNameCommand.cs rename to src/ImageHosting.Storage.WebApi/Features/Images/Models/UpdateNameCommand.cs index 501650e..6cca651 100644 --- a/ImageHosting.Storage/Features/Images/Models/UpdateNameCommand.cs +++ b/src/ImageHosting.Storage.WebApi/Features/Images/Models/UpdateNameCommand.cs @@ -1,6 +1,6 @@ using FluentValidation; -namespace ImageHosting.Storage.Features.Images.Models; +namespace ImageHosting.Storage.WebApi.Features.Images.Models; public class UpdateNameCommand { diff --git a/src/ImageHosting.Storage.WebApi/Features/Images/Services/FileUploadCommand.cs b/src/ImageHosting.Storage.WebApi/Features/Images/Services/FileUploadCommand.cs new file mode 100644 index 0000000..fa5dd45 --- /dev/null +++ b/src/ImageHosting.Storage.WebApi/Features/Images/Services/FileUploadCommand.cs @@ -0,0 +1,40 @@ +using ImageHosting.Storage.Application.DTOs; +using ImageHosting.Storage.Application.Services; +using ImageHosting.Storage.Domain.Common; +using ImageHosting.Storage.Domain.ValueTypes; + +namespace ImageHosting.Storage.WebApi.Features.Images.Services; + +public interface IFileUploadCommandFactory +{ + IRollbackCommand CreateCommand(UserId userId, ImageId imageId, long length, string contentType, Stream stream); +} + +public class FileUploadCommandFactory(IFileService fileService) : IFileUploadCommandFactory +{ + public IRollbackCommand CreateCommand(UserId userId, ImageId imageId, long length, string contentType, + Stream stream) + { + return new FileUploadCommand(fileService, userId.ToString("D"), imageId.ToString("D"), length, contentType, + stream); + } +} + +public class FileUploadCommand( + IFileService fileService, + string userId, + string imageId, + long length, + string contentType, + Stream stream) + : IRollbackCommand +{ + public Task ExecuteAsync(CancellationToken cancellationToken = default) + { + return fileService.WriteFileAsync(new WriteFileDTO(userId, imageId, length, contentType, stream), + cancellationToken); + } + + public Task RollbackAsync(CancellationToken cancellationToken = default) => + fileService.RemoveFileAsync(new RemoveFileDTO(userId, imageId), cancellationToken); +} \ No newline at end of file diff --git a/ImageHosting.Storage/Features/Images/Services/MetadataUploadCommand.cs b/src/ImageHosting.Storage.WebApi/Features/Images/Services/MetadataUploadCommand.cs similarity index 73% rename from ImageHosting.Storage/Features/Images/Services/MetadataUploadCommand.cs rename to src/ImageHosting.Storage.WebApi/Features/Images/Services/MetadataUploadCommand.cs index 0f47904..63f4130 100644 --- a/ImageHosting.Storage/Features/Images/Services/MetadataUploadCommand.cs +++ b/src/ImageHosting.Storage.WebApi/Features/Images/Services/MetadataUploadCommand.cs @@ -1,11 +1,9 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using ImageHosting.Persistence.ValueTypes; -using ImageHosting.Storage.Features.Images.Models; -using ImageHosting.Storage.Models; +using ImageHosting.Storage.Application.DTOs; +using ImageHosting.Storage.Application.Services; +using ImageHosting.Storage.Domain.Common; +using ImageHosting.Storage.Domain.ValueTypes; -namespace ImageHosting.Storage.Features.Images.Services; +namespace ImageHosting.Storage.WebApi.Features.Images.Services; public interface IMetadataUploadCommandFactory { @@ -30,7 +28,7 @@ public class MetadataUploadCommand( : IRollbackCommand { public Task ExecuteAsync(CancellationToken cancellationToken = default) => - metadataService.WriteMetadataAsync(new ImageMetadata(imageId, objectName, userId, uploadedAt, hidden), + metadataService.WriteMetadataAsync(new ImageMetadataDTO(imageId, objectName, userId, uploadedAt, hidden), cancellationToken); public Task RollbackAsync(CancellationToken cancellationToken = default) => diff --git a/ImageHosting.Storage/Features/Images/Services/PublishNewMessageCommand.cs b/src/ImageHosting.Storage.WebApi/Features/Images/Services/PublishNewMessageCommand.cs similarity index 78% rename from ImageHosting.Storage/Features/Images/Services/PublishNewMessageCommand.cs rename to src/ImageHosting.Storage.WebApi/Features/Images/Services/PublishNewMessageCommand.cs index 2985c63..1cbb52b 100644 --- a/ImageHosting.Storage/Features/Images/Services/PublishNewMessageCommand.cs +++ b/src/ImageHosting.Storage.WebApi/Features/Images/Services/PublishNewMessageCommand.cs @@ -1,10 +1,9 @@ -using System.Threading; -using System.Threading.Tasks; -using ImageHosting.Persistence.ValueTypes; -using ImageHosting.Storage.Features.Images.Models; -using ImageHosting.Storage.Models; +using ImageHosting.Storage.Application.Services; +using ImageHosting.Storage.Domain.Common; +using ImageHosting.Storage.Domain.Messages; +using ImageHosting.Storage.Domain.ValueTypes; -namespace ImageHosting.Storage.Features.Images.Services; +namespace ImageHosting.Storage.WebApi.Features.Images.Services; public interface IPublishNewMessageCommandFactory { diff --git a/ImageHosting.Storage/Http/ValidationFilter.cs b/src/ImageHosting.Storage.WebApi/Filters/ValidationFilter.cs similarity index 85% rename from ImageHosting.Storage/Http/ValidationFilter.cs rename to src/ImageHosting.Storage.WebApi/Filters/ValidationFilter.cs index baa6676..85d5f9e 100644 --- a/ImageHosting.Storage/Http/ValidationFilter.cs +++ b/src/ImageHosting.Storage.WebApi/Filters/ValidationFilter.cs @@ -1,9 +1,6 @@ -using System.Linq; -using System.Threading.Tasks; using FluentValidation; -using Microsoft.AspNetCore.Http; -namespace ImageHosting.Storage.Http; +namespace ImageHosting.Storage.WebApi.Filters; public class ValidationFilter(IValidator validator) : IEndpointFilter { diff --git a/ImageHosting.Storage/ImageHosting.Storage.csproj b/src/ImageHosting.Storage.WebApi/ImageHosting.Storage.WebApi.csproj similarity index 54% rename from ImageHosting.Storage/ImageHosting.Storage.csproj rename to src/ImageHosting.Storage.WebApi/ImageHosting.Storage.WebApi.csproj index 63da267..2c720b0 100644 --- a/ImageHosting.Storage/ImageHosting.Storage.csproj +++ b/src/ImageHosting.Storage.WebApi/ImageHosting.Storage.WebApi.csproj @@ -3,28 +3,37 @@ net8.0 enable + enable Linux default + ImageHosting.Storage.WebApi + ImageHosting.Storage.WebApi + ..\.. + ..\..\docker-compose.dcproj - - + + + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + + + - + - + + diff --git a/ImageHosting.Storage/OpenApi/NamedSwaggerGenOptions.cs b/src/ImageHosting.Storage.WebApi/OpenApi/NamedSwaggerGenOptions.cs similarity index 81% rename from ImageHosting.Storage/OpenApi/NamedSwaggerGenOptions.cs rename to src/ImageHosting.Storage.WebApi/OpenApi/NamedSwaggerGenOptions.cs index c9d16ff..e56038e 100644 --- a/ImageHosting.Storage/OpenApi/NamedSwaggerGenOptions.cs +++ b/src/ImageHosting.Storage.WebApi/OpenApi/NamedSwaggerGenOptions.cs @@ -1,10 +1,9 @@ using Asp.Versioning.ApiExplorer; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; -namespace ImageHosting.Storage.OpenApi; +namespace ImageHosting.Storage.WebApi.OpenApi; public class NamedSwaggerGenOptions(IApiVersionDescriptionProvider provider) : IConfigureNamedOptions { @@ -14,7 +13,7 @@ public void Configure(SwaggerGenOptions options) { var info = new OpenApiInfo { - Title = $"ImageHosting.Storage {description.GroupName}", + Title = $"ImageHosting.Storage.WebApi {description.GroupName}", Version = description.ApiVersion.ToString() }; options.SwaggerDoc(description.GroupName, info); diff --git a/src/ImageHosting.Storage.WebApi/Options/ImagesOptions.cs b/src/ImageHosting.Storage.WebApi/Options/ImagesOptions.cs new file mode 100644 index 0000000..8af3622 --- /dev/null +++ b/src/ImageHosting.Storage.WebApi/Options/ImagesOptions.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations; + +namespace ImageHosting.Storage.WebApi.Options; + +public class ImagesOptions +{ + public const string SectionName = "Images"; + + [Required] public required Uri BaseUri { get; init; } +} \ No newline at end of file diff --git a/ImageHosting.Storage/Program.cs b/src/ImageHosting.Storage.WebApi/Program.cs similarity index 67% rename from ImageHosting.Storage/Program.cs rename to src/ImageHosting.Storage.WebApi/Program.cs index 5dc412b..b8cc0c4 100644 --- a/ImageHosting.Storage/Program.cs +++ b/src/ImageHosting.Storage.WebApi/Program.cs @@ -1,36 +1,28 @@ -using System; using Asp.Versioning; -using Confluent.Kafka; using FluentValidation; using Hellang.Middleware.ProblemDetails; using Hellang.Middleware.ProblemDetails.Mvc; -using ImageHosting.Persistence.DbContexts; -using ImageHosting.Persistence.Extensions.DependencyInjection; -using ImageHosting.Persistence.ValueTypes; -using ImageHosting.Storage; -using ImageHosting.Storage.Extensions.DependencyInjection; -using ImageHosting.Storage.Features.Images.Endpoints; -using ImageHosting.Storage.Features.Images.Extensions; -using ImageHosting.Storage.Features.Images.Models; -using ImageHosting.Storage.OpenApi; -using ImageHosting.Storage.Services; +using ImageHosting.Storage.Application.Services; +using ImageHosting.Storage.Domain.Messages; +using ImageHosting.Storage.Domain.ValueTypes; +using ImageHosting.Storage.Infrastructure.DbContexts; +using ImageHosting.Storage.Infrastructure.Extensions.DependencyInjection; +using ImageHosting.Storage.WebApi.Features.Images.Endpoints; +using ImageHosting.Storage.WebApi.Features.Images.Extensions; +using ImageHosting.Storage.WebApi.OpenApi; using MassTransit; -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Microsoft.OpenApi.Models; using Serilog; try { var builder = WebApplication.CreateBuilder(args); - + Log.Logger = new LoggerConfiguration() .ReadFrom.Configuration(builder.Configuration) .CreateLogger(); builder.Services.AddSerilog(); - + builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(options => { @@ -50,7 +42,6 @@ builder.Services.AddImageHostingDbContext("ImageHosting"); ProblemDetailsExtensions.AddProblemDetails(builder.Services) .AddProblemDetailsConventions(); - builder.Services.AddKafkaOptions(); builder.Services.AddInitializeUserBucket(); builder.Services.AddApiVersioning() .AddApiExplorer(options => @@ -60,11 +51,10 @@ }); builder.Services.ConfigureOptions(); builder.Services.AddValidatorsFromAssemblyContaining(typeof(Program)); - + builder.Services.AddInfrastructureServices(); + var newImagesTopicName = builder.Configuration["Kafka:NewImagesProducer:TopicName"]; var bootstrapServers = builder.Configuration.GetSection("Kafka:BootstrapServers").Get(); - var categoriesTopicName = builder.Configuration["Kafka:CategoriesConsumer:TopicName"]; - var categoriesGroupId = builder.Configuration["Kafka:CategoriesConsumer:GroupId"]; builder.Services.AddMassTransit(massTransit => { @@ -73,21 +63,12 @@ massTransit.AddRider(rider => { rider.AddProducer(newImagesTopicName); - rider.AddConsumer(consumerDefinitionType: typeof(AssignTagsConsumerDefinition)); - rider.UsingKafka((context, kafka) => - { - kafka.Host(bootstrapServers); - kafka.TopicEndpoint(categoriesTopicName, categoriesGroupId, endpoint => - { - endpoint.ConfigureConsumer(context); - endpoint.AutoOffsetReset = AutoOffsetReset.Earliest; - endpoint.ConcurrentDeliveryLimit = 10; - }); - }); + rider.UsingKafka((_, kafka) => kafka.Host(bootstrapServers)); }); }); - + + var app = builder.Build(); app.UseProblemDetails(); @@ -114,7 +95,7 @@ } }); } - + using (var serviceScope = app.Services.CreateScope()) { var dbContext = serviceScope.ServiceProvider.GetRequiredService(); diff --git a/src/ImageHosting.Storage.WebApi/Properties/launchSettings.json b/src/ImageHosting.Storage.WebApi/Properties/launchSettings.json new file mode 100644 index 0000000..a3758f0 --- /dev/null +++ b/src/ImageHosting.Storage.WebApi/Properties/launchSettings.json @@ -0,0 +1,25 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:8080" + }, + "Container (Dockerfile)": { + "commandName": "Docker", + "launchBrowser": true, + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", + "environmentVariables": { + "ASPNETCORE_HTTP_PORTS": "8080" + }, + "publishAllPorts": true, + "useSSL": false + } + }, + "$schema": "https://json.schemastore.org/launchsettings.json" +} \ No newline at end of file diff --git a/ImageHosting.Storage/appsettings.Development.json b/src/ImageHosting.Storage.WebApi/appsettings.Development.json similarity index 86% rename from ImageHosting.Storage/appsettings.Development.json rename to src/ImageHosting.Storage.WebApi/appsettings.Development.json index 7db621c..3bd20ff 100644 --- a/ImageHosting.Storage/appsettings.Development.json +++ b/src/ImageHosting.Storage.WebApi/appsettings.Development.json @@ -31,11 +31,6 @@ ], "NewImagesProducer": { "TopicName": "new-images.v1" - }, - "CategoriesConsumer": { - "TopicName": "categories.v1", - "GroupId": "image-tagger", - "Threshold": 0.8 } }, "Images": { diff --git a/ImageHosting.Storage.UnitTests/Generic/RollbackCommandsTests.cs b/tests/ImageHosting.Storage.UnitTests/Generic/RollbackCommandsTests.cs similarity index 97% rename from ImageHosting.Storage.UnitTests/Generic/RollbackCommandsTests.cs rename to tests/ImageHosting.Storage.UnitTests/Generic/RollbackCommandsTests.cs index 69d4ab4..4ead99b 100644 --- a/ImageHosting.Storage.UnitTests/Generic/RollbackCommandsTests.cs +++ b/tests/ImageHosting.Storage.UnitTests/Generic/RollbackCommandsTests.cs @@ -1,5 +1,5 @@ -using ImageHosting.Storage.Exceptions; -using ImageHosting.Storage.Models; +using ImageHosting.Storage.Domain.Common; +using ImageHosting.Storage.Domain.Exceptions; using NSubstitute.ExceptionExtensions; namespace ImageHosting.Storage.UnitTests.Generic; diff --git a/ImageHosting.Storage.UnitTests/ImageHosting.Storage.UnitTests.csproj b/tests/ImageHosting.Storage.UnitTests/ImageHosting.Storage.UnitTests.csproj similarity index 86% rename from ImageHosting.Storage.UnitTests/ImageHosting.Storage.UnitTests.csproj rename to tests/ImageHosting.Storage.UnitTests/ImageHosting.Storage.UnitTests.csproj index dedacd1..380b21f 100644 --- a/ImageHosting.Storage.UnitTests/ImageHosting.Storage.UnitTests.csproj +++ b/tests/ImageHosting.Storage.UnitTests/ImageHosting.Storage.UnitTests.csproj @@ -9,30 +9,30 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/ImageHosting.Storage.UnitTests/Services/UploadFileTests.cs b/tests/ImageHosting.Storage.UnitTests/Services/UploadFileTests.cs similarity index 93% rename from ImageHosting.Storage.UnitTests/Services/UploadFileTests.cs rename to tests/ImageHosting.Storage.UnitTests/Services/UploadFileTests.cs index de2a26b..a77bb37 100644 --- a/ImageHosting.Storage.UnitTests/Services/UploadFileTests.cs +++ b/tests/ImageHosting.Storage.UnitTests/Services/UploadFileTests.cs @@ -1,10 +1,12 @@ using Confluent.Kafka; -using ImageHosting.Persistence.ValueTypes; -using ImageHosting.Storage.Exceptions; -using ImageHosting.Storage.Features.Images.Exceptions; -using ImageHosting.Storage.Features.Images.Handlers; -using ImageHosting.Storage.Features.Images.Models; -using ImageHosting.Storage.Features.Images.Services; +using ImageHosting.Storage.Application.DTOs; +using ImageHosting.Storage.Application.Exceptions; +using ImageHosting.Storage.Application.Services; +using ImageHosting.Storage.Domain.Exceptions; +using ImageHosting.Storage.Domain.Messages; +using ImageHosting.Storage.Domain.ValueTypes; +using ImageHosting.Storage.WebApi.Features.Images.Handlers; +using ImageHosting.Storage.WebApi.Features.Images.Services; using Microsoft.EntityFrameworkCore; using NSubstitute.ExceptionExtensions; @@ -52,7 +54,7 @@ public async Task Upload_already_exists_file() var metadataService = Substitute.For(); var metadataUploadCommandFactory = new MetadataUploadCommandFactory(metadataService); var fileService = Substitute.For(); - fileService.WriteFileAsync(Arg.Any()) + fileService.WriteFileAsync(Arg.Any()) .ThrowsAsync(new ImageObjectAlreadyExistsException(userId.ToString(), formFile.FileName)); var newImageProducer = Substitute.For(); var publishNewMessageCommandFactory = new PublishNewMessageCommandFactory(newImageProducer); @@ -77,7 +79,7 @@ public async Task Write_already_exists_metadata() const bool hidden = false; var uploadedAt = DateTime.Now; var metadataService = Substitute.For(); - metadataService.WriteMetadataAsync(Arg.Any()) + metadataService.WriteMetadataAsync(Arg.Any()) .ThrowsAsync(); var metadataUploadCommandFactory = new MetadataUploadCommandFactory(metadataService); var fileService = Substitute.For(); diff --git a/ImageHosting.Storage.UnitTests/Usings.cs b/tests/ImageHosting.Storage.UnitTests/Usings.cs similarity index 100% rename from ImageHosting.Storage.UnitTests/Usings.cs rename to tests/ImageHosting.Storage.UnitTests/Usings.cs diff --git a/ImageHosting.Storage.UnitTests/Xunit2/FileFixture.cs b/tests/ImageHosting.Storage.UnitTests/Xunit2/FileFixture.cs similarity index 100% rename from ImageHosting.Storage.UnitTests/Xunit2/FileFixture.cs rename to tests/ImageHosting.Storage.UnitTests/Xunit2/FileFixture.cs