mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
commit
55054d1910
3
.gitignore
vendored
3
.gitignore
vendored
@ -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/
|
47
API.Tests/Services/BackupServiceTests.cs
Normal file
47
API.Tests/Services/BackupServiceTests.cs
Normal 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();
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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"));
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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)
|
||||||
|
@ -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 =>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
7
API/Interfaces/Services/ICleanupService.cs
Normal file
7
API/Interfaces/Services/ICleanupService.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
namespace API.Interfaces.Services
|
||||||
|
{
|
||||||
|
public interface ICleanupService
|
||||||
|
{
|
||||||
|
void Cleanup();
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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;
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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());
|
||||||
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
33
API/Services/Tasks/CleanupService.cs
Normal file
33
API/Services/Tasks/CleanupService.cs
Normal 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();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
{
|
{
|
@ -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"]));
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
12
README.md
12
README.md
@ -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
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user