diff options
Diffstat (limited to 'Services/MediaService.cs')
| -rw-r--r-- | Services/MediaService.cs | 329 |
1 files changed, 329 insertions, 0 deletions
diff --git a/Services/MediaService.cs b/Services/MediaService.cs new file mode 100644 index 0000000..104d0db --- /dev/null +++ b/Services/MediaService.cs @@ -0,0 +1,329 @@ +using ImageMagick; +using Microsoft.EntityFrameworkCore; +using MimeDetective; +using MimeDetective.Definitions; +using System.Security.Cryptography; +using System.Text.RegularExpressions; + +namespace HyperBooru.Services; + +public interface IMediaService { + public void SetDescription( + Media media, + string? shortDescription, + 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, + string? path = null, + PathType? pathType = null); + + public void Delete(Guid media); + public void Delete(Media media); + public void DeleteThumbnails(Guid media); + public void DeleteThumbnails(Media media); + public Stream GetThumbnail(Guid media, int? width, int? height); + public Stream GetThumbnail(Media media, int? width, int? height); + public string GetPath(Media media); + public string GetPath(Media media, int width, int height); + +} + +public class MediaService : IMediaService { + private IDbContextFactory<HBContext> dbFactory; + private IConfigService config; + + private IContentInspector inspector; + + public MediaService(IDbContextFactory<HBContext> dbFactory, + IConfigService config) { + + this.dbFactory = dbFactory; + this.config = config; + + ContentInspectorBuilder inspectorBuilder = new() { + Definitions = + DefaultDefinitions.FileTypes.Images.All() + .Union(DefaultDefinitions.FileTypes.Video.All()) + .ToList() + }; + + inspector = inspectorBuilder.Build(); + } + + public void SetIngest(Media media, bool ingest) { + using var db = dbFactory.CreateDbContext(); + media = db.Media + .Include(m => m.Tags) + .ThenInclude(t => t.TagDefinition) + .First(m => m.Guid == media.Guid); + var ingestTag = db.TagDefinitions + .First(td => td.Guid == HBContext.IngestTag); + + if(ingest) { + if(!media.Tags.Select(t => t.TagDefinition.Guid).Contains(HBContext.IngestTag)) + media.Tags.Add(new(ingestTag)); + } else { + media.Tags.RemoveAll(t => t.TagDefinition.Guid == HBContext.IngestTag); + } + + db.SaveChanges(); + } + + public void SetDescription( + Media media, + string? shortDescription, + string? longDescription) { + + using var db = dbFactory.CreateDbContext(); + var m = db.Media.First(m => m.Guid == media.Guid); + + shortDescription = shortDescription?.Trim(); + longDescription = longDescription?.Trim(); + + if(string.IsNullOrEmpty(shortDescription)) + shortDescription = null; + if(string.IsNullOrEmpty(longDescription)) + longDescription = null; + + m.ShortDescription = shortDescription; + m.LongDescription = longDescription; + + db.SaveChanges(); + } + + public Media Create( + Stream fileData, + string fileName, + string? checksum = null, + DateTime? lastAccessTime = null, + DateTime? lastWriteTime = null, + DateTime? createTime = null, + string? path = null, + PathType? pathType = 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"); + + // Determine the MIME type + 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"); + + // Read the image with ImageMagick to determine the width and height + fileData.Seek(0, SeekOrigin.Begin); + using var magickImage = new MagickImage(fileData); + + var media = db.UploadedFiles + .Include(uf => uf.Media) + .FirstOrDefault(uf => uf.Checksum == hash)? + .Media; + + var fileRecord = new UploadedFile() { + Filename = fileName, + Length = fileData.Length, + Checksum = hash, + ChecksumVerified = checksum is not null, + MimeType = mime, + Width = (int) magickImage.Width, + Height = (int) magickImage.Height, + UploadTime = DateTime.UtcNow, + LastAccessTime = lastAccessTime, + LastWriteTime = lastWriteTime, + CreateTime = createTime, + Path = pathType is null ? null : path, + PathType = pathType + }; + + 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 = 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 { + db.Entry(media).Collection(m => m.UploadedFiles).Load(); + var fileHashes = media.UploadedFiles + .Select(uf => GetUploadedFileHash(uf)); + // Only add the uploaded file record if it contains new information + if(!fileHashes.Contains(GetUploadedFileHash(fileRecord))) + 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); + + var path = Path.Join( + config.MediaBasePath, + m.Guid.ToString().Substring(0, 2), + m.Guid.ToString().Substring(2, 2), + m.Guid.ToString()); + + try { + var fileInfo = new FileInfo(path); + fileInfo.Delete(); + fileInfo.Directory?.Delete(); + fileInfo.Directory?.Parent?.Delete(); + } catch(IOException) {} + + try { + DeleteThumbnails(media); + } catch {} + + db.Media.Remove(m); + db.SaveChanges(); + } + + public void Delete(Media media) => + Delete(media.Guid); + + public void DeleteThumbnails(Guid media) { + var dir = new DirectoryInfo(Path.Join( + config.ThumbnailBasePath, + media.ToString().Substring(0, 2), + media.ToString().Substring(2, 2))); + + var pattern = new Regex($"^{media}-[0-9]+-[0-9]+$"); + var toDelete = dir.GetFiles() + .Where(f => pattern.IsMatch(f.Name)) + .ToList(); + + List<Exception> exceptions = new(); + + foreach(var file in toDelete) { + try { + file.Delete(); + } catch(Exception e) { + exceptions.Add(e); + } + } + + try { + dir.Delete(); + dir.Parent?.Delete(); + } catch(Exception e) { + exceptions.Add(e); + } + + // TODO: wrap the AggregateException in a ThumbnailException + if(exceptions.Count() > 1) + throw new AggregateException(exceptions); + } + + public void DeleteThumbnails(Media media) => + DeleteThumbnails(media.Guid); + + public Stream GetThumbnail(Guid media, int? width, int? height) { + using var db = dbFactory.CreateDbContext(); + + var m = db.Media + .Include(m => m.CurrentUploadedFile) + .First(m => m.Guid == media); + if(m is null) + throw new ObjectNotFoundException(media); + + if(m.CurrentUploadedFile.MimeType.Split("/")[0] != "image") + throw new ThumbnailException("Media object not an image", m); + + using var image = new MagickImage(GetPath(m)); + + if(width is null && height is null) + throw new ThumbnailException("Both width and height cannot be null!", m); + + if(width > image.Width || height > image.Height) + throw new ThumbnailException("Requested thumbnail size is larger than original media", m); + + #pragma warning disable CS8629 + int w = (int) (width is not null ? width : image.Width * height / image.Height); + int h = (int) (height is not null ? height : image.Height * width / image.Width); + #pragma warning restore CS8629 + + var thumbPath = GetPath(m, w, h); + + if(!File.Exists(thumbPath)) { + image.Resize((uint) w, (uint) h); + image.Write(thumbPath); + } + + return System.IO.File.OpenRead(thumbPath); + } + + public Stream GetThumbnail(Media media, int? width, int? height) => + GetThumbnail(media.Guid, width, height); + + 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; + } + + private int GetUploadedFileHash(UploadedFile uf) => ( + uf.CreateTime, + uf.LastWriteTime, + uf.Filename, + uf.Length, + uf.Checksum).GetHashCode(); +} |
