Bookmark Refactor (#893)

* Fixed a bug which didn't take sort direction when not changing sort field

* Added foundation for Bookmark refactor

* Code broken, need to take a break. Issue is Getting bookmark image needs authentication but UI doesn't send.

* Implemented the ability to send bookmarked files to the web. Implemented ability to clear bookmarks on disk on a re-occuring basis.

* Updated the bookmark design to have it's own card that is self contained. View bookmarks modal has been updated to better lay out the cards.

* Refactored download bookmark codes to select files from bookmark directory directly rather than open underlying files.

* Wrote the basic logic to kick start the bookmark migration.

Added Installed Version into the DB to allow us to know more accurately when to run migrations

* Implemented the ability to change the bookmarks directory

* Updated all references to BookmarkDirectory to use setting from the DB.

Updated Server Settings page to use 2 col for some rows.

* Refactored some code to DirectoryService (hasWriteAccess) and fixed up some unit tests from a previous PR.

* Treat folders that start with ._ as blacklisted.

* Implemented Reset User preferences. Some extra code to prep for the migration.

* Implemented a migration for existing bookmarks to using new filesystem based bookmarks
This commit is contained in:
Joseph Milazzo 2022-01-05 09:56:49 -08:00 committed by GitHub
parent 04ffd1ef6f
commit a1a6333f09
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
45 changed files with 2006 additions and 103 deletions

View file

@ -21,6 +21,10 @@ namespace API.Services
string TempDirectory { get; }
string ConfigDirectory { get; }
/// <summary>
/// Original BookmarkDirectory. Only used for resetting directory. Use <see cref="ServerSettings.BackupDirectory"/> for actual path.
/// </summary>
string BookmarkDirectory { get; }
/// <summary>
/// Lists out top-level folders for a given directory. Filters out System and Hidden folders.
/// </summary>
/// <param name="rootPath">Absolute path of directory to scan.</param>
@ -50,7 +54,7 @@ namespace API.Services
void DeleteFiles(IEnumerable<string> files);
void RemoveNonImages(string directoryName);
void Flatten(string directoryName);
Task<bool> CheckWriteAccess(string directoryName);
}
public class DirectoryService : IDirectoryService
{
@ -60,6 +64,7 @@ namespace API.Services
public string LogDirectory { get; }
public string TempDirectory { get; }
public string ConfigDirectory { get; }
public string BookmarkDirectory { get; }
private readonly ILogger<DirectoryService> _logger;
private static readonly Regex ExcludeDirectories = new Regex(
@ -76,6 +81,7 @@ namespace API.Services
LogDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "logs");
TempDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "temp");
ConfigDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config");
BookmarkDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "bookmarks");
}
/// <summary>
@ -268,7 +274,7 @@ namespace API.Services
/// <returns></returns>
public bool IsDirectoryEmpty(string path)
{
return Directory.EnumerateFileSystemEntries(path).Any();
return FileSystem.Directory.Exists(path) && !FileSystem.Directory.EnumerateFileSystemEntries(path).Any();
}
public string[] GetFilesWithExtension(string path, string searchPatternExpression = "")
@ -682,6 +688,30 @@ namespace API.Services
FlattenDirectory(directory, directory, ref index);
}
/// <summary>
/// Checks whether a directory has write permissions
/// </summary>
/// <param name="directoryName">Fully qualified path</param>
/// <returns></returns>
public async Task<bool> CheckWriteAccess(string directoryName)
{
try
{
ExistOrCreate(directoryName);
await FileSystem.File.WriteAllTextAsync(
FileSystem.Path.Join(directoryName, "test.txt"),
string.Empty);
}
catch (Exception ex)
{
ClearAndDeleteDirectory(directoryName);
return false;
}
ClearAndDeleteDirectory(directoryName);
return true;
}
private void FlattenDirectory(IDirectoryInfo root, IDirectoryInfo directory, ref int directoryIndex)
{

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using API.Comparators;
@ -19,19 +20,29 @@ public interface IReaderService
Task<int> CapPageToChapter(int chapterId, int page);
Task<int> GetNextChapterIdAsync(int seriesId, int volumeId, int currentChapterId, int userId);
Task<int> GetPrevChapterIdAsync(int seriesId, int volumeId, int currentChapterId, int userId);
//Task<string> BookmarkFile();
}
public class ReaderService : IReaderService
{
private readonly IUnitOfWork _unitOfWork;
private readonly ILogger<ReaderService> _logger;
private readonly IDirectoryService _directoryService;
private readonly ICacheService _cacheService;
private readonly ChapterSortComparer _chapterSortComparer = new ChapterSortComparer();
private readonly ChapterSortComparerZeroFirst _chapterSortComparerForInChapterSorting = new ChapterSortComparerZeroFirst();
public ReaderService(IUnitOfWork unitOfWork, ILogger<ReaderService> logger)
public ReaderService(IUnitOfWork unitOfWork, ILogger<ReaderService> logger, IDirectoryService directoryService, ICacheService cacheService)
{
_unitOfWork = unitOfWork;
_logger = logger;
_directoryService = directoryService;
_cacheService = cacheService;
}
public static string FormatBookmarkFolderPath(string baseDirectory, int userId, int seriesId, int chapterId)
{
return Path.Join(baseDirectory, $"{userId}", $"{seriesId}", $"{chapterId}");
}
/// <summary>
@ -299,6 +310,17 @@ public class ReaderService : IReaderService
return -1;
}
// public async Task<string> BookmarkFile()
// {
// var chapter = await _cacheService.Ensure(bookmarkDto.ChapterId);
// if (chapter == null) return string.Empty;
// var path = _cacheService.GetCachedPagePath(chapter, bookmarkDto.Page);
// var fileInfo = new FileInfo(path);
//
// return _directoryService.CopyFileToDirectory(path, Path.Join(_directoryService.BookmarkDirectory,
// $"{user.Id}", $"{bookmarkDto.SeriesId}"));
// }
private static int GetNextChapterId(IEnumerable<ChapterDto> chapters, string currentChapterNumber)
{
var next = false;

View file

@ -1,4 +1,5 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using API.Data;
@ -19,6 +20,7 @@ namespace API.Services.Tasks
Task DeleteChapterCoverImages();
Task DeleteTagCoverImages();
Task CleanupBackups();
Task CleanupBookmarks();
}
/// <summary>
/// Cleans up after operations on reoccurring basis
@ -63,6 +65,9 @@ namespace API.Services.Tasks
await DeleteChapterCoverImages();
await SendProgress(0.7F);
await DeleteTagCoverImages();
await SendProgress(0.8F);
_logger.LogInformation("Cleaning old bookmarks");
await CleanupBookmarks();
await SendProgress(1F);
_logger.LogInformation("Cleanup finished");
}
@ -163,5 +168,34 @@ namespace API.Services.Tasks
}
_logger.LogInformation("Finished cleanup of Database backups at {Time}", DateTime.Now);
}
/// <summary>
/// Removes all files in the BookmarkDirectory that don't currently have bookmarks in the Database
/// </summary>
public async Task CleanupBookmarks()
{
// 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);
var bookmarks = (await _unitOfWork.UserRepository.GetAllBookmarksAsync())
.Select(b => _directoryService.FileSystem.Path.Join(bookmarkDirectory,
b.FileName));
var filesToDelete = allBookmarkFiles.Except(bookmarks);
_directoryService.DeleteFiles(filesToDelete);
// Clear all empty directories
foreach (var directory in _directoryService.FileSystem.Directory.GetDirectories(bookmarkDirectory))
{
if (_directoryService.FileSystem.Directory.GetFiles(directory).Length == 0 &&
_directoryService.FileSystem.Directory.GetDirectories(directory).Length == 0)
{
_directoryService.FileSystem.Directory.Delete(directory, false);
}
}
}
}
}

