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);