Merge pull request #71 from Kareadita/feature/download

Download Logs
This commit is contained in:
Joseph Milazzo 2021-02-24 16:10:19 -06:00 committed by GitHub
commit 55054d1910
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 382 additions and 55 deletions

3
.gitignore vendored
View File

@ -450,4 +450,5 @@ appsettings.json
/API/Hangfire-log.db
cache/
/API/wwwroot/
/API/cache/
/API/cache/
/API/temp/

View File

@ -0,0 +1,47 @@
using API.Interfaces;
using API.Services;
using API.Services.Tasks;
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();
// }
}
}

View File

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

View File

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

View File

@ -4,6 +4,7 @@ using API.Entities;
using API.Interfaces;
using API.Interfaces.Services;
using API.Services;
using API.Services.Tasks;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Xunit;

View File

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

View File

@ -1,6 +1,13 @@
using API.Extensions;
using System;
using System.IO;
using System.IO.Compression;
using System.Threading.Tasks;
using API.Extensions;
using API.Interfaces;
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 +18,20 @@ namespace API.Controllers
{
private readonly IHostApplicationLifetime _applicationLifetime;
private readonly ILogger<ServerController> _logger;
private readonly IConfiguration _config;
private readonly IDirectoryService _directoryService;
private readonly IBackupService _backupService;
private readonly ITaskScheduler _taskScheduler;
public ServerController(IHostApplicationLifetime applicationLifetime, ILogger<ServerController> logger)
public ServerController(IHostApplicationLifetime applicationLifetime, ILogger<ServerController> logger, IConfiguration config,
IDirectoryService directoryService, IBackupService backupService, ITaskScheduler taskScheduler)
{
_applicationLifetime = applicationLifetime;
_logger = logger;
_config = config;
_directoryService = directoryService;
_backupService = backupService;
_taskScheduler = taskScheduler;
}
[HttpPost("restart")]
@ -26,5 +42,38 @@ namespace API.Controllers
_applicationLifetime.StopApplication();
return Ok();
}
[HttpGet("logs")]
public async Task<ActionResult> GetLogs()
{
var files = _backupService.LogFiles(_config.GetMaxRollingFiles(), _config.GetLoggingFileName());
var tempDirectory = Path.Join(Directory.GetCurrentDirectory(), "temp");
var dateString = DateTime.Now.ToShortDateString().Replace("/", "_");
var tempLocation = Path.Join(tempDirectory, "logs_" + dateString);
_directoryService.ExistOrCreate(tempLocation);
if (!_directoryService.CopyFilesToDirectory(files, tempLocation))
{
return BadRequest("Unable to copy files to temp directory for log download.");
}
var zipPath = Path.Join(tempDirectory, $"kavita_logs_{dateString}.zip");
try
{
ZipFile.CreateFromDirectory(tempLocation, zipPath);
}
catch (AggregateException ex)
{
_logger.LogError(ex, "There was an issue when archiving library backup");
return BadRequest("There was an issue when archiving library backup");
}
var fileBytes = await _directoryService.ReadFileAsync(zipPath);
_directoryService.ClearAndDeleteDirectory(tempLocation);
(new FileInfo(zipPath)).Delete();
return File(fileBytes, "application/zip", Path.GetFileName(zipPath));
}
}
}

View File

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

View File

@ -3,6 +3,7 @@ using API.Helpers;
using API.Interfaces;
using API.Interfaces.Services;
using API.Services;
using API.Services.Tasks;
using AutoMapper;
using Hangfire;
using Hangfire.LiteDB;
@ -27,12 +28,16 @@ namespace API.Extensions
services.AddScoped<IArchiveService, ArchiveService>();
services.AddScoped<IMetadataService, MetadataService>();
services.AddScoped<IBackupService, BackupService>();
services.AddScoped<ICleanupService, CleanupService>();
services.AddDbContext<DataContext>(options =>
{
options.UseSqlite(config.GetConnectionString("DefaultConnection"));
options.UseSqlite(config.GetConnectionString("DefaultConnection"), builder =>
{
//builder.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery);
});
});
services.AddLogging(loggingBuilder =>

View 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;
}
}
}

