From a29b11c3663567afdf10b02fa9259837354c55f4 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Wed, 3 Nov 2021 08:36:04 -0500 Subject: [PATCH] Breaking Changes: Docker Parity (#698) * Refactored all the config files for Kavita to be loaded from config/. This will allow docker to just mount one folder and for Update functionality to be trivial. * Cleaned up documentation around new update method. * Updated docker files to support config directory * Removed entrypoint, no longer needed * Update appsettings to point to config directory for logs * Updated message for docker users that are upgrading * Ensure that docker users that have not updated their mount points from upgrade cannot start the server * Code smells * More cleanup * Added entrypoint to fix bind mount issues * Updated README with new folder structure * Fixed build system for new setup * Updated string path if user is docker * Updated the migration flow for docker to work properly and Fixed LogFile configuration updating. * Migrating docker images is now working 100% * Fixed config from bad code * Code cleanup Co-authored-by: Chris Plaatjes --- .github/ISSUE_TEMPLATE/bug_report.md | 4 +- .gitignore | 12 + API.Tests/Services/DirectoryServiceTests.cs | 10 +- API/Controllers/DownloadController.cs | 4 +- API/Controllers/OPDSController.cs | 2 +- API/Data/MigrateConfigFiles.cs | 142 +++++ API/Data/Seed.cs | 6 +- API/Interfaces/Services/IDirectoryService.cs | 12 - API/Program.cs | 145 +++-- API/Services/CacheService.cs | 9 +- API/Services/DirectoryService.cs | 26 +- API/Services/ImageService.cs | 8 +- API/Services/TaskScheduler.cs | 3 +- API/Services/Tasks/BackupService.cs | 10 +- API/Services/Tasks/CleanupService.cs | 12 +- API/Startup.cs | 10 + API/{ => config}/appsettings.Development.json | 4 +- API/config/stats/app_stats - Copy.json | 1 + API/config/stats/app_stats.json | 1 + Dockerfile | 9 +- INSTALL.txt | 4 +- Kavita.Common/AppSettingsConfig.cs | 7 + Kavita.Common/Configuration.cs | 511 +++++++++++------- Kavita.sln.DotSettings | 3 +- README.md | 11 +- build.sh | 2 +- docker-compose.yml | 5 +- entrypoint.sh | 130 +---- pull_request_template.md | 5 +- 29 files changed, 670 insertions(+), 438 deletions(-) create mode 100644 API/Data/MigrateConfigFiles.cs rename API/{ => config}/appsettings.Development.json (82%) create mode 100644 API/config/stats/app_stats - Copy.json create mode 100644 API/config/stats/app_stats.json create mode 100644 Kavita.Common/AppSettingsConfig.cs mode change 100755 => 100644 entrypoint.sh diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index f2a39131a..01de1fb82 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -2,7 +2,7 @@ name: Bug report about: Create a report to help us improve title: '' -labels: bug +labels: needs-triage assignees: '' --- @@ -24,7 +24,7 @@ A clear and concise description of what you expected to happen. If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - - OS: [e.g. iOS] + - OS: [e.g. iOS, Docker] - Browser [e.g. chrome, safari] - Version [e.g. 22] (can be found on Server Settings -> System tab) diff --git a/.gitignore b/.gitignore index 4fbd82c72..c047966d1 100644 --- a/.gitignore +++ b/.gitignore @@ -501,6 +501,18 @@ API/stats/ UI/Web/dist/ /API.Tests/Extensions/Test Data/modified on run.txt /API/covers/ +# All config files in config except appsettings.json +/API/config/covers/ +/API/config/logs/ +/API/config/backups/ +/API/config/cache/ +/API/config/temp/ +/API/config/kavita.db +/API/config/kavita.db-shm +/API/config/kavita.db-wal +/API/config/Hangfire.db +/API/config/Hangfire-log.db API/config/covers/ API/config/*.db + UI/Web/.vscode/settings.json diff --git a/API.Tests/Services/DirectoryServiceTests.cs b/API.Tests/Services/DirectoryServiceTests.cs index 90cf1a217..d64df0d82 100644 --- a/API.Tests/Services/DirectoryServiceTests.cs +++ b/API.Tests/Services/DirectoryServiceTests.cs @@ -36,7 +36,7 @@ namespace API.Tests.Services public void GetFiles_WithCustomRegex_ShouldPass_Test() { var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/DirectoryService/regex"); - var files = _directoryService.GetFiles(testDirectory, @"file\d*.txt"); + var files = DirectoryService.GetFiles(testDirectory, @"file\d*.txt"); Assert.Equal(2, files.Count()); } @@ -44,7 +44,7 @@ namespace API.Tests.Services public void GetFiles_TopLevel_ShouldBeEmpty_Test() { var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/DirectoryService"); - var files = _directoryService.GetFiles(testDirectory); + var files = DirectoryService.GetFiles(testDirectory); Assert.Empty(files); } @@ -52,7 +52,7 @@ namespace API.Tests.Services public void GetFilesWithExtensions_ShouldBeEmpty_Test() { var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/DirectoryService/extensions"); - var files = _directoryService.GetFiles(testDirectory, "*.txt"); + var files = DirectoryService.GetFiles(testDirectory, "*.txt"); Assert.Empty(files); } @@ -60,7 +60,7 @@ namespace API.Tests.Services public void GetFilesWithExtensions_Test() { var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/DirectoryService/extension"); - var files = _directoryService.GetFiles(testDirectory, ".cbz|.rar"); + var files = DirectoryService.GetFiles(testDirectory, ".cbz|.rar"); Assert.Equal(3, files.Count()); } @@ -68,7 +68,7 @@ namespace API.Tests.Services public void GetFilesWithExtensions_BadDirectory_ShouldBeEmpty_Test() { var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/DirectoryService/doesntexist"); - var files = _directoryService.GetFiles(testDirectory, ".cbz|.rar"); + var files = DirectoryService.GetFiles(testDirectory, ".cbz|.rar"); Assert.Empty(files); } diff --git a/API/Controllers/DownloadController.cs b/API/Controllers/DownloadController.cs index d5080846a..d1ea4e8fb 100644 --- a/API/Controllers/DownloadController.cs +++ b/API/Controllers/DownloadController.cs @@ -164,7 +164,7 @@ namespace API.Controllers case MangaFormat.Archive: case MangaFormat.Pdf: _cacheService.ExtractChapterFiles(chapterExtractPath, mangaFiles.ToList()); - var originalFiles = _directoryService.GetFilesWithExtension(chapterExtractPath, + var originalFiles = DirectoryService.GetFilesWithExtension(chapterExtractPath, Parser.Parser.ImageFileExtensions); _directoryService.CopyFilesToDirectory(originalFiles, chapterExtractPath, $"{chapterId}_"); DirectoryService.DeleteFiles(originalFiles); @@ -175,7 +175,7 @@ namespace API.Controllers return BadRequest("Series is not in a valid format. Please rescan series and try again."); } - var files = _directoryService.GetFilesWithExtension(chapterExtractPath, Parser.Parser.ImageFileExtensions); + var files = DirectoryService.GetFilesWithExtension(chapterExtractPath, Parser.Parser.ImageFileExtensions); // Filter out images that aren't in bookmarks Array.Sort(files, _numericComparer); totalFilePaths.AddRange(files.Where((_, i) => chapterPages.Contains(i))); diff --git a/API/Controllers/OPDSController.cs b/API/Controllers/OPDSController.cs index 22103eb2b..34f0d3132 100644 --- a/API/Controllers/OPDSController.cs +++ b/API/Controllers/OPDSController.cs @@ -739,7 +739,7 @@ namespace API.Controllers [HttpGet("{apiKey}/favicon")] public async Task GetFavicon(string apiKey) { - var files = _directoryService.GetFilesWithExtension(Path.Join(Directory.GetCurrentDirectory(), ".."), @"\.ico"); + var files = DirectoryService.GetFilesWithExtension(Path.Join(Directory.GetCurrentDirectory(), ".."), @"\.ico"); if (files.Length == 0) return BadRequest("Cannot find icon"); var path = files[0]; var content = await _directoryService.ReadFileAsync(path); diff --git a/API/Data/MigrateConfigFiles.cs b/API/Data/MigrateConfigFiles.cs new file mode 100644 index 000000000..8b108b98e --- /dev/null +++ b/API/Data/MigrateConfigFiles.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using API.Services; +using Kavita.Common; + +namespace API.Data +{ + public static class MigrateConfigFiles + { + private static readonly List LooseLeafFiles = new List() + { + "appsettings.json", + "appsettings.Development.json", + "kavita.db", + }; + + private static readonly List AppFolders = new List() + { + "covers", + "stats", + "logs", + "backups", + "cache", + "temp" + }; + + private static readonly string ConfigDirectory = Path.Join(Directory.GetCurrentDirectory(), "config"); + + + /// + /// In v0.4.8 we moved all config files to config/ to match with how docker was setup. This will move all config files from current directory + /// to config/ + /// + public static void Migrate(bool isDocker) + { + Console.WriteLine("Checking if migration to config/ is needed"); + + if (isDocker) + { + Console.WriteLine( + "Migrating files from pre-v0.4.8. All Kavita config files are now located in config/"); + + CopyAppFolders(); + DeleteAppFolders(); + + UpdateConfiguration(); + + Console.WriteLine("Migration complete. All config files are now in config/ directory"); + return; + } + + if (!new FileInfo(Path.Join(Directory.GetCurrentDirectory(), "appsettings.json")).Exists) + { + Console.WriteLine("Migration to config/ not needed"); + return; + } + + Console.WriteLine( + "Migrating files from pre-v0.4.8. All Kavita config files are now located in config/"); + + Console.WriteLine($"Creating {ConfigDirectory}"); + DirectoryService.ExistOrCreate(ConfigDirectory); + + CopyLooseLeafFiles(); + + CopyAppFolders(); + + // Then we need to update the config file to point to the new DB file + UpdateConfiguration(); + + // Finally delete everything in the source directory + Console.WriteLine("Removing old files"); + DeleteLooseFiles(); + DeleteAppFolders(); + Console.WriteLine("Removing old files...DONE"); + + Console.WriteLine("Migration complete. All config files are now in config/ directory"); + } + + private static void DeleteAppFolders() + { + foreach (var folderToDelete in AppFolders) + { + if (!new DirectoryInfo(Path.Join(Directory.GetCurrentDirectory(), folderToDelete)).Exists) continue; + + DirectoryService.ClearAndDeleteDirectory(Path.Join(Directory.GetCurrentDirectory(), folderToDelete)); + } + } + + private static void DeleteLooseFiles() + { + var configFiles = LooseLeafFiles.Select(file => new FileInfo(Path.Join(Directory.GetCurrentDirectory(), file))) + .Where(f => f.Exists); + DirectoryService.DeleteFiles(configFiles.Select(f => f.FullName)); + } + + private static void CopyAppFolders() + { + Console.WriteLine("Moving folders to config"); + foreach (var folderToMove in AppFolders) + { + if (new DirectoryInfo(Path.Join(ConfigDirectory, folderToMove)).Exists) continue; + + DirectoryService.CopyDirectoryToDirectory(Path.Join(Directory.GetCurrentDirectory(), folderToMove), + Path.Join(ConfigDirectory, folderToMove)); + } + + Console.WriteLine("Moving folders to config...DONE"); + } + + private static void CopyLooseLeafFiles() + { + var configFiles = LooseLeafFiles.Select(file => new FileInfo(Path.Join(Directory.GetCurrentDirectory(), file))) + .Where(f => f.Exists); + // First step is to move all the files + Console.WriteLine("Moving files to config/"); + foreach (var fileInfo in configFiles) + { + try + { + fileInfo.CopyTo(Path.Join(ConfigDirectory, fileInfo.Name)); + } + catch (Exception) + { + /* Swallow exception when already exists */ + } + } + + Console.WriteLine("Moving files to config...DONE"); + } + + private static void UpdateConfiguration() + { + Console.WriteLine("Updating appsettings.json to new paths"); + Configuration.DatabasePath = "config//kavita.db"; + Configuration.LogPath = "config//logs/kavita.log"; + Console.WriteLine("Updating appsettings.json to new paths...DONE"); + } + } +} diff --git a/API/Data/Seed.cs b/API/Data/Seed.cs index 6b62089d0..ae7c9e818 100644 --- a/API/Data/Seed.cs +++ b/API/Data/Seed.cs @@ -41,11 +41,11 @@ namespace API.Data IList defaultSettings = new List() { - new() {Key = ServerSettingKey.CacheDirectory, Value = CacheService.CacheDirectory}, + new() {Key = ServerSettingKey.CacheDirectory, Value = DirectoryService.CacheDirectory}, new () {Key = ServerSettingKey.TaskScan, Value = "daily"}, new () {Key = ServerSettingKey.LoggingLevel, Value = "Information"}, // Not used from DB, but DB is sync with appSettings.json new () {Key = ServerSettingKey.TaskBackup, Value = "weekly"}, - new () {Key = ServerSettingKey.BackupDirectory, Value = Path.GetFullPath(Path.Join(Directory.GetCurrentDirectory(), "backups/"))}, + new () {Key = ServerSettingKey.BackupDirectory, Value = Path.GetFullPath(DirectoryService.BackupDirectory)}, new () {Key = ServerSettingKey.Port, Value = "5000"}, // Not used from DB, but DB is sync with appSettings.json new () {Key = ServerSettingKey.AllowStatCollection, Value = "true"}, new () {Key = ServerSettingKey.EnableOpds, Value = "false"}, @@ -69,6 +69,8 @@ namespace API.Data Configuration.Port + string.Empty; context.ServerSetting.First(s => s.Key == ServerSettingKey.LoggingLevel).Value = Configuration.LogLevel + string.Empty; + context.ServerSetting.First(s => s.Key == ServerSettingKey.CacheDirectory).Value = + DirectoryService.CacheDirectory + string.Empty; await context.SaveChangesAsync(); diff --git a/API/Interfaces/Services/IDirectoryService.cs b/API/Interfaces/Services/IDirectoryService.cs index 43779774c..a8ae8c05f 100644 --- a/API/Interfaces/Services/IDirectoryService.cs +++ b/API/Interfaces/Services/IDirectoryService.cs @@ -12,21 +12,9 @@ namespace API.Interfaces.Services /// Absolute path of directory to scan. /// List of folder names IEnumerable ListDirectory(string rootPath); - /// - /// Gets files in a directory. If searchPatternExpression is passed, will match the regex against for filtering. - /// - /// - /// - /// - string[] GetFilesWithExtension(string path, string searchPatternExpression = ""); Task ReadFileAsync(string path); bool CopyFilesToDirectory(IEnumerable filePaths, string directoryPath, string prepend = ""); bool Exists(string directory); - - IEnumerable GetFiles(string path, string searchPatternExpression = "", - SearchOption searchOption = SearchOption.TopDirectoryOnly); - void CopyFileToDirectory(string fullFilePath, string targetDirectory); - public bool CopyDirectoryToDirectory(string sourceDirName, string destDirName, string searchPattern = "*"); } } diff --git a/API/Program.cs b/API/Program.cs index 06c860366..f35bf8bd3 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -1,98 +1,127 @@ using System; +using System.Collections.Generic; using System.IO; +using System.Linq; using System.Security.Cryptography; using System.Threading.Tasks; using API.Data; using API.Entities; using API.Services; using Kavita.Common; +using Kavita.Common.EnvironmentInfo; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace API { - public class Program - { - private static readonly int HttpPort = Configuration.Port; + public class Program + { + private static readonly int HttpPort = Configuration.Port; - protected Program() - { - } + protected Program() + { + } - public static async Task Main(string[] args) - { - Console.OutputEncoding = System.Text.Encoding.UTF8; + public static async Task Main(string[] args) + { + Console.OutputEncoding = System.Text.Encoding.UTF8; + var isDocker = new OsInfo(Array.Empty()).IsDocker; - // Before anything, check if JWT has been generated properly or if user still has default - if (!Configuration.CheckIfJwtTokenSet() && - Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") != Environments.Development) - { - Console.WriteLine("Generating JWT TokenKey for encrypting user sessions..."); - var rBytes = new byte[128]; - using (var crypto = new RNGCryptoServiceProvider()) crypto.GetBytes(rBytes); - Configuration.JwtToken = Convert.ToBase64String(rBytes).Replace("/", string.Empty); - } + MigrateConfigFiles.Migrate(isDocker); - var host = CreateHostBuilder(args).Build(); + // Before anything, check if JWT has been generated properly or if user still has default + if (!Configuration.CheckIfJwtTokenSet() && + Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") != Environments.Development) + { + Console.WriteLine("Generating JWT TokenKey for encrypting user sessions..."); + var rBytes = new byte[128]; + using (var crypto = new RNGCryptoServiceProvider()) crypto.GetBytes(rBytes); + Configuration.JwtToken = Convert.ToBase64String(rBytes).Replace("/", string.Empty); + } - using var scope = host.Services.CreateScope(); - var services = scope.ServiceProvider; + var host = CreateHostBuilder(args).Build(); - try - { - var context = services.GetRequiredService(); - var roleManager = services.GetRequiredService>(); + using var scope = host.Services.CreateScope(); + var services = scope.ServiceProvider; - var requiresCoverImageMigration = !Directory.Exists(DirectoryService.CoverImageDirectory); try { - // If this is a new install, tables wont exist yet + var context = services.GetRequiredService(); + var roleManager = services.GetRequiredService>(); + + if (isDocker && new FileInfo("data/appsettings.json").Exists) + { + var logger = services.GetRequiredService>(); + logger.LogCritical("WARNING! Mount point is incorrect, nothing here will persist. Please change your container mount from /kavita/data to /kavita/config"); + return; + } + + + var requiresCoverImageMigration = !Directory.Exists(DirectoryService.CoverImageDirectory); + try + { + // If this is a new install, tables wont exist yet + if (requiresCoverImageMigration) + { + MigrateCoverImages.ExtractToImages(context); + } + } + catch (Exception) + { + requiresCoverImageMigration = false; + } + + // Apply all migrations on startup + await context.Database.MigrateAsync(); + if (requiresCoverImageMigration) { - MigrateCoverImages.ExtractToImages(context); + await MigrateCoverImages.UpdateDatabaseWithImages(context); } + + await Seed.SeedRoles(roleManager); + await Seed.SeedSettings(context); + await Seed.SeedUserApiKeys(context); } - catch (Exception ) + catch (Exception ex) { - requiresCoverImageMigration = false; + var logger = services.GetRequiredService>(); + logger.LogError(ex, "An error occurred during migration"); } - // Apply all migrations on startup - await context.Database.MigrateAsync(); + await host.RunAsync(); + } - if (requiresCoverImageMigration) - { - await MigrateCoverImages.UpdateDatabaseWithImages(context); - } + private static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureAppConfiguration((hostingContext, config) => + { + config.Sources.Clear(); - await Seed.SeedRoles(roleManager); - await Seed.SeedSettings(context); - await Seed.SeedUserApiKeys(context); - } - catch (Exception ex) - { - var logger = services.GetRequiredService>(); - logger.LogError(ex, "An error occurred during migration"); - } + var env = hostingContext.HostingEnvironment; - await host.RunAsync(); - } + config.AddJsonFile("config/appsettings.json", optional: true, reloadOnChange: false) + .AddJsonFile($"config/appsettings.{env.EnvironmentName}.json", + optional: true, reloadOnChange: false); + }) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseKestrel((opts) => + { + opts.ListenAnyIP(HttpPort, options => { options.Protocols = HttpProtocols.Http1AndHttp2; }); + }); - private static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseKestrel((opts) => - { - opts.ListenAnyIP(HttpPort, options => { options.Protocols = HttpProtocols.Http1AndHttp2; }); - }); + webBuilder.UseStartup(); + }); - webBuilder.UseStartup(); - }); - } + + + + } } diff --git a/API/Services/CacheService.cs b/API/Services/CacheService.cs index 8decdeccd..a64bde675 100644 --- a/API/Services/CacheService.cs +++ b/API/Services/CacheService.cs @@ -21,7 +21,6 @@ namespace API.Services private readonly IDirectoryService _directoryService; private readonly IBookService _bookService; private readonly NumericComparer _numericComparer; - public static readonly string CacheDirectory = Path.GetFullPath(Path.Join(Directory.GetCurrentDirectory(), "cache/")); public CacheService(ILogger logger, IUnitOfWork unitOfWork, IArchiveService archiveService, IDirectoryService directoryService, IBookService bookService) @@ -38,7 +37,7 @@ namespace API.Services { if (!DirectoryService.ExistOrCreate(DirectoryService.CacheDirectory)) { - _logger.LogError("Cache directory {CacheDirectory} is not accessible or does not exist. Creating...", CacheDirectory); + _logger.LogError("Cache directory {CacheDirectory} is not accessible or does not exist. Creating...", DirectoryService.CacheDirectory); } } @@ -102,7 +101,7 @@ namespace API.Services } else { - _directoryService.CopyDirectoryToDirectory(Path.GetDirectoryName(files[0].FilePath), extractPath, + DirectoryService.CopyDirectoryToDirectory(Path.GetDirectoryName(files[0].FilePath), extractPath, Parser.Parser.ImageFileExtensions); } @@ -147,7 +146,7 @@ namespace API.Services try { - DirectoryService.ClearDirectory(CacheDirectory); + DirectoryService.ClearDirectory(DirectoryService.CacheDirectory); } catch (Exception ex) { @@ -198,7 +197,7 @@ namespace API.Services if (page <= (mangaFile.Pages + pagesSoFar)) { var path = GetCachePath(chapter.Id); - var files = _directoryService.GetFilesWithExtension(path, Parser.Parser.ImageFileExtensions); + var files = DirectoryService.GetFilesWithExtension(path, Parser.Parser.ImageFileExtensions); Array.Sort(files, _numericComparer); if (files.Length == 0) diff --git a/API/Services/DirectoryService.cs b/API/Services/DirectoryService.cs index fd1256e49..a606b774c 100644 --- a/API/Services/DirectoryService.cs +++ b/API/Services/DirectoryService.cs @@ -16,11 +16,12 @@ namespace API.Services private static readonly Regex ExcludeDirectories = new Regex( @"@eaDir|\.DS_Store", RegexOptions.Compiled | RegexOptions.IgnoreCase); - public static readonly string TempDirectory = Path.Join(Directory.GetCurrentDirectory(), "temp"); - public static readonly string LogDirectory = Path.Join(Directory.GetCurrentDirectory(), "logs"); - public static readonly string CacheDirectory = Path.Join(Directory.GetCurrentDirectory(), "cache"); - public static readonly string CoverImageDirectory = Path.Join(Directory.GetCurrentDirectory(), "covers"); - public static readonly string StatsDirectory = Path.Join(Directory.GetCurrentDirectory(), "stats"); + public static readonly string TempDirectory = Path.Join(Directory.GetCurrentDirectory(), "config", "temp"); + public static readonly string LogDirectory = Path.Join(Directory.GetCurrentDirectory(), "config", "logs"); + public static readonly string CacheDirectory = Path.Join(Directory.GetCurrentDirectory(), "config", "cache"); + public static readonly string CoverImageDirectory = Path.Join(Directory.GetCurrentDirectory(), "config", "covers"); + public static readonly string BackupDirectory = Path.Join(Directory.GetCurrentDirectory(), "config", "backups"); + public static readonly string StatsDirectory = Path.Join(Directory.GetCurrentDirectory(), "config", "stats"); public DirectoryService(ILogger logger) { @@ -47,7 +48,6 @@ namespace API.Services reSearchPattern.IsMatch(Path.GetExtension(file)) && !Path.GetFileName(file).StartsWith(Parser.Parser.MacOsMetadataFileStartsWith)); } - /// /// Returns a list of folders from end of fullPath to rootPath. If a file is passed at the end of the fullPath, it will be ignored. /// @@ -96,7 +96,7 @@ namespace API.Services return di.Exists; } - public IEnumerable GetFiles(string path, string searchPatternExpression = "", + public static IEnumerable GetFiles(string path, string searchPatternExpression = "", SearchOption searchOption = SearchOption.TopDirectoryOnly) { if (searchPatternExpression != string.Empty) @@ -132,10 +132,10 @@ namespace API.Services /// /// /// - /// Defaults to *, meaning all files + /// Defaults to empty string, meaning all files /// /// - public bool CopyDirectoryToDirectory(string sourceDirName, string destDirName, string searchPattern = "*") + public static bool CopyDirectoryToDirectory(string sourceDirName, string destDirName, string searchPattern = "") { if (string.IsNullOrEmpty(sourceDirName)) return false; @@ -177,7 +177,13 @@ namespace API.Services - public string[] GetFilesWithExtension(string path, string searchPatternExpression = "") + /// + /// Get files with a file extension + /// + /// + /// Regex to use for searching on regex. Defaults to empty string for all files + /// + public static string[] GetFilesWithExtension(string path, string searchPatternExpression = "") { if (searchPatternExpression != string.Empty) { diff --git a/API/Services/ImageService.cs b/API/Services/ImageService.cs index c2b3d4126..7f663c37d 100644 --- a/API/Services/ImageService.cs +++ b/API/Services/ImageService.cs @@ -13,7 +13,6 @@ namespace API.Services public class ImageService : IImageService { private readonly ILogger _logger; - private readonly IDirectoryService _directoryService; public const string ChapterCoverImageRegex = @"v\d+_c\d+"; public const string SeriesCoverImageRegex = @"seres\d+"; public const string CollectionTagCoverImageRegex = @"tag\d+"; @@ -24,10 +23,9 @@ namespace API.Services /// private const int ThumbnailWidth = 320; - public ImageService(ILogger logger, IDirectoryService directoryService) + public ImageService(ILogger logger) { _logger = logger; - _directoryService = directoryService; } /// @@ -44,9 +42,9 @@ namespace API.Services return null; } - var firstImage = _directoryService.GetFilesWithExtension(directory, Parser.Parser.ImageFileExtensions) + var firstImage = DirectoryService.GetFilesWithExtension(directory, Parser.Parser.ImageFileExtensions) .OrderBy(f => f, new NaturalSortComparer()).FirstOrDefault(); - + return firstImage; } diff --git a/API/Services/TaskScheduler.cs b/API/Services/TaskScheduler.cs index 5804ad9ce..ee68df106 100644 --- a/API/Services/TaskScheduler.cs +++ b/API/Services/TaskScheduler.cs @@ -138,8 +138,7 @@ namespace API.Services public void CleanupTemp() { - var tempDirectory = Path.Join(Directory.GetCurrentDirectory(), "temp"); - BackgroundJob.Enqueue(() => DirectoryService.ClearDirectory(tempDirectory)); + BackgroundJob.Enqueue(() => DirectoryService.ClearDirectory(DirectoryService.TempDirectory)); } public void RefreshSeriesMetadata(int libraryId, int seriesId, bool forceUpdate = true) diff --git a/API/Services/Tasks/BackupService.cs b/API/Services/Tasks/BackupService.cs index ab8a84ea9..e71f35e9f 100644 --- a/API/Services/Tasks/BackupService.cs +++ b/API/Services/Tasks/BackupService.cs @@ -20,8 +20,8 @@ namespace API.Services.Tasks private readonly IUnitOfWork _unitOfWork; private readonly ILogger _logger; private readonly IDirectoryService _directoryService; - private readonly string _tempDirectory = Path.Join(Directory.GetCurrentDirectory(), "temp"); - private readonly string _logDirectory = Path.Join(Directory.GetCurrentDirectory(), "logs"); + private readonly string _tempDirectory = DirectoryService.TempDirectory; + private readonly string _logDirectory = DirectoryService.LogDirectory; private readonly IList _backupFiles; @@ -72,7 +72,7 @@ namespace API.Services.Tasks var fi = new FileInfo(logFileName); var files = maxRollingFiles > 0 - ? _directoryService.GetFiles(_logDirectory, $@"{Path.GetFileNameWithoutExtension(fi.Name)}{multipleFileRegex}\.log") + ? DirectoryService.GetFiles(_logDirectory, $@"{Path.GetFileNameWithoutExtension(fi.Name)}{multipleFileRegex}\.log") : new[] {"kavita.log"}; return files; } @@ -148,7 +148,7 @@ namespace API.Services.Tasks // Swallow exception. This can be a duplicate cover being copied as chapter and volumes can share same file. } - if (!_directoryService.GetFiles(outputTempDir).Any()) + if (!DirectoryService.GetFiles(outputTempDir).Any()) { DirectoryService.ClearAndDeleteDirectory(outputTempDir); } @@ -164,7 +164,7 @@ namespace API.Services.Tasks var backupDirectory = Task.Run(() => _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BackupDirectory)).Result.Value; if (!_directoryService.Exists(backupDirectory)) return; var deltaTime = DateTime.Today.Subtract(TimeSpan.FromDays(dayThreshold)); - var allBackups = _directoryService.GetFiles(backupDirectory).ToList(); + var allBackups = DirectoryService.GetFiles(backupDirectory).ToList(); var expiredBackups = allBackups.Select(filename => new FileInfo(filename)) .Where(f => f.CreationTime > deltaTime) .ToList(); diff --git a/API/Services/Tasks/CleanupService.cs b/API/Services/Tasks/CleanupService.cs index 93f8ec5db..a3c63c30f 100644 --- a/API/Services/Tasks/CleanupService.cs +++ b/API/Services/Tasks/CleanupService.cs @@ -16,16 +16,14 @@ namespace API.Services.Tasks private readonly ILogger _logger; private readonly IBackupService _backupService; private readonly IUnitOfWork _unitOfWork; - private readonly IDirectoryService _directoryService; public CleanupService(ICacheService cacheService, ILogger logger, - IBackupService backupService, IUnitOfWork unitOfWork, IDirectoryService directoryService) + IBackupService backupService, IUnitOfWork unitOfWork) { _cacheService = cacheService; _logger = logger; _backupService = backupService; _unitOfWork = unitOfWork; - _directoryService = directoryService; } public void CleanupCacheDirectory() @@ -42,7 +40,7 @@ namespace API.Services.Tasks { _logger.LogInformation("Starting Cleanup"); _logger.LogInformation("Cleaning temp directory"); - var tempDirectory = Path.Join(Directory.GetCurrentDirectory(), "temp"); + var tempDirectory = DirectoryService.TempDirectory; DirectoryService.ClearDirectory(tempDirectory); CleanupCacheDirectory(); _logger.LogInformation("Cleaning old database backups"); @@ -57,7 +55,7 @@ namespace API.Services.Tasks private async Task DeleteSeriesCoverImages() { var images = await _unitOfWork.SeriesRepository.GetAllCoverImagesAsync(); - var files = _directoryService.GetFiles(DirectoryService.CoverImageDirectory, ImageService.SeriesCoverImageRegex); + var files = DirectoryService.GetFiles(DirectoryService.CoverImageDirectory, ImageService.SeriesCoverImageRegex); foreach (var file in files) { if (images.Contains(Path.GetFileName(file))) continue; @@ -69,7 +67,7 @@ namespace API.Services.Tasks private async Task DeleteChapterCoverImages() { var images = await _unitOfWork.ChapterRepository.GetAllCoverImagesAsync(); - var files = _directoryService.GetFiles(DirectoryService.CoverImageDirectory, ImageService.ChapterCoverImageRegex); + var files = DirectoryService.GetFiles(DirectoryService.CoverImageDirectory, ImageService.ChapterCoverImageRegex); foreach (var file in files) { if (images.Contains(Path.GetFileName(file))) continue; @@ -81,7 +79,7 @@ namespace API.Services.Tasks private async Task DeleteTagCoverImages() { var images = await _unitOfWork.CollectionTagRepository.GetAllCoverImagesAsync(); - var files = _directoryService.GetFiles(DirectoryService.CoverImageDirectory, ImageService.CollectionTagCoverImageRegex); + var files = DirectoryService.GetFiles(DirectoryService.CoverImageDirectory, ImageService.CollectionTagCoverImageRegex); foreach (var file in files) { if (images.Contains(Path.GetFileName(file))) continue; diff --git a/API/Startup.cs b/API/Startup.cs index 1eebf2d09..6668927b4 100644 --- a/API/Startup.cs +++ b/API/Startup.cs @@ -24,6 +24,7 @@ using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; using Microsoft.OpenApi.Models; namespace API @@ -217,6 +218,15 @@ namespace API applicationLifetime.ApplicationStopping.Register(OnShutdown); applicationLifetime.ApplicationStarted.Register(() => { + try + { + var logger = serviceProvider.GetRequiredService>(); + logger.LogInformation("Kavita - v{Version}", BuildInfo.Version); + } + catch (Exception) + { + /* Swallow Exception */ + } Console.WriteLine($"Kavita - v{BuildInfo.Version}"); }); } diff --git a/API/appsettings.Development.json b/API/config/appsettings.Development.json similarity index 82% rename from API/appsettings.Development.json rename to API/config/appsettings.Development.json index ec9502e47..ac1707592 100644 --- a/API/appsettings.Development.json +++ b/API/config/appsettings.Development.json @@ -1,6 +1,6 @@ { "ConnectionStrings": { - "DefaultConnection": "Data source=kavita.db" + "DefaultConnection": "Data source=config//kavita.db" }, "TokenKey": "super secret unguessable key", "Logging": { @@ -12,7 +12,7 @@ "Microsoft.AspNetCore.Hosting.Internal.WebHost": "Information" }, "File": { - "Path": "logs/kavita.log", + "Path": "config//logs/kavita.log", "Append": "True", "FileSizeLimitBytes": 26214400, "MaxRollingFiles": 2 diff --git a/API/config/stats/app_stats - Copy.json b/API/config/stats/app_stats - Copy.json new file mode 100644 index 000000000..2ea2d8321 --- /dev/null +++ b/API/config/stats/app_stats - Copy.json @@ -0,0 +1 @@ +{"InstallId":"2c158339","LastUpdate":"2021-07-26T00:32:01.7509137Z","UsageInfo":null,"ServerInfo":null,"ClientsInfo":[{"KavitaUiVersion":"0.4.2","ScreenResolution":"1920 x 1080","PlatformType":"desktop","Browser":{"Name":"Chrome","Version":"91.0.4472.124"},"Os":{"Name":"Windows","Version":"NT 10.0"},"CollectedAt":"2021-07-26T00:32:01.7289388Z","UsingDarkTheme":true}]} \ No newline at end of file diff --git a/API/config/stats/app_stats.json b/API/config/stats/app_stats.json new file mode 100644 index 000000000..b02f7729f --- /dev/null +++ b/API/config/stats/app_stats.json @@ -0,0 +1 @@ +{"InstallId":"2c158339","LastUpdate":"2021-10-20T12:44:40.269713Z","UsageInfo":{"UsersCount":6,"FileTypes":[".epub",".pdf",".cbr",".cbz",".zip",".rar",".jpg",".7z",".png"],"LibraryTypesCreated":[{"Type":0,"Count":2},{"Type":1,"Count":2},{"Type":2,"Count":1}]},"ServerInfo":{"Os":"Microsoft Windows 10.0.19042","DotNetVersion":"5.0.9","RunTimeVersion":".NET 5.0.9","KavitaVersion":"0.4.7.16","BuildBranch":"Debug","Culture":"en-US","IsDocker":false,"NumOfCores":12},"ClientsInfo":[{"KavitaUiVersion":"0.4.2","ScreenResolution":"1920 x 1080","PlatformType":"desktop","Browser":{"Name":"Chrome","Version":"94.0.4606.81"},"Os":{"Name":"Windows","Version":"NT 10.0"},"CollectedAt":"2021-10-20T12:36:27.2838212Z","UsingDarkTheme":true}]} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index a1d36ee56..82fd49132 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,19 +20,14 @@ COPY --from=copytask /files/wwwroot /kavita/wwwroot #Installs program dependencies RUN apt-get update \ - && apt-get install -y libicu-dev libssl1.1 pwgen libgdiplus \ + && apt-get install -y libicu-dev libssl1.1 libgdiplus \ && rm -rf /var/lib/apt/lists/* -#Creates the data directory -RUN mkdir /kavita/data - -RUN sed -i 's/Data source=kavita.db/Data source=data\/kavita.db/g' /kavita/appsettings.json - COPY entrypoint.sh /entrypoint.sh EXPOSE 5000 WORKDIR /kavita -ENTRYPOINT ["/bin/bash"] +ENTRYPOINT [ "/bin/bash" ] CMD ["/entrypoint.sh"] diff --git a/INSTALL.txt b/INSTALL.txt index a7d2bd1bc..9119da82c 100644 --- a/INSTALL.txt +++ b/INSTALL.txt @@ -2,4 +2,6 @@ 1. Unzip the archive to a directory that is writable. If on windows, do not place in Program Files. 2. (Linux only) Chmod and Chown so Kavita can write to the directory you placed in. 3. Run Kavita executable. -4. Open localhost:5000 and setup your account and libraries in the UI. \ No newline at end of file +4. Open localhost:5000 and setup your account and libraries in the UI. + +If updating, copy everything but the config/ directory over. Restart Kavita. diff --git a/Kavita.Common/AppSettingsConfig.cs b/Kavita.Common/AppSettingsConfig.cs new file mode 100644 index 000000000..c7718b230 --- /dev/null +++ b/Kavita.Common/AppSettingsConfig.cs @@ -0,0 +1,7 @@ +namespace Kavita.Common +{ + public class AppSettingsConfig + { + + } +} diff --git a/Kavita.Common/Configuration.cs b/Kavita.Common/Configuration.cs index c2967c883..f5f995fce 100644 --- a/Kavita.Common/Configuration.cs +++ b/Kavita.Common/Configuration.cs @@ -6,236 +6,349 @@ using Microsoft.Extensions.Hosting; namespace Kavita.Common { - public static class Configuration - { - private static readonly string AppSettingsFilename = GetAppSettingFilename(); - public static string Branch - { - get => GetBranch(GetAppSettingFilename()); - set => SetBranch(GetAppSettingFilename(), value); - } + public static class Configuration + { + private static readonly string AppSettingsFilename = Path.Join("config", GetAppSettingFilename()); - public static int Port - { - get => GetPort(GetAppSettingFilename()); - set => SetPort(GetAppSettingFilename(), value); - } + public static string Branch + { + get => GetBranch(GetAppSettingFilename()); + set => SetBranch(GetAppSettingFilename(), value); + } - public static string JwtToken - { - get => GetJwtToken(GetAppSettingFilename()); - set => SetJwtToken(GetAppSettingFilename(), value); - } + public static int Port + { + get => GetPort(GetAppSettingFilename()); + set => SetPort(GetAppSettingFilename(), value); + } - public static string LogLevel - { - get => GetLogLevel(GetAppSettingFilename()); - set => SetLogLevel(GetAppSettingFilename(), value); - } + public static string JwtToken + { + get => GetJwtToken(GetAppSettingFilename()); + set => SetJwtToken(GetAppSettingFilename(), value); + } - private static string GetAppSettingFilename() - { - if (!string.IsNullOrEmpty(AppSettingsFilename)) - { - return AppSettingsFilename; - } - var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); - var isDevelopment = environment == Environments.Development; - return "appsettings" + (isDevelopment ? ".Development" : "") + ".json"; - } + public static string LogLevel + { + get => GetLogLevel(GetAppSettingFilename()); + set => SetLogLevel(GetAppSettingFilename(), value); + } - #region JWT Token + public static string LogPath + { + get => GetLoggingFile(GetAppSettingFilename()); + set => SetLoggingFile(GetAppSettingFilename(), value); + } - private static string GetJwtToken(string filePath) - { - try - { - var json = File.ReadAllText(filePath); - var jsonObj = JsonSerializer.Deserialize(json); - const string key = "TokenKey"; + public static string DatabasePath + { + get => GetDatabasePath(GetAppSettingFilename()); + set => SetDatabasePath(GetAppSettingFilename(), value); + } - if (jsonObj.TryGetProperty(key, out JsonElement tokenElement)) + private static string GetAppSettingFilename() + { + if (!string.IsNullOrEmpty(AppSettingsFilename)) { - return tokenElement.GetString(); + return AppSettingsFilename; + } + + var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); + var isDevelopment = environment == Environments.Development; + return "appsettings" + (isDevelopment ? ".Development" : string.Empty) + ".json"; + } + + #region JWT Token + + private static string GetJwtToken(string filePath) + { + try + { + var json = File.ReadAllText(filePath); + var jsonObj = JsonSerializer.Deserialize(json); + const string key = "TokenKey"; + + if (jsonObj.TryGetProperty(key, out JsonElement tokenElement)) + { + return tokenElement.GetString(); + } + + return string.Empty; + } + catch (Exception ex) + { + Console.WriteLine("Error reading app settings: " + ex.Message); } return string.Empty; - } - catch (Exception ex) - { - Console.WriteLine("Error reading app settings: " + ex.Message); - } + } - return string.Empty; - } + private static void SetJwtToken(string filePath, string token) + { + try + { + var currentToken = GetJwtToken(filePath); + var json = File.ReadAllText(filePath) + .Replace("\"TokenKey\": \"" + currentToken, "\"TokenKey\": \"" + token); + File.WriteAllText(filePath, json); + } + catch (Exception) + { + /* Swallow exception */ + } + } - private static void SetJwtToken(string filePath, string token) - { - try - { - var currentToken = GetJwtToken(filePath); - var json = File.ReadAllText(filePath) - .Replace("\"TokenKey\": \"" + currentToken, "\"TokenKey\": \"" + token); - File.WriteAllText(filePath, json); - } - catch (Exception) - { - /* Swallow exception */ - } - } + public static bool CheckIfJwtTokenSet() + { + try + { + return GetJwtToken(GetAppSettingFilename()) != "super secret unguessable key"; + } + catch (Exception ex) + { + Console.WriteLine("Error writing app settings: " + ex.Message); + } - public static bool CheckIfJwtTokenSet() - { - try - { - return GetJwtToken(GetAppSettingFilename()) != "super secret unguessable key"; - } - catch (Exception ex) - { - Console.WriteLine("Error writing app settings: " + ex.Message); - } + return false; + } - return false; - } + #endregion + #region Port - #endregion + private static void SetPort(string filePath, int port) + { + if (new OsInfo(Array.Empty()).IsDocker) + { + return; + } - #region Port + try + { + var currentPort = GetPort(filePath); + var json = File.ReadAllText(filePath).Replace("\"Port\": " + currentPort, "\"Port\": " + port); + File.WriteAllText(filePath, json); + } + catch (Exception) + { + /* Swallow Exception */ + } + } - private static void SetPort(string filePath, int port) - { - if (new OsInfo(Array.Empty()).IsDocker) - { - return; - } + private static int GetPort(string filePath) + { + const int defaultPort = 5000; + if (new OsInfo(Array.Empty()).IsDocker) + { + return defaultPort; + } - try - { - var currentPort = GetPort(filePath); - var json = File.ReadAllText(filePath).Replace("\"Port\": " + currentPort, "\"Port\": " + port); - File.WriteAllText(filePath, json); - } - catch (Exception) - { - /* Swallow Exception */ - } - } + try + { + var json = File.ReadAllText(filePath); + var jsonObj = JsonSerializer.Deserialize(json); + const string key = "Port"; + + if (jsonObj.TryGetProperty(key, out JsonElement tokenElement)) + { + return tokenElement.GetInt32(); + } + } + catch (Exception ex) + { + Console.WriteLine("Error writing app settings: " + ex.Message); + } - private static int GetPort(string filePath) - { - const int defaultPort = 5000; - if (new OsInfo(Array.Empty()).IsDocker) - { return defaultPort; - } + } - try - { - var json = File.ReadAllText(filePath); - var jsonObj = JsonSerializer.Deserialize(json); - const string key = "Port"; + #endregion - if (jsonObj.TryGetProperty(key, out JsonElement tokenElement)) + #region LogLevel + + private static void SetLogLevel(string filePath, string logLevel) + { + try { - return tokenElement.GetInt32(); + var currentLevel = GetLogLevel(filePath); + var json = File.ReadAllText(filePath) + .Replace($"\"Default\": \"{currentLevel}\"", $"\"Default\": \"{logLevel}\""); + File.WriteAllText(filePath, json); } - } - catch (Exception ex) - { - Console.WriteLine("Error writing app settings: " + ex.Message); - } - - return defaultPort; - } - - #endregion - - #region LogLevel - - private static void SetLogLevel(string filePath, string logLevel) - { - try - { - var currentLevel = GetLogLevel(filePath); - var json = File.ReadAllText(filePath) - .Replace($"\"Default\": \"{currentLevel}\"", $"\"Default\": \"{logLevel}\""); - File.WriteAllText(filePath, json); - } - catch (Exception) - { - /* Swallow Exception */ - } - } - - private static string GetLogLevel(string filePath) - { - try - { - var json = File.ReadAllText(filePath); - var jsonObj = JsonSerializer.Deserialize(json); - - if (jsonObj.TryGetProperty("Logging", out JsonElement tokenElement)) + catch (Exception) { - foreach (var property in tokenElement.EnumerateObject()) - { - if (!property.Name.Equals("LogLevel")) continue; - foreach (var logProperty in property.Value.EnumerateObject()) - { - if (logProperty.Name.Equals("Default")) - { - return logProperty.Value.GetString(); - } - } - } + /* Swallow Exception */ } - } - catch (Exception ex) - { - Console.WriteLine("Error writing app settings: " + ex.Message); - } + } - return "Information"; - } - - #endregion - - private static string GetBranch(string filePath) - { - const string defaultBranch = "main"; - - try - { - var json = File.ReadAllText(filePath); - var jsonObj = JsonSerializer.Deserialize(json); - const string key = "Branch"; - - if (jsonObj.TryGetProperty(key, out JsonElement tokenElement)) + private static string GetLogLevel(string filePath) + { + try { - return tokenElement.GetString(); + var json = File.ReadAllText(filePath); + var jsonObj = JsonSerializer.Deserialize(json); + + if (jsonObj.TryGetProperty("Logging", out JsonElement tokenElement)) + { + foreach (var property in tokenElement.EnumerateObject()) + { + if (!property.Name.Equals("LogLevel")) continue; + foreach (var logProperty in property.Value.EnumerateObject()) + { + if (logProperty.Name.Equals("Default")) + { + return logProperty.Value.GetString(); + } + } + } + } + } + catch (Exception ex) + { + Console.WriteLine("Error writing app settings: " + ex.Message); } - } - catch (Exception ex) - { - Console.WriteLine("Error reading app settings: " + ex.Message); - } - return defaultBranch; - } + return "Information"; + } - private static void SetBranch(string filePath, string updatedBranch) - { - try - { - var currentBranch = GetBranch(filePath); - var json = File.ReadAllText(filePath) - .Replace("\"Branch\": " + currentBranch, "\"Branch\": " + updatedBranch); - File.WriteAllText(filePath, json); - } - catch (Exception) - { - /* Swallow Exception */ - } - } - } + #endregion + + private static string GetBranch(string filePath) + { + const string defaultBranch = "main"; + + try + { + var json = File.ReadAllText(filePath); + var jsonObj = JsonSerializer.Deserialize(json); + const string key = "Branch"; + + if (jsonObj.TryGetProperty(key, out JsonElement tokenElement)) + { + return tokenElement.GetString(); + } + } + catch (Exception ex) + { + Console.WriteLine("Error reading app settings: " + ex.Message); + } + + return defaultBranch; + } + + private static void SetBranch(string filePath, string updatedBranch) + { + try + { + var currentBranch = GetBranch(filePath); + var json = File.ReadAllText(filePath) + .Replace("\"Branch\": " + currentBranch, "\"Branch\": " + updatedBranch); + File.WriteAllText(filePath, json); + } + catch (Exception) + { + /* Swallow Exception */ + } + } + + private static string GetLoggingFile(string filePath) + { + const string defaultFile = "config/logs/kavita.log"; + + try + { + var json = File.ReadAllText(filePath); + var jsonObj = JsonSerializer.Deserialize(json); + + if (jsonObj.TryGetProperty("Logging", out JsonElement tokenElement)) + { + foreach (var property in tokenElement.EnumerateObject()) + { + if (!property.Name.Equals("File")) continue; + foreach (var logProperty in property.Value.EnumerateObject()) + { + if (logProperty.Name.Equals("Path")) + { + return logProperty.Value.GetString(); + } + } + } + } + } + catch (Exception ex) + { + Console.WriteLine("Error writing app settings: " + ex.Message); + } + + return defaultFile; + } + + /// + /// This should NEVER be called except by + /// + /// + /// + private static void SetLoggingFile(string filePath, string directory) + { + try + { + var currentFile = GetLoggingFile(filePath); + var json = File.ReadAllText(filePath) + .Replace("\"Path\": \"" + currentFile + "\"", "\"Path\": \"" + directory + "\""); + File.WriteAllText(filePath, json); + } + catch (Exception ex) + { + /* Swallow Exception */ + Console.WriteLine(ex); + } + } + + private static string GetDatabasePath(string filePath) + { + const string defaultFile = "config/kavita.db"; + + try + { + var json = File.ReadAllText(filePath); + var jsonObj = JsonSerializer.Deserialize(json); + + if (jsonObj.TryGetProperty("ConnectionStrings", out JsonElement tokenElement)) + { + foreach (var property in tokenElement.EnumerateObject()) + { + if (!property.Name.Equals("DefaultConnection")) continue; + return property.Value.GetString(); + } + } + } + catch (Exception ex) + { + Console.WriteLine("Error writing app settings: " + ex.Message); + } + + return defaultFile; + } + + /// + /// This should NEVER be called except by + /// + /// + /// + private static void SetDatabasePath(string filePath, string updatedPath) + { + try + { + var existingString = GetDatabasePath(filePath); + var json = File.ReadAllText(filePath) + .Replace(existingString, + "Data source=" + updatedPath); + File.WriteAllText(filePath, json); + } + catch (Exception) + { + /* Swallow Exception */ + } + } + } } diff --git a/Kavita.sln.DotSettings b/Kavita.sln.DotSettings index 322251617..add1b3a35 100644 --- a/Kavita.sln.DotSettings +++ b/Kavita.sln.DotSettings @@ -2,4 +2,5 @@ ExplicitlyExcluded True True - True \ No newline at end of file + True + True \ No newline at end of file diff --git a/README.md b/README.md index 0cccdd53b..0bf98f2c8 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ Password: Demouser64 - Place in a directory that is writable. If on windows, do not place in Program Files - Linux users must ensure the directory & kavita.db is writable by Kavita (might require starting server once) - Run Kavita -- If you are updating, do not copy appsettings.json from the new version over. It will override your TokenKey and you will have to reauthenticate on your devices. +- If you are updating, copy everything over into install location. All Kavita data is stored in config/, so nothing will be overwritten. - Open localhost:5000 and setup your account and libraries in the UI. ### Docker Running your Kavita server in docker is super easy! Barely an inconvenience. You can run it with this command: @@ -56,7 +56,7 @@ Running your Kavita server in docker is super easy! Barely an inconvenience. You ``` docker run --name kavita -p 5000:5000 \ -v /your/manga/directory:/manga \ --v /kavita/data/directory:/kavita/data \ +-v /kavita/data/directory:/kavita/config \ --restart unless-stopped \ -d kizaing/kavita:latest ``` @@ -64,19 +64,20 @@ docker run --name kavita -p 5000:5000 \ You can also run it via the docker-compose file: ``` -version: '3.9' +version: '3' services: kavita: image: kizaing/kavita:latest + container_name: kavita volumes: - ./manga:/manga - - ./data:/kavita/data + - ./config:/kavita/config ports: - "5000:5000" restart: unless-stopped ``` -**Note: Kavita is under heavy development and is being updated all the time, so the tag for current builds is `:nightly`. The `:latest` tag will be the latest stable release. There is also the `:alpine` tag if you want a smaller image, but it is only available for x64 systems.** +**Note: Kavita is under heavy development and is being updated all the time, so the tag for current builds is `:nightly`. The `:latest` tag will be the latest stable release.** ## Feature Requests Got a great idea? Throw it up on the FeatHub or vote on another idea. Please check the [Project Board](https://github.com/Kareadita/Kavita/projects) first for a list of planned features. diff --git a/build.sh b/build.sh index 2e76cfdf0..066b22dee 100755 --- a/build.sh +++ b/build.sh @@ -105,7 +105,7 @@ Package() fi echo "Copying appsettings.json" - cp appsettings.Development.json $lOutputFolder/appsettings.json + cp config/appsettings.Development.json $lOutputFolder/config/appsettings.json echo "Creating tar" cd ../$outputFolder/"$runtime"/ diff --git a/docker-compose.yml b/docker-compose.yml index 15623731a..fd6d9b8a6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,10 +1,11 @@ -version: '3.9' +version: '3' services: kavita: image: kizaing/kavita:latest + container_name: kavita volumes: - ./manga:/manga - - ./data:/kavita/data + - ./config:/kavita/config ports: - "5000:5000" restart: unless-stopped diff --git a/entrypoint.sh b/entrypoint.sh old mode 100755 new mode 100644 index c0e61fdae..53bed162f --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,107 +1,31 @@ -#!/bin/bash +#! /bin/bash -#Checks if a token has been set, and then generates a new token if not -if grep -q 'super secret unguessable key' /kavita/appsettings.json -then - export TOKEN_KEY="$(pwgen -s 16 1)" - sed -i "s/super secret unguessable key/${TOKEN_KEY}/g" /kavita/appsettings.json +if [ ! -f "/kavita/config/appsettings.json" ]; then + echo "Kavita configuration file does not exist, creating..." + echo '{ + "ConnectionStrings": { + "DefaultConnection": "Data source=config//kavita.db" + }, + "TokenKey": "super secret unguessable key", + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Information", + "Microsoft.Hosting.Lifetime": "Error", + "Hangfire": "Information", + "Microsoft.AspNetCore.Hosting.Internal.WebHost": "Information" + }, + "File": { + "Path": "config//logs/kavita.log", + "Append": "True", + "FileSizeLimitBytes": 26214400, + "MaxRollingFiles": 2 + } + }, + "Port": 5000 +}' >> /kavita/config/appsettings.json fi -#Checks if the appsettings.json already exists in bind mount -if test -f "/kavita/data/appsettings.json" -then - rm /kavita/appsettings.json - ln -s /kavita/data/appsettings.json /kavita/ -else - mv /kavita/appsettings.json /kavita/data/ || true - ln -s /kavita/data/appsettings.json /kavita/ -fi +chmod +x Kavita -#Checks if the data folders exist -if [ -d /kavita/data/temp ] -then - if [ -d /kavita/temp ] - then - unlink /kavita/temp - ln -s /kavita/data/temp /kavita/temp - else - ln -s /kavita/data/temp /kavita/temp - fi -else - mkdir /kavita/data/temp - ln -s /kavita/data/temp /kavita/temp -fi - -if [ -d /kavita/data/cache ] -then - if [ -d /kavita/cache ] - then - unlink /kavita/cache - ln -s /kavita/data/cache /kavita/cache - else - ln -s /kavita/data/cache /kavita/cache - fi -else - mkdir /kavita/data/cache - ln -s /kavita/data/cache /kavita/cache -fi - -if [ -d /kavita/data/logs ] -then - if [ -d /kavita/logs ] - then - unlink /kavita/logs - ln -s /kavita/data/logs /kavita/logs - else - ln -s /kavita/data/logs /kavita/logs - fi -else - mkdir /kavita/data/logs - ln -s /kavita/data/logs /kavita/logs -fi - -if [ -d /kavita/data/backups ] -then - if [ -d /kavita/backups ] - then - unlink /kavita/backups - ln -s /kavita/data/backups /kavita/backups - else - ln -s /kavita/data/backups /kavita/backups - fi -else - mkdir /kavita/data/backups - ln -s /kavita/data/backups /kavita/backups -fi - -if [ -d /kavita/data/stats ] -then - if [ -d /kavita/stats ] - then - unlink /kavita/stats - ln -s /kavita/data/stats /kavita/stats - else - ln -s /kavita/data/stats /kavita/stats - fi -else - mkdir /kavita/data/stats - ln -s /kavita/data/stats /kavita/stats -fi - -if [ -d /kavita/data/covers ] -then - if [ -d /kavita/covers ] - then - unlink /kavita/covers - ln -s /kavita/data/covers /kavita/covers - else - ln -s /kavita/data/covers /kavita/covers - fi -else - mkdir /kavita/data/covers - ln -s /kavita/data/covers /kavita/covers -fi - -chmod +x ./Kavita - -./Kavita +./Kavita \ No newline at end of file diff --git a/pull_request_template.md b/pull_request_template.md index 3e3166397..f9627fea5 100644 --- a/pull_request_template.md +++ b/pull_request_template.md @@ -1,5 +1,5 @@ # Added -- New features +- Added: New features # Changed - Changed: Changed how something existing works (Closes #bug number) @@ -9,6 +9,9 @@ # Checklist (delete section) +- Ensure your issues are not generic and instead talk about the feature or area they fix/enhance. + - DONT: Fixed: Fixed a styling issue on top of screen + - DO: Fixed: Fixed a styling issue on top of the book reader which caused content to be pushed down on smaller devices - Please delete any that are not relevant. - You MUST use Fixed:, Changed:, Added: in front of any bullet points. - Do not use double quotes, use ' instead.