Files
mixtape/zero.Core/Media/MediaCreator.cs
T

153 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 IMediaStore Store { get; set; }
protected MediaOptions Options { get; set; }
public MediaCreator(IMediaFileSystem fileSystem, IMediaStore store, IZeroOptions options)
{
FileSystem = fileSystem;
Store = store;
Options = options.For<MediaOptions>();
}
/// <inheritdoc />
public 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("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<Rgba32> image = await Image.LoadAsync<Rgba32>(fileInfo.AbsolutePath);
model.ImageMeta = GetImageMetadata(image);
string extension = Path.GetExtension(model.Path);
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 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<Media>.Success(model);
}
/// <inheritdoc />
protected virtual MediaImageMetadata GetImageMetadata(Image<Rgba32> 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
};
}
/// <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);
}