Files
mixtape/zero/Media/MediaCreator.cs
T
2024-03-21 13:34:35 +01:00

150 lines
5.0 KiB
C#

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 MediaOptions Options { get; set; }
public MediaCreator(IMediaFileSystem fileSystem, IZeroOptions options)
{
FileSystem = fileSystem;
Options = options.For<MediaOptions>();
}
/// <inheritdoc />
public virtual async Task<Result<Media>> 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<Media>.Fail("This file type is not supported");
}
Media model = new();
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<Rgba32> image = await Image.LoadAsync<Rgba32>(fileInfo.AbsolutePath, cancellationToken);
model.Metadata = GetImageMetadata(image);
string extension = Path.GetExtension(model.Path);
if (Options.Thumbnails != null)
{
foreach ((string key, ResizeOptions opts) in Options.Thumbnails)
{
Image<Rgba32> 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 PngEncoder(), cancellationToken);
stream.Position = 0;
string thumbFilename = normalizedFilename.TrimEnd(extension) + "." + Safenames.File(key) + ".png";
string path = directory + '/' + thumbFilename;
await FileSystem.CreateFile(path, stream, cancellationToken: cancellationToken);
model.Thumbnails[key] = path;
}
}
}
return Result<Media>.Success(model);
}
/// <inheritdoc />
protected virtual MediaMetadata GetImageMetadata(IImageInfo image)
{
PngMetadata pngMetadata = image.Metadata.GetPngMetadata();
//WebpMetadata webpMetadata = image.Metadata.GetWebpMetadata();
return new MediaMetadata()
{
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
};
}
/// <summary>
/// Create a new directory for a file.
/// This method is collision-aware and repeats until a directory can be created.
/// </summary>
protected virtual async Task<string> 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);
}
}
/// <summary>
/// Builds a directory name for a new media item
/// </summary>
protected virtual string GetNewDirectoryName()
{
return Guid.NewGuid().ToString();
}
}
public interface IMediaCreator
{
/// <summary>
/// Uploads a file by using the attached file system
/// </summary>
/// <returns>A temporary media file which can be persisted in a store</returns>
Task<Result<Media>> UploadFile(Stream fileStream, string filename, string folderId = null, CancellationToken cancellationToken = default);
}