using Microsoft.EntityFrameworkCore; using MimeDetective; using MimeDetective.Definitions; using System.Security.Cryptography; 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); 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) { 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(); 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); 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) { 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() { Checksum = hash, MimeType = mime, 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); } 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; } }