From c51ff4e755f009ca0bc8e935a92c04e583c4ee8a Mon Sep 17 00:00:00 2001 From: Jake Mannens Date: Tue, 17 Mar 2026 03:04:36 +1100 Subject: Initial commit --- Controllers/ApiMediaController.cs | 227 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 Controllers/ApiMediaController.cs (limited to 'Controllers/ApiMediaController.cs') diff --git a/Controllers/ApiMediaController.cs b/Controllers/ApiMediaController.cs new file mode 100644 index 0000000..bb6c81e --- /dev/null +++ b/Controllers/ApiMediaController.cs @@ -0,0 +1,227 @@ +using HyperBooru.ApiModels; +using HyperBooru.Services; +using HyperBooru.Util; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using System.Text.Json; + +namespace HyperBooru.Controllers; + +[ApiController] +[Authorize] +[Route("/api/media")] +public class ApiMediaController : Controller { + private IDbContextFactory dbFactory; + private IMediaService mediaService; + + public ApiMediaController(IDbContextFactory dbFactory, IMediaService mediaService) { + this.dbFactory = dbFactory; + this.mediaService = mediaService; + } + + [HttpGet("{mediaId}")] + public async Task Get([FromRoute] Guid mediaId) { + using var db = dbFactory.CreateDbContext(); + + var media = await db.Media.FirstOrDefaultAsync(m => m.Guid == mediaId); + + if(media is null) + throw new ObjectNotFoundException([ mediaId ]); + + return Ok((ApiModels.Media) media); + } + + [HttpGet("{mediaId}/files")] + public async Task GetUploadedFiles([FromRoute] Guid mediaId) { + using var db = dbFactory.CreateDbContext(); + + var media = await db.Media + .Include(m => m.UploadedFiles) + .FirstOrDefaultAsync(m => m.Guid == mediaId); + + if(media is null) + throw new ObjectNotFoundException([ mediaId ]); + + return Ok(media.UploadedFiles.Select(uf => (ApiModels.UploadedFile) uf).ToArray()); + } + + [HttpPatch] + public async Task UpdateMedia([FromBody] ApiModels.Media updatedMedia) { + using var db = dbFactory.CreateDbContext(); + using var transaction = await db.Database.BeginTransactionAsync(); + + var media = await db.Media.FirstOrDefaultAsync(m => m.Guid == updatedMedia.MediaId); + if(media is null) + throw new ObjectNotFoundException([ updatedMedia.MediaId ]); + + media.ShortDescription = updatedMedia.ShortDescription?.NullIfEmpty(); + media.LongDescription = updatedMedia.LongDescription?.NullIfEmpty(); + + await db.SaveChangesAsync(); + await transaction.CommitAsync(); + + return Ok(); + } + + [HttpPost] + public IActionResult Upload() { + if(Request.Form.Files.Count == 0) + throw new ApiModels.ArgumentException("No files"); + if(Request.Form.Files.Count > 1) + throw new ApiModels.ArgumentException("More than one file supplied"); + + var metadataString = Request.Form.Files + .First() + .Headers["X-HyperBooru-Metadata"] + .ElementAtOrDefault(0); + + MediaUploadRequest? metadata = metadataString is null ? null : + JsonSerializer.Deserialize(metadataString); + + var formFile = Request.Form.Files.First(); + + var media = mediaService.Create( + formFile.OpenReadStream(), + formFile.FileName, + metadata?.Checksum, + metadata?.LastAccessTime, + metadata?.LastWriteTime, + metadata?.CreateTime, + metadata?.Path.NullIfEmpty(), + metadata?.PathType, + metadata?.Tags); + + return Ok((ApiModels.Media) media); + } + + [HttpDelete("{mediaId}")] + public void Delete([FromRoute] Guid mediaId) => + mediaService.Delete(mediaId); + + [HttpGet("{mediaId}/tags")] + public async Task GetMediaTagsAsync([FromRoute] Guid mediaId) { + using var db = dbFactory.CreateDbContext(); + + 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) + throw new ObjectNotFoundException([ mediaId ]); + + return Ok(media.Tags.Select(t => (ApiModels.TagDefinition) t.TagDefinition).ToArray()); + } + + [HttpPatch("{mediaId}/tags")] + public async Task AddTagsToMediaAsync( + [FromRoute] Guid mediaId, + [FromBody] Guid[] tagIds) { + + 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) + missing.Add(mediaId); + + tagIds = tagIds.Distinct().ToArray(); + + var tags = await db.TagDefinitions + .Where(td => tagIds.Contains(td.Guid)) + .ToArrayAsync(); + + 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)) + .Select(td => new Tag() { TagDefinition = td })); + + await db.SaveChangesAsync(); + await transaction.CommitAsync(); + + return Ok(media.Tags.Select(t => (ApiModels.TagDefinition) t.TagDefinition).ToArray()); + } + + [HttpPut("{mediaId}/tags")] + public async Task ReplaceMediaTagsAsync( + [FromRoute] Guid mediaId, + [FromBody] Guid[] tagIds) { + + 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) + missing.Add(mediaId); + + tagIds = tagIds.Distinct().Order().ToArray(); + var tags = await db.TagDefinitions + .Where(td => tagIds.Contains(td.Guid)) + .ToArrayAsync(); + + 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)) + .Select(td => new Tag() { TagDefinition = td })); + + db.Tags.RemoveRange( + media.Tags.Where(t => !tagIds.Contains(t.TagDefinition.Guid))); + + await db.SaveChangesAsync(); + await transaction.CommitAsync(); + + return Ok(media.Tags.Select(t => (ApiModels.TagDefinition) t.TagDefinition).ToArray()); + } + + [HttpPatch("{mediaId}/tags/delete")] + public async Task DeleteTagsFromMediaAsync( + [FromRoute] Guid mediaId, + [FromBody] Guid[] tagIds) { + + 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) + missing.Add(mediaId); + + tagIds = tagIds.Distinct().Order().ToArray(); + + 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))); + + await db.SaveChangesAsync(); + await transaction.CommitAsync(); + + return Ok(media.Tags.Select(t => (ApiModels.TagDefinition) t.TagDefinition).ToArray()); + } +} -- cgit v1.3