diff --git a/zero.Core/FileStorage/FileSystemOptions.cs b/zero.Core/FileStorage/FileSystemOptions.cs new file mode 100644 index 00000000..97c25f95 --- /dev/null +++ b/zero.Core/FileStorage/FileSystemOptions.cs @@ -0,0 +1,6 @@ +namespace zero.FileStorage; + +public class FileSystemOptions +{ + public string ZeroAssetsPath { get; set; } +} \ No newline at end of file diff --git a/zero.Core/FileStorage/IFileMeta.cs b/zero.Core/FileStorage/IFileMeta.cs index 48edc66c..cd6b49dd 100644 --- a/zero.Core/FileStorage/IFileMeta.cs +++ b/zero.Core/FileStorage/IFileMeta.cs @@ -1,12 +1,17 @@ namespace zero.FileStorage; -public interface IFileInfo +public interface IFileMeta { /// /// Full path of the item within the file system /// string Path { get; } + /// + /// Absolute path of the item + /// + string AbsolutePath { get; } + /// /// Name of the item /// diff --git a/zero.Core/FileStorage/IFileSystem.cs b/zero.Core/FileStorage/IFileSystem.cs index 6d34bcc5..b2a28a47 100644 --- a/zero.Core/FileStorage/IFileSystem.cs +++ b/zero.Core/FileStorage/IFileSystem.cs @@ -4,6 +4,11 @@ namespace zero.FileStorage; public interface IFileSystem { + /// + /// Determines whether a file or directory at the given path already exists + /// + Task Exists(string path, CancellationToken cancellationToken = default); + /// /// Get information of a file /// diff --git a/zero.Core/FileStorage/PhysicalFileMeta.cs b/zero.Core/FileStorage/PhysicalFileMeta.cs index 13aa767f..6f89c6dc 100644 --- a/zero.Core/FileStorage/PhysicalFileMeta.cs +++ b/zero.Core/FileStorage/PhysicalFileMeta.cs @@ -4,12 +4,15 @@ namespace zero.FileStorage; public class PhysicalFileMeta : IFileMeta { + /// + public string Name { get; } + /// public string Path { get; } /// - public string Name { get; } - + public string AbsolutePath { get; } + /// public string DirectoryPath { get; } @@ -29,6 +32,7 @@ public class PhysicalFileMeta : IFileMeta public PhysicalFileMeta(IFileInfo fileInfo, string path) { Path = path; + AbsolutePath = fileInfo.PhysicalPath; Name = fileInfo.Name; DirectoryPath = path.Substring(0, path.Length - fileInfo.Name.Length).TrimEnd('/'); LastModifiedDate = fileInfo.LastModified; diff --git a/zero.Core/FileStorage/PhysicalFileSystem.cs b/zero.Core/FileStorage/PhysicalFileSystem.cs index 8ef5d01a..a633fc5c 100644 --- a/zero.Core/FileStorage/PhysicalFileSystem.cs +++ b/zero.Core/FileStorage/PhysicalFileSystem.cs @@ -5,17 +5,32 @@ namespace zero.FileStorage; public class PhysicalFileSystem : IFileSystem { - readonly string basePath; + readonly string _root; - public PhysicalFileSystem(string basePath) + public PhysicalFileSystem(string root) { - this.basePath = basePath; + _root = root; } /// - public Task GetFileInfo(string path, CancellationToken cancellationToken = default) + public virtual Task Exists(string path, CancellationToken cancellationToken = default) + { + try + { + string resolvedPath = ResolvePath(path); + return Task.FromResult(File.Exists(resolvedPath) || Directory.Exists(resolvedPath)); + } + catch (Exception ex) when (ex is not FileSystemException) + { + throw new FileSystemException($"Could not get file exists info for path '{path}'", ex); + } + } + + + /// + public virtual Task GetFileInfo(string path, CancellationToken cancellationToken = default) { try { @@ -38,7 +53,7 @@ public class PhysicalFileSystem : IFileSystem /// - public Task StreamFile(string path, CancellationToken cancellationToken = default) + public virtual Task StreamFile(string path, CancellationToken cancellationToken = default) { try { @@ -59,7 +74,7 @@ public class PhysicalFileSystem : IFileSystem /// - public async Task CreateFile(string path, Stream fileStream, bool overwrite = false, CancellationToken cancellationToken = default) + public virtual async Task CreateFile(string path, Stream fileStream, bool overwrite = false, CancellationToken cancellationToken = default) { try { @@ -85,7 +100,7 @@ public class PhysicalFileSystem : IFileSystem /// - public Task DeleteFile(string path, CancellationToken cancellationToken = default) + public virtual Task DeleteFile(string path, CancellationToken cancellationToken = default) { try { @@ -101,7 +116,7 @@ public class PhysicalFileSystem : IFileSystem /// - public Task MoveFile(string oldPath, string newPath, CancellationToken cancellationToken = default) + public virtual Task MoveFile(string oldPath, string newPath, CancellationToken cancellationToken = default) { try { @@ -129,7 +144,7 @@ public class PhysicalFileSystem : IFileSystem /// - public Task CopyFile(string oldPath, string newPath, CancellationToken cancellationToken = default) + public virtual Task CopyFile(string oldPath, string newPath, CancellationToken cancellationToken = default) { try { @@ -157,11 +172,11 @@ public class PhysicalFileSystem : IFileSystem /// - public Task GetDirectoryInfo(string path, CancellationToken cancellationToken = default) => GetFileInfo(path, cancellationToken); + public virtual Task GetDirectoryInfo(string path, CancellationToken cancellationToken = default) => GetFileInfo(path, cancellationToken); /// - public IAsyncEnumerable GetDirectoryContent(string path = null, bool recursive = false, CancellationToken cancellationToken = default) + public virtual IAsyncEnumerable GetDirectoryContent(string path = null, bool recursive = false, CancellationToken cancellationToken = default) { try { @@ -177,13 +192,13 @@ public class PhysicalFileSystem : IFileSystem results.AddRange(Directory.GetDirectories(resolvedPath, "*", searchOption).Select(f => { PhysicalDirectoryInfo fileSystemInfo = new(new DirectoryInfo(f)); - return new PhysicalFileMeta(fileSystemInfo, ResolvePath(f.Substring(basePath.Length))); + return new PhysicalFileMeta(fileSystemInfo, ResolvePath(f.Substring(_root.Length))); })); results.AddRange(Directory.GetFiles(resolvedPath, "*", searchOption).Select(f => { PhysicalFileInfo fileSystemInfo = new(new FileInfo(f)); - return new PhysicalFileMeta(fileSystemInfo, ResolvePath(f.Substring(basePath.Length))); + return new PhysicalFileMeta(fileSystemInfo, ResolvePath(f.Substring(_root.Length))); })); return results.ToAsyncEnumerable(); @@ -196,7 +211,7 @@ public class PhysicalFileSystem : IFileSystem /// - public Task CreateDirectory(string path, CancellationToken cancellationToken = default) + public virtual Task CreateDirectory(string path, CancellationToken cancellationToken = default) { try { @@ -222,7 +237,7 @@ public class PhysicalFileSystem : IFileSystem /// - public Task DeleteDirectory(string path, bool recursive = false, CancellationToken cancellationToken = default) + public virtual Task DeleteDirectory(string path, bool recursive = false, CancellationToken cancellationToken = default) { try { @@ -240,23 +255,23 @@ public class PhysicalFileSystem : IFileSystem /// /// Creates a fully-usable absolte path from the given path /// - protected string ResolvePath(string path) + protected virtual string ResolvePath(string path) { try { if (path.IsNullOrWhiteSpace()) { - return basePath; + return _root; } // normalize path path = path.Replace('\\', '/').FullTrim().Trim('/'); - string physicalPath = Path.Combine(basePath, path); + string physicalPath = Path.Combine(_root, path); // do not allow paths which are outside this file system // the boundaries are set be the basePath which is assigned in the file system constructor - if (!Path.GetFullPath(physicalPath).StartsWith(basePath, StringComparison.InvariantCultureIgnoreCase)) + if (!Path.GetFullPath(physicalPath).StartsWith(_root, StringComparison.InvariantCultureIgnoreCase)) { throw new FileSystemException($"The path '{path}' is outside the file system"); } diff --git a/zero.Core/FileStorage/ServiceCollectionExtensions.cs b/zero.Core/FileStorage/ServiceCollectionExtensions.cs index 82ac759b..49bdfe0c 100644 --- a/zero.Core/FileStorage/ServiceCollectionExtensions.cs +++ b/zero.Core/FileStorage/ServiceCollectionExtensions.cs @@ -1,13 +1,20 @@ using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace zero.FileStorage; internal static class ServiceCollectionExtensions { - public static IServiceCollection AddZeroFileStorage(this IServiceCollection services) + public static IServiceCollection AddZeroFileStorage(this IServiceCollection services, IConfiguration config) { - services.AddScoped(factory => new Paths(factory.GetService(), true)); + services.AddScoped(factory => new Paths(factory.GetService())); + + services.AddOptions().Bind(config.GetSection("Zero:FileSystem")).Configure(opts => + { + opts.ZeroAssetsPath = "zero"; + }); + return services; } } \ No newline at end of file diff --git a/zero.Core/Media/Indexes/Media_ByParent.cs b/zero.Core/Media/Indexes/Media_ByParent.cs index a0fe06e5..e1419225 100644 --- a/zero.Core/Media/Indexes/Media_ByParent.cs +++ b/zero.Core/Media/Indexes/Media_ByParent.cs @@ -27,7 +27,7 @@ public class Media_ByParent : ZeroMultiMapIndex CreatedDate = item.CreatedDate, IsFolder = false, Name = item.Name, - Image = item.PreviewSource, + Image = item.PathPreview, Children = 0, Size = item.Size, IsShared = item.Blueprint != null, diff --git a/zero.Core/Media/MediaCollection.cs b/zero.Core/Media/MediaCollection.cs index ff1d1526..df247156 100644 --- a/zero.Core/Media/MediaCollection.cs +++ b/zero.Core/Media/MediaCollection.cs @@ -222,15 +222,15 @@ namespace zero.Core.Collections /// /// Create image data if available /// - protected virtual MediaImageMeta GetImageMeta(Image image) + protected virtual MediaImageMetadata GetImageMeta(Image image) { var pngMetadata = image.Metadata.GetPngMetadata(); - return new MediaImageMeta() + return new MediaImageMetadata() { Width = image.Width, Height = image.Height, - CreatedDate = new DateTimeOffset(image.Metadata.IccProfile?.Header?.CreationDate ?? DateTime.Now), + 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, diff --git a/zero.Core/Media/MediaCreator.cs b/zero.Core/Media/MediaCreator.cs new file mode 100644 index 00000000..10275f0d --- /dev/null +++ b/zero.Core/Media/MediaCreator.cs @@ -0,0 +1,166 @@ +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.PixelFormats; +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(filename, StringComparer.InvariantCultureIgnoreCase); + + if (!isImage && !isDocument) + { + // TODO error + return null; + } + + Media model = await Store.Empty(); + model.Name = normalizedFilename; + model.ParentId = folderId; + model.Type = isImage ? MediaType.Image : MediaType.File; + + // 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); + + // TODO save thumbnails + } + + return model; + } + + + //public virtual async Task CreateImageThumbnails(Image image, CancellationToken cancellationToken = default) + //{ + // 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)); + // } + // string source = SaveThumbnail(media, imageFrame, PREVIEW_EXTENSION, new ResizeOptions() + // { + // Size = new Size(210, 210), + // Mode = ResizeMode.Min + // }); + + // //media.ThumbnailSource = SaveThumbnail(media, imageFrame, THUMB_EXTENSION, new ResizeOptions() + // //{ + // // Size = new Size(100, 100), + // // Mode = ResizeMode.Max + // //}); + //} + + + /// + /// Saves a thumbnail of an image + /// + //public virtual string SaveThumbnail(Media media, Image image, string extensionPrefix, ResizeOptions resizeOptions) + //{ + // string extension = Path.GetExtension(media.Path); + + // image.Mutate(x => x.Resize(resizeOptions)); + + // string thumbFileName = media.Name.TrimEnd(extension) + extensionPrefix + extension; + // image.Save() + // image.Save(Path.Combine(Paths.Media, media.FileId, thumbFileName)); + // return Path.Combine(PATH_PREFIX, media.FileId, thumbFileName).Replace(Path.DirectorySeparatorChar, PATH_SEPARATOR); + //} + + + /// + protected virtual MediaImageMetadata GetImageMetadata(Image image) + { + PngMetadata pngMetadata = image.Metadata.GetPngMetadata(); + + 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); +} \ No newline at end of file diff --git a/zero.Core/Media/MediaFileSystem.cs b/zero.Core/Media/MediaFileSystem.cs new file mode 100644 index 00000000..3b381485 --- /dev/null +++ b/zero.Core/Media/MediaFileSystem.cs @@ -0,0 +1,28 @@ +namespace zero.Media; + +public class MediaFileSystem : PhysicalFileSystem, IMediaFileSystem +{ + protected string PublicPathPrefix { get; } + + + public MediaFileSystem(string root, string publicPathPrefix) : base(root) + { + PublicPathPrefix = (publicPathPrefix ?? String.Empty).EnsureEndsWith('/'); + } + + + /// + public virtual string MapToPublicPath(string path) + { + return PublicPathPrefix + path.TrimStart('/'); + } +} + + +public interface IMediaFileSystem : IFileSystem +{ + /// + /// Map an internal path to a public accessible path + /// + string MapToPublicPath(string path); +} \ No newline at end of file diff --git a/zero.Core/Media/MediaFolderStore.cs b/zero.Core/Media/MediaFolderStore.cs deleted file mode 100644 index 58cb0192..00000000 --- a/zero.Core/Media/MediaFolderStore.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace zero.Media; - -public class MediaFolderStore : EntityStore, IMediaFolderStore -{ - public MediaFolderStore(IStoreContext context) : base(context) - { - - } -} - - -public interface IMediaFolderStore : IEntityStore -{ -} \ No newline at end of file diff --git a/zero.Core/Media/MediaManagement.cs b/zero.Core/Media/MediaManagement.cs new file mode 100644 index 00000000..ec548c20 --- /dev/null +++ b/zero.Core/Media/MediaManagement.cs @@ -0,0 +1,111 @@ +namespace zero.Media; + +public class MediaManagement : IMediaManagement +{ + protected IMediaFileSystem FileSystem { get; set; } + + protected IMediaStore Store { get; set; } + + + public MediaManagement(IMediaFileSystem fileSystem, IMediaStore store) + { + FileSystem = fileSystem; + Store = store; + } + + + /// + public virtual async Task GetFile(string id) + { + Media file = await Store.Load(id); + return file != null && file.Type != MediaType.Folder ? file : null; + } + + + /// + public virtual async Task> UpdateFile(Media file) + { + // TODO check new file/image/media + return await Store.Update(file); + } + + + /// + public virtual async Task> DeleteFile(Media file) + { + // TODO delete in file system + return await Store.Delete(file); + } + + + /// + public virtual async Task GetFolder(string id) + { + Media folder = await Store.Load(id); + return folder != null && folder.Type == MediaType.Folder ? folder : null; + } + + + /// + public virtual async Task> CreateFolder(Media folder) + { + folder.IsActive = true; + folder.Type = MediaType.Folder; + return await Store.Create(folder); + } + + + /// + public virtual async Task> CreateFolder(string name, string parentId = null) + { + Media media = await Store.Empty(); + media.Name = name; + media.ParentId = parentId; + return await CreateFolder(media); + } + + + /// + public virtual async Task> UpdateFolder(Media folder) + { + return await Store.Update(folder); + } + + + /// + public virtual async Task> DeleteFolder(Media folder) + { + // TODO recursive + return await Store.DeleteWithDescendants(folder); + } +} + + +public interface IMediaManagement +{ + /// + /// Get a media folder by id + /// + Task GetFolder(string id); + + /// + /// Creates a new media folder + /// + Task> CreateFolder(Media folder); + + /// + /// Creates a new media folder + /// + Task> CreateFolder(string name, string parentId = null); + + /// + /// Rename and store a media folder + /// + Task> UpdateFolder(Media folder); + + + /// + /// Deletes a folder, as well as all descendant folders and files + /// + Task> DeleteFolder(Media folder); +} diff --git a/zero.Core/Media/MediaManagementExtensions.cs b/zero.Core/Media/MediaManagementExtensions.cs new file mode 100644 index 00000000..40853b56 --- /dev/null +++ b/zero.Core/Media/MediaManagementExtensions.cs @@ -0,0 +1,69 @@ +namespace zero.Media +{ + public static class MediaManagementExtensions + { + /// + /// Rename and store a media folder + /// + public static async Task> RenameFolder(this IMediaManagement media, Media folder, string newName) + { + folder.Name = newName; + return await media.UpdateFolder(folder); + } + + + /// + /// Rename and store a media folder + /// + public static async Task> RenameFolder(this IMediaManagement media, string folderId, string newName) + { + Media folder = await media.GetFolder(folderId); + if (folder == null) + { + return EntityResult.Fail("@errors.idnotfound"); + } + + return await RenameFolder(media, folder, newName); + } + + + /// + /// Move a media folder to a new parent + /// + public static async Task> MoveFolder(this IMediaManagement media, Media folder, string newParentId) + { + folder.ParentId = newParentId; + return await media.UpdateFolder(folder); + } + + + /// + /// Move a media folder to a new parent + /// + public static async Task> MoveFolder(this IMediaManagement media, string folderId, string newParentId) + { + Media folder = await media.GetFolder(folderId); + if (folder == null) + { + return EntityResult.Fail("@errors.idnotfound"); + } + + return await MoveFolder(media, folder, newParentId); + } + + + /// + /// Deletes a folder by id + /// + public static async Task> DeleteFolder(this IMediaManagement media, string folderId) + { + Media folder = await media.GetFolder(folderId); + if (folder == null) + { + return EntityResult.Fail("@errors.idnotfound"); + } + + return await media.DeleteFolder(folder); + } + } +} diff --git a/zero.Core/Media/MediaOptions.cs b/zero.Core/Media/MediaOptions.cs new file mode 100644 index 00000000..34580cb2 --- /dev/null +++ b/zero.Core/Media/MediaOptions.cs @@ -0,0 +1,17 @@ +using SixLabors.ImageSharp.Processing; + +namespace zero.Media; + +public class MediaOptions +{ + public string FolderPath { get; set; } + + public string PublicPathPrefix { get; set; } + + public List AllowedOtherFileExtensions { get; set; } + + public List AllowedImageFileExtensions { get; set; } + + public Dictionary Thumbnails { get; set; } +} +} \ No newline at end of file diff --git a/zero.Core/Media/MediaStore.cs b/zero.Core/Media/MediaStore.cs index 902489d1..3fbb88a8 100644 --- a/zero.Core/Media/MediaStore.cs +++ b/zero.Core/Media/MediaStore.cs @@ -1,14 +1,30 @@ namespace zero.Media; -public class MediaStore : EntityStore, IMediaStore +public class MediaStore : TreeEntityStore, IMediaStore { public MediaStore(IStoreContext context) : base(context) { } + + /// + /// A media (either file or folder) can only be saved for the following circumstances: + /// 1. The parent is null (=root) + /// 2. The parent is a folder + /// + public override async Task IsAllowedAsChild(Media model, string parentId) + { + if (parentId.IsNullOrEmpty()) + { + return true; + } + + Media parent = await Load(parentId); + return parent != null && parent.Type == MediaType.Folder; + } } -public interface IMediaStore : IEntityStore +public interface IMediaStore : ITreeEntityStore { } \ No newline at end of file diff --git a/zero.Core/Media/Models/Media.cs b/zero.Core/Media/Models/Media.cs index aca12a48..f5454586 100644 --- a/zero.Core/Media/Models/Media.cs +++ b/zero.Core/Media/Models/Media.cs @@ -4,7 +4,7 @@ /// A media file (can contain an image or other media like videos and documents) /// [RavenCollection("Media")] -public class Media : ZeroEntity +public class Media : ZeroEntity, IZeroTreeEntity { /// /// Id/name of the phyiscal folder which is stored on disk/cloud @@ -12,9 +12,9 @@ public class Media : ZeroEntity public string FileId { get; set; } /// - /// Id of the media folder + /// Id of the parent folder /// - public string FolderId { get; set; } + public string ParentId { get; set; } /// /// Alternative text which is used when the image can't be loaded @@ -29,17 +29,13 @@ public class Media : ZeroEntity /// /// Path of the media item /// - public string Source { get; set; } + public string Path { get; set; } /// - /// For images this is the source for a 100x100px thumbnail + /// Define custom thumbnails which are generated on upload + /// (see IZeroOptions.For().Thumbnails) /// - public string ThumbnailSource { get; set; } - - /// - /// For images this is the source for a [proportional]x210px thumbnail - /// - public string PreviewSource { get; set; } + public Dictionary Thumbnails { get; set; } = new(); /// /// Filesize in bytes @@ -49,7 +45,7 @@ public class Media : ZeroEntity /// /// Meta data for images /// - public MediaImageMeta ImageMeta { get; set; } + public MediaImageMetadata ImageMeta { get; set; } /// /// Optional focal point for an image diff --git a/zero.Core/Media/Models/MediaImageMeta.cs b/zero.Core/Media/Models/MediaImageMetadata.cs similarity index 90% rename from zero.Core/Media/Models/MediaImageMeta.cs rename to zero.Core/Media/Models/MediaImageMetadata.cs index 2aca31d0..8528d066 100644 --- a/zero.Core/Media/Models/MediaImageMeta.cs +++ b/zero.Core/Media/Models/MediaImageMetadata.cs @@ -3,7 +3,7 @@ /// /// Metadata for images /// -public class MediaImageMeta +public class MediaImageMetadata { /// /// Width in pixels @@ -23,7 +23,7 @@ public class MediaImageMeta /// /// Date the image was taken /// - public DateTimeOffset? CreatedDate { get; set; } + public DateTimeOffset? ImageTakenDate { get; set; } /// /// Original color space of the image diff --git a/zero.Core/Media/Models/MediaSourceSize.cs b/zero.Core/Media/Models/MediaSourceSize.cs deleted file mode 100644 index 3fe01731..00000000 --- a/zero.Core/Media/Models/MediaSourceSize.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace zero.Media; - -public enum MediaSourceSize -{ - Original = 0, - Preview = 1, - Thumbnail = 2 -} diff --git a/zero.Core/Media/Models/MediaType.cs b/zero.Core/Media/Models/MediaType.cs index 92884f4c..c3c900c1 100644 --- a/zero.Core/Media/Models/MediaType.cs +++ b/zero.Core/Media/Models/MediaType.cs @@ -2,6 +2,7 @@ public enum MediaType { - File = 0, - Image = 1 + Folder = 0, + File = 1, + Image = 2 } \ No newline at end of file diff --git a/zero.Core/Media/ServiceCollectionExtensions.cs b/zero.Core/Media/ServiceCollectionExtensions.cs index 868524b0..58f8c137 100644 --- a/zero.Core/Media/ServiceCollectionExtensions.cs +++ b/zero.Core/Media/ServiceCollectionExtensions.cs @@ -1,13 +1,38 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using SixLabors.ImageSharp.Processing; +using System.IO; namespace zero.Media; internal static class ServiceCollectionExtensions { - public static IServiceCollection AddZeroMedia(this IServiceCollection services) + public static IServiceCollection AddZeroMedia(this IServiceCollection services, IConfiguration config) { services.AddScoped(); - services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + services.AddSingleton(svc => + { + IOptions options = svc.GetRequiredService>(); + IWebHostEnvironment env = svc.GetRequiredService(); + return new(Path.Combine(env.WebRootPath, options.Value.FolderPath), options.Value.PublicPathPrefix); + }); + + services.AddOptions().Bind(config.GetSection("Zero:Media")).Configure(opts => + { + opts.FolderPath = "uploads"; + opts.AllowedOtherFileExtensions = new() { ".pdf" }; + opts.AllowedImageFileExtensions = new() { ".jpg", ".jpeg", ".png", ".bmp", ".webp", ".gif" }; + opts.Thumbnails = new() + { + { "thumb", new ResizeOptions() { Size = new(100, 100), Mode = ResizeMode.Max } }, + { "preview", new ResizeOptions() { Size = new(210, 210), Mode = ResizeMode.Min } } + }; + }); services.Configure(opts => { diff --git a/zero.Core/Stores/TreeEntityStore.cs b/zero.Core/Stores/TreeEntityStore.cs index a9a44020..33139cb6 100644 --- a/zero.Core/Stores/TreeEntityStore.cs +++ b/zero.Core/Stores/TreeEntityStore.cs @@ -7,6 +7,27 @@ public abstract class TreeEntityStore : EntityStore, ITreeEntityStore w { public TreeEntityStore(IStoreContext collectionContext) : base(collectionContext) { } + + /// + public override async Task> Create(T model) + { + if (!await IsAllowedAsChild(model, model.ParentId)) + { + return EntityResult.Fail("@errors.treeentity.parentnotallowed"); + } + return await base.Create(model); + } + + /// + public override async Task> Update(T model) + { + if (!await IsAllowedAsChild(model, model.ParentId)) + { + return EntityResult.Fail("@errors.treeentity.parentnotallowed"); + } + return await base.Update(model); + } + /// public virtual Task IsAllowedAsChild(T model, string parentId) => Task.FromResult(true);