using System;
using System.Linq;
using System.Threading.Tasks;
using API.Constants;
using API.Data;
using API.Data.Repositories;
using API.DTOs.Uploads;
using API.Entities.Enums;
using API.Extensions;
using API.Services;
using API.SignalR;
using Flurl.Http;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace API.Controllers;
#nullable enable
///
///
///
public class UploadController : BaseApiController
{
private readonly IUnitOfWork _unitOfWork;
private readonly IImageService _imageService;
private readonly ILogger _logger;
private readonly ITaskScheduler _taskScheduler;
private readonly IDirectoryService _directoryService;
private readonly IEventHub _eventHub;
private readonly IReadingListService _readingListService;
private readonly ILocalizationService _localizationService;
///
public UploadController(IUnitOfWork unitOfWork, IImageService imageService, ILogger logger,
ITaskScheduler taskScheduler, IDirectoryService directoryService, IEventHub eventHub, IReadingListService readingListService,
ILocalizationService localizationService)
{
_unitOfWork = unitOfWork;
_imageService = imageService;
_logger = logger;
_taskScheduler = taskScheduler;
_directoryService = directoryService;
_eventHub = eventHub;
_readingListService = readingListService;
_localizationService = localizationService;
}
///
/// This stores a file (image) in temp directory for use in a cover image replacement flow.
/// This is automatically cleaned up.
///
/// Escaped url to download from
/// filename
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("upload-by-url")]
public async Task> GetImageFromFile(UploadUrlDto dto)
{
var dateString = $"{DateTime.UtcNow.ToShortDateString()}_{DateTime.UtcNow.ToLongTimeString()}".Replace('/', '_').Replace(':', '_');
var format = _directoryService.FileSystem.Path.GetExtension(dto.Url.Split('?')[0]).Replace(".", string.Empty);
try
{
var path = await dto.Url
.DownloadFileAsync(_directoryService.TempDirectory, $"coverupload_{dateString}.{format}");
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path))
return BadRequest(await _localizationService.Translate(User.GetUserId(), "url-not-valid"));
if (!await _imageService.IsImage(path)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "url-not-valid"));
return $"coverupload_{dateString}.{format}";
}
catch (FlurlHttpException ex)
{
// Unauthorized
if (ex.StatusCode == 401)
return BadRequest(await _localizationService.Translate(User.GetUserId(), "url-not-valid"));
}
return BadRequest(await _localizationService.Translate(User.GetUserId(), "url-not-valid"));
}
///
/// Replaces series cover image and locks it with a base64 encoded image
///
///
///
[Authorize(Policy = "RequireAdminRole")]
[RequestSizeLimit(ControllerConstants.MaxUploadSizeBytes)]
[HttpPost("series")]
public async Task UploadSeriesCoverImageFromUrl(UploadFileDto uploadFileDto)
{
// Check if Url is non empty, request the image and place in temp, then ask image service to handle it.
// See if we can do this all in memory without touching underlying system
try
{
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(uploadFileDto.Id);
if (series == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "series-doesnt-exist"));
var filePath = string.Empty;
var lockState = false;
if (!string.IsNullOrEmpty(uploadFileDto.Url))
{
filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetSeriesFormat(uploadFileDto.Id)}");
lockState = uploadFileDto.LockCover;
}
if (!string.IsNullOrEmpty(filePath))
{
series.CoverImage = filePath;
series.CoverImageLocked = lockState;
_imageService.UpdateColorScape(series);
_unitOfWork.SeriesRepository.Update(series);
}
if (_unitOfWork.HasChanges())
{
// Refresh covers
if (string.IsNullOrEmpty(uploadFileDto.Url))
{
_taskScheduler.RefreshSeriesMetadata(series.LibraryId, series.Id, true);
}
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
MessageFactory.CoverUpdateEvent(series.Id, MessageFactoryEntityTypes.Series), false);
await _unitOfWork.CommitAsync();
return Ok();
}
}
catch (Exception e)
{
_logger.LogError(e, "There was an issue uploading cover image for Series {Id}", uploadFileDto.Id);
await _unitOfWork.RollbackAsync();
}
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-cover-series-save"));
}
///
/// Replaces collection tag cover image and locks it with a base64 encoded image
///
///
///
[Authorize(Policy = "RequireAdminRole")]
[RequestSizeLimit(ControllerConstants.MaxUploadSizeBytes)]
[HttpPost("collection")]
public async Task UploadCollectionCoverImageFromUrl(UploadFileDto uploadFileDto)
{
// Check if Url is non empty, request the image and place in temp, then ask image service to handle it.
// See if we can do this all in memory without touching underlying system
try
{
var tag = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(uploadFileDto.Id);
if (tag == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "collection-doesnt-exist"));
var filePath = string.Empty;
var lockState = false;
if (!string.IsNullOrEmpty(uploadFileDto.Url))
{
filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetCollectionTagFormat(uploadFileDto.Id)}");
lockState = uploadFileDto.LockCover;
}
tag.CoverImage = filePath;
tag.CoverImageLocked = lockState;
_imageService.UpdateColorScape(tag);
_unitOfWork.CollectionTagRepository.Update(tag);
if (_unitOfWork.HasChanges())
{
await _unitOfWork.CommitAsync();
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
MessageFactory.CoverUpdateEvent(tag.Id, MessageFactoryEntityTypes.CollectionTag), false);
return Ok();
}
}
catch (Exception e)
{
_logger.LogError(e, "There was an issue uploading cover image for Collection Tag {Id}", uploadFileDto.Id);
await _unitOfWork.RollbackAsync();
}
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-cover-collection-save"));
}
///
/// Replaces reading list cover image and locks it with a base64 encoded image
///
/// This is the only API that can be called by non-admins, but the authenticated user must have a readinglist permission
///
///
[RequestSizeLimit(ControllerConstants.MaxUploadSizeBytes)]
[HttpPost("reading-list")]
public async Task UploadReadingListCoverImageFromUrl(UploadFileDto uploadFileDto)
{
// Check if Url is non-empty, request the image and place in temp, then ask image service to handle it.
// See if we can do this all in memory without touching underlying system
if (await _readingListService.UserHasReadingListAccess(uploadFileDto.Id, User.GetUsername()) == null)
return Unauthorized(await _localizationService.Translate(User.GetUserId(), "access-denied"));
try
{
var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(uploadFileDto.Id);
if (readingList == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-doesnt-exist"));
var filePath = string.Empty;
var lockState = false;
if (!string.IsNullOrEmpty(uploadFileDto.Url))
{
filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetReadingListFormat(uploadFileDto.Id)}");
lockState = uploadFileDto.LockCover;
}
readingList.CoverImage = filePath;
readingList.CoverImageLocked = lockState;
_imageService.UpdateColorScape(readingList);
_unitOfWork.ReadingListRepository.Update(readingList);
if (_unitOfWork.HasChanges())
{
await _unitOfWork.CommitAsync();
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
MessageFactory.CoverUpdateEvent(readingList.Id, MessageFactoryEntityTypes.ReadingList), false);
return Ok();
}
}
catch (Exception e)
{
_logger.LogError(e, "There was an issue uploading cover image for Reading List {Id}", uploadFileDto.Id);
await _unitOfWork.RollbackAsync();
}
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-cover-reading-list-save"));
}
private async Task CreateThumbnail(UploadFileDto uploadFileDto, string filename)
{
var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
var encodeFormat = settings.EncodeMediaAs;
var coverImageSize = settings.CoverImageSize;
return _imageService.CreateThumbnailFromBase64(uploadFileDto.Url,
filename, encodeFormat, coverImageSize.GetDimensions().Width);
}
///
/// Replaces chapter cover image and locks it with a base64 encoded image. This will update the parent volume's cover image.
///
///
///
[Authorize(Policy = "RequireAdminRole")]
[RequestSizeLimit(ControllerConstants.MaxUploadSizeBytes)]
[HttpPost("chapter")]
public async Task UploadChapterCoverImageFromUrl(UploadFileDto uploadFileDto)
{
// Check if Url is non empty, request the image and place in temp, then ask image service to handle it.
// See if we can do this all in memory without touching underlying system
try
{
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(uploadFileDto.Id);
if (chapter == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
var filePath = string.Empty;
var lockState = false;
if (!string.IsNullOrEmpty(uploadFileDto.Url))
{
filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetChapterFormat(uploadFileDto.Id, chapter.VolumeId)}");
lockState = uploadFileDto.LockCover;
}
chapter.CoverImage = filePath;
chapter.CoverImageLocked = lockState;
_unitOfWork.ChapterRepository.Update(chapter);
var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(chapter.VolumeId);
if (volume != null)
{
volume.CoverImage = chapter.CoverImage;
volume.CoverImageLocked = lockState;
_unitOfWork.VolumeRepository.Update(volume);
}
if (_unitOfWork.HasChanges())
{
await _unitOfWork.CommitAsync();
// Refresh covers
if (string.IsNullOrEmpty(uploadFileDto.Url))
{
var series = (await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume!.SeriesId))!;
_taskScheduler.RefreshSeriesMetadata(series.LibraryId, series.Id, true);
}
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
MessageFactory.CoverUpdateEvent(chapter.VolumeId, MessageFactoryEntityTypes.Volume), false);
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
MessageFactory.CoverUpdateEvent(chapter.Id, MessageFactoryEntityTypes.Chapter), false);
return Ok();
}
}
catch (Exception e)
{
_logger.LogError(e, "There was an issue uploading cover image for Chapter {Id}", uploadFileDto.Id);
await _unitOfWork.RollbackAsync();
}
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-cover-chapter-save"));
}
///
/// Replaces volume cover image and locks it with a base64 encoded image.
///
/// This will not update the underlying chapter
///
///
[Authorize(Policy = "RequireAdminRole")]
[RequestSizeLimit(ControllerConstants.MaxUploadSizeBytes)]
[HttpPost("volume")]
public async Task UploadVolumeCoverImageFromUrl(UploadFileDto uploadFileDto)
{
// Check if Url is non empty, request the image and place in temp, then ask image service to handle it.
// See if we can do this all in memory without touching underlying system
try
{
var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(uploadFileDto.Id, VolumeIncludes.Chapters);
if (volume == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "volume-doesnt-exist"));
var filePath = string.Empty;
var lockState = false;
if (!string.IsNullOrEmpty(uploadFileDto.Url))
{
filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetVolumeFormat(uploadFileDto.Id)}");
lockState = uploadFileDto.LockCover;
}
volume.CoverImage = filePath;
volume.CoverImageLocked = lockState;
_imageService.UpdateColorScape(volume);
_unitOfWork.VolumeRepository.Update(volume);
if (_unitOfWork.HasChanges())
{
await _unitOfWork.CommitAsync();
// Refresh covers
if (string.IsNullOrEmpty(uploadFileDto.Url))
{
var series = (await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId))!;
_taskScheduler.RefreshSeriesMetadata(series.LibraryId, series.Id, true);
}
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
MessageFactory.CoverUpdateEvent(uploadFileDto.Id, MessageFactoryEntityTypes.Volume), false);
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
MessageFactory.CoverUpdateEvent(volume.Id, MessageFactoryEntityTypes.Chapter), false);
return Ok();
}
}
catch (Exception e)
{
_logger.LogError(e, "There was an issue uploading cover image for Volume {Id}", uploadFileDto.Id);
await _unitOfWork.RollbackAsync();
}
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-cover-volume-save"));
}
///
/// Replaces library cover image with a base64 encoded image. If empty string passed, will reset to null.
///
///
///
[Authorize(Policy = "RequireAdminRole")]
[RequestSizeLimit(ControllerConstants.MaxUploadSizeBytes)]
[HttpPost("library")]
public async Task UploadLibraryCoverImageFromUrl(UploadFileDto uploadFileDto)
{
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(uploadFileDto.Id);
if (library == null) return BadRequest("This library does not exist");
// Check if Url is non empty, request the image and place in temp, then ask image service to handle it.
// See if we can do this all in memory without touching underlying system
if (string.IsNullOrEmpty(uploadFileDto.Url))
{
library.CoverImage = null;
library.ResetColorScape();
_unitOfWork.LibraryRepository.Update(library);
if (_unitOfWork.HasChanges())
{
await _unitOfWork.CommitAsync();
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
MessageFactory.CoverUpdateEvent(library.Id, MessageFactoryEntityTypes.Library), false);
}
return Ok();
}
try
{
var filePath = await CreateThumbnail(uploadFileDto,
$"{ImageService.GetLibraryFormat(uploadFileDto.Id)}");
if (!string.IsNullOrEmpty(filePath))
{
library.CoverImage = filePath;
_imageService.UpdateColorScape(library);
_unitOfWork.LibraryRepository.Update(library);
}
if (_unitOfWork.HasChanges())
{
await _unitOfWork.CommitAsync();
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
MessageFactory.CoverUpdateEvent(library.Id, MessageFactoryEntityTypes.Library), false);
return Ok();
}
}
catch (Exception e)
{
_logger.LogError(e, "There was an issue uploading cover image for Library {Id}", uploadFileDto.Id);
await _unitOfWork.RollbackAsync();
}
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-cover-library-save"));
}
///
/// Replaces chapter cover image and locks it with a base64 encoded image. This will update the parent volume's cover image.
///
/// Does not use Url property
///
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("reset-chapter-lock")]
[Obsolete("Use LockCover in UploadFileDto")]
public async Task ResetChapterLock(UploadFileDto uploadFileDto)
{
try
{
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(uploadFileDto.Id);
if (chapter == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
var originalFile = chapter.CoverImage;
chapter.CoverImage = string.Empty;
chapter.CoverImageLocked = false;
_unitOfWork.ChapterRepository.Update(chapter);
var volume = (await _unitOfWork.VolumeRepository.GetVolumeAsync(chapter.VolumeId))!;
volume.CoverImage = chapter.CoverImage;
_unitOfWork.VolumeRepository.Update(volume);
var series = (await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId))!;
if (_unitOfWork.HasChanges())
{
await _unitOfWork.CommitAsync();
if (originalFile != null) System.IO.File.Delete(originalFile);
_taskScheduler.RefreshSeriesMetadata(series.LibraryId, series.Id, true);
return Ok();
}
}
catch (Exception e)
{
_logger.LogError(e, "There was an issue resetting cover lock for Chapter {Id}", uploadFileDto.Id);
await _unitOfWork.RollbackAsync();
}
return BadRequest(await _localizationService.Translate(User.GetUserId(), "reset-chapter-lock"));
}
}