updated controllers and some services to use async/await

This commit is contained in:
Cameron
2024-08-29 05:36:01 -05:00
parent 18de6d599b
commit 8302e3ea61
36 changed files with 1981 additions and 1809 deletions

View File

@@ -1,8 +1,8 @@
using Microsoft.EntityFrameworkCore;
using ComiServ.Entities;
namespace ComiServ
{
namespace ComiServ;
public class ComicsContext : DbContext
{
//TODO is this the best place for this to live?
@@ -78,4 +78,3 @@ namespace ComiServ
return handle.ToUpper();
}
}
}

View File

@@ -12,10 +12,11 @@ using ComiServ.Extensions;
using System.Runtime.InteropServices;
using ComiServ.Services;
using System.Security.Cryptography.X509Certificates;
using System.Data;
using SQLitePCL;
namespace ComiServ.Controllers
{
namespace ComiServ.Controllers;
[Route(ROUTE)]
[ApiController]
public class ComicController(ComicsContext context, ILogger<ComicController> logger, IConfigService config, IComicAnalyzer analyzer, IPictureConverter converter, IAuthenticationService _auth)
@@ -31,7 +32,7 @@ namespace ComiServ.Controllers
//TODO search parameters
[HttpGet]
[ProducesResponseType<Paginated<ComicData>>(StatusCodes.Status200OK)]
public IActionResult SearchComics(
public async Task<IActionResult> SearchComics(
[FromQuery(Name = "TitleSearch")]
string? titleSearch,
[FromQuery(Name = "DescriptionSearch")]
@@ -143,16 +144,16 @@ namespace ComiServ.Controllers
results = results.Where(c => EF.Functions.Like(c.Description, $"%{descSearch}%"));
}
int offset = page * pageSize;
return Ok(new Paginated<ComicData>(pageSize, page, results
return Ok(await Paginated<ComicData>.CreateAsync(pageSize, page, results
.OrderBy(c => c.Id)
.Select(c => new ComicData(c))));
}
[HttpDelete]
[ProducesResponseType(StatusCodes.Status200OK)]
public IActionResult DeleteComicsThatDontExist()
public async Task<IActionResult> DeleteComicsThatDontExist()
{
var search = _context.Comics.Where(c => !c.Exists);
var nonExtant = search.ToList();
var nonExtant = await search.ToListAsync();
search.ExecuteDelete();
_context.SaveChanges();
return Ok(search.Select(c => new ComicData(c)));
@@ -161,16 +162,16 @@ namespace ComiServ.Controllers
[ProducesResponseType<ComicData>(StatusCodes.Status200OK)]
[ProducesResponseType<RequestError>(StatusCodes.Status400BadRequest)]
[ProducesResponseType<RequestError>(StatusCodes.Status404NotFound)]
public IActionResult GetSingleComicInfo(string handle)
public async Task<IActionResult> GetSingleComicInfo(string handle)
{
//_logger.LogInformation("GetSingleComicInfo: {handle}", handle);
var validated = ComicsContext.CleanValidateHandle(handle);
if (validated is null)
return BadRequest(RequestError.InvalidHandle);
var comic = _context.Comics
var comic = await _context.Comics
.Include("ComicAuthors.Author")
.Include("ComicTags.Tag")
.SingleOrDefault(c => c.Handle == handle);
.SingleOrDefaultAsync(c => c.Handle == handle);
if (comic is Comic actualComic)
return Ok(new ComicData(comic));
else
@@ -182,11 +183,11 @@ namespace ComiServ.Controllers
[ProducesResponseType<RequestError>(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType<RequestError>(StatusCodes.Status404NotFound)]
public IActionResult UpdateComicMetadata(string handle, [FromBody] ComicMetadataUpdateRequest metadata)
public async Task<IActionResult> UpdateComicMetadata(string handle, [FromBody] ComicMetadataUpdateRequest metadata)
{
if (handle.Length != ComicsContext.HANDLE_LENGTH)
return BadRequest(RequestError.InvalidHandle);
var comic = _context.Comics.SingleOrDefault(c => c.Handle == handle);
var comic = await _context.Comics.SingleOrDefaultAsync(c => c.Handle == handle);
if (comic is Comic actualComic)
{
if (metadata.Title != null)
@@ -196,7 +197,7 @@ namespace ComiServ.Controllers
//make sure all authors exist, without changing Id of pre-existing authors
_context.InsertOrIgnore(authors.Select(author => new Author() { Name = author }), ignorePrimaryKey: true);
//get the Id of needed authors
var authorEntities = _context.Authors.Where(a => authors.Contains(a.Name)).ToList();
var authorEntities = await _context.Authors.Where(a => authors.Contains(a.Name)).ToListAsync();
//delete existing author mappings
_context.ComicAuthors.RemoveRange(_context.ComicAuthors.Where(ca => ca.Comic.Id == comic.Id));
//add all author mappings
@@ -207,13 +208,13 @@ namespace ComiServ.Controllers
//make sure all tags exist, without changing Id of pre-existing tags
_context.InsertOrIgnore(tags.Select(t => new Tag() { Name = t }), ignorePrimaryKey: true);
//get the needed tags
var tagEntities = _context.Tags.Where(t => tags.Contains(t.Name)).ToList();
var tagEntities = await _context.Tags.Where(t => tags.Contains(t.Name)).ToListAsync();
//delete existing tag mappings
_context.ComicTags.RemoveRange(_context.ComicTags.Where(ta => ta.Comic.Id == comic.Id));
//add all tag mappings
_context.ComicTags.AddRange(tagEntities.Select(t => new ComicTag { Comic = comic, Tag = t }));
}
_context.SaveChanges();
await _context.SaveChangesAsync();
return Ok();
}
else
@@ -223,14 +224,14 @@ namespace ComiServ.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType<RequestError>(StatusCodes.Status400BadRequest)]
[ProducesResponseType<RequestError>(StatusCodes.Status401Unauthorized)]
public IActionResult MarkComicAsRead(
public async Task<IActionResult> MarkComicAsRead(
ComicsContext context,
string handle)
{
var validated = ComicsContext.CleanValidateHandle(handle);
if (validated is null)
return BadRequest(RequestError.InvalidHandle);
var comic = context.Comics.SingleOrDefault(c => c.Handle == validated);
var comic = await context.Comics.SingleOrDefaultAsync(c => c.Handle == validated);
if (comic is null)
return NotFound(RequestError.ComicNotFound);
if (_auth.User is null)
@@ -242,32 +243,32 @@ namespace ComiServ.Controllers
ComicId = comic.Id
};
context.InsertOrIgnore(comicRead, ignorePrimaryKey: false);
context.SaveChanges();
await context.SaveChangesAsync();
return Ok();
}
[HttpPatch("{handle}/markunread")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType<RequestError>(StatusCodes.Status400BadRequest)]
[ProducesResponseType<RequestError>(StatusCodes.Status401Unauthorized)]
public IActionResult MarkComicAsUnread(
public async Task<IActionResult> MarkComicAsUnread(
ComicsContext context,
string handle)
{
var validated = ComicsContext.CleanValidateHandle(handle);
if (validated is null)
return BadRequest(RequestError.InvalidHandle);
var comic = context.Comics.SingleOrDefault(c => c.Handle == validated);
var comic = await context.Comics.SingleOrDefaultAsync(c => c.Handle == validated);
if (comic is null)
return NotFound(RequestError.ComicNotFound);
if (_auth.User is null)
//user shouldn't have passed authentication if username doesn't match
return StatusCode(StatusCodes.Status500InternalServerError);
var comicRead = context.ComicsRead.SingleOrDefault(cr =>
var comicRead = await context.ComicsRead.SingleOrDefaultAsync(cr =>
cr.ComicId == comic.Id && cr.UserId == _auth.User.Id);
if (comicRead is null)
return Ok();
context.ComicsRead.Remove(comicRead);
context.SaveChanges();
await context.SaveChangesAsync();
return Ok();
}
[HttpDelete("{handle}")]
@@ -276,7 +277,7 @@ namespace ComiServ.Controllers
[ProducesResponseType<RequestError>(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType<RequestError>(StatusCodes.Status404NotFound)]
public IActionResult DeleteComic(
public async Task<IActionResult> DeleteComic(
string handle,
[FromBody]
ComicDeleteRequest req)
@@ -288,14 +289,14 @@ namespace ComiServ.Controllers
}
if (_auth.User.UserTypeId != UserTypeEnum.Administrator)
return Forbid();
var comic = _context.Comics.SingleOrDefault(c => c.Handle == handle);
var comic = await _context.Comics.SingleOrDefaultAsync(c => c.Handle == handle);
if (comic is null)
return NotFound(RequestError.ComicNotFound);
comic.Exists = _analyzer.ComicFileExists(string.Join(config.Config.LibraryRoot, comic.Filepath));
if (comic.Exists && !req.DeleteIfFileExists)
return BadRequest(RequestError.ComicFileExists);
_context.Comics.Remove(comic);
_context.SaveChanges();
await _context.SaveChangesAsync();
_analyzer.DeleteComicFile(string.Join(config.Config.LibraryRoot, comic.Filepath));
return Ok();
}
@@ -303,34 +304,34 @@ namespace ComiServ.Controllers
[ProducesResponseType<byte[]>(StatusCodes.Status200OK)]
[ProducesResponseType<RequestError>(StatusCodes.Status400BadRequest)]
[ProducesResponseType<RequestError>(StatusCodes.Status404NotFound)]
public IActionResult GetComicFile(string handle)
public async Task<IActionResult> GetComicFile(string handle)
{
//_logger.LogInformation(nameof(GetComicFile) + ": {handle}", handle);
var validated = ComicsContext.CleanValidateHandle(handle);
if (validated is null)
return BadRequest(RequestError.InvalidHandle);
var comic = _context.Comics.SingleOrDefault(c => c.Handle == handle);
var comic = await _context.Comics.SingleOrDefaultAsync(c => c.Handle == handle);
if (comic is null)
return NotFound(RequestError.ComicNotFound);
var data = System.IO.File.ReadAllBytes(Path.Join(_config.LibraryRoot, comic.Filepath));
var data = await System.IO.File.ReadAllBytesAsync(Path.Join(_config.LibraryRoot, comic.Filepath));
return File(data, "application/octet-stream", new FileInfo(comic.Filepath).Name);
}
[HttpGet("{handle}/cover")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType<RequestError>(StatusCodes.Status400BadRequest)]
[ProducesResponseType<RequestError>(StatusCodes.Status404NotFound)]
public IActionResult GetComicCover(string handle)
public async Task<IActionResult> GetComicCover(string handle)
{
//_logger.LogInformation(nameof(GetComicCover) + ": {handle}", handle);
var validated = ComicsContext.CleanValidateHandle(handle);
if (validated is null)
return BadRequest(RequestError.InvalidHandle);
var comic = _context.Comics
.SingleOrDefault(c => c.Handle == validated);
var comic = await _context.Comics
.SingleOrDefaultAsync(c => c.Handle == validated);
if (comic is null)
return NotFound(RequestError.ComicNotFound);
var cover = _context.Covers
.SingleOrDefault(cov => cov.FileXxhash64 == comic.FileXxhash64);
var cover = await _context.Covers
.SingleOrDefaultAsync(cov => cov.FileXxhash64 == comic.FileXxhash64);
if (cover is null)
return NotFound(RequestError.CoverNotFound);
var mime = IComicAnalyzer.GetImageMime(cover.Filename);
@@ -342,7 +343,7 @@ namespace ComiServ.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType<RequestError>(StatusCodes.Status400BadRequest)]
[ProducesResponseType<RequestError>(StatusCodes.Status404NotFound)]
public IActionResult GetComicPage(string handle, int page,
public async Task<IActionResult> GetComicPage(string handle, int page,
[FromQuery]
[DefaultValue(0)]
int? maxWidth,
@@ -357,10 +358,10 @@ namespace ComiServ.Controllers
var validated = ComicsContext.CleanValidateHandle(handle);
if (validated is null)
return BadRequest(RequestError.InvalidHandle);
var comic = _context.Comics.SingleOrDefault(c => c.Handle == validated);
var comic = await _context.Comics.SingleOrDefaultAsync(c => c.Handle == validated);
if (comic is null)
return NotFound(RequestError.ComicNotFound);
var comicPage = _analyzer.GetComicPage(Path.Join(_config.LibraryRoot, comic.Filepath), page);
var comicPage = await _analyzer.GetComicPageAsync(Path.Join(_config.LibraryRoot, comic.Filepath), page);
if (comicPage is null)
//TODO rethink error code
return NotFound(RequestError.PageNotFound);
@@ -382,7 +383,8 @@ namespace ComiServ.Controllers
null => comicPage.Mime,
};
//TODO using the stream directly throws but I think it should be valid, need to debug
var arr = _converter.ResizeIfBigger(stream, limit, format).ReadAllBytes();
using var resizedStream = await _converter.ResizeIfBigger(stream, limit, format);
var arr = await resizedStream.ReadAllBytesAsync();
return File(arr, mime);
}
else
@@ -392,7 +394,7 @@ namespace ComiServ.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType<RequestError>(StatusCodes.Status400BadRequest)]
[ProducesResponseType<RequestError>(StatusCodes.Status404NotFound)]
public IActionResult GetComicThumbnail(
public async Task<IActionResult> GetComicThumbnail(
string handle,
[FromQuery]
[DefaultValue(false)]
@@ -403,7 +405,7 @@ namespace ComiServ.Controllers
var validated = ComicsContext.CleanValidateHandle(handle);
if (validated is null)
return BadRequest(RequestError.InvalidHandle);
var comic = _context.Comics.SingleOrDefault(c => c.Handle == validated);
var comic = await _context.Comics.SingleOrDefaultAsync(c => c.Handle == validated);
if (comic is null)
return NotFound(RequestError.ComicNotFound);
if (comic.ThumbnailWebp is byte[] img)
@@ -412,7 +414,7 @@ namespace ComiServ.Controllers
}
if (fallbackToCover)
{
var cover = _context.Covers.SingleOrDefault(c => c.FileXxhash64 == comic.FileXxhash64);
var cover = await _context.Covers.SingleOrDefaultAsync(c => c.FileXxhash64 == comic.FileXxhash64);
if (cover is not null)
{
//TODO should this convert to a thumbnail on the fly?
@@ -424,23 +426,23 @@ namespace ComiServ.Controllers
}
[HttpPost("cleandb")]
[ProducesResponseType(StatusCodes.Status200OK)]
public IActionResult CleanUnusedTagAuthors()
public async Task<IActionResult> CleanUnusedTagAuthors()
{
_context.Authors
await _context.Authors
.Include(a => a.ComicAuthors)
.Where(a => a.ComicAuthors.Count == 0)
.ExecuteDelete();
_context.Tags
.ExecuteDeleteAsync();
await _context.Tags
.Include(a => a.ComicTags)
.Where(a => a.ComicTags.Count == 0)
.ExecuteDelete();
.ExecuteDeleteAsync();
//ExecuteDelete doesn't wait for SaveChanges
//_context.SaveChanges();
return Ok();
}
[HttpGet("duplicates")]
[ProducesResponseType<Paginated<ComicDuplicateList>>(StatusCodes.Status200OK)]
public IActionResult GetDuplicateFiles(
public async Task<IActionResult> GetDuplicateFiles(
[FromQuery]
[DefaultValue(0)]
int page,
@@ -454,7 +456,7 @@ namespace ComiServ.Controllers
.GroupBy(c => c.FileXxhash64)
.Where(g => g.Count() > 1)
.OrderBy(g => g.Key);
var ret = new Paginated<ComicDuplicateList>(pageSize, page,
var ret = await Paginated<ComicDuplicateList>.CreateAsync(pageSize, page,
groups.Select(g =>
new ComicDuplicateList(g.Key, g.Select(g => g))
));
@@ -464,12 +466,11 @@ namespace ComiServ.Controllers
[ProducesResponseType<LibraryResponse>(StatusCodes.Status200OK)]
[ProducesResponseType<RequestError>(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public IActionResult GetLibraryStats()
public async Task<IActionResult> GetLibraryStats()
{
return Ok(new LibraryResponse(
_context.Comics.Count(),
_context.Comics.Select(c => c.FileXxhash64).Distinct().Count()
await _context.Comics.CountAsync(),
await _context.Comics.Select(c => c.FileXxhash64).Distinct().CountAsync()
));
}
}
}

View File

@@ -6,8 +6,8 @@ using ComiServ.Background;
using ComiServ.Models;
using System.ComponentModel;
namespace ComiServ.Controllers
{
namespace ComiServ.Controllers;
[Route(ROUTE)]
[ApiController]
public class MiscController(ComicsContext context, ILogger<MiscController> logger, IConfigService config, IAuthenticationService auth)
@@ -22,7 +22,7 @@ namespace ComiServ.Controllers
[ProducesResponseType<Paginated<AuthorResponse>>(StatusCodes.Status200OK)]
[ProducesResponseType<RequestError>(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public IActionResult GetAuthors(
public async Task<IActionResult> GetAuthors(
[FromQuery]
[DefaultValue(0)]
int page,
@@ -38,16 +38,16 @@ namespace ComiServ.Controllers
}
if (_auth.User.UserTypeId != UserTypeEnum.Administrator)
return Forbid();
var items = context.Authors
var items = _context.Authors
.OrderBy(a => a.ComicAuthors.Count())
.Select(a => new AuthorResponse(a.Name, a.ComicAuthors.Count()));
return Ok(new Paginated<AuthorResponse>(pageSize, page, items));
return Ok(await Paginated<AuthorResponse>.CreateAsync(pageSize, page, items));
}
[HttpGet("tags")]
[ProducesResponseType<Paginated<TagResponse>>(StatusCodes.Status200OK)]
[ProducesResponseType<RequestError>(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public IActionResult GetTags(
public async Task<IActionResult> GetTags(
[FromQuery]
[DefaultValue(0)]
int page,
@@ -63,10 +63,9 @@ namespace ComiServ.Controllers
}
if (_auth.User.UserTypeId != UserTypeEnum.Administrator)
return Forbid();
var items = context.Tags
var items = _context.Tags
.OrderBy(t => t.ComicTags.Count())
.Select(t => new TagResponse(t.Name, t.ComicTags.Count()));
return Ok(new Paginated<TagResponse>(pageSize, page, items));
}
return Ok(await Paginated<TagResponse>.CreateAsync(pageSize, page, items));
}
}

View File

@@ -7,8 +7,8 @@ using Microsoft.AspNetCore.Mvc;
using System.ComponentModel;
using System.Security.Policy;
namespace ComiServ.Controllers
{
namespace ComiServ.Controllers;
[Route(ROUTE)]
[ApiController]
public class TaskController(
@@ -26,7 +26,7 @@ namespace ComiServ.Controllers
private readonly CancellationTokenSource cancellationToken = new();
[HttpGet]
[ProducesResponseType<Truncated<string>>(StatusCodes.Status200OK)]
public IActionResult GetTasks(
public Task<IActionResult> GetTasks(
[FromQuery]
[DefaultValue(20)]
int limit
@@ -58,4 +58,3 @@ namespace ComiServ.Controllers
return Ok();
}
}
}

View File

@@ -7,8 +7,8 @@ using Microsoft.EntityFrameworkCore;
using System.ComponentModel;
using System.Text;
namespace ComiServ.Controllers
{
namespace ComiServ.Controllers;
[Route(ROUTE)]
[ApiController]
public class UserController
@@ -19,7 +19,7 @@ namespace ComiServ.Controllers
[ProducesResponseType<Paginated<UserDescription>>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public IActionResult GetUsers(IAuthenticationService auth,
public async Task<IActionResult> GetUsers(IAuthenticationService auth,
ComicsContext context,
[FromQuery]
[DefaultValue(null)]
@@ -46,7 +46,7 @@ namespace ComiServ.Controllers
users = users.Where(u => u.UserTypeId == t);
if (!string.IsNullOrWhiteSpace(search))
users = users.Where(u => EF.Functions.Like(u.Username, $"%{search}%"));
return Ok(new Paginated<UserDescription>(pageSize, page, users
return Ok(await Paginated<UserDescription>.CreateAsync(pageSize, page, users
.Include(u => u.UserType)
.Select(u => new UserDescription(u.Username, u.UserType.Name))));
}
@@ -54,7 +54,7 @@ namespace ComiServ.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType<RequestError>(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public IActionResult AddUser(IAuthenticationService auth,
public async Task<IActionResult> AddUser(IAuthenticationService auth,
ComicsContext context,
[FromBody]
UserCreateRequest req)
@@ -76,7 +76,7 @@ namespace ComiServ.Controllers
UserTypeId = req.UserType
};
context.Users.Add(newUser);
context.SaveChanges();
await context.SaveChangesAsync();
return Ok();
}
[HttpDelete("delete")]
@@ -84,7 +84,7 @@ namespace ComiServ.Controllers
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public IActionResult DeleteUser(IAuthenticationService auth,
public async Task<IActionResult> DeleteUser(IAuthenticationService auth,
ComicsContext context,
[FromBody]
string username)
@@ -97,17 +97,18 @@ namespace ComiServ.Controllers
if (auth.User.UserTypeId != UserTypeEnum.Administrator)
return Forbid();
username = username.Trim();
var user = context.Users.SingleOrDefault(u => EF.Functions.Like(u.Username, $"{username}"));
var user = await context.Users.SingleOrDefaultAsync(u => EF.Functions.Like(u.Username, $"{username}"));
if (user is null)
return BadRequest();
context.Users.Remove(user);
await context.SaveChangesAsync();
return Ok();
}
[HttpPost("modify")]
[ProducesResponseType<RequestError>(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public IActionResult ModifyUser(IAuthenticationService auth,
public async Task<IActionResult> ModifyUser(IAuthenticationService auth,
ComicsContext context,
[FromBody]
UserModifyRequest req)
@@ -131,8 +132,8 @@ namespace ComiServ.Controllers
{
return Forbid();
}
var user = context.Users
.SingleOrDefault(u => EF.Functions.Like(u.Username, req.Username));
var user = await context.Users
.SingleOrDefaultAsync(u => EF.Functions.Like(u.Username, req.Username));
if (user is null)
{
return BadRequest(RequestError.UserNotFound);
@@ -145,8 +146,7 @@ namespace ComiServ.Controllers
{
user.UserTypeId = nut;
}
context.SaveChanges();
await context.SaveChangesAsync();
return Ok();
}
}
}

View File

@@ -4,8 +4,8 @@ using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
//using System.ComponentModel.DataAnnotations.Schema;
namespace ComiServ.Entities
{
namespace ComiServ.Entities;
[Index(nameof(Name), IsUnique = true)]
public class Author
{
@@ -14,4 +14,3 @@ namespace ComiServ.Entities
public string Name { get; set; } = null!;
public ICollection<ComicAuthor> ComicAuthors { get; set; } = null!;
}
}

View File

@@ -3,8 +3,8 @@ using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ComiServ.Entities
{
namespace ComiServ.Entities;
[Index(nameof(Handle), IsUnique = true)]
[Index(nameof(Filepath), IsUnique = true)]
public class Comic
@@ -32,4 +32,3 @@ namespace ComiServ.Entities
[InverseProperty("Comic")]
public ICollection<ComicRead> ReadBy { get; set; } = [];
}
}

View File

@@ -2,8 +2,8 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ComiServ.Entities
{
namespace ComiServ.Entities;
[PrimaryKey("ComicId", "AuthorId")]
[Index("ComicId")]
[Index("AuthorId")]
@@ -18,4 +18,3 @@ namespace ComiServ.Entities
[Required]
public Author Author { get; set; } = null!;
}
}

View File

@@ -1,8 +1,8 @@
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations.Schema;
namespace ComiServ.Entities
{
namespace ComiServ.Entities;
[PrimaryKey(nameof(UserId), nameof(ComicId))]
[Index(nameof(UserId))]
[Index(nameof(ComicId))]
@@ -13,4 +13,3 @@ namespace ComiServ.Entities
public int ComicId { get; set; }
public Comic Comic { get; set; }
}
}

View File

@@ -1,7 +1,7 @@
using Microsoft.EntityFrameworkCore;
namespace ComiServ.Entities
{
namespace ComiServ.Entities;
[PrimaryKey("ComicId", "TagId")]
[Index("ComicId")]
[Index("TagId")]
@@ -12,4 +12,3 @@ namespace ComiServ.Entities
public int TagId { get; set; }
public Tag Tag { get; set; } = null!;
}
}

View File

@@ -1,7 +1,7 @@
using Microsoft.EntityFrameworkCore;
namespace ComiServ.Entities
{
namespace ComiServ.Entities;
[PrimaryKey("FileXxhash64")]
public class Cover
{
@@ -9,4 +9,3 @@ namespace ComiServ.Entities
public string Filename { get; set; } = null!;
public byte[] CoverFile { get; set; } = null!;
}
}

View File

@@ -2,8 +2,8 @@
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace ComiServ.Entities
{
namespace ComiServ.Entities;
/// <summary>
/// This was originally made to remove Entity types that were being added to the Swagger schema.
/// I found that there was a bug in `ProducesResponseTypeAttribute` that caused it, and this is
@@ -25,13 +25,12 @@ namespace ComiServ.Entities
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
return;
foreach (var item in context.SchemaRepository.Schemas.Keys)
{
if (FILTER.Contains(item))
{
context.SchemaRepository.Schemas.Remove(item);
}
}
}
//foreach (var item in context.SchemaRepository.Schemas.Keys)
//{
// if (FILTER.Contains(item))
// {
// context.SchemaRepository.Schemas.Remove(item);
// }
//}
}
}

View File

@@ -2,8 +2,8 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ComiServ.Entities
{
namespace ComiServ.Entities;
[Index(nameof(Name), IsUnique = true)]
public class Tag
{
@@ -13,4 +13,3 @@ namespace ComiServ.Entities
public string Name { get; set; } = null!;
public ICollection<ComicTag> ComicTags { get; set; } = null!;
}
}

View File

@@ -3,8 +3,8 @@ using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Security.Cryptography;
namespace ComiServ.Entities
{
namespace ComiServ.Entities;
[PrimaryKey(nameof(Id))]
[Index(nameof(Username), IsUnique = true)]
public class User
@@ -35,4 +35,3 @@ namespace ComiServ.Entities
return SHA512.HashData(salted);
}
}
}

View File

@@ -1,8 +1,8 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
namespace ComiServ.Entities
{
namespace ComiServ.Entities;
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum UserTypeEnum
{
@@ -25,4 +25,3 @@ namespace ComiServ.Entities
public string Name { get; set; }
public ICollection<User> Users { get; set; }
}
}

View File

@@ -4,8 +4,8 @@ using System.Runtime.CompilerServices;
//https://stackoverflow.com/a/42467710/25956209
//https://archive.ph/RvjOy
namespace ComiServ.Extensions
{
namespace ComiServ.Extensions;
public static class DatabaseExtensions
{
//with a compound primary key, `ignorePrimaryKey` will ignore all of them
@@ -59,4 +59,3 @@ namespace ComiServ.Extensions
return context.Database.ExecuteSql(formattable);
}
}
}

View File

@@ -1,5 +1,5 @@
namespace ComiServ.Extensions
{
namespace ComiServ.Extensions;
public static class StreamExtensions
{
//https://stackoverflow.com/questions/1080442/how-do-i-convert-a-stream-into-a-byte-in-c
@@ -9,11 +9,17 @@
if (instream is MemoryStream)
return ((MemoryStream)instream).ToArray();
using (var memoryStream = new MemoryStream())
{
using var memoryStream = new MemoryStream();
instream.CopyTo(memoryStream);
return memoryStream.ToArray();
}
}
public static async Task<byte[]> ReadAllBytesAsync(this Stream instream)
{
if (instream is MemoryStream)
return ((MemoryStream)instream).ToArray();
using var memoryStream = new MemoryStream();
await instream.CopyToAsync(memoryStream);
return memoryStream.ToArray();
}
}

View File

@@ -1,7 +1,6 @@
namespace ComiServ.Logging
{
namespace ComiServ.Logging;
public static class Events
{
}
}

View File

@@ -1,7 +1,6 @@
namespace ComiServ.Models
{
namespace ComiServ.Models;
public record class AuthorResponse(string Name, int WorkCount)
{
}
}

View File

@@ -1,8 +1,7 @@
namespace ComiServ.Models
{
namespace ComiServ.Models;
//handle is taken from URL
public record class ComicDeleteRequest
(
bool DeleteIfFileExists
);
}

View File

@@ -1,7 +1,7 @@
using ComiServ.Entities;
namespace ComiServ.Models
{
namespace ComiServ.Models;
public class ComicDuplicateList
{
public long Hash { get; set; }
@@ -20,4 +20,3 @@ namespace ComiServ.Models
Count = Comics.Count;
}
}
}

View File

@@ -1,5 +1,5 @@
namespace ComiServ.Models
{
namespace ComiServ.Models;
public class ComicMetadataUpdateRequest
{
public string? Title { get; set; }
@@ -7,4 +7,3 @@
public List<string>? Tags { get; set; }
public List<string>? Authors { get; set; }
}
}

View File

@@ -1,6 +1,5 @@
namespace ComiServ.Models
{
namespace ComiServ.Models;
public record class LibraryResponse(int ComicCount, int UniqueFiles)
{
}
}

View File

@@ -1,5 +1,8 @@
namespace ComiServ.Models
{
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.EntityFrameworkCore;
namespace ComiServ.Models;
public class Paginated<T>
{
public int Max { get; }
@@ -9,8 +12,6 @@
public List<T> Items { get; }
public Paginated(int max, int page, IEnumerable<T> iter)
{
Max = max;
Page = page;
if (max <= 0)
{
throw new ArgumentOutOfRangeException(nameof(max), max, "must be greater than 0");
@@ -19,6 +20,8 @@
{
throw new ArgumentOutOfRangeException(nameof(page), page, "must be greater than or equal to 0");
}
Max = max;
Page = page;
Items = iter.Skip(max * page).Take(max + 1).ToList();
if (Items.Count > max)
{
@@ -31,5 +34,62 @@
}
Count = Items.Count;
}
private Paginated(int max, int page, bool last, List<T> items)
{
Max = max;
Page = page;
Last = last;
Items = items;
Count = Items.Count;
}
public static async Task<Paginated<T>> CreateAsync(int max, int page, IQueryable<T> iter)
{
if (max <= 0)
{
throw new ArgumentOutOfRangeException(nameof(max), max, "must be greater than 0");
}
if (page < 0)
{
throw new ArgumentOutOfRangeException(nameof(page), page, "must be greater than or equal to 0");
}
var items = await iter.Skip(max * page).Take(max + 1).ToListAsync();
bool last = true;
if (items.Count > max)
{
last = false;
items.RemoveAt(max);
}
return new(max, page, last, items);
}
public static async Task<Paginated<T>> CreateAsync(int max, int page, IAsyncEnumerable<T> iter)
{
if (max <= 0)
{
throw new ArgumentOutOfRangeException(nameof(max), max, "must be greater than 0");
}
if (page < 0)
{
throw new ArgumentOutOfRangeException(nameof(page), page, "must be greater than or equal to 0");
}
List<T> items = [];
var skip = max * page;
await foreach (T item in iter)
{
if (skip > 0)
{
skip--;
continue;
}
items.Add(item);
if (items.Count >= max + 1)
break;
}
var last = true;
if (items.Count > max)
{
last = false;
items.RemoveAt(max);
}
return new(max, page, last, items);
}
}

View File

@@ -1,7 +1,7 @@
using System.Collections;
namespace ComiServ.Models
{
namespace ComiServ.Models;
public class RequestError : IEnumerable<string>
{
public static RequestError InvalidHandle => new("Invalid handle");
@@ -49,4 +49,3 @@ namespace ComiServ.Models
return GetEnumerator();
}
}
}

View File

@@ -1,7 +1,6 @@
namespace ComiServ.Models
{
namespace ComiServ.Models;
public record class TagResponse(string Name, int WorkCount)
{
}
}

View File

@@ -1,21 +1,23 @@
using System.Reflection.PortableExecutable;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.EntityFrameworkCore;
using System.Reflection.PortableExecutable;
namespace ComiServ.Models;
namespace ComiServ.Models
{
public class Truncated<T>
{
public int Max { get; }
public int Count { get; }
public bool Complete { get; }
public List<T> Items { get; }
public Truncated(int max, IEnumerable<T> items)
public Truncated(int max, IEnumerable<T> iter)
{
if (max <= 0)
{
throw new ArgumentOutOfRangeException(nameof(max), max, "must be greater than 0");
}
Max = max;
Items = items.Take(max+1).ToList();
Items = iter.Take(max+1).ToList();
if (Items.Count <= max)
{
Complete = true;
@@ -27,5 +29,47 @@ namespace ComiServ.Models
}
Count = Items.Count;
}
private Truncated(int max, bool complete, List<T> items)
{
Max = max;
Complete = complete;
Count = items.Count;
Items = items;
}
public static async Task<Truncated<T>> CreateAsync(int max, IQueryable<T> iter)
{
if (max <= 0)
{
throw new ArgumentOutOfRangeException(nameof(max), max, "must be greater than 0");
}
var items = await iter.Take(max+1).ToListAsync();
var complete = true;
if (items.Count < max)
{
items.RemoveAt(max);
complete = false;
}
return new(max, complete, items);
}
public static async Task<Truncated<T>> CreateAsync(int max, IAsyncEnumerable<T> iter)
{
if (max <= 0)
{
throw new ArgumentOutOfRangeException(nameof(max), max, "must be greater than 0");
}
List<T> items = [];
await foreach (T item in iter)
{
items.Add(item);
if (items.Count > max)
break;
}
var complete = true;
if (items.Count <= max)
{
items.RemoveAt(max);
complete = false;
}
return new Truncated<T>(max, complete, items);
}
}

View File

@@ -1,7 +1,7 @@
using ComiServ.Entities;
namespace ComiServ.Models
{
namespace ComiServ.Models;
public class UserCreateRequest
{
public string Username { get; set; }
@@ -9,4 +9,3 @@ namespace ComiServ.Models
//NOT HASHED do not persist this object
public string Password { get; set; }
}
}

View File

@@ -1,4 +1,6 @@
namespace ComiServ.Models
namespace ComiServ.Models;
public record class UserDescription(string Username, string Usertype)
{
public record class UserDescription(string Username, string Usertype);
}

View File

@@ -1,11 +1,10 @@
using ComiServ.Entities;
namespace ComiServ.Models
{
namespace ComiServ.Models;
public class UserModifyRequest
{
public string Username { get; set; }
public string? NewUsername { get; set; }
public UserTypeEnum? NewUserType { get; set; }
}
}

View File

@@ -1,7 +1,7 @@
using ComiServ.Entities;
namespace ComiServ.Services
{
namespace ComiServ.Services;
public interface IAuthenticationService
{
public bool Tested { get; }
@@ -30,4 +30,3 @@ namespace ComiServ.Services
Tested = true;
}
}
}

View File

@@ -8,8 +8,8 @@ using System.IO.Compression;
using System.IO.Hashing;
using System.Linq;
namespace ComiServ.Background
{
namespace ComiServ.Background;
public record class ComicAnalysis
(
long FileSizeBytes,
@@ -34,6 +34,7 @@ namespace ComiServ.Background
public Task<ComicAnalysis?> AnalyzeComicAsync(string filename);
//returns null if out of range, throws for file error
public ComicPage? GetComicPage(string filepath, int page);
public Task<ComicPage?> GetComicPageAsync(string filepath, int page);
//based purely on filename, doesn't try to open file
//returns null for ALL UNRECOGNIZED OR NON-IMAGES
public static string? GetImageMime(string filename)
@@ -146,6 +147,12 @@ namespace ComiServ.Background
return GetPage7Zip(filepath, page);
else return null;
}
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
public async Task<ComicPage?> GetComicPageAsync(string filepath, int page)
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
{
return GetComicPage(filepath, page);
}
protected ComicPage? GetPageZip(string filepath, int page)
{
Debug.Assert(page >= 1, "Page number must be positive");
@@ -225,4 +232,3 @@ namespace ComiServ.Background
);
}
}
}

View File

@@ -9,8 +9,8 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration.Ini;
using Microsoft.OpenApi.Writers;
namespace ComiServ.Background
{
namespace ComiServ.Background;
public record class ComicScanItem
(
string Filepath,
@@ -65,14 +65,14 @@ namespace ComiServ.Background
}
public void TriggerLibraryScan()
{
TaskItem ti = new(
SyncTaskItem ti = new(
TaskTypes.Scan,
"Library Scan",
token =>
async token =>
{
var items = PerfomLibraryScan(token);
token?.ThrowIfCancellationRequested();
UpdateDatabaseWithScanResults(items);
await UpdateDatabaseWithScanResults(items);
},
null);
_manager.StartTask(ti);
@@ -83,19 +83,19 @@ namespace ComiServ.Background
RepeatedLibraryScanTokenSource?.Cancel();
RepeatedLibraryScanTokenSource?.Dispose();
RepeatedLibraryScanTokenSource = new();
TaskItem ti = new(
AsyncTaskItem ti = new(
TaskTypes.Scan,
"Scheduled Library Scan",
token =>
async token =>
{
var items = PerfomLibraryScan(token);
token?.ThrowIfCancellationRequested();
UpdateDatabaseWithScanResults(items);
await UpdateDatabaseWithScanResults(items);
},
RepeatedLibraryScanTokenSource.Token);
_manager.ScheduleTask(ti, interval);
}
public void UpdateDatabaseWithScanResults(IDictionary<string, ComicScanItem> items)
public async Task UpdateDatabaseWithScanResults(IDictionary<string, ComicScanItem> items)
{
using var scope = _provider.CreateScope();
var services = scope.ServiceProvider;
@@ -135,30 +135,46 @@ namespace ComiServ.Background
FileXxhash64 = p.Value.Xxhash,
PageCount = p.Value.PageCount
}).ToList();
newComics.ForEach(c => _manager.StartTask(new(
//newComics.ForEach(c => _manager.StartTask(new(
// TaskTypes.GetCover,
// $"Get Cover: {c.Title}",
// token => InsertCover(Path.Join(_config.LibraryRoot, c.Filepath), c.FileXxhash64)
// )));
foreach (var comic in newComics)
{
_manager.StartTask((AsyncTaskItem)new(
TaskTypes.GetCover,
$"Get Cover: {c.Title}",
token => InsertCover(Path.Join(_config.LibraryRoot, c.Filepath), c.FileXxhash64)
)));
newComics.ForEach(c => _manager.StartTask(new(
TaskTypes.MakeThumbnail,
$"Make Thumbnail: {c.Title}",
token => InsertThumbnail(c.Handle, Path.Join(_config.LibraryRoot, c.Filepath), 1)
)));
context.Comics.AddRange(newComics);
context.SaveChanges();
$"Get Cover: {comic.Title}",
token => InsertCover(Path.Join(_config.LibraryRoot, comic.Filepath), comic.FileXxhash64)
));
}
protected void InsertCover(string filepath, long hash)
//newComics.ForEach(c => _manager.StartTask(new(
// TaskTypes.MakeThumbnail,
// $"Make Thumbnail: {c.Title}",
// token => InsertThumbnail(c.Handle, Path.Join(_config.LibraryRoot, c.Filepath), 1)
// )));
foreach (var comic in newComics)
{
_manager.StartTask((AsyncTaskItem)new(
TaskTypes.MakeThumbnail,
$"Make Thumbnail: {comic.Title}",
token => InsertThumbnail(comic.Handle, Path.Join(_config.LibraryRoot, comic.Filepath), 1)
));
}
context.Comics.AddRange(newComics);
await context.SaveChangesAsync();
}
protected async Task InsertCover(string filepath, long hash)
{
using var scope = _provider.CreateScope();
var services = scope.ServiceProvider;
using var context = services.GetRequiredService<ComicsContext>();
var existing = context.Covers.SingleOrDefault(c => c.FileXxhash64 == hash);
var existing = await context.Covers.SingleOrDefaultAsync(c => c.FileXxhash64 == hash);
//assuming no hash overlap
//if you already have a cover, assume it's correct
if (existing is not null)
return;
var page = _analyzer.GetComicPage(filepath, 1);
var page = await _analyzer.GetComicPageAsync(filepath, 1);
if (page is null)
return;
Cover cover = new()
@@ -169,12 +185,12 @@ namespace ComiServ.Background
};
context.InsertOrIgnore(cover, true);
}
protected void InsertThumbnail(string handle, string filepath, int page = 1)
protected async Task InsertThumbnail(string handle, string filepath, int page = 1)
{
using var scope = _provider.CreateScope();
var services = scope.ServiceProvider;
using var context = services.GetRequiredService<ComicsContext>();
var comic = context.Comics.SingleOrDefault(c => c.Handle == handle);
var comic = await context.Comics.SingleOrDefaultAsync(c => c.Handle == handle);
if (comic?.ThumbnailWebp is null)
return;
var comicPage = _analyzer.GetComicPage(filepath, page);
@@ -182,12 +198,12 @@ namespace ComiServ.Background
return;
var converter = services.GetRequiredService<IPictureConverter>();
using var inStream = new MemoryStream(comicPage.Data);
var outStream = converter.MakeThumbnail(inStream);
var outStream = await converter.MakeThumbnail(inStream);
comic.ThumbnailWebp = outStream.ReadAllBytes();
}
public void Dispose()
{
RepeatedLibraryScanTokenSource?.Dispose();
}
GC.SuppressFinalize(this);
}
}

View File

@@ -1,7 +1,7 @@
using System.Text.Json;
namespace ComiServ.Services
{
namespace ComiServ.Services;
public class Configuration
{
public string LibraryRoot { get; set; }
@@ -28,4 +28,3 @@ namespace ComiServ.Services
?? throw new ArgumentException("Failed to parse config file");
}
}
}

View File

@@ -11,8 +11,8 @@ using SixLabors.ImageSharp.Formats.Bmp;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.StaticFiles;
namespace ComiServ.Background
{
namespace ComiServ.Background;
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum PictureFormats
{
@@ -29,9 +29,9 @@ namespace ComiServ.Background
public static PictureFormats ThumbnailFormat => PictureFormats.Webp;
//keeps aspect ratio, crops to horizontally to center, vertically to top
//uses System.Drawing.Size so interface isn't dependant on ImageSharp
public Stream Resize(Stream image, System.Drawing.Size newSize, PictureFormats? newFormat = null);
public Stream ResizeIfBigger(Stream image, System.Drawing.Size maxSize, PictureFormats? newFormat = null);
public Stream MakeThumbnail(Stream image);
public Task<Stream> Resize(Stream image, System.Drawing.Size newSize, PictureFormats? newFormat = null);
public Task<Stream> ResizeIfBigger(Stream image, System.Drawing.Size maxSize, PictureFormats? newFormat = null);
public Task<Stream> MakeThumbnail(Stream image);
public static string GetMime(PictureFormats format)
{
switch (format)
@@ -73,7 +73,7 @@ namespace ComiServ.Background
}
}
public bool WebpLossless { get; } = webpLossless;
public Stream Resize(Stream image, System.Drawing.Size newSize, PictureFormats? newFormat = null)
public async Task<Stream> Resize(Stream image, System.Drawing.Size newSize, PictureFormats? newFormat = null)
{
using var img = Image.Load(image);
IImageFormat format;
@@ -110,15 +110,15 @@ namespace ComiServ.Background
{
FileFormat = WebpLossless ? WebpFileFormatType.Lossless : WebpFileFormatType.Lossy
};
img.Save(outStream, enc);
await img.SaveAsync(outStream, enc);
}
else
{
img.Save(outStream, format);
await img.SaveAsync(outStream, format);
}
return outStream;
}
public Stream ResizeIfBigger(Stream image, System.Drawing.Size maxSize, PictureFormats? newFormat = null)
public async Task<Stream> ResizeIfBigger(Stream image, System.Drawing.Size maxSize, PictureFormats? newFormat = null)
{
using Image img = Image.Load(image);
IImageFormat format;
@@ -146,17 +146,16 @@ namespace ComiServ.Background
{
FileFormat = WebpLossless ? WebpFileFormatType.Lossless : WebpFileFormatType.Lossy
};
img.Save(outStream, enc);
await img.SaveAsync(outStream, enc);
}
else
{
img.Save(outStream, format);
await img.SaveAsync(outStream, format);
}
return outStream;
}
public Stream MakeThumbnail(Stream image)
public async Task<Stream> MakeThumbnail(Stream image)
{
return Resize(image, IPictureConverter.ThumbnailResolution, IPictureConverter.ThumbnailFormat);
}
return await Resize(image, IPictureConverter.ThumbnailResolution, IPictureConverter.ThumbnailFormat);
}
}

View File

@@ -1,38 +1,68 @@
using System.Collections.Concurrent;
using NuGet.Common;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Xml.Linq;
namespace ComiServ.Services;
namespace ComiServ.Background
{
public enum TaskTypes
{
Scan,
GetCover,
MakeThumbnail,
}
//task needs to use the token parameter rather than its own token, because it gets merged with the master token
public class TaskItem(TaskTypes type, string name, Action<CancellationToken?> action, CancellationToken? token = null)
public abstract class BaseTaskItem
{
public readonly TaskTypes Type = type;
public readonly string Name = name;
public readonly Action<CancellationToken?> Action = action;
public readonly CancellationToken Token = token ?? CancellationToken.None;
public readonly TaskTypes Type;
public readonly string Name;
public readonly CancellationToken Token;
protected BaseTaskItem(TaskTypes type, string name, CancellationToken? token = null)
{
Type = type;
Name = name;
Token = token ?? CancellationToken.None;
}
}
//task needs to use the token parameter rather than its own token, because it gets merged with the master token
public class SyncTaskItem
: BaseTaskItem
{
public readonly Action<CancellationToken?> Action;
public SyncTaskItem(TaskTypes type, string name, Action<CancellationToken?> action, CancellationToken? token = null)
: base(type, name, token)
{
Action = action;
}
}
public class AsyncTaskItem
: BaseTaskItem
{
public readonly Func<CancellationToken?, Task?> AsyncAction;
public AsyncTaskItem(TaskTypes type, string name, Func<CancellationToken?, Task?> asyncAction, CancellationToken? token = null)
: base(type, name, token)
{
AsyncAction = asyncAction;
}
}
public interface ITaskManager : IDisposable
{
public void StartTask(TaskItem taskItem);
public void ScheduleTask(TaskItem taskItem, TimeSpan interval);
public void StartTask(SyncTaskItem taskItem);
public void StartTask(AsyncTaskItem taskItem);
public void ScheduleTask(BaseTaskItem taskItem, TimeSpan interval);
public string[] GetTasks(int limit);
public void CancelAll();
}
public class TaskManager(ILogger<ITaskManager>? logger)
: ITaskManager
{
private readonly ConcurrentDictionary<Task, TaskItem> ActiveTasks = [];
private readonly ConcurrentDictionary<Task, BaseTaskItem> ActiveTasks = [];
private CancellationTokenSource MasterToken { get; set; } = new();
private readonly ILogger<ITaskManager>? _logger = logger;
private readonly ConcurrentDictionary<System.Timers.Timer,TaskItem> Scheduled = [];
public void StartTask(TaskItem taskItem)
private readonly ConcurrentDictionary<System.Timers.Timer, BaseTaskItem> Scheduled = [];
public void StartTask(SyncTaskItem taskItem)
{
_logger?.LogTrace($"Start Task: {taskItem.Name}");
//_logger?.LogTrace($"Start Task: {taskItem.Name}");
var tokenSource = CancellationTokenSource.CreateLinkedTokenSource(MasterToken.Token, taskItem.Token);
var newTask = Task.Run(() => taskItem.Action(tokenSource.Token),
tokenSource.Token);
@@ -44,7 +74,20 @@ namespace ComiServ.Background
//TODO should master token actually cancel followup?
newTask.ContinueWith(ManageFinishedTasks, MasterToken.Token);
}
public void ScheduleTask(TaskItem taskItem, TimeSpan interval)
public void StartTask(AsyncTaskItem taskItem)
{
var tokenSource = CancellationTokenSource.CreateLinkedTokenSource(MasterToken.Token, taskItem.Token);
var newTask = Task.Run(() => taskItem.AsyncAction(tokenSource.Token),
tokenSource.Token);
if (!ActiveTasks.TryAdd(newTask, taskItem))
{
//TODO better exception
throw new Exception("failed to add task");
}
//TODO should master token actually cancel followup?
newTask.ContinueWith(ManageFinishedTasks, MasterToken.Token);
}
public void ScheduleTask(SyncTaskItem taskItem, TimeSpan interval)
{
//var timer = new Timer((_) => StartTask(taskItem), null, dueTime, period ?? Timeout.InfiniteTimeSpan);
var timer = new System.Timers.Timer(interval);
@@ -58,6 +101,27 @@ namespace ComiServ.Background
timer.Elapsed += (_, _) => taskItem.Action(token.Token);
timer.Start();
}
public void ScheduleTask(BaseTaskItem taskItem, TimeSpan interval)
{
var timer = new System.Timers.Timer(interval);
var token = CancellationTokenSource.CreateLinkedTokenSource(MasterToken.Token, taskItem.Token);
Scheduled.TryAdd(timer, taskItem);
token.Token.Register(() =>
{
timer.Stop();
Scheduled.TryRemove(timer, out var _);
});
if (taskItem is AsyncTaskItem ati)
timer.Elapsed += async (_, _) =>
{
var task = ati.AsyncAction(token.Token);
if (task != null)
await task;
};
else if (taskItem is SyncTaskItem sti)
timer.Elapsed += (_, _) => sti.Action(token.Token);
timer.Start();
}
public string[] GetTasks(int limit)
{
return ActiveTasks.Select(p => p.Value.Name).Take(limit).ToArray();
@@ -87,7 +151,7 @@ namespace ComiServ.Background
bool taskRemoved = ActiveTasks.TryRemove(pair.Key, out _);
if (taskRemoved)
{
_logger?.LogTrace($"Removed Task: {pair.Value.Name}");
_logger?.LogTrace("Removed Task: {TaskName}", pair.Value.Name);
}
}
}
@@ -96,6 +160,6 @@ namespace ComiServ.Background
public void Dispose()
{
MasterToken?.Dispose();
}
GC.SuppressFinalize(this);
}
}