mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-06-19 13:34:15 -04:00
* Introduced a new claim on the Token to get UserId as well as Username, thus allowing for many places of reduced DB calls. All users will need to reauthenticate. Introduced UTC Dates throughout the application, they are not exposed in all DTOs, that will come later when we fully switch over. For now, Utc dates will be updated along side timezone specific dates. Refactored get-progress/progress api to be 50% faster by reducing how much data is loaded from the query. * Speed up the following apis: collection/search, download/bookmarks, reader/bookmark-info, recommended/quick-reads, recommended/quick-catchup-reads, recommended/highly-rated, recommended/more-in, recommended/rediscover, want-to-read/ * Added a migration to sync all dates with their new UTC counterpart. * Added LastReadingProgressUtc onto ChapterDto for some browsing apis, but not all. Added LastReadingProgressUtc to reading list items. Refactored the migration to run raw SQL which is much faster. * Added LastReadingProgressUtc onto ChapterDto for some browsing apis, but not all. Added LastReadingProgressUtc to reading list items. Refactored the migration to run raw SQL which is much faster. * Fixed the unit tests * Fixed an issue with auto mapper which was causing progress page number to not get sent to UI * series/volume has chapter last reading progress * Added filesize and library name on reading list item dto for CDisplayEx. * Some minor code cleanup * Forgot to fill a field
235 lines
9.2 KiB
C#
235 lines
9.2 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.Logging;
|
|
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 all log files for Kavita
|
|
/// </summary>
|
|
/// <param name="rollFiles">If file rolling is enabled. Defaults to True.</param>
|
|
/// <returns></returns>
|
|
IEnumerable<string> GetLogFiles(bool rollFiles = LogLevelOptions.LogRollingEnabled);
|
|
}
|
|
public class BackupService : IBackupService
|
|
{
|
|
private readonly IUnitOfWork _unitOfWork;
|
|
private readonly ILogger<BackupService> _logger;
|
|
private readonly IDirectoryService _directoryService;
|
|
private readonly IEventHub _eventHub;
|
|
|
|
private readonly IList<string> _backupFiles;
|
|
|
|
public BackupService(ILogger<BackupService> logger, IUnitOfWork unitOfWork,
|
|
IDirectoryService directoryService, IEventHub eventHub)
|
|
{
|
|
_unitOfWork = unitOfWork;
|
|
_logger = logger;
|
|
_directoryService = directoryService;
|
|
_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
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a list of all log files for Kavita
|
|
/// </summary>
|
|
/// <param name="rollFiles">If file rolling is enabled. Defaults to True.</param>
|
|
/// <returns></returns>
|
|
public IEnumerable<string> GetLogFiles(bool rollFiles = LogLevelOptions.LogRollingEnabled)
|
|
{
|
|
var multipleFileRegex = rollFiles ? @"\d*" : string.Empty;
|
|
var fi = _directoryService.FileSystem.FileInfo.FromFileName(LogLevelOptions.LogFile);
|
|
|
|
var files = rollFiles
|
|
? _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.UtcNow.ToShortDateString()}_{DateTime.UtcNow.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 files = GetLogFiles();
|
|
_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);
|
|
|
|
var libraryImages = await _unitOfWork.LibraryRepository.GetAllCoverImagesAsync();
|
|
_directoryService.CopyFilesToDirectory(
|
|
libraryImages.Select(s => _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, s)), outputTempDir);
|
|
|
|
var readingListImages = await _unitOfWork.ReadingListRepository.GetAllCoverImagesAsync();
|
|
_directoryService.CopyFilesToDirectory(
|
|
readingListImages.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));
|
|
}
|
|
|
|
}
|