diff --git a/.gitignore b/.gitignore index 9999bab3e..f80c762de 100644 --- a/.gitignore +++ b/.gitignore @@ -450,4 +450,5 @@ appsettings.json /API/Hangfire-log.db cache/ /API/wwwroot/ -/API/cache/ \ No newline at end of file +/API/cache/ +/API/temp/ \ No newline at end of file diff --git a/API.Tests/Services/BackupServiceTests.cs b/API.Tests/Services/BackupServiceTests.cs new file mode 100644 index 000000000..878b57c94 --- /dev/null +++ b/API.Tests/Services/BackupServiceTests.cs @@ -0,0 +1,47 @@ +using API.Interfaces; +using API.Services; +using API.Services.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using NSubstitute; + +namespace API.Tests.Services +{ + public class BackupServiceTests + { + private readonly DirectoryService _directoryService; + private readonly BackupService _backupService; + private readonly IUnitOfWork _unitOfWork = Substitute.For(); + private readonly ILogger _directoryLogger = Substitute.For>(); + private readonly ILogger _logger = Substitute.For>(); + private readonly IConfiguration _config; + + // public BackupServiceTests() + // { + // var inMemorySettings = new Dictionary { + // {"Logging:File:MaxRollingFiles", "0"}, + // {"Logging:File:Path", "file.log"}, + // }; + // + // _config = new ConfigurationBuilder() + // .AddInMemoryCollection(inMemorySettings) + // .Build(); + // + // //_config.GetMaxRollingFiles().Returns(0); + // //_config.GetLoggingFileName().Returns("file.log"); + // //var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/BackupService/"); + // //Directory.GetCurrentDirectory().Returns(testDirectory); + // + // _directoryService = new DirectoryService(_directoryLogger); + // _backupService = new BackupService(_unitOfWork, _logger, _directoryService, _config); + // } + // + // [Fact] + // public void Test() + // { + // _backupService.BackupDatabase(); + // } + + + } +} \ No newline at end of file diff --git a/API.Tests/Services/CacheServiceTests.cs b/API.Tests/Services/CacheServiceTests.cs index 3e05fb585..87ed5a655 100644 --- a/API.Tests/Services/CacheServiceTests.cs +++ b/API.Tests/Services/CacheServiceTests.cs @@ -48,7 +48,7 @@ namespace API.Tests.Services // var cacheService = Substitute.ForPartsOf(); // cacheService.Configure().CacheDirectoryIsAccessible().Returns(true); // cacheService.Configure().GetVolumeCachePath(1, volume.Files.ElementAt(0)).Returns("cache/1/"); - // _directoryService.Configure().GetFiles("cache/1/").Returns(new string[] {"pexels-photo-6551949.jpg"}); + // _directoryService.Configure().GetFilesWithExtension("cache/1/").Returns(new string[] {"pexels-photo-6551949.jpg"}); // Assert.Equal(expected, _cacheService.GetCachedPagePath(volume, pageNum)); Assert.True(true); } diff --git a/API.Tests/Services/DirectoryServiceTests.cs b/API.Tests/Services/DirectoryServiceTests.cs index 0ae265008..39ff717c5 100644 --- a/API.Tests/Services/DirectoryServiceTests.cs +++ b/API.Tests/Services/DirectoryServiceTests.cs @@ -1,4 +1,6 @@ -using API.Services; +using System.IO; +using System.Linq; +using API.Services; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; @@ -17,15 +19,60 @@ namespace API.Tests.Services } [Fact] - public void GetFiles_Test() + public void GetFiles_WithCustomRegex_ShouldPass_Test() { - //_directoryService.GetFiles() + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/DirectoryService/regex"); + var files = _directoryService.GetFiles(testDirectory, @"file\d*.txt"); + Assert.Equal(2, files.Count()); + } + + [Fact] + public void GetFiles_TopLevel_ShouldBeEmpty_Test() + { + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/DirectoryService"); + var files = _directoryService.GetFiles(testDirectory); + Assert.Empty(files); + } + + [Fact] + public void GetFilesWithExtensions_ShouldBeEmpty_Test() + { + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/DirectoryService/extensions"); + var files = _directoryService.GetFiles(testDirectory, "*.txt"); + Assert.Empty(files); + } + + [Fact] + public void GetFilesWithExtensions_Test() + { + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/DirectoryService/extension"); + var files = _directoryService.GetFiles(testDirectory, ".cbz|.rar"); + Assert.Equal(3, files.Count()); + } + + [Fact] + public void GetFilesWithExtensions_BadDirectory_ShouldBeEmpty_Test() + { + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/DirectoryService/doesntexist"); + var files = _directoryService.GetFiles(testDirectory, ".cbz|.rar"); + Assert.Empty(files); } [Fact] - public void ListDirectory_Test() + public void ListDirectory_SubDirectory_Test() { - + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/DirectoryService/"); + var dirs = _directoryService.ListDirectory(testDirectory); + Assert.Contains(dirs, s => s.Contains("regex")); + + } + + [Fact] + public void ListDirectory_NoSubDirectory_Test() + { + var dirs = _directoryService.ListDirectory(""); + Assert.DoesNotContain(dirs, s => s.Contains("regex")); + } } } \ No newline at end of file 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.Tests/Services/Test Data/DirectoryService/extension/file.cbz b/API.Tests/Services/Test Data/DirectoryService/extension/file.cbz new file mode 100644 index 000000000..e69de29bb diff --git a/API.Tests/Services/Test Data/DirectoryService/extension/file.rar b/API.Tests/Services/Test Data/DirectoryService/extension/file.rar new file mode 100644 index 000000000..e69de29bb diff --git a/API.Tests/Services/Test Data/DirectoryService/extension/file2.cbz b/API.Tests/Services/Test Data/DirectoryService/extension/file2.cbz new file mode 100644 index 000000000..e69de29bb diff --git a/API.Tests/Services/Test Data/DirectoryService/regex/file.txt b/API.Tests/Services/Test Data/DirectoryService/regex/file.txt new file mode 100644 index 000000000..e69de29bb diff --git a/API.Tests/Services/Test Data/DirectoryService/regex/file2.txt b/API.Tests/Services/Test Data/DirectoryService/regex/file2.txt new file mode 100644 index 000000000..e69de29bb diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index 08cde5314..067ea4f41 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -142,7 +142,7 @@ namespace API.Controllers public async Task Bookmark(BookmarkDto bookmarkDto) { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); - _logger.LogInformation("Saving {UserName} progress for Chapter {ChapterId} to page {PageNum}", user.UserName, bookmarkDto.ChapterId, bookmarkDto.PageNum); + _logger.LogDebug("Saving {UserName} progress for Chapter {ChapterId} to page {PageNum}", user.UserName, bookmarkDto.ChapterId, bookmarkDto.PageNum); // Don't let user bookmark past total pages. var chapter = await _unitOfWork.VolumeRepository.GetChapterAsync(bookmarkDto.ChapterId); diff --git a/API/Controllers/ServerController.cs b/API/Controllers/ServerController.cs index 10357d330..36491de4a 100644 --- a/API/Controllers/ServerController.cs +++ b/API/Controllers/ServerController.cs @@ -1,6 +1,13 @@ -using API.Extensions; +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; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -11,11 +18,20 @@ namespace API.Controllers { private readonly IHostApplicationLifetime _applicationLifetime; 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) + public ServerController(IHostApplicationLifetime applicationLifetime, ILogger logger, IConfiguration config, + IDirectoryService directoryService, IBackupService backupService, ITaskScheduler taskScheduler) { _applicationLifetime = applicationLifetime; _logger = logger; + _config = config; + _directoryService = directoryService; + _backupService = backupService; + _taskScheduler = taskScheduler; } [HttpPost("restart")] @@ -26,5 +42,38 @@ namespace API.Controllers _applicationLifetime.StopApplication(); return Ok(); } + + [HttpGet("logs")] + public async Task GetLogs() + { + var files = _backupService.LogFiles(_config.GetMaxRollingFiles(), _config.GetLoggingFileName()); + + var tempDirectory = Path.Join(Directory.GetCurrentDirectory(), "temp"); + var dateString = DateTime.Now.ToShortDateString().Replace("/", "_"); + + 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."); + } + + 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); + + _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/Data/DataContext.cs b/API/Data/DataContext.cs index 24a2dae47..f6626d2a8 100644 --- a/API/Data/DataContext.cs +++ b/API/Data/DataContext.cs @@ -16,8 +16,8 @@ namespace API.Data { ChangeTracker.Tracked += OnEntityTracked; ChangeTracker.StateChanged += OnEntityStateChanged; - } + public DbSet Library { get; set; } public DbSet Series { get; set; } @@ -33,6 +33,7 @@ namespace API.Data protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); + builder.Entity() .HasMany(ur => ur.UserRoles) diff --git a/API/Extensions/ApplicationServiceExtensions.cs b/API/Extensions/ApplicationServiceExtensions.cs index a71083191..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,12 +28,16 @@ namespace API.Extensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddDbContext(options => { - options.UseSqlite(config.GetConnectionString("DefaultConnection")); + options.UseSqlite(config.GetConnectionString("DefaultConnection"), builder => + { + //builder.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery); + }); }); services.AddLogging(loggingBuilder => diff --git a/API/Extensions/ConfigurationExtensions.cs b/API/Extensions/ConfigurationExtensions.cs new file mode 100644 index 000000000..2388fee21 --- /dev/null +++ b/API/Extensions/ConfigurationExtensions.cs @@ -0,0 +1,16 @@ +using Microsoft.Extensions.Configuration; + +namespace API.Extensions +{ + public static class ConfigurationExtensions + { + public static int GetMaxRollingFiles(this IConfiguration config) + { + return int.Parse(config.GetSection("Logging").GetSection("File").GetSection("MaxRollingFiles").Value); + } + public static string GetLoggingFileName(this IConfiguration config) + { + return config.GetSection("Logging").GetSection("File").GetSection("Path").Value; + } + } +} \ No newline at end of file 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 abd83dd36..93fbfd64f 100644 --- a/API/Interfaces/Services/IDirectoryService.cs +++ b/API/Interfaces/Services/IDirectoryService.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.IO; using System.Threading.Tasks; using API.DTOs; @@ -20,7 +21,7 @@ namespace API.Interfaces.Services /// /// /// - string[] GetFiles(string path, string searchPatternExpression = ""); + string[] GetFilesWithExtension(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. /// @@ -28,6 +29,23 @@ namespace API.Interfaces.Services /// bool ExistOrCreate(string directoryPath); + Task ReadFileAsync(string path); + + /// + /// Deletes all files within the directory, then the directory itself. + /// + /// void ClearAndDeleteDirectory(string directoryPath); + /// + /// Deletes all files within the directory. + /// + /// + /// + void ClearDirectory(string directoryPath); + + bool CopyFilesToDirectory(IEnumerable filePaths, string directoryPath); + + IEnumerable GetFiles(string path, string searchPatternExpression = "", + SearchOption searchOption = SearchOption.TopDirectoryOnly); } } \ No newline at end of file diff --git a/API/Services/ArchiveService.cs b/API/Services/ArchiveService.cs index f1c52a0d0..c54be44c2 100644 --- a/API/Services/ArchiveService.cs +++ b/API/Services/ArchiveService.cs @@ -3,12 +3,10 @@ using System.Diagnostics; using System.IO; using System.IO.Compression; using System.Linq; -using System.Xml; -using System.Xml.Linq; using System.Xml.Serialization; using API.Extensions; -using API.Interfaces; using API.Interfaces.Services; +using API.Services.Tasks; using Microsoft.Extensions.Logging; using NetVips; @@ -20,7 +18,7 @@ namespace API.Services public class ArchiveService : IArchiveService { private readonly ILogger _logger; - private const int ThumbnailWidth = 320; + private const int ThumbnailWidth = 320; // 153w x 230h TODO: Look into optimizing the images to be smaller public ArchiveService(ILogger logger) { @@ -94,7 +92,7 @@ namespace API.Services { using var stream = entry.Open(); using var ms = new MemoryStream(); - stream.CopyTo(ms); + stream.CopyTo(ms); // TODO: Check if we can use CopyToAsync here var data = ms.ToArray(); return data; diff --git a/API/Services/CacheService.cs b/API/Services/CacheService.cs index 7399efa34..4b4f457ee 100644 --- a/API/Services/CacheService.cs +++ b/API/Services/CacheService.cs @@ -111,7 +111,7 @@ namespace API.Services if (page <= (mangaFile.NumberOfPages + pagesSoFar)) { var path = GetCachePath(chapter.Id); - var files = _directoryService.GetFiles(path, Parser.Parser.ImageFileExtensions); + var files = _directoryService.GetFilesWithExtension(path, Parser.Parser.ImageFileExtensions); Array.Sort(files, _numericComparer); // Since array is 0 based, we need to keep that in account (only affects last image) diff --git a/API/Services/DirectoryService.cs b/API/Services/DirectoryService.cs index 9481302bd..cd1dead46 100644 --- a/API/Services/DirectoryService.cs +++ b/API/Services/DirectoryService.cs @@ -39,8 +39,23 @@ namespace API.Services .Where(file => reSearchPattern.IsMatch(Path.GetExtension(file))); } + + public IEnumerable GetFiles(string path, string searchPatternExpression = "", + SearchOption searchOption = SearchOption.TopDirectoryOnly) + { + if (searchPatternExpression != string.Empty) + { + if (!Directory.Exists(path)) return ImmutableList.Empty; + var reSearchPattern = new Regex(searchPatternExpression, RegexOptions.IgnoreCase); + return Directory.EnumerateFiles(path, "*", searchOption) + .Where(file => + reSearchPattern.IsMatch(file)); + } + + return !Directory.Exists(path) ? Array.Empty() : Directory.GetFiles(path); + } - public string[] GetFiles(string path, string searchPatternExpression = "") + public string[] GetFilesWithExtension(string path, string searchPatternExpression = "") { if (searchPatternExpression != string.Empty) { @@ -70,6 +85,16 @@ namespace API.Services { DirectoryInfo di = new DirectoryInfo(directoryPath); + ClearDirectory(directoryPath); + + di.Delete(true); + } + + public void ClearDirectory(string directoryPath) + { + var di = new DirectoryInfo(directoryPath); + if (!di.Exists) return; + foreach (var file in di.EnumerateFiles()) { file.Delete(); @@ -78,8 +103,35 @@ namespace API.Services { dir.Delete(true); } - - di.Delete(true); + } + + public bool CopyFilesToDirectory(IEnumerable filePaths, string directoryPath) + { + string currentFile = null; + try + { + foreach (var file in filePaths) + { + currentFile = file; + var fileInfo = new FileInfo(file); + if (fileInfo.Exists) + { + fileInfo.CopyTo(Path.Join(directoryPath, fileInfo.Name)); + } + else + { + _logger.LogWarning("Tried to copy {File} but it doesn't exist", file); + } + + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Unable to copy {File} to {DirectoryPath}", currentFile, directoryPath); + return false; + } + + return true; } public IEnumerable ListDirectory(string rootPath) @@ -105,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, @@ -114,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 59% rename from API/Services/BackupService.cs rename to API/Services/Tasks/BackupService.cs index 17b9cfb94..3a5685035 100644 --- a/API/Services/BackupService.cs +++ b/API/Services/Tasks/BackupService.cs @@ -5,11 +5,13 @@ using System.IO.Compression; using System.Linq; using System.Threading.Tasks; using API.Entities.Enums; +using API.Extensions; using API.Interfaces; using API.Interfaces.Services; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; -namespace API.Services +namespace API.Services.Tasks { public class BackupService : IBackupService { @@ -18,22 +20,41 @@ namespace API.Services private readonly IDirectoryService _directoryService; private readonly string _tempDirectory = Path.Join(Directory.GetCurrentDirectory(), "temp"); - private readonly IList _backupFiles = new List() - { - "appsettings.json", - "Hangfire.db", - "Hangfire-log.db", - "kavita.db", - "kavita.db-shm", - "kavita.db-wal", - "kavita.log", - }; + private readonly IList _backupFiles; - public BackupService(IUnitOfWork unitOfWork, ILogger logger, IDirectoryService directoryService) + public BackupService(IUnitOfWork unitOfWork, ILogger logger, IDirectoryService directoryService, IConfiguration config) { _unitOfWork = unitOfWork; _logger = logger; _directoryService = directoryService; + + var maxRollingFiles = config.GetMaxRollingFiles(); + var loggingSection = config.GetLoggingFileName(); + var files = LogFiles(maxRollingFiles, loggingSection); + _backupFiles = new List() + { + "appsettings.json", + "Hangfire.db", + "Hangfire-log.db", + "kavita.db", + "kavita.db-shm", // This wont always be there + "kavita.db-wal", // This wont always be there + }; + foreach (var file in files.Select(f => (new FileInfo(f)).Name).ToList()) + { + _backupFiles.Add(file); + } + } + + 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() @@ -59,14 +80,10 @@ namespace API.Services var tempDirectory = Path.Join(_tempDirectory, dateString); _directoryService.ExistOrCreate(tempDirectory); - - - foreach (var file in _backupFiles) - { - var originalFile = new FileInfo(Path.Join(Directory.GetCurrentDirectory(), file)); - originalFile.CopyTo(Path.Join(tempDirectory, originalFile.Name)); - } - + _directoryService.ClearDirectory(tempDirectory); + + _directoryService.CopyFilesToDirectory( + _backupFiles.Select(file => Path.Join(Directory.GetCurrentDirectory(), file)).ToList(), tempDirectory); try { ZipFile.CreateFromDirectory(tempDirectory, zipPath); @@ -79,5 +96,6 @@ namespace API.Services _directoryService.ClearAndDeleteDirectory(tempDirectory); _logger.LogInformation("Database backup completed"); } + } } \ No newline at end of file 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 { diff --git a/API/Services/TokenService.cs b/API/Services/TokenService.cs index 670776bef..3b292cb8c 100644 --- a/API/Services/TokenService.cs +++ b/API/Services/TokenService.cs @@ -22,6 +22,7 @@ namespace API.Services public TokenService(IConfiguration config, UserManager userManager) { + _userManager = userManager; _key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["TokenKey"])); } diff --git a/API/Startup.cs b/API/Startup.cs index 0f2d5b80c..8b59f2f12 100644 --- a/API/Startup.cs +++ b/API/Startup.cs @@ -24,7 +24,6 @@ namespace API // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { - services.AddApplicationServices(_config); services.AddControllers(); services.Configure(options => @@ -72,7 +71,7 @@ namespace API app.UseStaticFiles(new StaticFileOptions { - ContentTypeProvider = new FileExtensionContentTypeProvider() // this is not set by default + ContentTypeProvider = new FileExtensionContentTypeProvider() }); diff --git a/README.md b/README.md index 20aca8a85..9a8dee63e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Kavita -Kavita, meaning Stories, is a lightweight manga server. The goal is to replace Ubooqity with an +Kavita, meaning Stories, is a lightweight manga server. The goal is to replace Ubooquity with an open source variant that is flexible and packs more punch, without sacrificing ease to use. +Think: ***Plex but for Manga.*** ## Goals: * Serve up Manga (cbr, cbz, zip/rar, raw images) and Books (epub, mobi, azw, djvu, pdf) @@ -9,10 +10,15 @@ open source variant that is flexible and packs more punch, without sacrificing e * Provide hooks into metadata providers to fetch Manga data * Metadata should allow for collections, want to read integration from 3rd party services, genres. * Ability to manage users, access, and ratings -* Expose an OPDS API/Stream for external readers to use -* Allow downloading files directly from WebApp ## How to Deploy * Build kavita-webui via ng build --prod. The dest should be placed in the API/wwwroot directory * Run publish command + +## How to install +1. Unzip the archive for your target OS +2. Place in a directory that is writable. If on windows, do not place in Program Files +3. Open appsettings.json and modify TokenKey to a random string ideally generated from [https://passwordsgenerator.net/](https://passwordsgenerator.net/) +4. Run API.exe +