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 <kizaing@gmail.com>
This commit is contained in:
Joseph Milazzo 2021-11-03 08:36:04 -05:00 committed by GitHub
parent 66b79e8cbe
commit a29b11c366
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 670 additions and 438 deletions

View File

@ -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)

12
.gitignore vendored
View File

@ -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

View File

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

View File

@ -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)));

View File

@ -739,7 +739,7 @@ namespace API.Controllers
[HttpGet("{apiKey}/favicon")]
public async Task<ActionResult> 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);

View File

@ -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<string> LooseLeafFiles = new List<string>()
{
"appsettings.json",
"appsettings.Development.json",
"kavita.db",
};
private static readonly List<string> AppFolders = new List<string>()
{
"covers",
"stats",
"logs",
"backups",
"cache",
"temp"
};
private static readonly string ConfigDirectory = Path.Join(Directory.GetCurrentDirectory(), "config");
/// <summary>
/// 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/
/// </summary>
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");
}
}
}

View File

@ -41,11 +41,11 @@ namespace API.Data
IList<ServerSetting> defaultSettings = new List<ServerSetting>()
{
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();

View File

@ -12,21 +12,9 @@ namespace API.Interfaces.Services
/// <param name="rootPath">Absolute path of directory to scan.</param>
/// <returns>List of folder names</returns>
IEnumerable<string> ListDirectory(string rootPath);
/// <summary>
/// Gets files in a directory. If searchPatternExpression is passed, will match the regex against for filtering.
/// </summary>
/// <param name="path"></param>
/// <param name="searchPatternExpression"></param>
/// <returns></returns>
string[] GetFilesWithExtension(string path, string searchPatternExpression = "");
Task<byte[]> ReadFileAsync(string path);
bool CopyFilesToDirectory(IEnumerable<string> filePaths, string directoryPath, string prepend = "");
bool Exists(string directory);
IEnumerable<string> GetFiles(string path, string searchPatternExpression = "",
SearchOption searchOption = SearchOption.TopDirectoryOnly);
void CopyFileToDirectory(string fullFilePath, string targetDirectory);
public bool CopyDirectoryToDirectory(string sourceDirName, string destDirName, string searchPattern = "*");
}
}

View File

@ -1,15 +1,19 @@
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;
@ -27,6 +31,9 @@ namespace API
public static async Task Main(string[] args)
{
Console.OutputEncoding = System.Text.Encoding.UTF8;
var isDocker = new OsInfo(Array.Empty<IOsVersionAdapter>()).IsDocker;
MigrateConfigFiles.Migrate(isDocker);
// Before anything, check if JWT has been generated properly or if user still has default
if (!Configuration.CheckIfJwtTokenSet() &&
@ -48,6 +55,14 @@ namespace API
var context = services.GetRequiredService<DataContext>();
var roleManager = services.GetRequiredService<RoleManager<AppRole>>();
if (isDocker && new FileInfo("data/appsettings.json").Exists)
{
var logger = services.GetRequiredService<ILogger<Startup>>();
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
{
@ -57,7 +72,7 @@ namespace API
MigrateCoverImages.ExtractToImages(context);
}
}
catch (Exception )
catch (Exception)
{
requiresCoverImageMigration = false;
}
@ -85,6 +100,16 @@ namespace API
private static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((hostingContext, config) =>
{
config.Sources.Clear();
var env = hostingContext.HostingEnvironment;
config.AddJsonFile("config/appsettings.json", optional: true, reloadOnChange: false)
.AddJsonFile($"config/appsettings.{env.EnvironmentName}.json",
optional: true, reloadOnChange: false);
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseKestrel((opts) =>
@ -94,5 +119,9 @@ namespace API
webBuilder.UseStartup<Startup>();
});
}
}

View File

@ -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<CacheService> 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)

View File

