mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
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.
This commit is contained in:
parent
30352403cf
commit
bbb4240e20
3
.gitignore
vendored
3
.gitignore
vendored
@ -450,4 +450,5 @@ appsettings.json
|
||||
/API/Hangfire-log.db
|
||||
cache/
|
||||
/API/wwwroot/
|
||||
/API/cache/
|
||||
/API/cache/
|
||||
/API/temp/
|
46
API.Tests/Services/BackupServiceTests.cs
Normal file
46
API.Tests/Services/BackupServiceTests.cs
Normal file
@ -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<IUnitOfWork>();
|
||||
private readonly ILogger<DirectoryService> _directoryLogger = Substitute.For<ILogger<DirectoryService>>();
|
||||
private readonly ILogger<BackupService> _logger = Substitute.For<ILogger<BackupService>>();
|
||||
private readonly IConfiguration _config;
|
||||
|
||||
// public BackupServiceTests()
|
||||
// {
|
||||
// var inMemorySettings = new Dictionary<string, string> {
|
||||
// {"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();
|
||||
// }
|
||||
|
||||
|
||||
}
|
||||
}
|
@ -48,7 +48,7 @@ namespace API.Tests.Services
|
||||
// var cacheService = Substitute.ForPartsOf<CacheService>();
|
||||
// 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);
|
||||
}
|
||||
|
@ -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"));
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -142,7 +142,7 @@ namespace API.Controllers
|
||||
public async Task<ActionResult> 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);
|
||||
|
@ -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<ServerController> _logger;
|
||||
private readonly IConfiguration _config;
|
||||
private readonly IDirectoryService _directoryService;
|
||||
|
||||
public ServerController(IHostApplicationLifetime applicationLifetime, ILogger<ServerController> logger)
|
||||
public ServerController(IHostApplicationLifetime applicationLifetime, ILogger<ServerController> 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<ActionResult> 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));
|
||||
}
|
||||
}
|
||||
}
|
@ -16,8 +16,8 @@ namespace API.Data
|
||||
{
|
||||
ChangeTracker.Tracked += OnEntityTracked;
|
||||
ChangeTracker.StateChanged += OnEntityStateChanged;
|
||||
|
||||
}
|
||||
|
||||
public DbSet<Library> Library { get; set; }
|
||||
public DbSet<Series> Series { get; set; }
|
||||
|
||||
@ -33,6 +33,7 @@ namespace API.Data
|
||||
protected override void OnModelCreating(ModelBuilder builder)
|
||||
{
|
||||
base.OnModelCreating(builder);
|
||||
|
||||
|
||||
builder.Entity<AppUser>()
|
||||
.HasMany(ur => ur.UserRoles)
|
||||
|
@ -32,7 +32,10 @@ namespace API.Extensions
|
||||
|
||||
services.AddDbContext<DataContext>(options =>
|
||||
{
|
||||
options.UseSqlite(config.GetConnectionString("DefaultConnection"));
|
||||
options.UseSqlite(config.GetConnectionString("DefaultConnection"), builder =>
|
||||
{
|
||||
//builder.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery);
|
||||
});
|
||||
});
|
||||
|
||||
services.AddLogging(loggingBuilder =>
|
||||
|
16
API/Extensions/ConfigurationExtensions.cs
Normal file
16
API/Extensions/ConfigurationExtensions.cs
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
/// <param name="path"></param>
|
||||
/// <param name="searchPatternExpression"></param>
|
||||
/// <returns></returns>
|
||||
string[] GetFiles(string path, string searchPatternExpression = "");
|
||||
string[] GetFilesWithExtension(string path, string searchPatternExpression = "");
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
@ -28,6 +29,21 @@ namespace API.Interfaces.Services
|
||||
/// <returns></returns>
|
||||
bool ExistOrCreate(string directoryPath);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes all files within the directory, then the directory itself.
|
||||
/// </summary>
|
||||
/// <param name="directoryPath"></param>
|
||||
void ClearAndDeleteDirectory(string directoryPath);
|
||||
/// <summary>
|
||||
/// Deletes all files within the directory.
|
||||
/// </summary>
|
||||
/// <param name="directoryPath"></param>
|
||||
/// <returns></returns>
|
||||
void ClearDirectory(string directoryPath);
|
||||
|
||||
bool CopyFilesToDirectory(IEnumerable<string> filePaths, string directoryPath);
|
||||
|
||||
IEnumerable<string> GetFiles(string path, string searchPatternExpression = "",
|
||||
SearchOption searchOption = SearchOption.TopDirectoryOnly);
|
||||
}
|
||||
}
|
@ -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<ArchiveService> _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<ArchiveService> 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;
|
||||
|
@ -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<string> _backupFiles = new List<string>()
|
||||
{
|
||||
"appsettings.json",
|
||||
"Hangfire.db",
|
||||
"Hangfire-log.db",
|
||||
"kavita.db",
|
||||
"kavita.db-shm",
|
||||
"kavita.db-wal",
|
||||
"kavita.log",
|
||||
};
|
||||
private readonly IList<string> _backupFiles;
|
||||
|
||||
public BackupService(IUnitOfWork unitOfWork, ILogger<BackupService> logger, IDirectoryService directoryService)
|
||||
public BackupService(IUnitOfWork unitOfWork, ILogger<BackupService> 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<string>()
|
||||
{
|
||||
"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");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -39,8 +39,23 @@ namespace API.Services
|
||||
.Where(file =>
|
||||
reSearchPattern.IsMatch(Path.GetExtension(file)));
|
||||
}
|
||||
|
||||
public IEnumerable<string> GetFiles(string path, string searchPatternExpression = "",
|
||||
SearchOption searchOption = SearchOption.TopDirectoryOnly)
|
||||
{
|
||||
if (searchPatternExpression != string.Empty)
|
||||
{
|
||||
if (!Directory.Exists(path)) return ImmutableList<string>.Empty;
|
||||
var reSearchPattern = new Regex(searchPatternExpression, RegexOptions.IgnoreCase);
|
||||
return Directory.EnumerateFiles(path, "*", searchOption)
|
||||
.Where(file =>
|
||||
reSearchPattern.IsMatch(file));
|
||||
}
|
||||
|
||||
return !Directory.Exists(path) ? Array.Empty<string>() : 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<string> 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<string> ListDirectory(string rootPath)
|
||||
|
@ -22,6 +22,7 @@ namespace API.Services
|
||||
|
||||
public TokenService(IConfiguration config, UserManager<AppUser> userManager)
|
||||
{
|
||||
|
||||
_userManager = userManager;
|
||||
_key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["TokenKey"]));
|
||||
}
|
||||
|
@ -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<ForwardedHeadersOptions>(options =>
|
||||
@ -72,7 +71,7 @@ namespace API
|
||||
|
||||
app.UseStaticFiles(new StaticFileOptions
|
||||
{
|
||||
ContentTypeProvider = new FileExtensionContentTypeProvider() // this is not set by default
|
||||
ContentTypeProvider = new FileExtensionContentTypeProvider()
|
||||
});
|
||||
|
||||
|
||||
|
12
README.md
12
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
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user