From 1c9127ca194af9a4f552fbe5af2a916000d8f32b Mon Sep 17 00:00:00 2001 From: Amatsugu Date: Tue, 15 Apr 2025 23:00:11 -0400 Subject: [PATCH] implement media serving --- AobaCore/AobaCore.csproj | 1 - AobaCore/AobaService.cs | 6 ++++- AobaCore/Extensions.cs | 8 ++++++ AobaCore/MediaService.cs | 23 +++++++++++----- AobaServer/AobaAuthenticationHandler.cs | 4 +-- AobaServer/AobaServer.csproj | 13 ++++----- AobaServer/Auth.json | 1 + AobaServer/AuthInfo.cs | 11 ++++---- AobaServer/Controllers/Api/AuthApi.cs | 2 +- AobaServer/Controllers/AuthController.cs | 2 +- AobaServer/Controllers/MediaController.cs | 27 +++++++++++++++---- AobaServer/Dockerfile | 1 - .../Models/BsonIdModelBinderProvider.cs | 2 +- AobaServer/Program.cs | 14 +++------- 14 files changed, 72 insertions(+), 43 deletions(-) create mode 100644 AobaServer/Auth.json diff --git a/AobaCore/AobaCore.csproj b/AobaCore/AobaCore.csproj index a7695ea..5c456a7 100644 --- a/AobaCore/AobaCore.csproj +++ b/AobaCore/AobaCore.csproj @@ -8,7 +8,6 @@ - diff --git a/AobaCore/AobaService.cs b/AobaCore/AobaService.cs index 8be06cf..6b5317d 100644 --- a/AobaCore/AobaService.cs +++ b/AobaCore/AobaService.cs @@ -9,7 +9,6 @@ public class AobaService(IMongoDatabase db) { private readonly IMongoCollection _media = db.GetCollection("media"); - public async Task GetMediaAsync(ObjectId id) { return await _media.Find(m => m.Id == id).FirstOrDefaultAsync(); @@ -19,4 +18,9 @@ public class AobaService(IMongoDatabase db) { return _media.InsertOneAsync(media); } + + public Task IncrementViewCountAsync(ObjectId id) + { + return _media.UpdateOneAsync(m => m.Id == id, Builders.Update.Inc(m => m.ViewCount, 1)); + } } diff --git a/AobaCore/Extensions.cs b/AobaCore/Extensions.cs index 84c8dd9..6964596 100644 --- a/AobaCore/Extensions.cs +++ b/AobaCore/Extensions.cs @@ -1,6 +1,8 @@ global using MaybeError; using Microsoft.Extensions.DependencyInjection; +using MongoDB.Driver; + using System; using System.Collections.Generic; using System.Linq; @@ -12,7 +14,13 @@ public static class Extensions { public static IServiceCollection AddAoba(this IServiceCollection services) { + var dbClient = new MongoClient("mongodb://NinoIna:27017"); + var db = dbClient.GetDatabase("Aoba"); + + services.AddSingleton(dbClient); + services.AddSingleton(db); services.AddSingleton(); + services.AddSingleton(); return services; } } diff --git a/AobaCore/MediaService.cs b/AobaCore/MediaService.cs index 2d87df3..0573ba8 100644 --- a/AobaCore/MediaService.cs +++ b/AobaCore/MediaService.cs @@ -1,14 +1,11 @@ -using AobaV2.Models; +using AobaV2.Models; + +using MaybeError.Errors; using MongoDB.Bson; using MongoDB.Driver; using MongoDB.Driver.GridFS; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace AobaCore; public class MediaService(IMongoDatabase db, AobaService aobaService) @@ -18,8 +15,20 @@ public class MediaService(IMongoDatabase db, AobaService aobaService) public async Task> UploadMediaAsync(Stream data, string filename, ObjectId owner, CancellationToken cancellationToken = default) { var fileId = await _gridFs.UploadFromStreamAsync(filename, data, cancellationToken: cancellationToken); - var media = new Media(fileId, filename, owner); + var media = new Media(fileId, filename, owner); await aobaService.AddMediaAsync(media); return media; } + + public async Task>> GetMediaStreamAsync(ObjectId id, bool seekable = false) + { + try + { + return await _gridFs.OpenDownloadStreamAsync(id, new GridFSDownloadOptions { Seekable = seekable }); + } + catch (GridFSException ex) + { + return new ExceptionError(ex); + } + } } diff --git a/AobaServer/AobaAuthenticationHandler.cs b/AobaServer/AobaAuthenticationHandler.cs index 7cbbfe4..757378e 100644 --- a/AobaServer/AobaAuthenticationHandler.cs +++ b/AobaServer/AobaAuthenticationHandler.cs @@ -2,11 +2,11 @@ using Microsoft.Extensions.Options; using System.Text.Encodings.Web; -namespace AobaV2; +namespace AobaServer; internal class AobaAuthenticationHandler : AuthenticationHandler { - public AobaAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) + public AobaAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder) : base(options, logger, encoder) { } diff --git a/AobaServer/AobaServer.csproj b/AobaServer/AobaServer.csproj index b1e16d6..851fd5e 100644 --- a/AobaServer/AobaServer.csproj +++ b/AobaServer/AobaServer.csproj @@ -1,4 +1,4 @@ - + net9.0 @@ -10,14 +10,11 @@ - - - - + + + - - - + diff --git a/AobaServer/Auth.json b/AobaServer/Auth.json new file mode 100644 index 0000000..78dc759 --- /dev/null +++ b/AobaServer/Auth.json @@ -0,0 +1 @@ +{"Issuer":"aobaV2","Audience":"aoba","SecureKey":"iOx/85/SdBG4xji/aNzNMNpwOIZgPgr3hul4ddDrcp8jHxVL4uGPKvOBjVfOqG0IOMDZvaxOjieKdqdJnU9eNA=="} \ No newline at end of file diff --git a/AobaServer/AuthInfo.cs b/AobaServer/AuthInfo.cs index 4125d4c..a548687 100644 --- a/AobaServer/AuthInfo.cs +++ b/AobaServer/AuthInfo.cs @@ -1,14 +1,15 @@ using MongoDB.Bson.IO; + using System.Security.Cryptography; using System.Text.Json; -namespace AobaV2; +namespace AobaServer; public class AuthInfo { - public required string Issuer; - public required string Audience; - public required byte[] SecureKey; + public required string Issuer { get; set; } + public required string Audience { get; set; } + public required byte[] SecureKey { get; set; } /// /// Save this auth into in a json format to the sepcified file @@ -53,7 +54,7 @@ public class AuthInfo if (File.Exists(path)) { var loaded = Load(path); - if(loaded != null) + if (loaded != null) return loaded; } var info = Create(issuer, audience); diff --git a/AobaServer/Controllers/Api/AuthApi.cs b/AobaServer/Controllers/Api/AuthApi.cs index c4f4fd6..d9e82a9 100644 --- a/AobaServer/Controllers/Api/AuthApi.cs +++ b/AobaServer/Controllers/Api/AuthApi.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Mvc; -namespace AobaV2.Controllers.Api; +namespace AobaServer.Controllers.Api; [Route("/api/auth")] public class AuthApi : ControllerBase diff --git a/AobaServer/Controllers/AuthController.cs b/AobaServer/Controllers/AuthController.cs index 86fb5a5..6f97b6b 100644 --- a/AobaServer/Controllers/AuthController.cs +++ b/AobaServer/Controllers/AuthController.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace AobaV2.Controllers; +namespace AobaServer.Controllers; [AllowAnonymous] [Route("auth")] diff --git a/AobaServer/Controllers/MediaController.cs b/AobaServer/Controllers/MediaController.cs index 104d30e..25b5f69 100644 --- a/AobaServer/Controllers/MediaController.cs +++ b/AobaServer/Controllers/MediaController.cs @@ -1,27 +1,44 @@ using AobaCore; +using HeyRed.Mime; + using Microsoft.AspNetCore.Mvc; using MongoDB.Bson; using MongoDB.Driver; -namespace AobaV2.Controllers; +namespace AobaServer.Controllers; [Route("/m")] -public class MediaController(MediaService media) : Controller +public class MediaController(MediaService mediaService, ILogger logger) : Controller { [HttpGet("{id}")] - public IActionResult Media(ObjectId id) + [ResponseCache(Duration = int.MaxValue)] + public async Task MediaAsync(ObjectId id) { - return View(); + var file = await mediaService.GetMediaStreamAsync(id); + if (file.HasError) + { + logger.LogError(file.Error.Exception, "Failed to load media stream"); + return NotFound(); + } + var mime = MimeTypesMap.GetMimeType(file.Value.FileInfo.Filename); + return File(file, mime, true); } + /// + /// Redirect legacy media urls to the new url + /// + /// + /// + /// + /// [HttpGet("/i/{id}/{*rest}")] public async Task LegacyRedirectAsync(ObjectId id, string rest, [FromServices] AobaService aoba) { var media = await aoba.GetMediaAsync(id); if (media == null) return NotFound(); - return LocalRedirectPermanent($"/m/{media.Id}/{rest}"); + return LocalRedirectPermanent($"/m/{media.MediaId}/{rest}"); } } diff --git a/AobaServer/Dockerfile b/AobaServer/Dockerfile index 69c446d..2908744 100644 --- a/AobaServer/Dockerfile +++ b/AobaServer/Dockerfile @@ -14,7 +14,6 @@ ARG BUILD_CONFIGURATION=Release WORKDIR /src COPY ["AobaV2/AobaV2.csproj", "AobaV2/"] RUN dotnet restore "./AobaV2/AobaV2.csproj" -RUN npm install COPY . . WORKDIR "/src/AobaV2" RUN dotnet build "./AobaV2.csproj" -c $BUILD_CONFIGURATION -o /app/build diff --git a/AobaServer/Models/BsonIdModelBinderProvider.cs b/AobaServer/Models/BsonIdModelBinderProvider.cs index 888ce45..4abec4a 100644 --- a/AobaServer/Models/BsonIdModelBinderProvider.cs +++ b/AobaServer/Models/BsonIdModelBinderProvider.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding; using MongoDB.Bson; -namespace AobaV2.Models; +namespace AobaServer.Models; public class BsonIdModelBinderProvider : IModelBinderProvider { diff --git a/AobaServer/Program.cs b/AobaServer/Program.cs index e311912..a9b4435 100644 --- a/AobaServer/Program.cs +++ b/AobaServer/Program.cs @@ -1,10 +1,9 @@ using AobaCore; -using AobaV2; -using AobaV2.Models; +using AobaServer; +using AobaServer.Models; using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Http.Features; using Microsoft.IdentityModel.Tokens; @@ -12,8 +11,7 @@ using Microsoft.IdentityModel.Tokens; var builder = WebApplication.CreateBuilder(args); // Add services to the container. -builder.Services - .AddControllers(opt => opt.ModelBinderProviders.Add(new BsonIdModelBinderProvider())); +builder.Services.AddControllers(opt => opt.ModelBinderProviders.Add(new BsonIdModelBinderProvider())); var authInfo = AuthInfo.LoadOrCreate("Auth.json", "aobaV2", "aoba"); @@ -89,12 +87,8 @@ app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); -app.MapStaticAssets(); -app.MapControllerRoute( - name: "default", - pattern: "{controller=Home}/{action=Index}/{id?}") - .WithStaticAssets(); +app.MapControllers(); app.Run();