From 05c35a1cb6b9b2c1c6d6953481c24db507311ae9 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Tue, 8 Feb 2022 13:43:24 -0800 Subject: [PATCH] Bookmark Refactor (#1049) * Tweaked how the migration to change users with ChangePassword role happens. It will now only run once. * Refactored bookmarks into it's own service with unit tests. Bookmark management happens in real time and we no longer delete bookmarks on a schedule. This means once you bookmark something, even if you delete the entity, the files will remain. * Commented out a test that no longer is needed --- API.Tests/Services/BookmarkServiceTests.cs | 345 ++++++++++++++++++ API.Tests/Services/CleanupServiceTests.cs | 276 +++++++------- API/Controllers/ReaderController.cs | 73 +--- .../ApplicationServiceExtensions.cs | 1 + API/Services/BookmarkService.cs | 147 ++++++++ API/Services/DirectoryService.cs | 9 +- API/Services/Tasks/CleanupService.cs | 56 +-- API/Startup.cs | 8 +- 8 files changed, 685 insertions(+), 230 deletions(-) create mode 100644 API.Tests/Services/BookmarkServiceTests.cs create mode 100644 API/Services/BookmarkService.cs diff --git a/API.Tests/Services/BookmarkServiceTests.cs b/API.Tests/Services/BookmarkServiceTests.cs new file mode 100644 index 000000000..f83fb55ed --- /dev/null +++ b/API.Tests/Services/BookmarkServiceTests.cs @@ -0,0 +1,345 @@ +using System.Collections.Generic; +using System.Data.Common; +using System.IO; +using System.IO.Abstractions.TestingHelpers; +using System.Linq; +using System.Threading.Tasks; +using API.Data; +using API.Data.Repositories; +using API.DTOs.Reader; +using API.Entities; +using API.Entities.Enums; +using API.Services; +using API.Services.Tasks; +using API.SignalR; +using AutoMapper; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.Extensions.Logging; +using NetVips; +using NSubstitute; +using Xunit; + +namespace API.Tests.Services; + +public class BookmarkServiceTests +{ + private readonly ILogger _logger = Substitute.For>(); + private readonly IUnitOfWork _unitOfWork; + private readonly IHubContext _messageHub = Substitute.For>(); + + private readonly DbConnection _connection; + private readonly DataContext _context; + + private const string CacheDirectory = "C:/kavita/config/cache/"; + private const string CoverImageDirectory = "C:/kavita/config/covers/"; + private const string BackupDirectory = "C:/kavita/config/backups/"; + private const string BookmarkDirectory = "C:/kavita/config/bookmarks/"; + + + public BookmarkServiceTests() + { + var contextOptions = new DbContextOptionsBuilder() + .UseSqlite(CreateInMemoryDatabase()) + .Options; + _connection = RelationalOptionsExtension.Extract(contextOptions).Connection; + + _context = new DataContext(contextOptions); + Task.Run(SeedDb).GetAwaiter().GetResult(); + + _unitOfWork = new UnitOfWork(_context, Substitute.For(), null); + } + + #region Setup + + private static DbConnection CreateInMemoryDatabase() + { + var connection = new SqliteConnection("Filename=:memory:"); + + connection.Open(); + + return connection; + } + + public void Dispose() => _connection.Dispose(); + + private async Task SeedDb() + { + await _context.Database.MigrateAsync(); + var filesystem = CreateFileSystem(); + + await Seed.SeedSettings(_context, new DirectoryService(Substitute.For>(), filesystem)); + + var setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.CacheDirectory).SingleAsync(); + setting.Value = CacheDirectory; + + setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.BackupDirectory).SingleAsync(); + setting.Value = BackupDirectory; + + setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.BookmarkDirectory).SingleAsync(); + setting.Value = BookmarkDirectory; + + _context.ServerSetting.Update(setting); + + _context.Library.Add(new Library() + { + Name = "Manga", + Folders = new List() + { + new FolderPath() + { + Path = "C:/data/" + } + } + }); + return await _context.SaveChangesAsync() > 0; + } + + private async Task ResetDB() + { + _context.Series.RemoveRange(_context.Series.ToList()); + _context.Users.RemoveRange(_context.Users.ToList()); + _context.AppUserBookmark.RemoveRange(_context.AppUserBookmark.ToList()); + + await _context.SaveChangesAsync(); + } + + private static MockFileSystem CreateFileSystem() + { + var fileSystem = new MockFileSystem(); + fileSystem.Directory.SetCurrentDirectory("C:/kavita/"); + fileSystem.AddDirectory("C:/kavita/config/"); + fileSystem.AddDirectory(CacheDirectory); + fileSystem.AddDirectory(CoverImageDirectory); + fileSystem.AddDirectory(BackupDirectory); + fileSystem.AddDirectory(BookmarkDirectory); + fileSystem.AddDirectory("C:/data/"); + + return fileSystem; + } + + #endregion + + #region BookmarkPage + + [Fact] + public async Task BookmarkPage_ShouldCopyTheFileAndUpdateDB() + { + var filesystem = CreateFileSystem(); + filesystem.AddFile($"{CacheDirectory}1/0001.jpg", new MockFileData("123")); + + // Delete all Series to reset state + await ResetDB(); + + _context.Series.Add(new Series() + { + Name = "Test", + Library = new Library() { + Name = "Test LIb", + Type = LibraryType.Manga, + }, + Volumes = new List() + { + new Volume() + { + Chapters = new List() + { + new Chapter() + { + + } + } + } + } + }); + + _context.AppUser.Add(new AppUser() + { + UserName = "Joe" + }); + + await _context.SaveChangesAsync(); + + + var ds = new DirectoryService(Substitute.For>(), filesystem); + var bookmarkService = new BookmarkService(Substitute.For>(), _unitOfWork, ds); + var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Bookmarks); + + var result = await bookmarkService.BookmarkPage(user, new BookmarkDto() + { + ChapterId = 1, + Page = 1, + SeriesId = 1, + VolumeId = 1 + }, $"{CacheDirectory}1/0001.jpg"); + + + Assert.True(result); + Assert.Equal(1, ds.GetFiles(BookmarkDirectory, searchOption:SearchOption.AllDirectories).Count()); + Assert.NotNull(await _unitOfWork.UserRepository.GetBookmarkAsync(1)); + } + + [Fact] + public async Task BookmarkPage_ShouldDeleteFileOnUnbookmark() + { + var filesystem = CreateFileSystem(); + filesystem.AddFile($"{CacheDirectory}1/0001.jpg", new MockFileData("123")); + filesystem.AddFile($"{BookmarkDirectory}1/1/0001.jpg", new MockFileData("123")); + + // Delete all Series to reset state + await ResetDB(); + + _context.Series.Add(new Series() + { + Name = "Test", + Library = new Library() { + Name = "Test LIb", + Type = LibraryType.Manga, + }, + Volumes = new List() + { + new Volume() + { + Chapters = new List() + { + new Chapter() + { + + } + } + } + } + }); + + + _context.AppUser.Add(new AppUser() + { + UserName = "Joe", + Bookmarks = new List() + { + new AppUserBookmark() + { + Page = 1, + ChapterId = 1, + FileName = $"1/1/0001.jpg", + SeriesId = 1, + VolumeId = 1 + } + } + }); + + await _context.SaveChangesAsync(); + + + var ds = new DirectoryService(Substitute.For>(), filesystem); + var bookmarkService = new BookmarkService(Substitute.For>(), _unitOfWork, ds); + var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Bookmarks); + + var result = await bookmarkService.RemoveBookmarkPage(user, new BookmarkDto() + { + ChapterId = 1, + Page = 1, + SeriesId = 1, + VolumeId = 1 + }); + + + Assert.True(result); + Assert.Equal(0, ds.GetFiles(BookmarkDirectory, searchOption:SearchOption.AllDirectories).Count()); + Assert.Null(await _unitOfWork.UserRepository.GetBookmarkAsync(1)); + } + + #endregion + + #region DeleteBookmarkFiles + + [Fact] + public async Task DeleteBookmarkFiles_ShouldDeleteOnlyPassedFiles() + { + var filesystem = CreateFileSystem(); + filesystem.AddFile($"{CacheDirectory}1/0001.jpg", new MockFileData("123")); + filesystem.AddFile($"{BookmarkDirectory}1/1/1/0001.jpg", new MockFileData("123")); + filesystem.AddFile($"{BookmarkDirectory}1/2/1/0002.jpg", new MockFileData("123")); + filesystem.AddFile($"{BookmarkDirectory}1/2/1/0001.jpg", new MockFileData("123")); + + // Delete all Series to reset state + await ResetDB(); + + _context.Series.Add(new Series() + { + Name = "Test", + Library = new Library() { + Name = "Test LIb", + Type = LibraryType.Manga, + }, + Volumes = new List() + { + new Volume() + { + Chapters = new List() + { + new Chapter() + { + + } + } + } + } + }); + + + _context.AppUser.Add(new AppUser() + { + UserName = "Joe", + Bookmarks = new List() + { + new AppUserBookmark() + { + Page = 1, + ChapterId = 1, + FileName = $"1/1/1/0001.jpg", + SeriesId = 1, + VolumeId = 1 + }, + new AppUserBookmark() + { + Page = 2, + ChapterId = 1, + FileName = $"1/2/1/0002.jpg", + SeriesId = 2, + VolumeId = 1 + }, + new AppUserBookmark() + { + Page = 1, + ChapterId = 2, + FileName = $"1/2/1/0001.jpg", + SeriesId = 2, + VolumeId = 1 + } + } + }); + + await _context.SaveChangesAsync(); + + + var ds = new DirectoryService(Substitute.For>(), filesystem); + var bookmarkService = new BookmarkService(Substitute.For>(), _unitOfWork, ds); + + await bookmarkService.DeleteBookmarkFiles(new [] {new AppUserBookmark() + { + Page = 1, + ChapterId = 1, + FileName = $"1/1/1/0001.jpg", + SeriesId = 1, + VolumeId = 1 + }}); + + + Assert.Equal(2, ds.GetFiles(BookmarkDirectory, searchOption:SearchOption.AllDirectories).Count()); + Assert.False(ds.FileSystem.FileInfo.FromFileName(Path.Join(BookmarkDirectory, "1/1/1/0001.jpg")).Exists); + } + #endregion +} diff --git a/API.Tests/Services/CleanupServiceTests.cs b/API.Tests/Services/CleanupServiceTests.cs index 404a47702..77984a10c 100644 --- a/API.Tests/Services/CleanupServiceTests.cs +++ b/API.Tests/Services/CleanupServiceTests.cs @@ -364,142 +364,142 @@ public class CleanupServiceTests #endregion - #region CleanupBookmarks - - [Fact] - public async Task CleanupBookmarks_LeaveAllFiles() - { - var filesystem = CreateFileSystem(); - filesystem.AddFile($"{BookmarkDirectory}1/1/1/0001.jpg", new MockFileData("")); - filesystem.AddFile($"{BookmarkDirectory}1/1/1/0002.jpg", new MockFileData("")); - - // Delete all Series to reset state - await ResetDB(); - - _context.Series.Add(new Series() - { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - new Volume() - { - Chapters = new List() - { - new Chapter() - { - - } - } - } - } - }); - - await _context.SaveChangesAsync(); - - _context.AppUser.Add(new AppUser() - { - Bookmarks = new List() - { - new AppUserBookmark() - { - AppUserId = 1, - ChapterId = 1, - Page = 1, - FileName = "1/1/1/0001.jpg", - SeriesId = 1, - VolumeId = 1 - }, - new AppUserBookmark() - { - AppUserId = 1, - ChapterId = 1, - Page = 2, - FileName = "1/1/1/0002.jpg", - SeriesId = 1, - VolumeId = 1 - } - } - }); - - await _context.SaveChangesAsync(); - - - var ds = new DirectoryService(Substitute.For>(), filesystem); - var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub, - ds); - - await cleanupService.CleanupBookmarks(); - - Assert.Equal(2, ds.GetFiles(BookmarkDirectory, searchOption:SearchOption.AllDirectories).Count()); - - } - - [Fact] - public async Task CleanupBookmarks_LeavesOneFiles() - { - var filesystem = CreateFileSystem(); - filesystem.AddFile($"{BookmarkDirectory}1/1/1/0001.jpg", new MockFileData("")); - filesystem.AddFile($"{BookmarkDirectory}1/1/2/0002.jpg", new MockFileData("")); - - // Delete all Series to reset state - await ResetDB(); - - _context.Series.Add(new Series() - { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - new Volume() - { - Chapters = new List() - { - new Chapter() - { - - } - } - } - } - }); - - await _context.SaveChangesAsync(); - - _context.AppUser.Add(new AppUser() - { - Bookmarks = new List() - { - new AppUserBookmark() - { - AppUserId = 1, - ChapterId = 1, - Page = 1, - FileName = "1/1/1/0001.jpg", - SeriesId = 1, - VolumeId = 1 - } - } - }); - - await _context.SaveChangesAsync(); - - - var ds = new DirectoryService(Substitute.For>(), filesystem); - var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub, - ds); - - await cleanupService.CleanupBookmarks(); - - Assert.Equal(1, ds.GetFiles(BookmarkDirectory, searchOption:SearchOption.AllDirectories).Count()); - Assert.Equal(1, ds.FileSystem.Directory.GetDirectories($"{BookmarkDirectory}1/1/").Length); - } - - #endregion + // #region CleanupBookmarks + // + // [Fact] + // public async Task CleanupBookmarks_LeaveAllFiles() + // { + // var filesystem = CreateFileSystem(); + // filesystem.AddFile($"{BookmarkDirectory}1/1/1/0001.jpg", new MockFileData("")); + // filesystem.AddFile($"{BookmarkDirectory}1/1/1/0002.jpg", new MockFileData("")); + // + // // Delete all Series to reset state + // await ResetDB(); + // + // _context.Series.Add(new Series() + // { + // Name = "Test", + // Library = new Library() { + // Name = "Test LIb", + // Type = LibraryType.Manga, + // }, + // Volumes = new List() + // { + // new Volume() + // { + // Chapters = new List() + // { + // new Chapter() + // { + // + // } + // } + // } + // } + // }); + // + // await _context.SaveChangesAsync(); + // + // _context.AppUser.Add(new AppUser() + // { + // Bookmarks = new List() + // { + // new AppUserBookmark() + // { + // AppUserId = 1, + // ChapterId = 1, + // Page = 1, + // FileName = "1/1/1/0001.jpg", + // SeriesId = 1, + // VolumeId = 1 + // }, + // new AppUserBookmark() + // { + // AppUserId = 1, + // ChapterId = 1, + // Page = 2, + // FileName = "1/1/1/0002.jpg", + // SeriesId = 1, + // VolumeId = 1 + // } + // } + // }); + // + // await _context.SaveChangesAsync(); + // + // + // var ds = new DirectoryService(Substitute.For>(), filesystem); + // var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub, + // ds); + // + // await cleanupService.CleanupBookmarks(); + // + // Assert.Equal(2, ds.GetFiles(BookmarkDirectory, searchOption:SearchOption.AllDirectories).Count()); + // + // } + // + // [Fact] + // public async Task CleanupBookmarks_LeavesOneFiles() + // { + // var filesystem = CreateFileSystem(); + // filesystem.AddFile($"{BookmarkDirectory}1/1/1/0001.jpg", new MockFileData("")); + // filesystem.AddFile($"{BookmarkDirectory}1/1/2/0002.jpg", new MockFileData("")); + // + // // Delete all Series to reset state + // await ResetDB(); + // + // _context.Series.Add(new Series() + // { + // Name = "Test", + // Library = new Library() { + // Name = "Test LIb", + // Type = LibraryType.Manga, + // }, + // Volumes = new List() + // { + // new Volume() + // { + // Chapters = new List() + // { + // new Chapter() + // { + // + // } + // } + // } + // } + // }); + // + // await _context.SaveChangesAsync(); + // + // _context.AppUser.Add(new AppUser() + // { + // Bookmarks = new List() + // { + // new AppUserBookmark() + // { + // AppUserId = 1, + // ChapterId = 1, + // Page = 1, + // FileName = "1/1/1/0001.jpg", + // SeriesId = 1, + // VolumeId = 1 + // } + // } + // }); + // + // await _context.SaveChangesAsync(); + // + // + // var ds = new DirectoryService(Substitute.For>(), filesystem); + // var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub, + // ds); + // + // await cleanupService.CleanupBookmarks(); + // + // Assert.Equal(1, ds.GetFiles(BookmarkDirectory, searchOption:SearchOption.AllDirectories).Count()); + // Assert.Equal(1, ds.FileSystem.Directory.GetDirectories($"{BookmarkDirectory}1/1/").Length); + // } + // + // #endregion } diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index 7f4e3e1fc..909fabd87 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -28,12 +28,13 @@ namespace API.Controllers private readonly IReaderService _readerService; private readonly IDirectoryService _directoryService; private readonly ICleanupService _cleanupService; + private readonly IBookmarkService _bookmarkService; /// public ReaderController(ICacheService cacheService, IUnitOfWork unitOfWork, ILogger logger, IReaderService readerService, IDirectoryService directoryService, - ICleanupService cleanupService) + ICleanupService cleanupService, IBookmarkService bookmarkService) { _cacheService = cacheService; _unitOfWork = unitOfWork; @@ -41,6 +42,7 @@ namespace API.Controllers _readerService = readerService; _directoryService = directoryService; _cleanupService = cleanupService; + _bookmarkService = bookmarkService; } /// @@ -451,6 +453,7 @@ namespace API.Controllers if (user.Bookmarks == null) return Ok("Nothing to remove"); try { + var bookmarksToRemove = user.Bookmarks.Where(bmk => bmk.SeriesId == dto.SeriesId).ToList(); user.Bookmarks = user.Bookmarks.Where(bmk => bmk.SeriesId != dto.SeriesId).ToList(); _unitOfWork.UserRepository.Update(user); @@ -458,7 +461,7 @@ namespace API.Controllers { try { - await _cleanupService.CleanupBookmarks(); + await _bookmarkService.DeleteBookmarkFiles(bookmarksToRemove); } catch (Exception ex) { @@ -514,49 +517,17 @@ namespace API.Controllers { // Don't let user save past total pages. bookmarkDto.Page = await _readerService.CapPageToChapter(bookmarkDto.ChapterId, bookmarkDto.Page); + var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks); + var chapter = await _cacheService.Ensure(bookmarkDto.ChapterId); + if (chapter == null) return BadRequest("Could not find cached image. Reload and try again."); + var path = _cacheService.GetCachedPagePath(chapter, bookmarkDto.Page); - try + if (await _bookmarkService.BookmarkPage(user, bookmarkDto, path)) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks); - var userBookmark = - await _unitOfWork.UserRepository.GetBookmarkForPage(bookmarkDto.Page, bookmarkDto.ChapterId, user.Id); - - // We need to get the image - var chapter = await _cacheService.Ensure(bookmarkDto.ChapterId); - if (chapter == null) return BadRequest("There was an issue finding image file for reading"); - var path = _cacheService.GetCachedPagePath(chapter, bookmarkDto.Page); - var fileInfo = new FileInfo(path); - - var bookmarkDirectory = - (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value; - _directoryService.CopyFileToDirectory(path, Path.Join(bookmarkDirectory, - $"{user.Id}", $"{bookmarkDto.SeriesId}", $"{bookmarkDto.ChapterId}")); - - - if (userBookmark == null) - { - user.Bookmarks ??= new List(); - user.Bookmarks.Add(new AppUserBookmark() - { - Page = bookmarkDto.Page, - VolumeId = bookmarkDto.VolumeId, - SeriesId = bookmarkDto.SeriesId, - ChapterId = bookmarkDto.ChapterId, - FileName = Path.Join($"{user.Id}", $"{bookmarkDto.SeriesId}", $"{bookmarkDto.ChapterId}", fileInfo.Name) - - }); - _unitOfWork.UserRepository.Update(user); - } - - await _unitOfWork.CommitAsync(); - } - catch (Exception) - { - await _unitOfWork.RollbackAsync(); - return BadRequest("Could not save bookmark"); + return Ok(); } - return Ok(); + return BadRequest("Could not save bookmark"); } /// @@ -568,27 +539,11 @@ namespace API.Controllers public async Task UnBookmarkPage(BookmarkDto bookmarkDto) { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks); - if (user.Bookmarks == null) return Ok(); - try - { - var bookmarkToDelete = user.Bookmarks.SingleOrDefault(x => - x.ChapterId == bookmarkDto.ChapterId && x.AppUserId == user.Id && x.Page == bookmarkDto.Page && - x.SeriesId == bookmarkDto.SeriesId); - if (bookmarkToDelete != null) - { - _unitOfWork.UserRepository.Delete(bookmarkToDelete); - } - - if (await _unitOfWork.CommitAsync()) - { - return Ok(); - } - } - catch (Exception) + if (await _bookmarkService.RemoveBookmarkPage(user, bookmarkDto)) { - await _unitOfWork.RollbackAsync(); + return Ok(); } return BadRequest("Could not remove bookmark"); diff --git a/API/Extensions/ApplicationServiceExtensions.cs b/API/Extensions/ApplicationServiceExtensions.cs index f5f440275..f11f0a8d1 100644 --- a/API/Extensions/ApplicationServiceExtensions.cs +++ b/API/Extensions/ApplicationServiceExtensions.cs @@ -38,6 +38,7 @@ namespace API.Extensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/API/Services/BookmarkService.cs b/API/Services/BookmarkService.cs new file mode 100644 index 000000000..baf304bd3 --- /dev/null +++ b/API/Services/BookmarkService.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using API.Data; +using API.Data.Repositories; +using API.DTOs.Reader; +using API.Entities; +using API.Entities.Enums; +using Microsoft.Extensions.Logging; + +namespace API.Services; + +public interface IBookmarkService +{ + Task DeleteBookmarkFiles(IEnumerable bookmarks); + Task BookmarkPage(AppUser userWithBookmarks, BookmarkDto bookmarkDto, string imageToBookmark); + Task RemoveBookmarkPage(AppUser userWithBookmarks, BookmarkDto bookmarkDto); +} + +public class BookmarkService : IBookmarkService +{ + private readonly ILogger _logger; + private readonly IUnitOfWork _unitOfWork; + private readonly IDirectoryService _directoryService; + + public BookmarkService(ILogger logger, IUnitOfWork unitOfWork, IDirectoryService directoryService) + { + _logger = logger; + _unitOfWork = unitOfWork; + _directoryService = directoryService; + } + + /// + /// Deletes the files associated with the list of Bookmarks passed. Will clean up empty folders. + /// + /// + public async Task DeleteBookmarkFiles(IEnumerable bookmarks) + { + var bookmarkDirectory = + (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value; + + var bookmarkFilesToDelete = bookmarks.Select(b => Parser.Parser.NormalizePath( + _directoryService.FileSystem.Path.Join(bookmarkDirectory, + b.FileName))).ToList(); + + if (bookmarkFilesToDelete.Count == 0) return; + + _directoryService.DeleteFiles(bookmarkFilesToDelete); + + // Delete any leftover folders + foreach (var directory in _directoryService.FileSystem.Directory.GetDirectories(bookmarkDirectory, "", SearchOption.AllDirectories)) + { + if (_directoryService.FileSystem.Directory.GetFiles(directory, "", SearchOption.AllDirectories).Length == 0 && + _directoryService.FileSystem.Directory.GetDirectories(directory).Length == 0) + { + _directoryService.FileSystem.Directory.Delete(directory, false); + } + } + } + /// + /// Creates a new entry in the AppUserBookmarks and copies an image to BookmarkDirectory. + /// + /// An AppUser object with Bookmarks populated + /// + /// Full path to the cached image that is going to be copied + /// If the save to DB and copy was successful + public async Task BookmarkPage(AppUser userWithBookmarks, BookmarkDto bookmarkDto, string imageToBookmark) + { + try + { + var userBookmark = + await _unitOfWork.UserRepository.GetBookmarkForPage(bookmarkDto.Page, bookmarkDto.ChapterId, userWithBookmarks.Id); + + if (userBookmark != null) + { + _logger.LogError("Bookmark already exists for Series {SeriesId}, Volume {VolumeId}, Chapter {ChapterId}, Page {PageNum}", bookmarkDto.SeriesId, bookmarkDto.VolumeId, bookmarkDto.ChapterId, bookmarkDto.Page); + return false; + } + + var fileInfo = new FileInfo(imageToBookmark); + var bookmarkDirectory = + (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value; + var targetFolderStem = BookmarkStem(userWithBookmarks.Id, bookmarkDto.SeriesId, bookmarkDto.ChapterId); + var targetFilepath = Path.Join(bookmarkDirectory, targetFolderStem); + + userWithBookmarks.Bookmarks ??= new List(); + userWithBookmarks.Bookmarks.Add(new AppUserBookmark() + { + Page = bookmarkDto.Page, + VolumeId = bookmarkDto.VolumeId, + SeriesId = bookmarkDto.SeriesId, + ChapterId = bookmarkDto.ChapterId, + FileName = Path.Join(targetFolderStem, fileInfo.Name) + }); + _directoryService.CopyFileToDirectory(imageToBookmark, targetFilepath); + _unitOfWork.UserRepository.Update(userWithBookmarks); + await _unitOfWork.CommitAsync(); + } + catch (Exception ex) + { + _logger.LogError(ex, "There was an exception when saving bookmark"); + await _unitOfWork.RollbackAsync(); + return false; + } + + return true; + } + + /// + /// Removes the Bookmark entity and the file from BookmarkDirectory + /// + /// + /// + /// + public async Task RemoveBookmarkPage(AppUser userWithBookmarks, BookmarkDto bookmarkDto) + { + if (userWithBookmarks.Bookmarks == null) return true; + try + { + var bookmarkToDelete = userWithBookmarks.Bookmarks.SingleOrDefault(x => + x.ChapterId == bookmarkDto.ChapterId && x.AppUserId == userWithBookmarks.Id && x.Page == bookmarkDto.Page && + x.SeriesId == bookmarkDto.SeriesId); + + if (bookmarkToDelete != null) + { + await DeleteBookmarkFiles(new[] {bookmarkToDelete}); + _unitOfWork.UserRepository.Delete(bookmarkToDelete); + } + + await _unitOfWork.CommitAsync(); + } + catch (Exception) + { + await _unitOfWork.RollbackAsync(); + return false; + } + + return true; + } + + private static string BookmarkStem(int userId, int seriesId, int chapterId) + { + return Path.Join($"{userId}", $"{seriesId}", $"{chapterId}"); + } +} diff --git a/API/Services/DirectoryService.cs b/API/Services/DirectoryService.cs index c47b3469c..24286c22d 100644 --- a/API/Services/DirectoryService.cs +++ b/API/Services/DirectoryService.cs @@ -198,11 +198,10 @@ namespace API.Services try { var fileInfo = FileSystem.FileInfo.FromFileName(fullFilePath); - if (fileInfo.Exists) - { - ExistOrCreate(targetDirectory); - fileInfo.CopyTo(FileSystem.Path.Join(targetDirectory, fileInfo.Name), true); - } + if (!fileInfo.Exists) return; + + ExistOrCreate(targetDirectory); + fileInfo.CopyTo(FileSystem.Path.Join(targetDirectory, fileInfo.Name), true); } catch (Exception ex) { diff --git a/API/Services/Tasks/CleanupService.cs b/API/Services/Tasks/CleanupService.cs index 05dfdc6c0..0e66a3860 100644 --- a/API/Services/Tasks/CleanupService.cs +++ b/API/Services/Tasks/CleanupService.cs @@ -66,8 +66,8 @@ namespace API.Services.Tasks await SendProgress(0.7F); await DeleteTagCoverImages(); await SendProgress(0.8F); - _logger.LogInformation("Cleaning old bookmarks"); - await CleanupBookmarks(); + //_logger.LogInformation("Cleaning old bookmarks"); + //await CleanupBookmarks(); await SendProgress(1F); _logger.LogInformation("Cleanup finished"); } @@ -172,33 +172,35 @@ namespace API.Services.Tasks /// /// Removes all files in the BookmarkDirectory that don't currently have bookmarks in the Database /// - public async Task CleanupBookmarks() + public Task CleanupBookmarks() { + // This is disabled for now while we test and validate a new method of deleting bookmarks + return Task.CompletedTask; // Search all files in bookmarks/ except bookmark files and delete those - var bookmarkDirectory = - (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value; - var allBookmarkFiles = _directoryService.GetFiles(bookmarkDirectory, searchOption: SearchOption.AllDirectories).Select(Parser.Parser.NormalizePath); - var bookmarks = (await _unitOfWork.UserRepository.GetAllBookmarksAsync()) - .Select(b => Parser.Parser.NormalizePath(_directoryService.FileSystem.Path.Join(bookmarkDirectory, - b.FileName))); - - - var filesToDelete = allBookmarkFiles.AsEnumerable().Except(bookmarks).ToList(); - _logger.LogDebug("[Bookmarks] Bookmark cleanup wants to delete {Count} files", filesToDelete.Count); - - if (filesToDelete.Count == 0) return; - - _directoryService.DeleteFiles(filesToDelete); - - // Clear all empty directories - foreach (var directory in _directoryService.FileSystem.Directory.GetDirectories(bookmarkDirectory, "", SearchOption.AllDirectories)) - { - if (_directoryService.FileSystem.Directory.GetFiles(directory, "", SearchOption.AllDirectories).Length == 0 && - _directoryService.FileSystem.Directory.GetDirectories(directory).Length == 0) - { - _directoryService.FileSystem.Directory.Delete(directory, false); - } - } + // var bookmarkDirectory = + // (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value; + // var allBookmarkFiles = _directoryService.GetFiles(bookmarkDirectory, searchOption: SearchOption.AllDirectories).Select(Parser.Parser.NormalizePath); + // var bookmarks = (await _unitOfWork.UserRepository.GetAllBookmarksAsync()) + // .Select(b => Parser.Parser.NormalizePath(_directoryService.FileSystem.Path.Join(bookmarkDirectory, + // b.FileName))); + // + // + // var filesToDelete = allBookmarkFiles.AsEnumerable().Except(bookmarks).ToList(); + // _logger.LogDebug("[Bookmarks] Bookmark cleanup wants to delete {Count} files", filesToDelete.Count); + // + // if (filesToDelete.Count == 0) return; + // + // _directoryService.DeleteFiles(filesToDelete); + // + // // Clear all empty directories + // foreach (var directory in _directoryService.FileSystem.Directory.GetDirectories(bookmarkDirectory, "", SearchOption.AllDirectories)) + // { + // if (_directoryService.FileSystem.Directory.GetFiles(directory, "", SearchOption.AllDirectories).Length == 0 && + // _directoryService.FileSystem.Directory.GetDirectories(directory).Length == 0) + // { + // _directoryService.FileSystem.Directory.Delete(directory, false); + // } + // } } } } diff --git a/API/Startup.cs b/API/Startup.cs index fce12b69d..b66c58a18 100644 --- a/API/Startup.cs +++ b/API/Startup.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Net; using System.Net.Sockets; using System.Threading.Tasks; +using API.Constants; using API.Data; using API.Entities; using API.Extensions; @@ -150,7 +151,12 @@ namespace API await MigrateBookmarks.Migrate(directoryService, unitOfWork, logger, cacheService); - await MigrateChangePasswordRoles.Migrate(unitOfWork, userManager); + // Only run this if we are upgrading + var usersWithRole = await userManager.GetUsersInRoleAsync(PolicyConstants.ChangePasswordRole); + if (usersWithRole.Count == 0) + { + await MigrateChangePasswordRoles.Migrate(unitOfWork, userManager); + } var requiresCoverImageMigration = !Directory.Exists(directoryService.CoverImageDirectory); try