using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using System.IO; namespace zero.Media; public class MediaCreator : IMediaCreator { protected IMediaFileSystem FileSystem { get; set; } protected IMediaStore Store { get; set; } protected MediaOptions Options { get; set; } public MediaCreator(IMediaFileSystem fileSystem, IMediaStore store, IZeroOptions options) { FileSystem = fileSystem; Store = store; Options = options.For(); } /// public async Task> UploadFile(Stream fileStream, string filename, string folderId = null, CancellationToken cancellationToken = default) { string fileExtension = Path.GetExtension(filename); string normalizedFilename = Safenames.File(filename); bool isImage = Options.AllowedImageFileExtensions.Contains(fileExtension, StringComparer.InvariantCultureIgnoreCase); bool isDocument = !isImage && Options.AllowedOtherFileExtensions.Contains(fileExtension, StringComparer.InvariantCultureIgnoreCase); if (!isImage && !isDocument) { // TODO error return Result.Fail("ERROR"); } Media model = await Store.Empty(); model.Name = normalizedFilename; model.ParentId = folderId; model.IsFolder = false; // create directory which hosts the media file // the media directory is a flat folder where each folder contains one media file (+ thumbs) string directory = await CreateDirectory(cancellationToken); model.FileId = directory; // the path is a combination of the folder name + filename, e.g. 129021309123/myfile.jpg model.Path = directory + '/' + normalizedFilename; // store the file in the attached file system // we do not need to check if the file already exists in the file system // as we only use newly created directories, therefore a collision shouldn't happen await FileSystem.CreateFile(model.Path, fileStream, cancellationToken: cancellationToken); // we need file metadata to get info about file size and the physical path for image modification IFileMeta fileInfo = await FileSystem.GetFileInfo(model.Path, cancellationToken); model.Size = fileInfo.Length; if (isImage) { using Image image = await Image.LoadAsync(fileInfo.AbsolutePath); model.ImageMeta = GetImageMetadata(image); string extension = Path.GetExtension(model.Path); foreach ((string key, ResizeOptions opts) in Options.Thumbnails) { Image imageFrame = image.Frames.Count > 1 ? image.Frames.CloneFrame(0) : image.Clone(); imageFrame.Mutate(x => x.Resize(opts)); using MemoryStream stream = new(); await imageFrame.SaveAsync(stream, new JpegEncoder() { Quality = 80 }, cancellationToken); stream.Position = 0; string thumbFilename = normalizedFilename.TrimEnd(extension) + "." + Safenames.File(key) + ".jpg"; string path = directory + '/' + thumbFilename; await FileSystem.CreateFile(path, stream, cancellationToken: cancellationToken); model.ImageMeta.Thumbnails[key] = path; } } return Result.Success(model); } /// protected virtual MediaImageMetadata GetImageMetadata(Image image) { PngMetadata pngMetadata = image.Metadata.GetPngMetadata(); //WebpMetadata webpMetadata = image.Metadata.GetWebpMetadata(); return new MediaImageMetadata() { Width = image.Width, Height = image.Height, ImageTakenDate = new DateTimeOffset(image.Metadata.IccProfile?.Header?.CreationDate ?? DateTime.Now), DPI = image.Metadata.HorizontalResolution, ColorSpace = image.Metadata.IccProfile?.Header?.DataColorSpace.ToString(), HasTransparency = pngMetadata?.HasTransparency ?? false, Frames = image.Frames.Count }; } /// /// Create a new directory for a file. /// This method is collision-aware and repeats until a directory can be created. /// protected virtual async Task CreateDirectory(CancellationToken cancellationToken = default) { try { string directoryName = GetNewDirectoryName(); await FileSystem.CreateDirectory(directoryName, cancellationToken); return directoryName; } catch (FileSystemException ex) when (ex.Message.Contains("already exists")) { return await CreateDirectory(cancellationToken); } } /// /// Builds a directory name for a new media item /// protected virtual string GetNewDirectoryName() { return Guid.NewGuid().ToString(); } } public interface IMediaCreator { /// /// Uploads a file by using the attached file system /// /// A temporary media file which can be persisted in a store Task> UploadFile(Stream fileStream, string filename, string folderId = null, CancellationToken cancellationToken = default); }