mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
Added a basic cache service to handle interations with the underlying cache implementation. Refactored some code to be more robust.
This commit is contained in:
parent
59a4921ba9
commit
cd8a1d2892
@ -11,16 +11,19 @@ namespace API.Controllers
|
|||||||
{
|
{
|
||||||
private readonly ISeriesRepository _seriesRepository;
|
private readonly ISeriesRepository _seriesRepository;
|
||||||
private readonly IDirectoryService _directoryService;
|
private readonly IDirectoryService _directoryService;
|
||||||
|
private readonly ICacheService _cacheService;
|
||||||
|
|
||||||
public ReaderController(ISeriesRepository seriesRepository, IDirectoryService directoryService)
|
public ReaderController(ISeriesRepository seriesRepository, IDirectoryService directoryService, ICacheService cacheService)
|
||||||
{
|
{
|
||||||
_seriesRepository = seriesRepository;
|
_seriesRepository = seriesRepository;
|
||||||
_directoryService = directoryService;
|
_directoryService = directoryService;
|
||||||
|
_cacheService = cacheService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("info")]
|
[HttpGet("info")]
|
||||||
public async Task<ActionResult<int>> GetInformation(int volumeId)
|
public async Task<ActionResult<int>> GetInformation(int volumeId)
|
||||||
{
|
{
|
||||||
|
// TODO: This will be refactored out. No longer needed.
|
||||||
Volume volume = await _seriesRepository.GetVolumeAsync(volumeId);
|
Volume volume = await _seriesRepository.GetVolumeAsync(volumeId);
|
||||||
|
|
||||||
// Assume we always get first Manga File
|
// Assume we always get first Manga File
|
||||||
@ -28,24 +31,21 @@ namespace API.Controllers
|
|||||||
{
|
{
|
||||||
return BadRequest("There are no files in the volume to read.");
|
return BadRequest("There are no files in the volume to read.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_cacheService.Ensure(volumeId);
|
||||||
|
|
||||||
var filepath = volume.Files.ElementAt(0).FilePath;
|
return Ok(volume.Files.Select(x => x.NumberOfPages).Sum());
|
||||||
|
|
||||||
var extractPath = _directoryService.ExtractArchive(filepath, volumeId);
|
|
||||||
if (string.IsNullOrEmpty(extractPath))
|
|
||||||
{
|
|
||||||
return BadRequest("There file is no longer there or has no images. Please rescan.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOTE: I'm starting to think this should actually cache the information about Volume/Manga file in the DB.
|
|
||||||
// It will be updated each time this is called which is on open of a manga.
|
|
||||||
return Ok(_directoryService.ListFiles(extractPath).Count());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("image")]
|
[HttpGet("image")]
|
||||||
public async Task<ActionResult<ImageDto>> GetImage(int volumeId, int page)
|
public async Task<ActionResult<ImageDto>> GetImage(int volumeId, int page)
|
||||||
{
|
{
|
||||||
// Temp let's iterate the directory each call to get next image
|
// Temp let's iterate the directory each call to get next image
|
||||||
|
_cacheService.Ensure(volumeId);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
var files = _directoryService.ListFiles(_directoryService.GetExtractPath(volumeId));
|
var files = _directoryService.ListFiles(_directoryService.GetExtractPath(volumeId));
|
||||||
var path = files.ElementAt(page);
|
var path = files.ElementAt(page);
|
||||||
var file = await _directoryService.ReadImageAsync(path);
|
var file = await _directoryService.ReadImageAsync(path);
|
||||||
|
@ -19,6 +19,7 @@ namespace API.Extensions
|
|||||||
services.AddScoped<ITaskScheduler, TaskScheduler>();
|
services.AddScoped<ITaskScheduler, TaskScheduler>();
|
||||||
services.AddScoped<IUserRepository, UserRepository>();
|
services.AddScoped<IUserRepository, UserRepository>();
|
||||||
services.AddScoped<ITokenService, TokenService>();
|
services.AddScoped<ITokenService, TokenService>();
|
||||||
|
services.AddScoped<ICacheService, CacheService>();
|
||||||
services.AddScoped<ISeriesRepository, SeriesRepository>();
|
services.AddScoped<ISeriesRepository, SeriesRepository>();
|
||||||
services.AddScoped<IDirectoryService, DirectoryService>();
|
services.AddScoped<IDirectoryService, DirectoryService>();
|
||||||
services.AddScoped<ILibraryRepository, LibraryRepository>();
|
services.AddScoped<ILibraryRepository, LibraryRepository>();
|
||||||
|
19
API/Extensions/ZipArchiveExtensions.cs
Normal file
19
API/Extensions/ZipArchiveExtensions.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.IO.Compression;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace API.Extensions
|
||||||
|
{
|
||||||
|
public static class ZipArchiveExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if archive has one or more files. Excludes directory entries.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="archive"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static bool HasFiles(this ZipArchive archive)
|
||||||
|
{
|
||||||
|
return archive.Entries.Any(x => Path.HasExtension(x.FullName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using API.Extensions;
|
||||||
using NetVips;
|
using NetVips;
|
||||||
|
|
||||||
namespace API.IO
|
namespace API.IO
|
||||||
@ -21,7 +22,7 @@ namespace API.IO
|
|||||||
if (!File.Exists(filepath) || !Parser.Parser.IsArchive(filepath)) return Array.Empty<byte>();
|
if (!File.Exists(filepath) || !Parser.Parser.IsArchive(filepath)) return Array.Empty<byte>();
|
||||||
|
|
||||||
using ZipArchive archive = ZipFile.OpenRead(filepath);
|
using ZipArchive archive = ZipFile.OpenRead(filepath);
|
||||||
if (archive.Entries.Count <= 0) return Array.Empty<byte>();
|
if (!archive.HasFiles()) return Array.Empty<byte>();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -48,7 +49,7 @@ namespace API.IO
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ExtractEntryToImage(entry);
|
return ExtractEntryToImage(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte[] ExtractEntryToImage(ZipArchiveEntry entry)
|
private static byte[] ExtractEntryToImage(ZipArchiveEntry entry)
|
||||||
|
21
API/Interfaces/ICacheService.cs
Normal file
21
API/Interfaces/ICacheService.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
using API.Entities;
|
||||||
|
|
||||||
|
namespace API.Interfaces
|
||||||
|
{
|
||||||
|
public interface ICacheService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Ensures the cache is created for the given volume and if not, will create it.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="volumeId"></param>
|
||||||
|
void Ensure(int volumeId);
|
||||||
|
|
||||||
|
bool Cleanup(Volume volume);
|
||||||
|
|
||||||
|
//bool CleanupAll();
|
||||||
|
|
||||||
|
string GetCachePath(int volumeId);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.IO.Compression;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
|
|
||||||
@ -16,7 +18,7 @@ namespace API.Interfaces
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Lists out top-level files for a given directory.
|
/// Lists out top-level files for a given directory.
|
||||||
/// TODO: Implement ability to provide a filter for file types
|
/// TODO: Implement ability to provide a filter for file types (done in another implementation on DirectoryService)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="rootPath">Absolute path </param>
|
/// <param name="rootPath">Absolute path </param>
|
||||||
/// <returns>List of folder names</returns>
|
/// <returns>List of folder names</returns>
|
||||||
@ -34,6 +36,7 @@ namespace API.Interfaces
|
|||||||
/// Extracts an archive to a temp cache directory. Returns path to new directory. If temp cache directory already exists,
|
/// Extracts an archive to a temp cache directory. Returns path to new directory. If temp cache directory already exists,
|
||||||
/// will return that without performing an extraction. Returns empty string if there are any invalidations which would
|
/// will return that without performing an extraction. Returns empty string if there are any invalidations which would
|
||||||
/// prevent operations to perform correctly (missing archivePath file, empty archive, etc).
|
/// prevent operations to perform correctly (missing archivePath file, empty archive, etc).
|
||||||
|
/// Deprecated.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="archivePath">A valid file to an archive file.</param>
|
/// <param name="archivePath">A valid file to an archive file.</param>
|
||||||
/// <param name="volumeId">Id of volume being extracted.</param>
|
/// <param name="volumeId">Id of volume being extracted.</param>
|
||||||
@ -42,11 +45,23 @@ namespace API.Interfaces
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the path a volume would be extracted to.
|
/// Returns the path a volume would be extracted to.
|
||||||
|
/// Deprecated.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="volumeId"></param>
|
/// <param name="volumeId"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
string GetExtractPath(int volumeId);
|
string GetExtractPath(int volumeId);
|
||||||
|
|
||||||
Task<ImageDto> ReadImageAsync(string imagePath);
|
Task<ImageDto> ReadImageAsync(string imagePath);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extracts an archive to a temp cache directory. Returns path to new directory. If temp cache directory already exists,
|
||||||
|
/// will return that without performing an extraction. Returns empty string if there are any invalidations which would
|
||||||
|
/// prevent operations to perform correctly (missing archivePath file, empty archive, etc).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="archivePath">A valid file to an archive file.</param>
|
||||||
|
/// <param name="extractPath">Path to extract to</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
string ExtractArchive(string archivePath, string extractPath);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
46
API/Services/CacheService.cs
Normal file
46
API/Services/CacheService.cs
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
using System.IO;
|
||||||
|
using API.Entities;
|
||||||
|
using API.Interfaces;
|
||||||
|
|
||||||
|
namespace API.Services
|
||||||
|
{
|
||||||
|
public class CacheService : ICacheService
|
||||||
|
{
|
||||||
|
private readonly IDirectoryService _directoryService;
|
||||||
|
private readonly ISeriesRepository _seriesRepository;
|
||||||
|
|
||||||
|
public CacheService(IDirectoryService directoryService, ISeriesRepository seriesRepository)
|
||||||
|
{
|
||||||
|
_directoryService = directoryService;
|
||||||
|
_seriesRepository = seriesRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void Ensure(int volumeId)
|
||||||
|
{
|
||||||
|
Volume volume = await _seriesRepository.GetVolumeAsync(volumeId);
|
||||||
|
foreach (var file in volume.Files)
|
||||||
|
{
|
||||||
|
var extractPath = GetCachePath(volumeId);
|
||||||
|
if (file.Chapter > 0)
|
||||||
|
{
|
||||||
|
extractPath = Path.Join(extractPath, file.Chapter + "");
|
||||||
|
}
|
||||||
|
|
||||||
|
_directoryService.ExtractArchive(file.FilePath, extractPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Cleanup(Volume volume)
|
||||||
|
{
|
||||||
|
throw new System.NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetCachePath(int volumeId)
|
||||||
|
{
|
||||||
|
// TODO: Make this an absolute path, no ..'s in it.
|
||||||
|
return Path.Join(Directory.GetCurrentDirectory(), $"../cache/{volumeId}/");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -11,6 +11,7 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
|
using API.Extensions;
|
||||||
using API.Interfaces;
|
using API.Interfaces;
|
||||||
using API.IO;
|
using API.IO;
|
||||||
using API.Parser;
|
using API.Parser;
|
||||||
@ -43,11 +44,11 @@ namespace API.Services
|
|||||||
/// <param name="searchPatternExpression">Regex version of search pattern (ie \.mp3|\.mp4)</param>
|
/// <param name="searchPatternExpression">Regex version of search pattern (ie \.mp3|\.mp4)</param>
|
||||||
/// <param name="searchOption">SearchOption to use, defaults to TopDirectoryOnly</param>
|
/// <param name="searchOption">SearchOption to use, defaults to TopDirectoryOnly</param>
|
||||||
/// <returns>List of file paths</returns>
|
/// <returns>List of file paths</returns>
|
||||||
public static IEnumerable<string> GetFiles(string path,
|
private static IEnumerable<string> GetFiles(string path,
|
||||||
string searchPatternExpression = "",
|
string searchPatternExpression = "",
|
||||||
SearchOption searchOption = SearchOption.TopDirectoryOnly)
|
SearchOption searchOption = SearchOption.TopDirectoryOnly)
|
||||||
{
|
{
|
||||||
Regex reSearchPattern = new Regex(searchPatternExpression, RegexOptions.IgnoreCase);
|
var reSearchPattern = new Regex(searchPatternExpression, RegexOptions.IgnoreCase);
|
||||||
return Directory.EnumerateFiles(path, "*", searchOption)
|
return Directory.EnumerateFiles(path, "*", searchOption)
|
||||||
.Where(file =>
|
.Where(file =>
|
||||||
reSearchPattern.IsMatch(Path.GetExtension(file)));
|
reSearchPattern.IsMatch(Path.GetExtension(file)));
|
||||||
@ -89,9 +90,8 @@ namespace API.Services
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ConcurrentBag<ParserInfo> tempBag;
|
|
||||||
ConcurrentBag<ParserInfo> newBag = new ConcurrentBag<ParserInfo>();
|
ConcurrentBag<ParserInfo> newBag = new ConcurrentBag<ParserInfo>();
|
||||||
if (_scannedSeries.TryGetValue(info.Series, out tempBag))
|
if (_scannedSeries.TryGetValue(info.Series, out var tempBag))
|
||||||
{
|
{
|
||||||
var existingInfos = tempBag.ToArray();
|
var existingInfos = tempBag.ToArray();
|
||||||
foreach (var existingInfo in existingInfos)
|
foreach (var existingInfo in existingInfos)
|
||||||
@ -118,7 +118,7 @@ namespace API.Services
|
|||||||
|
|
||||||
if (series == null)
|
if (series == null)
|
||||||
{
|
{
|
||||||
series = new Series()
|
series = new Series
|
||||||
{
|
{
|
||||||
Name = seriesName,
|
Name = seriesName,
|
||||||
OriginalName = seriesName,
|
OriginalName = seriesName,
|
||||||
@ -159,11 +159,10 @@ namespace API.Services
|
|||||||
{
|
{
|
||||||
ICollection<Volume> volumes = new List<Volume>();
|
ICollection<Volume> volumes = new List<Volume>();
|
||||||
IList<Volume> existingVolumes = _seriesRepository.GetVolumes(series.Id).ToList();
|
IList<Volume> existingVolumes = _seriesRepository.GetVolumes(series.Id).ToList();
|
||||||
Volume existingVolume = null;
|
|
||||||
|
|
||||||
foreach (var info in infos)
|
foreach (var info in infos)
|
||||||
{
|
{
|
||||||
existingVolume = existingVolumes.SingleOrDefault(v => v.Name == info.Volumes);
|
var existingVolume = existingVolumes.SingleOrDefault(v => v.Name == info.Volumes);
|
||||||
if (existingVolume != null)
|
if (existingVolume != null)
|
||||||
{
|
{
|
||||||
var existingFile = existingVolume.Files.SingleOrDefault(f => f.FilePath == info.FullFilePath);
|
var existingFile = existingVolume.Files.SingleOrDefault(f => f.FilePath == info.FullFilePath);
|
||||||
@ -177,14 +176,6 @@ namespace API.Services
|
|||||||
{
|
{
|
||||||
existingVolume.Files.Add(CreateMangaFile(info));
|
existingVolume.Files.Add(CreateMangaFile(info));
|
||||||
}
|
}
|
||||||
// existingVolume.Files = new List<MangaFile>()
|
|
||||||
// {
|
|
||||||
// new MangaFile()
|
|
||||||
// {
|
|
||||||
// FilePath = info.FullFilePath,
|
|
||||||
// Chapter = Int32.Parse(info.Chapters)
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
if (forceUpdate || existingVolume.CoverImage == null || existingVolumes.Count == 0)
|
if (forceUpdate || existingVolume.CoverImage == null || existingVolumes.Count == 0)
|
||||||
{
|
{
|
||||||
@ -301,13 +292,37 @@ namespace API.Services
|
|||||||
|
|
||||||
using ZipArchive archive = ZipFile.OpenRead(archivePath);
|
using ZipArchive archive = ZipFile.OpenRead(archivePath);
|
||||||
|
|
||||||
if (archive.Entries.Count <= 0) return "";
|
if (!archive.HasFiles()) return "";
|
||||||
|
|
||||||
archive.ExtractToDirectory(extractPath);
|
archive.ExtractToDirectory(extractPath);
|
||||||
_logger.LogInformation($"Extracting archive to {extractPath}");
|
_logger.LogInformation($"Extracting archive to {extractPath}");
|
||||||
|
|
||||||
return extractPath;
|
return extractPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string ExtractArchive(string archivePath, string extractPath)
|
||||||
|
{
|
||||||
|
if (!File.Exists(archivePath) || !Parser.Parser.IsArchive(archivePath))
|
||||||
|
{
|
||||||
|
_logger.LogError($"Archive {archivePath} could not be found.");
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Directory.Exists(extractPath))
|
||||||
|
{
|
||||||
|
_logger.LogDebug($"Archive {archivePath} has already been extracted. Returning existing folder.");
|
||||||
|
return extractPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
using ZipArchive archive = ZipFile.OpenRead(archivePath);
|
||||||
|
|
||||||
|
if (!archive.HasFiles()) return "";
|
||||||
|
|
||||||
|
archive.ExtractToDirectory(extractPath);
|
||||||
|
_logger.LogDebug($"Extracting archive to {extractPath}");
|
||||||
|
|
||||||
|
return extractPath;
|
||||||
|
}
|
||||||
|
|
||||||
private int GetNumberOfPagesFromArchive(string archivePath)
|
private int GetNumberOfPagesFromArchive(string archivePath)
|
||||||
{
|
{
|
||||||
@ -320,6 +335,7 @@ namespace API.Services
|
|||||||
using ZipArchive archive = ZipFile.OpenRead(archivePath);
|
using ZipArchive archive = ZipFile.OpenRead(archivePath);
|
||||||
return archive.Entries.Count(e => Parser.Parser.IsImage(e.FullName));
|
return archive.Entries.Count(e => Parser.Parser.IsImage(e.FullName));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task<ImageDto> ReadImageAsync(string imagePath)
|
public async Task<ImageDto> ReadImageAsync(string imagePath)
|
||||||
{
|
{
|
||||||
@ -347,7 +363,6 @@ namespace API.Services
|
|||||||
{
|
{
|
||||||
//Count of files traversed and timer for diagnostic output
|
//Count of files traversed and timer for diagnostic output
|
||||||
int fileCount = 0;
|
int fileCount = 0;
|
||||||
//var sw = Stopwatch.StartNew();
|
|
||||||
|
|
||||||
// Determine whether to parallelize file processing on each folder based on processor count.
|
// Determine whether to parallelize file processing on each folder based on processor count.
|
||||||
int procCount = Environment.ProcessorCount;
|
int procCount = Environment.ProcessorCount;
|
||||||
@ -434,9 +449,6 @@ namespace API.Services
|
|||||||
foreach (string str in subDirs)
|
foreach (string str in subDirs)
|
||||||
dirs.Push(str);
|
dirs.Push(str);
|
||||||
}
|
}
|
||||||
|
|
||||||
// For diagnostic purposes.
|
|
||||||
//Console.WriteLine("Processed {0} files in {1} milliseconds", fileCount, sw.ElapsedMilliseconds);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user