mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-06-08 08:04:15 -04:00
* Started with the redesign of the cover image chooser redesign to be less click intensive for volume/chapter images. Made some headings bold in card detail drawer. * Tweaked the styles * Moved where the info cards show * Added an ability to open a page settings drawer * Cleaned up some old code that isn't needed anymore. * Started implementing a list view. Refactored some title code to a dedicated component * List view implemented but way too many API calls. Either need caching or adjusting the SeriesDetail api. * Fixed a bug where if the progress bar didn't render on a card item while a download was in progress, the download indicator would be removed. * Large refactor to move a lot of the needed fields to the chapter and volume dtos for series detail. All fields are noted when only used in series detail. * Implemented cards for other tabs (except related) * Fixed the unit test which needed a mocked reader service call. * More cleanup around age rating and removing old code from the refactor. Commented out sorting till i feel motivated to work on that. * Some cleanup and restored cards as initial layout. Time to test this out and see if there is value add. * Added ability for Chapters tab to show the volume chapters belong to (if applicable) * Adding style fixes * Cover image updates, don't allow the first image (which is what is currently set) to respond to cover changes. Hide the ID field on list item for series detail. * Refactored the title for list item to be injectable * Cleaned up the selection code to make it less finicky on mobile when tap scrolling. * Refactored chapter tab to show volume as well on list view. * Ensure word count shows for Volumes * Started adding virtual scrolling, pushing up so Robbie can mess around * Started adding virtual scrolling, pushing up so Robbie can mess around * Fixed a bug where all chapters would come under specials * Show title data as accent if set. * Style fixes for virtual scroller * Restyling scroll * Implemented a way to show storyline with virtual scrolling * Show Word Count for chapters and cleaned up some logics. * I might have card layout working with virtual scroll code. * Some cleanup to hide more system like properties from info bar on series detail page. Fixed some missing time estimate info on storyline chapters. * Fixed a regression on series service when I integrated VolumeTitle. * Refactored read time to the backend. Added WordCount to the volume itself so we don't need to calculate on frontend. When asking to analyze files from a series, force the calculation. * Fixed SeriesDetail api code * Fixed up the code in the drawer to better update list/card mode * Basic infinite scroll implemented, however due to how we are updating the list to render, we are re-rending cards that haven't been touched. * Updated how we render and layout data for infinite scroll on library detail. It's almost there. * Started laying foundation for loading pages backwards. Removed lazy loading of images since we are now using virtual paging. * Hooked in some basic code to allow user to load a prev page with infinite scroll. * Fixed up series detail api and undid the non-lazy loaded images. Changed the router to help with this infinite loading on Firefox issue. * Fixed up some naming issues with Series Detail and added a new test. * This is an infinite scroll without pagination implementation. It is not fully done, but off to a good start. Virtual scroller with jump bar is working pretty well, def needs more polishing and tweaking. There are hacks in this implementation that need to be revisited. * Refactored code so that we don't use any pagination and load all results by default. * Misc code cleanup from build warnings. * Cleaned up some logic for how to display titles in list view. * More title cleanup for specials * Hooked up page layout to user preferences and renamed an existing user pref name to match the dto. * Swapped out everything but storyline with virtual-scroller over CDK * Removed CDK from series detail. * Default value for migration on page layout * Updating card layout for library detail page * fixing height for mobile * Moved scrollbar * Tweaked some styling for layouts when there is no data * Refactored the series cards into their own component to make it re-usable. * More tweaks on series info cards layout and enhanced a few pages with trackby functions. * Removed some dead code * Added download on series detail to actionables to fit in with new scroll strategy. * Fixed language not being updated and sent to the backend for series update. * Fixed a bad migration (if you ran any prior migration in this branch, you need to undo before you use this commit) * Adding sticky tabs * fixed mobile gap on sticky tab * Enhanced the card title for books to show number up front. * Adjusted the gutters on admin dashboard * Removed debug code * Removing duplicate book title * Cleaned up old references to cdk scroller * Implemented a basic jump bar scaling algorithm. Not perfect, but works pretty well. * Code smells Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
226 lines
8.7 KiB
C#
226 lines
8.7 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.IO.Compression;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using API.Data;
|
|
using API.Entities.Enums;
|
|
using API.Extensions;
|
|
using API.SignalR;
|
|
using Hangfire;
|
|
using Microsoft.AspNetCore.SignalR;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace API.Services.Tasks;
|
|
|
|
public interface IBackupService
|
|
{
|
|
Task BackupDatabase();
|
|
/// <summary>
|
|
/// Returns a list of full paths of the logs files detailed in <see cref="IConfiguration"/>.
|
|
/// </summary>
|
|
/// <param name="maxRollingFiles"></param>
|
|
/// <param name="logFileName"></param>
|
|
/// <returns></returns>
|
|
IEnumerable<string> GetLogFiles(int maxRollingFiles, string logFileName);
|
|
}
|
|
public class BackupService : IBackupService
|
|
{
|
|
private readonly IUnitOfWork _unitOfWork;
|
|
private readonly ILogger<BackupService> _logger;
|
|
private readonly IDirectoryService _directoryService;
|
|
private readonly IConfiguration _config;
|
|
private readonly IEventHub _eventHub;
|
|
|
|
private readonly IList<string> _backupFiles;
|
|
|
|
public BackupService(ILogger<BackupService> logger, IUnitOfWork unitOfWork,
|
|
IDirectoryService directoryService, IConfiguration config, IEventHub eventHub)
|
|
{
|
|
_unitOfWork = unitOfWork;
|
|
_logger = logger;
|
|
_directoryService = directoryService;
|
|
_config = config;
|
|
_eventHub = eventHub;
|
|
|
|
_backupFiles = new List<string>()
|
|
{
|
|
"appsettings.json",
|
|
"Hangfire.db", // This is not used atm
|
|
"Hangfire-log.db", // This is not used atm
|
|
"kavita.db",
|
|
"kavita.db-shm", // This wont always be there
|
|
"kavita.db-wal" // This wont always be there
|
|
};
|
|
}
|
|
|
|
public IEnumerable<string> GetLogFiles(int maxRollingFiles, string logFileName)
|
|
{
|
|
var multipleFileRegex = maxRollingFiles > 0 ? @"\d*" : string.Empty;
|
|
var fi = _directoryService.FileSystem.FileInfo.FromFileName(logFileName);
|
|
|
|
var files = maxRollingFiles > 0
|
|
? _directoryService.GetFiles(_directoryService.LogDirectory,
|
|
$@"{_directoryService.FileSystem.Path.GetFileNameWithoutExtension(fi.Name)}{multipleFileRegex}\.log")
|
|
: new[] {_directoryService.FileSystem.Path.Join(_directoryService.LogDirectory, "kavita.log")};
|
|
return files;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Will backup anything that needs to be backed up. This includes logs, setting files, bare minimum cover images (just locked and first cover).
|
|
/// </summary>
|
|
[AutomaticRetry(Attempts = 3, LogEvents = false, OnAttemptsExceeded = AttemptsExceededAction.Fail)]
|
|
public async Task BackupDatabase()
|
|
{
|
|
_logger.LogInformation("Beginning backup of Database at {BackupTime}", DateTime.Now);
|
|
var backupDirectory = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BackupDirectory)).Value;
|
|
|
|
_logger.LogDebug("Backing up to {BackupDirectory}", backupDirectory);
|
|
if (!_directoryService.ExistOrCreate(backupDirectory))
|
|
{
|
|
_logger.LogCritical("Could not write to {BackupDirectory}; aborting backup", backupDirectory);
|
|
await _eventHub.SendMessageAsync(MessageFactory.Error,
|
|
MessageFactory.ErrorEvent("Backup Service Error",$"Could not write to {backupDirectory}; aborting backup"));
|
|
return;
|
|
}
|
|
|
|
await SendProgress(0F, "Started backup");
|
|
await SendProgress(0.1F, "Copying core files");
|
|
|
|
var dateString = $"{DateTime.Now.ToShortDateString()}_{DateTime.Now.ToLongTimeString()}".Replace("/", "_").Replace(":", "_");
|
|
var zipPath = _directoryService.FileSystem.Path.Join(backupDirectory, $"kavita_backup_{dateString}.zip");
|
|
|
|
if (File.Exists(zipPath))
|
|
{
|
|
_logger.LogCritical("{ZipFile} already exists, aborting", zipPath);
|
|
await _eventHub.SendMessageAsync(MessageFactory.Error,
|
|
MessageFactory.ErrorEvent("Backup Service Error",$"{zipPath} already exists, aborting"));
|
|
return;
|
|
}
|
|
|
|
var tempDirectory = Path.Join(_directoryService.TempDirectory, dateString);
|
|
_directoryService.ExistOrCreate(tempDirectory);
|
|
_directoryService.ClearDirectory(tempDirectory);
|
|
|
|
_directoryService.CopyFilesToDirectory(
|
|
_backupFiles.Select(file => _directoryService.FileSystem.Path.Join(_directoryService.ConfigDirectory, file)).ToList(), tempDirectory);
|
|
|
|
CopyLogsToBackupDirectory(tempDirectory);
|
|
|
|
await SendProgress(0.25F, "Copying cover images");
|
|
|
|
await CopyCoverImagesToBackupDirectory(tempDirectory);
|
|
|
|
await SendProgress(0.5F, "Copying bookmarks");
|
|
|
|
await CopyBookmarksToBackupDirectory(tempDirectory);
|
|
|
|
await SendProgress(0.75F, "Copying themes");
|
|
|
|
CopyThemesToBackupDirectory(tempDirectory);
|
|
|
|
try
|
|
{
|
|
ZipFile.CreateFromDirectory(tempDirectory, zipPath);
|
|
}
|
|
catch (AggregateException ex)
|
|
{
|
|
_logger.LogError(ex, "There was an issue when archiving library backup");
|
|
}
|
|
|
|
_directoryService.ClearAndDeleteDirectory(tempDirectory);
|
|
_logger.LogInformation("Database backup completed");
|
|
await SendProgress(1F, "Completed backup");
|
|
}
|
|
|
|
private void CopyLogsToBackupDirectory(string tempDirectory)
|
|
{
|
|
var maxRollingFiles = _config.GetMaxRollingFiles();
|
|
var loggingSection = _config.GetLoggingFileName();
|
|
var files = GetLogFiles(maxRollingFiles, loggingSection);
|
|
_directoryService.CopyFilesToDirectory(files, _directoryService.FileSystem.Path.Join(tempDirectory, "logs"));
|
|
}
|
|
|
|
private async Task CopyCoverImagesToBackupDirectory(string tempDirectory)
|
|
{
|
|
var outputTempDir = Path.Join(tempDirectory, "covers");
|
|
_directoryService.ExistOrCreate(outputTempDir);
|
|
|
|
try
|
|
{
|
|
var seriesImages = await _unitOfWork.SeriesRepository.GetLockedCoverImagesAsync();
|
|
_directoryService.CopyFilesToDirectory(
|
|
seriesImages.Select(s => _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, s)), outputTempDir);
|
|
|
|
var collectionTags = await _unitOfWork.CollectionTagRepository.GetAllCoverImagesAsync();
|
|
_directoryService.CopyFilesToDirectory(
|
|
collectionTags.Select(s => _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, s)), outputTempDir);
|
|
|
|
var chapterImages = await _unitOfWork.ChapterRepository.GetCoverImagesForLockedChaptersAsync();
|
|
_directoryService.CopyFilesToDirectory(
|
|
chapterImages.Select(s => _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, s)), outputTempDir);
|
|
}
|
|
catch (IOException)
|
|
{
|
|
// Swallow exception. This can be a duplicate cover being copied as chapter and volumes can share same file.
|
|
}
|
|
|
|
if (!_directoryService.GetFiles(outputTempDir, searchOption: SearchOption.AllDirectories).Any())
|
|
{
|
|
_directoryService.ClearAndDeleteDirectory(outputTempDir);
|
|
}
|
|
}
|
|
|
|
private async Task CopyBookmarksToBackupDirectory(string tempDirectory)
|
|
{
|
|
var bookmarkDirectory =
|
|
(await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value;
|
|
|
|
var outputTempDir = Path.Join(tempDirectory, "bookmarks");
|
|
_directoryService.ExistOrCreate(outputTempDir);
|
|
|
|
try
|
|
{
|
|
_directoryService.CopyDirectoryToDirectory(bookmarkDirectory, outputTempDir);
|
|
}
|
|
catch (IOException)
|
|
{
|
|
// Swallow exception.
|
|
}
|
|
|
|
if (!_directoryService.GetFiles(outputTempDir, searchOption: SearchOption.AllDirectories).Any())
|
|
{
|
|
_directoryService.ClearAndDeleteDirectory(outputTempDir);
|
|
}
|
|
}
|
|
|
|
private void CopyThemesToBackupDirectory(string tempDirectory)
|
|
{
|
|
var outputTempDir = Path.Join(tempDirectory, "themes");
|
|
_directoryService.ExistOrCreate(outputTempDir);
|
|
|
|
try
|
|
{
|
|
_directoryService.CopyDirectoryToDirectory(_directoryService.SiteThemeDirectory, outputTempDir);
|
|
}
|
|
catch (IOException)
|
|
{
|
|
// Swallow exception.
|
|
}
|
|
|
|
if (!_directoryService.GetFiles(outputTempDir, searchOption: SearchOption.AllDirectories).Any())
|
|
{
|
|
_directoryService.ClearAndDeleteDirectory(outputTempDir);
|
|
}
|
|
}
|
|
|
|
private async Task SendProgress(float progress, string subtitle)
|
|
{
|
|
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
|
MessageFactory.BackupDatabaseProgressEvent(progress, subtitle));
|
|
}
|
|
|
|
}
|