From 531ad3c2d032ee0766df4adaa6336652c8153754 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Jasi=C5=84ski?= Date: Tue, 12 May 2026 20:12:12 +0200 Subject: [PATCH] refactor: address PR review comments for ingestion workflow --- .../Services/IBookStorageService.cs | 22 +++++++++ .../Commands/Library/IngestEbookCommand.cs | 9 ++++ .../Library/IngestEbookCommandHandler.cs | 30 +++++++++---- .../Queries/Reader/LocalEpubMetadata.cs | 14 ++++++ .../DependencyInjection.cs | 2 +- .../Services/BookStorageService.cs | 45 ++++++++++++++----- .../Organisms/BookIngestionModal.razor | 3 +- .../Organisms/BookIngestionModal.razor.css | 10 ++--- .../Services/IReaderNavigationService.cs | 5 +++ .../Services/ReaderNavigationService.cs | 13 ++++++ src/NexusReader.Web.Client/Program.cs | 8 +++- src/NexusReader.Web/Program.cs | 1 - 12 files changed, 131 insertions(+), 31 deletions(-) diff --git a/src/NexusReader.Application/Abstractions/Services/IBookStorageService.cs b/src/NexusReader.Application/Abstractions/Services/IBookStorageService.cs index eb39b53..a45b420 100644 --- a/src/NexusReader.Application/Abstractions/Services/IBookStorageService.cs +++ b/src/NexusReader.Application/Abstractions/Services/IBookStorageService.cs @@ -1,7 +1,29 @@ namespace NexusReader.Application.Abstractions.Services; +/// +/// Service for managing ebook and cover file storage. +/// public interface IBookStorageService { + /// + /// Saves an ebook file and returns its relative path/URL. + /// Task SaveEbookAsync(byte[] data, string fileName); + + /// + /// Saves an ebook file using a stream and returns its relative path/URL. + /// + Task SaveEbookAsync(Stream data, string fileName); + + /// + /// Saves a cover image and returns its relative path/URL. + /// Returns null if no cover data is provided. + /// Task SaveCoverAsync(byte[] data, string fileName); + + /// + /// Saves a cover image using a stream and returns its relative path/URL. + /// Returns null if no cover data is provided. + /// + Task SaveCoverAsync(Stream data, string fileName); } diff --git a/src/NexusReader.Application/Commands/Library/IngestEbookCommand.cs b/src/NexusReader.Application/Commands/Library/IngestEbookCommand.cs index 18c11c4..7026e25 100644 --- a/src/NexusReader.Application/Commands/Library/IngestEbookCommand.cs +++ b/src/NexusReader.Application/Commands/Library/IngestEbookCommand.cs @@ -2,6 +2,15 @@ using NexusReader.Application.Abstractions.Messaging; namespace NexusReader.Application.Commands.Library; +/// +/// Command to ingest a new ebook into the library. +/// +/// The title of the book. +/// The name of the author. +/// The raw bytes of the cover image (optional). +/// The raw bytes of the EPUB file. +/// The ID of the user owning the book. +/// The tenant ID for multi-tenant isolation. Defaults to "global" for single-tenant or default usage. public record IngestEbookCommand( string Title, string AuthorName, diff --git a/src/NexusReader.Application/Commands/Library/IngestEbookCommandHandler.cs b/src/NexusReader.Application/Commands/Library/IngestEbookCommandHandler.cs index f01dcd3..0b2727d 100644 --- a/src/NexusReader.Application/Commands/Library/IngestEbookCommandHandler.cs +++ b/src/NexusReader.Application/Commands/Library/IngestEbookCommandHandler.cs @@ -23,18 +23,30 @@ public class IngestEbookCommandHandler : IRequestHandler> Handle(IngestEbookCommand request, CancellationToken cancellationToken) { + using var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + + string epubPath; + string? coverUrl; + try { - using var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - // 1. Save Files - var epubPath = await _storageService.SaveEbookAsync(request.EpubData, $"{request.Title}.epub"); - var coverUrl = request.CoverImage != null && request.CoverImage.Length > 0 + epubPath = await _storageService.SaveEbookAsync(request.EpubData, $"{request.Title}.epub"); + coverUrl = request.CoverImage != null && request.CoverImage.Length > 0 ? await _storageService.SaveCoverAsync(request.CoverImage, $"{request.Title}_cover.jpg") : null; + } + catch (Exception ex) + { + return Result.Fail(new Error($"Storage failure: {ex.Message}").CausedBy(ex)); + } + try + { // 2. Resolve Author var authorName = string.IsNullOrWhiteSpace(request.AuthorName) ? "Unknown Author" : request.AuthorName.Trim(); + + // Use case-insensitive comparison var author = await context.Authors .FirstOrDefaultAsync(a => a.Name.ToLower() == authorName.ToLower(), cancellationToken); @@ -42,8 +54,6 @@ public class IngestEbookCommandHandler : IRequestHandler +/// Represents metadata extracted from a local EPUB file. +/// public record LocalEpubMetadata { + /// + /// The title of the book. + /// public string Title { get; set; } = string.Empty; + + /// + /// The author(s) of the book. + /// public string Author { get; set; } = string.Empty; + + /// + /// The raw bytes of the cover image, if available. + /// public byte[]? CoverImage { get; set; } } diff --git a/src/NexusReader.Infrastructure/DependencyInjection.cs b/src/NexusReader.Infrastructure/DependencyInjection.cs index 2779061..3484315 100644 --- a/src/NexusReader.Infrastructure/DependencyInjection.cs +++ b/src/NexusReader.Infrastructure/DependencyInjection.cs @@ -75,7 +75,7 @@ public static class DependencyInjection services.AddScoped(); services.AddTransient(); services.AddTransient(); - services.AddScoped(); + services.AddSingleton(); services.AddAuthorizationCore(options => { diff --git a/src/NexusReader.Infrastructure/Services/BookStorageService.cs b/src/NexusReader.Infrastructure/Services/BookStorageService.cs index caaed22..58ec6ca 100644 --- a/src/NexusReader.Infrastructure/Services/BookStorageService.cs +++ b/src/NexusReader.Infrastructure/Services/BookStorageService.cs @@ -3,6 +3,10 @@ using NexusReader.Application.Abstractions.Services; namespace NexusReader.Infrastructure.Services; +/// +/// Infrastructure implementation of book storage using local filesystem. +/// All paths returned are relative to the web root. +/// public class BookStorageService : IBookStorageService { private readonly IWebHostEnvironment _environment; @@ -13,38 +17,55 @@ public class BookStorageService : IBookStorageService } public async Task SaveEbookAsync(byte[] data, string fileName) + { + using var stream = new MemoryStream(data); + return await SaveEbookAsync(stream, fileName); + } + + public async Task SaveEbookAsync(Stream data, string fileName) { var uploadsFolder = Path.Combine(_environment.WebRootPath, "uploads"); - if (!Directory.Exists(uploadsFolder)) - { - Directory.CreateDirectory(uploadsFolder); - } + EnsureDirectoryExists(uploadsFolder); var uniqueFileName = $"{Guid.NewGuid()}_{fileName}"; var filePath = Path.Combine(uploadsFolder, uniqueFileName); - await File.WriteAllBytesAsync(filePath, data); + using (var fileStream = new FileStream(filePath, FileMode.Create)) + { + await data.CopyToAsync(fileStream); + } - // Return relative path for web access if needed, but entity expects FilePath - // Let's return the relative path from wwwroot return Path.Combine("uploads", uniqueFileName); } public async Task SaveCoverAsync(byte[] data, string fileName) { if (data == null || data.Length == 0) return null; + using var stream = new MemoryStream(data); + return await SaveCoverAsync(stream, fileName); + } + public async Task SaveCoverAsync(Stream data, string fileName) + { var coversFolder = Path.Combine(_environment.WebRootPath, "covers"); - if (!Directory.Exists(coversFolder)) - { - Directory.CreateDirectory(coversFolder); - } + EnsureDirectoryExists(coversFolder); var uniqueFileName = $"{Guid.NewGuid()}_{fileName}"; var filePath = Path.Combine(coversFolder, uniqueFileName); - await File.WriteAllBytesAsync(filePath, data); + using (var fileStream = new FileStream(filePath, FileMode.Create)) + { + await data.CopyToAsync(fileStream); + } return Path.Combine("covers", uniqueFileName); } + + private void EnsureDirectoryExists(string path) + { + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + } + } } diff --git a/src/NexusReader.UI.Shared/Components/Organisms/BookIngestionModal.razor b/src/NexusReader.UI.Shared/Components/Organisms/BookIngestionModal.razor index b9d87c0..6caa2b0 100644 --- a/src/NexusReader.UI.Shared/Components/Organisms/BookIngestionModal.razor +++ b/src/NexusReader.UI.Shared/Components/Organisms/BookIngestionModal.razor @@ -216,8 +216,7 @@ if (result != null) { await CloseModal(); - // Navigate to the newly added book - // ReaderNavigation.NavigateToBook(result.Id); + ReaderNavigation.NavigateToBook(result.Id); } } else diff --git a/src/NexusReader.UI.Shared/Components/Organisms/BookIngestionModal.razor.css b/src/NexusReader.UI.Shared/Components/Organisms/BookIngestionModal.razor.css index e8689a2..64f412f 100644 --- a/src/NexusReader.UI.Shared/Components/Organisms/BookIngestionModal.razor.css +++ b/src/NexusReader.UI.Shared/Components/Organisms/BookIngestionModal.razor.css @@ -291,7 +291,7 @@ position: absolute; width: 150%; height: 150%; - background: radial-gradient(circle, rgba(0, 255, 153, 0.15) 0%, transparent 70%); + background: radial-gradient(circle, var(--nexus-neon-alpha, rgba(0, 255, 153, 0.15)) 0%, transparent 70%); animation: pulseGlow 4s infinite alternate; } @@ -299,7 +299,7 @@ color: var(--nexus-neon, #00ffaa); opacity: 0.5; z-index: 1; - filter: drop-shadow(0 0 10px rgba(0, 255, 153, 0.3)); + filter: drop-shadow(0 0 10px var(--nexus-neon-alpha-deep, rgba(0, 255, 153, 0.3))); } .verification-form { @@ -341,13 +341,13 @@ .error-message { margin-top: 1rem; - color: #ff5555; + color: var(--nexus-error, #ff5555); text-align: center; font-size: 0.9rem; padding: 0.75rem; - background: rgba(255, 85, 85, 0.1); + background: var(--nexus-error-alpha, rgba(255, 85, 85, 0.1)); border-radius: 8px; - border: 1px solid rgba(255, 85, 85, 0.2); + border: 1px solid var(--nexus-error-alpha-deep, rgba(255, 85, 85, 0.2)); } @keyframes pulseGlow { diff --git a/src/NexusReader.UI.Shared/Services/IReaderNavigationService.cs b/src/NexusReader.UI.Shared/Services/IReaderNavigationService.cs index 31c1b25..ff7096d 100644 --- a/src/NexusReader.UI.Shared/Services/IReaderNavigationService.cs +++ b/src/NexusReader.UI.Shared/Services/IReaderNavigationService.cs @@ -12,4 +12,9 @@ public interface IReaderNavigationService Task GoToNextChapter(); Task GoToPreviousChapter(); Task UpdateMetadataAsync(int currentIndex, int totalChapters, string title); + + /// + /// Navigates to the reader for a specific book. + /// + void NavigateToBook(Guid bookId); } diff --git a/src/NexusReader.UI.Shared/Services/ReaderNavigationService.cs b/src/NexusReader.UI.Shared/Services/ReaderNavigationService.cs index 837daf7..15e55ce 100644 --- a/src/NexusReader.UI.Shared/Services/ReaderNavigationService.cs +++ b/src/NexusReader.UI.Shared/Services/ReaderNavigationService.cs @@ -1,9 +1,17 @@ using System.Linq; +using Microsoft.AspNetCore.Components; namespace NexusReader.UI.Shared.Services; public class ReaderNavigationService : IReaderNavigationService { + private readonly NavigationManager _navigationManager; + + public ReaderNavigationService(NavigationManager navigationManager) + { + _navigationManager = navigationManager; + } + public int CurrentChapterIndex { get; private set; } = 0; public int TotalChapters { get; private set; } = 1; public string ChapterTitle { get; private set; } = "Loading..."; @@ -47,6 +55,11 @@ public class ReaderNavigationService : IReaderNavigationService } } + public void NavigateToBook(Guid bookId) + { + _navigationManager.NavigateTo($"/reader/{bookId}"); + } + private async Task NotifyNavigationChangedAsync() { var handlers = OnNavigationChanged?.GetInvocationList(); diff --git a/src/NexusReader.Web.Client/Program.cs b/src/NexusReader.Web.Client/Program.cs index cb7cd55..eeb4f74 100644 --- a/src/NexusReader.Web.Client/Program.cs +++ b/src/NexusReader.Web.Client/Program.cs @@ -68,6 +68,10 @@ public class ThrowingEmbeddingGenerator : IEmbeddingGenerator SaveEbookAsync(byte[] data, string fileName) => throw new NotSupportedException(); - public Task SaveCoverAsync(byte[] data, string fileName) => throw new NotSupportedException(); + private const string ErrorMessage = "File storage operations are not supported in the WASM client. Use the API endpoint for ingestion."; + + public Task SaveEbookAsync(byte[] data, string fileName) => throw new NotSupportedException(ErrorMessage); + public Task SaveEbookAsync(Stream data, string fileName) => throw new NotSupportedException(ErrorMessage); + public Task SaveCoverAsync(byte[] data, string fileName) => throw new NotSupportedException(ErrorMessage); + public Task SaveCoverAsync(Stream data, string fileName) => throw new NotSupportedException(ErrorMessage); } diff --git a/src/NexusReader.Web/Program.cs b/src/NexusReader.Web/Program.cs index 0ed60e3..0cdd861 100644 --- a/src/NexusReader.Web/Program.cs +++ b/src/NexusReader.Web/Program.cs @@ -67,7 +67,6 @@ builder.Services.AddHttpClient("NexusAPI", (sp, client) => }); builder.Services.AddScoped(sp => sp.GetRequiredService().CreateClient("NexusAPI")); -builder.Services.AddHttpContextAccessor(); builder.Services.AddScoped(); builder.Services.AddCascadingAuthenticationState();