mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
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:
parent
66b79e8cbe
commit
a29b11c366
4
.github/ISSUE_TEMPLATE/bug_report.md
vendored
4
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -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
12
.gitignore
vendored
@ -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
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)));
|
||||||
|
@ -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);
|
||||||
|
142
API/Data/MigrateConfigFiles.cs
Normal file
142
API/Data/MigrateConfigFiles.cs
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
|
||||||
|
@ -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 = "*");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
{
|
{
|
||||||
|
@ -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;
|
||||||
|
@ -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)
|
||||||
|
@ -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();
|
||||||
|
@ -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;
|
||||||
|
@ -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}");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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
|
1
API/config/stats/app_stats - Copy.json
Normal file
1
API/config/stats/app_stats - Copy.json
Normal 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}]}
|
1
API/config/stats/app_stats.json
Normal file
1
API/config/stats/app_stats.json
Normal 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}]}
|
@ -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"]
|
||||||
|
@ -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.
|
||||||
|
7
Kavita.Common/AppSettingsConfig.cs
Normal file
7
Kavita.Common/AppSettingsConfig.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
namespace Kavita.Common
|
||||||
|
{
|
||||||
|
public class AppSettingsConfig
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -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 */
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
11
README.md
11
README.md
@ -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.
|
||||||
|
2
build.sh
2
build.sh
@ -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"/
|
||||||
|
@ -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
128
entrypoint.sh
Executable file → Normal 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
|
@ -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.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user