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:
parent
04ffd1ef6f
commit
a1a6333f09
45 changed files with 2006 additions and 103 deletions
|
@ -136,10 +136,10 @@ namespace API.Controllers
|
|||
public async Task<ActionResult> DownloadBookmarkPages(DownloadBookmarkDto downloadBookmarkDto)
|
||||
{
|
||||
// We know that all bookmarks will be for one single seriesId
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(downloadBookmarkDto.Bookmarks.First().SeriesId);
|
||||
var totalFilePaths = new List<string>();
|
||||
|
||||
var tempFolder = $"download_{series.Id}_bookmarks";
|
||||
var tempFolder = $"download_{user.Id}_{series.Id}_bookmarks";
|
||||
var fullExtractPath = Path.Join(_directoryService.TempDirectory, tempFolder);
|
||||
if (_directoryService.FileSystem.DirectoryInfo.FromDirectoryName(fullExtractPath).Exists)
|
||||
{
|
||||
|
@ -148,42 +148,14 @@ namespace API.Controllers
|
|||
}
|
||||
_directoryService.ExistOrCreate(fullExtractPath);
|
||||
|
||||
var uniqueChapterIds = downloadBookmarkDto.Bookmarks.Select(b => b.ChapterId).Distinct().ToList();
|
||||
var bookmarkDirectory =
|
||||
(await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value;
|
||||
var files = (await _unitOfWork.UserRepository.GetAllBookmarksByIds(downloadBookmarkDto.Bookmarks
|
||||
.Select(b => b.Id)
|
||||
.ToList()))
|
||||
.Select(b => _directoryService.FileSystem.Path.Join(bookmarkDirectory, b.FileName));
|
||||
|
||||
foreach (var chapterId in uniqueChapterIds)
|
||||
{
|
||||
var chapterExtractPath = Path.Join(fullExtractPath, $"{series.Id}_bookmark_{chapterId}");
|
||||
var chapterPages = downloadBookmarkDto.Bookmarks.Where(b => b.ChapterId == chapterId)
|
||||
.Select(b => b.Page).ToList();
|
||||
var mangaFiles = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId);
|
||||
switch (series.Format)
|
||||
{
|
||||
case MangaFormat.Image:
|
||||
_directoryService.ExistOrCreate(chapterExtractPath);
|
||||
_directoryService.CopyFilesToDirectory(mangaFiles.Select(f => f.FilePath), chapterExtractPath, $"{chapterId}_");
|
||||
break;
|
||||
case MangaFormat.Archive:
|
||||
case MangaFormat.Pdf:
|
||||
_cacheService.ExtractChapterFiles(chapterExtractPath, mangaFiles.ToList());
|
||||
var originalFiles = _directoryService.GetFilesWithExtension(chapterExtractPath,
|
||||
Parser.Parser.ImageFileExtensions);
|
||||
_directoryService.CopyFilesToDirectory(originalFiles, chapterExtractPath, $"{chapterId}_");
|
||||
_directoryService.DeleteFiles(originalFiles);
|
||||
break;
|
||||
case MangaFormat.Epub:
|
||||
return BadRequest("Series is not in a valid format.");
|
||||
default:
|
||||
return BadRequest("Series is not in a valid format. Please rescan series and try again.");
|
||||
}
|
||||
|
||||
var files = _directoryService.GetFilesWithExtension(chapterExtractPath, Parser.Parser.ImageFileExtensions);
|
||||
// Filter out images that aren't in bookmarks
|
||||
Array.Sort(files, _numericComparer);
|
||||
totalFilePaths.AddRange(files.Where((_, i) => chapterPages.Contains(i)));
|
||||
}
|
||||
|
||||
|
||||
var (fileBytes, _) = await _archiveService.CreateZipForDownload(totalFilePaths,
|
||||
var (fileBytes, _) = await _archiveService.CreateZipForDownload(files,
|
||||
tempFolder);
|
||||
_directoryService.ClearAndDeleteDirectory(fullExtractPath);
|
||||
return File(fileBytes, DefaultContentType, $"{series.Name} - Bookmarks.zip");
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
@ -85,5 +86,27 @@ namespace API.Controllers
|
|||
Response.AddCacheHeader(path);
|
||||
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns image for a given bookmark page
|
||||
/// </summary>
|
||||
/// <remarks>This request is served unauthenticated, but user must be passed via api key to validate</remarks>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <param name="pageNum">Starts at 0</param>
|
||||
/// <param name="apiKey">API Key for user. Needed to authenticate request</param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("bookmark")]
|
||||
public async Task<ActionResult> GetBookmarkImage(int chapterId, int pageNum, string apiKey)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
|
||||
var bookmark = await _unitOfWork.UserRepository.GetBookmarkForPage(pageNum, chapterId, userId);
|
||||
if (bookmark == null) return BadRequest("Bookmark does not exist");
|
||||
|
||||
var bookmarkDirectory =
|
||||
(await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value;
|
||||
var file = new FileInfo(Path.Join(bookmarkDirectory, bookmark.FileName));
|
||||
var format = Path.GetExtension(file.FullName).Replace(".", "");
|
||||
return PhysicalFile(file.FullName, "image/" + format, Path.GetFileName(file.FullName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,8 +8,10 @@ using API.Data.Repositories;
|
|||
using API.DTOs;
|
||||
using API.DTOs.Reader;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Services;
|
||||
using API.Services.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
|
@ -24,15 +26,21 @@ namespace API.Controllers
|
|||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ILogger<ReaderController> _logger;
|
||||
private readonly IReaderService _readerService;
|
||||
private readonly IDirectoryService _directoryService;
|
||||
private readonly ICleanupService _cleanupService;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ReaderController(ICacheService cacheService,
|
||||
IUnitOfWork unitOfWork, ILogger<ReaderController> logger, IReaderService readerService)
|
||||
IUnitOfWork unitOfWork, ILogger<ReaderController> logger,
|
||||
IReaderService readerService, IDirectoryService directoryService,
|
||||
ICleanupService cleanupService)
|
||||
{
|
||||
_cacheService = cacheService;
|
||||
_unitOfWork = unitOfWork;
|
||||
_logger = logger;
|
||||
_readerService = readerService;
|
||||
_directoryService = directoryService;
|
||||
_cleanupService = cleanupService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -398,6 +406,14 @@ namespace API.Controllers
|
|||
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
{
|
||||
try
|
||||
{
|
||||
await _cleanupService.CleanupBookmarks();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "There was an issue cleaning up old bookmarks");
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
|
@ -455,6 +471,18 @@ namespace API.Controllers
|
|||
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<AppUserBookmark>();
|
||||
|
@ -464,6 +492,8 @@ namespace API.Controllers
|
|||
VolumeId = bookmarkDto.VolumeId,
|
||||
SeriesId = bookmarkDto.SeriesId,
|
||||
ChapterId = bookmarkDto.ChapterId,
|
||||
FileName = Path.Join($"{user.Id}", $"{bookmarkDto.SeriesId}", $"{bookmarkDto.ChapterId}", fileInfo.Name)
|
||||
|
||||
});
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ using API.Entities.Enums;
|
|||
using API.Extensions;
|
||||
using API.Helpers.Converters;
|
||||
using API.Services;
|
||||
using AutoMapper;
|
||||
using Kavita.Common;
|
||||
using Kavita.Common.Extensions;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
@ -23,13 +24,18 @@ namespace API.Controllers
|
|||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ITaskScheduler _taskScheduler;
|
||||
private readonly IAccountService _accountService;
|
||||
private readonly IDirectoryService _directoryService;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public SettingsController(ILogger<SettingsController> logger, IUnitOfWork unitOfWork, ITaskScheduler taskScheduler, IAccountService accountService)
|
||||
public SettingsController(ILogger<SettingsController> logger, IUnitOfWork unitOfWork, ITaskScheduler taskScheduler,
|
||||
IAccountService accountService, IDirectoryService directoryService, IMapper mapper)
|
||||
{
|
||||
_logger = logger;
|
||||
_unitOfWork = unitOfWork;
|
||||
_taskScheduler = taskScheduler;
|
||||
_accountService = accountService;
|
||||
_directoryService = directoryService;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
|
@ -45,11 +51,26 @@ namespace API.Controllers
|
|||
public async Task<ActionResult<ServerSettingDto>> GetSettings()
|
||||
{
|
||||
var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
||||
// TODO: Is this needed as it gets updated in the DB on startup
|
||||
settingsDto.Port = Configuration.Port;
|
||||
settingsDto.LoggingLevel = Configuration.LogLevel;
|
||||
return Ok(settingsDto);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("reset")]
|
||||
public async Task<ActionResult<ServerSettingDto>> ResetSettings()
|
||||
{
|
||||
_logger.LogInformation("{UserName} is resetting Server Settings", User.GetUsername());
|
||||
|
||||
|
||||
// We do not allow CacheDirectory changes, so we will ignore.
|
||||
// var currentSettings = await _unitOfWork.SettingsRepository.GetSettingsAsync();
|
||||
// currentSettings = Seed.DefaultSettings;
|
||||
|
||||
return await UpdateSettings(_mapper.Map<ServerSettingDto>(Seed.DefaultSettings));
|
||||
}
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<ServerSettingDto>> UpdateSettings(ServerSettingDto updateSettingsDto)
|
||||
|
@ -69,6 +90,20 @@ namespace API.Controllers
|
|||
// We do not allow CacheDirectory changes, so we will ignore.
|
||||
var currentSettings = await _unitOfWork.SettingsRepository.GetSettingsAsync();
|
||||
var updateAuthentication = false;
|
||||
var updateBookmarks = false;
|
||||
var originalBookmarkDirectory = _directoryService.BookmarkDirectory;
|
||||
|
||||
var bookmarkDirectory = updateSettingsDto.BookmarksDirectory;
|
||||
if (!updateSettingsDto.BookmarksDirectory.EndsWith("bookmarks") &&
|
||||
!updateSettingsDto.BookmarksDirectory.EndsWith("bookmarks/"))
|
||||
{
|
||||
bookmarkDirectory = _directoryService.FileSystem.Path.Join(updateSettingsDto.BookmarksDirectory, "bookmarks");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(updateSettingsDto.BookmarksDirectory))
|
||||
{
|
||||
bookmarkDirectory = _directoryService.BookmarkDirectory;
|
||||
}
|
||||
|
||||
foreach (var setting in currentSettings)
|
||||
{
|
||||
|
@ -117,6 +152,22 @@ namespace API.Controllers
|
|||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.BookmarkDirectory && bookmarkDirectory != setting.Value)
|
||||
{
|
||||
// Validate new directory can be used
|
||||
if (!await _directoryService.CheckWriteAccess(bookmarkDirectory))
|
||||
{
|
||||
return BadRequest("Bookmark Directory does not have correct permissions for Kavita to use");
|
||||
}
|
||||
|
||||
originalBookmarkDirectory = setting.Value;
|
||||
// Normalize the path deliminators. Just to look nice in DB, no functionality
|
||||
setting.Value = _directoryService.FileSystem.Path.GetFullPath(bookmarkDirectory);
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
updateBookmarks = true;
|
||||
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.EnableAuthentication && updateSettingsDto.EnableAuthentication + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = updateSettingsDto.EnableAuthentication + string.Empty;
|
||||
|
@ -159,6 +210,13 @@ namespace API.Controllers
|
|||
|
||||
_logger.LogInformation("Server authentication changed. Updated all non-admins to default password");
|
||||
}
|
||||
|
||||
if (updateBookmarks)
|
||||
{
|
||||
_directoryService.ExistOrCreate(bookmarkDirectory);
|
||||
_directoryService.CopyDirectoryToDirectory(originalBookmarkDirectory, bookmarkDirectory);
|
||||
_directoryService.ClearAndDeleteDirectory(originalBookmarkDirectory);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue