.NET 6 Coding Patterns + Unit Tests (#823)

* Refactored all files to have Interfaces within the same file. Started moving over to file-scoped namespaces.

* Refactored common methods for getting underlying file's cover, pages, and extracting into 1 interface.

* More refactoring around removing dependence on explicit filetype testing for getting information.

* Code is buildable, tests are broken. Huge refactor (not completed) which makes most of DirectoryService testable with a mock filesystem (and thus the services that utilize it).

* Finished porting DirectoryService to use mocked filesystem implementation.

* Added a null check

* Added a null check

* Finished all unit tests for DirectoryService.

* Some misc cleanup on the code

* Fixed up some bugs from refactoring scan loop.

* Implemented CleanupService testing and refactored more of DirectoryService to be non-static.

Fixed a bug where cover file cleanup wasn't properly finding files due to a regex bug.

* Fixed an issue in CleanupBackup() where we weren't properly selecting database files older than 30 days. Finished CleanupService Tests.

* Refactored Flatten and RemoveNonImages to directory service to allow CacheService to be testable.

* Finally have CacheService tested. Rewrote GetCachedPagePath() to be much more straightforward & performant.

* Updated DefaultParserTests.cs to contain all existing tests and follow new test layout format.

* All tests fixed up
This commit is contained in:
Joseph Milazzo 2021-12-05 10:58:53 -06:00 committed by GitHub
parent bf1876ff44
commit bbe8f800f6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
115 changed files with 6734 additions and 5370 deletions

View file

@ -4,12 +4,11 @@ using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using API.Constants;
using API.Data;
using API.DTOs;
using API.DTOs.Account;
using API.Entities;
using API.Extensions;
using API.Interfaces;
using API.Interfaces.Services;
using API.Services;
using AutoMapper;
using Kavita.Common;

View file

@ -1,12 +1,11 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using API.Data;
using API.DTOs;
using API.DTOs.Reader;
using API.Entities.Enums;
using API.Extensions;
using API.Interfaces;
using API.Interfaces.Services;
using API.Services;
using HtmlAgilityPack;
using Microsoft.AspNetCore.Mvc;

View file

@ -4,10 +4,8 @@ using System.Linq;
using System.Threading.Tasks;
using API.Data;
using API.DTOs.CollectionTags;
using API.Entities;
using API.Entities.Metadata;
using API.Extensions;
using API.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

View file

@ -4,12 +4,11 @@ using System.IO;
using System.Linq;
using System.Threading.Tasks;
using API.Comparators;
using API.Data;
using API.DTOs.Downloads;
using API.Entities;
using API.Entities.Enums;
using API.Extensions;
using API.Interfaces;
using API.Interfaces.Services;
using API.Services;
using API.SignalR;
using Kavita.Common;
@ -47,21 +46,21 @@ namespace API.Controllers
public async Task<ActionResult<long>> GetVolumeSize(int volumeId)
{
var files = await _unitOfWork.VolumeRepository.GetFilesForVolume(volumeId);
return Ok(DirectoryService.GetTotalSize(files.Select(c => c.FilePath)));
return Ok(_directoryService.GetTotalSize(files.Select(c => c.FilePath)));
}
[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)));
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)));
return Ok(_directoryService.GetTotalSize(files.Select(c => c.FilePath)));
}
[HttpGet("volume")]
@ -141,13 +140,13 @@ namespace API.Controllers
var totalFilePaths = new List<string>();
var tempFolder = $"download_{series.Id}_bookmarks";
var fullExtractPath = Path.Join(DirectoryService.TempDirectory, tempFolder);
if (new DirectoryInfo(fullExtractPath).Exists)
var fullExtractPath = Path.Join(_directoryService.TempDirectory, tempFolder);
if (_directoryService.FileSystem.DirectoryInfo.FromDirectoryName(fullExtractPath).Exists)
{
return BadRequest(
"Server is currently processing this exact download. Please try again in a few minutes.");
}
DirectoryService.ExistOrCreate(fullExtractPath);
_directoryService.ExistOrCreate(fullExtractPath);
var uniqueChapterIds = downloadBookmarkDto.Bookmarks.Select(b => b.ChapterId).Distinct().ToList();
@ -160,16 +159,16 @@ namespace API.Controllers
switch (series.Format)
{
case MangaFormat.Image:
DirectoryService.ExistOrCreate(chapterExtractPath);
_directoryService.ExistOrCreate(chapterExtractPath);
_directoryService.CopyFilesToDirectory(mangaFiles.Select(f => f.FilePath), chapterExtractPath, $"{chapterId}_");
break;
case MangaFormat.Archive:
case MangaFormat.Pdf:
_cacheService.ExtractChapterFiles(chapterExtractPath, mangaFiles.ToList());
var originalFiles = DirectoryService.GetFilesWithExtension(chapterExtractPath,
var originalFiles = _directoryService.GetFilesWithExtension(chapterExtractPath,
Parser.Parser.ImageFileExtensions);
_directoryService.CopyFilesToDirectory(originalFiles, chapterExtractPath, $"{chapterId}_");
DirectoryService.DeleteFiles(originalFiles);
_directoryService.DeleteFiles(originalFiles);
break;
case MangaFormat.Epub:
return BadRequest("Series is not in a valid format.");
@ -177,7 +176,7 @@ namespace API.Controllers
return BadRequest("Series is not in a valid format. Please rescan series and try again.");
}
var files = DirectoryService.GetFilesWithExtension(chapterExtractPath, Parser.Parser.ImageFileExtensions);
var files = _directoryService.GetFilesWithExtension(chapterExtractPath, Parser.Parser.ImageFileExtensions);
// Filter out images that aren't in bookmarks
Array.Sort(files, _numericComparer);
totalFilePaths.AddRange(files.Where((_, i) => chapterPages.Contains(i)));
@ -186,7 +185,7 @@ namespace API.Controllers
var (fileBytes, _) = await _archiveService.CreateZipForDownload(totalFilePaths,
tempFolder);
DirectoryService.ClearAndDeleteDirectory(fullExtractPath);
_directoryService.ClearAndDeleteDirectory(fullExtractPath);
return File(fileBytes, DefaultContentType, $"{series.Name} - Bookmarks.zip");
}