View File

@ -9,5 +9,6 @@
void ScanLibrary(int libraryId, bool forceUpdate = false);
void CleanupChapters(int[] chapterIds);
void RefreshMetadata(int libraryId, bool forceUpdate = true);
void CleanupTemp();
}
}

View File

@ -11,5 +11,6 @@ namespace API.Interfaces.Services
byte[] GetCoverImage(string filepath, bool createThumbnail = false);
bool IsValidArchive(string archivePath);
string GetSummaryInfo(string archivePath);
}
}

View File

@ -1,7 +1,17 @@
namespace API.Interfaces.Services
using System.Collections.Generic;
using Microsoft.Extensions.Configuration;
namespace API.Interfaces.Services
{
public interface IBackupService
{
void BackupDatabase();
/// <summary>
/// Returns a list of full paths of the logs files detailed in <see cref="IConfiguration"/>.
/// </summary>
/// <param name="maxRollingFiles"></param>
/// <param name="logFileName"></param>
/// <returns></returns>
IEnumerable<string> LogFiles(int maxRollingFiles, string logFileName);
}
}

View File

@ -0,0 +1,7 @@
namespace API.Interfaces.Services
{
public interface ICleanupService
{
void Cleanup();
}
}

View File

@ -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,23 @@ namespace API.Interfaces.Services
/// <returns></returns>
bool ExistOrCreate(string directoryPath);
Task<byte[]> ReadFileAsync(string path);
/// <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);
}
}

View File

@ -3,12 +3,10 @@ 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 API.Services.Tasks;
using Microsoft.Extensions.Logging;
using NetVips;
@ -20,7 +18,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 +92,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;

View File

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

View File

@ -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,16 @@ namespace API.Services
{
DirectoryInfo di = new DirectoryInfo(directoryPath);
ClearDirectory(directoryPath);
di.Delete(true);
}
public void ClearDirectory(string directoryPath)
{
var di = new DirectoryInfo(directoryPath);
if (!di.Exists) return;
foreach (var file in di.EnumerateFiles())
{
file.Delete();
@ -78,8 +103,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)
@ -105,7 +157,7 @@ namespace API.Services
return new ImageDto
{
Content = await File.ReadAllBytesAsync(imagePath),
Content = await ReadFileAsync(imagePath),
Filename = Path.GetFileNameWithoutExtension(imagePath),
FullPath = Path.GetFullPath(imagePath),
Width = image.Width,
@ -114,6 +166,12 @@ namespace API.Services
};
}
public async Task<byte[]> ReadFileAsync(string path)
{
if (!File.Exists(path)) return Array.Empty<byte>();
return await File.ReadAllBytesAsync(path);
}
/// <summary>
/// Recursively scans files and applies an action on them. This uses as many cores the underlying PC has to speed

View File