@ -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<DirectoryService> logger)
{
@ -47,7 +48,6 @@ namespace API.Services
reSearchPattern.IsMatch(Path.GetExtension(file)) && !Path.GetFileName(file).StartsWith(Parser.Parser.MacOsMetadataFileStartsWith));
}
/// <summary>
/// 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<string> GetFiles(string path, string searchPatternExpression = "",
public static IEnumerable<string> GetFiles(string path, string searchPatternExpression = "",
SearchOption searchOption = SearchOption.TopDirectoryOnly)
{
if (searchPatternExpression != string.Empty)
@ -132,10 +132,10 @@ namespace API.Services
/// </summary>
/// <param name="sourceDirName"></param>
/// <param name="destDirName"></param>
/// <param name="searchPattern">Defaults to *, meaning all files</param>
/// <param name="searchPattern">Defaults to empty string, meaning all files</param>
/// <returns></returns>
/// <exception cref="DirectoryNotFoundException"></exception>
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 = "")
/// <summary>
/// Get files with a file extension
/// </summary>
/// <param name="path"></param>
/// <param name="searchPatternExpression">Regex to use for searching on regex. Defaults to empty string for all files</param>
/// <returns></returns>
public static string[] GetFilesWithExtension(string path, string searchPatternExpression = "")
{
if (searchPatternExpression != string.Empty)
{

View File

@ -13,7 +13,6 @@ namespace API.Services
public class ImageService : IImageService
{
private readonly ILogger<ImageService> _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
/// </summary>
private const int ThumbnailWidth = 320;
public ImageService(ILogger<ImageService> logger, IDirectoryService directoryService)
public ImageService(ILogger<ImageService> logger)
{
_logger = logger;
_directoryService = directoryService;
}
/// <summary>
@ -44,7 +42,7 @@ 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;

View File

@ -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)

View File

@ -20,8 +20,8 @@ namespace API.Services.Tasks
private readonly IUnitOfWork _unitOfWork;
private readonly ILogger<BackupService> _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<string> _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();

View File

@ -16,16 +16,14 @@ namespace API.Services.Tasks
private readonly ILogger<CleanupService> _logger;
private readonly IBackupService _backupService;
private readonly IUnitOfWork _unitOfWork;
private readonly IDirectoryService _directoryService;
public CleanupService(ICacheService cacheService, ILogger<CleanupService> 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;

View File

@ -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<ILogger<Startup>>();
logger.LogInformation("Kavita - v{Version}", BuildInfo.Version);
}
catch (Exception)
{
/* Swallow Exception */
}
Console.WriteLine($"Kavita - v{BuildInfo.Version}");
});
}

View File

@ -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

View File

@ -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}]}

View File

@ -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}]}

View File

@ -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"]

View File

@ -3,3 +3,5 @@
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.
If updating, copy everything but the config/ directory over. Restart Kavita.

View File

@ -0,0 +1,7 @@
namespace Kavita.Common
{
public class AppSettingsConfig
{
}
}

View File

@ -8,7 +8,8 @@ namespace Kavita.Common
{
public static class Configuration
{
private static readonly string AppSettingsFilename = GetAppSettingFilename();
private static readonly string AppSettingsFilename = Path.Join("config", GetAppSettingFilename());
public static string Branch
{
get => GetBranch(GetAppSettingFilename());
@ -33,15 +34,28 @@ namespace Kavita.Common
set => SetLogLevel(GetAppSettingFilename(), value);
}
public static string LogPath
{
get => GetLoggingFile(GetAppSettingFilename());
set => SetLoggingFile(GetAppSettingFilename(), value);
}
public static string DatabasePath
{
get => GetDatabasePath(GetAppSettingFilename());
set => SetDatabasePath(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";
return "appsettings" + (isDevelopment ? ".Development" : string.Empty) + ".json";
}
#region JWT Token
@ -98,7 +112,6 @@ namespace Kavita.Common
return false;
}
#endregion
#region Port
@ -237,5 +250,105 @@ namespace Kavita.Common
/* 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<dynamic>(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;
}
/// <summary>
/// This should NEVER be called except by <see cref="MigrateConfigFiles"/>
/// </summary>
/// <param name="filePath"></param>
/// <param name="directory"></param>
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<dynamic>(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;
}
/// <summary>
/// This should NEVER be called except by <see cref="MigrateConfigFiles"/>
/// </summary>
/// <param name="filePath"></param>
/// <param name="updatedPath"></param>
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 */
}
}
}
}

View File

@ -2,4 +2,5 @@
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=1BC0273F_002DFEBE_002D4DA1_002DBC04_002D3A3167E4C86C_002Fd_003AData_002Fd_003AMigrations/@EntryIndexedValue">ExplicitlyExcluded</s:String>
<s:Boolean x:Key="/Default/CodeInspection/Highlighting/RunLongAnalysisInSwa/@EntryValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/Highlighting/RunValueAnalysisInNullableWarningsEnabledContext2/@EntryValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Opds/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Opds/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=rewinded/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@ -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.

View File

@ -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"/

View File

@ -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

128
entrypoint.sh Executable file → Normal file
View File

@ -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
#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
chmod +x Kavita
./Kavita

View File

@ -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.