mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-05-24 00:52:23 -04:00
* Fixed a bug where cache TTL was using a field which always was 0. * Updated Scan Series task (from UI) to always re-calculate what's on file and not rely on last update. This leads to more reliable results, despite extra overhead. * Added image range processing on images for the reader, for slower networks or large files * On manga (single) try to use prefetched image, rather than re-requesting an image on pagination * Reduced some more latency when rendering first page of next chapter via continuous reading mode * Fixed a bug where metadata filter, after updating a typeahead, collapsing filter area then re-opening, the filter would still be applied, but the typeahead wouldn't show the modification. * Coded an idea around download reporting, commiting for history, might not go with it. * Refactored the download indicator into it's own component. Cleaning up some code for download within card component * Another throw away commit. Put in some temp code, not working but not sure if I'm ditching entirely. * Updated download service to enable range processing (so downloads can resume) and to reduce re-zipping if we've just downloaded something. * Refactored events widget download indicator to the correct design. I will be moving forward with this new functionality. * Added Required fields to ProgressDTO * Cleaned up the event widget and updated existing download progress to indicate preparing the download, rather than the download itself. * Updated dependencies for security alerts * Refactored all download code to be streamlined and globally handled * Updated ScanSeries to find the highest folder path before library, not just within the files. This could lead to scan series missing files due to nested folders on same parent level. * Updated the caching code to use a builtin annotation. Images are now caching correctly. * Fixed a bad redirect on an auth guard * Tweaked how long we allow cache for, as the cover update now doesn't work well. * Fixed a bug on downloading bookmarks from multiple series, where it would just choose the first series id for the temp file. * Added an extra check for downloading bookmarks * UI Security updates, Fixed a bug on bookmark reader, the reader on last page would throw some errors and not show No Next Chapter toast. * After scan, clear temp * Code smells
223 lines
9.9 KiB
C#
223 lines
9.9 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using API.Constants;
|
|
using API.Data;
|
|
using API.DTOs.Downloads;
|
|
using API.Entities;
|
|
using API.Extensions;
|
|
using API.Services;
|
|
using API.SignalR;
|
|
using Kavita.Common;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Identity;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace API.Controllers
|
|
{
|
|
/// <summary>
|
|
/// All APIs related to downloading entities from the system. Requires Download Role or Admin Role.
|
|
/// </summary>
|
|
[Authorize(Policy="RequireDownloadRole")]
|
|
public class DownloadController : BaseApiController
|
|
{
|
|
private readonly IUnitOfWork _unitOfWork;
|
|
private readonly IArchiveService _archiveService;
|
|
private readonly IDirectoryService _directoryService;
|
|
private readonly IDownloadService _downloadService;
|
|
private readonly IEventHub _eventHub;
|
|
private readonly ILogger<DownloadController> _logger;
|
|
private readonly IBookmarkService _bookmarkService;
|
|
private const string DefaultContentType = "application/octet-stream";
|
|
|
|
public DownloadController(IUnitOfWork unitOfWork, IArchiveService archiveService, IDirectoryService directoryService,
|
|
IDownloadService downloadService, IEventHub eventHub, ILogger<DownloadController> logger, IBookmarkService bookmarkService)
|
|
{
|
|
_unitOfWork = unitOfWork;
|
|
_archiveService = archiveService;
|
|
_directoryService = directoryService;
|
|
_downloadService = downloadService;
|
|
_eventHub = eventHub;
|
|
_logger = logger;
|
|
_bookmarkService = bookmarkService;
|
|
}
|
|
|
|
/// <summary>
|
|
/// For a given volume, return the size in bytes
|
|
/// </summary>
|
|
/// <param name="volumeId"></param>
|
|
/// <returns></returns>
|
|
[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)));
|
|
}
|
|
|
|
/// <summary>
|
|
/// For a given chapter, return the size in bytes
|
|
/// </summary>
|
|
/// <param name="chapterId"></param>
|
|
/// <returns></returns>
|
|
[HttpGet("chapter-size")]
|
|
public async Task<ActionResult<long>> GetChapterSize(int chapterId)
|
|
{
|
|
var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId);
|
|
return Ok(_directoryService.GetTotalSize(files.Select(c => c.FilePath)));
|
|
}
|
|
|
|
/// <summary>
|
|
/// For a series, return the size in bytes
|
|
/// </summary>
|
|
/// <param name="seriesId"></param>
|
|
/// <returns></returns>
|
|
[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)));
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Downloads all chapters within a volume. If the chapters are multiple zips, they will all be zipped up.
|
|
/// </summary>
|
|
/// <param name="volumeId"></param>
|
|
/// <returns></returns>
|
|
[Authorize(Policy="RequireDownloadRole")]
|
|
[HttpGet("volume")]
|
|
public async Task<ActionResult> DownloadVolume(int volumeId)
|
|
{
|
|
if (!await HasDownloadPermission()) return BadRequest("You do not have permission");
|
|
|
|
var files = await _unitOfWork.VolumeRepository.GetFilesForVolume(volumeId);
|
|
var volume = await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(volumeId);
|
|
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId);
|
|
try
|
|
{
|
|
return await DownloadFiles(files, $"download_{User.GetUsername()}_v{volumeId}", $"{series.Name} - Volume {volume.Number}.zip");
|
|
}
|
|
catch (KavitaException ex)
|
|
{
|
|
return BadRequest(ex.Message);
|
|
}
|
|
}
|
|
|
|
private async Task<bool> HasDownloadPermission()
|
|
{
|
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
|
return await _downloadService.HasDownloadPermission(user);
|
|
}
|
|
|
|
private ActionResult GetFirstFileDownload(IEnumerable<MangaFile> files)
|
|
{
|
|
var (zipFile, contentType, fileDownloadName) = _downloadService.GetFirstFileDownload(files);
|
|
return PhysicalFile(zipFile, contentType, fileDownloadName, true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the zip for a single chapter. If the chapter contains multiple files, they will be zipped.
|
|
/// </summary>
|
|
/// <param name="chapterId"></param>
|
|
/// <returns></returns>
|
|
[HttpGet("chapter")]
|
|
public async Task<ActionResult> DownloadChapter(int chapterId)
|
|
{
|
|
if (!await HasDownloadPermission()) return BadRequest("You do not have permission");
|
|
var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId);
|
|
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
|
|
var volume = await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(chapter.VolumeId);
|
|
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId);
|
|
try
|
|
{
|
|
return await DownloadFiles(files, $"download_{User.GetUsername()}_c{chapterId}", $"{series.Name} - Chapter {chapter.Number}.zip");
|
|
}
|
|
catch (KavitaException ex)
|
|
{
|
|
return BadRequest(ex.Message);
|
|
}
|
|
}
|
|
|
|
private async Task<ActionResult> DownloadFiles(ICollection<MangaFile> files, string tempFolder, string downloadName)
|
|
{
|
|
try
|
|
{
|
|
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
|
MessageFactory.DownloadProgressEvent(User.GetUsername(),
|
|
Path.GetFileNameWithoutExtension(downloadName), 0F, "started"));
|
|
if (files.Count == 1)
|
|
{
|
|
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
|
MessageFactory.DownloadProgressEvent(User.GetUsername(),
|
|
Path.GetFileNameWithoutExtension(downloadName), 1F, "ended"));
|
|
return GetFirstFileDownload(files);
|
|
}
|
|
|
|
var filePath = _archiveService.CreateZipForDownload(files.Select(c => c.FilePath), tempFolder);
|
|
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
|
MessageFactory.DownloadProgressEvent(User.GetUsername(),
|
|
Path.GetFileNameWithoutExtension(downloadName), 1F, "ended"));
|
|
return PhysicalFile(filePath, DefaultContentType, downloadName, true);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "There was an exception when trying to download files");
|
|
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
|
MessageFactory.DownloadProgressEvent(User.GetUsername(),
|
|
Path.GetFileNameWithoutExtension(downloadName), 1F, "ended"));
|
|
throw;
|
|
}
|
|
}
|
|
|
|
[HttpGet("series")]
|
|
public async Task<ActionResult> DownloadSeries(int seriesId)
|
|
{
|
|
if (!await HasDownloadPermission()) return BadRequest("You do not have permission");
|
|
var files = await _unitOfWork.SeriesRepository.GetFilesForSeries(seriesId);
|
|
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId);
|
|
try
|
|
{
|
|
return await DownloadFiles(files, $"download_{User.GetUsername()}_s{seriesId}", $"{series.Name}.zip");
|
|
}
|
|
catch (KavitaException ex)
|
|
{
|
|
return BadRequest(ex.Message);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Downloads all bookmarks in a zip for
|
|
/// </summary>
|
|
/// <param name="downloadBookmarkDto"></param>
|
|
/// <returns></returns>
|
|
[HttpPost("bookmarks")]
|
|
public async Task<ActionResult> DownloadBookmarkPages(DownloadBookmarkDto downloadBookmarkDto)
|
|
{
|
|
if (!await HasDownloadPermission()) return BadRequest("You do not have permission");
|
|
if (!downloadBookmarkDto.Bookmarks.Any()) return BadRequest("Bookmarks cannot be empty");
|
|
|
|
// We know that all bookmarks will be for one single seriesId
|
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
|
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(downloadBookmarkDto.Bookmarks.First().SeriesId);
|
|
|
|
var files = await _bookmarkService.GetBookmarkFilesById(downloadBookmarkDto.Bookmarks.Select(b => b.Id));
|
|
|
|
var filename = $"{series.Name} - Bookmarks.zip";
|
|
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
|
MessageFactory.DownloadProgressEvent(User.GetUsername(), Path.GetFileNameWithoutExtension(filename), 0F));
|
|
var seriesIds = string.Join("_", downloadBookmarkDto.Bookmarks.Select(b => b.SeriesId).Distinct());
|
|
var filePath = _archiveService.CreateZipForDownload(files,
|
|
$"download_{user.Id}_{seriesIds}_bookmarks");
|
|
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
|
MessageFactory.DownloadProgressEvent(User.GetUsername(), Path.GetFileNameWithoutExtension(filename), 1F));
|
|
|
|
|
|
return PhysicalFile(filePath, DefaultContentType, filename, true);
|
|
}
|
|
|
|
}
|
|
}
|