summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Controllers/ApiMediaController.cs98
-rw-r--r--Controllers/ApiTagController.cs36
-rw-r--r--Controllers/LoginController.cs1
-rw-r--r--Controllers/MediaController.cs3
-rw-r--r--Dockerfile7
-rw-r--r--Enum.cs12
-rw-r--r--Exception.cs69
-rw-r--r--ExceptionMiddleware.cs64
-rw-r--r--HBContext.cs4
-rw-r--r--Media.cs37
-rw-r--r--Pages/Component/TagEditDialog.razor2
-rw-r--r--Pages/Gallery.razor4
-rw-r--r--Program.cs9
-rw-r--r--Server.csproj8
-rw-r--r--Services/ConfigService.cs4
-rw-r--r--Services/FeedService.cs3
-rw-r--r--Services/MediaService.cs4
-rw-r--r--Services/TagService.cs6
-rw-r--r--Services/UserService.cs1
-rw-r--r--Tag.cs12
20 files changed, 274 insertions, 110 deletions
diff --git a/Controllers/ApiMediaController.cs b/Controllers/ApiMediaController.cs
new file mode 100644
index 0000000..0539b77
--- /dev/null
+++ b/Controllers/ApiMediaController.cs
@@ -0,0 +1,98 @@
+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);
+}
diff --git a/Controllers/ApiTagController.cs b/Controllers/ApiTagController.cs
new file mode 100644
index 0000000..afd5b05
--- /dev/null
+++ b/Controllers/ApiTagController.cs
@@ -0,0 +1,36 @@
+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();
+ }
+}
diff --git a/Controllers/LoginController.cs b/Controllers/LoginController.cs
index b01553c..c93f0d5 100644
--- a/Controllers/LoginController.cs
+++ b/Controllers/LoginController.cs
@@ -1,7 +1,6 @@
using HyperBooru.Services;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
-using Microsoft.AspNetCore.Cryptography.KeyDerivation;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
diff --git a/Controllers/MediaController.cs b/Controllers/MediaController.cs
index 7eb46c6..96ecb44 100644
--- a/Controllers/MediaController.cs
+++ b/Controllers/MediaController.cs
@@ -1,4 +1,5 @@
-using HyperBooru.Services;
+using HyperBooru.ApiModels;
+using HyperBooru.Services;
using HyperBooru.Util;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
diff --git a/Dockerfile b/Dockerfile
index 55a86db..7769bf4 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,7 +1,8 @@
FROM mcr.microsoft.com/dotnet/sdk:10.0@sha256:f061e5a7532b36fa1d1b684857fe1f504ba92115b9934f154643266613c44c62 AS build
-WORKDIR /App
+WORKDIR /App/Server
-COPY . ./
+COPY Server /App/Server
+COPY ApiModels /App/ApiModels
RUN dotnet restore
RUN dotnet publish -o out
@@ -11,5 +12,5 @@ RUN apt install -y imagemagick tesseract-ocr tesseract-ocr-eng
RUN apt clean
RUN rm -rf /var/lib/apt/lists/*
WORKDIR /App
-COPY --from=build /App/out .
+COPY --from=build /App/Server/out .
ENTRYPOINT [ "dotnet", "HyperBooru.dll" ]
diff --git a/Enum.cs b/Enum.cs
deleted file mode 100644
index bc49691..0000000
--- a/Enum.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-namespace HyperBooru;
-
-public enum SortOrder {
- ObjectId,
- LastWriteTime,
- Random,
-}
-
-public enum PathType {
- Windows,
- Unix
-}
diff --git a/Exception.cs b/Exception.cs
deleted file mode 100644
index fc3feda..0000000
--- a/Exception.cs
+++ /dev/null
@@ -1,69 +0,0 @@
-namespace HyperBooru;
-
-public class HBException : Exception {
- public HBException()
- : base() {}
- public HBException(string message)
- : base(message) {}
- public HBException(string message, Exception inner)
- : base(message, inner) {}
-}
-
-public class ObjectNotFoundException : HBException {
- public Guid Guid { get; private init; }
-
- public ObjectNotFoundException(Guid guid)
- : base($"Object not found: {guid}") {}
-}
-
-public class TagException : HBException {
- public TagDefinition? TagDefinition { get; private init; }
-
- public TagException(string message) : base(message) {}
- public TagException(string message, TagDefinition tagDefinition)
- : base(message) =>
- TagDefinition = tagDefinition;
-}
-
-public class TagDuplicateException : TagException {
- public bool NameExists { get; private init; }
- public bool AliasExists { get; private init; }
-
- public TagDuplicateException(bool nameExists, bool aliasExists)
- : base(GenerateMessage(nameExists, aliasExists)) {
-
- NameExists = nameExists;
- AliasExists = aliasExists;
- }
-
- private static string GenerateMessage(bool nameExists, bool aliasExists) {
- if(nameExists && aliasExists)
- return $"Both tag name and alias already exist!";
- else if(nameExists)
- return $"Tag name already exists!";
- else
- return $"Tag alias already exists";
- }
-}
-
-public class MediaException : HBException {
- public Guid? MediaId { get; private init; } = null;
-
- public MediaException(string message) : base(message) {}
- public MediaException(string message, Guid mediaId) : base(message) =>
- MediaId = mediaId;
- public MediaException(string message, Media media) : base(message) =>
- MediaId = media.Guid;
-}
-
-public class MediaCreateException : MediaException {
- public MediaCreateException(string message)
- : base(message) {}
-}
-
-public class ThumbnailException : MediaException {
- public ThumbnailException(string message, Guid mediaId)
- : base(message, mediaId) {}
- public ThumbnailException(string message, Media media)
- : base(message, media) {}
-}
diff --git a/ExceptionMiddleware.cs b/ExceptionMiddleware.cs
new file mode 100644
index 0000000..29d0e10
--- /dev/null
+++ b/ExceptionMiddleware.cs
@@ -0,0 +1,64 @@
+using HyperBooru.ApiModels;
+using System.Reflection;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Text.Json.Serialization.Metadata;
+
+namespace HyperBooru;
+
+// Middleware class to intercept API controller exceptions and
+// return said exceptions to API clients as serialized JSON objects
+public sealed class ExceptionMiddleware {
+ private RequestDelegate next;
+
+ public ExceptionMiddleware(RequestDelegate next) =>
+ this.next = next;
+
+ public async Task Invoke(HttpContext context) {
+ try {
+ await next(context);
+ } catch(HBException e) {
+ context.Response.ContentType = "application/json";
+ context.Response.StatusCode =
+ e.GetType().GetCustomAttribute<ExceptionStatusCodeAttribute>()?.StatusCode ??
+ StatusCodes.Status500InternalServerError;
+
+ await context.Response.WriteAsJsonAsync(e);
+
+ var x = 1;
+ } catch(Exception) {
+ context.Response.StatusCode = StatusCodes.Status500InternalServerError;
+ context.Response.ContentType = "application/json";
+
+ context.Response.Clear();
+
+ await context.Response.WriteAsync(string.Empty);
+ }
+ }
+}
+
+// This class is needed as the JSON serializer often fails to serialize
+// members of the native 'Exception' class
+public sealed class ExceptionJsonResolver : DefaultJsonTypeInfoResolver {
+ public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options) {
+ var info = base.GetTypeInfo(type, options);
+
+ if(!typeof(Exception).IsAssignableFrom(type))
+ return info;
+
+ string[] excludedProps = [
+ "data",
+ "hResult",
+ "helpLink",
+ "innerException",
+ "source",
+ "stackTrace",
+ "targetSite"
+ ];
+
+ foreach(var p in info.Properties.Where(p => excludedProps.Contains(p.Name)))
+ p.ShouldSerialize = (_, _) => false;
+
+ return info;
+ }
+}
diff --git a/HBContext.cs b/HBContext.cs
index bb0572f..b684a51 100644
--- a/HBContext.cs
+++ b/HBContext.cs
@@ -1,5 +1,5 @@
-using Microsoft.EntityFrameworkCore;
-using HyperBooru.Services;
+using HyperBooru.Services;
+using Microsoft.EntityFrameworkCore;
namespace HyperBooru;
diff --git a/Media.cs b/Media.cs
index 51626be..2ff9e63 100644
--- a/Media.cs
+++ b/Media.cs
@@ -1,8 +1,7 @@
-using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
+using HyperBooru.ApiModels;
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
-using System.Net.NetworkInformation;
namespace HyperBooru;
@@ -27,6 +26,13 @@ public class Media : HBObject {
.First()?.Filename ?? Guid.ToString().ToUpper();
}
}
+
+ public static explicit operator ApiModels.Media(Media media) =>
+ new() {
+ MediaId = media.Guid,
+ ShortDescription = media.ShortDescription,
+ LongDescription = media.LongDescription
+ };
}
public class UploadedFile : HBObject {
@@ -44,6 +50,25 @@ public class UploadedFile : HBObject {
public string? Path { get; set; }
public PathType? PathType { get; set; }
public virtual Media Media { get; set; }
+
+ public static explicit operator ApiModels.UploadedFile(UploadedFile uploadedFile) =>
+ new() {
+ MediaId = uploadedFile.Media.Guid,
+ UploadedFileId = uploadedFile.Guid,
+ Checksum = uploadedFile.Checksum,
+ ChecksumVerified = uploadedFile.ChecksumVerified,
+ Filename = uploadedFile.Filename,
+ Length = uploadedFile.Length,
+ MimeType = uploadedFile.MimeType,
+ Width = uploadedFile.Width,
+ Height = uploadedFile.Height,
+ UploadTime = uploadedFile.UploadTime,
+ LastAccessTime = uploadedFile.LastAccessTime,
+ LastWriteTime = uploadedFile.LastWriteTime,
+ CreateTime = uploadedFile.CreateTime,
+ Path = uploadedFile.Path,
+ PathType = (ApiModels.PathType?) uploadedFile.PathType
+ };
}
public class OcrData {
@@ -56,4 +81,12 @@ public class OcrData {
public string SearchableText { get; set; }
public DateTime Timestamp { get; set; }
public virtual Media Media { get; set; }
+
+ public static explicit operator ApiModels.OcrData(OcrData ocrData) =>
+ new() {
+ MediaId = ocrData.Media.Guid,
+ Text = ocrData.Text,
+ SearchableText = ocrData.SearchableText,
+ Timestamp = ocrData.Timestamp
+ };
} \ No newline at end of file
diff --git a/Pages/Component/TagEditDialog.razor b/Pages/Component/TagEditDialog.razor
index 2e443d4..afa312e 100644
--- a/Pages/Component/TagEditDialog.razor
+++ b/Pages/Component/TagEditDialog.razor
@@ -84,7 +84,7 @@
} else {
tagService.UpdateTagDefinition(TagDefinition, tagName, tagNamespace, tagAlias);
}
- } catch(TagDuplicateException e) {
+ } catch(ApiModels.TagDuplicateException e) {
nameExists = e.NameExists;
aliasExists = e.AliasExists;
return;
diff --git a/Pages/Gallery.razor b/Pages/Gallery.razor
index 1464c13..743485e 100644
--- a/Pages/Gallery.razor
+++ b/Pages/Gallery.razor
@@ -98,8 +98,8 @@
if(initial)
displayMedia = new();
- SortOrder? sortOrder = null;
- if(Enum.TryParse<SortOrder>(SortOrder, true, out var so))
+ ApiModels.SortOrder? sortOrder = null;
+ if(Enum.TryParse<ApiModels.SortOrder>(SortOrder, true, out var so))
sortOrder = so;
if(TagId is not null && Query is null) {
diff --git a/Program.cs b/Program.cs
index 548f6e9..34db56f 100644
--- a/Program.cs
+++ b/Program.cs
@@ -1,8 +1,9 @@
-using Microsoft.EntityFrameworkCore;
-using System.Text.Json.Serialization;
using HyperBooru.Services;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.DataProtection;
+using Microsoft.AspNetCore.Http.Json;
+using Microsoft.EntityFrameworkCore;
+using System.Text.Json.Serialization;
namespace HyperBooru;
@@ -17,6 +18,9 @@ public class Program {
var converter = new JsonStringEnumConverter();
o.JsonSerializerOptions.Converters.Add(converter);
});
+ builder.Services.Configure<JsonOptions>(o => {
+ o.SerializerOptions.TypeInfoResolverChain.Insert(0, new ExceptionJsonResolver());
+ });
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
@@ -49,6 +53,7 @@ public class Program {
app.UseHsts();
app.UseHttpsRedirection();
app.UseStaticFiles();
+ app.UseMiddleware<ExceptionMiddleware>();
app.UseRouting();
app.MapBlazorHub();
app.MapControllers();
diff --git a/Server.csproj b/Server.csproj
index b743529..c9afced 100644
--- a/Server.csproj
+++ b/Server.csproj
@@ -6,9 +6,9 @@
<ImplicitUsings>enable</ImplicitUsings>
<AssemblyName>HyperBooru</AssemblyName>
<RootNamespace>HyperBooru</RootNamespace>
- <AssemblyVersion>0.12.0.0</AssemblyVersion>
+ <AssemblyVersion>0.13.0.0</AssemblyVersion>
<FileVersion>$(AssemblyVersion)</FileVersion>
- <Version>0.12-alpha</Version>
+ <Version>0.13-alpha</Version>
<UserSecretsId>2907567f-4640-4581-8f4d-0977952d26bd</UserSecretsId>
</PropertyGroup>
@@ -45,4 +45,8 @@
<PackageReference Include="Tesseract" Version="5.2.0" />
</ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\ApiModels\ApiModels.csproj" />
+ </ItemGroup>
+
</Project>
diff --git a/Services/ConfigService.cs b/Services/ConfigService.cs
index 752f9f5..ac1f155 100644
--- a/Services/ConfigService.cs
+++ b/Services/ConfigService.cs
@@ -1,4 +1,6 @@
-namespace HyperBooru.Services;
+using HyperBooru.ApiModels;
+
+namespace HyperBooru.Services;
public interface IConfigService {
public string DataPath { get; }
diff --git a/Services/FeedService.cs b/Services/FeedService.cs
index 77f92b5..067bff7 100644
--- a/Services/FeedService.cs
+++ b/Services/FeedService.cs
@@ -1,4 +1,5 @@
-using Microsoft.EntityFrameworkCore;
+using HyperBooru.ApiModels;
+using Microsoft.EntityFrameworkCore;
namespace HyperBooru.Services;
diff --git a/Services/MediaService.cs b/Services/MediaService.cs
index 763b953..e497570 100644
--- a/Services/MediaService.cs
+++ b/Services/MediaService.cs
@@ -1,5 +1,5 @@
-using ImageMagick;
-using ImageMagick.Formats;
+using HyperBooru.ApiModels;
+using ImageMagick;
using Microsoft.EntityFrameworkCore;
using MimeDetective;
using MimeDetective.Definitions;
diff --git a/Services/TagService.cs b/Services/TagService.cs
index 56aba04..f7b91dc 100644
--- a/Services/TagService.cs
+++ b/Services/TagService.cs
@@ -1,9 +1,5 @@
-using Microsoft.AspNetCore.Components.Web;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
+using HyperBooru.ApiModels;
using Microsoft.EntityFrameworkCore;
-using System.Reflection;
-using System.Reflection.Metadata;
namespace HyperBooru.Services;
diff --git a/Services/UserService.cs b/Services/UserService.cs
index 39b1963..9e79dc6 100644
--- a/Services/UserService.cs
+++ b/Services/UserService.cs
@@ -1,5 +1,4 @@
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
-using Microsoft.EntityFrameworkCore;
namespace HyperBooru.Services;
diff --git a/Tag.cs b/Tag.cs
index 0150b39..172fe8d 100644
--- a/Tag.cs
+++ b/Tag.cs
@@ -1,6 +1,4 @@
-using Microsoft.EntityFrameworkCore;
-using System.ComponentModel.DataAnnotations;
-using System.ComponentModel.DataAnnotations.Schema;
+using System.ComponentModel.DataAnnotations.Schema;
namespace HyperBooru;
@@ -15,6 +13,14 @@ public class TagDefinition : HBObject {
public string Name { get; set; }
public string? Alias { get; set;}
public virtual List<TagDefinition> ImplicitTags { get; set; } = new();
+
+ public static explicit operator ApiModels.TagDefinition(TagDefinition tagDefinition) => new() {
+ TagDefinitionId = tagDefinition.Guid,
+ Namespace = tagDefinition.Namespace,
+ Name = tagDefinition.Name,
+ Alias = tagDefinition.Alias,
+ ImplicitTags = tagDefinition.ImplicitTags.Select(td => td.Guid).ToArray()
+ };
}
public class Tag : HBObject {