Removed some dead code on the interfaces. Introduced UnitOfWork to simplify repo injection.

This commit is contained in:
Joseph Milazzo 2021-01-18 13:07:48 -06:00
parent 4a2296a18a
commit 825afd83a2
23 changed files with 165 additions and 204 deletions

View File

@ -17,20 +17,20 @@ namespace API.Controllers
private readonly UserManager<AppUser> _userManager; private readonly UserManager<AppUser> _userManager;
private readonly SignInManager<AppUser> _signInManager; private readonly SignInManager<AppUser> _signInManager;
private readonly ITokenService _tokenService; private readonly ITokenService _tokenService;
private readonly IUserRepository _userRepository; private readonly IUnitOfWork _unitOfWork;
private readonly ILogger<AccountController> _logger; private readonly ILogger<AccountController> _logger;
private readonly IMapper _mapper; private readonly IMapper _mapper;
public AccountController(UserManager<AppUser> userManager, public AccountController(UserManager<AppUser> userManager,
SignInManager<AppUser> signInManager, SignInManager<AppUser> signInManager,
ITokenService tokenService, IUserRepository userRepository, ITokenService tokenService, IUnitOfWork unitOfWork,
ILogger<AccountController> logger, ILogger<AccountController> logger,
IMapper mapper) IMapper mapper)
{ {
_userManager = userManager; _userManager = userManager;
_signInManager = signInManager; _signInManager = signInManager;
_tokenService = tokenService; _tokenService = tokenService;
_userRepository = userRepository; _unitOfWork = unitOfWork;
_logger = logger; _logger = logger;
_mapper = mapper; _mapper = mapper;
} }
@ -38,7 +38,6 @@ namespace API.Controllers
[HttpPost("register")] [HttpPost("register")]
public async Task<ActionResult<UserDto>> Register(RegisterDto registerDto) public async Task<ActionResult<UserDto>> Register(RegisterDto registerDto)
{ {
if (await _userManager.Users.AnyAsync(x => x.UserName == registerDto.Username)) if (await _userManager.Users.AnyAsync(x => x.UserName == registerDto.Username))
{ {
return BadRequest("Username is taken."); return BadRequest("Username is taken.");
@ -77,8 +76,8 @@ namespace API.Controllers
// Update LastActive on account // Update LastActive on account
user.LastActive = DateTime.Now; user.LastActive = DateTime.Now;
_userRepository.Update(user); _unitOfWork.UserRepository.Update(user);
await _userRepository.SaveAllAsync(); await _unitOfWork.Complete();
_logger.LogInformation($"{user.UserName} logged in at {user.LastActive}"); _logger.LogInformation($"{user.UserName} logged in at {user.LastActive}");
@ -88,10 +87,5 @@ namespace API.Controllers
Token = await _tokenService.CreateToken(user) Token = await _tokenService.CreateToken(user)
}; };
} }
// private async Task<bool> UserExists(string username)
// {
// return await _userManager.Users.AnyAsync(user => user.UserName == username.ToLower());
// }
} }
} }

View File

