mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-31 14:33:50 -04:00
Download Support (#298)
* Implemented the ability to download files (series, volume, chapter) * Added RBS checks to ensure user is either an admin or has download role * Added the ability to change a users feature RBS. Changed the Role seed to use reflection
This commit is contained in:
parent
4ae9f078b0
commit
16a77fa8d6
@ -38,6 +38,10 @@ namespace API.Tests.Comparers
|
|||||||
new[] {"Batman - Black white vol 1 #04.cbr", "Batman - Black white vol 1 #03.cbr", "Batman - Black white vol 1 #01.cbr", "Batman - Black white vol 1 #02.cbr"},
|
new[] {"Batman - Black white vol 1 #04.cbr", "Batman - Black white vol 1 #03.cbr", "Batman - Black white vol 1 #01.cbr", "Batman - Black white vol 1 #02.cbr"},
|
||||||
new[] {"Batman - Black white vol 1 #01.cbr", "Batman - Black white vol 1 #02.cbr", "Batman - Black white vol 1 #03.cbr", "Batman - Black white vol 1 #04.cbr"}
|
new[] {"Batman - Black white vol 1 #01.cbr", "Batman - Black white vol 1 #02.cbr", "Batman - Black white vol 1 #03.cbr", "Batman - Black white vol 1 #04.cbr"}
|
||||||
)]
|
)]
|
||||||
|
[InlineData(
|
||||||
|
new[] {"3and4.cbz", "The World God Only Knows - Oneshot.cbz", "5.cbz", "1and2.cbz"},
|
||||||
|
new[] {"1and2.cbz", "3and4.cbz", "5.cbz", "The World God Only Knows - Oneshot.cbz"}
|
||||||
|
)]
|
||||||
public void TestNaturalSortComparer(string[] input, string[] expected)
|
public void TestNaturalSortComparer(string[] input, string[] expected)
|
||||||
{
|
{
|
||||||
Array.Sort(input, _nc);
|
Array.Sort(input, _nc);
|
||||||
|
@ -16,11 +16,12 @@ namespace API.Tests.Services
|
|||||||
private readonly ITestOutputHelper _testOutputHelper;
|
private readonly ITestOutputHelper _testOutputHelper;
|
||||||
private readonly ArchiveService _archiveService;
|
private readonly ArchiveService _archiveService;
|
||||||
private readonly ILogger<ArchiveService> _logger = Substitute.For<ILogger<ArchiveService>>();
|
private readonly ILogger<ArchiveService> _logger = Substitute.For<ILogger<ArchiveService>>();
|
||||||
|
private readonly ILogger<DirectoryService> _directoryServiceLogger = Substitute.For<ILogger<DirectoryService>>();
|
||||||
|
|
||||||
public ArchiveServiceTests(ITestOutputHelper testOutputHelper)
|
public ArchiveServiceTests(ITestOutputHelper testOutputHelper)
|
||||||
{
|
{
|
||||||
_testOutputHelper = testOutputHelper;
|
_testOutputHelper = testOutputHelper;
|
||||||
_archiveService = new ArchiveService(_logger);
|
_archiveService = new ArchiveService(_logger, new DirectoryService(_directoryServiceLogger));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
@ -154,7 +155,7 @@ namespace API.Tests.Services
|
|||||||
[InlineData("sorting.zip", "sorting.expected.jpg")]
|
[InlineData("sorting.zip", "sorting.expected.jpg")]
|
||||||
public void GetCoverImage_Default_Test(string inputFile, string expectedOutputFile)
|
public void GetCoverImage_Default_Test(string inputFile, string expectedOutputFile)
|
||||||
{
|
{
|
||||||
var archiveService = Substitute.For<ArchiveService>(_logger);
|
var archiveService = Substitute.For<ArchiveService>(_logger, new DirectoryService(_directoryServiceLogger));
|
||||||
var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/CoverImages");
|
var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/CoverImages");
|
||||||
var expectedBytes = File.ReadAllBytes(Path.Join(testDirectory, expectedOutputFile));
|
var expectedBytes = File.ReadAllBytes(Path.Join(testDirectory, expectedOutputFile));
|
||||||
archiveService.Configure().CanOpen(Path.Join(testDirectory, inputFile)).Returns(ArchiveLibrary.Default);
|
archiveService.Configure().CanOpen(Path.Join(testDirectory, inputFile)).Returns(ArchiveLibrary.Default);
|
||||||
@ -174,7 +175,7 @@ namespace API.Tests.Services
|
|||||||
[InlineData("sorting.zip", "sorting.expected.jpg")]
|
[InlineData("sorting.zip", "sorting.expected.jpg")]
|
||||||
public void GetCoverImage_SharpCompress_Test(string inputFile, string expectedOutputFile)
|
public void GetCoverImage_SharpCompress_Test(string inputFile, string expectedOutputFile)
|
||||||
{
|
{
|
||||||
var archiveService = Substitute.For<ArchiveService>(_logger);
|
var archiveService = Substitute.For<ArchiveService>(_logger, new DirectoryService(_directoryServiceLogger));
|
||||||
var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/CoverImages");
|
var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/CoverImages");
|
||||||
var expectedBytes = File.ReadAllBytes(Path.Join(testDirectory, expectedOutputFile));
|
var expectedBytes = File.ReadAllBytes(Path.Join(testDirectory, expectedOutputFile));
|
||||||
|
|
||||||
|
@ -4,5 +4,9 @@
|
|||||||
{
|
{
|
||||||
public const string AdminRole = "Admin";
|
public const string AdminRole = "Admin";
|
||||||
public const string PlebRole = "Pleb";
|
public const string PlebRole = "Pleb";
|
||||||
|
/// <summary>
|
||||||
|
/// Used to give a user ability to download files from the server
|
||||||
|
/// </summary>
|
||||||
|
public const string DownloadRole = "Download";
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Constants;
|
using API.Constants;
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
@ -150,5 +151,45 @@ namespace API.Controllers
|
|||||||
Preferences = _mapper.Map<UserPreferencesDto>(user.UserPreferences)
|
Preferences = _mapper.Map<UserPreferencesDto>(user.UserPreferences)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("roles")]
|
||||||
|
public ActionResult<IList<string>> GetRoles()
|
||||||
|
{
|
||||||
|
return typeof(PolicyConstants)
|
||||||
|
.GetFields(BindingFlags.Public | BindingFlags.Static)
|
||||||
|
.Where(f => f.FieldType == typeof(string))
|
||||||
|
.ToDictionary(f => f.Name,
|
||||||
|
f => (string) f.GetValue(null)).Values.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("update-rbs")]
|
||||||
|
public async Task<ActionResult> UpdateRoles(UpdateRbsDto updateRbsDto)
|
||||||
|
{
|
||||||
|
var user = await _userManager.Users
|
||||||
|
.Include(u => u.UserPreferences)
|
||||||
|
//.Include(u => u.UserRoles)
|
||||||
|
.SingleOrDefaultAsync(x => x.NormalizedUserName == updateRbsDto.Username.ToUpper());
|
||||||
|
if (updateRbsDto.Roles.Contains(PolicyConstants.AdminRole) ||
|
||||||
|
updateRbsDto.Roles.Contains(PolicyConstants.PlebRole))
|
||||||
|
{
|
||||||
|
return BadRequest("Invalid Roles");
|
||||||
|
}
|
||||||
|
|
||||||
|
var existingRoles = (await _userManager.GetRolesAsync(user))
|
||||||
|
.Where(s => s != PolicyConstants.AdminRole && s != PolicyConstants.PlebRole)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// Find what needs to be added and what needs to be removed
|
||||||
|
var rolesToRemove = existingRoles.Except(updateRbsDto.Roles);
|
||||||
|
var result = await _userManager.AddToRolesAsync(user, updateRbsDto.Roles);
|
||||||
|
|
||||||
|
if (!result.Succeeded) return BadRequest("Something went wrong, unable to update user's roles");
|
||||||
|
if ((await _userManager.RemoveFromRolesAsync(user, rolesToRemove)).Succeeded)
|
||||||
|
{
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
return BadRequest("Something went wrong, unable to update user's roles");
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
96
API/Controllers/DownloadController.cs
Normal file
96
API/Controllers/DownloadController.cs
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using API.Extensions;
|
||||||
|
using API.Interfaces;
|
||||||
|
using API.Interfaces.Services;
|
||||||
|
using API.Services;
|
||||||
|
using Kavita.Common;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace API.Controllers
|
||||||
|
{
|
||||||
|
[Authorize(Policy = "RequireDownloadRole")]
|
||||||
|
public class DownloadController : BaseApiController
|
||||||
|
{
|
||||||
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
|
private readonly IArchiveService _archiveService;
|
||||||
|
|
||||||
|
public DownloadController(IUnitOfWork unitOfWork, IArchiveService archiveService)
|
||||||
|
{
|
||||||
|
_unitOfWork = unitOfWork;
|
||||||
|
_archiveService = archiveService;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("volume-size")]
|
||||||
|
public async Task<ActionResult<long>> GetVolumeSize(int volumeId)
|
||||||
|
{
|
||||||
|
var files = await _unitOfWork.VolumeRepository.GetFilesForVolume(volumeId);
|
||||||
|
return Ok(DirectoryService.GetTotalSize(files.Select(c => c.FilePath)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("chapter-size")]
|
||||||
|
public async Task<ActionResult<long>> GetChapterSize(int chapterId)
|
||||||
|
{
|
||||||
|
var files = await _unitOfWork.VolumeRepository.GetFilesForChapter(chapterId);
|
||||||
|
return Ok(DirectoryService.GetTotalSize(files.Select(c => c.FilePath)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("series-size")]
|
||||||
|
public async Task<ActionResult<long>> GetSeriesSize(int seriesId)
|
||||||
|
{
|
||||||
|
var files = await _unitOfWork.SeriesRepository.GetFilesForSeries(seriesId);
|
||||||
|
return Ok(DirectoryService.GetTotalSize(files.Select(c => c.FilePath)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("volume")]
|
||||||
|
public async Task<ActionResult> DownloadVolume(int volumeId)
|
||||||
|
{
|
||||||
|
var files = await _unitOfWork.VolumeRepository.GetFilesForVolume(volumeId);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var (fileBytes, zipPath) = await _archiveService.CreateZipForDownload(files.Select(c => c.FilePath),
|
||||||
|
$"download_{User.GetUsername()}_v{volumeId}");
|
||||||
|
return File(fileBytes, "application/zip", Path.GetFileName(zipPath));
|
||||||
|
}
|
||||||
|
catch (KavitaException ex)
|
||||||
|
{
|
||||||
|
return BadRequest(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("chapter")]
|
||||||
|
public async Task<ActionResult> DownloadChapter(int chapterId)
|
||||||
|
{
|
||||||
|
var files = await _unitOfWork.VolumeRepository.GetFilesForChapter(chapterId);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var (fileBytes, zipPath) = await _archiveService.CreateZipForDownload(files.Select(c => c.FilePath),
|
||||||
|
$"download_{User.GetUsername()}_c{chapterId}");
|
||||||
|
return File(fileBytes, "application/zip", Path.GetFileName(zipPath));
|
||||||
|
}
|
||||||
|
catch (KavitaException ex)
|
||||||
|
{
|
||||||
|
return BadRequest(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("series")]
|
||||||
|
public async Task<ActionResult> DownloadSeries(int seriesId)
|
||||||
|
{
|
||||||
|
var files = await _unitOfWork.SeriesRepository.GetFilesForSeries(seriesId);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var (fileBytes, zipPath) = await _archiveService.CreateZipForDownload(files.Select(c => c.FilePath),
|
||||||
|
$"download_{User.GetUsername()}_s{seriesId}");
|
||||||
|
return File(fileBytes, "application/zip", Path.GetFileName(zipPath));
|
||||||
|
}
|
||||||
|
catch (KavitaException ex)
|
||||||
|
{
|
||||||
|
return BadRequest(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,7 @@ using System.Threading.Tasks;
|
|||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Interfaces.Services;
|
using API.Interfaces.Services;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
|
using Kavita.Common;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
@ -19,19 +20,19 @@ 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 IConfiguration _config;
|
||||||
private readonly IDirectoryService _directoryService;
|
|
||||||
private readonly IBackupService _backupService;
|
private readonly IBackupService _backupService;
|
||||||
|
private readonly IArchiveService _archiveService;
|
||||||
|
|
||||||
public ServerController(IHostApplicationLifetime applicationLifetime, ILogger<ServerController> logger, IConfiguration config,
|
public ServerController(IHostApplicationLifetime applicationLifetime, ILogger<ServerController> logger, IConfiguration config,
|
||||||
IDirectoryService directoryService, IBackupService backupService)
|
IBackupService backupService, IArchiveService archiveService)
|
||||||
{
|
{
|
||||||
_applicationLifetime = applicationLifetime;
|
_applicationLifetime = applicationLifetime;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_config = config;
|
_config = config;
|
||||||
_directoryService = directoryService;
|
|
||||||
_backupService = backupService;
|
_backupService = backupService;
|
||||||
|
_archiveService = archiveService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("restart")]
|
[HttpPost("restart")]
|
||||||
public ActionResult RestartServer()
|
public ActionResult RestartServer()
|
||||||
{
|
{
|
||||||
@ -45,33 +46,17 @@ namespace API.Controllers
|
|||||||
public async Task<ActionResult> GetLogs()
|
public async Task<ActionResult> GetLogs()
|
||||||
{
|
{
|
||||||
var files = _backupService.LogFiles(_config.GetMaxRollingFiles(), _config.GetLoggingFileName());
|
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
|
try
|
||||||
{
|
{
|
||||||
ZipFile.CreateFromDirectory(tempLocation, zipPath);
|
var (fileBytes, zipPath) = await _archiveService.CreateZipForDownload(files, "logs");
|
||||||
|
return File(fileBytes, "application/zip", Path.GetFileName(zipPath));
|
||||||
}
|
}
|
||||||
catch (AggregateException ex)
|
catch (KavitaException ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "There was an issue when archiving library backup");
|
return BadRequest(ex.Message);
|
||||||
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,7 +16,7 @@ using Microsoft.Extensions.Logging;
|
|||||||
|
|
||||||
namespace API.Controllers
|
namespace API.Controllers
|
||||||
{
|
{
|
||||||
[Authorize]
|
[Authorize(Policy = "RequireAdminRole")]
|
||||||
public class SettingsController : BaseApiController
|
public class SettingsController : BaseApiController
|
||||||
{
|
{
|
||||||
private readonly ILogger<SettingsController> _logger;
|
private readonly ILogger<SettingsController> _logger;
|
||||||
@ -31,7 +31,7 @@ namespace API.Controllers
|
|||||||
_taskScheduler = taskScheduler;
|
_taskScheduler = taskScheduler;
|
||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("")]
|
[HttpGet("")]
|
||||||
public async Task<ActionResult<ServerSettingDto>> GetSettings()
|
public async Task<ActionResult<ServerSettingDto>> GetSettings()
|
||||||
{
|
{
|
||||||
@ -40,8 +40,7 @@ namespace API.Controllers
|
|||||||
settingsDto.LoggingLevel = Configuration.GetLogLevel(Program.GetAppSettingFilename());
|
settingsDto.LoggingLevel = Configuration.GetLogLevel(Program.GetAppSettingFilename());
|
||||||
return Ok(settingsDto);
|
return Ok(settingsDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize(Policy = "RequireAdminRole")]
|
|
||||||
[HttpPost("")]
|
[HttpPost("")]
|
||||||
public async Task<ActionResult<ServerSettingDto>> UpdateSettings(ServerSettingDto updateSettingsDto)
|
public async Task<ActionResult<ServerSettingDto>> UpdateSettings(ServerSettingDto updateSettingsDto)
|
||||||
{
|
{
|
||||||
@ -103,22 +102,19 @@ namespace API.Controllers
|
|||||||
_taskScheduler.ScheduleTasks();
|
_taskScheduler.ScheduleTasks();
|
||||||
return Ok(updateSettingsDto);
|
return Ok(updateSettingsDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize(Policy = "RequireAdminRole")]
|
|
||||||
[HttpGet("task-frequencies")]
|
[HttpGet("task-frequencies")]
|
||||||
public ActionResult<IEnumerable<string>> GetTaskFrequencies()
|
public ActionResult<IEnumerable<string>> GetTaskFrequencies()
|
||||||
{
|
{
|
||||||
return Ok(CronConverter.Options);
|
return Ok(CronConverter.Options);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize(Policy = "RequireAdminRole")]
|
|
||||||
[HttpGet("library-types")]
|
[HttpGet("library-types")]
|
||||||
public ActionResult<IEnumerable<string>> GetLibraryTypes()
|
public ActionResult<IEnumerable<string>> GetLibraryTypes()
|
||||||
{
|
{
|
||||||
return Ok(Enum.GetNames(typeof(LibraryType)));
|
return Ok(Enum.GetNames(typeof(LibraryType)));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize(Policy = "RequireAdminRole")]
|
|
||||||
[HttpGet("log-levels")]
|
[HttpGet("log-levels")]
|
||||||
public ActionResult<IEnumerable<string>> GetLogLevels()
|
public ActionResult<IEnumerable<string>> GetLogLevels()
|
||||||
{
|
{
|
||||||
|
10
API/DTOs/UpdateRBSDto.cs
Normal file
10
API/DTOs/UpdateRBSDto.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace API.DTOs
|
||||||
|
{
|
||||||
|
public class UpdateRbsDto
|
||||||
|
{
|
||||||
|
public string Username { get; init; }
|
||||||
|
public IList<string> Roles { get; init; }
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Constants;
|
using API.Constants;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
@ -15,11 +16,13 @@ namespace API.Data
|
|||||||
{
|
{
|
||||||
public static async Task SeedRoles(RoleManager<AppRole> roleManager)
|
public static async Task SeedRoles(RoleManager<AppRole> roleManager)
|
||||||
{
|
{
|
||||||
var roles = new List<AppRole>
|
var roles = typeof(PolicyConstants)
|
||||||
{
|
.GetFields(BindingFlags.Public | BindingFlags.Static)
|
||||||
new() {Name = PolicyConstants.AdminRole},
|
.Where(f => f.FieldType == typeof(string))
|
||||||
new() {Name = PolicyConstants.PlebRole}
|
.ToDictionary(f => f.Name,
|
||||||
};
|
f => (string) f.GetValue(null)).Values
|
||||||
|
.Select(policyName => new AppRole() {Name = policyName})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
foreach (var role in roles)
|
foreach (var role in roles)
|
||||||
{
|
{
|
||||||
|
@ -411,5 +411,16 @@ namespace API.Data
|
|||||||
|
|
||||||
return await PagedList<SeriesDto>.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
|
return await PagedList<SeriesDto>.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IList<MangaFile>> GetFilesForSeries(int seriesId)
|
||||||
|
{
|
||||||
|
return await _context.Volume
|
||||||
|
.Where(v => v.SeriesId == seriesId)
|
||||||
|
.Include(v => v.Chapters)
|
||||||
|
.ThenInclude(c => c.Files)
|
||||||
|
.SelectMany(v => v.Chapters.SelectMany(c => c.Files))
|
||||||
|
.AsNoTracking()
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -65,6 +65,8 @@ namespace API.Data
|
|||||||
.SingleOrDefaultAsync();
|
.SingleOrDefaultAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public async Task<ChapterDto> GetChapterDtoAsync(int chapterId)
|
public async Task<ChapterDto> GetChapterDtoAsync(int chapterId)
|
||||||
{
|
{
|
||||||
@ -84,5 +86,15 @@ namespace API.Data
|
|||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IList<MangaFile>> GetFilesForVolume(int volumeId)
|
||||||
|
{
|
||||||
|
return await _context.Chapter
|
||||||
|
.Where(c => volumeId == c.VolumeId)
|
||||||
|
.Include(c => c.Files)
|
||||||
|
.SelectMany(c => c.Files)
|
||||||
|
.AsNoTracking()
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -39,6 +39,7 @@ namespace API.Extensions
|
|||||||
services.AddAuthorization(opt =>
|
services.AddAuthorization(opt =>
|
||||||
{
|
{
|
||||||
opt.AddPolicy("RequireAdminRole", policy => policy.RequireRole(PolicyConstants.AdminRole));
|
opt.AddPolicy("RequireAdminRole", policy => policy.RequireRole(PolicyConstants.AdminRole));
|
||||||
|
opt.AddPolicy("RequireDownloadRole", policy => policy.RequireRole(PolicyConstants.DownloadRole, PolicyConstants.AdminRole));
|
||||||
});
|
});
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
|
@ -61,5 +61,6 @@ namespace API.Interfaces
|
|||||||
Task<PagedList<SeriesDto>> GetRecentlyAdded(int libraryId, int userId, UserParams userParams);
|
Task<PagedList<SeriesDto>> GetRecentlyAdded(int libraryId, int userId, UserParams userParams);
|
||||||
Task<SeriesMetadataDto> GetSeriesMetadata(int seriesId);
|
Task<SeriesMetadataDto> GetSeriesMetadata(int seriesId);
|
||||||
Task<PagedList<SeriesDto>> GetSeriesDtoForCollectionAsync(int collectionId, int userId, UserParams userParams);
|
Task<PagedList<SeriesDto>> GetSeriesDtoForCollectionAsync(int collectionId, int userId, UserParams userParams);
|
||||||
|
Task<IList<MangaFile>> GetFilesForSeries(int seriesId);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -13,5 +13,6 @@ namespace API.Interfaces
|
|||||||
Task<IList<MangaFile>> GetFilesForChapter(int chapterId);
|
Task<IList<MangaFile>> GetFilesForChapter(int chapterId);
|
||||||
Task<IList<Chapter>> GetChaptersAsync(int volumeId);
|
Task<IList<Chapter>> GetChaptersAsync(int volumeId);
|
||||||
Task<byte[]> GetChapterCoverImageAsync(int chapterId);
|
Task<byte[]> GetChapterCoverImageAsync(int chapterId);
|
||||||
|
Task<IList<MangaFile>> GetFilesForVolume(int volumeId);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,5 +1,9 @@
|
|||||||
using System.IO.Compression;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO.Compression;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using API.Archive;
|
using API.Archive;
|
||||||
|
using API.Entities;
|
||||||
|
|
||||||
namespace API.Interfaces.Services
|
namespace API.Interfaces.Services
|
||||||
{
|
{
|
||||||
@ -12,5 +16,6 @@ namespace API.Interfaces.Services
|
|||||||
string GetSummaryInfo(string archivePath);
|
string GetSummaryInfo(string archivePath);
|
||||||
ArchiveLibrary CanOpen(string archivePath);
|
ArchiveLibrary CanOpen(string archivePath);
|
||||||
bool ArchiveNeedsFlattening(ZipArchive archive);
|
bool ArchiveNeedsFlattening(ZipArchive archive);
|
||||||
|
Task<Tuple<byte[], string>> CreateZipForDownload(IEnumerable<string> files, string tempFolder);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -4,12 +4,14 @@ 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.Threading.Tasks;
|
||||||
using System.Xml.Serialization;
|
using System.Xml.Serialization;
|
||||||
using API.Archive;
|
using API.Archive;
|
||||||
using API.Comparators;
|
using API.Comparators;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Interfaces.Services;
|
using API.Interfaces.Services;
|
||||||
using API.Services.Tasks;
|
using API.Services.Tasks;
|
||||||
|
using Kavita.Common;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.IO;
|
using Microsoft.IO;
|
||||||
using SharpCompress.Archives;
|
using SharpCompress.Archives;
|
||||||
@ -25,13 +27,15 @@ namespace API.Services
|
|||||||
public class ArchiveService : IArchiveService
|
public class ArchiveService : IArchiveService
|
||||||
{
|
{
|
||||||
private readonly ILogger<ArchiveService> _logger;
|
private readonly ILogger<ArchiveService> _logger;
|
||||||
|
private readonly IDirectoryService _directoryService;
|
||||||
private const int ThumbnailWidth = 320; // 153w x 230h
|
private const int ThumbnailWidth = 320; // 153w x 230h
|
||||||
private static readonly RecyclableMemoryStreamManager StreamManager = new();
|
private static readonly RecyclableMemoryStreamManager StreamManager = new();
|
||||||
private readonly NaturalSortComparer _comparer;
|
private readonly NaturalSortComparer _comparer;
|
||||||
|
|
||||||
public ArchiveService(ILogger<ArchiveService> logger)
|
public ArchiveService(ILogger<ArchiveService> logger, IDirectoryService directoryService)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_directoryService = directoryService;
|
||||||
_comparer = new NaturalSortComparer();
|
_comparer = new NaturalSortComparer();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -216,7 +220,39 @@ namespace API.Services
|
|||||||
!Path.HasExtension(archive.Entries.ElementAt(0).FullName) ||
|
!Path.HasExtension(archive.Entries.ElementAt(0).FullName) ||
|
||||||
archive.Entries.Any(e => e.FullName.Contains(Path.AltDirectorySeparatorChar) && !Parser.Parser.HasBlacklistedFolderInPath(e.FullName));
|
archive.Entries.Any(e => e.FullName.Contains(Path.AltDirectorySeparatorChar) && !Parser.Parser.HasBlacklistedFolderInPath(e.FullName));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<Tuple<byte[], string>> CreateZipForDownload(IEnumerable<string> files, string tempFolder)
|
||||||
|
{
|
||||||
|
var tempDirectory = Path.Join(Directory.GetCurrentDirectory(), "temp");
|
||||||
|
var dateString = DateTime.Now.ToShortDateString().Replace("/", "_");
|
||||||
|
|
||||||
|
var tempLocation = Path.Join(tempDirectory, $"{tempFolder}_{dateString}");
|
||||||
|
DirectoryService.ExistOrCreate(tempLocation);
|
||||||
|
if (!_directoryService.CopyFilesToDirectory(files, tempLocation))
|
||||||
|
{
|
||||||
|
throw new KavitaException("Unable to copy files to temp directory archive download.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var zipPath = Path.Join(tempDirectory, $"kavita_{tempFolder}_{dateString}.zip");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ZipFile.CreateFromDirectory(tempLocation, zipPath);
|
||||||
|
}
|
||||||
|
catch (AggregateException ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "There was an issue creating temp archive");
|
||||||
|
throw new KavitaException("There was an issue creating temp archive");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var fileBytes = await _directoryService.ReadFileAsync(zipPath);
|
||||||
|
|
||||||
|
DirectoryService.ClearAndDeleteDirectory(tempLocation);
|
||||||
|
(new FileInfo(zipPath)).Delete();
|
||||||
|
|
||||||
|
return Tuple.Create(fileBytes, zipPath);
|
||||||
|
}
|
||||||
|
|
||||||
private byte[] CreateThumbnail(string entryName, Stream stream, string formatExtension = ".jpg")
|
private byte[] CreateThumbnail(string entryName, Stream stream, string formatExtension = ".jpg")
|
||||||
{
|
{
|
||||||
if (!formatExtension.StartsWith("."))
|
if (!formatExtension.StartsWith("."))
|
||||||
|
@ -102,6 +102,16 @@ namespace API.Services
|
|||||||
return !Directory.Exists(path) ? Array.Empty<string>() : Directory.GetFiles(path);
|
return !Directory.Exists(path) ? Array.Empty<string>() : Directory.GetFiles(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the total number of bytes for a given set of full file paths
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="paths"></param>
|
||||||
|
/// <returns>Total bytes</returns>
|
||||||
|
public static long GetTotalSize(IEnumerable<string> paths)
|
||||||
|
{
|
||||||
|
return paths.Sum(path => new FileInfo(path).Length);
|
||||||
|
}
|
||||||
|
|
||||||
/// <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>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user