From a26e9b6a628cfd311b08e1c4d2bf612d9af9bb7c Mon Sep 17 00:00:00 2001 From: Jake Mannens Date: Fri, 25 Aug 2023 11:11:38 +1000 Subject: Moved media controller upload functionality into media service Multiple uploads can now be handled --- Controllers/MediaController.cs | 127 +++++++++---------------------- Exception.cs | 25 ++++++ Pages/Gallery.razor | 2 +- Pages/ViewMedia.razor | 18 ++++- Services/ConfigService.cs | 29 +------ Services/MediaService.cs | 169 ++++++++++++++++++++++++++++++++++++++++- 6 files changed, 246 insertions(+), 124 deletions(-) create mode 100644 Exception.cs diff --git a/Controllers/MediaController.cs b/Controllers/MediaController.cs index b7c8314..b476dec 100644 --- a/Controllers/MediaController.cs +++ b/Controllers/MediaController.cs @@ -9,23 +9,18 @@ namespace HyperBooru.Controllers; [ApiController] [Route("/media")] public class MediaController : Controller { + private IMediaService mediaService; private IConfigService config; - private HBContext db; + private HBContext db; - private ContentInspector inspector; + public MediaController( + IMediaService mediaService, + IConfigService config, + HBContext db) { - 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(); + this.mediaService = mediaService; + this.config = config; + this.db = db; } [HttpGet("{mediaId}")] @@ -34,7 +29,7 @@ public class MediaController : Controller { if(media is null) return NotFound(); - var fs = System.IO.File.OpenRead(config.GetPath(media)); + var fs = System.IO.File.OpenRead(mediaService.GetPath(media)); return new FileStreamResult(fs, media.MimeType); } @@ -52,7 +47,7 @@ public class MediaController : Controller { if(media.MimeType.Split("/")[0] != "image") return BadRequest("Media object not an image"); - using var image = new MagickImage(config.GetPath(media)); + using var image = new MagickImage(mediaService.GetPath(media)); if(w is null && h is null) return BadRequest("Both width and height cannot be null!"); @@ -63,7 +58,7 @@ public class MediaController : Controller { 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); + var thumbPath = mediaService.GetPath(media, width, height); if(!System.IO.File.Exists(thumbPath)) { image.Resize(new MagickGeometry(width, height)); @@ -76,16 +71,8 @@ public class MediaController : Controller { } [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(); + public void Delete([FromRoute] Guid mediaId) { + mediaService.Delete(mediaId); } [HttpPost] @@ -95,72 +82,28 @@ public class MediaController : Controller { [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); + if(Request.Form.Files.Count == 0) + return BadRequest("No files"); + + Media media = new(); + + foreach(var formFile in Request.Form.Files) { + try { + media = mediaService.Create( + formFile.OpenReadStream(), + formFile.FileName, + checksum, + lastAccessTime, + lastWriteTime, + createTime); + } catch(MediaCreateException e) { + return BadRequest(e.Message); + } } - db.SaveChanges(); - transaction.Commit(); - - return Redirect($"/ViewMedia?m={media.Guid}"); + if(Request.Form.Files.Count == 1) + return Redirect($"/ViewMedia?m={media.Guid}"); + else + return Redirect($"/Gallery"); } } \ No newline at end of file diff --git a/Exception.cs b/Exception.cs new file mode 100644 index 0000000..528be52 --- /dev/null +++ b/Exception.cs @@ -0,0 +1,25 @@ +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 MediaException : HBException { + public Media? Media { get; set; } + + public MediaException(string message) : base(message) {} + public MediaException(string message, Media media) : base(message) => + Media = media; +} + +public class MediaCreateException : MediaException { + public MediaCreateException(string message) + : base(message) {} + public MediaCreateException(string message, Media media) + : base(message, media) {} +} diff --git a/Pages/Gallery.razor b/Pages/Gallery.razor index 88400b3..7894fb7 100644 --- a/Pages/Gallery.razor +++ b/Pages/Gallery.razor @@ -10,7 +10,7 @@
- +
diff --git a/Pages/ViewMedia.razor b/Pages/ViewMedia.razor index 52300e3..7d924ef 100644 --- a/Pages/ViewMedia.razor +++ b/Pages/ViewMedia.razor @@ -1,4 +1,5 @@ @page "/ViewMedia" +@inject IJSRuntime jsRuntime @inject IDbContextFactory dbFactory @inject ITagService tagService @inject IMediaService mediaService @@ -75,8 +76,8 @@
- - + +
@@ -125,11 +126,15 @@ mediaTagTable.Refresh(); } - private void SetIngest(bool ingest) { + private async void SetIngest(bool ingest) { mediaService.SetIngest(media, ingest); db.Entry(media).State = EntityState.Detached; LoadMedia(); - StateHasChanged(); + + if(ingest) + StateHasChanged(); + else + await jsRuntime.InvokeVoidAsync("history.back"); } private bool InfoEditMode { @@ -154,5 +159,10 @@ infoEditMode = false; } + private async void DeleteMedia() { + mediaService.Delete(media); + await jsRuntime.InvokeVoidAsync("history.back"); + } + public void Dispose() => db.Dispose(); } diff --git a/Services/ConfigService.cs b/Services/ConfigService.cs index 814a47b..b42b80c 100644 --- a/Services/ConfigService.cs +++ b/Services/ConfigService.cs @@ -4,9 +4,7 @@ public interface IConfigService { public string DataPath { get; } public string DbConnectionString { get; } public string MediaBasePath { get; } - - public string GetPath(Media media); - public string GetPath(Media media, int width, int height); + public string ThumbnailBasePath { get; } } public class ConfigService : IConfigService { @@ -39,7 +37,8 @@ public class ConfigService : IConfigService { } public string DbConnectionString => - config.GetConnectionString("DefaultConnection"); + config.GetConnectionString("DefaultConnection") ?? + throw new HBException("Unable to get default connection string"); public string MediaBasePath => Path.Join(DataPath, "media"); @@ -52,28 +51,6 @@ public class ConfigService : IConfigService { InitDirectoryStructure(); } - public string GetPath(Media media) { - var fileInfo = new FileInfo(Path.Join( - MediaBasePath, - media.Guid.ToString().Substring(0, 2), - media.Guid.ToString().Substring(2, 2), - media.Guid.ToString())); - - Directory.CreateDirectory(fileInfo.Directory.FullName); - return fileInfo.FullName; - } - - public string GetPath(Media media, int width, int height) { - var fileInfo = new FileInfo(Path.Join( - ThumbnailBasePath, - media.Guid.ToString().Substring(0, 2), - media.Guid.ToString().Substring(2, 2), - $"{media.Guid.ToString()}-{width}-{height}")); - - Directory.CreateDirectory(fileInfo.Directory.FullName); - return fileInfo.FullName; - } - private void InitDirectoryStructure() { Directory.CreateDirectory(DataPath); Directory.CreateDirectory(MediaBasePath); diff --git a/Services/MediaService.cs b/Services/MediaService.cs index 2f84b27..460e0c7 100644 --- a/Services/MediaService.cs +++ b/Services/MediaService.cs @@ -1,4 +1,7 @@ using Microsoft.EntityFrameworkCore; +using MimeDetective; +using MimeDetective.Definitions; +using System.Security.Cryptography; namespace HyperBooru.Services; @@ -9,13 +12,43 @@ public interface IMediaService { string? longDescription); public void SetIngest(Media media, bool ingest); + + public Media Create( + Stream fileData, + string fileName, + string? checksum = null, + DateTime? lastAccessTime = null, + DateTime? lastWriteTime = null, + DateTime? createTime = null); + + public void Delete(Guid media); + public void Delete(Media media); + public string GetPath(Media media); + public string GetPath(Media media, int width, int height); + } public class MediaService : IMediaService { private IDbContextFactory dbFactory; + private IConfigService config; + + private ContentInspector inspector; + + public MediaService(IDbContextFactory dbFactory, + IConfigService config) { - public MediaService(IDbContextFactory dbFactory) => this.dbFactory = dbFactory; + this.config = config; + + ContentInspectorBuilder inspectorBuilder = new() { + Definitions = + Default.FileTypes.Images.All() + .Union(Default.FileTypes.Video.All()) + .ToList() + }; + + inspector = inspectorBuilder.Build(); + } public void SetIngest(Media media, bool ingest) { using var db = dbFactory.CreateDbContext(); @@ -57,4 +90,138 @@ public class MediaService : IMediaService { db.SaveChanges(); } + + public Media Create( + Stream fileData, + string fileName, + string? checksum = null, + DateTime? lastAccessTime = null, + DateTime? lastWriteTime = null, + DateTime? createTime = null) { + + using var db = dbFactory.CreateDbContext(); + using var transaction = db.Database.BeginTransaction(); + + if(fileData.Length == 0) + throw new MediaCreateException("File is empty"); + + // Calculate the checksum using the in-memory file contents + var hash = BitConverter + .ToString(MD5.Create().ComputeHash(fileData)) + .Replace("-", "") + .ToLower(); + + if(checksum is not null && hash != checksum.ToLower()) + throw new MediaCreateException("Checksum does not match"); + + var fileRecord = new UploadedFile() { + Filename = fileName, + OriginalChecksum = hash, + UploadTime = DateTime.UtcNow, + LastAccessTime = lastAccessTime, + LastWriteTime = lastWriteTime, + CreateTime = createTime + }; + + fileData.Seek(0, SeekOrigin.Begin); + var defs = inspector.Inspect(fileData); + + var mime = defs.ByMimeType().FirstOrDefault()?.MimeType; + if(mime is null) + throw new MediaCreateException("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() { + UploadedFiles = new() { + fileRecord + }, + Tags = new() { + new() { TagDefinition = ingestTagDef } + } + }; + + using var newFile = System.IO.File.Create(GetPath(media)); + + fileData.Seek(0, SeekOrigin.Begin); + fileData.CopyTo(newFile); + newFile.Flush(); + + db.Media.Add(media); + db.SaveChanges(); + media.CurrentUploadedFile = fileRecord; + db.SaveChanges(); + } else { + media.UploadedFiles.Add(fileRecord); + db.Update(media); + db.SaveChanges(); + } + + transaction.Commit(); + + return media; + } + + public void Delete(Guid media) { + using var db = dbFactory.CreateDbContext(); + var m = db.Media.First(m => m.Guid == media); + + try { + System.IO.File.Delete( + Path.Join( + config.MediaBasePath, + m.Guid.ToString().Substring(0, 2), + m.Guid.ToString().Substring(2, 2), + m.Guid.ToString())); + } catch(IOException) {} + + try { + System.IO.Directory.Delete( + Path.Join( + config.MediaBasePath, + m.Guid.ToString().Substring(0, 2), + m.Guid.ToString().Substring(2, 2))); + } catch(IOException) {} + + try { + System.IO.Directory.Delete( + Path.Join( + config.MediaBasePath, + m.Guid.ToString().Substring(0, 2))); + } catch(IOException) {} + + db.Media.Remove(m); + db.SaveChanges(); + } + + public void Delete(Media media) => + Delete(media.Guid); + + public string GetPath(Media media) { + var fileInfo = new FileInfo( + Path.Join( + config.MediaBasePath, + media.Guid.ToString().Substring(0, 2), + media.Guid.ToString().Substring(2, 2), + media.Guid.ToString())); + + Directory.CreateDirectory(fileInfo.Directory.FullName); + return fileInfo.FullName; + } + + public string GetPath(Media media, int width, int height) { + var fileInfo = new FileInfo(Path.Join( + config.ThumbnailBasePath, + media.Guid.ToString().Substring(0, 2), + media.Guid.ToString().Substring(2, 2), + $"{media.Guid.ToString()}-{width}-{height}")); + + Directory.CreateDirectory(fileInfo.Directory.FullName); + return fileInfo.FullName; + } } -- cgit v1.3