@ -18,25 +18,21 @@ namespace API.Controllers
public class LibraryController : BaseApiController public class LibraryController : BaseApiController
{ {
private readonly IDirectoryService _directoryService; private readonly IDirectoryService _directoryService;
private readonly ILibraryRepository _libraryRepository;
private readonly ILogger<LibraryController> _logger; private readonly ILogger<LibraryController> _logger;
private readonly IUserRepository _userRepository;
private readonly IMapper _mapper; private readonly IMapper _mapper;
private readonly ITaskScheduler _taskScheduler; private readonly ITaskScheduler _taskScheduler;
private readonly ISeriesRepository _seriesRepository; private readonly IUnitOfWork _unitOfWork;
private readonly ICacheService _cacheService; private readonly ICacheService _cacheService;
public LibraryController(IDirectoryService directoryService, public LibraryController(IDirectoryService directoryService,
ILibraryRepository libraryRepository, ILogger<LibraryController> logger, IUserRepository userRepository, ILogger<LibraryController> logger, IMapper mapper, ITaskScheduler taskScheduler,
IMapper mapper, ITaskScheduler taskScheduler, ISeriesRepository seriesRepository, ICacheService cacheService) IUnitOfWork unitOfWork, ICacheService cacheService)
{ {
_directoryService = directoryService; _directoryService = directoryService;
_libraryRepository = libraryRepository;
_logger = logger; _logger = logger;
_userRepository = userRepository;
_mapper = mapper; _mapper = mapper;
_taskScheduler = taskScheduler; _taskScheduler = taskScheduler;
_seriesRepository = seriesRepository; _unitOfWork = unitOfWork;
_cacheService = cacheService; _cacheService = cacheService;
} }
@ -49,12 +45,12 @@ namespace API.Controllers
[HttpPost("create")] [HttpPost("create")]
public async Task<ActionResult> AddLibrary(CreateLibraryDto createLibraryDto) public async Task<ActionResult> AddLibrary(CreateLibraryDto createLibraryDto)
{ {
if (await _libraryRepository.LibraryExists(createLibraryDto.Name)) if (await _unitOfWork.LibraryRepository.LibraryExists(createLibraryDto.Name))
{ {
return BadRequest("Library name already exists. Please choose a unique name to the server."); return BadRequest("Library name already exists. Please choose a unique name to the server.");
} }
var admins = (await _userRepository.GetAdminUsersAsync()).ToList(); var admins = (await _unitOfWork.UserRepository.GetAdminUsersAsync()).ToList();
var library = new Library var library = new Library
{ {
@ -72,15 +68,16 @@ namespace API.Controllers
} }
if (await _userRepository.SaveAllAsync()) if (!await _unitOfWork.Complete())
{ {
_logger.LogInformation($"Created a new library: {library.Name}"); return BadRequest("There was a critical issue. Please try again.");
var createdLibrary = await _libraryRepository.GetLibraryForNameAsync(library.Name);
BackgroundJob.Enqueue(() => _directoryService.ScanLibrary(createdLibrary.Id, false));
return Ok();
} }
return BadRequest("There was a critical issue. Please try again."); _logger.LogInformation($"Created a new library: {library.Name}");
var createdLibrary = await _unitOfWork.LibraryRepository.GetLibraryForNameAsync(library.Name);
BackgroundJob.Enqueue(() => _directoryService.ScanLibrary(createdLibrary.Id, false));
return Ok();
} }
/// <summary> /// <summary>
@ -105,14 +102,14 @@ namespace API.Controllers
[HttpGet] [HttpGet]
public async Task<ActionResult<IEnumerable<LibraryDto>>> GetLibraries() public async Task<ActionResult<IEnumerable<LibraryDto>>> GetLibraries()
{ {
return Ok(await _libraryRepository.GetLibrariesAsync()); return Ok(await _unitOfWork.LibraryRepository.GetLibrariesAsync());
} }
[Authorize(Policy = "RequireAdminRole")] [Authorize(Policy = "RequireAdminRole")]
[HttpPut("update-for")] [HttpPut("update-for")]
public async Task<ActionResult<MemberDto>> AddLibraryToUser(UpdateLibraryForUserDto updateLibraryForUserDto) public async Task<ActionResult<MemberDto>> AddLibraryToUser(UpdateLibraryForUserDto updateLibraryForUserDto)
{ {
var user = await _userRepository.GetUserByUsernameAsync(updateLibraryForUserDto.Username); var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(updateLibraryForUserDto.Username);
if (user == null) return BadRequest("Could not validate user"); if (user == null) return BadRequest("Could not validate user");
@ -123,7 +120,7 @@ namespace API.Controllers
user.Libraries.Add(_mapper.Map<Library>(selectedLibrary)); user.Libraries.Add(_mapper.Map<Library>(selectedLibrary));
} }
if (await _userRepository.SaveAllAsync()) if (await _unitOfWork.Complete())
{ {
_logger.LogInformation($"Added: {updateLibraryForUserDto.SelectedLibraries} to {updateLibraryForUserDto.Username}"); _logger.LogInformation($"Added: {updateLibraryForUserDto.SelectedLibraries} to {updateLibraryForUserDto.Username}");
return Ok(user); return Ok(user);
@ -143,7 +140,7 @@ namespace API.Controllers
[HttpGet("libraries-for")] [HttpGet("libraries-for")]
public async Task<ActionResult<IEnumerable<LibraryDto>>> GetLibrariesForUser(string username) public async Task<ActionResult<IEnumerable<LibraryDto>>> GetLibrariesForUser(string username)
{ {
return Ok(await _libraryRepository.GetLibrariesDtoForUsernameAsync(username)); return Ok(await _unitOfWork.LibraryRepository.GetLibrariesDtoForUsernameAsync(username));
} }
[HttpGet("series")] [HttpGet("series")]
@ -151,10 +148,10 @@ namespace API.Controllers
{ {
if (forUser) if (forUser)
{ {
var user = await _userRepository.GetUserByUsernameAsync(User.GetUsername()); var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
return Ok(await _seriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, user.Id)); return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, user.Id));
} }
return Ok(await _seriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId)); return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId));
} }
[Authorize(Policy = "RequireAdminRole")] [Authorize(Policy = "RequireAdminRole")]
@ -163,10 +160,10 @@ namespace API.Controllers
{ {
var username = User.GetUsername(); var username = User.GetUsername();
_logger.LogInformation($"Library {libraryId} is being deleted by {username}."); _logger.LogInformation($"Library {libraryId} is being deleted by {username}.");
var series = await _seriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId); var series = await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId);
var volumes = (await _seriesRepository.GetVolumesForSeriesAsync(series.Select(x => x.Id).ToArray())) var volumes = (await _unitOfWork.SeriesRepository.GetVolumesForSeriesAsync(series.Select(x => x.Id).ToArray()))
.Select(x => x.Id).ToArray(); .Select(x => x.Id).ToArray();
var result = await _libraryRepository.DeleteLibrary(libraryId); var result = await _unitOfWork.LibraryRepository.DeleteLibrary(libraryId);
if (result && volumes.Any()) if (result && volumes.Any())
{ {
@ -180,7 +177,7 @@ namespace API.Controllers
[HttpPost("update")] [HttpPost("update")]
public async Task<ActionResult> UpdateLibrary(UpdateLibraryDto libraryForUserDto) public async Task<ActionResult> UpdateLibrary(UpdateLibraryDto libraryForUserDto)
{ {
var library = await _libraryRepository.GetLibraryForIdAsync(libraryForUserDto.Id); var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryForUserDto.Id);
var originalFolders = library.Folders.Select(x => x.Path); var originalFolders = library.Folders.Select(x => x.Path);
var differenceBetweenFolders = originalFolders.Except(libraryForUserDto.Folders); var differenceBetweenFolders = originalFolders.Except(libraryForUserDto.Folders);
@ -190,9 +187,9 @@ namespace API.Controllers
_libraryRepository.Update(library); _unitOfWork.LibraryRepository.Update(library);
if (await _libraryRepository.SaveAllAsync()) if (await _unitOfWork.Complete())
{ {
if (differenceBetweenFolders.Any()) if (differenceBetweenFolders.Any())
{ {

View File

@ -6,9 +6,7 @@ using API.DTOs;
using API.Entities; using API.Entities;
using API.Extensions; using API.Extensions;
using API.Interfaces; using API.Interfaces;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace API.Controllers namespace API.Controllers
@ -18,22 +16,15 @@ namespace API.Controllers
private readonly IDirectoryService _directoryService; private readonly IDirectoryService _directoryService;
private readonly ICacheService _cacheService; private readonly ICacheService _cacheService;
private readonly ILogger<ReaderController> _logger; private readonly ILogger<ReaderController> _logger;
private readonly UserManager<AppUser> _userManager; private readonly IUnitOfWork _unitOfWork;
private readonly DataContext _dataContext; // TODO: Refactor code into repo
private readonly IUserRepository _userRepository;
private readonly ISeriesRepository _seriesRepository;
public ReaderController(IDirectoryService directoryService, ICacheService cacheService, public ReaderController(IDirectoryService directoryService, ICacheService cacheService,
ILogger<ReaderController> logger, UserManager<AppUser> userManager, DataContext dataContext, ILogger<ReaderController> logger, IUnitOfWork unitOfWork)
IUserRepository userRepository, ISeriesRepository seriesRepository)
{ {
_directoryService = directoryService; _directoryService = directoryService;
_cacheService = cacheService; _cacheService = cacheService;
_logger = logger; _logger = logger;
_userManager = userManager; _unitOfWork = unitOfWork;
_dataContext = dataContext;
_userRepository = userRepository;
_seriesRepository = seriesRepository;
} }
[HttpGet("image")] [HttpGet("image")]
@ -52,7 +43,7 @@ namespace API.Controllers
[HttpGet("get-bookmark")] [HttpGet("get-bookmark")]
public async Task<ActionResult<int>> GetBookmark(int volumeId) public async Task<ActionResult<int>> GetBookmark(int volumeId)
{ {
var user = await _userRepository.GetUserByUsernameAsync(User.GetUsername()); var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
if (user.Progresses == null) return Ok(0); if (user.Progresses == null) return Ok(0);
var progress = user.Progresses.SingleOrDefault(x => x.AppUserId == user.Id && x.VolumeId == volumeId); var progress = user.Progresses.SingleOrDefault(x => x.AppUserId == user.Id && x.VolumeId == volumeId);
@ -64,7 +55,7 @@ namespace API.Controllers
[HttpPost("bookmark")] [HttpPost("bookmark")]
public async Task<ActionResult> Bookmark(BookmarkDto bookmarkDto) public async Task<ActionResult> Bookmark(BookmarkDto bookmarkDto)
{ {
var user = await _userRepository.GetUserByUsernameAsync(User.GetUsername()); var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
_logger.LogInformation($"Saving {user.UserName} progress for {bookmarkDto.VolumeId} to page {bookmarkDto.PageNum}"); _logger.LogInformation($"Saving {user.UserName} progress for {bookmarkDto.VolumeId} to page {bookmarkDto.PageNum}");
user.Progresses ??= new List<AppUserProgress>(); user.Progresses ??= new List<AppUserProgress>();
@ -87,9 +78,9 @@ namespace API.Controllers
} }
_userRepository.Update(user); _unitOfWork.UserRepository.Update(user);
if (await _userRepository.SaveAllAsync()) if (await _unitOfWork.Complete())
{ {
return Ok(); return Ok();
} }

View File

@ -15,28 +15,23 @@ namespace API.Controllers
public class SeriesController : BaseApiController public class SeriesController : BaseApiController
{ {
private readonly ILogger<SeriesController> _logger; private readonly ILogger<SeriesController> _logger;
private readonly IMapper _mapper;
private readonly ITaskScheduler _taskScheduler; private readonly ITaskScheduler _taskScheduler;
private readonly ISeriesRepository _seriesRepository;
private readonly ICacheService _cacheService; private readonly ICacheService _cacheService;
private readonly IUserRepository _userRepository; private readonly IUnitOfWork _unitOfWork;
public SeriesController(ILogger<SeriesController> logger, IMapper mapper, public SeriesController(ILogger<SeriesController> logger, ITaskScheduler taskScheduler,
ITaskScheduler taskScheduler, ISeriesRepository seriesRepository, ICacheService cacheService, IUnitOfWork unitOfWork)
ICacheService cacheService, IUserRepository userRepository)
{ {
_logger = logger; _logger = logger;
_mapper = mapper;
_taskScheduler = taskScheduler; _taskScheduler = taskScheduler;
_seriesRepository = seriesRepository;
_cacheService = cacheService; _cacheService = cacheService;
_userRepository = userRepository; _unitOfWork = unitOfWork;
} }
[HttpGet("{seriesId}")] [HttpGet("{seriesId}")]
public async Task<ActionResult<SeriesDto>> GetSeries(int seriesId) public async Task<ActionResult<SeriesDto>> GetSeries(int seriesId)
{ {
return Ok(await _seriesRepository.GetSeriesDtoByIdAsync(seriesId)); return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId));
} }
[Authorize(Policy = "RequireAdminRole")] [Authorize(Policy = "RequireAdminRole")]
@ -44,9 +39,9 @@ namespace API.Controllers
public async Task<ActionResult<bool>> DeleteSeries(int seriesId) public async Task<ActionResult<bool>> DeleteSeries(int seriesId)
{ {
var username = User.GetUsername(); var username = User.GetUsername();
var volumes = (await _seriesRepository.GetVolumesForSeriesAsync(new []{seriesId})).Select(x => x.Id).ToArray(); var volumes = (await _unitOfWork.SeriesRepository.GetVolumesForSeriesAsync(new []{seriesId})).Select(x => x.Id).ToArray();
_logger.LogInformation($"Series {seriesId} is being deleted by {username}."); _logger.LogInformation($"Series {seriesId} is being deleted by {username}.");
var result = await _seriesRepository.DeleteSeriesAsync(seriesId); var result = await _unitOfWork.SeriesRepository.DeleteSeriesAsync(seriesId);
if (result) if (result)
{ {
@ -60,16 +55,16 @@ namespace API.Controllers
{ {
if (forUser) if (forUser)
{ {
var user = await _userRepository.GetUserByUsernameAsync(User.GetUsername()); var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
return Ok(await _seriesRepository.GetVolumesDtoAsync(seriesId, user.Id)); return Ok(await _unitOfWork.SeriesRepository.GetVolumesDtoAsync(seriesId, user.Id));
} }
return Ok(await _seriesRepository.GetVolumesDtoAsync(seriesId)); // TODO: Refactor out forUser = false since everything is user based return Ok(await _unitOfWork.SeriesRepository.GetVolumesDtoAsync(seriesId)); // TODO: Refactor out forUser = false since everything is user based
} }
[HttpGet("volume")] [HttpGet("volume")]
public async Task<ActionResult<VolumeDto>> GetVolume(int volumeId) public async Task<ActionResult<VolumeDto>> GetVolume(int volumeId)
{ {
return Ok(await _seriesRepository.GetVolumeDtoAsync(volumeId)); return Ok(await _unitOfWork.SeriesRepository.GetVolumeDtoAsync(volumeId));
} }
} }
} }

View File

@ -12,23 +12,21 @@ namespace API.Controllers
[Authorize] [Authorize]
public class UsersController : BaseApiController public class UsersController : BaseApiController
{ {
private readonly IUserRepository _userRepository; private readonly IUnitOfWork _unitOfWork;
private readonly ILibraryRepository _libraryRepository;
public UsersController(IUserRepository userRepository, ILibraryRepository libraryRepository) public UsersController(IUnitOfWork unitOfWork)
{ {
_userRepository = userRepository; _unitOfWork = unitOfWork;
_libraryRepository = libraryRepository;
} }
[Authorize(Policy = "RequireAdminRole")] [Authorize(Policy = "RequireAdminRole")]
[HttpDelete("delete-user")] [HttpDelete("delete-user")]
public async Task<ActionResult> DeleteUser(string username) public async Task<ActionResult> DeleteUser(string username)
{ {
var user = await _userRepository.GetUserByUsernameAsync(username); var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(username);
_userRepository.Delete(user); _unitOfWork.UserRepository.Delete(user);
if (await _userRepository.SaveAllAsync()) if (await _unitOfWork.Complete())
{ {
return Ok(); return Ok();
} }
@ -40,7 +38,7 @@ namespace API.Controllers
[HttpGet] [HttpGet]
public async Task<ActionResult<IEnumerable<MemberDto>>> GetUsers() public async Task<ActionResult<IEnumerable<MemberDto>>> GetUsers()
{ {
return Ok(await _userRepository.GetMembersAsync()); return Ok(await _unitOfWork.UserRepository.GetMembersAsync());
} }
[HttpGet("has-library-access")] [HttpGet("has-library-access")]
@ -48,11 +46,11 @@ namespace API.Controllers
{ {
// TODO: refactor this to use either userexists or usermanager // TODO: refactor this to use either userexists or usermanager
var user = await _userRepository.GetUserByUsernameAsync(User.GetUsername()); var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
if (user == null) return BadRequest("Could not validate user"); if (user == null) return BadRequest("Could not validate user");
var libs = await _libraryRepository.GetLibrariesDtoForUsernameAsync(user.UserName); var libs = await _unitOfWork.LibraryRepository.GetLibrariesDtoForUsernameAsync(user.UserName);
return Ok(libs.Any(x => x.Id == libraryId)); return Ok(libs.Any(x => x.Id == libraryId));
} }

View File

@ -2,17 +2,16 @@
{ {
public class SeriesDto public class SeriesDto
{ {
public int Id { get; set; } public int Id { get; init; }
public string Name { get; set; } public string Name { get; init; }
public string OriginalName { get; set; } public string OriginalName { get; init; }
public string SortName { get; set; } public string SortName { get; init; }
public string Summary { get; set; } public string Summary { get; init; }
public byte[] CoverImage { get; set; } public byte[] CoverImage { get; init; }
public int Pages { get; init; }
// Read Progress /// <summary>
public int Pages { get; set; } /// Sum of pages read from linked Volumes. Calculated at API-time.
/// </summary>
public int PagesRead { get; set; } public int PagesRead { get; set; }
//public int VolumesComplete { get; set; }
//public int TotalVolumes { get; set; }
} }
} }

36
API/Data/UnitOfWork.cs Normal file
View File

@ -0,0 +1,36 @@
using System.Threading.Tasks;
using API.Entities;
using API.Interfaces;
using AutoMapper;
using Microsoft.AspNetCore.Identity;
namespace API.Data
{
public class UnitOfWork : IUnitOfWork
{
private readonly DataContext _context;
private readonly IMapper _mapper;
private readonly UserManager<AppUser> _userManager;
public UnitOfWork(DataContext context, IMapper mapper, UserManager<AppUser> userManager)
{
_context = context;
_mapper = mapper;
_userManager = userManager;
}
public ISeriesRepository SeriesRepository => new SeriesRepository(_context, _mapper);
public IUserRepository UserRepository => new UserRepository(_context, _mapper, _userManager);
public ILibraryRepository LibraryRepository => new LibraryRepository(_context, _mapper);
public async Task<bool> Complete()
{
return await _context.SaveChangesAsync() > 0;
}
public bool HasChanges()
{
return _context.ChangeTracker.HasChanges();
}
}
}

View File

@ -12,11 +12,7 @@ namespace API.Entities
public DateTime Created { get; set; } = DateTime.Now; public DateTime Created { get; set; } = DateTime.Now;
public DateTime LastActive { get; set; } public DateTime LastActive { get; set; }
public ICollection<Library> Libraries { get; set; } public ICollection<Library> Libraries { get; set; }
public ICollection<AppUserRole> UserRoles { get; set; } public ICollection<AppUserRole> UserRoles { get; set; }
//public ICollection<SeriesProgress> SeriesProgresses { get; set; }
public ICollection<AppUserProgress> Progresses { get; set; } public ICollection<AppUserProgress> Progresses { get; set; }
[ConcurrencyCheck] [ConcurrencyCheck]

View File

@ -7,10 +7,11 @@
{ {
public int Id { get; set; } public int Id { get; set; }
public int PagesRead { get; set; } public int PagesRead { get; set; }
public AppUser AppUser { get; set; }
public int AppUserId { get; set; }
public int VolumeId { get; set; } public int VolumeId { get; set; }
public int SeriesId { get; set; } public int SeriesId { get; set; }
// Relationships
public AppUser AppUser { get; set; }
public int AppUserId { get; set; }
} }
} }

View File

@ -9,8 +9,6 @@ namespace API.Entities
[Description("Comic")] [Description("Comic")]
Comic = 1, Comic = 1,
[Description("Book")] [Description("Book")]
Book = 2, Book = 2
[Description("Raw")]
Raw = 3
} }
} }

View File

@ -4,9 +4,12 @@ namespace API.Entities
public class MangaFile public class MangaFile
{ {
public int Id { get; set; } public int Id { get; set; }
/// <summary>
/// Absolute path to the archive file
/// </summary>
public string FilePath { get; set; } public string FilePath { get; set; }
/// <summary> /// <summary>
/// Do not expect this to be set. If this MangaFile represents a volume file, this will be null. /// Used to track if multiple MangaFiles (archives) represent a single Volume. If only one volume file, this will be 0.
/// </summary> /// </summary>
public int Chapter { get; set; } public int Chapter { get; set; }
/// <summary> /// <summary>

View File

@ -30,10 +30,8 @@ namespace API.Entities
/// Sum of all Volume pages /// Sum of all Volume pages
/// </summary> /// </summary>
public int Pages { get; set; } public int Pages { get; set; }
/// <summary>
/// Total Volumes linked to Entity // Relationships
/// </summary>
//public int TotalVolumes { get; set; }
public ICollection<Volume> Volumes { get; set; } public ICollection<Volume> Volumes { get; set; }
public Library Library { get; set; } public Library Library { get; set; }
public int LibraryId { get; set; } public int LibraryId { get; set; }

View File

@ -17,7 +17,7 @@ namespace API.Entities
// Many-to-One relationships // Relationships
public Series Series { get; set; } public Series Series { get; set; }
public int SeriesId { get; set; } public int SeriesId { get; set; }
} }

View File

@ -2,9 +2,9 @@
{ {
public class ApiException public class ApiException
{ {
public int Status { get; set; } public int Status { get; init; }
public string Message { get; set; } public string Message { get; init; }
public string Details { get; set; } public string Details { get; init; }
public ApiException(int status, string message = null, string details = null) public ApiException(int status, string message = null, string details = null)
{ {

View File

@ -17,14 +17,13 @@ namespace API.Extensions
{ {
services.AddAutoMapper(typeof(AutoMapperProfiles).Assembly); services.AddAutoMapper(typeof(AutoMapperProfiles).Assembly);
services.AddScoped<ITaskScheduler, TaskScheduler>(); services.AddScoped<ITaskScheduler, TaskScheduler>();
services.AddScoped<IUserRepository, UserRepository>(); services.AddScoped<IDirectoryService, DirectoryService>();
services.AddScoped<ITokenService, TokenService>(); services.AddScoped<ITokenService, TokenService>();
services.AddScoped<ICacheService, CacheService>(); services.AddScoped<ICacheService, CacheService>();
services.AddScoped<ISeriesRepository, SeriesRepository>(); services.AddScoped<IUnitOfWork, UnitOfWork>();
services.AddScoped<IDirectoryService, DirectoryService>();
services.AddScoped<ILibraryRepository, LibraryRepository>();
services.AddDbContext<DataContext>(options => services.AddDbContext<DataContext>(options =>
{ {
options.UseSqlite(config.GetConnectionString("DefaultConnection")); options.UseSqlite(config.GetConnectionString("DefaultConnection"));

View File

@ -13,13 +13,8 @@ namespace API.Interfaces
/// <returns>List of folder names</returns> /// <returns>List of folder names</returns>
IEnumerable<string> ListDirectory(string rootPath); IEnumerable<string> ListDirectory(string rootPath);
/// <summary>
/// Lists out top-level files for a given directory. //IList<string> ListFiles(string rootPath);
/// TODO: Implement ability to provide a filter for file types (done in another implementation on DirectoryService)
/// </summary>
/// <param name="rootPath">Absolute path </param>
/// <returns>List of folder names</returns>
IList<string> ListFiles(string rootPath);
/// <summary> /// <summary>
/// Given a library id, scans folders for said library. Parses files and generates DB updates. Will overwrite /// Given a library id, scans folders for said library. Parses files and generates DB updates. Will overwrite

View File

@ -8,7 +8,6 @@ namespace API.Interfaces
public interface ILibraryRepository public interface ILibraryRepository
{ {
void Update(Library library); void Update(Library library);
Task<bool> SaveAllAsync();
Task<IEnumerable<LibraryDto>> GetLibrariesAsync(); Task<IEnumerable<LibraryDto>> GetLibrariesAsync();
Task<bool> LibraryExists(string libraryName); Task<bool> LibraryExists(string libraryName);
Task<Library> GetLibraryForIdAsync(int libraryId); Task<Library> GetLibraryForIdAsync(int libraryId);

View File

@ -8,17 +8,15 @@ namespace API.Interfaces
public interface ISeriesRepository public interface ISeriesRepository
{ {
void Update(Series series); void Update(Series series);
Task<bool> SaveAllAsync();
Task<Series> GetSeriesByNameAsync(string name); Task<Series> GetSeriesByNameAsync(string name);
Series GetSeriesByName(string name); Series GetSeriesByName(string name);
bool SaveAll();
Task<IEnumerable<SeriesDto>> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId = 0); Task<IEnumerable<SeriesDto>> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId = 0);
Task<IEnumerable<VolumeDto>> GetVolumesDtoAsync(int seriesId, int userId = 0); Task<IEnumerable<VolumeDto>> GetVolumesDtoAsync(int seriesId, int userId = 0);
IEnumerable<Volume> GetVolumes(int seriesId); IEnumerable<Volume> GetVolumes(int seriesId);
Task<SeriesDto> GetSeriesDtoByIdAsync(int seriesId); Task<SeriesDto> GetSeriesDtoByIdAsync(int seriesId);
Task<Volume> GetVolumeAsync(int volumeId); Task<Volume> GetVolumeAsync(int volumeId);
Task<VolumeDto> GetVolumeDtoAsync(int volumeId); // TODO: Likely need to update here Task<VolumeDto> GetVolumeDtoAsync(int volumeId);
Task<IEnumerable<Volume>> GetVolumesForSeriesAsync(int[] seriesIds); Task<IEnumerable<Volume>> GetVolumesForSeriesAsync(int[] seriesIds);
Task<bool> DeleteSeriesAsync(int seriesId); Task<bool> DeleteSeriesAsync(int seriesId);

View File

@ -0,0 +1,13 @@
using System.Threading.Tasks;
namespace API.Interfaces
{
public interface IUnitOfWork
{
ISeriesRepository SeriesRepository { get; }
IUserRepository UserRepository { get; }
ILibraryRepository LibraryRepository { get; }
Task<bool> Complete();
bool HasChanges();
}
}

View File

@ -8,12 +8,8 @@ namespace API.Interfaces
public interface IUserRepository public interface IUserRepository
{ {
void Update(AppUser user); void Update(AppUser user);
Task<bool> SaveAllAsync();
Task<IEnumerable<AppUser>> GetUsersAsync();
Task<AppUser> GetUserByIdAsync(int id);
Task<AppUser> GetUserByUsernameAsync(string username); Task<AppUser> GetUserByUsernameAsync(string username);
Task<IEnumerable<MemberDto>> GetMembersAsync(); Task<IEnumerable<MemberDto>> GetMembersAsync();
Task<MemberDto> GetMemberAsync(string username);
public void Delete(AppUser user); public void Delete(AppUser user);
Task<IEnumerable<AppUser>> GetAdminUsersAsync(); Task<IEnumerable<AppUser>> GetAdminUsersAsync();
} }

View File

@ -13,16 +13,16 @@ namespace API.Services
public class CacheService : ICacheService public class CacheService : ICacheService
{ {
private readonly IDirectoryService _directoryService; private readonly IDirectoryService _directoryService;
private readonly ISeriesRepository _seriesRepository;
private readonly ILogger<CacheService> _logger; private readonly ILogger<CacheService> _logger;
private readonly IUnitOfWork _unitOfWork;
private readonly NumericComparer _numericComparer; private readonly NumericComparer _numericComparer;
private readonly string _cacheDirectory = Path.GetFullPath(Path.Join(Directory.GetCurrentDirectory(), "../cache/")); private readonly string _cacheDirectory = Path.GetFullPath(Path.Join(Directory.GetCurrentDirectory(), "../cache/"));
public CacheService(IDirectoryService directoryService, ISeriesRepository seriesRepository, ILogger<CacheService> logger) public CacheService(IDirectoryService directoryService, ILogger<CacheService> logger, IUnitOfWork unitOfWork)
{ {
_directoryService = directoryService; _directoryService = directoryService;
_seriesRepository = seriesRepository;
_logger = logger; _logger = logger;
_unitOfWork = unitOfWork;
_numericComparer = new NumericComparer(); _numericComparer = new NumericComparer();
} }
@ -38,7 +38,7 @@ namespace API.Services
{ {
return null; return null;
} }
Volume volume = await _seriesRepository.GetVolumeAsync(volumeId); Volume volume = await _unitOfWork.SeriesRepository.GetVolumeAsync(volumeId);
foreach (var file in volume.Files) foreach (var file in volume.Files)
{ {
var extractPath = GetVolumeCachePath(volumeId, file); var extractPath = GetVolumeCachePath(volumeId, file);
@ -109,8 +109,7 @@ namespace API.Services
if (page + 1 < (mangaFile.NumberOfPages + pagesSoFar)) if (page + 1 < (mangaFile.NumberOfPages + pagesSoFar))
{ {
var path = GetVolumeCachePath(volume.Id, mangaFile); var path = GetVolumeCachePath(volume.Id, mangaFile);
var files = DirectoryService.GetFiles(path);
var files = _directoryService.ListFiles(path);
var array = files.ToArray(); var array = files.ToArray();
Array.Sort(array, _numericComparer); // TODO: Find a way to apply numericComparer to IList. Array.Sort(array, _numericComparer); // TODO: Find a way to apply numericComparer to IList.

View File

@ -23,31 +23,28 @@ namespace API.Services
public class DirectoryService : IDirectoryService public class DirectoryService : IDirectoryService
{ {
private readonly ILogger<DirectoryService> _logger; private readonly ILogger<DirectoryService> _logger;
private readonly ISeriesRepository _seriesRepository; private readonly IUnitOfWork _unitOfWork;
private readonly ILibraryRepository _libraryRepository;
private ConcurrentDictionary<string, ConcurrentBag<ParserInfo>> _scannedSeries; private ConcurrentDictionary<string, ConcurrentBag<ParserInfo>> _scannedSeries;
public DirectoryService(ILogger<DirectoryService> logger, public DirectoryService(ILogger<DirectoryService> logger, IUnitOfWork unitOfWork)
ISeriesRepository seriesRepository,
ILibraryRepository libraryRepository)
{ {
_logger = logger; _logger = logger;
_seriesRepository = seriesRepository; _unitOfWork = unitOfWork;
_libraryRepository = libraryRepository;
} }
/// <summary> /// <summary>
/// Given a set of regex search criteria, get files in the given path. /// Given a set of regex search criteria, get files in the given path.
/// </summary> /// </summary>
/// <param name="path">Directory to search</param> /// <param name="path">Directory to search</param>
/// <param name="searchPatternExpression">Regex version of search pattern (ie \.mp3|\.mp4)</param> /// <param name="searchPatternExpression">Regex version of search pattern (ie \.mp3|\.mp4). Defaults to * meaning all files.</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>
private static IEnumerable<string> GetFiles(string path, public static IEnumerable<string> GetFiles(string path,
string searchPatternExpression = "", string searchPatternExpression = "*",
SearchOption searchOption = SearchOption.TopDirectoryOnly) SearchOption searchOption = SearchOption.TopDirectoryOnly)
{ {
if (!Directory.Exists(path)) return ImmutableList<string>.Empty;
var 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 =>
@ -67,11 +64,11 @@ namespace API.Services
return dirs; return dirs;
} }
public IList<string> ListFiles(string rootPath) // public IList<string> ListFiles(string rootPath)
{ // {
if (!Directory.Exists(rootPath)) return ImmutableList<string>.Empty; // if (!Directory.Exists(rootPath)) return ImmutableList<string>.Empty;
return Directory.GetFiles(rootPath); // return Directory.GetFiles(rootPath);
} // }
/// <summary> /// <summary>
@ -114,7 +111,7 @@ namespace API.Services
private Series UpdateSeries(string seriesName, ParserInfo[] infos, bool forceUpdate) private Series UpdateSeries(string seriesName, ParserInfo[] infos, bool forceUpdate)
{ {
var series = _seriesRepository.GetSeriesByName(seriesName) ?? new Series var series = _unitOfWork.SeriesRepository.GetSeriesByName(seriesName) ?? new Series
{ {
Name = seriesName, Name = seriesName,
OriginalName = seriesName, OriginalName = seriesName,
@ -126,7 +123,7 @@ namespace API.Services
series.Volumes = volumes; series.Volumes = volumes;
series.CoverImage = volumes.OrderBy(x => x.Number).FirstOrDefault()?.CoverImage; series.CoverImage = volumes.OrderBy(x => x.Number).FirstOrDefault()?.CoverImage;
series.Pages = volumes.Sum(v => v.Pages); series.Pages = volumes.Sum(v => v.Pages);
//series.TotalVolumes = volumes.Count;
return series; return series;
} }
@ -154,7 +151,7 @@ namespace API.Services
private ICollection<Volume> UpdateVolumes(Series series, ParserInfo[] infos, bool forceUpdate) private ICollection<Volume> UpdateVolumes(Series series, ParserInfo[] infos, bool forceUpdate)
{ {
ICollection<Volume> volumes = new List<Volume>(); ICollection<Volume> volumes = new List<Volume>();
IList<Volume> existingVolumes = _seriesRepository.GetVolumes(series.Id).ToList(); IList<Volume> existingVolumes = _unitOfWork.SeriesRepository.GetVolumes(series.Id).ToList();
foreach (var info in infos) foreach (var info in infos)
{ {
@ -220,7 +217,7 @@ namespace API.Services
Library library; Library library;
try try
{ {
library = Task.Run(() => _libraryRepository.GetLibraryForIdAsync(libraryId)).Result; library = Task.Run(() => _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId)).Result;
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -264,12 +261,10 @@ namespace API.Services
_logger.LogInformation($"Created/Updated series {mangaSeries.Name}"); _logger.LogInformation($"Created/Updated series {mangaSeries.Name}");
library.Series.Add(mangaSeries); library.Series.Add(mangaSeries);
} }
_unitOfWork.LibraryRepository.Update(library);
if (Task.Run(() => _unitOfWork.Complete()).Result)
_libraryRepository.Update(library);
if (_libraryRepository.SaveAll())
{ {
_logger.LogInformation($"Scan completed on {library.Name}. Parsed {series.Keys.Count()} series."); _logger.LogInformation($"Scan completed on {library.Name}. Parsed {series.Keys.Count()} series.");
} }
@ -287,46 +282,6 @@ namespace API.Services
return Path.Join(Directory.GetCurrentDirectory(), $"../cache/{volumeId}/"); return Path.Join(Directory.GetCurrentDirectory(), $"../cache/{volumeId}/");
} }
/// <summary>
/// TODO: Delete this method
/// </summary>
/// <param name="archivePath"></param>
/// <param name="volumeId"></param>
/// <returns></returns>
private string ExtractArchive(string archivePath, int volumeId)
{
if (!File.Exists(archivePath) || !Parser.Parser.IsArchive(archivePath))
{
_logger.LogError($"Archive {archivePath} could not be found.");
return "";
}
var extractPath = GetExtractPath(volumeId);
if (Directory.Exists(extractPath))
{
_logger.LogInformation($"Archive {archivePath} has already been extracted. Returning existing folder.");
return extractPath;
}
using ZipArchive archive = ZipFile.OpenRead(archivePath);
// TODO: Throw error if we couldn't extract
var needsFlattening = archive.Entries.Count > 0 && !Path.HasExtension(archive.Entries.ElementAt(0).FullName);
if (!archive.HasFiles() && !needsFlattening) return "";
archive.ExtractToDirectory(extractPath);
_logger.LogInformation($"Extracting archive to {extractPath}");
if (needsFlattening)
{
_logger.LogInformation("Extracted archive is nested in root folder, flattening...");
new DirectoryInfo(extractPath).Flatten();
}
return extractPath;
}
public string ExtractArchive(string archivePath, string extractPath) public string ExtractArchive(string archivePath, string extractPath)
{ {
if (!File.Exists(archivePath) || !Parser.Parser.IsArchive(archivePath)) if (!File.Exists(archivePath) || !Parser.Parser.IsArchive(archivePath))

View File

@ -16,6 +16,7 @@ namespace API.Services
_logger.LogInformation("Scheduling/Updating cache cleanup on a daily basis."); _logger.LogInformation("Scheduling/Updating cache cleanup on a daily basis.");
RecurringJob.AddOrUpdate(() => cacheService.Cleanup(), Cron.Daily); RecurringJob.AddOrUpdate(() => cacheService.Cleanup(), Cron.Daily);
//RecurringJob.AddOrUpdate(() => scanService.ScanLibraries(), Cron.Daily);
} }