mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-06-16 20:14:14 -04:00
* Fixed a scaling issue in the epub reader, where images could scale when they shouldn't. * Removed some caching on library/ api and added more output for a foreign key constraint * Hooked in Restricted Profile stat collection * Added a new boolean on age restrictions to explicitly allow unknowns or not. Since unknown is the default state of metadata, if users are allowed access to Unknown, age restricted content could leak. * Fixed a bug where sometimes series cover generation could fail under conditions where only specials existed. * Fixed foreign constraint issue when cleaning up series not seen at end of scan loop * Removed an additional epub parse when scanning and handled merging differently * Code smell
274 lines
12 KiB
C#
274 lines
12 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using API.Data;
|
|
using API.Data.Repositories;
|
|
using API.DTOs.Filtering;
|
|
using API.Entities;
|
|
using API.Entities.Enums;
|
|
using API.Helpers;
|
|
using API.SignalR;
|
|
using Hangfire;
|
|
using Microsoft.AspNetCore.SignalR;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace API.Services.Tasks;
|
|
|
|
public interface ICleanupService
|
|
{
|
|
Task Cleanup();
|
|
Task CleanupDbEntries();
|
|
void CleanupCacheAndTempDirectories();
|
|
Task DeleteSeriesCoverImages();
|
|
Task DeleteChapterCoverImages();
|
|
Task DeleteTagCoverImages();
|
|
Task CleanupBackups();
|
|
Task CleanupLogs();
|
|
void CleanupTemp();
|
|
/// <summary>
|
|
/// Responsible to remove Series from Want To Read when user's have fully read the series and the series has Publication Status of Completed or Cancelled.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
Task CleanupWantToRead();
|
|
}
|
|
/// <summary>
|
|
/// Cleans up after operations on reoccurring basis
|
|
/// </summary>
|
|
public class CleanupService : ICleanupService
|
|
{
|
|
private readonly ILogger<CleanupService> _logger;
|
|
private readonly IUnitOfWork _unitOfWork;
|
|
private readonly IEventHub _eventHub;
|
|
private readonly IDirectoryService _directoryService;
|
|
|
|
public CleanupService(ILogger<CleanupService> logger,
|
|
IUnitOfWork unitOfWork, IEventHub eventHub,
|
|
IDirectoryService directoryService)
|
|
{
|
|
_logger = logger;
|
|
_unitOfWork = unitOfWork;
|
|
_eventHub = eventHub;
|
|
_directoryService = directoryService;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Cleans up Temp, cache, deleted cover images, and old database backups
|
|
/// </summary>
|
|
[AutomaticRetry(Attempts = 3, LogEvents = false, OnAttemptsExceeded = AttemptsExceededAction.Fail)]
|
|
public async Task Cleanup()
|
|
{
|
|
_logger.LogInformation("Starting Cleanup");
|
|
await SendProgress(0F, "Starting cleanup");
|
|
_logger.LogInformation("Cleaning temp directory");
|
|
_directoryService.ClearDirectory(_directoryService.TempDirectory);
|
|
await SendProgress(0.1F, "Cleaning temp directory");
|
|
CleanupCacheAndTempDirectories();
|
|
await SendProgress(0.25F, "Cleaning old database backups");
|
|
_logger.LogInformation("Cleaning old database backups");
|
|
await CleanupBackups();
|
|
await SendProgress(0.50F, "Cleaning deleted cover images");
|
|
_logger.LogInformation("Cleaning deleted cover images");
|
|
await DeleteSeriesCoverImages();
|
|
await SendProgress(0.6F, "Cleaning deleted cover images");
|
|
await DeleteChapterCoverImages();
|
|
await SendProgress(0.7F, "Cleaning deleted cover images");
|
|
await DeleteTagCoverImages();
|
|
await DeleteReadingListCoverImages();
|
|
await SendProgress(0.8F, "Cleaning old logs");
|
|
await CleanupLogs();
|
|
await SendProgress(1F, "Cleanup finished");
|
|
_logger.LogInformation("Cleanup finished");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Cleans up abandon rows in the DB
|
|
/// </summary>
|
|
public async Task CleanupDbEntries()
|
|
{
|
|
await _unitOfWork.AppUserProgressRepository.CleanupAbandonedChapters();
|
|
await _unitOfWork.PersonRepository.RemoveAllPeopleNoLongerAssociated();
|
|
await _unitOfWork.GenreRepository.RemoveAllGenreNoLongerAssociated();
|
|
await _unitOfWork.CollectionTagRepository.RemoveTagsWithoutSeries();
|
|
}
|
|
|
|
private async Task SendProgress(float progress, string subtitle)
|
|
{
|
|
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
|
MessageFactory.CleanupProgressEvent(progress, subtitle));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes all series images that are not in the database. They must follow <see cref="ImageService.SeriesCoverImageRegex"/> filename pattern.
|
|
/// </summary>
|
|
public async Task DeleteSeriesCoverImages()
|
|
{
|
|
var images = await _unitOfWork.SeriesRepository.GetAllCoverImagesAsync();
|
|
var files = _directoryService.GetFiles(_directoryService.CoverImageDirectory, ImageService.SeriesCoverImageRegex);
|
|
_directoryService.DeleteFiles(files.Where(file => !images.Contains(_directoryService.FileSystem.Path.GetFileName(file))));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes all chapter/volume images that are not in the database. They must follow <see cref="ImageService.ChapterCoverImageRegex"/> filename pattern.
|
|
/// </summary>
|
|
public async Task DeleteChapterCoverImages()
|
|
{
|
|
var images = await _unitOfWork.ChapterRepository.GetAllCoverImagesAsync();
|
|
var files = _directoryService.GetFiles(_directoryService.CoverImageDirectory, ImageService.ChapterCoverImageRegex);
|
|
_directoryService.DeleteFiles(files.Where(file => !images.Contains(_directoryService.FileSystem.Path.GetFileName(file))));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes all collection tag images that are not in the database. They must follow <see cref="ImageService.CollectionTagCoverImageRegex"/> filename pattern.
|
|
/// </summary>
|
|
public async Task DeleteTagCoverImages()
|
|
{
|
|
var images = await _unitOfWork.CollectionTagRepository.GetAllCoverImagesAsync();
|
|
var files = _directoryService.GetFiles(_directoryService.CoverImageDirectory, ImageService.CollectionTagCoverImageRegex);
|
|
_directoryService.DeleteFiles(files.Where(file => !images.Contains(_directoryService.FileSystem.Path.GetFileName(file))));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes all reading list images that are not in the database. They must follow <see cref="ImageService.ReadingListCoverImageRegex"/> filename pattern.
|
|
/// </summary>
|
|
public async Task DeleteReadingListCoverImages()
|
|
{
|
|
var images = await _unitOfWork.ReadingListRepository.GetAllCoverImagesAsync();
|
|
var files = _directoryService.GetFiles(_directoryService.CoverImageDirectory, ImageService.ReadingListCoverImageRegex);
|
|
_directoryService.DeleteFiles(files.Where(file => !images.Contains(_directoryService.FileSystem.Path.GetFileName(file))));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes all files and directories in the cache and temp directory
|
|
/// </summary>
|
|
public void CleanupCacheAndTempDirectories()
|
|
{
|
|
_logger.LogInformation("Performing cleanup of Cache & Temp directories");
|
|
_directoryService.ExistOrCreate(_directoryService.CacheDirectory);
|
|
_directoryService.ExistOrCreate(_directoryService.TempDirectory);
|
|
|
|
try
|
|
{
|
|
_directoryService.ClearDirectory(_directoryService.CacheDirectory);
|
|
_directoryService.ClearDirectory(_directoryService.TempDirectory);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "There was an issue deleting one or more folders/files during cleanup");
|
|
}
|
|
|
|
_logger.LogInformation("Cache and temp directory purged");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes Database backups older than configured total backups. If all backups are older than total backups days, only the latest is kept.
|
|
/// </summary>
|
|
public async Task CleanupBackups()
|
|
{
|
|
var dayThreshold = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).TotalBackups;
|
|
_logger.LogInformation("Beginning cleanup of Database backups at {Time}", DateTime.Now);
|
|
var backupDirectory =
|
|
(await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BackupDirectory)).Value;
|
|
if (!_directoryService.Exists(backupDirectory)) return;
|
|
|
|
var deltaTime = DateTime.Today.Subtract(TimeSpan.FromDays(dayThreshold));
|
|
var allBackups = _directoryService.GetFiles(backupDirectory).ToList();
|
|
var expiredBackups = allBackups.Select(filename => _directoryService.FileSystem.FileInfo.FromFileName(filename))
|
|
.Where(f => f.CreationTime < deltaTime)
|
|
.ToList();
|
|
|
|
if (expiredBackups.Count == allBackups.Count)
|
|
{
|
|
_logger.LogInformation("All expired backups are older than {Threshold} days. Removing all but last backup", dayThreshold);
|
|
var toDelete = expiredBackups.OrderByDescending(f => f.CreationTime).ToList();
|
|
_directoryService.DeleteFiles(toDelete.Take(toDelete.Count - 1).Select(f => f.FullName));
|
|
}
|
|
else
|
|
{
|
|
_directoryService.DeleteFiles(expiredBackups.Select(f => f.FullName));
|
|
}
|
|
_logger.LogInformation("Finished cleanup of Database backups at {Time}", DateTime.Now);
|
|
}
|
|
|
|
public async Task CleanupLogs()
|
|
{
|
|
_logger.LogInformation("Performing cleanup of logs directory");
|
|
var dayThreshold = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).TotalLogs;
|
|
var deltaTime = DateTime.Today.Subtract(TimeSpan.FromDays(dayThreshold));
|
|
var allLogs = _directoryService.GetFiles(_directoryService.LogDirectory).ToList();
|
|
var expiredLogs = allLogs.Select(filename => _directoryService.FileSystem.FileInfo.FromFileName(filename))
|
|
.Where(f => f.CreationTime < deltaTime)
|
|
.ToList();
|
|
|
|
if (expiredLogs.Count == allLogs.Count)
|
|
{
|
|
_logger.LogInformation("All expired backups are older than {Threshold} days. Removing all but last backup", dayThreshold);
|
|
var toDelete = expiredLogs.OrderBy(f => f.CreationTime).ToList();
|
|
_directoryService.DeleteFiles(toDelete.Take(toDelete.Count - 1).Select(f => f.FullName));
|
|
}
|
|
else
|
|
{
|
|
_directoryService.DeleteFiles(expiredLogs.Select(f => f.FullName));
|
|
}
|
|
_logger.LogInformation("Finished cleanup of logs at {Time}", DateTime.Now);
|
|
}
|
|
|
|
public void CleanupTemp()
|
|
{
|
|
_logger.LogInformation("Performing cleanup of Temp directory");
|
|
_directoryService.ExistOrCreate(_directoryService.TempDirectory);
|
|
|
|
try
|
|
{
|
|
_directoryService.ClearDirectory(_directoryService.TempDirectory);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "There was an issue deleting one or more folders/files during cleanup");
|
|
}
|
|
|
|
_logger.LogInformation("Temp directory purged");
|
|
}
|
|
|
|
public async Task CleanupWantToRead()
|
|
{
|
|
_logger.LogInformation("Performing cleanup of Series that are Completed and have been fully read that are in Want To Read list");
|
|
|
|
var libraryIds = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()).Select(l => l.Id).ToList();
|
|
var filter = new FilterDto()
|
|
{
|
|
PublicationStatus = new List<PublicationStatus>()
|
|
{
|
|
PublicationStatus.Completed,
|
|
PublicationStatus.Cancelled
|
|
},
|
|
Libraries = libraryIds,
|
|
ReadStatus = new ReadStatus()
|
|
{
|
|
Read = true,
|
|
InProgress = false,
|
|
NotRead = false
|
|
}
|
|
};
|
|
foreach (var user in await _unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.WantToRead))
|
|
{
|
|
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(0, user.Id, new UserParams(), filter);
|
|
var seriesIds = series.Select(s => s.Id).ToList();
|
|
if (seriesIds.Count == 0) continue;
|
|
|
|
user.WantToRead ??= new List<Series>();
|
|
user.WantToRead = user.WantToRead.Where(s => !seriesIds.Contains(s.Id)).ToList();
|
|
_unitOfWork.UserRepository.Update(user);
|
|
}
|
|
|
|
if (_unitOfWork.HasChanges())
|
|
{
|
|
await _unitOfWork.CommitAsync();
|
|
}
|
|
|
|
_logger.LogInformation("Performing cleanup of Series that are Completed and have been fully read that are in Want To Read list, completed");
|
|
}
|
|
}
|