@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System.IO;
using System.Threading.Tasks;
using API.Entities.Enums;
using API.Helpers.Converters;
using API.Interfaces;
@ -16,6 +17,8 @@ namespace API.Services
private readonly IUnitOfWork _unitOfWork;
private readonly IMetadataService _metadataService;
private readonly IBackupService _backupService;
private readonly ICleanupService _cleanupService;
private readonly IDirectoryService _directoryService;
public BackgroundJobServer Client => new BackgroundJobServer();
// new BackgroundJobServerOptions()
@ -24,7 +27,8 @@ namespace API.Services
// }
public TaskScheduler(ICacheService cacheService, ILogger<TaskScheduler> logger, IScannerService scannerService,
IUnitOfWork unitOfWork, IMetadataService metadataService, IBackupService backupService)
IUnitOfWork unitOfWork, IMetadataService metadataService, IBackupService backupService, ICleanupService cleanupService,
IDirectoryService directoryService)
{
_cacheService = cacheService;
_logger = logger;
@ -32,6 +36,8 @@ namespace API.Services
_unitOfWork = unitOfWork;
_metadataService = metadataService;
_backupService = backupService;
_cleanupService = cleanupService;
_directoryService = directoryService;
ScheduleTasks();
@ -65,7 +71,7 @@ namespace API.Services
RecurringJob.AddOrUpdate(() => _backupService.BackupDatabase(), Cron.Weekly);
}
RecurringJob.AddOrUpdate(() => _cacheService.Cleanup(), Cron.Daily);
RecurringJob.AddOrUpdate(() => _cleanupService.Cleanup(), Cron.Daily);
}
public void ScanLibrary(int libraryId, bool forceUpdate = false)
@ -85,6 +91,12 @@ namespace API.Services
BackgroundJob.Enqueue((() => _metadataService.RefreshMetadata(libraryId, forceUpdate)));
}
public void CleanupTemp()
{
var tempDirectory = Path.Join(Directory.GetCurrentDirectory(), "temp");
BackgroundJob.Enqueue((() => _directoryService.ClearDirectory(tempDirectory)));
}
public void BackupDatabase()
{
BackgroundJob.Enqueue(() => _backupService.BackupDatabase());

View File

@ -5,11 +5,13 @@ 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
namespace API.Services.Tasks
{
public class BackupService : IBackupService
{
@ -18,22 +20,41 @@ 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 files = LogFiles(maxRollingFiles, loggingSection);
_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 IEnumerable<string> LogFiles(int maxRollingFiles, string logFileName)
{
var multipleFileRegex = maxRollingFiles > 0 ? @"\d*" : string.Empty;
var fi = new FileInfo(logFileName);
var files = maxRollingFiles > 0
? _directoryService.GetFiles(Directory.GetCurrentDirectory(), $@"{fi.Name}{multipleFileRegex}\.log")
: new string[] {"kavita.log"};
return files;
}
public void BackupDatabase()
@ -59,14 +80,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 +96,6 @@ namespace API.Services
_directoryService.ClearAndDeleteDirectory(tempDirectory);
_logger.LogInformation("Database backup completed");
}
}
}

View File

@ -0,0 +1,33 @@
using System.IO;
using API.Interfaces.Services;
using Microsoft.Extensions.Logging;
namespace API.Services.Tasks
{
/// <summary>
/// Cleans up after operations on reoccurring basis
/// </summary>
public class CleanupService : ICleanupService
{
private readonly ICacheService _cacheService;
private readonly IDirectoryService _directoryService;
private readonly ILogger<CleanupService> _logger;
public CleanupService(ICacheService cacheService, IDirectoryService directoryService, ILogger<CleanupService> logger)
{
_cacheService = cacheService;
_directoryService = directoryService;
_logger = logger;
}
public void Cleanup()
{
_logger.LogInformation("Cleaning temp directory");
var tempDirectory = Path.Join(Directory.GetCurrentDirectory(), "temp");
_directoryService.ClearDirectory(tempDirectory);
_logger.LogInformation("Cleaning cache directory");
_cacheService.Cleanup();
}
}
}

View File

@ -4,7 +4,6 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using API.Entities;
using API.Entities.Enums;
@ -14,8 +13,7 @@ using API.Parser;
using Hangfire;
using Microsoft.Extensions.Logging;
[assembly: InternalsVisibleTo("API.Tests")]
namespace API.Services
namespace API.Services.Tasks
{
public class ScannerService : IScannerService
{

View File

@ -22,6 +22,7 @@ namespace API.Services
public TokenService(IConfiguration config, UserManager<AppUser> userManager)
{
_userManager = userManager;
_key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["TokenKey"]));
}

View File

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

View File

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