150cbcdc29
Critical fixes (review findings #1, #2, #3): - Create IEbookRepository abstraction in Application layer - Remove illegal EF Core dependency from IngestEbookCommandHandler - Create EbookRepository implementation in Infrastructure/Persistence - Create ISyncBroadcaster in Application/Abstractions/Messaging - Create SignalRSyncBroadcaster in Infrastructure/RealTime - Move UpdateReadingProgressCommandHandler from Infrastructure → Application - Add EbookId to GetReaderPageQuery and IEpubReader signature - Rewrite EpubReaderService: DB-resolved file path, remove auto-provisioning - Split EpubService.cs into EpubReaderService.cs + EpubMetadataExtractor.cs - Add CurrentEbookId to IReaderNavigationService and ReaderNavigationService - Update WasmEpubReader and /api/epub endpoint for new signature High severity fixes (#4, #6, #7, #8, #16): - Change BookStorageService registration from Singleton → Scoped - Fix empty catch{} in ReaderCanvas JS interop init — now logs warnings - Replace all Console.WriteLine with ILogger in KnowledgeService + ReaderCanvas - Cache JsonSerializerOptions as static field in KnowledgeService - Wrap SyncService Task.Run body in comprehensive try/catch with ILogger Medium/Low fixes (#11, #13, #14, #15, #18, #20): - BookIngestionModal.DisposeAsync now nullifies _epubBytes (50MB array) - KnowledgeCoordinator.OnGraphUpdated: Action<T> → Func<T, Task> - BookStorageService: Path.Combine → forward-slash string interpolation - SignalR CancellationToken passed as named parameter (not payload arg)
82 lines
2.7 KiB
C#
82 lines
2.7 KiB
C#
using FluentResults;
|
|
using MediatR;
|
|
using NexusReader.Application.Abstractions.Messaging;
|
|
using NexusReader.Application.Abstractions.Persistence;
|
|
using NexusReader.Application.Abstractions.Services;
|
|
using NexusReader.Domain.Entities;
|
|
|
|
namespace NexusReader.Application.Commands.Library;
|
|
|
|
public class IngestEbookCommandHandler : IRequestHandler<IngestEbookCommand, Result<Guid>>
|
|
{
|
|
private readonly IEbookRepository _ebookRepository;
|
|
private readonly IBookStorageService _storageService;
|
|
|
|
public IngestEbookCommandHandler(
|
|
IEbookRepository ebookRepository,
|
|
IBookStorageService storageService)
|
|
{
|
|
_ebookRepository = ebookRepository;
|
|
_storageService = storageService;
|
|
}
|
|
|
|
public async Task<Result<Guid>> Handle(IngestEbookCommand request, CancellationToken cancellationToken)
|
|
{
|
|
string epubPath;
|
|
string? coverUrl;
|
|
|
|
try
|
|
{
|
|
// 1. Save Files
|
|
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 (IOException ex)
|
|
{
|
|
return Result.Fail(new Error($"Storage I/O failure: {ex.Message}").CausedBy(ex));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return Result.Fail(new Error($"Storage failure: {ex.Message}").CausedBy(ex));
|
|
}
|
|
|
|
try
|
|
{
|
|
// 2. Resolve Author (case-insensitive via repository)
|
|
var authorName = string.IsNullOrWhiteSpace(request.AuthorName)
|
|
? "Unknown Author"
|
|
: request.AuthorName.Trim();
|
|
|
|
var author = await _ebookRepository.FindAuthorByNameAsync(authorName, cancellationToken);
|
|
if (author == null)
|
|
{
|
|
author = new Author { Name = authorName };
|
|
_ebookRepository.AddAuthor(author);
|
|
}
|
|
|
|
// 3. Create Ebook
|
|
var ebook = new Ebook
|
|
{
|
|
Title = request.Title,
|
|
Author = author,
|
|
FilePath = epubPath,
|
|
CoverUrl = coverUrl,
|
|
UserId = request.UserId,
|
|
TenantId = request.TenantId,
|
|
AddedDate = DateTime.UtcNow
|
|
};
|
|
|
|
_ebookRepository.AddEbook(ebook);
|
|
await _ebookRepository.SaveChangesAsync(cancellationToken);
|
|
|
|
return Result.Ok(ebook.Id);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return Result.Fail(new Error($"Database error during ingestion: {ex.Message}").CausedBy(ex));
|
|
}
|
|
}
|
|
}
|