diff --git a/API.Tests/Services/BackupServiceTests.cs b/API.Tests/Services/BackupServiceTests.cs index 5ed0700fb..878b57c94 100644 --- a/API.Tests/Services/BackupServiceTests.cs +++ b/API.Tests/Services/BackupServiceTests.cs @@ -1,5 +1,6 @@ using API.Interfaces; using API.Services; +using API.Services.Tasks; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using NSubstitute; diff --git a/API.Tests/Services/ScannerServiceTests.cs b/API.Tests/Services/ScannerServiceTests.cs index 18833faf4..c052a8880 100644 --- a/API.Tests/Services/ScannerServiceTests.cs +++ b/API.Tests/Services/ScannerServiceTests.cs @@ -4,6 +4,7 @@ using API.Entities; using API.Interfaces; using API.Interfaces.Services; using API.Services; +using API.Services.Tasks; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; diff --git a/API/Controllers/ServerController.cs b/API/Controllers/ServerController.cs index c0e9d5150..36491de4a 100644 --- a/API/Controllers/ServerController.cs +++ b/API/Controllers/ServerController.cs @@ -1,7 +1,9 @@ using System; using System.IO; +using System.IO.Compression; using System.Threading.Tasks; using API.Extensions; +using API.Interfaces; using API.Interfaces.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -18,14 +20,18 @@ namespace API.Controllers private readonly ILogger _logger; private readonly IConfiguration _config; private readonly IDirectoryService _directoryService; + private readonly IBackupService _backupService; + private readonly ITaskScheduler _taskScheduler; public ServerController(IHostApplicationLifetime applicationLifetime, ILogger logger, IConfiguration config, - IDirectoryService directoryService) + IDirectoryService directoryService, IBackupService backupService, ITaskScheduler taskScheduler) { _applicationLifetime = applicationLifetime; _logger = logger; _config = config; _directoryService = directoryService; + _backupService = backupService; + _taskScheduler = taskScheduler; } [HttpPost("restart")] @@ -40,40 +46,34 @@ namespace API.Controllers [HttpGet("logs")] public async Task GetLogs() { - // TODO: Zip up the log files - var maxRollingFiles = int.Parse(_config.GetSection("Logging").GetSection("File").GetSection("MaxRollingFiles").Value); - var loggingSection = _config.GetSection("Logging").GetSection("File").GetSection("Path").Value; - - var multipleFileRegex = maxRollingFiles > 0 ? @"\d*" : string.Empty; - FileInfo fi = new FileInfo(loggingSection); - - var files = _directoryService.GetFilesWithExtension(Directory.GetCurrentDirectory(), $@"{fi.Name}{multipleFileRegex}\.log"); - Console.WriteLine(files); + var files = _backupService.LogFiles(_config.GetMaxRollingFiles(), _config.GetLoggingFileName()); - var logFile = Path.Join(Directory.GetCurrentDirectory(), loggingSection); - _logger.LogInformation("Fetching download of logs: {LogFile}", logFile); - - // First, copy the file to temp - - var originalFile = new FileInfo(logFile); var tempDirectory = Path.Join(Directory.GetCurrentDirectory(), "temp"); - _directoryService.ExistOrCreate(tempDirectory); - var tempLocation = Path.Join(tempDirectory, originalFile.Name); - originalFile.CopyTo(tempLocation); // TODO: Make this unique based on date + var dateString = DateTime.Now.ToShortDateString().Replace("/", "_"); - // Read into memory - await using var memory = new MemoryStream(); - // We need to copy it else it will throw an exception - await using (var stream = new FileStream(tempLocation, FileMode.Open, FileAccess.Read)) - { - await stream.CopyToAsync(memory); + var tempLocation = Path.Join(tempDirectory, "logs_" + dateString); + _directoryService.ExistOrCreate(tempLocation); + if (!_directoryService.CopyFilesToDirectory(files, tempLocation)) + { + return BadRequest("Unable to copy files to temp directory for log download."); } - memory.Position = 0; - // Delete temp - (new FileInfo(tempLocation)).Delete(); + var zipPath = Path.Join(tempDirectory, $"kavita_logs_{dateString}.zip"); + try + { + ZipFile.CreateFromDirectory(tempLocation, zipPath); + } + catch (AggregateException ex) + { + _logger.LogError(ex, "There was an issue when archiving library backup"); + return BadRequest("There was an issue when archiving library backup"); + } + var fileBytes = await _directoryService.ReadFileAsync(zipPath); - return File(memory, "text/plain", Path.GetFileName(logFile)); + _directoryService.ClearAndDeleteDirectory(tempLocation); + (new FileInfo(zipPath)).Delete(); + + return File(fileBytes, "application/zip", Path.GetFileName(zipPath)); } } } \ No newline at end of file diff --git a/API/Extensions/ApplicationServiceExtensions.cs b/API/Extensions/ApplicationServiceExtensions.cs index e26aa80c8..09cc03a0d 100644 --- a/API/Extensions/ApplicationServiceExtensions.cs +++ b/API/Extensions/ApplicationServiceExtensions.cs @@ -3,6 +3,7 @@ using API.Helpers; using API.Interfaces; using API.Interfaces.Services; using API.Services; +using API.Services.Tasks; using AutoMapper; using Hangfire; using Hangfire.LiteDB; @@ -27,6 +28,7 @@ namespace API.Extensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); diff --git a/API/Interfaces/ITaskScheduler.cs b/API/Interfaces/ITaskScheduler.cs index 4d145a432..5de2f6941 100644 --- a/API/Interfaces/ITaskScheduler.cs +++ b/API/Interfaces/ITaskScheduler.cs @@ -9,5 +9,6 @@ void ScanLibrary(int libraryId, bool forceUpdate = false); void CleanupChapters(int[] chapterIds); void RefreshMetadata(int libraryId, bool forceUpdate = true); + void CleanupTemp(); } } \ No newline at end of file diff --git a/API/Interfaces/Services/IArchiveService.cs b/API/Interfaces/Services/IArchiveService.cs index 73452859f..1c0a638db 100644 --- a/API/Interfaces/Services/IArchiveService.cs +++ b/API/Interfaces/Services/IArchiveService.cs @@ -11,5 +11,6 @@ namespace API.Interfaces.Services byte[] GetCoverImage(string filepath, bool createThumbnail = false); bool IsValidArchive(string archivePath); string GetSummaryInfo(string archivePath); + } } \ No newline at end of file diff --git a/API/Interfaces/Services/IBackupService.cs b/API/Interfaces/Services/IBackupService.cs index c34ab272b..0f46a77c9 100644 --- a/API/Interfaces/Services/IBackupService.cs +++ b/API/Interfaces/Services/IBackupService.cs @@ -1,7 +1,17 @@ -namespace API.Interfaces.Services +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; + +namespace API.Interfaces.Services { public interface IBackupService { void BackupDatabase(); + /// + /// Returns a list of full paths of the logs files detailed in . + /// + /// + /// + /// + IEnumerable LogFiles(int maxRollingFiles, string logFileName); } } \ No newline at end of file diff --git a/API/Interfaces/Services/ICleanupService.cs b/API/Interfaces/Services/ICleanupService.cs new file mode 100644 index 000000000..da61943fe --- /dev/null +++ b/API/Interfaces/Services/ICleanupService.cs @@ -0,0 +1,7 @@ +namespace API.Interfaces.Services +{ + public interface ICleanupService + { + void Cleanup(); + } +} \ No newline at end of file diff --git a/API/Interfaces/Services/IDirectoryService.cs b/API/Interfaces/Services/IDirectoryService.cs index e73b55960..93fbfd64f 100644 --- a/API/Interfaces/Services/IDirectoryService.cs +++ b/API/Interfaces/Services/IDirectoryService.cs @@ -29,6 +29,8 @@ namespace API.Interfaces.Services /// bool ExistOrCreate(string directoryPath); + Task ReadFileAsync(string path); + /// /// Deletes all files within the directory, then the directory itself. /// diff --git a/API/Services/ArchiveService.cs b/API/Services/ArchiveService.cs index 419fd1032..c54be44c2 100644 --- a/API/Services/ArchiveService.cs +++ b/API/Services/ArchiveService.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Xml.Serialization; using API.Extensions; using API.Interfaces.Services; +using API.Services.Tasks; using Microsoft.Extensions.Logging; using NetVips; diff --git a/API/Services/DirectoryService.cs b/API/Services/DirectoryService.cs index e8467a928..cd1dead46 100644 --- a/API/Services/DirectoryService.cs +++ b/API/Services/DirectoryService.cs @@ -92,7 +92,8 @@ namespace API.Services public void ClearDirectory(string directoryPath) { - DirectoryInfo di = new DirectoryInfo(directoryPath); + var di = new DirectoryInfo(directoryPath); + if (!di.Exists) return; foreach (var file in di.EnumerateFiles()) { @@ -156,7 +157,7 @@ namespace API.Services return new ImageDto { - Content = await File.ReadAllBytesAsync(imagePath), + Content = await ReadFileAsync(imagePath), Filename = Path.GetFileNameWithoutExtension(imagePath), FullPath = Path.GetFullPath(imagePath), Width = image.Width, @@ -165,6 +166,12 @@ namespace API.Services }; } + public async Task ReadFileAsync(string path) + { + if (!File.Exists(path)) return Array.Empty(); + return await File.ReadAllBytesAsync(path); + } + /// /// Recursively scans files and applies an action on them. This uses as many cores the underlying PC has to speed diff --git a/API/Services/TaskScheduler.cs b/API/Services/TaskScheduler.cs index 1eba033dd..dd3f21150 100644 --- a/API/Services/TaskScheduler.cs +++ b/API/Services/TaskScheduler.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.IO; +using System.Threading.Tasks; using API.Entities.Enums; using API.Helpers.Converters; using API.Interfaces; @@ -16,6 +17,8 @@ namespace API.Services private readonly IUnitOfWork _unitOfWork; private readonly IMetadataService _metadataService; private readonly IBackupService _backupService; + private readonly ICleanupService _cleanupService; + private readonly IDirectoryService _directoryService; public BackgroundJobServer Client => new BackgroundJobServer(); // new BackgroundJobServerOptions() @@ -24,7 +27,8 @@ namespace API.Services // } public TaskScheduler(ICacheService cacheService, ILogger logger, IScannerService scannerService, - IUnitOfWork unitOfWork, IMetadataService metadataService, IBackupService backupService) + IUnitOfWork unitOfWork, IMetadataService metadataService, IBackupService backupService, ICleanupService cleanupService, + IDirectoryService directoryService) { _cacheService = cacheService; _logger = logger; @@ -32,6 +36,8 @@ namespace API.Services _unitOfWork = unitOfWork; _metadataService = metadataService; _backupService = backupService; + _cleanupService = cleanupService; + _directoryService = directoryService; ScheduleTasks(); @@ -65,7 +71,7 @@ namespace API.Services RecurringJob.AddOrUpdate(() => _backupService.BackupDatabase(), Cron.Weekly); } - RecurringJob.AddOrUpdate(() => _cacheService.Cleanup(), Cron.Daily); + RecurringJob.AddOrUpdate(() => _cleanupService.Cleanup(), Cron.Daily); } public void ScanLibrary(int libraryId, bool forceUpdate = false) @@ -85,6 +91,12 @@ namespace API.Services BackgroundJob.Enqueue((() => _metadataService.RefreshMetadata(libraryId, forceUpdate))); } + public void CleanupTemp() + { + var tempDirectory = Path.Join(Directory.GetCurrentDirectory(), "temp"); + BackgroundJob.Enqueue((() => _directoryService.ClearDirectory(tempDirectory))); + } + public void BackupDatabase() { BackgroundJob.Enqueue(() => _backupService.BackupDatabase()); diff --git a/API/Services/BackupService.cs b/API/Services/Tasks/BackupService.cs similarity index 93% rename from API/Services/BackupService.cs rename to API/Services/Tasks/BackupService.cs index fae49bcdc..3a5685035 100644 --- a/API/Services/BackupService.cs +++ b/API/Services/Tasks/BackupService.cs @@ -11,7 +11,7 @@ using API.Interfaces.Services; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; -namespace API.Services +namespace API.Services.Tasks { public class BackupService : IBackupService { @@ -27,16 +27,10 @@ namespace API.Services _unitOfWork = unitOfWork; _logger = logger; _directoryService = directoryService; + var maxRollingFiles = config.GetMaxRollingFiles(); var loggingSection = config.GetLoggingFileName(); - - var multipleFileRegex = maxRollingFiles > 0 ? @"\d*" : string.Empty; - var fi = new FileInfo(loggingSection); - - - var files = maxRollingFiles > 0 - ? _directoryService.GetFiles(Directory.GetCurrentDirectory(), $@"{fi.Name}{multipleFileRegex}\.log") - : new string[] {"kavita.log"}; + var files = LogFiles(maxRollingFiles, loggingSection); _backupFiles = new List() { "appsettings.json", @@ -52,6 +46,17 @@ namespace API.Services } } + public IEnumerable LogFiles(int maxRollingFiles, string logFileName) + { + var multipleFileRegex = maxRollingFiles > 0 ? @"\d*" : string.Empty; + var fi = new FileInfo(logFileName); + + var files = maxRollingFiles > 0 + ? _directoryService.GetFiles(Directory.GetCurrentDirectory(), $@"{fi.Name}{multipleFileRegex}\.log") + : new string[] {"kavita.log"}; + return files; + } + public void BackupDatabase() { _logger.LogInformation("Beginning backup of Database at {BackupTime}", DateTime.Now); diff --git a/API/Services/Tasks/CleanupService.cs b/API/Services/Tasks/CleanupService.cs new file mode 100644 index 000000000..d7079b4a7 --- /dev/null +++ b/API/Services/Tasks/CleanupService.cs @@ -0,0 +1,33 @@ +using System.IO; +using API.Interfaces.Services; +using Microsoft.Extensions.Logging; + +namespace API.Services.Tasks +{ + /// + /// Cleans up after operations on reoccurring basis + /// + public class CleanupService : ICleanupService + { + private readonly ICacheService _cacheService; + private readonly IDirectoryService _directoryService; + private readonly ILogger _logger; + + public CleanupService(ICacheService cacheService, IDirectoryService directoryService, ILogger logger) + { + _cacheService = cacheService; + _directoryService = directoryService; + _logger = logger; + } + + public void Cleanup() + { + _logger.LogInformation("Cleaning temp directory"); + var tempDirectory = Path.Join(Directory.GetCurrentDirectory(), "temp"); + _directoryService.ClearDirectory(tempDirectory); + _logger.LogInformation("Cleaning cache directory"); + _cacheService.Cleanup(); + + } + } +} \ No newline at end of file diff --git a/API/Services/ScannerService.cs b/API/Services/Tasks/ScannerService.cs similarity index 99% rename from API/Services/ScannerService.cs rename to API/Services/Tasks/ScannerService.cs index 131ac6189..ac93d4c7d 100644 --- a/API/Services/ScannerService.cs +++ b/API/Services/Tasks/ScannerService.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; -using System.Runtime.CompilerServices; using System.Threading.Tasks; using API.Entities; using API.Entities.Enums; @@ -14,8 +13,7 @@ using API.Parser; using Hangfire; using Microsoft.Extensions.Logging; -[assembly: InternalsVisibleTo("API.Tests")] -namespace API.Services +namespace API.Services.Tasks { public class ScannerService : IScannerService {