using HyperBooru.Services; using ImageMagick; using Microsoft.AspNetCore.Mvc; using MimeDetective; using System.Security.Cryptography; namespace HyperBooru.Controllers; [ApiController] [Route("/media")] public class MediaController : Controller { private IConfigService config; private HBContext db; private ContentInspector inspector; public MediaController(IConfigService config, HBContext db) { this.config = config; this.db = db; ContentInspectorBuilder inspectorBuilder = new() { Definitions = MimeDetective.Definitions.Default.FileTypes.Images.All() .Union(MimeDetective.Definitions.Default.FileTypes.Video.All()) .ToList() }; inspector = inspectorBuilder.Build(); } [HttpGet("list")] public IActionResult EnumerateMedia() => Ok(db.Media.Select(m => m.ObjectId).ToArray()); [HttpGet("{mediaId}")] public IActionResult Fetch([FromRoute] Guid mediaId) { var media = db.Media.First(m => m.Guid == mediaId); if(media is null) return NotFound(); var fs = System.IO.File.OpenRead(config.GetPath(media)); return new FileStreamResult(fs, media.MimeType); } [HttpGet("thumb/{mediaId}")] public IActionResult Thumbnail( [FromRoute] Guid mediaId, [FromQuery] int? w, [FromQuery] int? h) { var media = db.Media.First(m => m.Guid == mediaId); if(media is null) return NotFound(); if(media.MimeType.Split("/")[0] != "image") return BadRequest("Media object not an image"); using var image = new MagickImage(config.GetPath(media)); if(w is null && h is null) return BadRequest("Both width and height cannot be null!"); if(w > image.Width || h > image.Height) return BadRequest("Requested thumbnail size is larger than original media"); int width = (int)(w is not null ? w : image.Width * h / image.Height); int height = (int)(h is not null ? h : image.Height * w / image.Width); var thumbPath = config.GetPath(media, width, height); if(!System.IO.File.Exists(thumbPath)) { image.Resize(new MagickGeometry(width, height)); image.Write(thumbPath); } var fs = System.IO.File.OpenRead(thumbPath); return new FileStreamResult(fs, "image/jpeg"); } [HttpDelete("{mediaId}")] public IActionResult Delete([FromRoute] Guid mediaId) { var media = db.Media.First(m => m.Guid == mediaId); if(media is null) return NotFound(); System.IO.File.Delete(config.GetPath(media)); db.Media.Remove(media); db.SaveChanges(); return Ok(); } [HttpPost] public IActionResult Upload( [FromForm] string? checksum, [FromForm] DateTime? lastAccessTime, [FromForm] DateTime? lastWriteTime, [FromForm] DateTime? createTime) { using var transaction = db.Database.BeginTransaction(); var formFile = Request.Form.Files[0]; if(formFile.Length < 1) return BadRequest("Empty file"); var formStream = formFile.OpenReadStream(); // Calculate the checksum using the in-memory file contents var hash = BitConverter .ToString(MD5.Create().ComputeHash(formStream)) .Replace("-", "") .ToLower(); if(checksum is not null && hash != checksum.ToLower()) return BadRequest("Checksum does not match"); var fileRecord = new UploadedFile() { Filename = formFile.FileName, OriginalChecksum = hash, UploadTime = DateTime.UtcNow, LastAccessTime = lastAccessTime, LastWriteTime = lastWriteTime, CreateTime = createTime }; formStream.Seek(0, SeekOrigin.Begin); var defs = inspector.Inspect(formStream); var mime = defs.ByMimeType().FirstOrDefault()?.MimeType; if(mime is null) return BadRequest("Unsupported file type"); var media = db.Media .FirstOrDefault(m => m.Checksum == hash); if(media is null) { var ingestTagDef = db.TagDefinitions .First(td => td.Guid == HBContext.IngestTag); media = new() { Checksum = hash, MimeType = mime, UploadedFiles = new() { fileRecord }, Tags = new() { new() { TagDefinition = ingestTagDef } } }; using var newFile = System.IO.File.Create(config.GetPath(media)); formStream.Seek(0, SeekOrigin.Begin); formStream.CopyTo(newFile); newFile.Flush(); db.Media.Add(media); } else { media.UploadedFiles.Add(fileRecord); db.Update(media); } db.SaveChanges(); transaction.Commit(); return Ok(media.Guid); } }