View file

@ -1,5 +1,5 @@
using System.IO;
using API.Interfaces;
using API.Services;
using Microsoft.AspNetCore.Mvc;
namespace API.Controllers
@ -12,7 +12,7 @@ namespace API.Controllers
public FallbackController(ITaskScheduler taskScheduler)
{
// This is used to load TaskScheduler on startup without having to navigate to a Controller that uses.
// This is used to load TaskScheduler on startup without having to navigate to a Controller that uses.
_taskScheduler = taskScheduler;
}
@ -21,4 +21,4 @@ namespace API.Controllers
return PhysicalFile(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "index.html"), "text/HTML");
}
}
}
}

View file

@ -1,7 +1,7 @@
using System.IO;
using System.Threading.Tasks;
using API.Data;
using API.Extensions;
using API.Interfaces;
using API.Services;
using Microsoft.AspNetCore.Mvc;
@ -13,11 +13,13 @@ namespace API.Controllers
public class ImageController : BaseApiController
{
private readonly IUnitOfWork _unitOfWork;
private readonly IDirectoryService _directoryService;
/// <inheritdoc />
public ImageController(IUnitOfWork unitOfWork)
public ImageController(IUnitOfWork unitOfWork, IDirectoryService directoryService)
{
_unitOfWork = unitOfWork;
_directoryService = directoryService;
}
/// <summary>
@ -28,12 +30,12 @@ namespace API.Controllers
[HttpGet("chapter-cover")]
public async Task<ActionResult> GetChapterCoverImage(int chapterId)
{
var path = Path.Join(DirectoryService.CoverImageDirectory, await _unitOfWork.ChapterRepository.GetChapterCoverImageAsync(chapterId));
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"No cover image");
var format = Path.GetExtension(path).Replace(".", "");
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.ChapterRepository.GetChapterCoverImageAsync(chapterId));
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", "");
Response.AddCacheHeader(path);
return PhysicalFile(path, "image/" + format, Path.GetFileName(path));
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
}
/// <summary>
@ -44,12 +46,12 @@ namespace API.Controllers
[HttpGet("volume-cover")]
public async Task<ActionResult> GetVolumeCoverImage(int volumeId)
{
var path = Path.Join(DirectoryService.CoverImageDirectory, await _unitOfWork.VolumeRepository.GetVolumeCoverImageAsync(volumeId));
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"No cover image");
var format = Path.GetExtension(path).Replace(".", "");
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.VolumeRepository.GetVolumeCoverImageAsync(volumeId));
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", "");
Response.AddCacheHeader(path);
return PhysicalFile(path, "image/" + format, Path.GetFileName(path));
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
}
/// <summary>
@ -60,12 +62,12 @@ namespace API.Controllers
[HttpGet("series-cover")]
public async Task<ActionResult> GetSeriesCoverImage(int seriesId)
{
var path = Path.Join(DirectoryService.CoverImageDirectory, await _unitOfWork.SeriesRepository.GetSeriesCoverImageAsync(seriesId));
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"No cover image");
var format = Path.GetExtension(path).Replace(".", "");
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.SeriesRepository.GetSeriesCoverImageAsync(seriesId));
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", "");
Response.AddCacheHeader(path);
return PhysicalFile(path, "image/" + format, Path.GetFileName(path));
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
}
/// <summary>
@ -76,12 +78,12 @@ namespace API.Controllers
[HttpGet("collection-cover")]
public async Task<ActionResult> GetCollectionCoverImage(int collectionTagId)
{
var path = Path.Join(DirectoryService.CoverImageDirectory, await _unitOfWork.CollectionTagRepository.GetCoverImageAsync(collectionTagId));
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"No cover image");
var format = Path.GetExtension(path).Replace(".", "");
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.CollectionTagRepository.GetCoverImageAsync(collectionTagId));
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", "");
Response.AddCacheHeader(path);
return PhysicalFile(path, "image/" + format, Path.GetFileName(path));
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
}
}
}

