summaryrefslogtreecommitdiff
path: root/Server/Controllers
diff options
context:
space:
mode:
Diffstat (limited to 'Server/Controllers')
-rw-r--r--Server/Controllers/ApiFeedController.cs23
-rw-r--r--Server/Controllers/ApiMediaController.cs219
-rw-r--r--Server/Controllers/ApiTagController.cs252
-rw-r--r--Server/Controllers/ApiUserController.cs109
-rw-r--r--Server/Controllers/MediaController.cs154
5 files changed, 757 insertions, 0 deletions
diff --git a/Server/Controllers/ApiFeedController.cs b/Server/Controllers/ApiFeedController.cs
new file mode 100644
index 0000000..fb260e6
--- /dev/null
+++ b/Server/Controllers/ApiFeedController.cs
@@ -0,0 +1,23 @@
+using HyperBooru.ApiModels;
+using HyperBooru.Services;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+
+namespace HyperBooru.Controllers;
+
+[ApiController]
+[Route("/api/feed")]
+public class ApiFeedController : Controller {
+ private IFeedService feedService;
+
+ public ApiFeedController(IDbContextFactory<HBContext> dbFactory, IFeedService feedService) =>
+ this.feedService = feedService;
+
+ [HttpPost]
+ public IActionResult FetchChunkAsync([FromBody] FeedRequest feedRequest) {
+ if(feedRequest.Count > 1000)
+ return BadRequest("Total number of requested items exceeds maximum");
+
+ return Ok(feedService.LoadChunk(feedRequest).Select(m => m.Guid).ToArray());
+ }
+}
diff --git a/Server/Controllers/ApiMediaController.cs b/Server/Controllers/ApiMediaController.cs
new file mode 100644
index 0000000..5a8ef21
--- /dev/null
+++ b/Server/Controllers/ApiMediaController.cs
@@ -0,0 +1,219 @@
+using HyperBooru.ApiModels;
+using HyperBooru.Services;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using System.Text.Json;
+
+namespace HyperBooru.Controllers;
+
+[ApiController]
+[Route("/api/media")]
+public class ApiMediaController : Controller {
+ private IDbContextFactory<HBContext> dbFactory;
+ private IMediaService mediaService;
+
+ public ApiMediaController(IDbContextFactory<HBContext> dbFactory, IMediaService mediaService) {
+ this.dbFactory = dbFactory;
+ this.mediaService = mediaService;
+ }
+
+ [HttpGet("{mediaId}")]
+ public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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)
+ return NotFound();
+
+ media.ShortDescription = updatedMedia.ShortDescription;
+ media.LongDescription = updatedMedia.LongDescription;
+
+ await db.SaveChangesAsync();
+ await transaction.CommitAsync();
+
+ return Ok();
+ }
+
+ [HttpPost]
+ public IActionResult Upload() {
+ if(Request.Form.Files.Count == 0)
+ return BadRequest("No files");
+ if(Request.Form.Files.Count > 1)
+ return BadRequest("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<MediaUploadRequest>(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,
+ 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<IActionResult> 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)
+ return NotFound();
+
+ return Ok(media.Tags.Select(t => (ApiModels.TagDefinition) t.TagDefinition).ToArray());
+ }
+
+ [HttpPatch("{mediaId}/tags")]
+ public async Task<IActionResult> AddTagsToMediaAsync(
+ [FromRoute] Guid mediaId,
+ [FromBody] Guid[] tagIds) {
+
+ using var db = dbFactory.CreateDbContext();
+ using var transaction = await db.Database.BeginTransactionAsync();
+
+ 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();
+
+ tagIds = tagIds.Distinct().ToArray();
+
+ var tags = await db.TagDefinitions
+ .Where(td => tagIds.Contains(td.Guid))
+ .ToArrayAsync();
+
+ if(tags.Count() < tagIds.Count())
+ return NotFound("Invalid tag IDs specified");
+
+ 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<IActionResult> ReplaceMediaTagsAsync(
+ [FromRoute] Guid mediaId,
+ [FromBody] Guid[] tagIds) {
+
+ using var db = dbFactory.CreateDbContext();
+ using var transaction = await db.Database.BeginTransactionAsync();
+
+ 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();
+
+ 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}");
+
+ 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<IActionResult> DeleteTagsFromMediaAsync(
+ [FromRoute] Guid mediaId,
+ [FromBody] Guid[] tagIds) {
+
+ using var db = dbFactory.CreateDbContext();
+ using var transaction = await db.Database.BeginTransactionAsync();
+
+ 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();
+
+ 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}");
+
+ 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());
+ }
+}
diff --git a/Server/Controllers/ApiTagController.cs b/Server/Controllers/ApiTagController.cs
new file mode 100644
index 0000000..e8417d2
--- /dev/null
+++ b/Server/Controllers/ApiTagController.cs
@@ -0,0 +1,252 @@
+using HyperBooru.ApiModels;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+
+namespace HyperBooru.Controllers;
+
+[ApiController]
+[Route("/api/tag")]
+public class ApiTagController : Controller {
+ private IDbContextFactory<HBContext> dbFactory;
+
+ public ApiTagController(IDbContextFactory<HBContext> dbFactory) =>
+ this.dbFactory = dbFactory;
+
+ [HttpGet("definition")]
+ public async Task<IActionResult> GetAllTagDefinitionsAsync() {
+ using var db = dbFactory.CreateDbContext();
+
+ var definitions = await db.TagDefinitions
+ .Include(td => td.ImplicitTags)
+ .Select(td => (ApiModels.TagDefinition)td)
+ .ToArrayAsync();
+
+ return Ok(definitions);
+ }
+
+ [HttpGet("definition/{tagDefinitionId}")]
+ public async Task<IActionResult> GetTagDefinitionAsync([FromRoute] Guid tagDefinitionId) {
+ using var db = dbFactory.CreateDbContext();
+
+ var tagDefinition = await db.TagDefinitions
+ .Include(td => td.ImplicitTags)
+ .FirstOrDefaultAsync(td => td.Guid == tagDefinitionId);
+
+ return tagDefinition is not null ? Ok(tagDefinition) : NotFound();
+ }
+
+ [HttpPost("definition")]
+ public async Task<IActionResult> CreateTagDefinitionAsync([FromBody] TagCreateRequest request) {
+ 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");
+
+ if(request.Alias is not null)
+ if(db.TagDefinitions.Any(td => td.Alias == request.Alias))
+ return BadRequest("Alias already exists");
+
+ List<TagDefinition> implicitTags = new();
+ if(request.ImplicitTags is not null) {
+ implicitTags = await db.TagDefinitions
+ .Where(td => request.ImplicitTags.Distinct().Contains(td.Guid))
+ .ToListAsync();
+ }
+
+ var tagDefinition = new TagDefinition {
+ Source = TagSource.UserTag,
+ Namespace = request.Namespace,
+ Name = request.Name,
+ Alias = request.Alias,
+ ImplicitTags = implicitTags
+ };
+
+ db.TagDefinitions.Add(tagDefinition);
+
+ await db.SaveChangesAsync();
+ await transaction.CommitAsync();
+
+ return Ok((ApiModels.TagDefinition) tagDefinition);
+ }
+
+ [HttpDelete("definition/{tagDefinitionId}")]
+ public async Task<IActionResult> DeleteTagDefinitionAsync([FromRoute] Guid tagDefinitionId) {
+ using var db = dbFactory.CreateDbContext();
+ using var transaction = await db.Database.BeginTransactionAsync();
+
+ var tagDefinition = await db.TagDefinitions
+ .FirstOrDefaultAsync(td => td.Guid == tagDefinitionId);
+
+ if(tagDefinition is null)
+ return NotFound("Tag definition not found");
+
+ if(tagDefinition.ObjectId < 0)
+ return BadRequest("Cannot delete built-in tag definition");
+
+ db.TagDefinitions.Remove(tagDefinition);
+
+ await db.SaveChangesAsync();
+ await transaction.CommitAsync();
+
+ return Ok();
+ }
+
+ [HttpPatch("definition/{tagDefinitionId}")]
+ public async Task<IActionResult> UpdateTagDefinitionAsync(
+ [FromRoute] Guid tagDefinitionId,
+ [FromBody] TagUpdateRequest request) {
+
+ using var db = dbFactory.CreateDbContext();
+ using var transaction = await db.Database.BeginTransactionAsync();
+
+ var tagDefinition = await db.TagDefinitions
+ .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");
+
+ if(request.Name is not null)
+ if(db.TagDefinitions.Any(td => td.Name == request.Name))
+ return BadRequest("Name already exists");
+
+ if(request.Alias is not null)
+ if(db.TagDefinitions.Any(td => td.Alias == request.Alias))
+ return BadRequest("Alias already exists");
+
+ tagDefinition.Namespace = request.Namespace ?? tagDefinition.Namespace;
+ tagDefinition.Name = request.Name ?? tagDefinition.Name;
+ tagDefinition.Alias = request.Alias ?? tagDefinition.Alias;
+
+ await db.SaveChangesAsync();
+ await transaction.CommitAsync();
+
+ return Ok((ApiModels.TagDefinition) tagDefinition);
+ }
+
+ [HttpPatch("definition/{tagDefinitionId}/implicit")]
+ public async Task<IActionResult> AddImplicitTagsAsync(
+ [FromRoute] Guid tagDefinitionId,
+ [FromBody] Guid[] implicitTagIds) {
+
+ using var db = dbFactory.CreateDbContext();
+ using var transaction = await db.Database.BeginTransactionAsync();
+
+ 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");
+
+ implicitTagIds = implicitTagIds.Distinct().ToArray();
+
+ var implicitTags = db.TagDefinitions
+ .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}");
+
+ tagDefinition.ImplicitTags.AddRange(
+ implicitTags.ExceptBy(tagDefinition.ImplicitTags.Select(td => td.Guid), td => td.Guid));
+
+ await db.SaveChangesAsync();
+ await transaction.CommitAsync();
+
+ return Ok();
+ }
+
+ [HttpPut("definition/{tagDefinitionId}/implicit")]
+ public async Task<IActionResult> ReplaceImplicitTagsAsync(
+ [FromRoute] Guid tagDefinitionId,
+ [FromBody] Guid[] implicitTagIds) {
+
+ using var db = dbFactory.CreateDbContext();
+ using var transaction = await db.Database.BeginTransactionAsync();
+
+ 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");
+
+ implicitTagIds = implicitTagIds.Distinct().ToArray();
+
+ var implicitTags = db.TagDefinitions
+ .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}");
+
+ tagDefinition.ImplicitTags.AddRange(
+ implicitTags.ExceptBy(tagDefinition.ImplicitTags.Select(td => td.Guid), td => td.Guid));
+
+ var toRemove = tagDefinition.ImplicitTags
+ .Where(td => !implicitTags.Select(td => td.Guid).Contains(td.Guid))
+ .ToArray();
+
+ foreach(var td in toRemove)
+ tagDefinition.ImplicitTags.Remove(td);
+
+ await db.SaveChangesAsync();
+ await transaction.CommitAsync();
+
+ return Ok();
+ }
+
+ [HttpPost("definition/{tagDefinitionId}/implicit/delete")]
+ public async Task<IActionResult> DeleteImplicitTagAsync(
+ [FromRoute] Guid tagDefinitionId,
+ [FromBody] Guid[] implicitTagIds) {
+
+ using var db = dbFactory.CreateDbContext();
+ using var transaction = await db.Database.BeginTransactionAsync();
+
+ 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");
+
+ 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 toRemove = tagDefinition.ImplicitTags
+ .Where(td => !implicitTagIds.Contains(td.Guid))
+ .ToArray();
+
+ foreach(var td in toRemove)
+ tagDefinition.ImplicitTags.Remove(td);
+
+ await db.SaveChangesAsync();
+ await transaction.CommitAsync();
+
+ return Ok();
+ }
+}
diff --git a/Server/Controllers/ApiUserController.cs b/Server/Controllers/ApiUserController.cs
new file mode 100644
index 0000000..d678287
--- /dev/null
+++ b/Server/Controllers/ApiUserController.cs
@@ -0,0 +1,109 @@
+using HyperBooru.Services;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+
+namespace HyperBooru.Controllers;
+
+[ApiController]
+[Authorize]
+[Route("/api/user")]
+public class ApiUserController : Controller {
+ private IDbContextFactory<HBContext> dbFactory;
+
+ public ApiUserController(IDbContextFactory<HBContext> dbFactory) =>
+ this.dbFactory = dbFactory;
+
+ [HttpGet]
+ public async Task<IActionResult> GetAllUsersAsync() {
+ using var db = dbFactory.CreateDbContext();
+
+ return Ok(await db.Users
+ .Select(u => (ApiModels.User) u)
+ .ToArrayAsync());
+ }
+
+ [HttpGet("{userId}")]
+ public async Task<IActionResult> GetUserAsync([FromRoute] Guid userId) {
+ using var db = dbFactory.CreateDbContext();
+
+ var user = await db.Users
+ .FirstOrDefaultAsync(u => u.Guid == userId);
+
+ return user is null ? NotFound() : Ok((ApiModels.User) user);
+ }
+
+ [HttpPost]
+ public async Task<IActionResult> CreateUserAsync([FromBody] ApiModels.UserCreateRequest request) {
+ using var db = dbFactory.CreateDbContext();
+
+ using var transaction = await db.Database.BeginTransactionAsync();
+
+ if(await db.Users.AnyAsync(u => u.Username == request.Username))
+ return BadRequest("Username already exists");
+
+ var user = new User() {
+ Username = request.Username,
+ PasswordHash = UserService.HashPassword(request.Password)
+ };
+
+ db.Users.Add(user);
+
+ await db.SaveChangesAsync();
+ await transaction.CommitAsync();
+
+ return Ok((ApiModels.User) user);
+ }
+
+ [HttpPatch("{userId}")]
+ public async Task<IActionResult> UpdateUserAsync(
+ [FromRoute] Guid userId,
+ [FromBody] ApiModels.UserUpdateRequest request) {
+
+ using var db = dbFactory.CreateDbContext();
+
+ using var transaction = await db.Database.BeginTransactionAsync();
+
+ var user = await db.Users.FirstOrDefaultAsync(u => u.Guid == userId);
+ if(user is null)
+ return NotFound();
+
+ if(request.Username is not null) {
+ if(string.IsNullOrWhiteSpace(request.Username))
+ return BadRequest("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");
+ user.PasswordHash = UserService.HashPassword(request.Password);
+ }
+
+ await db.SaveChangesAsync();
+ await transaction.CommitAsync();
+
+ return Ok((ApiModels.User) user);
+ }
+
+ [HttpDelete("{userId}")]
+ public async Task<IActionResult> DeleteUserAsync([FromRoute] Guid userId) {
+ if(userId == HBContext.AdminUser)
+ return BadRequest("Cannot delete the admin user");
+
+ using var db = dbFactory.CreateDbContext();
+
+ using var transaction = await db.Database.BeginTransactionAsync();
+
+ var user = await db.Users.FirstOrDefaultAsync(u => u.Guid == userId);
+ if(user is null)
+ return NotFound();
+
+ db.Users.Remove(user);
+
+ await db.SaveChangesAsync();
+ await transaction.CommitAsync();
+
+ return Ok((ApiModels.User) user);
+ }
+}
diff --git a/Server/Controllers/MediaController.cs b/Server/Controllers/MediaController.cs
new file mode 100644
index 0000000..27c3cbd
--- /dev/null
+++ b/Server/Controllers/MediaController.cs
@@ -0,0 +1,154 @@
+using HyperBooru.ApiModels;
+using HyperBooru.Services;
+using HyperBooru.Util;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+
+namespace HyperBooru.Controllers;
+
+[ApiController]
+[Route("/media")]
+public class MediaController : Controller {
+ private IHttpContextAccessor httpContextAccessor;
+ private IMediaService mediaService;
+ private IConfigService config;
+ private HBContext db;
+
+ private readonly string[] FormatPriority = [
+ "image/webp",
+ "image/png"
+ ];
+
+ public MediaController(
+ IHttpContextAccessor httpContextAccessor,
+ IMediaService mediaService,
+ IConfigService config,
+ HBContext db) {
+
+ this.httpContextAccessor = httpContextAccessor;
+ this.mediaService = mediaService;
+ this.config = config;
+ this.db = db;
+ }
+
+ [HttpGet("{mediaId}")]
+ public IActionResult Fetch([FromRoute] Guid mediaId) {
+ var media = db.Media
+ .Include(m => m.CurrentUploadedFile)
+ .First(m => m.Guid == mediaId);
+ if(media is null)
+ return NotFound();
+
+ // Check if the requested media item is a HEIC image and if it is, convert it
+ // otherwise, return the original file content, unaltered
+ if(media.CurrentUploadedFile!.MimeType == "image/heic") {
+ // If the media needs to be converted, check the HTTP request for allowed
+ // media formats, and convert to the best available format or WebP otherwise
+ var allowedTypes = httpContextAccessor
+ .HttpContext?
+ .Request
+ .GetTypedHeaders().Accept.Select(h => h.MediaType.ToString()) ?? Array.Empty<string>();
+
+ var format = FormatPriority.FirstOrDefault(f => allowedTypes.Contains(f)) ?? "image/webp";
+
+ var fs = mediaService.GetConverted(media, format);
+
+ return new FileStreamResult(fs, format);
+ } else {
+ var fs = System.IO.File.OpenRead(mediaService.GetPath(media));
+ return new FileStreamResult(fs, media.CurrentUploadedFile!.MimeType);
+ }
+ }
+
+ [HttpGet("thumb/{mediaId}")]
+ public IActionResult Thumbnail(
+ [FromRoute] Guid mediaId,
+ [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);
+ }
+ }
+
+ [HttpDelete("{mediaId}")]
+ public void Delete([FromRoute] Guid mediaId) {
+ mediaService.Delete(mediaId);
+ }
+
+ [HttpPost]
+ public IActionResult Upload() {
+ if(Request.Form.Files.Count == 0)
+ return BadRequest("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"]
+ .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);
+ }
+ }
+
+ if(Request.Form.Files.Count == 1)
+ return Redirect($"/ViewMedia?m={media.Guid}");
+ else
+ return Redirect($"/Gallery");
+ }
+} \ No newline at end of file