From 0fcaaff9765ed4b24e4205836ae7a0d3cb3c54c0 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Wed, 6 Apr 2022 18:21:38 -0500 Subject: [PATCH] PDF Rendering on Pi (64bit) & Backup Fix (#1204) * Updated dependencies. SharpCompress has been updated to v2.1.0 which should fix pdf rendering on pi/arm64 devices. * Removed some dependencies not needed and updated the Backup code to account for themes and ensure everything gets copied every time. --- API.Tests/API.Tests.csproj | 4 +- API.Tests/Services/BackupServiceTests.cs | 83 +++++++++++++++++-- API.Tests/Services/DirectoryServiceTests.cs | 15 ++++ API/API.csproj | 24 +++--- API/Controllers/UsersController.cs | 1 + API/DTOs/UpdateUserRole.cs | 10 --- API/Services/DirectoryService.cs | 5 ++ API/Services/Tasks/BackupService.cs | 59 ++++++++++--- API/Startup.cs | 3 - Kavita.Common/Kavita.Common.csproj | 2 +- .../src/app/dashboard/dashboard.component.ts | 19 +---- 11 files changed, 160 insertions(+), 65 deletions(-) delete mode 100644 API/DTOs/UpdateUserRole.cs diff --git a/API.Tests/API.Tests.csproj b/API.Tests/API.Tests.csproj index 782385ffc..3b4b8cb94 100644 --- a/API.Tests/API.Tests.csproj +++ b/API.Tests/API.Tests.csproj @@ -7,10 +7,10 @@ - + - + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/API.Tests/Services/BackupServiceTests.cs b/API.Tests/Services/BackupServiceTests.cs index 31896a38c..4ad416dc6 100644 --- a/API.Tests/Services/BackupServiceTests.cs +++ b/API.Tests/Services/BackupServiceTests.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Data.Common; using System.IO.Abstractions.TestingHelpers; +using System.IO.Compression; using System.Linq; using System.Threading.Tasks; using API.Data; @@ -36,6 +37,9 @@ public class BackupServiceTests private const string CoverImageDirectory = "C:/kavita/config/covers/"; private const string BackupDirectory = "C:/kavita/config/backups/"; private const string LogDirectory = "C:/kavita/config/logs/"; + private const string ConfigDirectory = "C:/kavita/config/"; + private const string BookmarkDirectory = "C:/kavita/config/bookmarks"; + private const string ThemesDirectory = "C:/kavita/config/theme"; public BackupServiceTests() { @@ -110,6 +114,8 @@ public class BackupServiceTests fileSystem.AddDirectory(CoverImageDirectory); fileSystem.AddDirectory(BackupDirectory); fileSystem.AddDirectory(LogDirectory); + fileSystem.AddDirectory(ThemesDirectory); + fileSystem.AddDirectory(BookmarkDirectory); fileSystem.AddDirectory("C:/data/"); return fileSystem; @@ -121,6 +127,7 @@ public class BackupServiceTests #region GetLogFiles + [Fact] public void GetLogFiles_ExpectAllFiles_NoRollingFiles() { var filesystem = CreateFileSystem(); @@ -128,16 +135,82 @@ public class BackupServiceTests filesystem.AddFile($"{LogDirectory}kavita1.log", new MockFileData("")); var ds = new DirectoryService(Substitute.For>(), filesystem); - // You can't mock _config extensions because they are static - _config.GetMaxRollingFiles().Returns(1); - _config.GetLoggingFileName().Returns(ds.FileSystem.Path.Join(LogDirectory, "kavita.log")); + var inMemorySettings = new Dictionary { + {"Logging:File:Path", "config/logs/kavita.log"}, + {"Logging:File:MaxRollingFiles", "0"}, + }; + IConfiguration configuration = new ConfigurationBuilder() + .AddInMemoryCollection(inMemorySettings) + .Build(); - var backupService = new BackupService(_logger, _unitOfWork, ds, _config, _messageHub); + var backupService = new BackupService(_logger, _unitOfWork, ds, configuration, _messageHub); - Assert.Single(backupService.GetLogFiles(1, LogDirectory)); + var backupLogFiles = backupService.GetLogFiles(0, LogDirectory).ToList(); + Assert.Single(backupLogFiles); + Assert.Equal(API.Parser.Parser.NormalizePath($"{LogDirectory}kavita.log"), API.Parser.Parser.NormalizePath(backupLogFiles.First())); + } + + [Fact] + public void GetLogFiles_ExpectAllFiles_WithRollingFiles() + { + var filesystem = CreateFileSystem(); + filesystem.AddFile($"{LogDirectory}kavita.log", new MockFileData("")); + filesystem.AddFile($"{LogDirectory}kavita1.log", new MockFileData("")); + + var ds = new DirectoryService(Substitute.For>(), filesystem); + var inMemorySettings = new Dictionary { + {"Logging:File:Path", "config/logs/kavita.log"}, + {"Logging:File:MaxRollingFiles", "1"}, + }; + IConfiguration configuration = new ConfigurationBuilder() + .AddInMemoryCollection(inMemorySettings) + .Build(); + + var backupService = new BackupService(_logger, _unitOfWork, ds, configuration, _messageHub); + + var backupLogFiles = backupService.GetLogFiles(1, LogDirectory).Select(API.Parser.Parser.NormalizePath).ToList(); + Assert.NotEmpty(backupLogFiles.Where(file => file.Equals(API.Parser.Parser.NormalizePath($"{LogDirectory}kavita.log")) || file.Equals(API.Parser.Parser.NormalizePath($"{LogDirectory}kavita1.log")))); } #endregion + #region BackupFiles + + // I don't think I can unit test this due to ZipFile.Create + // [Fact] + // public async Task BackupDatabase_ExpectAllFiles() + // { + // var filesystem = CreateFileSystem(); + // filesystem.AddFile($"{LogDirectory}kavita.log", new MockFileData("")); + // filesystem.AddFile($"{ConfigDirectory}kavita.db", new MockFileData("")); + // filesystem.AddFile($"{CoverImageDirectory}1.png", new MockFileData("")); + // filesystem.AddFile($"{BookmarkDirectory}1.png", new MockFileData("")); + // filesystem.AddFile($"{ConfigDirectory}appsettings.json", new MockFileData("")); + // filesystem.AddFile($"{ThemesDirectory}joe.css", new MockFileData("")); + // + // + // var ds = new DirectoryService(Substitute.For>(), filesystem); + // var inMemorySettings = new Dictionary { + // {"Logging:File:Path", $"{LogDirectory}kavita.log"}, + // {"Logging:File:MaxRollingFiles", "0"}, + // }; + // IConfiguration configuration = new ConfigurationBuilder() + // .AddInMemoryCollection(inMemorySettings) + // .Build(); + // + // var backupService = new BackupService(_logger, _unitOfWork, ds, configuration, _messageHub); + // + // await backupService.BackupDatabase(); + // + // + // var files = ds.GetFiles(BackupDirectory).ToList(); + // Assert.NotEmpty(files); + // var zipFile = files.FirstOrDefault(); + // Assert.NotNull(zipFile); + // using var zipArchive = ZipFile.OpenRead(zipFile); + // + // } + + #endregion } diff --git a/API.Tests/Services/DirectoryServiceTests.cs b/API.Tests/Services/DirectoryServiceTests.cs index 1ec21efe8..9a4bef08e 100644 --- a/API.Tests/Services/DirectoryServiceTests.cs +++ b/API.Tests/Services/DirectoryServiceTests.cs @@ -508,6 +508,21 @@ namespace API.Tests.Services Assert.Equal(2, ds.GetFiles("/manga/output/").Count()); } + [Fact] + public void CopyFilesToDirectory_ShouldMoveAllFilesAndNotFailOnNonExistentFiles() + { + const string testDirectory = "/manga/"; + var fileSystem = new MockFileSystem(); + for (var i = 0; i < 10; i++) + { + fileSystem.AddFile($"{testDirectory}file_{i}.zip", new MockFileData("")); + } + + var ds = new DirectoryService(Substitute.For>(), fileSystem); + ds.CopyFilesToDirectory(new []{$"{testDirectory}file_{0}.zip", $"{testDirectory}file_{200}.zip", $"{testDirectory}file_{1}.zip"}, "/manga/output/"); + Assert.Equal(2, ds.GetFiles("/manga/output/").Count()); + } + [Fact] public void CopyFilesToDirectory_ShouldMoveAllFiles_InclFilesInNestedFolders() { diff --git a/API/API.csproj b/API/API.csproj index 764ed1c2c..492430a44 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -48,32 +48,30 @@ - - - - - + + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - + + diff --git a/API/Controllers/UsersController.cs b/API/Controllers/UsersController.cs index 0a7eeb4d1..3fda79468 100644 --- a/API/Controllers/UsersController.cs +++ b/API/Controllers/UsersController.cs @@ -86,6 +86,7 @@ namespace API.Controllers existingPreferences.BookReaderFontSize = preferencesDto.BookReaderFontSize; existingPreferences.BookReaderTapToPaginate = preferencesDto.BookReaderTapToPaginate; existingPreferences.BookReaderReadingDirection = preferencesDto.BookReaderReadingDirection; + preferencesDto.Theme ??= await _unitOfWork.SiteThemeRepository.GetDefaultTheme(); existingPreferences.Theme = await _unitOfWork.SiteThemeRepository.GetThemeById(preferencesDto.Theme.Id); // TODO: Remove this code - this overrides layout mode to be single until the mode is released diff --git a/API/DTOs/UpdateUserRole.cs b/API/DTOs/UpdateUserRole.cs deleted file mode 100644 index a37076d2c..000000000 --- a/API/DTOs/UpdateUserRole.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Collections.Generic; -using MediatR; - -namespace API.DTOs; - -public class UpdateUserRole : IRequest -{ - public string Username { get; init; } - public IList Roles { get; init; } -} diff --git a/API/Services/DirectoryService.cs b/API/Services/DirectoryService.cs index 227aa9c29..cfbd2b138 100644 --- a/API/Services/DirectoryService.cs +++ b/API/Services/DirectoryService.cs @@ -373,6 +373,11 @@ namespace API.Services { currentFile = file; + if (!FileSystem.File.Exists(file)) + { + _logger.LogError("Unable to copy {File} to {DirectoryPath} as it doesn't exist", file, directoryPath); + continue; + } var fileInfo = FileSystem.FileInfo.FromFileName(file); var targetFile = FileSystem.FileInfo.FromFileName(RenameFileForCopy(file, directoryPath, prepend)); diff --git a/API/Services/Tasks/BackupService.cs b/API/Services/Tasks/BackupService.cs index 0f1b70f9f..240f2807d 100644 --- a/API/Services/Tasks/BackupService.cs +++ b/API/Services/Tasks/BackupService.cs @@ -31,6 +31,7 @@ public class BackupService : IBackupService private readonly IUnitOfWork _unitOfWork; private readonly ILogger _logger; private readonly IDirectoryService _directoryService; + private readonly IConfiguration _config; private readonly IEventHub _eventHub; private readonly IList _backupFiles; @@ -41,11 +42,12 @@ public class BackupService : IBackupService _unitOfWork = unitOfWork; _logger = logger; _directoryService = directoryService; + _config = config; _eventHub = eventHub; - var maxRollingFiles = config.GetMaxRollingFiles(); - var loggingSection = config.GetLoggingFileName(); - var files = GetLogFiles(maxRollingFiles, loggingSection); + // var maxRollingFiles = config.GetMaxRollingFiles(); + // var loggingSection = config.GetLoggingFileName(); + // var files = GetLogFiles(maxRollingFiles, loggingSection); _backupFiles = new List() @@ -58,12 +60,10 @@ public class BackupService : IBackupService "kavita.db-wal" // This wont always be there }; - foreach (var file in files.Select(f => (_directoryService.FileSystem.FileInfo.FromFileName(f)).Name).ToList()) - { - _backupFiles.Add(file); - } - - + // foreach (var file in files.Select(f => (_directoryService.FileSystem.FileInfo.FromFileName(f)).Name)) + // { + // _backupFiles.Add(file); + // } } public IEnumerable GetLogFiles(int maxRollingFiles, string logFileName) @@ -74,7 +74,7 @@ public class BackupService : IBackupService var files = maxRollingFiles > 0 ? _directoryService.GetFiles(_directoryService.LogDirectory, $@"{_directoryService.FileSystem.Path.GetFileNameWithoutExtension(fi.Name)}{multipleFileRegex}\.log") - : new[] {"kavita.log"}; + : new[] {_directoryService.FileSystem.Path.Join(_directoryService.LogDirectory, "kavita.log")}; return files; } @@ -97,6 +97,7 @@ public class BackupService : IBackupService } 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"); @@ -116,15 +117,19 @@ public class BackupService : IBackupService _directoryService.CopyFilesToDirectory( _backupFiles.Select(file => _directoryService.FileSystem.Path.Join(_directoryService.ConfigDirectory, file)).ToList(), tempDirectory); - await SendProgress(0.25F, "Copying core files"); + CopyLogsToBackupDirectory(tempDirectory); + + await SendProgress(0.25F, "Copying cover images"); await CopyCoverImagesToBackupDirectory(tempDirectory); - await SendProgress(0.5F, "Copying cover images"); + await SendProgress(0.5F, "Copying bookmarks"); await CopyBookmarksToBackupDirectory(tempDirectory); - await SendProgress(0.75F, "Copying bookmarks"); + await SendProgress(0.75F, "Copying themes"); + + CopyThemesToBackupDirectory(tempDirectory); try { @@ -140,6 +145,14 @@ public class BackupService : IBackupService await SendProgress(1F, "Completed backup"); } + private void CopyLogsToBackupDirectory(string tempDirectory) + { + var maxRollingFiles = _config.GetMaxRollingFiles(); + var loggingSection = _config.GetLoggingFileName(); + var files = GetLogFiles(maxRollingFiles, loggingSection); + _directoryService.CopyFilesToDirectory(files, _directoryService.FileSystem.Path.Join(tempDirectory, "logs")); + } + private async Task CopyCoverImagesToBackupDirectory(string tempDirectory) { var outputTempDir = Path.Join(tempDirectory, "covers"); @@ -193,6 +206,26 @@ public class BackupService : IBackupService } } + 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, diff --git a/API/Startup.cs b/API/Startup.cs index 2f8ac4d1a..e1dd28a81 100644 --- a/API/Startup.cs +++ b/API/Startup.cs @@ -19,7 +19,6 @@ using Hangfire; using Hangfire.MemoryStorage; using Kavita.Common; using Kavita.Common.EnvironmentInfo; -using MediatR; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; @@ -132,8 +131,6 @@ namespace API // Add IHostedService for startup tasks // Any services that should be bootstrapped go here services.AddHostedService(); - - services.AddMediatR(typeof(Startup)); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/Kavita.Common/Kavita.Common.csproj b/Kavita.Common/Kavita.Common.csproj index f16bbc695..e570f7a4e 100644 --- a/Kavita.Common/Kavita.Common.csproj +++ b/Kavita.Common/Kavita.Common.csproj @@ -12,7 +12,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/UI/Web/src/app/dashboard/dashboard.component.ts b/UI/Web/src/app/dashboard/dashboard.component.ts index 518ee208e..c25c86d9a 100644 --- a/UI/Web/src/app/dashboard/dashboard.component.ts +++ b/UI/Web/src/app/dashboard/dashboard.component.ts @@ -1,8 +1,6 @@ import { Component, OnInit } from '@angular/core'; import { Title } from '@angular/platform-browser'; import { ActivatedRoute } from '@angular/router'; -import { ToastrService } from 'ngx-toastr'; -import { ServerService } from '../_services/server.service'; @Component({ selector: 'app-dashboard', @@ -11,23 +9,8 @@ import { ServerService } from '../_services/server.service'; }) export class DashboardComponent implements OnInit { - tabs: Array<{title: string, fragment: string}> = [ - {title: 'Libraries', fragment: ''}, - {title: 'Lists', fragment: 'lists'}, - {title: 'Collections', fragment: 'collections'}, - ]; - active = this.tabs[0]; - constructor(public route: ActivatedRoute, private serverService: ServerService, - private toastr: ToastrService, private titleService: Title) { - this.route.fragment.subscribe(frag => { - const tab = this.tabs.filter(item => item.fragment === frag); - if (tab.length > 0) { - this.active = tab[0]; - } else { - this.active = this.tabs[0]; // Default to first tab - } - }); + constructor(public route: ActivatedRoute, private titleService: Title) { this.titleService.setTitle('Kavita - Dashboard'); }