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 /API/Hangfire-log.db
cache/ cache/
/API/wwwroot/ /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>(); // var cacheService = Substitute.ForPartsOf<CacheService>();
// cacheService.Configure().CacheDirectoryIsAccessible().Returns(true); // cacheService.Configure().CacheDirectoryIsAccessible().Returns(true);
// cacheService.Configure().GetVolumeCachePath(1, volume.Files.ElementAt(0)).Returns("cache/1/"); // 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.Equal(expected, _cacheService.GetCachedPagePath(volume, pageNum));
Assert.True(true); 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 Microsoft.Extensions.Logging;
using NSubstitute; using NSubstitute;
using Xunit; using Xunit;
@ -17,15 +19,60 @@ namespace API.Tests.Services
} }
[Fact] [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] [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;
using API.Interfaces.Services; using API.Interfaces.Services;
using API.Services; using API.Services;
using API.Services.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using NSubstitute; using NSubstitute;
using Xunit; using Xunit;

View File

@ -142,7 +142,7 @@ namespace API.Controllers
public async Task<ActionResult> Bookmark(BookmarkDto bookmarkDto) public async Task<ActionResult> Bookmark(BookmarkDto bookmarkDto)
{ {
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); 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. // Don't let user bookmark past total pages.
var chapter = await _unitOfWork.VolumeRepository.GetChapterAsync(bookmarkDto.ChapterId); 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.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -11,11 +18,20 @@ namespace API.Controllers
{ {
private readonly IHostApplicationLifetime _applicationLifetime; private readonly IHostApplicationLifetime _applicationLifetime;
private readonly ILogger<ServerController> _logger; 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; _applicationLifetime = applicationLifetime;
_logger = logger; _logger = logger;
_config = config;
_directoryService = directoryService;
_backupService = backupService;
_taskScheduler = taskScheduler;
} }
[HttpPost("restart")] [HttpPost("restart")]
@ -26,5 +42,38 @@ namespace API.Controllers
_applicationLifetime.StopApplication(); _applicationLifetime.StopApplication();
return Ok(); 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.Tracked += OnEntityTracked;
ChangeTracker.StateChanged += OnEntityStateChanged; ChangeTracker.StateChanged += OnEntityStateChanged;
} }
public DbSet<Library> Library { get; set; } public DbSet<Library> Library { get; set; }
public DbSet<Series> Series { get; set; } public DbSet<Series> Series { get; set; }
@ -33,6 +33,7 @@ namespace API.Data
protected override void OnModelCreating(ModelBuilder builder) protected override void OnModelCreating(ModelBuilder builder)
{ {
base.OnModelCreating(builder); base.OnModelCreating(builder);
builder.Entity<AppUser>() builder.Entity<AppUser>()
.HasMany(ur => ur.UserRoles) .HasMany(ur => ur.UserRoles)

View File

@ -3,6 +3,7 @@ using API.Helpers;
using API.Interfaces; using API.Interfaces;
using API.Interfaces.Services; using API.Interfaces.Services;
using API.Services; using API.Services;
using API.Services.Tasks;
using AutoMapper; using AutoMapper;
using Hangfire; using Hangfire;
using Hangfire.LiteDB; using Hangfire.LiteDB;
@ -27,12 +28,16 @@ namespace API.Extensions
services.AddScoped<IArchiveService, ArchiveService>(); services.AddScoped<IArchiveService, ArchiveService>();
services.AddScoped<IMetadataService, MetadataService>(); services.AddScoped<IMetadataService, MetadataService>();
services.AddScoped<IBackupService, BackupService>(); services.AddScoped<IBackupService, BackupService>();
services.AddScoped<ICleanupService, CleanupService>();
services.AddDbContext<DataContext>(options => services.AddDbContext<DataContext>(options =>
{ {
options.UseSqlite(config.GetConnectionString("DefaultConnection")); options.UseSqlite(config.GetConnectionString("DefaultConnection"), builder =>
{
//builder.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery);
});
}); });
services.AddLogging(loggingBuilder => 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 ScanLibrary(int libraryId, bool forceUpdate = false);
void CleanupChapters(int[] chapterIds); void CleanupChapters(int[] chapterIds);
void RefreshMetadata(int libraryId, bool forceUpdate = true); 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); byte[] GetCoverImage(string filepath, bool createThumbnail = false);
bool IsValidArchive(string archivePath); bool IsValidArchive(string archivePath);
string GetSummaryInfo(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 public interface IBackupService
{ {
void BackupDatabase(); 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.Collections.Generic;
using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using API.DTOs; using API.DTOs;
@ -20,7 +21,7 @@ namespace API.Interfaces.Services
/// <param name="path"></param> /// <param name="path"></param>
/// <param name="searchPatternExpression"></param> /// <param name="searchPatternExpression"></param>
/// <returns></returns> /// <returns></returns>
string[] GetFiles(string path, string searchPatternExpression = ""); string[] GetFilesWithExtension(string path, string searchPatternExpression = "");
/// <summary> /// <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. /// 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> /// </summary>
@ -28,6 +29,23 @@ namespace API.Interfaces.Services
/// <returns></returns> /// <returns></returns>
bool ExistOrCreate(string directoryPath); 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); 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;
using System.IO.Compression; using System.IO.Compression;
using System.Linq; using System.Linq;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Serialization; using System.Xml.Serialization;
using API.Extensions; using API.Extensions;
using API.Interfaces;
using API.Interfaces.Services; using API.Interfaces.Services;
using API.Services.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using NetVips; using NetVips;
@ -20,7 +18,7 @@ namespace API.Services
public class ArchiveService : IArchiveService public class ArchiveService : IArchiveService
{ {
private readonly ILogger<ArchiveService> _logger; 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) public ArchiveService(ILogger<ArchiveService> logger)
{ {
@ -94,7 +92,7 @@ namespace API.Services
{ {
using var stream = entry.Open(); using var stream = entry.Open();
using var ms = new MemoryStream(); using var ms = new MemoryStream();
stream.CopyTo(ms); stream.CopyTo(ms); // TODO: Check if we can use CopyToAsync here
var data = ms.ToArray(); var data = ms.ToArray();
return data; return data;

View File

@ -111,7 +111,7 @@ namespace API.Services
if (page <= (mangaFile.NumberOfPages + pagesSoFar)) if (page <= (mangaFile.NumberOfPages + pagesSoFar))
{ {
var path = GetCachePath(chapter.Id); 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); Array.Sort(files, _numericComparer);
// Since array is 0 based, we need to keep that in account (only affects last image) // 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 => .Where(file =>
reSearchPattern.IsMatch(Path.GetExtension(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) if (searchPatternExpression != string.Empty)
{ {
@ -70,6 +85,16 @@ namespace API.Services
{ {
DirectoryInfo di = new DirectoryInfo(directoryPath); 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()) foreach (var file in di.EnumerateFiles())
{ {
file.Delete(); file.Delete();
@ -78,8 +103,35 @@ namespace API.Services
{ {
dir.Delete(true); 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) public IEnumerable<string> ListDirectory(string rootPath)
@ -105,7 +157,7 @@ namespace API.Services
return new ImageDto return new ImageDto
{ {
Content = await File.ReadAllBytesAsync(imagePath), Content = await ReadFileAsync(imagePath),
Filename = Path.GetFileNameWithoutExtension(imagePath), Filename = Path.GetFileNameWithoutExtension(imagePath),
FullPath = Path.GetFullPath(imagePath), FullPath = Path.GetFullPath(imagePath),
Width = image.Width, 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> /// <summary>
/// Recursively scans files and applies an action on them. This uses as many cores the underlying PC has to speed /// 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.Entities.Enums;
using API.Helpers.Converters; using API.Helpers.Converters;
using API.Interfaces; using API.Interfaces;
@ -16,6 +17,8 @@ namespace API.Services
private readonly IUnitOfWork _unitOfWork; private readonly IUnitOfWork _unitOfWork;
private readonly IMetadataService _metadataService; private readonly IMetadataService _metadataService;
private readonly IBackupService _backupService; private readonly IBackupService _backupService;
private readonly ICleanupService _cleanupService;
private readonly IDirectoryService _directoryService;
public BackgroundJobServer Client => new BackgroundJobServer(); public BackgroundJobServer Client => new BackgroundJobServer();
// new BackgroundJobServerOptions() // new BackgroundJobServerOptions()
@ -24,7 +27,8 @@ namespace API.Services
// } // }
public TaskScheduler(ICacheService cacheService, ILogger<TaskScheduler> logger, IScannerService scannerService, 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; _cacheService = cacheService;
_logger = logger; _logger = logger;
@ -32,6 +36,8 @@ namespace API.Services
_unitOfWork = unitOfWork; _unitOfWork = unitOfWork;
_metadataService = metadataService; _metadataService = metadataService;
_backupService = backupService; _backupService = backupService;
_cleanupService = cleanupService;
_directoryService = directoryService;
ScheduleTasks(); ScheduleTasks();
@ -65,7 +71,7 @@ namespace API.Services
RecurringJob.AddOrUpdate(() => _backupService.BackupDatabase(), Cron.Weekly); 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) public void ScanLibrary(int libraryId, bool forceUpdate = false)
@ -85,6 +91,12 @@ namespace API.Services
BackgroundJob.Enqueue((() => _metadataService.RefreshMetadata(libraryId, forceUpdate))); BackgroundJob.Enqueue((() => _metadataService.RefreshMetadata(libraryId, forceUpdate)));
} }
public void CleanupTemp()
{
var tempDirectory = Path.Join(Directory.GetCurrentDirectory(), "temp");
BackgroundJob.Enqueue((() => _directoryService.ClearDirectory(tempDirectory)));
}
public void BackupDatabase() public void BackupDatabase()
{ {
BackgroundJob.Enqueue(() => _backupService.BackupDatabase()); BackgroundJob.Enqueue(() => _backupService.BackupDatabase());

View File

@ -5,11 +5,13 @@ using System.IO.Compression;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using API.Entities.Enums; using API.Entities.Enums;
using API.Extensions;
using API.Interfaces; using API.Interfaces;
using API.Interfaces.Services; using API.Interfaces.Services;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace API.Services namespace API.Services.Tasks
{ {
public class BackupService : IBackupService public class BackupService : IBackupService
{ {
@ -18,22 +20,41 @@ namespace API.Services
private readonly IDirectoryService _directoryService; private readonly IDirectoryService _directoryService;
private readonly string _tempDirectory = Path.Join(Directory.GetCurrentDirectory(), "temp"); private readonly string _tempDirectory = Path.Join(Directory.GetCurrentDirectory(), "temp");
private readonly IList<string> _backupFiles = new List<string>() private readonly IList<string> _backupFiles;
{
"appsettings.json",
"Hangfire.db",
"Hangfire-log.db",
"kavita.db",
"kavita.db-shm",
"kavita.db-wal",
"kavita.log",
};
public BackupService(IUnitOfWork unitOfWork, ILogger<BackupService> logger, IDirectoryService directoryService) public BackupService(IUnitOfWork unitOfWork, ILogger<BackupService> logger, IDirectoryService directoryService, IConfiguration config)
{ {
_unitOfWork = unitOfWork; _unitOfWork = unitOfWork;
_logger = logger; _logger = logger;
_directoryService = directoryService; _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() public void BackupDatabase()
@ -59,14 +80,10 @@ namespace API.Services
var tempDirectory = Path.Join(_tempDirectory, dateString); var tempDirectory = Path.Join(_tempDirectory, dateString);
_directoryService.ExistOrCreate(tempDirectory); _directoryService.ExistOrCreate(tempDirectory);
_directoryService.ClearDirectory(tempDirectory);
foreach (var file in _backupFiles) _directoryService.CopyFilesToDirectory(
{ _backupFiles.Select(file => Path.Join(Directory.GetCurrentDirectory(), file)).ToList(), tempDirectory);
var originalFile = new FileInfo(Path.Join(Directory.GetCurrentDirectory(), file));
originalFile.CopyTo(Path.Join(tempDirectory, originalFile.Name));
}
try try
{ {
ZipFile.CreateFromDirectory(tempDirectory, zipPath); ZipFile.CreateFromDirectory(tempDirectory, zipPath);
@ -79,5 +96,6 @@ namespace API.Services
_directoryService.ClearAndDeleteDirectory(tempDirectory); _directoryService.ClearAndDeleteDirectory(tempDirectory);
_logger.LogInformation("Database backup completed"); _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.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using API.Entities; using API.Entities;
using API.Entities.Enums; using API.Entities.Enums;
@ -14,8 +13,7 @@ using API.Parser;
using Hangfire; using Hangfire;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
[assembly: InternalsVisibleTo("API.Tests")] namespace API.Services.Tasks
namespace API.Services
{ {
public class ScannerService : IScannerService public class ScannerService : IScannerService
{ {

View File

@ -22,6 +22,7 @@ namespace API.Services
public TokenService(IConfiguration config, UserManager<AppUser> userManager) public TokenService(IConfiguration config, UserManager<AppUser> userManager)
{ {
_userManager = userManager; _userManager = userManager;
_key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["TokenKey"])); _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. // This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services) public void ConfigureServices(IServiceCollection services)
{ {
services.AddApplicationServices(_config); services.AddApplicationServices(_config);
services.AddControllers(); services.AddControllers();
services.Configure<ForwardedHeadersOptions>(options => services.Configure<ForwardedHeadersOptions>(options =>
@ -72,7 +71,7 @@ namespace API
app.UseStaticFiles(new StaticFileOptions app.UseStaticFiles(new StaticFileOptions
{ {
ContentTypeProvider = new FileExtensionContentTypeProvider() // this is not set by default ContentTypeProvider = new FileExtensionContentTypeProvider()
}); });

View File

@ -1,6 +1,7 @@
# Kavita # 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. open source variant that is flexible and packs more punch, without sacrificing ease to use.
Think: ***Plex but for Manga.***
## Goals: ## Goals:
* Serve up Manga (cbr, cbz, zip/rar, raw images) and Books (epub, mobi, azw, djvu, pdf) * 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 * Provide hooks into metadata providers to fetch Manga data
* Metadata should allow for collections, want to read integration from 3rd party services, genres. * Metadata should allow for collections, want to read integration from 3rd party services, genres.
* Ability to manage users, access, and ratings * 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 ## How to Deploy
* Build kavita-webui via ng build --prod. The dest should be placed in the API/wwwroot directory * Build kavita-webui via ng build --prod. The dest should be placed in the API/wwwroot directory
* Run publish command * 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