View file

@ -3,13 +3,13 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using API.Data;
using API.Data.Repositories;
using API.DTOs;
using API.Entities;
using API.Entities.Enums;
using API.Extensions;
using API.Interfaces;
using API.Interfaces.Services;
using API.Services;
using AutoMapper;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
using System.Threading.Tasks;
using API.Data;
using API.DTOs;
using API.Interfaces;
using API.Interfaces.Services;
using API.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

View file

@ -3,13 +3,13 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using API.Data;
using API.Data.Repositories;
using API.DTOs;
using API.DTOs.Reader;
using API.Entities;
using API.Extensions;
using API.Interfaces;
using API.Interfaces.Services;
using API.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
@ -50,7 +50,7 @@ namespace API.Controllers
try
{
var (path, _) = await _cacheService.GetCachedPagePath(chapter, page);
var path = _cacheService.GetCachedPagePath(chapter, page);
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"No such image for page {page}");
var format = Path.GetExtension(path).Replace(".", "");
@ -90,7 +90,7 @@ namespace API.Controllers
LibraryId = dto.LibraryId,
IsSpecial = dto.IsSpecial,
Pages = dto.Pages,
ChapterTitle = dto.ChapterTitle
ChapterTitle = dto.ChapterTitle ?? string.Empty
});
}

View file

@ -2,11 +2,11 @@
using System.Linq;
using System.Threading.Tasks;
using API.Comparators;
using API.Data;
using API.DTOs.ReadingLists;
using API.Entities;
using API.Extensions;
using API.Helpers;
using API.Interfaces;
using Microsoft.AspNetCore.Mvc;
namespace API.Controllers

View file

@ -6,11 +6,10 @@ using API.Data;
using API.Data.Repositories;
using API.DTOs;
using API.DTOs.Filtering;
using API.DTOs.Metadata;
using API.Entities;
using API.Extensions;
using API.Helpers;
using API.Interfaces;
using API.Services;
using API.SignalR;
using Kavita.Common;
using Microsoft.AspNetCore.Authorization;

View file

@ -5,7 +5,8 @@ using System.Threading.Tasks;
using API.DTOs.Stats;
using API.DTOs.Update;
using API.Extensions;
using API.Interfaces.Services;
using API.Services;
using API.Services.Tasks;
using Kavita.Common;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@ -26,10 +27,11 @@ namespace API.Controllers
private readonly ICacheService _cacheService;
private readonly IVersionUpdaterService _versionUpdaterService;
private readonly IStatsService _statsService;
private readonly ICleanupService _cleanupService;
public ServerController(IHostApplicationLifetime applicationLifetime, ILogger<ServerController> logger, IConfiguration config,
IBackupService backupService, IArchiveService archiveService, ICacheService cacheService,
IVersionUpdaterService versionUpdaterService, IStatsService statsService)
IVersionUpdaterService versionUpdaterService, IStatsService statsService, ICleanupService cleanupService)
{
_applicationLifetime = applicationLifetime;
_logger = logger;
@ -39,6 +41,7 @@ namespace API.Controllers
_cacheService = cacheService;
_versionUpdaterService = versionUpdaterService;
_statsService = statsService;
_cleanupService = cleanupService;
}
/// <summary>
@ -62,7 +65,7 @@ namespace API.Controllers
public ActionResult ClearCache()
{
_logger.LogInformation("{UserName} is clearing cache of server from admin dashboard", User.GetUsername());
_cacheService.Cleanup();
_cleanupService.CleanupCacheDirectory();
return Ok();
}
@ -93,7 +96,7 @@ namespace API.Controllers
[HttpGet("logs")]
public async Task<ActionResult> GetLogs()
{
var files = _backupService.LogFiles(_config.GetMaxRollingFiles(), _config.GetLoggingFileName());
var files = _backupService.GetLogFiles(_config.GetMaxRollingFiles(), _config.GetLoggingFileName());
try
{
var (fileBytes, zipPath) = await _archiveService.CreateZipForDownload(files, "logs");

View file

@ -3,12 +3,11 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using API.Data;
using API.DTOs.Settings;
using API.Entities.Enums;
using API.Extensions;
using API.Helpers.Converters;
using API.Interfaces;
using API.Interfaces.Services;
using API.Services;
using Kavita.Common;
using Kavita.Common.Extensions;

View file

@ -1,8 +1,7 @@
using System;
using System.Threading.Tasks;
using API.Data;
using API.DTOs.Uploads;
using API.Interfaces;
using API.Interfaces.Services;
using API.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

View file

@ -1,10 +1,10 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using API.Data;
using API.Data.Repositories;
using API.DTOs;
using API.Extensions;
using API.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;