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');
}