View file

@ -84,6 +84,8 @@ namespace API.Services.Tasks.Scanner
{
ParserInfo info = null;
// TODO: Emit event with what is being processed. It can look like Kavita isn't doing anything during file scan
if (Parser.Parser.IsEpub(path))
{
info = _readingItemService.Parse(path, rootPath, type);
@ -114,8 +116,6 @@ namespace API.Services.Tasks.Scanner
info.ComicInfo = GetComicInfo(path);
if (info.ComicInfo != null)
{
var sw = Stopwatch.StartNew();
if (!string.IsNullOrEmpty(info.ComicInfo.Volume))
{
info.Volumes = info.ComicInfo.Volume;

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
@ -222,9 +222,12 @@ public class ScannerService : IScannerService
}
// For Docker instances check if any of the folder roots are not available (ie disconnected volumes, etc) and fail if any of them are
if (library.Folders.Any(f => !_directoryService.IsDirectoryEmpty(f.Path)))
if (library.Folders.Any(f => _directoryService.IsDirectoryEmpty(f.Path)))
{
_logger.LogError("Some of the root folders for the library are empty. Either your mount has been disconnected or you are trying to delete all series in the library. Scan will be aborted. Check that your mount is connected or change the library's root folder and rescan.");
_logger.LogError("Some of the root folders for the library are empty. " +
"Either your mount has been disconnected or you are trying to delete all series in the library. " +
"Scan will be aborted. " +
"Check that your mount is connected or change the library's root folder and rescan");
return;
}