From c3e9d39034e5afc3f2c3a12c8c7682eabe360b7d Mon Sep 17 00:00:00 2001 From: Jake Mannens Date: Fri, 29 May 2026 22:35:18 +1000 Subject: Modified controller endpoints to throw exceptions instead of returning HTTP responses on error --- Controllers/ApiFeedController.cs | 2 +- Controllers/ApiMediaController.cs | 47 ++++++++------- Controllers/ApiTagController.cs | 81 +++++++++++++------------- Controllers/ApiUserController.cs | 20 ++++--- Controllers/MediaController.cs | 116 +++++++++++++++++--------------------- 5 files changed, 134 insertions(+), 132 deletions(-) (limited to 'Controllers') diff --git a/Controllers/ApiFeedController.cs b/Controllers/ApiFeedController.cs index 382169e..068cc17 100644 --- a/Controllers/ApiFeedController.cs +++ b/Controllers/ApiFeedController.cs @@ -18,7 +18,7 @@ public class ApiFeedController : Controller { [HttpPost] public IActionResult FetchChunkAsync([FromBody] FeedRequest feedRequest) { if(feedRequest.Count > 1000) - return BadRequest("Total number of requested items exceeds maximum"); + throw new ApiModels.ArgumentException("Total number of requested items exceeds maximum"); return Ok(feedService.LoadChunk(feedRequest).Select(m => m.Guid).ToArray()); } diff --git a/Controllers/ApiMediaController.cs b/Controllers/ApiMediaController.cs index 58fd043..bb6c81e 100644 --- a/Controllers/ApiMediaController.cs +++ b/Controllers/ApiMediaController.cs @@ -27,7 +27,7 @@ public class ApiMediaController : Controller { var media = await db.Media.FirstOrDefaultAsync(m => m.Guid == mediaId); if(media is null) - throw new ObjectNotFoundException(mediaId); + throw new ObjectNotFoundException([ mediaId ]); return Ok((ApiModels.Media) media); } @@ -41,7 +41,7 @@ public class ApiMediaController : Controller { .FirstOrDefaultAsync(m => m.Guid == mediaId); if(media is null) - throw new ObjectNotFoundException(mediaId); + throw new ObjectNotFoundException([ mediaId ]); return Ok(media.UploadedFiles.Select(uf => (ApiModels.UploadedFile) uf).ToArray()); } @@ -53,10 +53,10 @@ public class ApiMediaController : Controller { var media = await db.Media.FirstOrDefaultAsync(m => m.Guid == updatedMedia.MediaId); if(media is null) - return NotFound(); + throw new ObjectNotFoundException([ updatedMedia.MediaId ]); - media.ShortDescription = updatedMedia.ShortDescription.NullIfEmpty(); - media.LongDescription = updatedMedia.LongDescription.NullIfEmpty(); + media.ShortDescription = updatedMedia.ShortDescription?.NullIfEmpty(); + media.LongDescription = updatedMedia.LongDescription?.NullIfEmpty(); await db.SaveChangesAsync(); await transaction.CommitAsync(); @@ -67,9 +67,9 @@ public class ApiMediaController : Controller { [HttpPost] public IActionResult Upload() { if(Request.Form.Files.Count == 0) - return BadRequest("No files"); + throw new ApiModels.ArgumentException("No files"); if(Request.Form.Files.Count > 1) - return BadRequest("More than one file supplied"); + throw new ApiModels.ArgumentException("More than one file supplied"); var metadataString = Request.Form.Files .First() @@ -109,7 +109,7 @@ public class ApiMediaController : Controller { .ThenInclude(td => td.ImplicitTags) .FirstOrDefaultAsync(m => m.Guid == mediaId); if(media is null) - return NotFound(); + throw new ObjectNotFoundException([ mediaId ]); return Ok(media.Tags.Select(t => (ApiModels.TagDefinition) t.TagDefinition).ToArray()); } @@ -122,13 +122,15 @@ public class ApiMediaController : Controller { using var db = dbFactory.CreateDbContext(); using var transaction = await db.Database.BeginTransactionAsync(); + var missing = new List(); + var media = await db.Media .Include(m => m.Tags) .ThenInclude(t => t.TagDefinition) .ThenInclude(td => td.ImplicitTags) .FirstOrDefaultAsync(m => m.Guid == mediaId); if(media is null) - return NotFound(); + missing.Add(mediaId); tagIds = tagIds.Distinct().ToArray(); @@ -136,8 +138,9 @@ public class ApiMediaController : Controller { .Where(td => tagIds.Contains(td.Guid)) .ToArrayAsync(); - if(tags.Count() < tagIds.Count()) - return NotFound("Invalid tag IDs specified"); + missing.AddRange(tagIds.Except(tags.Select(td => td.Guid))); + if(missing.Any()) + throw new ObjectNotFoundException(missing); media.Tags.AddRange(tags .Where(td => !media.Tags.Select(t => t.TagDefinition.Guid).Contains(td.Guid)) @@ -157,23 +160,24 @@ public class ApiMediaController : Controller { using var db = dbFactory.CreateDbContext(); using var transaction = await db.Database.BeginTransactionAsync(); + var missing = new List(); + var media = await db.Media .Include(m => m.Tags) .ThenInclude(t => t.TagDefinition) .ThenInclude(td => td.ImplicitTags) .FirstOrDefaultAsync(m => m.Guid == mediaId); if(media is null) - return NotFound(); + missing.Add(mediaId); tagIds = tagIds.Distinct().Order().ToArray(); var tags = await db.TagDefinitions .Where(td => tagIds.Contains(td.Guid)) .ToArrayAsync(); - var missingTags = tagIds.Except(tags.Select(td => td.Guid)); - var missingTagsString = string.Join(", ", missingTags.Select(t => t.ToString())); - if(missingTags.Any()) - return BadRequest($"Invalid tag IDs specified: {missingTagsString}"); + missing.AddRange(tagIds.Except(tags.Select(td => td.Guid))); + if(missing.Any()) + throw new ObjectNotFoundException(missing); media.Tags.AddRange(tags .Where(td => !media.Tags.Select(t => t.TagDefinition.Guid).Contains(td.Guid)) @@ -196,20 +200,21 @@ public class ApiMediaController : Controller { using var db = dbFactory.CreateDbContext(); using var transaction = await db.Database.BeginTransactionAsync(); + var missing = new List(); + var media = await db.Media .Include(m => m.Tags) .ThenInclude(t => t.TagDefinition) .ThenInclude(td => td.ImplicitTags) .FirstOrDefaultAsync(m => m.Guid == mediaId); if(media is null) - return NotFound(); + missing.Add(mediaId); tagIds = tagIds.Distinct().Order().ToArray(); - var missingTags = tagIds.Except(media.Tags.Select(t => t.TagDefinition.Guid)); - var missingTagsString = string.Join(", ", missingTags.Select(t => t.ToString())); - if(missingTags.Any()) - return BadRequest($"Media does not contain the following tags: {missingTagsString}"); + missing.AddRange(tagIds.Except(media.Tags.Select(t => t.TagDefinition.Guid))); + if(missing.Any()) + throw new ObjectNotFoundException(missing); db.Tags.RemoveRange( media.Tags.Where(t => tagIds.Contains(t.TagDefinition.Guid))); diff --git a/Controllers/ApiTagController.cs b/Controllers/ApiTagController.cs index f48cc05..d1e49ee 100644 --- a/Controllers/ApiTagController.cs +++ b/Controllers/ApiTagController.cs @@ -34,7 +34,10 @@ public class ApiTagController : Controller { .Include(td => td.ImplicitTags) .FirstOrDefaultAsync(td => td.Guid == tagDefinitionId); - return tagDefinition is not null ? Ok(tagDefinition) : NotFound(); + if(tagDefinition is null) + throw new ObjectNotFoundException([ tagDefinitionId ]); + + return Ok(tagDefinition); } [HttpPost("definition")] @@ -42,12 +45,12 @@ public class ApiTagController : Controller { using var db = dbFactory.CreateDbContext(); using var transaction = await db.Database.BeginTransactionAsync(); - if(db.TagDefinitions.Any(td => td.Name == request.Name)) - return BadRequest("Name already exists"); + var nameExists = db.TagDefinitions.Any(td => td.Name == request.Name); + var aliasExists = + request.Alias is not null && db.TagDefinitions.Any(td => td.Alias == request.Alias); - if(request.Alias is not null) - if(db.TagDefinitions.Any(td => td.Alias == request.Alias)) - return BadRequest("Alias already exists"); + if(nameExists || aliasExists) + throw new TagDuplicateException(nameExists, aliasExists); List implicitTags = new(); if(request.ImplicitTags is not null) { @@ -81,10 +84,10 @@ public class ApiTagController : Controller { .FirstOrDefaultAsync(td => td.Guid == tagDefinitionId); if(tagDefinition is null) - return NotFound("Tag definition not found"); + throw new ObjectNotFoundException([ tagDefinitionId ]); if(tagDefinition.ObjectId < 0) - return BadRequest("Cannot delete built-in tag definition"); + throw new ApiModels.ArgumentException("Cannot delete built-in tag definition"); db.TagDefinitions.Remove(tagDefinition); @@ -106,18 +109,18 @@ public class ApiTagController : Controller { .FirstOrDefaultAsync(td => td.Guid == tagDefinitionId); if(tagDefinition is null) - return NotFound("Tag definition not found"); + throw new ObjectNotFoundException([ tagDefinitionId ]); if(tagDefinition.ObjectId < 0) - return BadRequest("Cannot update built-in tag definition"); + throw new ApiModels.ArgumentException("Cannot update built-in tag definition"); - if(request.Name is not null) - if(db.TagDefinitions.Any(td => td.Name == request.Name)) - return BadRequest("Name already exists"); + var nameExists = + request.Name is not null && db.TagDefinitions.Any(td => td.Name == request.Name); + var aliasExists = + request.Alias is not null && db.TagDefinitions.Any(td => td.Alias == request.Alias); - if(request.Alias is not null) - if(db.TagDefinitions.Any(td => td.Alias == request.Alias)) - return BadRequest("Alias already exists"); + if(nameExists || aliasExists) + throw new TagDuplicateException(nameExists, aliasExists); tagDefinition.Namespace = request.Namespace ?? tagDefinition.Namespace; tagDefinition.Name = request.Name ?? tagDefinition.Name; @@ -137,15 +140,14 @@ public class ApiTagController : Controller { using var db = dbFactory.CreateDbContext(); using var transaction = await db.Database.BeginTransactionAsync(); + var missing = new List(); + var tagDefinition = await db.TagDefinitions .Include(td => td.ImplicitTags) .FirstOrDefaultAsync(td => td.Guid == tagDefinitionId); if(tagDefinition is null) - return NotFound("Tag definition not found"); - - if(tagDefinition.ObjectId < 0) - return BadRequest("Cannot update built-in tag definition"); + missing.Add(tagDefinitionId); implicitTagIds = implicitTagIds.Distinct().ToArray(); @@ -153,10 +155,12 @@ public class ApiTagController : Controller { .Where(td => implicitTagIds.Contains(td.Guid)) .ToArray(); - var missingTags = implicitTagIds.Except(implicitTags.Select(td => td.Guid)); - var missingTagsString = string.Join(", ", missingTags.Select(td => td.ToString())); - if(missingTags.Any()) - return BadRequest($"Invalid tag IDs specified: {missingTagsString}"); + missing.AddRange(implicitTagIds.Except(implicitTags.Select(td => td.Guid))); + if(missing.Any()) + throw new ObjectNotFoundException(missing); + + if(tagDefinition!.ObjectId < 0) + throw new ApiModels.ArgumentException("Cannot update built-in tag definition"); tagDefinition.ImplicitTags.AddRange( implicitTags.ExceptBy(tagDefinition.ImplicitTags.Select(td => td.Guid), td => td.Guid)); @@ -175,15 +179,14 @@ public class ApiTagController : Controller { using var db = dbFactory.CreateDbContext(); using var transaction = await db.Database.BeginTransactionAsync(); + var missing = new List(); + var tagDefinition = await db.TagDefinitions .Include(td => td.ImplicitTags) .FirstOrDefaultAsync(td => td.Guid == tagDefinitionId); if(tagDefinition is null) - return NotFound("Tag definition not found"); - - if(tagDefinition.ObjectId < 0) - return BadRequest("Cannot update built-in tag definition"); + missing.Add(tagDefinitionId); implicitTagIds = implicitTagIds.Distinct().ToArray(); @@ -191,10 +194,12 @@ public class ApiTagController : Controller { .Where(td => implicitTagIds.Contains(td.Guid)) .ToArray(); - var missingTags = implicitTagIds.Except(implicitTags.Select(td => td.Guid)); - var missingTagsString = string.Join(", ", missingTags.Select(td => td.ToString())); - if(missingTags.Any()) - return BadRequest($"Invalid tag IDs specified: {missingTagsString}"); + missing.AddRange(implicitTagIds.Except(implicitTags.Select(td => td.Guid))); + if(missing.Any()) + throw new ObjectNotFoundException(missing); + + if(tagDefinition!.ObjectId < 0) + throw new ApiModels.ArgumentException("Cannot update built-in tag definition"); tagDefinition.ImplicitTags.AddRange( implicitTags.ExceptBy(tagDefinition.ImplicitTags.Select(td => td.Guid), td => td.Guid)); @@ -225,18 +230,16 @@ public class ApiTagController : Controller { .FirstOrDefaultAsync(td => td.Guid == tagDefinitionId); if(tagDefinition is null) - return NotFound("Tag definition not found"); + throw new ObjectNotFoundException([ tagDefinitionId ]); if(tagDefinition.ObjectId < 0) - return BadRequest("Cannot update built-in tag definition"); + throw new ApiModels.ArgumentException("Cannot update built-in tag definition"); implicitTagIds = implicitTagIds.Distinct().ToArray(); - var missingTagIds = implicitTagIds - .Except(tagDefinition.ImplicitTags.Select(td => td.Guid)); - var missingTagsString = string.Join(", ", missingTagIds.Select(td => td.ToString())); - if(missingTagIds.Any()) - return BadRequest($"Invalid tag IDs specified: {missingTagsString}"); + var missingTags = implicitTagIds.Except(tagDefinition.ImplicitTags.Select(td => td.Guid)); + if(missingTags.Any()) + throw new ObjectNotFoundException(missingTags); var toRemove = tagDefinition.ImplicitTags .Where(td => !implicitTagIds.Contains(td.Guid)) diff --git a/Controllers/ApiUserController.cs b/Controllers/ApiUserController.cs index d678287..3230218 100644 --- a/Controllers/ApiUserController.cs +++ b/Controllers/ApiUserController.cs @@ -1,4 +1,5 @@ -using HyperBooru.Services; +using HyperBooru.ApiModels; +using HyperBooru.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -30,7 +31,10 @@ public class ApiUserController : Controller { var user = await db.Users .FirstOrDefaultAsync(u => u.Guid == userId); - return user is null ? NotFound() : Ok((ApiModels.User) user); + if(user is null) + throw new ObjectNotFoundException([ userId ]); + + return Ok((ApiModels.User) user); } [HttpPost] @@ -40,7 +44,7 @@ public class ApiUserController : Controller { using var transaction = await db.Database.BeginTransactionAsync(); if(await db.Users.AnyAsync(u => u.Username == request.Username)) - return BadRequest("Username already exists"); + throw new ApiModels.ArgumentException("Username already exists"); var user = new User() { Username = request.Username, @@ -66,17 +70,17 @@ public class ApiUserController : Controller { var user = await db.Users.FirstOrDefaultAsync(u => u.Guid == userId); if(user is null) - return NotFound(); + throw new ObjectNotFoundException([ userId ]); if(request.Username is not null) { if(string.IsNullOrWhiteSpace(request.Username)) - return BadRequest("Username cannot be empty"); + throw new ApiModels.ArgumentException("Username cannot be empty"); user.Username = request.Username; } if(request.Password is not null) { if(string.IsNullOrWhiteSpace(request.Password)) - return BadRequest("Password cannot be empty"); + throw new ApiModels.ArgumentException("Password cannot be empty"); user.PasswordHash = UserService.HashPassword(request.Password); } @@ -89,7 +93,7 @@ public class ApiUserController : Controller { [HttpDelete("{userId}")] public async Task DeleteUserAsync([FromRoute] Guid userId) { if(userId == HBContext.AdminUser) - return BadRequest("Cannot delete the admin user"); + throw new ApiModels.ArgumentException("Cannot delete the admin user"); using var db = dbFactory.CreateDbContext(); @@ -97,7 +101,7 @@ public class ApiUserController : Controller { var user = await db.Users.FirstOrDefaultAsync(u => u.Guid == userId); if(user is null) - return NotFound(); + throw new ObjectNotFoundException([ userId ]); db.Users.Remove(user); diff --git a/Controllers/MediaController.cs b/Controllers/MediaController.cs index 6a9e1fc..248765a 100644 --- a/Controllers/MediaController.cs +++ b/Controllers/MediaController.cs @@ -39,7 +39,7 @@ public class MediaController : Controller { .Include(m => m.CurrentUploadedFile) .First(m => m.Guid == mediaId); if(media is null) - return NotFound(); + throw new ObjectNotFoundException([ mediaId ]); // Check if the requested media item is a HEIC image and if it is, convert it // otherwise, return the original file content, unaltered @@ -68,14 +68,8 @@ public class MediaController : Controller { [FromQuery(Name = "w")] int? width, [FromQuery(Name = "h")] int? height) { - try { - var thumb = mediaService.GetThumbnail(mediaId, width, height); - return new FileStreamResult(thumb, "image/jpeg"); - } catch(ThumbnailException e) { - return BadRequest(e.Message); - } catch(ObjectNotFoundException e) { - return NotFound(e.Message); - } + var thumb = mediaService.GetThumbnail(mediaId, width, height); + return new FileStreamResult(thumb, "image/jpeg"); } [HttpDelete("{mediaId}")] @@ -86,65 +80,61 @@ public class MediaController : Controller { [HttpPost] public IActionResult Upload() { if(Request.Form.Files.Count == 0) - return BadRequest("No files"); + throw new ApiModels.ArgumentException("No files"); Media media = new(); foreach(var formFile in Request.Form.Files) { - try { - // Parse timestamps from headers - DateTime? lastAccessTime = - formFile.Headers["X-HyperBooru-LastAccessTime"] - .ElementAtOrDefault(0)? - .TryParseDateTimeUtc(); - DateTime? lastWriteTime = - formFile.Headers["X-HyperBooru-LastWriteTime"] - .ElementAtOrDefault(0)? - .TryParseDateTimeUtc(); - DateTime? createTime = - formFile.Headers["X-HyperBooru-CreateTime"] - .ElementAtOrDefault(0)? - .TryParseDateTimeUtc(); - - // Parse original path from headers - string? path = - formFile.Headers["X-HyperBooru-Path"] - .ElementAtOrDefault(0); - - object? pathType = null; - string? pathTypeString = - formFile.Headers["X-HyperBooru-PathType"] - .ElementAtOrDefault(0); - Enum.TryParse(typeof(PathType), pathTypeString, true, out pathType); - - // Parse tag IDs from headers - Guid[]? tagIds = formFile.Headers["X-HyperBooru-Tags"] + // Parse timestamps from headers + DateTime? lastAccessTime = + formFile.Headers["X-HyperBooru-LastAccessTime"] + .ElementAtOrDefault(0)? + .TryParseDateTimeUtc(); + DateTime? lastWriteTime = + formFile.Headers["X-HyperBooru-LastWriteTime"] + .ElementAtOrDefault(0)? + .TryParseDateTimeUtc(); + DateTime? createTime = + formFile.Headers["X-HyperBooru-CreateTime"] .ElementAtOrDefault(0)? - .Split(',') - .Select(t => Guid.Parse(t)) - .ToArray(); - - media = mediaService.Create( - formFile.OpenReadStream(), - formFile.FileName, - formFile.Headers["X-HyperBooru-Checksum"] - .ElementAtOrDefault(0), - lastAccessTime, - lastWriteTime, - createTime, - path, - (PathType?) pathType, - tagIds); - - // Return the GUID of the new media object if requested - bool returnMetadataParsed = bool.TryParse( - formFile.Headers["X-HyperBooru-ReturnMediaId"], out var returnMetadata); - - if(returnMetadataParsed && returnMetadata) - return Content(media.Guid.ToString()); - } catch(MediaCreateException e) { - return BadRequest(e.Message); - } + .TryParseDateTimeUtc(); + + // Parse original path from headers + string? path = + formFile.Headers["X-HyperBooru-Path"] + .ElementAtOrDefault(0); + + object? pathType = null; + string? pathTypeString = + formFile.Headers["X-HyperBooru-PathType"] + .ElementAtOrDefault(0); + Enum.TryParse(typeof(PathType), pathTypeString, true, out pathType); + + // Parse tag IDs from headers + Guid[]? tagIds = formFile.Headers["X-HyperBooru-Tags"] + .ElementAtOrDefault(0)? + .Split(',') + .Select(t => Guid.Parse(t)) + .ToArray(); + + media = mediaService.Create( + formFile.OpenReadStream(), + formFile.FileName, + formFile.Headers["X-HyperBooru-Checksum"] + .ElementAtOrDefault(0), + lastAccessTime, + lastWriteTime, + createTime, + path, + (PathType?) pathType, + tagIds); + + // Return the GUID of the new media object if requested + bool returnMetadataParsed = bool.TryParse( + formFile.Headers["X-HyperBooru-ReturnMediaId"], out var returnMetadata); + + if(returnMetadataParsed && returnMetadata) + return Content(media.Guid.ToString()); } if(Request.Form.Files.Count == 1) -- cgit v1.3