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 name: Bug report
about: Create a report to help us improve about: Create a report to help us improve
title: '' title: ''
labels: bug labels: needs-triage
assignees: '' 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. If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):** **Desktop (please complete the following information):**
- OS: [e.g. iOS] - OS: [e.g. iOS, Docker]
- Browser [e.g. chrome, safari] - Browser [e.g. chrome, safari]
- Version [e.g. 22] (can be found on Server Settings -> System tab) - 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/ UI/Web/dist/
/API.Tests/Extensions/Test Data/modified on run.txt /API.Tests/Extensions/Test Data/modified on run.txt
/API/covers/ /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/covers/
API/config/*.db API/config/*.db
UI/Web/.vscode/settings.json UI/Web/.vscode/settings.json

View File

@ -36,7 +36,7 @@ namespace API.Tests.Services
public void GetFiles_WithCustomRegex_ShouldPass_Test() public void GetFiles_WithCustomRegex_ShouldPass_Test()
{ {
var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/DirectoryService/regex"); 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()); Assert.Equal(2, files.Count());
} }
@ -44,7 +44,7 @@ namespace API.Tests.Services
public void GetFiles_TopLevel_ShouldBeEmpty_Test() public void GetFiles_TopLevel_ShouldBeEmpty_Test()
{ {
var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/DirectoryService"); var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/DirectoryService");
var files = _directoryService.GetFiles(testDirectory); var files = DirectoryService.GetFiles(testDirectory);
Assert.Empty(files); Assert.Empty(files);
} }
@ -52,7 +52,7 @@ namespace API.Tests.Services
public void GetFilesWithExtensions_ShouldBeEmpty_Test() public void GetFilesWithExtensions_ShouldBeEmpty_Test()
{ {
var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/DirectoryService/extensions"); 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); Assert.Empty(files);
} }
@ -60,7 +60,7 @@ namespace API.Tests.Services
public void GetFilesWithExtensions_Test() public void GetFilesWithExtensions_Test()
{ {
var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/DirectoryService/extension"); 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()); Assert.Equal(3, files.Count());
} }
@ -68,7 +68,7 @@ namespace API.Tests.Services
public void GetFilesWithExtensions_BadDirectory_ShouldBeEmpty_Test() public void GetFilesWithExtensions_BadDirectory_ShouldBeEmpty_Test()
{ {
var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/DirectoryService/doesntexist"); 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); Assert.Empty(files);
} }

View File

@ -164,7 +164,7 @@ namespace API.Controllers
case MangaFormat.Archive: case MangaFormat.Archive:
case MangaFormat.Pdf: case MangaFormat.Pdf:
_cacheService.ExtractChapterFiles(chapterExtractPath, mangaFiles.ToList()); _cacheService.ExtractChapterFiles(chapterExtractPath, mangaFiles.ToList());
var originalFiles = _directoryService.GetFilesWithExtension(chapterExtractPath, var originalFiles = DirectoryService.GetFilesWithExtension(chapterExtractPath,
Parser.Parser.ImageFileExtensions); Parser.Parser.ImageFileExtensions);
_directoryService.CopyFilesToDirectory(originalFiles, chapterExtractPath, $"{chapterId}_"); _directoryService.CopyFilesToDirectory(originalFiles, chapterExtractPath, $"{chapterId}_");
DirectoryService.DeleteFiles(originalFiles); 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."); 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 // Filter out images that aren't in bookmarks
Array.Sort(files, _numericComparer); Array.Sort(files, _numericComparer);
totalFilePaths.AddRange(files.Where((_, i) => chapterPages.Contains(i))); totalFilePaths.AddRange(files.Where((_, i) => chapterPages.Contains(i)));

View File

@ -739,7 +739,7 @@ namespace API.Controllers
[HttpGet("{apiKey}/favicon")] [HttpGet("{apiKey}/favicon")]
public async Task<ActionResult> GetFavicon(string apiKey) 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"); if (files.Length == 0) return BadRequest("Cannot find icon");
var path = files[0]; var path = files[0];
var content = await _directoryService.ReadFileAsync(path); 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>() 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.TaskScan, Value = "daily"},
new () {Key = ServerSettingKey.LoggingLevel, Value = "Information"}, // Not used from DB, but DB is sync with appSettings.json 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.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.Port, Value = "5000"}, // Not used from DB, but DB is sync with appSettings.json
new () {Key = ServerSettingKey.AllowStatCollection, Value = "true"}, new () {Key = ServerSettingKey.AllowStatCollection, Value = "true"},
new () {Key = ServerSettingKey.EnableOpds, Value = "false"}, new () {Key = ServerSettingKey.EnableOpds, Value = "false"},
@ -69,6 +69,8 @@ namespace API.Data
Configuration.Port + string.Empty; Configuration.Port + string.Empty;
context.ServerSetting.First(s => s.Key == ServerSettingKey.LoggingLevel).Value = context.ServerSetting.First(s => s.Key == ServerSettingKey.LoggingLevel).Value =
Configuration.LogLevel + string.Empty; Configuration.LogLevel + string.Empty;
context.ServerSetting.First(s => s.Key == ServerSettingKey.CacheDirectory).Value =
DirectoryService.CacheDirectory + string.Empty;
await context.SaveChangesAsync(); await context.SaveChangesAsync();

View File

@ -12,21 +12,9 @@ namespace API.Interfaces.Services
/// <param name="rootPath">Absolute path of directory to scan.</param> /// <param name="rootPath">Absolute path of directory to scan.</param>
/// <returns>List of folder names</returns> /// <returns>List of folder names</returns>
IEnumerable<string> ListDirectory(string rootPath); 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); Task<byte[]> ReadFileAsync(string path);
bool CopyFilesToDirectory(IEnumerable<string> filePaths, string directoryPath, string prepend = ""); bool CopyFilesToDirectory(IEnumerable<string> filePaths, string directoryPath, string prepend = "");
bool Exists(string directory); bool Exists(string directory);
IEnumerable<string> GetFiles(string path, string searchPatternExpression = "",
SearchOption searchOption = SearchOption.TopDirectoryOnly);
void CopyFileToDirectory(string fullFilePath, string targetDirectory); 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;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Threading.Tasks; using System.Threading.Tasks;
using API.Data; using API.Data;
using API.Entities; using API.Entities;
using API.Services; using API.Services;
using Kavita.Common; using Kavita.Common;
using Kavita.Common.EnvironmentInfo;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -27,6 +31,9 @@ namespace API
public static async Task Main(string[] args) public static async Task Main(string[] args)
{ {
Console.OutputEncoding = System.Text.Encoding.UTF8; 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 // Before anything, check if JWT has been generated properly or if user still has default
if (!Configuration.CheckIfJwtTokenSet() && if (!Configuration.CheckIfJwtTokenSet() &&
@ -48,6 +55,14 @@ namespace API
var context = services.GetRequiredService<DataContext>(); var context = services.GetRequiredService<DataContext>();
var roleManager = services.GetRequiredService<RoleManager<AppRole>>(); 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); var requiresCoverImageMigration = !Directory.Exists(DirectoryService.CoverImageDirectory);
try try
{ {
@ -57,7 +72,7 @@ namespace API
MigrateCoverImages.ExtractToImages(context); MigrateCoverImages.ExtractToImages(context);
} }
} }
catch (Exception ) catch (Exception)
{ {
requiresCoverImageMigration = false; requiresCoverImageMigration = false;
} }
@ -85,6 +100,16 @@ namespace API
private static IHostBuilder CreateHostBuilder(string[] args) => private static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(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 => .ConfigureWebHostDefaults(webBuilder =>
{ {
webBuilder.UseKestrel((opts) => webBuilder.UseKestrel((opts) =>
@ -94,5 +119,9 @@ namespace API
webBuilder.UseStartup<Startup>(); webBuilder.UseStartup<Startup>();
}); });
} }
} }

View File

@ -21,7 +21,6 @@ namespace API.Services
private readonly IDirectoryService _directoryService; private readonly IDirectoryService _directoryService;
private readonly IBookService _bookService; private readonly IBookService _bookService;
private readonly NumericComparer _numericComparer; 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, public CacheService(ILogger<CacheService> logger, IUnitOfWork unitOfWork, IArchiveService archiveService,
IDirectoryService directoryService, IBookService bookService) IDirectoryService directoryService, IBookService bookService)
@ -38,7 +37,7 @@ namespace API.Services
{ {
if (!DirectoryService.ExistOrCreate(DirectoryService.CacheDirectory)) 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 else
{ {
_directoryService.CopyDirectoryToDirectory(Path.GetDirectoryName(files[0].FilePath), extractPath, DirectoryService.CopyDirectoryToDirectory(Path.GetDirectoryName(files[0].FilePath), extractPath,
Parser.Parser.ImageFileExtensions); Parser.Parser.ImageFileExtensions);
} }
@ -147,7 +146,7 @@ namespace API.Services
try try
{ {
DirectoryService.ClearDirectory(CacheDirectory); DirectoryService.ClearDirectory(DirectoryService.CacheDirectory);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -198,7 +197,7 @@ namespace API.Services
if (page <= (mangaFile.Pages + pagesSoFar)) if (page <= (mangaFile.Pages + pagesSoFar))
{ {
var path = GetCachePath(chapter.Id); 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); Array.Sort(files, _numericComparer);
if (files.Length == 0) if (files.Length == 0)

View File

@ -16,11 +16,12 @@ namespace API.Services
private static readonly Regex ExcludeDirectories = new Regex( private static readonly Regex ExcludeDirectories = new Regex(
@"@eaDir|\.DS_Store", @"@eaDir|\.DS_Store",
RegexOptions.Compiled | RegexOptions.IgnoreCase); RegexOptions.Compiled | RegexOptions.IgnoreCase);
public static readonly string TempDirectory = Path.Join(Directory.GetCurrentDirectory(), "temp"); public static readonly string TempDirectory = Path.Join(Directory.GetCurrentDirectory(), "config", "temp");
public static readonly string LogDirectory = Path.Join(Directory.GetCurrentDirectory(), "logs"); public static readonly string LogDirectory = Path.Join(Directory.GetCurrentDirectory(), "config", "logs");
public static readonly string CacheDirectory = Path.Join(Directory.GetCurrentDirectory(), "cache"); public static readonly string CacheDirectory = Path.Join(Directory.GetCurrentDirectory(), "config", "cache");
public static readonly string CoverImageDirectory = Path.Join(Directory.GetCurrentDirectory(), "covers"); public static readonly string CoverImageDirectory = Path.Join(Directory.GetCurrentDirectory(), "config", "covers");
public static readonly string StatsDirectory = Path.Join(Directory.GetCurrentDirectory(), "stats"); 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) public DirectoryService(ILogger<DirectoryService> logger)
{ {
@ -47,7 +48,6 @@ namespace API.Services
reSearchPattern.IsMatch(Path.GetExtension(file)) && !Path.GetFileName(file).StartsWith(Parser.Parser.MacOsMetadataFileStartsWith)); reSearchPattern.IsMatch(Path.GetExtension(file)) && !Path.GetFileName(file).StartsWith(Parser.Parser.MacOsMetadataFileStartsWith));
} }
/// <summary> /// <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. /// 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; return di.Exists;
} }
public IEnumerable<string> GetFiles(string path, string searchPatternExpression = "", public static IEnumerable<string> GetFiles(string path, string searchPatternExpression = "",
SearchOption searchOption = SearchOption.TopDirectoryOnly) SearchOption searchOption = SearchOption.TopDirectoryOnly)
{ {
if (searchPatternExpression != string.Empty) if (searchPatternExpression != string.Empty)
@ -132,10 +132,10 @@ namespace API.Services
/// </summary> /// </summary>
/// <param name="sourceDirName"></param> /// <param name="sourceDirName"></param>
/// <param name="destDirName"></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> /// <returns></returns>
/// <exception cref="DirectoryNotFoundException"></exception> /// <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; 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) if (searchPatternExpression != string.Empty)
{ {

View File

@ -13,7 +13,6 @@ namespace API.Services
public class ImageService : IImageService public class ImageService : IImageService
{ {
private readonly ILogger<ImageService> _logger; private readonly ILogger<ImageService> _logger;
private readonly IDirectoryService _directoryService;
public const string ChapterCoverImageRegex = @"v\d+_c\d+"; public const string ChapterCoverImageRegex = @"v\d+_c\d+";
public const string SeriesCoverImageRegex = @"seres\d+"; public const string SeriesCoverImageRegex = @"seres\d+";
public const string CollectionTagCoverImageRegex = @"tag\d+"; public const string CollectionTagCoverImageRegex = @"tag\d+";
@ -24,10 +23,9 @@ namespace API.Services
/// </summary> /// </summary>
private const int ThumbnailWidth = 320; private const int ThumbnailWidth = 320;
public ImageService(ILogger<ImageService> logger, IDirectoryService directoryService) public ImageService(ILogger<ImageService> logger)
{ {
_logger = logger; _logger = logger;
_directoryService = directoryService;
} }
/// <summary> /// <summary>
@ -44,7 +42,7 @@ namespace API.Services
return null; return null;
} }
var firstImage = _directoryService.GetFilesWithExtension(directory, Parser.Parser.ImageFileExtensions) var firstImage = DirectoryService.GetFilesWithExtension(directory, Parser.Parser.ImageFileExtensions)
.OrderBy(f => f, new NaturalSortComparer()).FirstOrDefault(); .OrderBy(f => f, new NaturalSortComparer()).FirstOrDefault();
return firstImage; return firstImage;

View File

@ -138,8 +138,7 @@ namespace API.Services
public void CleanupTemp() public void CleanupTemp()
{ {
var tempDirectory = Path.Join(Directory.GetCurrentDirectory(), "temp"); BackgroundJob.Enqueue(() => DirectoryService.ClearDirectory(DirectoryService.TempDirectory));
BackgroundJob.Enqueue(() => DirectoryService.ClearDirectory(tempDirectory));
} }
public void RefreshSeriesMetadata(int libraryId, int seriesId, bool forceUpdate = true) 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 IUnitOfWork _unitOfWork;
private readonly ILogger<BackupService> _logger; private readonly ILogger<BackupService> _logger;
private readonly IDirectoryService _directoryService; private readonly IDirectoryService _directoryService;
private readonly string _tempDirectory = Path.Join(Directory.GetCurrentDirectory(), "temp"); private readonly string _tempDirectory = DirectoryService.TempDirectory;
private readonly string _logDirectory = Path.Join(Directory.GetCurrentDirectory(), "logs"); private readonly string _logDirectory = DirectoryService.LogDirectory;
private readonly IList<string> _backupFiles; private readonly IList<string> _backupFiles;
@ -72,7 +72,7 @@ namespace API.Services.Tasks
var fi = new FileInfo(logFileName); var fi = new FileInfo(logFileName);
var files = maxRollingFiles > 0 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"}; : new[] {"kavita.log"};
return files; 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. // 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); DirectoryService.ClearAndDeleteDirectory(outputTempDir);
} }
@ -164,7 +164,7 @@ namespace API.Services.Tasks
var backupDirectory = Task.Run(() => _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BackupDirectory)).Result.Value; var backupDirectory = Task.Run(() => _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BackupDirectory)).Result.Value;
if (!_directoryService.Exists(backupDirectory)) return; if (!_directoryService.Exists(backupDirectory)) return;
var deltaTime = DateTime.Today.Subtract(TimeSpan.FromDays(dayThreshold)); 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)) var expiredBackups = allBackups.Select(filename => new FileInfo(filename))
.Where(f => f.CreationTime > deltaTime) .Where(f => f.CreationTime > deltaTime)
.ToList(); .ToList();

View File

@ -16,16 +16,14 @@ namespace API.Services.Tasks
private readonly ILogger<CleanupService> _logger; private readonly ILogger<CleanupService> _logger;
private readonly IBackupService _backupService; private readonly IBackupService _backupService;
private readonly IUnitOfWork _unitOfWork; private readonly IUnitOfWork _unitOfWork;
private readonly IDirectoryService _directoryService;
public CleanupService(ICacheService cacheService, ILogger<CleanupService> logger, public CleanupService(ICacheService cacheService, ILogger<CleanupService> logger,
IBackupService backupService, IUnitOfWork unitOfWork, IDirectoryService directoryService) IBackupService backupService, IUnitOfWork unitOfWork)
{ {
_cacheService = cacheService; _cacheService = cacheService;
_logger = logger; _logger = logger;
_backupService = backupService; _backupService = backupService;
_unitOfWork = unitOfWork; _unitOfWork = unitOfWork;
_directoryService = directoryService;
} }
public void CleanupCacheDirectory() public void CleanupCacheDirectory()
@ -42,7 +40,7 @@ namespace API.Services.Tasks
{ {
_logger.LogInformation("Starting Cleanup"); _logger.LogInformation("Starting Cleanup");
_logger.LogInformation("Cleaning temp directory"); _logger.LogInformation("Cleaning temp directory");
var tempDirectory = Path.Join(Directory.GetCurrentDirectory(), "temp"); var tempDirectory = DirectoryService.TempDirectory;
DirectoryService.ClearDirectory(tempDirectory); DirectoryService.ClearDirectory(tempDirectory);
CleanupCacheDirectory(); CleanupCacheDirectory();
_logger.LogInformation("Cleaning old database backups"); _logger.LogInformation("Cleaning old database backups");
@ -57,7 +55,7 @@ namespace API.Services.Tasks
private async Task DeleteSeriesCoverImages() private async Task DeleteSeriesCoverImages()
{ {
var images = await _unitOfWork.SeriesRepository.GetAllCoverImagesAsync(); 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) foreach (var file in files)
{ {
if (images.Contains(Path.GetFileName(file))) continue; if (images.Contains(Path.GetFileName(file))) continue;
@ -69,7 +67,7 @@ namespace API.Services.Tasks
private async Task DeleteChapterCoverImages() private async Task DeleteChapterCoverImages()
{ {
var images = await _unitOfWork.ChapterRepository.GetAllCoverImagesAsync(); 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) foreach (var file in files)
{ {
if (images.Contains(Path.GetFileName(file))) continue; if (images.Contains(Path.GetFileName(file))) continue;
@ -81,7 +79,7 @@ namespace API.Services.Tasks
private async Task DeleteTagCoverImages() private async Task DeleteTagCoverImages()
{ {
var images = await _unitOfWork.CollectionTagRepository.GetAllCoverImagesAsync(); 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) foreach (var file in files)
{ {
if (images.Contains(Path.GetFileName(file))) continue; if (images.Contains(Path.GetFileName(file))) continue;

View File

@ -24,6 +24,7 @@ using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Models;
namespace API namespace API
@ -217,6 +218,15 @@ namespace API
applicationLifetime.ApplicationStopping.Register(OnShutdown); applicationLifetime.ApplicationStopping.Register(OnShutdown);
applicationLifetime.ApplicationStarted.Register(() => 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}"); Console.WriteLine($"Kavita - v{BuildInfo.Version}");
}); });
} }

View File

@ -1,6 +1,6 @@
{ {
"ConnectionStrings": { "ConnectionStrings": {
"DefaultConnection": "Data source=kavita.db" "DefaultConnection": "Data source=config//kavita.db"
}, },
"TokenKey": "super secret unguessable key", "TokenKey": "super secret unguessable key",
"Logging": { "Logging": {
@ -12,7 +12,7 @@
"Microsoft.AspNetCore.Hosting.Internal.WebHost": "Information" "Microsoft.AspNetCore.Hosting.Internal.WebHost": "Information"
}, },
"File": { "File": {
"Path": "logs/kavita.log", "Path": "config//logs/kavita.log",
"Append": "True", "Append": "True",
"FileSizeLimitBytes": 26214400, "FileSizeLimitBytes": 26214400,
"MaxRollingFiles": 2 "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 #Installs program dependencies
RUN apt-get update \ 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/* && 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 COPY entrypoint.sh /entrypoint.sh
EXPOSE 5000 EXPOSE 5000
WORKDIR /kavita WORKDIR /kavita
ENTRYPOINT ["/bin/bash"] ENTRYPOINT [ "/bin/bash" ]
CMD ["/entrypoint.sh"] 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. 2. (Linux only) Chmod and Chown so Kavita can write to the directory you placed in.
3. Run Kavita executable. 3. Run Kavita executable.
4. Open localhost:5000 and setup your account and libraries in the UI. 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 public static class Configuration
{ {
private static readonly string AppSettingsFilename = GetAppSettingFilename(); private static readonly string AppSettingsFilename = Path.Join("config", GetAppSettingFilename());
public static string Branch public static string Branch
{ {
get => GetBranch(GetAppSettingFilename()); get => GetBranch(GetAppSettingFilename());
@ -33,15 +34,28 @@ namespace Kavita.Common
set => SetLogLevel(GetAppSettingFilename(), value); 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() private static string GetAppSettingFilename()
{ {
if (!string.IsNullOrEmpty(AppSettingsFilename)) if (!string.IsNullOrEmpty(AppSettingsFilename))
{ {
return AppSettingsFilename; return AppSettingsFilename;
} }
var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
var isDevelopment = environment == Environments.Development; var isDevelopment = environment == Environments.Development;
return "appsettings" + (isDevelopment ? ".Development" : "") + ".json"; return "appsettings" + (isDevelopment ? ".Development" : string.Empty) + ".json";
} }
#region JWT Token #region JWT Token
@ -98,7 +112,6 @@ namespace Kavita.Common
return false; return false;
} }
#endregion #endregion
#region Port #region Port
@ -237,5 +250,105 @@ namespace Kavita.Common
/* Swallow 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<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: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/RunLongAnalysisInSwa/@EntryValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/Highlighting/RunValueAnalysisInNullableWarningsEnabledContext2/@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 - 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) - Linux users must ensure the directory & kavita.db is writable by Kavita (might require starting server once)
- Run Kavita - 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. - Open localhost:5000 and setup your account and libraries in the UI.
### Docker ### Docker
Running your Kavita server in docker is super easy! Barely an inconvenience. You can run it with this command: 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 \ docker run --name kavita -p 5000:5000 \
-v /your/manga/directory:/manga \ -v /your/manga/directory:/manga \
-v /kavita/data/directory:/kavita/data \ -v /kavita/data/directory:/kavita/config \
--restart unless-stopped \ --restart unless-stopped \
-d kizaing/kavita:latest -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: You can also run it via the docker-compose file:
``` ```
version: '3.9' version: '3'
services: services:
kavita: kavita:
image: kizaing/kavita:latest image: kizaing/kavita:latest
container_name: kavita
volumes: volumes:
- ./manga:/manga - ./manga:/manga
- ./data:/kavita/data - ./config:/kavita/config
ports: ports:
- "5000:5000" - "5000:5000"
restart: unless-stopped 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 ## 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. 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 fi
echo "Copying appsettings.json" echo "Copying appsettings.json"
cp appsettings.Development.json $lOutputFolder/appsettings.json cp config/appsettings.Development.json $lOutputFolder/config/appsettings.json
echo "Creating tar" echo "Creating tar"
cd ../$outputFolder/"$runtime"/ cd ../$outputFolder/"$runtime"/

View File

@ -1,10 +1,11 @@
version: '3.9' version: '3'
services: services:
kavita: kavita:
image: kizaing/kavita:latest image: kizaing/kavita:latest
container_name: kavita
volumes: volumes:
- ./manga:/manga - ./manga:/manga
- ./data:/kavita/data - ./config:/kavita/config
ports: ports:
- "5000:5000" - "5000:5000"
restart: unless-stopped 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 [ ! -f "/kavita/config/appsettings.json" ]; then
if grep -q 'super secret unguessable key' /kavita/appsettings.json echo "Kavita configuration file does not exist, creating..."
then echo '{
export TOKEN_KEY="$(pwgen -s 16 1)" "ConnectionStrings": {
sed -i "s/super secret unguessable key/${TOKEN_KEY}/g" /kavita/appsettings.json "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 fi
#Checks if the appsettings.json already exists in bind mount chmod +x Kavita
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
./Kavita ./Kavita

View File

@ -1,5 +1,5 @@
# Added # Added
- New features - Added: New features
# Changed # Changed
- Changed: Changed how something existing works (Closes #bug number) - Changed: Changed how something existing works (Closes #bug number)
@ -9,6 +9,9 @@
# Checklist (delete section) # 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. - Please delete any that are not relevant.
- You MUST use Fixed:, Changed:, Added: in front of any bullet points. - You MUST use Fixed:, Changed:, Added: in front of any bullet points.
- Do not use double quotes, use ' instead. - Do not use double quotes, use ' instead.