diff --git a/API.Tests/ParserTest.cs b/API.Tests/ParserTest.cs index 895cca89a..6c1b5cfd5 100644 --- a/API.Tests/ParserTest.cs +++ b/API.Tests/ParserTest.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using API.Entities.Enums; using API.Parser; diff --git a/API.Tests/Services/ArchiveServiceTests.cs b/API.Tests/Services/ArchiveServiceTests.cs index 86f186b95..8f30a675b 100644 --- a/API.Tests/Services/ArchiveServiceTests.cs +++ b/API.Tests/Services/ArchiveServiceTests.cs @@ -1,6 +1,6 @@ using System.IO; using System.IO.Compression; -using API.Interfaces; +using API.Interfaces.Services; using API.Services; using Microsoft.Extensions.Logging; using NSubstitute; diff --git a/API.Tests/Services/CacheServiceTests.cs b/API.Tests/Services/CacheServiceTests.cs index fcf4b0099..3e05fb585 100644 --- a/API.Tests/Services/CacheServiceTests.cs +++ b/API.Tests/Services/CacheServiceTests.cs @@ -10,10 +10,10 @@ namespace API.Tests.Services // private readonly IArchiveService _archiveService = Substitute.For(); // private readonly IDirectoryService _directoryService = Substitute.For(); - public CacheServiceTests() - { - //_cacheService = new CacheService(_logger, _unitOfWork, _archiveService, _directoryService); - } + // public CacheServiceTests() + // { + // //_cacheService = new CacheService(_logger, _unitOfWork, _archiveService, _directoryService); + // } //string GetCachedPagePath(Volume volume, int page) [Fact] diff --git a/API.Tests/Services/DirectoryServiceTests.cs b/API.Tests/Services/DirectoryServiceTests.cs index 567c7e6a9..0ae265008 100644 --- a/API.Tests/Services/DirectoryServiceTests.cs +++ b/API.Tests/Services/DirectoryServiceTests.cs @@ -1,5 +1,4 @@ -using API.Interfaces; -using API.Services; +using API.Services; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; diff --git a/API.Tests/Services/ScannerServiceTests.cs b/API.Tests/Services/ScannerServiceTests.cs index 96abd7b36..18833faf4 100644 --- a/API.Tests/Services/ScannerServiceTests.cs +++ b/API.Tests/Services/ScannerServiceTests.cs @@ -1,15 +1,10 @@ using System; using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; using API.Entities; -using API.Entities.Enums; using API.Interfaces; using API.Interfaces.Services; -using API.Parser; using API.Services; using Microsoft.Extensions.Logging; -using Microsoft.VisualStudio.TestTools.UnitTesting; using NSubstitute; using Xunit; using Xunit.Abstractions; diff --git a/API/Controllers/AccountController.cs b/API/Controllers/AccountController.cs index e1eda477c..d5ac130ca 100644 --- a/API/Controllers/AccountController.cs +++ b/API/Controllers/AccountController.cs @@ -7,6 +7,7 @@ using API.DTOs; using API.Entities; using API.Extensions; using API.Interfaces; +using API.Interfaces.Services; using AutoMapper; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; @@ -43,7 +44,7 @@ namespace API.Controllers [HttpPost("reset-password")] public async Task UpdatePassword(ResetPasswordDto resetPasswordDto) { - _logger.LogInformation($"{User.GetUsername()} is changing {resetPasswordDto.UserName}'s password."); + _logger.LogInformation("{UserName} is changing {ResetUser}'s password", User.GetUsername(), resetPasswordDto.UserName); var user = await _userManager.Users.SingleAsync(x => x.UserName == resetPasswordDto.UserName); var result = await _userManager.RemovePasswordAsync(user); if (!result.Succeeded) return BadRequest("Unable to update password"); @@ -77,14 +78,14 @@ namespace API.Controllers // When we register an admin, we need to grant them access to all Libraries. if (registerDto.IsAdmin) { - _logger.LogInformation($"{user.UserName} is being registered as admin. Granting access to all libraries."); + _logger.LogInformation("{UserName} is being registered as admin. Granting access to all libraries", user.UserName); var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()).ToList(); foreach (var lib in libraries) { lib.AppUsers ??= new List(); lib.AppUsers.Add(user); } - if (libraries.Any() && !await _unitOfWork.Complete()) _logger.LogError("There was an issue granting library access. Please do this manually."); + if (libraries.Any() && !await _unitOfWork.Complete()) _logger.LogError("There was an issue granting library access. Please do this manually"); } return new UserDto @@ -116,7 +117,7 @@ namespace API.Controllers _unitOfWork.UserRepository.Update(user); await _unitOfWork.Complete(); - _logger.LogInformation($"{user.UserName} logged in at {user.LastActive}"); + _logger.LogInformation("{UserName} logged in at {Time}", user.UserName, user.LastActive); return new UserDto { diff --git a/API/Controllers/FallbackController.cs b/API/Controllers/FallbackController.cs index 36b173745..82ed0e3f6 100644 --- a/API/Controllers/FallbackController.cs +++ b/API/Controllers/FallbackController.cs @@ -1,10 +1,19 @@ using System.IO; +using API.Interfaces; using Microsoft.AspNetCore.Mvc; namespace API.Controllers { public class FallbackController : Controller { + private readonly ITaskScheduler _taskScheduler; + + public FallbackController(ITaskScheduler taskScheduler) + { + // This is used to load TaskScheduler on startup without having to navigate to a Controller that uses. + _taskScheduler = taskScheduler; + } + public ActionResult Index() { return PhysicalFile(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "index.html"), "text/HTML"); diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs index e745936a3..7fa186532 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -2,18 +2,15 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text; using System.Threading.Tasks; -using API.Data; using API.DTOs; using API.Entities; using API.Extensions; -using API.Helpers; using API.Interfaces; +using API.Interfaces.Services; using AutoMapper; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; namespace API.Controllers @@ -26,18 +23,16 @@ namespace API.Controllers private readonly IMapper _mapper; private readonly ITaskScheduler _taskScheduler; private readonly IUnitOfWork _unitOfWork; - private readonly DataContext _dataContext; // TODO: Remove, only for FTS prototyping public LibraryController(IDirectoryService directoryService, ILogger logger, IMapper mapper, ITaskScheduler taskScheduler, - IUnitOfWork unitOfWork, DataContext dataContext) + IUnitOfWork unitOfWork) { _directoryService = directoryService; _logger = logger; _mapper = mapper; _taskScheduler = taskScheduler; _unitOfWork = unitOfWork; - _dataContext = dataContext; } /// @@ -182,7 +177,7 @@ namespace API.Controllers public async Task> DeleteLibrary(int libraryId) { var username = User.GetUsername(); - _logger.LogInformation($"Library {libraryId} is being deleted by {username}."); + _logger.LogInformation("Library {LibraryId} is being deleted by {UserName}", libraryId, username); var series = await _unitOfWork.SeriesRepository.GetSeriesForLibraryIdAsync(libraryId); var chapterIds = await _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync(series.Select(x => x.Id).ToArray()); @@ -226,6 +221,7 @@ namespace API.Controllers //NOTE: What about normalizing search query and only searching against normalizedname in Series? // So One Punch would match One-Punch // This also means less indexes we need. + // TODO: Add indexes of what we are searching on queryString = queryString.Replace(@"%", ""); var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index f6f88bc2a..08cde5314 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -6,6 +6,7 @@ using API.DTOs; using API.Entities; using API.Extensions; using API.Interfaces; +using API.Interfaces.Services; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; @@ -34,15 +35,6 @@ namespace API.Controllers var chapter = await _cacheService.Ensure(chapterId); if (chapter == null) return BadRequest("There was an issue finding image file for reading"); - - // TODO: This code works, but might need bounds checking. UI can send bad data - // if (page >= chapter.Pages) - // { - // page = chapter.Pages - 1; - // } else if (page < 0) - // { - // page = 0; - // } var (path, mangaFile) = await _cacheService.GetCachedPagePath(chapter, page); if (string.IsNullOrEmpty(path)) return BadRequest($"No such image for page {page}"); @@ -68,7 +60,7 @@ namespace API.Controllers public async Task MarkRead(MarkReadDto markReadDto) { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); - var volumes = await _unitOfWork.SeriesRepository.GetVolumes(markReadDto.SeriesId); // TODO: Make this async + var volumes = await _unitOfWork.SeriesRepository.GetVolumes(markReadDto.SeriesId); user.Progresses ??= new List(); foreach (var volume in volumes) { diff --git a/API/Controllers/SeriesController.cs b/API/Controllers/SeriesController.cs index e5bb04fa5..580ac29f1 100644 --- a/API/Controllers/SeriesController.cs +++ b/API/Controllers/SeriesController.cs @@ -36,7 +36,7 @@ namespace API.Controllers { var username = User.GetUsername(); var chapterIds = (await _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync(new []{seriesId})); - _logger.LogInformation($"Series {seriesId} is being deleted by {username}."); + _logger.LogInformation("Series {SeriesId} is being deleted by {UserName}", seriesId, username); var result = await _unitOfWork.SeriesRepository.DeleteSeriesAsync(seriesId); if (result) diff --git a/API/Controllers/ServerController.cs b/API/Controllers/ServerController.cs index d8176279d..10357d330 100644 --- a/API/Controllers/ServerController.cs +++ b/API/Controllers/ServerController.cs @@ -21,7 +21,7 @@ namespace API.Controllers [HttpPost("restart")] public ActionResult RestartServer() { - _logger.LogInformation($"{User.GetUsername()} is restarting server from admin dashboard."); + _logger.LogInformation("{UserName} is restarting server from admin dashboard", User.GetUsername()); _applicationLifetime.StopApplication(); return Ok(); diff --git a/API/Controllers/SettingsController.cs b/API/Controllers/SettingsController.cs index f7e937314..85f97e149 100644 --- a/API/Controllers/SettingsController.cs +++ b/API/Controllers/SettingsController.cs @@ -35,7 +35,7 @@ namespace API.Controllers [HttpPost("")] public async Task> UpdateSettings(ServerSettingDto updateSettingsDto) { - _logger.LogInformation($"{User.GetUsername()} is updating Server Settings"); + _logger.LogInformation("{UserName} is updating Server Settings", User.GetUsername()); if (updateSettingsDto.CacheDirectory.Equals(string.Empty)) { @@ -72,9 +72,11 @@ namespace API.Controllers } } + if (!_unitOfWork.HasChanges()) return Ok("Nothing was updated"); + if (_unitOfWork.HasChanges() && await _unitOfWork.Complete()) { - _logger.LogInformation("Server Settings updated."); + _logger.LogInformation("Server Settings updated"); return Ok(updateSettingsDto); } diff --git a/API/DTOs/ImageDto.cs b/API/DTOs/ImageDto.cs index 6e3f0ae7c..1d07c52fd 100644 --- a/API/DTOs/ImageDto.cs +++ b/API/DTOs/ImageDto.cs @@ -9,7 +9,6 @@ public int Height { get; init; } public string Format { get; init; } public byte[] Content { get; init; } - //public int Chapter { get; set; } public string MangaFileName { get; set; } public bool NeedsSplitting { get; set; } } diff --git a/API/Data/Seed.cs b/API/Data/Seed.cs index 7ba7ebeda..ad0c09236 100644 --- a/API/Data/Seed.cs +++ b/API/Data/Seed.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading.Tasks; using API.Constants; @@ -38,7 +39,8 @@ namespace API.Data new() {Key = ServerSettingKey.CacheDirectory, Value = CacheService.CacheDirectory}, new () {Key = ServerSettingKey.TaskScan, Value = "daily"}, //new () {Key = ServerSettingKey.LoggingLevel, Value = "Information"}, - //new () {Key = ServerSettingKey.TaskBackup, Value = "daily"}, + new () {Key = ServerSettingKey.TaskBackup, Value = "weekly"}, + new () {Key = ServerSettingKey.BackupDirectory, Value = Path.GetFullPath(Path.Join(Directory.GetCurrentDirectory(), "backups/"))}, new () {Key = ServerSettingKey.Port, Value = "5000"}, }; diff --git a/API/Data/SeriesRepository.cs b/API/Data/SeriesRepository.cs index 8e916b3b7..45b67894f 100644 --- a/API/Data/SeriesRepository.cs +++ b/API/Data/SeriesRepository.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; diff --git a/API/Entities/Enums/ServerSettingKey.cs b/API/Entities/Enums/ServerSettingKey.cs index 214e6fc22..0aa5563f2 100644 --- a/API/Entities/Enums/ServerSettingKey.cs +++ b/API/Entities/Enums/ServerSettingKey.cs @@ -1,11 +1,20 @@ -namespace API.Entities.Enums +using System.ComponentModel; + +namespace API.Entities.Enums { public enum ServerSettingKey { + [Description("TaskScan")] TaskScan = 0, + [Description("CacheDirectory")] CacheDirectory = 1, + [Description("TaskBackup")] TaskBackup = 2, + [Description("LoggingLevel")] LoggingLevel = 3, - Port = 4 + [Description("Port")] + Port = 4, + [Description("BackupDirectory")] + BackupDirectory = 5 } } \ No newline at end of file diff --git a/API/Extensions/ApplicationServiceExtensions.cs b/API/Extensions/ApplicationServiceExtensions.cs index ad3f48bcb..a71083191 100644 --- a/API/Extensions/ApplicationServiceExtensions.cs +++ b/API/Extensions/ApplicationServiceExtensions.cs @@ -26,6 +26,7 @@ namespace API.Extensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); diff --git a/API/Helpers/Converters/CronConverter.cs b/API/Helpers/Converters/CronConverter.cs index 6fece1bdb..c31afb417 100644 --- a/API/Helpers/Converters/CronConverter.cs +++ b/API/Helpers/Converters/CronConverter.cs @@ -5,6 +5,7 @@ namespace API.Helpers.Converters { public static class CronConverter { + // TODO: this isn't used. Replace strings with Enums? public static readonly IEnumerable Options = new [] { "disabled", diff --git a/API/Interfaces/ISeriesRepository.cs b/API/Interfaces/ISeriesRepository.cs index 9f77e08e0..6c78341a3 100644 --- a/API/Interfaces/ISeriesRepository.cs +++ b/API/Interfaces/ISeriesRepository.cs @@ -18,10 +18,12 @@ namespace API.Interfaces /// /// Task> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId); + /// /// Does not add user information like progress, ratings, etc. /// /// + /// Series name to search for /// Task> SearchSeries(int[] libraryIds, string searchQuery); Task> GetSeriesForLibraryIdAsync(int libraryId); diff --git a/API/Interfaces/ITaskScheduler.cs b/API/Interfaces/ITaskScheduler.cs index 2659eebec..4d145a432 100644 --- a/API/Interfaces/ITaskScheduler.cs +++ b/API/Interfaces/ITaskScheduler.cs @@ -2,6 +2,10 @@ { public interface ITaskScheduler { + /// + /// For use on Server startup + /// + void ScheduleTasks(); void ScanLibrary(int libraryId, bool forceUpdate = false); void CleanupChapters(int[] chapterIds); void RefreshMetadata(int libraryId, bool forceUpdate = true); diff --git a/API/Interfaces/IArchiveService.cs b/API/Interfaces/Services/IArchiveService.cs similarity index 92% rename from API/Interfaces/IArchiveService.cs rename to API/Interfaces/Services/IArchiveService.cs index 3b3ab14cd..b150dd761 100644 --- a/API/Interfaces/IArchiveService.cs +++ b/API/Interfaces/Services/IArchiveService.cs @@ -1,6 +1,6 @@ using System.IO.Compression; -namespace API.Interfaces +namespace API.Interfaces.Services { public interface IArchiveService { diff --git a/API/Interfaces/Services/IBackupService.cs b/API/Interfaces/Services/IBackupService.cs new file mode 100644 index 000000000..c34ab272b --- /dev/null +++ b/API/Interfaces/Services/IBackupService.cs @@ -0,0 +1,7 @@ +namespace API.Interfaces.Services +{ + public interface IBackupService + { + void BackupDatabase(); + } +} \ No newline at end of file diff --git a/API/Interfaces/ICacheService.cs b/API/Interfaces/Services/ICacheService.cs similarity index 97% rename from API/Interfaces/ICacheService.cs rename to API/Interfaces/Services/ICacheService.cs index 470099859..830b6bb42 100644 --- a/API/Interfaces/ICacheService.cs +++ b/API/Interfaces/Services/ICacheService.cs @@ -1,7 +1,7 @@ using System.Threading.Tasks; using API.Entities; -namespace API.Interfaces +namespace API.Interfaces.Services { public interface ICacheService { diff --git a/API/Interfaces/IDirectoryService.cs b/API/Interfaces/Services/IDirectoryService.cs similarity index 72% rename from API/Interfaces/IDirectoryService.cs rename to API/Interfaces/Services/IDirectoryService.cs index 539a4c0f6..19aba03dd 100644 --- a/API/Interfaces/IDirectoryService.cs +++ b/API/Interfaces/Services/IDirectoryService.cs @@ -2,7 +2,7 @@ using System.Threading.Tasks; using API.DTOs; -namespace API.Interfaces +namespace API.Interfaces.Services { public interface IDirectoryService { @@ -21,5 +21,11 @@ namespace API.Interfaces /// /// string[] GetFiles(string path, string searchPatternExpression = ""); + /// + /// Returns true if the path exists and is a directory. If path does not exist, this will create it. Returns false in all fail cases. + /// + /// + /// + bool ExistOrCreate(string directoryPath); } } \ No newline at end of file diff --git a/API/Interfaces/IScannerService.cs b/API/Interfaces/Services/IScannerService.cs similarity index 93% rename from API/Interfaces/IScannerService.cs rename to API/Interfaces/Services/IScannerService.cs index 10dd9303f..695bc59c5 100644 --- a/API/Interfaces/IScannerService.cs +++ b/API/Interfaces/Services/IScannerService.cs @@ -1,4 +1,4 @@ -namespace API.Interfaces +namespace API.Interfaces.Services { public interface IScannerService { diff --git a/API/Interfaces/ITokenService.cs b/API/Interfaces/Services/ITokenService.cs similarity index 81% rename from API/Interfaces/ITokenService.cs rename to API/Interfaces/Services/ITokenService.cs index 042426964..14765f2f0 100644 --- a/API/Interfaces/ITokenService.cs +++ b/API/Interfaces/Services/ITokenService.cs @@ -1,7 +1,7 @@ using System.Threading.Tasks; using API.Entities; -namespace API.Interfaces +namespace API.Interfaces.Services { public interface ITokenService { diff --git a/API/Program.cs b/API/Program.cs index f65bba4ff..7d7628252 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -2,6 +2,7 @@ using System; using System.Threading.Tasks; using API.Data; using API.Entities; +using API.Interfaces; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; @@ -40,6 +41,7 @@ namespace API logger.LogError(ex, "An error occurred during migration"); } + await host.RunAsync(); } diff --git a/API/Services/ArchiveService.cs b/API/Services/ArchiveService.cs index 6f3e31965..fe3d93e1b 100644 --- a/API/Services/ArchiveService.cs +++ b/API/Services/ArchiveService.cs @@ -5,6 +5,7 @@ using System.IO.Compression; using System.Linq; using API.Extensions; using API.Interfaces; +using API.Interfaces.Services; using Microsoft.Extensions.Logging; using NetVips; diff --git a/API/Services/BackupService.cs b/API/Services/BackupService.cs new file mode 100644 index 000000000..7ab168712 --- /dev/null +++ b/API/Services/BackupService.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Threading.Tasks; +using API.Entities.Enums; +using API.Interfaces; +using API.Interfaces.Services; +using Microsoft.Extensions.Logging; + +namespace API.Services +{ + public class BackupService : IBackupService + { + private readonly IUnitOfWork _unitOfWork; + private readonly ILogger _logger; + private readonly IDirectoryService _directoryService; + + private readonly IList _backupFiles = new List() + { + "appsettings.json", + "Hangfire.db", + "Hangfire-log.db", + "kavita.db", + "kavita.db-shm", + "kavita.db-wal", + "kavita.log", + }; + + public BackupService(IUnitOfWork unitOfWork, ILogger logger, IDirectoryService directoryService) + { + _unitOfWork = unitOfWork; + _logger = logger; + _directoryService = directoryService; + } + + public void BackupDatabase() + { + _logger.LogInformation("Beginning backup of Database at {BackupTime}", DateTime.Now); + var backupDirectory = Task.Run(() => _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BackupDirectory)).Result.Value; + _logger.LogDebug("Backing up to {BackupDirectory}", backupDirectory); + if (!_directoryService.ExistOrCreate(backupDirectory)) + { + _logger.LogError("Could not write to {BackupDirectory}; aborting backup", backupDirectory); + return; + } + + var fileInfos = _backupFiles.Select(file => new FileInfo(Path.Join(Directory.GetCurrentDirectory(), file))).ToList(); + + var zipPath = Path.Join(backupDirectory, $"kavita_backup_{DateTime.Now}.zip"); + using (var zipArchive = ZipFile.Open(zipPath, ZipArchiveMode.Create)) + { + foreach (var fileInfo in fileInfos) + { + zipArchive.CreateEntryFromFile(fileInfo.FullName, fileInfo.Name); + } + } + + _logger.LogInformation("Database backup completed"); + throw new System.NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/API/Services/CacheService.cs b/API/Services/CacheService.cs index b7b46e86f..7399efa34 100644 --- a/API/Services/CacheService.cs +++ b/API/Services/CacheService.cs @@ -6,6 +6,7 @@ using API.Comparators; using API.Entities; using API.Extensions; using API.Interfaces; +using API.Interfaces.Services; using Microsoft.Extensions.Logging; namespace API.Services @@ -30,11 +31,12 @@ namespace API.Services public void EnsureCacheDirectory() { - _logger.LogDebug($"Checking if valid Cache directory: {CacheDirectory}"); + // TODO: Replace with DirectoryService.ExistOrCreate() + _logger.LogDebug("Checking if valid Cache directory: {CacheDirectory}", CacheDirectory); var di = new DirectoryInfo(CacheDirectory); if (!di.Exists) { - _logger.LogError($"Cache directory {CacheDirectory} is not accessible or does not exist. Creating..."); + _logger.LogError("Cache directory {CacheDirectory} is not accessible or does not exist. Creating...", CacheDirectory); Directory.CreateDirectory(CacheDirectory); } } @@ -66,15 +68,15 @@ namespace API.Services } catch (Exception ex) { - _logger.LogError("There was an issue deleting one or more folders/files during cleanup.", ex); + _logger.LogError(ex, "There was an issue deleting one or more folders/files during cleanup"); } - _logger.LogInformation("Cache directory purged."); + _logger.LogInformation("Cache directory purged"); } public void CleanupChapters(int[] chapterIds) { - _logger.LogInformation($"Running Cache cleanup on Volumes"); + _logger.LogInformation("Running Cache cleanup on Volumes"); foreach (var chapter in chapterIds) { diff --git a/API/Services/DirectoryService.cs b/API/Services/DirectoryService.cs index 31df9ac80..e6410cd1c 100644 --- a/API/Services/DirectoryService.cs +++ b/API/Services/DirectoryService.cs @@ -7,7 +7,7 @@ using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using API.DTOs; -using API.Interfaces; +using API.Interfaces.Services; using Microsoft.Extensions.Logging; using NetVips; @@ -49,7 +49,23 @@ namespace API.Services return !Directory.Exists(path) ? Array.Empty() : Directory.GetFiles(path); } - + + public bool ExistOrCreate(string directoryPath) + { + var di = new DirectoryInfo(directoryPath); + if (di.Exists) return true; + try + { + Directory.CreateDirectory(directoryPath); + } + catch (Exception ex) + { + _logger.LogError(ex, "There was an issue creating directory: {Directory}", directoryPath); + return false; + } + return true; + } + public IEnumerable ListDirectory(string rootPath) { if (!Directory.Exists(rootPath)) return ImmutableList.Empty; @@ -66,7 +82,7 @@ namespace API.Services { if (!File.Exists(imagePath)) { - _logger.LogError("Image does not exist on disk."); + _logger.LogError("Image does not exist on disk"); return null; } using var image = Image.NewFromFile(imagePath); @@ -82,16 +98,16 @@ namespace API.Services }; } - - /// - /// Recursively scans files and applies an action on them. This uses as many cores the underlying PC has to speed - /// up processing. - /// - /// Directory to scan - /// Action to apply on file path - /// - public static int TraverseTreeParallelForEach(string root, Action action, string searchPattern) + /// + /// Recursively scans files and applies an action on them. This uses as many cores the underlying PC has to speed + /// up processing. + /// + /// Directory to scan + /// Action to apply on file path + /// Regex pattern to search against + /// + public static int TraverseTreeParallelForEach(string root, Action action, string searchPattern) { //Count of files traversed and timer for diagnostic output var fileCount = 0; @@ -127,9 +143,6 @@ namespace API.Services } try { - // TODO: In future, we need to take LibraryType into consideration for what extensions to allow (RAW should allow images) - // or we need to move this filtering to another area (Process) - // or we can get all files and put a check in place during Process to abandon files files = GetFilesWithCertainExtensions(currentDir, searchPattern) .ToArray(); } diff --git a/API/Services/ScannerService.cs b/API/Services/ScannerService.cs index d711c0d04..131ac6189 100644 --- a/API/Services/ScannerService.cs +++ b/API/Services/ScannerService.cs @@ -2,11 +2,9 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; -using System.Globalization; using System.IO; using System.Linq; using System.Runtime.CompilerServices; -using System.Threading; using System.Threading.Tasks; using API.Entities; using API.Entities.Enums; diff --git a/API/Services/TaskScheduler.cs b/API/Services/TaskScheduler.cs index 7b7a4900f..7a2b52fd8 100644 --- a/API/Services/TaskScheduler.cs +++ b/API/Services/TaskScheduler.cs @@ -13,60 +13,81 @@ namespace API.Services private readonly ICacheService _cacheService; private readonly ILogger _logger; private readonly IScannerService _scannerService; + private readonly IUnitOfWork _unitOfWork; private readonly IMetadataService _metadataService; + private readonly IBackupService _backupService; - public BackgroundJobServer Client => new BackgroundJobServer(new BackgroundJobServerOptions() - { - WorkerCount = 1 - }); + public BackgroundJobServer Client => new BackgroundJobServer(); + // new BackgroundJobServerOptions() + // { + // WorkerCount = 1 + // } public TaskScheduler(ICacheService cacheService, ILogger logger, IScannerService scannerService, - IUnitOfWork unitOfWork, IMetadataService metadataService) + IUnitOfWork unitOfWork, IMetadataService metadataService, IBackupService backupService) { _cacheService = cacheService; _logger = logger; _scannerService = scannerService; + _unitOfWork = unitOfWork; _metadataService = metadataService; + _backupService = backupService; - _logger.LogInformation("Scheduling/Updating cache cleanup on a daily basis."); - var setting = Task.Run(() => unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.TaskScan)).Result; + + ScheduleTasks(); + //JobStorage.Current.GetMonitoringApi(). + + } + + public void ScheduleTasks() + { + _logger.LogInformation("Scheduling reoccurring tasks"); + string setting = null; + setting = Task.Run(() => _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.TaskScan)).Result.Value; if (setting != null) { - RecurringJob.AddOrUpdate(() => _scannerService.ScanLibraries(), () => CronConverter.ConvertToCronNotation(setting.Value)); + _logger.LogDebug("Scheduling Scan Library Task for {Cron}", setting); + RecurringJob.AddOrUpdate(() => _scannerService.ScanLibraries(), () => CronConverter.ConvertToCronNotation(setting)); } else { - RecurringJob.AddOrUpdate(() => _cacheService.Cleanup(), Cron.Daily); RecurringJob.AddOrUpdate(() => _scannerService.ScanLibraries(), Cron.Daily); } - //JobStorage.Current.GetMonitoringApi(). + setting = Task.Run(() => _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.TaskBackup)).Result.Value; + if (setting != null) + { + _logger.LogDebug("Scheduling Backup Task for {Cron}", setting); + RecurringJob.AddOrUpdate(() => _backupService.BackupDatabase(), () => CronConverter.ConvertToCronNotation(setting2)); + } + else + { + RecurringJob.AddOrUpdate(() => _backupService.BackupDatabase(), Cron.Weekly); + } + RecurringJob.AddOrUpdate(() => _cacheService.Cleanup(), Cron.Daily); } public void ScanLibrary(int libraryId, bool forceUpdate = false) { - _logger.LogInformation($"Enqueuing library scan for: {libraryId}"); + _logger.LogInformation("Enqueuing library scan for: {LibraryId}", libraryId); BackgroundJob.Enqueue(() => _scannerService.ScanLibrary(libraryId, forceUpdate)); } public void CleanupChapters(int[] chapterIds) { BackgroundJob.Enqueue(() => _cacheService.CleanupChapters(chapterIds)); - } public void RefreshMetadata(int libraryId, bool forceUpdate = true) { - _logger.LogInformation($"Enqueuing library metadata refresh for: {libraryId}"); + _logger.LogInformation("Enqueuing library metadata refresh for: {LibraryId}", libraryId); BackgroundJob.Enqueue((() => _metadataService.RefreshMetadata(libraryId, forceUpdate))); } - public void ScanLibraryInternal(int libraryId, bool forceUpdate) + public void BackupDatabase() { - _scannerService.ScanLibrary(libraryId, forceUpdate); - _metadataService.RefreshMetadata(libraryId, forceUpdate); - } - + BackgroundJob.Enqueue(() => _backupService.BackupDatabase()); + } } } \ No newline at end of file diff --git a/API/Services/TokenService.cs b/API/Services/TokenService.cs index 45673dae8..670776bef 100644 --- a/API/Services/TokenService.cs +++ b/API/Services/TokenService.cs @@ -6,7 +6,7 @@ using System.Security.Claims; using System.Text; using System.Threading.Tasks; using API.Entities; -using API.Interfaces; +using API.Interfaces.Services; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Configuration; using Microsoft.IdentityModel.Tokens; diff --git a/API/Startup.cs b/API/Startup.cs index d7a6f77f1..0f2d5b80c 100644 --- a/API/Startup.cs +++ b/API/Startup.cs @@ -38,6 +38,8 @@ namespace API { c.SwaggerDoc("v1", new OpenApiInfo { Title = "API", Version = "v1" }); }); + + } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.