Kavita/API/Services/Tasks/BackupService.cs
Joe Milazzo 73d77e6264
Library Settings Modal + New Library Settings (#1660)
* Bump loader-utils from 2.0.3 to 2.0.4 in /UI/Web

Bumps [loader-utils](https://github.com/webpack/loader-utils) from 2.0.3 to 2.0.4.
- [Release notes](https://github.com/webpack/loader-utils/releases)
- [Changelog](https://github.com/webpack/loader-utils/blob/v2.0.4/CHANGELOG.md)
- [Commits](https://github.com/webpack/loader-utils/compare/v2.0.3...v2.0.4)

---
updated-dependencies:
- dependency-name: loader-utils
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

* Fixed want to read button on series detail not performing the correct action

* Started the library settings. Added ability to update a cover image for a library.

Updated backup db to also copy reading list (and now library) cover images.

* Integrated Edit Library into new settings (not tested) and hooked up a wizard-like flow for new library.

* Fixed a missing update event in backend when updating a library.

* Disable Save when form invalid. Do inline validation on Library name when user types to ensure the name is valid.

* Trim library names before you check anything

* General code cleanup

* Implemented advanced settings for library (include in dashboard, search, recommended) and ability to turn off folder watching for individual libraries.

Refactored some code to streamline perf in some flows.

* Removed old components replaced with new modal

* Code smells

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-18 09:38:32 -06:00

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.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 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));
}
}