From bbb4240e20fd0d915fedbe23fa2c6a0f3ce16d65 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Wed, 24 Feb 2021 11:59:16 -0600 Subject: [PATCH] Implemented download log files (not in service). Refactored backupservice to handle log file splitting. Improved a few interfaces and added some unit tests around them. --- .gitignore | 3 +- API.Tests/Services/BackupServiceTests.cs | 46 +++++++++++++++ API.Tests/Services/CacheServiceTests.cs | 2 +- API.Tests/Services/DirectoryServiceTests.cs | 57 +++++++++++++++++-- .../DirectoryService/extension/file.cbz | 0 .../DirectoryService/extension/file.rar | 0 .../DirectoryService/extension/file2.cbz | 0 .../Test Data/DirectoryService/regex/file.txt | 0 .../DirectoryService/regex/file2.txt | 0 API/Controllers/ReaderController.cs | 2 +- API/Controllers/ServerController.cs | 53 ++++++++++++++++- API/Data/DataContext.cs | 3 +- .../ApplicationServiceExtensions.cs | 5 +- API/Extensions/ConfigurationExtensions.cs | 16 ++++++ API/Interfaces/Services/IDirectoryService.cs | 18 +++++- API/Services/ArchiveService.cs | 7 +-- API/Services/BackupService.cs | 51 ++++++++++------- API/Services/CacheService.cs | 2 +- API/Services/DirectoryService.cs | 57 ++++++++++++++++++- API/Services/TokenService.cs | 1 + API/Startup.cs | 3 +- README.md | 12 +++- 22 files changed, 292 insertions(+), 46 deletions(-) create mode 100644 API.Tests/Services/BackupServiceTests.cs create mode 100644 API.Tests/Services/Test Data/DirectoryService/extension/file.cbz create mode 100644 API.Tests/Services/Test Data/DirectoryService/extension/file.rar create mode 100644 API.Tests/Services/Test Data/DirectoryService/extension/file2.cbz create mode 100644 API.Tests/Services/Test Data/DirectoryService/regex/file.txt create mode 100644 API.Tests/Services/Test Data/DirectoryService/regex/file2.txt create mode 100644 API/Extensions/ConfigurationExtensions.cs diff --git a/.gitignore b/.gitignore index 9999bab3e..f80c762de 100644 --- a/.gitignore +++ b/.gitignore @@ -450,4 +450,5 @@ appsettings.json /API/Hangfire-log.db cache/ /API/wwwroot/ -/API/cache/ \ No newline at end of file +/API/cache/ +/API/temp/ \ No newline at end of file diff --git a/API.Tests/Services/BackupServiceTests.cs b/API.Tests/Services/BackupServiceTests.cs new file mode 100644 index 000000000..5ed0700fb --- /dev/null +++ b/API.Tests/Services/BackupServiceTests.cs @@ -0,0 +1,46 @@ +using API.Interfaces; +using API.Services; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using NSubstitute; + +namespace API.Tests.Services +{ + public class BackupServiceTests + { + private readonly DirectoryService _directoryService; + private readonly BackupService _backupService; + private readonly IUnitOfWork _unitOfWork = Substitute.For(); + private readonly ILogger _directoryLogger = Substitute.For>(); + private readonly ILogger _logger = Substitute.For>(); + private readonly IConfiguration _config; + + // public BackupServiceTests() + // { + // var inMemorySettings = new Dictionary { + // {"Logging:File:MaxRollingFiles", "0"}, + // {"Logging:File:Path", "file.log"}, + // }; + // + // _config = new ConfigurationBuilder() + // .AddInMemoryCollection(inMemorySettings) + // .Build(); + // + // //_config.GetMaxRollingFiles().Returns(0); + // //_config.GetLoggingFileName().Returns("file.log"); + // //var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/BackupService/"); + // //Directory.GetCurrentDirectory().Returns(testDirectory); + // + // _directoryService = new DirectoryService(_directoryLogger); + // _backupService = new BackupService(_unitOfWork, _logger, _directoryService, _config); + // } + // + // [Fact] + // public void Test() + // { + // _backupService.BackupDatabase(); + // } + + + } +} \ No newline at end of file diff --git a/API.Tests/Services/CacheServiceTests.cs b/API.Tests/Services/CacheServiceTests.cs index 3e05fb585..87ed5a655 100644 --- a/API.Tests/Services/CacheServiceTests.cs +++ b/API.Tests/Services/CacheServiceTests.cs @@ -48,7 +48,7 @@ namespace API.Tests.Services // var cacheService = Substitute.ForPartsOf(); // cacheService.Configure().CacheDirectoryIsAccessible().Returns(true); // cacheService.Configure().GetVolumeCachePath(1, volume.Files.ElementAt(0)).Returns("cache/1/"); - // _directoryService.Configure().GetFiles("cache/1/").Returns(new string[] {"pexels-photo-6551949.jpg"}); + // _directoryService.Configure().GetFilesWithExtension("cache/1/").Returns(new string[] {"pexels-photo-6551949.jpg"}); // Assert.Equal(expected, _cacheService.GetCachedPagePath(volume, pageNum)); Assert.True(true); } diff --git a/API.Tests/Services/DirectoryServiceTests.cs b/API.Tests/Services/DirectoryServiceTests.cs index 0ae265008..39ff717c5 100644 --- a/API.Tests/Services/DirectoryServiceTests.cs +++ b/API.Tests/Services/DirectoryServiceTests.cs @@ -1,4 +1,6 @@ -using API.Services; +using System.IO; +using System.Linq; +using API.Services; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; @@ -17,15 +19,60 @@ namespace API.Tests.Services } [Fact] - public void GetFiles_Test() + public void GetFiles_WithCustomRegex_ShouldPass_Test() { - //_directoryService.GetFiles() + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/DirectoryService/regex"); + var files = _directoryService.GetFiles(testDirectory, @"file\d*.txt"); + Assert.Equal(2, files.Count()); + } + + [Fact] + public void GetFiles_TopLevel_ShouldBeEmpty_Test() + { + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/DirectoryService"); + var files = _directoryService.GetFiles(testDirectory); + Assert.Empty(files); + } + + [Fact] + public void GetFilesWithExtensions_ShouldBeEmpty_Test() + { + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/DirectoryService/extensions"); + var files = _directoryService.GetFiles(testDirectory, "*.txt"); + Assert.Empty(files); + } + + [Fact] + public void GetFilesWithExtensions_Test() + { + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/DirectoryService/extension"); + var files = _directoryService.GetFiles(testDirectory, ".cbz|.rar"); + Assert.Equal(3, files.Count()); + } + + [Fact] + public void GetFilesWithExtensions_BadDirectory_ShouldBeEmpty_Test() + { + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/DirectoryService/doesntexist"); + var files = _directoryService.GetFiles(testDirectory, ".cbz|.rar"); + Assert.Empty(files); } [Fact] - public void ListDirectory_Test() + public void ListDirectory_SubDirectory_Test() { - + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/DirectoryService/"); + var dirs = _directoryService.ListDirectory(testDirectory); + Assert.Contains(dirs, s => s.Contains("regex")); + + } + + [Fact] + public void ListDirectory_NoSubDirectory_Test() + { + var dirs = _directoryService.ListDirectory(""); + Assert.DoesNotContain(dirs, s => s.Contains("regex")); + } } } \ No newline at end of file diff --git a/API.Tests/Services/Test Data/DirectoryService/extension/file.cbz b/API.Tests/Services/Test Data/DirectoryService/extension/file.cbz new file mode 100644 index 000000000..e69de29bb diff --git a/API.Tests/Services/Test Data/DirectoryService/extension/file.rar b/API.Tests/Services/Test Data/DirectoryService/extension/file.rar new file mode 100644 index 000000000..e69de29bb diff --git a/API.Tests/Services/Test Data/DirectoryService/extension/file2.cbz b/API.Tests/Services/Test Data/DirectoryService/extension/file2.cbz new file mode 100644 index 000000000..e69de29bb diff --git a/API.Tests/Services/Test Data/DirectoryService/regex/file.txt b/API.Tests/Services/Test Data/DirectoryService/regex/file.txt new file mode 100644 index 000000000..e69de29bb diff --git a/API.Tests/Services/Test Data/DirectoryService/regex/file2.txt b/API.Tests/Services/Test Data/DirectoryService/regex/file2.txt new file mode 100644 index 000000000..e69de29bb diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index 08cde5314..067ea4f41 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -142,7 +142,7 @@ namespace API.Controllers public async Task Bookmark(BookmarkDto bookmarkDto) { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); - _logger.LogInformation("Saving {UserName} progress for Chapter {ChapterId} to page {PageNum}", user.UserName, bookmarkDto.ChapterId, bookmarkDto.PageNum); + _logger.LogDebug("Saving {UserName} progress for Chapter {ChapterId} to page {PageNum}", user.UserName, bookmarkDto.ChapterId, bookmarkDto.PageNum); // Don't let user bookmark past total pages. var chapter = await _unitOfWork.VolumeRepository.GetChapterAsync(bookmarkDto.ChapterId); diff --git a/API/Controllers/ServerController.cs b/API/Controllers/ServerController.cs index 10357d330..c0e9d5150 100644 --- a/API/Controllers/ServerController.cs +++ b/API/Controllers/ServerController.cs @@ -1,6 +1,11 @@ -using API.Extensions; +using System; +using System.IO; +using System.Threading.Tasks; +using API.Extensions; +using API.Interfaces.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -11,11 +16,16 @@ namespace API.Controllers { private readonly IHostApplicationLifetime _applicationLifetime; private readonly ILogger _logger; + private readonly IConfiguration _config; + private readonly IDirectoryService _directoryService; - public ServerController(IHostApplicationLifetime applicationLifetime, ILogger logger) + public ServerController(IHostApplicationLifetime applicationLifetime, ILogger logger, IConfiguration config, + IDirectoryService directoryService) { _applicationLifetime = applicationLifetime; _logger = logger; + _config = config; + _directoryService = directoryService; } [HttpPost("restart")] @@ -26,5 +36,44 @@ namespace API.Controllers _applicationLifetime.StopApplication(); return Ok(); } + + [HttpGet("logs")] + public async Task GetLogs() + { + // TODO: Zip up the log files + var maxRollingFiles = int.Parse(_config.GetSection("Logging").GetSection("File").GetSection("MaxRollingFiles").Value); + var loggingSection = _config.GetSection("Logging").GetSection("File").GetSection("Path").Value; + + var multipleFileRegex = maxRollingFiles > 0 ? @"\d*" : string.Empty; + FileInfo fi = new FileInfo(loggingSection); + + var files = _directoryService.GetFilesWithExtension(Directory.GetCurrentDirectory(), $@"{fi.Name}{multipleFileRegex}\.log"); + Console.WriteLine(files); + + var logFile = Path.Join(Directory.GetCurrentDirectory(), loggingSection); + _logger.LogInformation("Fetching download of logs: {LogFile}", logFile); + + // First, copy the file to temp + + var originalFile = new FileInfo(logFile); + var tempDirectory = Path.Join(Directory.GetCurrentDirectory(), "temp"); + _directoryService.ExistOrCreate(tempDirectory); + var tempLocation = Path.Join(tempDirectory, originalFile.Name); + originalFile.CopyTo(tempLocation); // TODO: Make this unique based on date + + // Read into memory + await using var memory = new MemoryStream(); + // We need to copy it else it will throw an exception + await using (var stream = new FileStream(tempLocation, FileMode.Open, FileAccess.Read)) + { + await stream.CopyToAsync(memory); + } + memory.Position = 0; + + // Delete temp + (new FileInfo(tempLocation)).Delete(); + + return File(memory, "text/plain", Path.GetFileName(logFile)); + } } } \ No newline at end of file diff --git a/API/Data/DataContext.cs b/API/Data/DataContext.cs index 24a2dae47..f6626d2a8 100644 --- a/API/Data/DataContext.cs +++ b/API/Data/DataContext.cs @@ -16,8 +16,8 @@ namespace API.Data { ChangeTracker.Tracked += OnEntityTracked; ChangeTracker.StateChanged += OnEntityStateChanged; - } + public DbSet Library { get; set; } public DbSet Series { get; set; } @@ -33,6 +33,7 @@ namespace API.Data protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); + builder.Entity() .HasMany(ur => ur.UserRoles) diff --git a/API/Extensions/ApplicationServiceExtensions.cs b/API/Extensions/ApplicationServiceExtensions.cs index a71083191..e26aa80c8 100644 --- a/API/Extensions/ApplicationServiceExtensions.cs +++ b/API/Extensions/ApplicationServiceExtensions.cs @@ -32,7 +32,10 @@ namespace API.Extensions services.AddDbContext(options => { - options.UseSqlite(config.GetConnectionString("DefaultConnection")); + options.UseSqlite(config.GetConnectionString("DefaultConnection"), builder => + { + //builder.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery); + }); }); services.AddLogging(loggingBuilder => diff --git a/API/Extensions/ConfigurationExtensions.cs b/API/Extensions/ConfigurationExtensions.cs new file mode 100644 index 000000000..2388fee21 --- /dev/null +++ b/API/Extensions/ConfigurationExtensions.cs @@ -0,0 +1,16 @@ +using Microsoft.Extensions.Configuration; + +namespace API.Extensions +{ + public static class ConfigurationExtensions + { + public static int GetMaxRollingFiles(this IConfiguration config) + { + return int.Parse(config.GetSection("Logging").GetSection("File").GetSection("MaxRollingFiles").Value); + } + public static string GetLoggingFileName(this IConfiguration config) + { + return config.GetSection("Logging").GetSection("File").GetSection("Path").Value; + } + } +} \ No newline at end of file diff --git a/API/Interfaces/Services/IDirectoryService.cs b/API/Interfaces/Services/IDirectoryService.cs index abd83dd36..e73b55960 100644 --- a/API/Interfaces/Services/IDirectoryService.cs +++ b/API/Interfaces/Services/IDirectoryService.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.IO; using System.Threading.Tasks; using API.DTOs; @@ -20,7 +21,7 @@ namespace API.Interfaces.Services /// /// /// - string[] GetFiles(string path, string searchPatternExpression = ""); + string[] GetFilesWithExtension(string path, string searchPatternExpression = ""); /// /// Returns true if the path exists and is a directory. If path does not exist, this will create it. Returns false in all fail cases. /// @@ -28,6 +29,21 @@ namespace API.Interfaces.Services /// bool ExistOrCreate(string directoryPath); + /// + /// Deletes all files within the directory, then the directory itself. + /// + /// void ClearAndDeleteDirectory(string directoryPath); + /// + /// Deletes all files within the directory. + /// + /// + /// + void ClearDirectory(string directoryPath); + + bool CopyFilesToDirectory(IEnumerable filePaths, string directoryPath); + + IEnumerable GetFiles(string path, string searchPatternExpression = "", + SearchOption searchOption = SearchOption.TopDirectoryOnly); } } \ No newline at end of file diff --git a/API/Services/ArchiveService.cs b/API/Services/ArchiveService.cs index f1c52a0d0..419fd1032 100644 --- a/API/Services/ArchiveService.cs +++ b/API/Services/ArchiveService.cs @@ -3,11 +3,8 @@ using System.Diagnostics; using System.IO; using System.IO.Compression; using System.Linq; -using System.Xml; -using System.Xml.Linq; using System.Xml.Serialization; using API.Extensions; -using API.Interfaces; using API.Interfaces.Services; using Microsoft.Extensions.Logging; using NetVips; @@ -20,7 +17,7 @@ namespace API.Services public class ArchiveService : IArchiveService { private readonly ILogger _logger; - private const int ThumbnailWidth = 320; + private const int ThumbnailWidth = 320; // 153w x 230h TODO: Look into optimizing the images to be smaller public ArchiveService(ILogger logger) { @@ -94,7 +91,7 @@ namespace API.Services { using var stream = entry.Open(); using var ms = new MemoryStream(); - stream.CopyTo(ms); + stream.CopyTo(ms); // TODO: Check if we can use CopyToAsync here var data = ms.ToArray(); return data; diff --git a/API/Services/BackupService.cs b/API/Services/BackupService.cs index 17b9cfb94..fae49bcdc 100644 --- a/API/Services/BackupService.cs +++ b/API/Services/BackupService.cs @@ -5,8 +5,10 @@ using System.IO.Compression; using System.Linq; using System.Threading.Tasks; using API.Entities.Enums; +using API.Extensions; using API.Interfaces; using API.Interfaces.Services; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; namespace API.Services @@ -18,22 +20,36 @@ namespace API.Services private readonly IDirectoryService _directoryService; private readonly string _tempDirectory = Path.Join(Directory.GetCurrentDirectory(), "temp"); - private readonly IList _backupFiles = new List() - { - "appsettings.json", - "Hangfire.db", - "Hangfire-log.db", - "kavita.db", - "kavita.db-shm", - "kavita.db-wal", - "kavita.log", - }; + private readonly IList _backupFiles; - public BackupService(IUnitOfWork unitOfWork, ILogger logger, IDirectoryService directoryService) + public BackupService(IUnitOfWork unitOfWork, ILogger logger, IDirectoryService directoryService, IConfiguration config) { _unitOfWork = unitOfWork; _logger = logger; _directoryService = directoryService; + var maxRollingFiles = config.GetMaxRollingFiles(); + var loggingSection = config.GetLoggingFileName(); + + var multipleFileRegex = maxRollingFiles > 0 ? @"\d*" : string.Empty; + var fi = new FileInfo(loggingSection); + + + var files = maxRollingFiles > 0 + ? _directoryService.GetFiles(Directory.GetCurrentDirectory(), $@"{fi.Name}{multipleFileRegex}\.log") + : new string[] {"kavita.log"}; + _backupFiles = new List() + { + "appsettings.json", + "Hangfire.db", + "Hangfire-log.db", + "kavita.db", + "kavita.db-shm", // This wont always be there + "kavita.db-wal", // This wont always be there + }; + foreach (var file in files.Select(f => (new FileInfo(f)).Name).ToList()) + { + _backupFiles.Add(file); + } } public void BackupDatabase() @@ -59,14 +75,10 @@ namespace API.Services var tempDirectory = Path.Join(_tempDirectory, dateString); _directoryService.ExistOrCreate(tempDirectory); - - - foreach (var file in _backupFiles) - { - var originalFile = new FileInfo(Path.Join(Directory.GetCurrentDirectory(), file)); - originalFile.CopyTo(Path.Join(tempDirectory, originalFile.Name)); - } - + _directoryService.ClearDirectory(tempDirectory); + + _directoryService.CopyFilesToDirectory( + _backupFiles.Select(file => Path.Join(Directory.GetCurrentDirectory(), file)).ToList(), tempDirectory); try { ZipFile.CreateFromDirectory(tempDirectory, zipPath); @@ -79,5 +91,6 @@ namespace API.Services _directoryService.ClearAndDeleteDirectory(tempDirectory); _logger.LogInformation("Database backup completed"); } + } } \ No newline at end of file diff --git a/API/Services/CacheService.cs b/API/Services/CacheService.cs index 7399efa34..4b4f457ee 100644 --- a/API/Services/CacheService.cs +++ b/API/Services/CacheService.cs @@ -111,7 +111,7 @@ namespace API.Services if (page <= (mangaFile.NumberOfPages + pagesSoFar)) { var path = GetCachePath(chapter.Id); - var files = _directoryService.GetFiles(path, Parser.Parser.ImageFileExtensions); + var files = _directoryService.GetFilesWithExtension(path, Parser.Parser.ImageFileExtensions); Array.Sort(files, _numericComparer); // Since array is 0 based, we need to keep that in account (only affects last image) diff --git a/API/Services/DirectoryService.cs b/API/Services/DirectoryService.cs index 9481302bd..e8467a928 100644 --- a/API/Services/DirectoryService.cs +++ b/API/Services/DirectoryService.cs @@ -39,8 +39,23 @@ namespace API.Services .Where(file => reSearchPattern.IsMatch(Path.GetExtension(file))); } + + public IEnumerable GetFiles(string path, string searchPatternExpression = "", + SearchOption searchOption = SearchOption.TopDirectoryOnly) + { + if (searchPatternExpression != string.Empty) + { + if (!Directory.Exists(path)) return ImmutableList.Empty; + var reSearchPattern = new Regex(searchPatternExpression, RegexOptions.IgnoreCase); + return Directory.EnumerateFiles(path, "*", searchOption) + .Where(file => + reSearchPattern.IsMatch(file)); + } + + return !Directory.Exists(path) ? Array.Empty() : Directory.GetFiles(path); + } - public string[] GetFiles(string path, string searchPatternExpression = "") + public string[] GetFilesWithExtension(string path, string searchPatternExpression = "") { if (searchPatternExpression != string.Empty) { @@ -70,6 +85,15 @@ namespace API.Services { DirectoryInfo di = new DirectoryInfo(directoryPath); + ClearDirectory(directoryPath); + + di.Delete(true); + } + + public void ClearDirectory(string directoryPath) + { + DirectoryInfo di = new DirectoryInfo(directoryPath); + foreach (var file in di.EnumerateFiles()) { file.Delete(); @@ -78,8 +102,35 @@ namespace API.Services { dir.Delete(true); } - - di.Delete(true); + } + + public bool CopyFilesToDirectory(IEnumerable filePaths, string directoryPath) + { + string currentFile = null; + try + { + foreach (var file in filePaths) + { + currentFile = file; + var fileInfo = new FileInfo(file); + if (fileInfo.Exists) + { + fileInfo.CopyTo(Path.Join(directoryPath, fileInfo.Name)); + } + else + { + _logger.LogWarning("Tried to copy {File} but it doesn't exist", file); + } + + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Unable to copy {File} to {DirectoryPath}", currentFile, directoryPath); + return false; + } + + return true; } public IEnumerable ListDirectory(string rootPath) diff --git a/API/Services/TokenService.cs b/API/Services/TokenService.cs index 670776bef..3b292cb8c 100644 --- a/API/Services/TokenService.cs +++ b/API/Services/TokenService.cs @@ -22,6 +22,7 @@ namespace API.Services public TokenService(IConfiguration config, UserManager userManager) { + _userManager = userManager; _key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["TokenKey"])); } diff --git a/API/Startup.cs b/API/Startup.cs index 0f2d5b80c..8b59f2f12 100644 --- a/API/Startup.cs +++ b/API/Startup.cs @@ -24,7 +24,6 @@ namespace API // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { - services.AddApplicationServices(_config); services.AddControllers(); services.Configure(options => @@ -72,7 +71,7 @@ namespace API app.UseStaticFiles(new StaticFileOptions { - ContentTypeProvider = new FileExtensionContentTypeProvider() // this is not set by default + ContentTypeProvider = new FileExtensionContentTypeProvider() }); diff --git a/README.md b/README.md index 20aca8a85..9a8dee63e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Kavita -Kavita, meaning Stories, is a lightweight manga server. The goal is to replace Ubooqity with an +Kavita, meaning Stories, is a lightweight manga server. The goal is to replace Ubooquity with an open source variant that is flexible and packs more punch, without sacrificing ease to use. +Think: ***Plex but for Manga.*** ## Goals: * Serve up Manga (cbr, cbz, zip/rar, raw images) and Books (epub, mobi, azw, djvu, pdf) @@ -9,10 +10,15 @@ open source variant that is flexible and packs more punch, without sacrificing e * Provide hooks into metadata providers to fetch Manga data * Metadata should allow for collections, want to read integration from 3rd party services, genres. * Ability to manage users, access, and ratings -* Expose an OPDS API/Stream for external readers to use -* Allow downloading files directly from WebApp ## How to Deploy * Build kavita-webui via ng build --prod. The dest should be placed in the API/wwwroot directory * Run publish command + +## How to install +1. Unzip the archive for your target OS +2. Place in a directory that is writable. If on windows, do not place in Program Files +3. Open appsettings.json and modify TokenKey to a random string ideally generated from [https://passwordsgenerator.net/](https://passwordsgenerator.net/) +4. Run API.exe +