Merge pull request 'Async migration PR 2: Tag, Settings & JobRecord repositories' (#195) from feature/webapi/async-tag-settings-jobs into develop
Reviewed-on: #195
This commit was merged in pull request #195.
This commit is contained in:
@@ -101,10 +101,11 @@ public class AuthController(
|
||||
/// <summary>
|
||||
/// Checks whether user registration is currently enabled.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <returns>200 if enabled, 403 if disabled.</returns>
|
||||
[HttpGet("register")]
|
||||
public ActionResult CheckRegistrationEnabled() {
|
||||
var regSetting = settingsRepository.Get(Settings.UserRegistrationEnabled.AsString());
|
||||
public async Task<ActionResult> CheckRegistrationEnabled(CancellationToken cancellationToken) {
|
||||
var regSetting = await settingsRepository.GetAsync(Settings.UserRegistrationEnabled.AsString(), cancellationToken);
|
||||
return regSetting?.Value == "true" ? Ok() : StatusCode(403);
|
||||
}
|
||||
|
||||
@@ -117,7 +118,7 @@ public class AuthController(
|
||||
//TODO: Switch Guid reply with Authentication Result (giving a complete reason in case of failure or giving the authentication token)
|
||||
[HttpPost("register")]
|
||||
public async Task<ActionResult> Register([FromBody] UserRegisterDto dto, CancellationToken cancellationToken) {
|
||||
var regSetting = settingsRepository.Get(Settings.UserRegistrationEnabled.AsString());
|
||||
var regSetting = await settingsRepository.GetAsync(Settings.UserRegistrationEnabled.AsString(), cancellationToken);
|
||||
if (regSetting?.Value != "true")
|
||||
return StatusCode(403, "Registration is currently disabled on this server");
|
||||
|
||||
|
||||
@@ -35,13 +35,17 @@ public class JobsController(JobManager jobManager, JobScheduler jobScheduler, La
|
||||
/// <summary>
|
||||
/// Returns a page of past root jobs with total count. Polled less frequently or on demand.
|
||||
/// </summary>
|
||||
/// <param name="page">The zero-based page number.</param>
|
||||
/// <param name="pageSize">The number of records per page.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <returns>A page of past jobs and the total count.</returns>
|
||||
[HttpGet("past")]
|
||||
[Authorize(Roles = "Admin")]
|
||||
public ActionResult<PastJobsResponse> GetPast([FromQuery] int page = 0, [FromQuery] int pageSize = 5) {
|
||||
public async Task<ActionResult<PastJobsResponse>> GetPast([FromQuery] int page = 0, [FromQuery] int pageSize = 5, CancellationToken cancellationToken = default) {
|
||||
if (page < 0 || pageSize < 1 || pageSize > PagedParametersDto.MaxPageSize) { return BadRequest(); }
|
||||
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
|
||||
if (accessLevel != EAccessLevel.Admin) { return Unauthorized(); }
|
||||
var (jobs, total) = jobManager.GetPastRootJobs(page, pageSize);
|
||||
var (jobs, total) = await jobManager.GetPastRootJobsAsync(page, pageSize, cancellationToken);
|
||||
return Ok(new PastJobsResponse { Jobs = jobs, Total = total });
|
||||
}
|
||||
|
||||
@@ -50,12 +54,16 @@ public class JobsController(JobManager jobManager, JobScheduler jobScheduler, La
|
||||
/// Supports delta sync — pass <paramref name="since"/> to receive only children changed after that timestamp.
|
||||
/// The <see cref="ChildrenResponse.Since"/> field contains the timestamp to use for the next delta poll.
|
||||
/// </summary>
|
||||
/// <param name="parentId">The parent job ID.</param>
|
||||
/// <param name="since">Optional timestamp for delta sync.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <returns>The children of the job, plus the timestamp for the next delta poll.</returns>
|
||||
[HttpGet("{parentId}/children")]
|
||||
[Authorize(Roles = "Admin")]
|
||||
public ActionResult<ChildrenResponse> GetChildren(Guid parentId, [FromQuery] DateTime? since) {
|
||||
public async Task<ActionResult<ChildrenResponse>> GetChildren(Guid parentId, [FromQuery] DateTime? since, CancellationToken cancellationToken) {
|
||||
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
|
||||
if (accessLevel != EAccessLevel.Admin) { return Unauthorized(); }
|
||||
var children = jobManager.GetChildren(parentId, since);
|
||||
var children = await jobManager.GetChildrenAsync(parentId, since, cancellationToken);
|
||||
return Ok(new ChildrenResponse { Children = children, Since = DateTime.UtcNow });
|
||||
}
|
||||
|
||||
|
||||
@@ -27,22 +27,24 @@ public class SettingsController(
|
||||
/// Creates a new application setting.
|
||||
/// </summary>
|
||||
/// <param name="settingDto">The setting to create.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <returns>200 on success.</returns>
|
||||
[HttpPut]
|
||||
[Authorize(Roles = "Admin")]
|
||||
public ActionResult Create([FromBody] SettingDto settingDto) {
|
||||
public async Task<ActionResult> Create([FromBody] SettingDto settingDto, CancellationToken cancellationToken) {
|
||||
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
|
||||
|
||||
if (accessLevel != EAccessLevel.Admin) { return Unauthorized(); }
|
||||
|
||||
settingsRepository.Create(
|
||||
await settingsRepository.CreateAsync(
|
||||
new Setting {
|
||||
Name = settingDto.Name,
|
||||
Value = settingDto.Value,
|
||||
Description = settingDto.Description,
|
||||
Type = settingDto.Type,
|
||||
DisplayType = settingDto.DisplayType
|
||||
}
|
||||
},
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
return Ok();
|
||||
@@ -51,28 +53,30 @@ public class SettingsController(
|
||||
/// <summary>
|
||||
/// Gets all application settings.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <returns>A list of all settings.</returns>
|
||||
[HttpGet]
|
||||
[Authorize(Roles = "Admin")]
|
||||
public ActionResult<SettingDto> Get() {
|
||||
public async Task<ActionResult<SettingDto>> Get(CancellationToken cancellationToken) {
|
||||
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
|
||||
if (accessLevel != EAccessLevel.Admin) return Unauthorized();
|
||||
|
||||
return Ok(settingsRepository.Get().Select(s => s.ToSettingDto()).ToList());
|
||||
return Ok((await settingsRepository.GetAllAsync(cancellationToken)).Select(s => s.ToSettingDto()).ToList());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates an existing application setting.
|
||||
/// </summary>
|
||||
/// <param name="settingDto">The updated setting data.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <returns>200 on success.</returns>
|
||||
[HttpPost]
|
||||
[Authorize(Roles = "Admin")]
|
||||
public ActionResult Update([FromBody] SettingDto settingDto) {
|
||||
public async Task<ActionResult> Update([FromBody] SettingDto settingDto, CancellationToken cancellationToken) {
|
||||
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
|
||||
if (accessLevel != EAccessLevel.Admin) return Unauthorized();
|
||||
|
||||
settingsRepository.Update(settingDto.ToSetting());
|
||||
await settingsRepository.UpdateAsync(settingDto.ToSetting(), cancellationToken);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
@@ -80,29 +84,31 @@ public class SettingsController(
|
||||
/// Deletes a specific application setting.
|
||||
/// </summary>
|
||||
/// <param name="settingDto">The setting to delete.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <returns>200 on success.</returns>
|
||||
[HttpDelete]
|
||||
[Authorize(Roles = "Admin")]
|
||||
public ActionResult Delete([FromBody] SettingDto settingDto) {
|
||||
public async Task<ActionResult> Delete([FromBody] SettingDto settingDto, CancellationToken cancellationToken) {
|
||||
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
|
||||
if (accessLevel != EAccessLevel.Admin) return Unauthorized();
|
||||
|
||||
settingsRepository.Delete(settingDto.ToSetting());
|
||||
await settingsRepository.DeleteAsync(settingDto.ToSetting(), cancellationToken);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes all application settings.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <returns>200 on success.</returns>
|
||||
[HttpPost]
|
||||
[Authorize(Roles = "Admin")]
|
||||
[Route("delete-all")]
|
||||
public ActionResult DeleteAll() {
|
||||
public async Task<ActionResult> DeleteAll(CancellationToken cancellationToken) {
|
||||
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
|
||||
if (accessLevel != EAccessLevel.Admin) return Unauthorized();
|
||||
|
||||
settingsRepository.Delete();
|
||||
await settingsRepository.DeleteAllAsync(cancellationToken);
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,10 +29,11 @@ public class TagController(
|
||||
/// <param name="id">The tag ID.</param>
|
||||
/// <param name="children">Whether to include child tags.</param>
|
||||
/// <param name="ancestors">Whether to include ancestor tags.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <returns>The tag with its hierarchy, or 404 if not found.</returns>
|
||||
[HttpGet("{id}")]
|
||||
public ActionResult<TagDto> Get(Guid id, bool children = false, bool ancestors = false) {
|
||||
Tag? tag = tagRepository.Find(id);
|
||||
public async Task<ActionResult<TagDto>> Get(Guid id, bool children = false, bool ancestors = false, CancellationToken cancellationToken = default) {
|
||||
Tag? tag = await tagRepository.FindAsync(id, cancellationToken);
|
||||
|
||||
if (tag == null) { return NotFound(); }
|
||||
var tagDto = TagsMapper.ToTagDto(tag);
|
||||
@@ -40,7 +41,7 @@ public class TagController(
|
||||
|
||||
if (children) {
|
||||
tagDto.Children = [];
|
||||
tagRepository.GetChildren(tag.Id).ForEach(t => tagDto.Children.Add(TagsMapper.ToTagDto(t)));
|
||||
(await tagRepository.GetChildrenAsync(tag.Id, cancellationToken)).ForEach(t => tagDto.Children.Add(TagsMapper.ToTagDto(t)));
|
||||
}
|
||||
|
||||
//TODO: Protect from overflow
|
||||
@@ -65,14 +66,15 @@ public class TagController(
|
||||
/// Searches tags with pagination and optional search term.
|
||||
/// </summary>
|
||||
/// <param name="pagedSearch">Pagination and search parameters.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <returns>A list of tags matching the search criteria.</returns>
|
||||
[HttpGet]
|
||||
public ActionResult<List<TagDto>> GetAll([FromQuery] PagedSearchParametersDto pagedSearch) {
|
||||
public async Task<ActionResult<List<TagDto>>> GetAll([FromQuery] PagedSearchParametersDto pagedSearch, CancellationToken cancellationToken) {
|
||||
pagedSearch.Search ??= string.Empty;
|
||||
if(pagedSearch.Page < 0) { return BadRequest("Page must be greater than 0"); }
|
||||
if(pagedSearch.PageSize < 1 || pagedSearch.PageSize > PagedParametersDto.MaxPageSize) { return BadRequest($"PageSize must be between 1 and {PagedParametersDto.MaxPageSize}"); }
|
||||
|
||||
List<Tag> tags = tagRepository.Search(pagedSearch.Search, pagedSearch.Page, pagedSearch.PageSize);
|
||||
|
||||
List<Tag> tags = await tagRepository.SearchAsync(pagedSearch.Search, pagedSearch.Page, pagedSearch.PageSize, cancellationToken);
|
||||
List<TagDto> tagsDto = [];
|
||||
|
||||
tags.ForEach(
|
||||
@@ -91,14 +93,15 @@ public class TagController(
|
||||
/// </summary>
|
||||
/// <param name="id">The tag ID.</param>
|
||||
/// <param name="tagDto">The updated tag data.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <returns>200 on success, 404 if not found, 409 if name conflict.</returns>
|
||||
[Authorize(Roles = "Maintainer,Curator,Admin")]
|
||||
[HttpPost("{id}")]
|
||||
public IStatusCodeActionResult Update([FromRoute] Guid id, [FromBody] TagUpdateDto tagDto) {
|
||||
public async Task<IStatusCodeActionResult> Update([FromRoute] Guid id, [FromBody] TagUpdateDto tagDto, CancellationToken cancellationToken) {
|
||||
|
||||
IStatusCodeActionResult result = UpdateTag(id, tagDto.Name, tagDto.Parent);
|
||||
IStatusCodeActionResult result = await UpdateTag(id, tagDto.Name, tagDto.Parent, cancellationToken);
|
||||
|
||||
if (result.StatusCode == StatusCodes.Status200OK) { tagRepository.Save(); }
|
||||
if (result.StatusCode == StatusCodes.Status200OK) { await tagRepository.SaveAsync(cancellationToken); }
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -107,18 +110,19 @@ public class TagController(
|
||||
/// Updates multiple tags in a bulk operation.
|
||||
/// </summary>
|
||||
/// <param name="tagBulkDto">The bulk update data with IDs and shared update values.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <returns>200 on success, or the first error response.</returns>
|
||||
[Authorize(Roles = "Maintainer,Curator,Admin")]
|
||||
[HttpPost]
|
||||
public IStatusCodeActionResult BulkUpdate([FromBody] BulkDto<TagUpdateDto> tagBulkDto) {
|
||||
public async Task<IStatusCodeActionResult> BulkUpdate([FromBody] BulkDto<TagUpdateDto> tagBulkDto, CancellationToken cancellationToken) {
|
||||
|
||||
foreach (Guid id in tagBulkDto.Ids) {
|
||||
IStatusCodeActionResult result = UpdateTag(id, tagBulkDto.Data.Name, tagBulkDto.Data.Parent);
|
||||
IStatusCodeActionResult result = await UpdateTag(id, tagBulkDto.Data.Name, tagBulkDto.Data.Parent, cancellationToken);
|
||||
|
||||
if (result.StatusCode != StatusCodes.Status200OK) { return result; }
|
||||
}
|
||||
|
||||
tagRepository.Save();
|
||||
await tagRepository.SaveAsync(cancellationToken);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
@@ -126,13 +130,14 @@ public class TagController(
|
||||
/// Deletes a tag by its ID (soft delete).
|
||||
/// </summary>
|
||||
/// <param name="id">The tag ID.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <returns>200 on success, 404 if not found.</returns>
|
||||
[Authorize(Roles = "Maintainer,Curator,Admin")]
|
||||
[HttpDelete("{id}")]
|
||||
public ActionResult Delete([FromRoute] Guid id) {
|
||||
IStatusCodeActionResult result = DeleteTag(id);
|
||||
public async Task<ActionResult> Delete([FromRoute] Guid id, CancellationToken cancellationToken) {
|
||||
IStatusCodeActionResult result = await DeleteTag(id, cancellationToken);
|
||||
|
||||
if (result.StatusCode == StatusCodes.Status200OK) { tagRepository.Save(); }
|
||||
if (result.StatusCode == StatusCodes.Status200OK) { await tagRepository.SaveAsync(cancellationToken); }
|
||||
|
||||
return (ActionResult)result;
|
||||
}
|
||||
@@ -141,17 +146,18 @@ public class TagController(
|
||||
/// Deletes multiple tags in a bulk operation.
|
||||
/// </summary>
|
||||
/// <param name="ids">The list of tag IDs to delete.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <returns>200 on success, or the first error response.</returns>
|
||||
[Authorize(Roles = "Maintainer,Curator,Admin")]
|
||||
[HttpDelete]
|
||||
public IActionResult BulkDelete([FromBody] List<Guid> ids) {
|
||||
public async Task<IActionResult> BulkDelete([FromBody] List<Guid> ids, CancellationToken cancellationToken) {
|
||||
foreach (Guid id in ids) {
|
||||
IStatusCodeActionResult result = DeleteTag(id);
|
||||
IStatusCodeActionResult result = await DeleteTag(id, cancellationToken);
|
||||
|
||||
if (result.StatusCode != StatusCodes.Status200OK) { return result; }
|
||||
}
|
||||
|
||||
tagRepository.Save();
|
||||
await tagRepository.SaveAsync(cancellationToken);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
@@ -159,29 +165,30 @@ public class TagController(
|
||||
/// Creates a new tag.
|
||||
/// </summary>
|
||||
/// <param name="tagDto">The tag creation data.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <returns>200 on success, 409 if a tag with the same name already exists, 400 if parent not found.</returns>
|
||||
[Authorize(Roles = "Maintainer,Curator,Admin")]
|
||||
[HttpPut]
|
||||
public ActionResult Create([FromBody] TagCreateDto tagDto) {
|
||||
public async Task<ActionResult> Create([FromBody] TagCreateDto tagDto, CancellationToken cancellationToken) {
|
||||
|
||||
if (tagRepository.FindByName(tagDto.Name) != null) { return Conflict($"Tag with name {tagDto.Name} already exist"); }
|
||||
if (await tagRepository.FindByNameAsync(tagDto.Name, cancellationToken) != null) { return Conflict($"Tag with name {tagDto.Name} already exist"); }
|
||||
|
||||
var tag = new Tag() {
|
||||
Name = tagDto.Name
|
||||
};
|
||||
|
||||
if (tagDto.Parent != null && tagRepository.Find(tagDto.Parent.Value) == null) {
|
||||
if (tagDto.Parent != null && await tagRepository.FindAsync(tagDto.Parent.Value, cancellationToken) == null) {
|
||||
return BadRequest($"No Tag with id {tagDto.Parent} found");
|
||||
}
|
||||
|
||||
tag.ParentId = tagDto.Parent;
|
||||
tagRepository.Insert(tag);
|
||||
tagRepository.Save();
|
||||
await tagRepository.SaveAsync(cancellationToken);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
IStatusCodeActionResult DeleteTag(Guid id) {
|
||||
Tag? tag = tagRepository.Find(id);
|
||||
async Task<IStatusCodeActionResult> DeleteTag(Guid id, CancellationToken cancellationToken) {
|
||||
Tag? tag = await tagRepository.FindAsync(id, cancellationToken);
|
||||
|
||||
if (tag == null) { return NotFound($"Tag {id} not found"); }
|
||||
tagRepository.Delete(tag);
|
||||
@@ -189,14 +196,14 @@ public class TagController(
|
||||
}
|
||||
|
||||
|
||||
IStatusCodeActionResult UpdateTag(Guid id, string? name, Guid? parentId) {
|
||||
async Task<IStatusCodeActionResult> UpdateTag(Guid id, string? name, Guid? parentId, CancellationToken cancellationToken) {
|
||||
|
||||
Tag? tag = tagRepository.Find(id);
|
||||
Tag? tag = await tagRepository.FindAsync(id, cancellationToken);
|
||||
|
||||
if (tag == null) { return NotFound($"Tag {id} not found"); }
|
||||
|
||||
if (name != null) {
|
||||
Tag? foundTag = tagRepository.FindByName(name);
|
||||
Tag? foundTag = await tagRepository.FindByNameAsync(name, cancellationToken);
|
||||
|
||||
if (foundTag != null && foundTag.Id != tag.Id) { return Conflict($"Tag with name '{name}' already exists"); }
|
||||
|
||||
@@ -206,7 +213,7 @@ public class TagController(
|
||||
if (parentId == null) { return Ok(); }
|
||||
|
||||
if (parentId.Value.Equals(Guid.Empty)) { tag.Parent = null; } else {
|
||||
Tag? parent = tagRepository.Find(parentId.Value);
|
||||
Tag? parent = await tagRepository.FindAsync(parentId.Value, cancellationToken);
|
||||
|
||||
if (parent == null) { return NotFound($"Parent tag on {id} was not found"); }
|
||||
tag.Parent = parent;
|
||||
|
||||
@@ -192,12 +192,12 @@ public sealed class ConvertAnimatedJob : Job {
|
||||
JobStatus.Start();
|
||||
Logger.LogInformation("Starting master animated conversion job.");
|
||||
|
||||
var thumbPathSetting = SettingsRepository.Get(Settings.ThumbnailPath.AsString());
|
||||
var convertedPathSetting = SettingsRepository.Get(Settings.ConvertedPath.AsString());
|
||||
var convertedFormatSetting = SettingsRepository.Get(Settings.ConvertedFormat.AsString());
|
||||
var ffmpegPathSetting = SettingsRepository.Get(Settings.FfmpegPath.AsString());
|
||||
var thumbSizeSetting = SettingsRepository.Get(Settings.ThumbnailSize.AsString());
|
||||
var batchSizeSetting = SettingsRepository.Get(Settings.JobBatchSize.AsString());
|
||||
var thumbPathSetting = await SettingsRepository.GetAsync(Settings.ThumbnailPath.AsString(), token);
|
||||
var convertedPathSetting = await SettingsRepository.GetAsync(Settings.ConvertedPath.AsString(), token);
|
||||
var convertedFormatSetting = await SettingsRepository.GetAsync(Settings.ConvertedFormat.AsString(), token);
|
||||
var ffmpegPathSetting = await SettingsRepository.GetAsync(Settings.FfmpegPath.AsString(), token);
|
||||
var thumbSizeSetting = await SettingsRepository.GetAsync(Settings.ThumbnailSize.AsString(), token);
|
||||
var batchSizeSetting = await SettingsRepository.GetAsync(Settings.JobBatchSize.AsString(), token);
|
||||
|
||||
if (thumbPathSetting == null || convertedPathSetting == null
|
||||
|| string.IsNullOrWhiteSpace(thumbPathSetting.Value) || string.IsNullOrWhiteSpace(convertedPathSetting.Value)) {
|
||||
|
||||
@@ -59,7 +59,7 @@ public sealed class FileSystemCrawlJob : Job {
|
||||
var settingsRepo = Scope.ServiceProvider.GetRequiredService<ISettingsRepository>();
|
||||
|
||||
Guid? uploaderId = null;
|
||||
var uploaderSetting = settingsRepo.Get(Settings.SystemUploaderId.AsString())?.Value;
|
||||
var uploaderSetting = (await settingsRepo.GetAsync(Settings.SystemUploaderId.AsString(), token))?.Value;
|
||||
if (Guid.TryParse(uploaderSetting, out var parsed)) uploaderId = parsed;
|
||||
|
||||
// Separate context for asset reconciliation so the change tracker can be cleared per directory
|
||||
|
||||
+12
-11
@@ -35,10 +35,10 @@ public class JobManager(IServiceProvider serviceProvider, ILogger<JobManager> lo
|
||||
/// <summary>
|
||||
/// Returns a page of past root jobs from the database.
|
||||
/// </summary>
|
||||
public (List<JobStatusDto> Jobs, int TotalCount) GetPastRootJobs(int page, int pageSize) {
|
||||
public async Task<(List<JobStatusDto> Jobs, int TotalCount)> GetPastRootJobsAsync(int page, int pageSize, CancellationToken cancellationToken) {
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
var repo = scope.ServiceProvider.GetRequiredService<IJobRecordRepository>();
|
||||
var records = repo.GetPastRootJobs(page, pageSize, out var total);
|
||||
var (records, total) = await repo.GetPastRootJobsAsync(page, pageSize, cancellationToken);
|
||||
logger.LogInformation("GetPastRootJobs page={Page} pageSize={PageSize}: {Count} records out of {Total}", page, pageSize, records.Count, total);
|
||||
return (records.Select(r => r.ToJobStatusDto()).ToList(), total);
|
||||
}
|
||||
@@ -49,7 +49,8 @@ public class JobManager(IServiceProvider serviceProvider, ILogger<JobManager> lo
|
||||
/// </summary>
|
||||
/// <param name="parentId">The parent job ID.</param>
|
||||
/// <param name="since">If set, only children changed after this timestamp are returned. Null returns all children.</param>
|
||||
public List<JobStatusDto> GetChildren(Guid parentId, DateTime? since) {
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
public async Task<List<JobStatusDto>> GetChildrenAsync(Guid parentId, DateTime? since, CancellationToken cancellationToken) {
|
||||
var seenIds = new HashSet<Guid>();
|
||||
|
||||
// Active children still in memory
|
||||
@@ -71,8 +72,8 @@ public class JobManager(IServiceProvider serviceProvider, ILogger<JobManager> lo
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
var repo = scope.ServiceProvider.GetRequiredService<IJobRecordRepository>();
|
||||
var records = since is null
|
||||
? repo.GetChildren(parentId)
|
||||
: repo.GetChildrenModifiedSince(parentId, since.Value);
|
||||
? await repo.GetChildrenAsync(parentId, cancellationToken)
|
||||
: await repo.GetChildrenModifiedSinceAsync(parentId, since.Value, cancellationToken);
|
||||
results.AddRange(records.Where(r => !seenIds.Contains(r.Id)).Select(r => r.ToJobStatusDto()));
|
||||
} catch (Exception ex) {
|
||||
logger.LogWarning(ex, "Could not load children for parent {ParentId}", parentId);
|
||||
@@ -178,14 +179,14 @@ public class JobManager(IServiceProvider serviceProvider, ILogger<JobManager> lo
|
||||
}
|
||||
|
||||
///<inheritdoc />
|
||||
protected override Task ExecuteAsync(CancellationToken stoppingToken) {
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
|
||||
logger.LogInformation("Preparing JobManager...");
|
||||
|
||||
// Load past jobs from database on startup
|
||||
try {
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
var repo = scope.ServiceProvider.GetRequiredService<IJobRecordRepository>();
|
||||
foreach (var record in repo.GetAll()) {
|
||||
foreach (var record in await repo.GetAllAsync(stoppingToken)) {
|
||||
pastJobs.TryAdd(record.Id, record.ToJobStatusDto());
|
||||
}
|
||||
logger.LogInformation($"Loaded {pastJobs.Count} past job(s) from database.");
|
||||
@@ -197,7 +198,7 @@ public class JobManager(IServiceProvider serviceProvider, ILogger<JobManager> lo
|
||||
try {
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
var settingsRepo = scope.ServiceProvider.GetRequiredService<ISettingsRepository>();
|
||||
var maxJobsSetting = settingsRepo.Get(Settings.MaxConcurrentJobs.AsString());
|
||||
var maxJobsSetting = await settingsRepo.GetAsync(Settings.MaxConcurrentJobs.AsString(), stoppingToken);
|
||||
if (maxJobsSetting != null && int.TryParse(maxJobsSetting.Value, out var maxJobs) && maxJobs > 0) {
|
||||
MaxConcurrentJobs = maxJobs;
|
||||
logger.LogInformation("Loaded MaxConcurrentJobs from settings: {Value}", MaxConcurrentJobs);
|
||||
@@ -227,7 +228,7 @@ public class JobManager(IServiceProvider serviceProvider, ILogger<JobManager> lo
|
||||
}
|
||||
|
||||
foreach (var job in queued) {
|
||||
job.Done += (sender, status) => {
|
||||
job.Done += async (sender, status) => {
|
||||
var j = (Job)sender!;
|
||||
var dto = j.ToJobStatusDto();
|
||||
|
||||
@@ -247,7 +248,7 @@ public class JobManager(IServiceProvider serviceProvider, ILogger<JobManager> lo
|
||||
Progress = dto.Progress
|
||||
};
|
||||
repo.Insert(record);
|
||||
repo.Save();
|
||||
await repo.SaveAsync(CancellationToken.None);
|
||||
} catch (Exception ex) {
|
||||
logger.LogError(ex, "Failed to persist job {JobId}", status.Id);
|
||||
}
|
||||
@@ -266,6 +267,6 @@ public class JobManager(IServiceProvider serviceProvider, ILogger<JobManager> lo
|
||||
);
|
||||
|
||||
lifetime.ApplicationStarted.Register(() => service.Start());
|
||||
return service;
|
||||
await service;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ public sealed class MetadataJob : Job {
|
||||
JobStatus.Start();
|
||||
|
||||
batchSize = 200;
|
||||
var batchSizeSetting = SettingsRepository?.Get(Settings.JobBatchSize.AsString());
|
||||
var batchSizeSetting = SettingsRepository is null ? null : await SettingsRepository.GetAsync(Settings.JobBatchSize.AsString(), token);
|
||||
if (batchSizeSetting != null && int.TryParse(batchSizeSetting.Value, out var parsed))
|
||||
batchSize = Math.Max(1, parsed);
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ public sealed class PHashJob : Job {
|
||||
JobStatus.Start();
|
||||
|
||||
batchSize = 200;
|
||||
var batchSizeSetting = SettingsRepository?.Get(Settings.JobBatchSize.AsString());
|
||||
var batchSizeSetting = SettingsRepository is null ? null : await SettingsRepository.GetAsync(Settings.JobBatchSize.AsString(), token);
|
||||
if (batchSizeSetting != null && int.TryParse(batchSizeSetting.Value, out var parsed))
|
||||
batchSize = Math.Max(1, parsed);
|
||||
|
||||
|
||||
@@ -189,10 +189,10 @@ public class PreviewJob : Job {
|
||||
JobStatus.Start();
|
||||
Logger.LogInformation("Starting master Preview job.");
|
||||
|
||||
var previewPathSetting = SettingsRepository.Get(Settings.PreviewPath.AsString());
|
||||
var previewSizeSetting = SettingsRepository.Get(Settings.PreviewSize.AsString());
|
||||
var previewQualitySetting = SettingsRepository.Get(Settings.PreviewQuality.AsString());
|
||||
var batchSizeSetting = SettingsRepository.Get(Settings.JobBatchSize.AsString());
|
||||
var previewPathSetting = await SettingsRepository.GetAsync(Settings.PreviewPath.AsString(), token);
|
||||
var previewSizeSetting = await SettingsRepository.GetAsync(Settings.PreviewSize.AsString(), token);
|
||||
var previewQualitySetting = await SettingsRepository.GetAsync(Settings.PreviewQuality.AsString(), token);
|
||||
var batchSizeSetting = await SettingsRepository.GetAsync(Settings.JobBatchSize.AsString(), token);
|
||||
|
||||
if (previewPathSetting == null) {
|
||||
Logger.LogError("Preview path not found. Cannot proceed.");
|
||||
|
||||
@@ -189,10 +189,10 @@ public class ThumbnailJob : Job {
|
||||
JobStatus.Start();
|
||||
Logger.LogInformation("Starting master Thumbnail job.");
|
||||
|
||||
var thumbPathSetting = SettingsRepository.Get(Settings.ThumbnailPath.AsString());
|
||||
var thumbSizeSetting = SettingsRepository.Get(Settings.ThumbnailSize.AsString());
|
||||
var thumbQualitySetting = SettingsRepository.Get(Settings.ThumbnailQuality.AsString());
|
||||
var batchSizeSetting = SettingsRepository.Get(Settings.JobBatchSize.AsString());
|
||||
var thumbPathSetting = await SettingsRepository.GetAsync(Settings.ThumbnailPath.AsString(), token);
|
||||
var thumbSizeSetting = await SettingsRepository.GetAsync(Settings.ThumbnailSize.AsString(), token);
|
||||
var thumbQualitySetting = await SettingsRepository.GetAsync(Settings.ThumbnailQuality.AsString(), token);
|
||||
var batchSizeSetting = await SettingsRepository.GetAsync(Settings.JobBatchSize.AsString(), token);
|
||||
|
||||
if (thumbPathSetting == null) {
|
||||
Logger.LogError("Thumbnail path not found. Cannot proceed.");
|
||||
|
||||
@@ -19,38 +19,45 @@ public interface IJobRecordRepository : IDisposable {
|
||||
/// <summary>
|
||||
/// Retrieves all job records, ordered by most recent.
|
||||
/// </summary>
|
||||
List<JobRecord> GetAll();
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
Task<List<JobRecord>> GetAllAsync(CancellationToken cancellationToken);
|
||||
/// <summary>
|
||||
/// Retrieves a page of past (completed/failed/canceled) root job records.
|
||||
/// Retrieves a page of past (completed/failed/canceled) root job records with the total count.
|
||||
/// </summary>
|
||||
/// <param name="page">The page number.</param>
|
||||
/// <param name="pageSize">The number of records per page.</param>
|
||||
/// <param name="total">The total number of past root jobs.</param>
|
||||
List<JobRecord> GetPastRootJobs(int page, int pageSize, out int total);
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <returns>The page of records and the total number of past root jobs.</returns>
|
||||
Task<(List<JobRecord> Jobs, int Total)> GetPastRootJobsAsync(int page, int pageSize, CancellationToken cancellationToken);
|
||||
/// <summary>
|
||||
/// Retrieves child job records for a given parent job.
|
||||
/// </summary>
|
||||
/// <param name="parentId">The parent job ID.</param>
|
||||
List<JobRecord> GetChildren(Guid parentId);
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
Task<List<JobRecord>> GetChildrenAsync(Guid parentId, CancellationToken cancellationToken);
|
||||
/// <summary>
|
||||
/// Retrieves child job records for a given parent job that have been modified since the specified timestamp.
|
||||
/// </summary>
|
||||
/// <param name="parentId">The parent job ID.</param>
|
||||
/// <param name="since">Only return records modified after this timestamp.</param>
|
||||
List<JobRecord> GetChildrenModifiedSince(Guid parentId, DateTime since);
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
Task<List<JobRecord>> GetChildrenModifiedSinceAsync(Guid parentId, DateTime since, CancellationToken cancellationToken);
|
||||
/// <summary>
|
||||
/// Retrieves a job record by its ID.
|
||||
/// </summary>
|
||||
/// <param name="id">The job record ID.</param>
|
||||
JobRecord? GetById(Guid id);
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
Task<JobRecord?> GetByIdAsync(Guid id, CancellationToken cancellationToken);
|
||||
/// <summary>
|
||||
/// Deletes job records that finished before the specified cutoff date.
|
||||
/// </summary>
|
||||
/// <param name="cutoff">The cutoff date.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <returns>The number of deleted records.</returns>
|
||||
int DeleteOldJobs(DateTime cutoff);
|
||||
Task<int> DeleteOldJobsAsync(DateTime cutoff, CancellationToken cancellationToken);
|
||||
/// <summary>
|
||||
/// Persists changes to the database.
|
||||
/// </summary>
|
||||
void Save();
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
Task SaveAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -10,41 +10,48 @@ public interface ISettingsRepository : IDisposable {
|
||||
/// Creates a settings entry in the repository. If a settings entry already exists, it will be overwritten.
|
||||
/// </summary>
|
||||
/// <param name="setting">The settings to create.</param>
|
||||
public void Create(Setting setting);
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
Task CreateAsync(Setting setting, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the settings from the repository.
|
||||
/// Retrieves all settings from the repository.
|
||||
/// </summary>
|
||||
/// <returns>The settings if found; otherwise, null.</returns>
|
||||
public IEnumerable<Setting> Get();
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <returns>The list of all settings.</returns>
|
||||
Task<List<Setting>> GetAllAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Try to get a specific setting by its name.
|
||||
/// </summary>
|
||||
/// <param name="name">name key of the setting</param>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <returns> The setting if found; otherwise, null.</returns>
|
||||
public Setting? Get(string name);
|
||||
Task<Setting?> GetAsync(string name, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Updates the settings in the repository.
|
||||
/// </summary>
|
||||
/// <param name="setting">The settings to update.</param>
|
||||
public void Update(Setting setting);
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
Task UpdateAsync(Setting setting, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the settings from the repository.
|
||||
/// Deletes all settings from the repository.
|
||||
/// </summary>
|
||||
public void Delete();
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
Task DeleteAllAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Delete a specific setting by its name.
|
||||
/// </summary>
|
||||
/// <param name="name">name key of the setting to remove</param>
|
||||
public void Delete(string name);
|
||||
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
Task DeleteAsync(string name, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a specific setting from the repository.
|
||||
/// </summary>
|
||||
/// <param name="setting">Setting instance to delete</param>
|
||||
public void Delete(Setting setting);
|
||||
|
||||
}
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
Task DeleteAsync(Setting setting, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -12,51 +12,56 @@ public interface ITagRepository : IDisposable {
|
||||
/// <param name="searchQuery">the search query</param>
|
||||
/// <param name="page"> the number of the page for the given query</param>
|
||||
/// <param name="pageSize">the size of the page</param>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <returns>list of tags of type <see cref="Tag"/> with the size <paramref name="pageSize"/></returns>
|
||||
List<Tag> Search(string searchQuery, int page, int pageSize);
|
||||
Task<List<Tag>> SearchAsync(string searchQuery, int page, int pageSize, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Returns all the tags
|
||||
/// Returns all the tags.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IEnumerable<Tag> GetAll();
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <returns>The list of all tags.</returns>
|
||||
Task<List<Tag>> GetAllAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Find a specific Tag given the id
|
||||
/// Find a specific Tag given the id.
|
||||
/// </summary>
|
||||
/// <param name="id">identifier of the tag</param>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <returns>the <see cref="Tag"/> or null</returns>
|
||||
Tag? Find(Guid id);
|
||||
|
||||
Task<Tag?> FindAsync(Guid id, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Find a specific Tag given the name
|
||||
/// Find a specific Tag given the name.
|
||||
/// </summary>
|
||||
/// <param name="name">the exact name of the tag</param>
|
||||
/// <returns></returns>
|
||||
Tag? FindByName(string name);
|
||||
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <returns>the <see cref="Tag"/> or null</returns>
|
||||
Task<Tag?> FindByNameAsync(string name, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Return the list of the children of the given tag
|
||||
/// Return the list of the children of the given tag.
|
||||
/// </summary>
|
||||
/// <param name="id">identifier of the tag</param>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <returns>the list of <see cref="Tag"/>, could be empty</returns>
|
||||
List<Tag> GetChildren(Guid id);
|
||||
Task<List<Tag>> GetChildrenAsync(Guid id, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Saves all changes to the repository.
|
||||
/// </summary>
|
||||
void Save();
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
Task SaveAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the given tag from the repository.
|
||||
/// </summary>
|
||||
/// <param name="tag"></param>
|
||||
/// <param name="tag">The tag to delete.</param>
|
||||
void Delete(Tag tag);
|
||||
|
||||
/// <summary>
|
||||
/// Insert a new Tag
|
||||
/// Insert a new Tag.
|
||||
/// </summary>
|
||||
/// <param name="tag"></param>
|
||||
/// <param name="tag">The tag to insert.</param>
|
||||
void Insert(Tag tag);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Butter.Types;
|
||||
using Lactose.Context;
|
||||
using Lactose.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Lactose.Repositories;
|
||||
|
||||
@@ -19,50 +20,52 @@ public class JobRecordRepository(LactoseDbContext context) : IJobRecordRepositor
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public List<JobRecord> GetAll() =>
|
||||
public Task<List<JobRecord>> GetAllAsync(CancellationToken cancellationToken) =>
|
||||
context.JobRecords
|
||||
.OrderByDescending(j => j.Finished ?? j.Created)
|
||||
.ToList();
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
static readonly EJobStatus[] PastStatuses = [EJobStatus.Completed, EJobStatus.CompletedWithErrors, EJobStatus.Failed, EJobStatus.Canceled];
|
||||
|
||||
/// <inheritdoc />
|
||||
public List<JobRecord> GetPastRootJobs(int page, int pageSize, out int total) {
|
||||
public async Task<(List<JobRecord> Jobs, int Total)> GetPastRootJobsAsync(int page, int pageSize, CancellationToken cancellationToken) {
|
||||
var query = context.JobRecords
|
||||
.Where(j => j.ParentJobId == null && PastStatuses.Contains(j.Status));
|
||||
|
||||
total = query.Count();
|
||||
var total = await query.CountAsync(cancellationToken);
|
||||
|
||||
return query
|
||||
var jobs = await query
|
||||
.OrderByDescending(j => j.Finished ?? j.Created)
|
||||
.Skip(page * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToList();
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return (jobs, total);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public List<JobRecord> GetChildren(Guid parentId) =>
|
||||
public Task<List<JobRecord>> GetChildrenAsync(Guid parentId, CancellationToken cancellationToken) =>
|
||||
context.JobRecords
|
||||
.Where(j => j.ParentJobId == parentId)
|
||||
.OrderBy(j => j.Created)
|
||||
.ToList();
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public List<JobRecord> GetChildrenModifiedSince(Guid parentId, DateTime since) =>
|
||||
public Task<List<JobRecord>> GetChildrenModifiedSinceAsync(Guid parentId, DateTime since, CancellationToken cancellationToken) =>
|
||||
context.JobRecords
|
||||
.Where(j => j.ParentJobId == parentId && j.ModifiedAt != null && j.ModifiedAt > since)
|
||||
.OrderBy(j => j.Created)
|
||||
.ToList();
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public JobRecord? GetById(Guid id) =>
|
||||
context.JobRecords.Find(id);
|
||||
public async Task<JobRecord?> GetByIdAsync(Guid id, CancellationToken cancellationToken) =>
|
||||
await context.JobRecords.FindAsync([id], cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public int DeleteOldJobs(DateTime cutoff) {
|
||||
var old = context.JobRecords
|
||||
public async Task<int> DeleteOldJobsAsync(DateTime cutoff, CancellationToken cancellationToken) {
|
||||
var old = await context.JobRecords
|
||||
.Where(j => j.Finished != null && j.Finished < cutoff)
|
||||
.ToList();
|
||||
.ToListAsync(cancellationToken);
|
||||
var count = old.Count;
|
||||
if (count > 0) {
|
||||
context.JobRecords.RemoveRange(old);
|
||||
@@ -71,7 +74,7 @@ public class JobRecordRepository(LactoseDbContext context) : IJobRecordRepositor
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Save() => context.SaveChanges();
|
||||
public Task SaveAsync(CancellationToken cancellationToken) => context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose() => context.Dispose();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Lactose.Context;
|
||||
using Lactose.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Data;
|
||||
|
||||
namespace Lactose.Repositories;
|
||||
@@ -7,32 +8,33 @@ namespace Lactose.Repositories;
|
||||
/// <inheritdoc />
|
||||
public class SettingsRepository(LactoseDbContext context) : ISettingsRepository {
|
||||
/// <inheritdoc />
|
||||
public void Create(Setting setting) {
|
||||
public async Task CreateAsync(Setting setting, CancellationToken cancellationToken) {
|
||||
//Check if the settings already exist
|
||||
var existingSettings = context.Settings.FirstOrDefault(s => s.Name == setting.Name);
|
||||
if (existingSettings != null)
|
||||
var existingSettings = await context.Settings.FirstOrDefaultAsync(s => s.Name == setting.Name, cancellationToken);
|
||||
if (existingSettings != null)
|
||||
throw new DuplicateNameException("A setting with the same key already exists: " + setting.Name);
|
||||
|
||||
SettingChange(setting);
|
||||
//Adds the new settings
|
||||
context.Settings.Add(setting);
|
||||
context.SaveChanges();
|
||||
await context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<Setting> Get() => context.Settings.AsEnumerable();
|
||||
public Task<List<Setting>> GetAllAsync(CancellationToken cancellationToken) => context.Settings.ToListAsync(cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Setting? Get(string name) => context.Settings.FirstOrDefault(s => s.Name == name);
|
||||
public Task<Setting?> GetAsync(string name, CancellationToken cancellationToken) =>
|
||||
context.Settings.FirstOrDefaultAsync(s => s.Name == name, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Update(Setting setting) {
|
||||
var existingSettings = context.Settings.FirstOrDefault(s => s.Name == setting.Name);
|
||||
public async Task UpdateAsync(Setting setting, CancellationToken cancellationToken) {
|
||||
var existingSettings = await context.Settings.FirstOrDefaultAsync(s => s.Name == setting.Name, cancellationToken);
|
||||
if (existingSettings != null) {
|
||||
if (existingSettings.Value != setting.Value) SettingChange(setting);
|
||||
existingSettings.Value = setting.Value;
|
||||
context.Settings.Update(existingSettings);
|
||||
context.SaveChanges();
|
||||
await context.SaveChangesAsync(cancellationToken);
|
||||
} else {
|
||||
// If the settings entry does not exist
|
||||
throw new KeyNotFoundException("The setting with the specified name does not exist: " + setting.Name);
|
||||
@@ -40,37 +42,38 @@ public class SettingsRepository(LactoseDbContext context) : ISettingsRepository
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Delete() {
|
||||
public async Task DeleteAllAsync(CancellationToken cancellationToken) {
|
||||
context.Settings.RemoveRange(context.Settings);
|
||||
context.SaveChanges();
|
||||
await context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Delete(Setting setting) {
|
||||
var existingSettings = context.Settings.FirstOrDefault(s => s.Name == setting.Name);
|
||||
public async Task DeleteAsync(Setting setting, CancellationToken cancellationToken) {
|
||||
var existingSettings = await context.Settings.FirstOrDefaultAsync(s => s.Name == setting.Name, cancellationToken);
|
||||
if (existingSettings != null) {
|
||||
context.Settings.Remove(existingSettings);
|
||||
context.SaveChanges();
|
||||
await context.SaveChangesAsync(cancellationToken);
|
||||
} else {
|
||||
// If the settings entry does not exist
|
||||
throw new KeyNotFoundException("The setting with the specified name does not exist: " + setting.Name);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Delete(string name) {
|
||||
var existingSettings = context.Settings.FirstOrDefault(s => s.Name == name);
|
||||
public async Task DeleteAsync(string name, CancellationToken cancellationToken) {
|
||||
var existingSettings = await context.Settings.FirstOrDefaultAsync(s => s.Name == name, cancellationToken);
|
||||
if (existingSettings == null) throw new KeyNotFoundException("The setting with the specified name does not exist: " + name);
|
||||
Delete(existingSettings);
|
||||
context.SaveChanges();
|
||||
context.Settings.Remove(existingSettings);
|
||||
await context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a setting value changes.
|
||||
/// </summary>
|
||||
public static event EventHandler<Setting>? SettingChanged;
|
||||
static void OnSettingChanged(Setting e) => SettingChanged?.Invoke(null, e);
|
||||
|
||||
|
||||
|
||||
|
||||
//TODO: May need to be change on a Event based system, because adding here all the Setting Actions could be too much ramification
|
||||
void SettingChange(Setting setting) {
|
||||
OnSettingChanged(setting);
|
||||
@@ -91,4 +94,4 @@ public class SettingsRepository(LactoseDbContext context) : ISettingsRepository
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose() => context.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@ public class StatsRepository(
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<StatsDto> GetStatsAsync(CancellationToken cancellationToken) {
|
||||
var thumbnailSizeSetting = settingsRepo.Get(Settings.ThumbnailSize.AsString());
|
||||
var previewSizeSetting = settingsRepo.Get(Settings.PreviewSize.AsString());
|
||||
var thumbnailSizeSetting = await settingsRepo.GetAsync(Settings.ThumbnailSize.AsString(), cancellationToken);
|
||||
var previewSizeSetting = await settingsRepo.GetAsync(Settings.PreviewSize.AsString(), cancellationToken);
|
||||
|
||||
var cacheKey = $"stats:{thumbnailSizeSetting?.Value}:{previewSizeSetting?.Value}";
|
||||
if (cache.TryGetValue(cacheKey, out StatsDto? cached) && cached is not null)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Lactose.Context;
|
||||
using Lactose.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Lactose.Repositories;
|
||||
|
||||
@@ -9,25 +10,27 @@ public class TagRepository(LactoseDbContext context) : ITagRepository {
|
||||
public void Dispose() => context.Dispose();
|
||||
|
||||
/// <inheritdoc />
|
||||
public List<Tag> Search(string searchQuery, int page = 0, int pageSize = 200) =>
|
||||
public Task<List<Tag>> SearchAsync(string searchQuery, int page, int pageSize, CancellationToken cancellationToken) =>
|
||||
context.Tags.Where(t => EF.Functions.Like(t.Name, $"%{searchQuery}%"))
|
||||
.OrderByDescending(t => t.Name)
|
||||
.Skip((page) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToList();
|
||||
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<Tag> GetAll() => context.Tags;
|
||||
public Task<List<Tag>> GetAllAsync(CancellationToken cancellationToken) => context.Tags.ToListAsync(cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Tag? Find(Guid id) => context.Tags.Find(id);
|
||||
public async Task<Tag?> FindAsync(Guid id, CancellationToken cancellationToken) =>
|
||||
await context.Tags.FindAsync([id], cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Tag? FindByName(string name) => context.Tags.FirstOrDefault(t => t.Name == name);
|
||||
public Task<Tag?> FindByNameAsync(string name, CancellationToken cancellationToken) =>
|
||||
context.Tags.FirstOrDefaultAsync(t => t.Name == name, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public List<Tag> GetChildren(Guid id) => context.Tags.Where(t=>t.ParentId==id).ToList();
|
||||
public Task<List<Tag>> GetChildrenAsync(Guid id, CancellationToken cancellationToken) =>
|
||||
context.Tags.Where(t => t.ParentId == id).ToListAsync(cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Delete(Tag tag) => context.Tags.Remove(tag);
|
||||
@@ -36,5 +39,5 @@ public class TagRepository(LactoseDbContext context) : ITagRepository {
|
||||
public void Insert(Tag tag) => context.Tags.Add(tag);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Save() => context.SaveChanges();
|
||||
public Task SaveAsync(CancellationToken cancellationToken) => context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
@@ -17,42 +17,45 @@ public class JobScheduler(IServiceProvider serviceProvider, ILogger<JobScheduler
|
||||
JobManager? jobManager;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task ExecuteAsync(CancellationToken stoppingToken) {
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
|
||||
var lifetime = serviceProvider.GetRequiredService<IHostApplicationLifetime>();
|
||||
|
||||
var service = new Task(() => {
|
||||
logger?.LogInformation("Preparing JobScheduler...");
|
||||
jobManager = serviceProvider.GetRequiredService<JobManager>();
|
||||
logger?.LogInformation("JobScheduler started!");
|
||||
// fetch settings on startup
|
||||
FetchSettings();
|
||||
// listen for settings changes
|
||||
SettingsRepository.SettingChanged += OnSettingsChanged;
|
||||
// keep the task alive until the service is stopping
|
||||
stoppingToken.WaitHandle.WaitOne();
|
||||
}, stoppingToken
|
||||
);
|
||||
|
||||
lifetime.ApplicationStarted.Register(() => service.Start());
|
||||
return service;
|
||||
|
||||
// wait until the application has started before preparing the scheduler
|
||||
var startedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
lifetime.ApplicationStarted.Register(() => startedTcs.SetResult());
|
||||
|
||||
try {
|
||||
await startedTcs.Task;
|
||||
logger?.LogInformation("Preparing JobScheduler...");
|
||||
jobManager = serviceProvider.GetRequiredService<JobManager>();
|
||||
logger?.LogInformation("JobScheduler started!");
|
||||
// fetch settings on startup
|
||||
await FetchSettingsAsync(stoppingToken);
|
||||
// listen for settings changes
|
||||
SettingsRepository.SettingChanged += OnSettingsChanged;
|
||||
// keep the service alive until the host is stopping
|
||||
await Task.Delay(Timeout.Infinite, stoppingToken);
|
||||
} catch (OperationCanceledException) {
|
||||
// normal shutdown
|
||||
}
|
||||
}
|
||||
|
||||
void FetchSettings() {
|
||||
async Task FetchSettingsAsync(CancellationToken cancellationToken = default) {
|
||||
logger?.LogInformation("Fetching settings...");
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
var settingsRepository = scope.ServiceProvider.GetRequiredService<ISettingsRepository>();
|
||||
var prevFolderScanEnabled = folderScanEnabled;
|
||||
// check if folder scanning is enabled
|
||||
folderScanEnabled = bool.Parse(settingsRepository.Get(Settings.FolderScanEnabled.AsString())?.Value ?? "false");
|
||||
|
||||
folderScanEnabled = bool.Parse((await settingsRepository.GetAsync(Settings.FolderScanEnabled.AsString(), cancellationToken))?.Value ?? "false");
|
||||
|
||||
if (folderScanEnabled && !prevFolderScanEnabled) {
|
||||
FolderRepository.FolderAdded += OnFolderAdded;
|
||||
FolderRepository.FolderRemoved += OnFolderRemoved;
|
||||
// defaults to 30 minutes
|
||||
int.TryParse(settingsRepository.Get(Settings.FolderScanInterval.AsString())?.Value ?? "30", out var interval);
|
||||
int.TryParse((await settingsRepository.GetAsync(Settings.FolderScanInterval.AsString(), cancellationToken))?.Value ?? "30", out var interval);
|
||||
fileSystemCrawl = new Timer(QueueFileSystemCrawlTimer, null, TimeSpan.Zero, TimeSpan.FromMinutes(interval));
|
||||
logger?.LogInformation($"Periodic folder scanning is now enabled every {interval} minutes.");
|
||||
|
||||
|
||||
var folderRepository = scope.ServiceProvider.GetRequiredService<IFolderRepository>();
|
||||
folders = folderRepository.GetAll().Where(f => f.Active).ToList();
|
||||
} else if(!folderScanEnabled && prevFolderScanEnabled) {
|
||||
@@ -63,13 +66,13 @@ public class JobScheduler(IServiceProvider serviceProvider, ILogger<JobScheduler
|
||||
logger?.LogInformation("Periodic folder scanning is now disabled.");
|
||||
} else if (folderScanEnabled && prevFolderScanEnabled) {
|
||||
// update interval
|
||||
int.TryParse(settingsRepository.Get(Settings.FolderScanInterval.AsString())?.Value ?? "30", out var interval);
|
||||
int.TryParse((await settingsRepository.GetAsync(Settings.FolderScanInterval.AsString(), cancellationToken))?.Value ?? "30", out var interval);
|
||||
fileSystemCrawl?.Change(TimeSpan.Zero, TimeSpan.FromMinutes(interval));
|
||||
logger?.LogInformation($"Periodic folder scanning interval updated to {interval} minutes.");
|
||||
}
|
||||
|
||||
// Job cleanup timer
|
||||
int.TryParse(settingsRepository.Get(Settings.JobRetentionDays.AsString())?.Value, out var retentionDays);
|
||||
int.TryParse((await settingsRepository.GetAsync(Settings.JobRetentionDays.AsString(), cancellationToken))?.Value, out var retentionDays);
|
||||
if (retentionDays > 0) {
|
||||
if (jobCleanup == null) {
|
||||
jobCleanup = new Timer(RunJobCleanup, null, TimeSpan.Zero, TimeSpan.FromHours(1));
|
||||
@@ -81,70 +84,70 @@ public class JobScheduler(IServiceProvider serviceProvider, ILogger<JobScheduler
|
||||
logger?.LogInformation("Job cleanup is now disabled (retention set to 0 or invalid).");
|
||||
}
|
||||
|
||||
EnsureJobSettings(settingsRepository);
|
||||
await EnsureJobSettingsAsync(settingsRepository, cancellationToken);
|
||||
}
|
||||
|
||||
void EnsureJobSettings(ISettingsRepository settingsRepository) {
|
||||
async Task EnsureJobSettingsAsync(ISettingsRepository settingsRepository, CancellationToken cancellationToken) {
|
||||
var batchSizeName = Settings.JobBatchSize.AsString();
|
||||
if (settingsRepository.Get(batchSizeName) == null) {
|
||||
settingsRepository.Create(new Setting {
|
||||
if (await settingsRepository.GetAsync(batchSizeName, cancellationToken) == null) {
|
||||
await settingsRepository.CreateAsync(new Setting {
|
||||
Name = batchSizeName,
|
||||
Value = "200",
|
||||
Description = "Number of assets to process per sub-job batch.",
|
||||
Type = EType.Integer,
|
||||
DisplayType = DisplayType.IntNumber
|
||||
});
|
||||
}, cancellationToken);
|
||||
logger?.LogInformation("Seeded default setting: {Name} = 200.", batchSizeName);
|
||||
}
|
||||
|
||||
var convertedPathName = Settings.ConvertedPath.AsString();
|
||||
if (settingsRepository.Get(convertedPathName) == null) {
|
||||
settingsRepository.Create(new Setting {
|
||||
if (await settingsRepository.GetAsync(convertedPathName, cancellationToken) == null) {
|
||||
await settingsRepository.CreateAsync(new Setting {
|
||||
Name = convertedPathName,
|
||||
Value = Path.Combine(Environment.CurrentDirectory, "converted"),
|
||||
Description = "Path where converted videos for animated assets (GIF -> mp4) are stored.",
|
||||
Type = EType.String,
|
||||
DisplayType = DisplayType.Text
|
||||
});
|
||||
}, cancellationToken);
|
||||
logger?.LogInformation("Seeded default setting: {Name} = {Value}.", convertedPathName, Path.Combine(Environment.CurrentDirectory, "converted"));
|
||||
}
|
||||
|
||||
var convertedFormatName = Settings.ConvertedFormat.AsString();
|
||||
if (settingsRepository.Get(convertedFormatName) == null) {
|
||||
settingsRepository.Create(new Setting {
|
||||
if (await settingsRepository.GetAsync(convertedFormatName, cancellationToken) == null) {
|
||||
await settingsRepository.CreateAsync(new Setting {
|
||||
Name = convertedFormatName,
|
||||
Value = "mp4",
|
||||
Description = "Container format for converted videos (e.g. mp4).",
|
||||
Type = EType.String,
|
||||
DisplayType = DisplayType.Text
|
||||
});
|
||||
}, cancellationToken);
|
||||
logger?.LogInformation("Seeded default setting: {Name} = mp4.", convertedFormatName);
|
||||
}
|
||||
|
||||
var ffmpegPathName = Settings.FfmpegPath.AsString();
|
||||
if (settingsRepository.Get(ffmpegPathName) == null) {
|
||||
settingsRepository.Create(new Setting {
|
||||
if (await settingsRepository.GetAsync(ffmpegPathName, cancellationToken) == null) {
|
||||
await settingsRepository.CreateAsync(new Setting {
|
||||
Name = ffmpegPathName,
|
||||
Value = "ffmpeg",
|
||||
Description = "Path to the ffmpeg executable. Defaults to 'ffmpeg' on PATH.",
|
||||
Type = EType.String,
|
||||
DisplayType = DisplayType.Text
|
||||
});
|
||||
}, cancellationToken);
|
||||
logger?.LogInformation("Seeded default setting: {Name} = ffmpeg.", ffmpegPathName);
|
||||
}
|
||||
}
|
||||
|
||||
void RunJobCleanup(object? state) {
|
||||
async void RunJobCleanup(object? state) {
|
||||
try {
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
var settingsRepository = scope.ServiceProvider.GetRequiredService<ISettingsRepository>();
|
||||
int.TryParse(settingsRepository.Get(Settings.JobRetentionDays.AsString())?.Value, out var retentionDays);
|
||||
int.TryParse((await settingsRepository.GetAsync(Settings.JobRetentionDays.AsString(), CancellationToken.None))?.Value, out var retentionDays);
|
||||
if (retentionDays <= 0) return;
|
||||
|
||||
var cutoff = DateTime.UtcNow.AddDays(-retentionDays);
|
||||
var repo = scope.ServiceProvider.GetRequiredService<IJobRecordRepository>();
|
||||
var deleted = repo.DeleteOldJobs(cutoff);
|
||||
repo.Save();
|
||||
var deleted = await repo.DeleteOldJobsAsync(cutoff, CancellationToken.None);
|
||||
await repo.SaveAsync(CancellationToken.None);
|
||||
|
||||
if (jobManager != null) {
|
||||
jobManager.CleanupPastJobs(cutoff);
|
||||
@@ -167,16 +170,20 @@ public class JobScheduler(IServiceProvider serviceProvider, ILogger<JobScheduler
|
||||
logger?.LogInformation($"Folder removed: {e.Id}");
|
||||
}
|
||||
|
||||
void OnSettingsChanged(object? sender, Setting e) {
|
||||
var name = e.Name;
|
||||
if (name == Settings.FolderScanEnabled.AsString()
|
||||
|| name == Settings.FolderScanInterval.AsString()
|
||||
|| name == Settings.JobRetentionDays.AsString()) {
|
||||
FetchSettings();
|
||||
}
|
||||
if (name == Settings.MaxConcurrentJobs.AsString() && jobManager != null && int.TryParse(e.Value, out var maxJobs) && maxJobs > 0) {
|
||||
jobManager.MaxConcurrentJobs = maxJobs;
|
||||
logger?.LogInformation("MaxConcurrentJobs updated to {Value}.", maxJobs);
|
||||
async void OnSettingsChanged(object? sender, Setting e) {
|
||||
try {
|
||||
var name = e.Name;
|
||||
if (name == Settings.FolderScanEnabled.AsString()
|
||||
|| name == Settings.FolderScanInterval.AsString()
|
||||
|| name == Settings.JobRetentionDays.AsString()) {
|
||||
await FetchSettingsAsync();
|
||||
}
|
||||
if (name == Settings.MaxConcurrentJobs.AsString() && jobManager != null && int.TryParse(e.Value, out var maxJobs) && maxJobs > 0) {
|
||||
jobManager.MaxConcurrentJobs = maxJobs;
|
||||
logger?.LogInformation("MaxConcurrentJobs updated to {Value}.", maxJobs);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
logger?.LogWarning(ex, "Failed to apply settings change for {SettingName}.", e.Name);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user