From 3fc06b5fd556972d60dd884d42dc8544e52a5998 Mon Sep 17 00:00:00 2001 From: Tobias Klika Date: Wed, 2 Sep 2020 15:46:54 +0200 Subject: [PATCH] new output for media --- zero.Core/Api/MediaApi.cs | 176 +++++++++++-- zero.Core/Api/MediaFolderApi.cs | 38 +-- zero.Core/Api/MediaUploadApi.cs | 143 ----------- zero.Core/Database/Indexes/Media_ByParent.cs | 41 +++ zero.Core/Entities/Media/Media.cs | 7 +- zero.Core/Entities/Media/MediaFolder.cs | 23 +- zero.Core/Entities/Media/MediaListItem.cs | 25 ++ zero.Core/Entities/Media/MediaListQuery.cs | 8 +- zero.Core/Validation/MediaFolderValidator.cs | 2 +- .../App/components/datagrid/datagrid.vue | 12 +- zero.Web.UI/App/pages/media/folder.vue | 2 +- .../App/pages/media/media-item-old.vue | 41 +++ zero.Web.UI/App/pages/media/media-item.vue | 63 ++++- zero.Web.UI/App/pages/media/media.vue | 234 +----------------- zero.Web.UI/App/resources/media.js | 6 + zero.Web/Controllers/MediaController.cs | 97 ++------ zero.Web/Controllers/MediaFolderController.cs | 48 +--- zero.Web/Defaults/ZeroBackofficePlugin.cs | 3 +- zero.Web/Models/MediaListResultModel.cs | 6 +- 19 files changed, 427 insertions(+), 548 deletions(-) delete mode 100644 zero.Core/Api/MediaUploadApi.cs create mode 100644 zero.Core/Database/Indexes/Media_ByParent.cs create mode 100644 zero.Core/Entities/Media/MediaListItem.cs create mode 100644 zero.Web.UI/App/pages/media/media-item-old.vue diff --git a/zero.Core/Api/MediaApi.cs b/zero.Core/Api/MediaApi.cs index 46e340df..b5504cf1 100644 --- a/zero.Core/Api/MediaApi.cs +++ b/zero.Core/Api/MediaApi.cs @@ -3,12 +3,32 @@ using System.Collections.Generic; using System.Threading.Tasks; using zero.Core.Entities; using zero.Core.Extensions; +using Microsoft.AspNetCore.Http; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using System; +using System.IO; +using System.Linq; +using System.Threading; +using zero.Core.Database.Indexes; +using Raven.Client.Documents; namespace zero.Core.Api { public class MediaApi : AppAwareBackofficeApi, IMediaApi { - protected IPaths Paths { get; private set; } + protected const char PATH_SEPARATOR = '/'; + + protected const string PATH_PREFIX = "/uploads"; + + protected const string THUMB_EXTENSION = ".thumb"; + + protected const string PREVIEW_EXTENSION = ".preview"; + + protected string[] ImageExtensions = new[] { ".jpg", ".jpeg", ".png", ".bmp", ".webp", ".jfif", ".gif" }; + + protected IPaths Paths { get; set; } public MediaApi(IBackofficeStore store, IPaths paths) : base(store) @@ -18,7 +38,7 @@ namespace zero.Core.Api /// - public async Task GetById(string id) + public async Task GetById(string id) { return await GetById(id); } @@ -27,7 +47,7 @@ namespace zero.Core.Api /// public async Task GetSourceById(string id, bool thumb = false) { - Media media = await GetById(id); + IMedia media = await GetById(id); if (media == null) { @@ -39,23 +59,23 @@ namespace zero.Core.Api /// - public async Task> GetById(IEnumerable ids) + public async Task> GetById(IEnumerable ids) { using (IAsyncDocumentSession session = Raven.OpenAsyncSession()) { - return await session.LoadAsync(ids); + return await session.LoadAsync(ids); } } /// - public async Task> GetByQuery(MediaListQuery query) + public async Task> GetByQuery(MediaListQuery query) { query.SearchFor(entity => entity.Name); using (IAsyncDocumentSession session = Raven.OpenAsyncSession()) { - return await session.Query() + return await session.Query() .Scope(Scope) .WhereIf(x => x.FolderId == query.FolderId, !query.FolderId.IsNullOrEmpty(), x => x.FolderId == null) .ToQueriedListAsync(query); @@ -64,7 +84,27 @@ namespace zero.Core.Api /// - public async Task> Save(Media model) + public async Task> GetListByQuery(MediaListItemQuery query) + { + query.SearchFor(entity => entity.Name); + + query.OrderQuery = q => q + .OrderByDescending(x => x.IsFolder) + .ThenBy(query.OrderBy, query.OrderIsDescending, query.OrderType == ListQueryOrderType.String ? OrderingType.String : OrderingType.Double); + + using (IAsyncDocumentSession session = Raven.OpenAsyncSession()) + { + return await session.Query() + .ProjectInto() + .Scope(Scope) + .WhereIf(x => x.ParentId == query.FolderId, !query.FolderId.IsNullOrEmpty(), x => x.ParentId == null) + .ToQueriedListAsync(query); + } + } + + + /// + public async Task> Save(IMedia model) { model.IsActive = true; return await SaveModel(model); @@ -72,9 +112,9 @@ namespace zero.Core.Api /// - public async Task> Delete(string id) + public async Task> Delete(string id) { - return await DeleteById(id); + return await DeleteById(id); } @@ -83,6 +123,100 @@ namespace zero.Core.Api { await Task.Delay(0); } + + + /// + public async Task Upload(IFormFile file, string folderId, CancellationToken cancellationToken = default) + { + Media media = new Media(); + + // generate file id which is used as the folder name on disk + media.FileId = Guid.NewGuid().ToString(); + media.FolderId = folderId; + + // generate file name + media.Name = Safenames.File(file.FileName); + + // build folder and full file path + string folderPath = Path.Combine(Paths.Media, media.FileId); + string filePath = Path.Combine(folderPath, media.Name); + string extension = Path.GetExtension(filePath); + + // find media type + media.Type = ImageExtensions.Contains(extension, StringComparer.InvariantCultureIgnoreCase) ? MediaType.Image : MediaType.File; + + Paths.Create(folderPath); + + // write media file to disk + using (var stream = File.Create(filePath)) + { + await file.CopyToAsync(stream, cancellationToken); + } + + // set new properties + media.Source = Path.Combine(PATH_PREFIX, media.FileId, media.Name).Replace(Path.DirectorySeparatorChar, PATH_SEPARATOR); + media.Size = file.Length; + + // write additional image data + thumbnail + if (media.Type == MediaType.Image) + { + using (Image image = Image.Load(filePath)) + { + media.ImageMeta = GetImageMeta(image); + + Image imageFrame = media.ImageMeta.Frames > 1 ? image.Frames.CloneFrame(0) : image; + + media.PreviewSource = 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 + }); + } + } + + return media; + } + + + /// + /// Saves a thumbnail of an image + /// + string SaveThumbnail(IMedia media, Image image, string extensionPrefix, ResizeOptions resizeOptions) + { + string extension = Path.GetExtension(media.Source); + + image.Mutate(x => x.Resize(resizeOptions)); + + string thumbFileName = media.Name.TrimEnd(extension) + extensionPrefix + extension; + image.Save(Path.Combine(Paths.Media, media.FileId, thumbFileName)); + return Path.Combine(PATH_PREFIX, media.FileId, thumbFileName).Replace(Path.DirectorySeparatorChar, PATH_SEPARATOR); + } + + + /// + /// Create image data if available + /// + MediaImageMeta GetImageMeta(Image image) + { + var pngMetadata = image.Metadata.GetPngMetadata(); + + return new MediaImageMeta() + { + Width = image.Width, + Height = image.Height, + CreatedDate = 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 + }; + } } @@ -91,7 +225,7 @@ namespace zero.Core.Api /// /// Get media by Id /// - Task GetById(string id); + Task GetById(string id); /// /// Get media source by Id @@ -101,26 +235,36 @@ namespace zero.Core.Api /// /// Get media by ids /// - Task> GetById(IEnumerable ids); + Task> GetById(IEnumerable ids); /// - /// Get all available media items (with query) + /// Get all available media items with query /// - Task> GetByQuery(MediaListQuery query); + Task> GetByQuery(MediaListQuery query); + + /// + /// Get all available media items (including folders) with query + /// + Task> GetListByQuery(MediaListItemQuery query); /// /// Creates or updates a media item /// - Task> Save(Media model); + Task> Save(IMedia model); /// /// Deletes a media item by Id /// - Task> Delete(string id); + Task> Delete(string id); /// /// Clean-up all media based on stored database information /// Task Cleanup(); + + /// + /// Uploads a file to the media folder + /// + Task Upload(IFormFile file, string folderId, CancellationToken cancellationToken = default); } } diff --git a/zero.Core/Api/MediaFolderApi.cs b/zero.Core/Api/MediaFolderApi.cs index 57346ff0..f71dff89 100644 --- a/zero.Core/Api/MediaFolderApi.cs +++ b/zero.Core/Api/MediaFolderApi.cs @@ -17,18 +17,18 @@ namespace zero.Core.Api /// - public async Task GetById(string id) + public async Task GetById(string id) { - return await GetById(id); + return await GetById(id); } /// - public async Task> GetAll(string parentId = null) + public async Task> GetAll(string parentId = null) { using (IAsyncDocumentSession session = Raven.OpenAsyncSession()) { - return await session.Query() + return await session.Query() .Scope(Scope) .WhereIf(x => x.ParentId == parentId, !parentId.IsNullOrEmpty(), x => x.ParentId == null) .OrderByDescending(x => x.Name) @@ -45,7 +45,7 @@ namespace zero.Core.Api using (IAsyncDocumentSession session = Raven.OpenAsyncSession()) { - IList folders = await session.Query() + IList folders = await session.Query() .Scope(Scope) .WhereIf(x => x.ParentId == parentId, !parentId.IsNullOrEmpty(), x => x.ParentId == null) .OrderByDescending(x => x.Name) @@ -57,7 +57,7 @@ namespace zero.Core.Api { MediaFolder_ByHierarchy.Result result = await session.Query() .ProjectInto() - .Include(x => x.Path.Select(p => p.Id)) + .Include(x => x.Path.Select(p => p.Id)) .Scope(Scope) .FirstOrDefaultAsync(x => x.Id == activeId); @@ -78,7 +78,7 @@ namespace zero.Core.Api .ToListAsync(); - foreach (MediaFolder folder in folders) + foreach (IMediaFolder folder in folders) { int childCount = children.Count(x => x.Id == folder.Id); @@ -121,28 +121,28 @@ namespace zero.Core.Api /// - public async Task> GetHierarchy(string id) + public async Task> GetHierarchy(string id) { using (IAsyncDocumentSession session = Raven.OpenAsyncSession()) { MediaFolder_ByHierarchy.Result result = await session.Query() .ProjectInto() - .Include(x => x.Path.Select(p => p.Id)) + .Include(x => x.Path.Select(p => p.Id)) .Scope(Scope) .FirstOrDefaultAsync(x => x.Id == id); if (result == null) { - return new List(); + return new List(); } - return (await session.LoadAsync(result.Path.Select(x => x.Id))).Select(x => x.Value).ToList(); + return (await session.LoadAsync(result.Path.Select(x => x.Id))).Select(x => x.Value).ToList(); } } /// - public async Task> Save(MediaFolder model) + public async Task> Save(IMediaFolder model) { model.IsActive = true; return await SaveModel(model, new MediaFolderValidator()); @@ -150,9 +150,9 @@ namespace zero.Core.Api /// - public async Task> Delete(string id) + public async Task> Delete(string id) { - return await DeleteById(id); + return await DeleteById(id); } } @@ -162,17 +162,17 @@ namespace zero.Core.Api /// /// Get application by Id /// - Task GetById(string id); + Task GetById(string id); /// /// Get hierarchy for a folder /// - Task> GetHierarchy(string id); + Task> GetHierarchy(string id); /// /// Get all folders with the specified parent or on root /// - Task> GetAll(string parentId = null); + Task> GetAll(string parentId = null); /// /// Get all folders with the specified parent or on root for tree output @@ -182,11 +182,11 @@ namespace zero.Core.Api /// /// Creates or updates a folder /// - Task> Save(MediaFolder model); + Task> Save(IMediaFolder model); /// /// Deletes a folder /// - Task> Delete(string id); + Task> Delete(string id); } } diff --git a/zero.Core/Api/MediaUploadApi.cs b/zero.Core/Api/MediaUploadApi.cs deleted file mode 100644 index fb9584d0..00000000 --- a/zero.Core/Api/MediaUploadApi.cs +++ /dev/null @@ -1,143 +0,0 @@ -using Microsoft.AspNetCore.Http; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; -using System; -using System.IO; -using System.Linq; -using System.Net.Http.Headers; -using System.Threading; -using System.Threading.Tasks; -using zero.Core.Entities; -using zero.Core.Extensions; - -namespace zero.Core.Api -{ - public class MediaUploadApi : IMediaUploadApi - { - protected IPaths Paths { get; set; } - - private const char PATH_SEPARATOR = '/'; - - private const string PATH_PREFIX = "/uploads"; - - private const char SEP = ','; - - private const string THUMB_EXTENSION = ".thumb"; - - private const string PREVIEW_EXTENSION = ".preview"; - - private const string UPLOAD_PREFIX = "upload:"; - - private string[] ImageExtensions = new[] { ".jpg", ".jpeg", ".png", ".bmp", ".webp", ".jfif", ".gif" }; - - - public MediaUploadApi(IPaths paths) - { - Paths = paths; - } - - - /// - public async Task Upload(IFormFile file, string folderId, CancellationToken cancellationToken = default) - { - Media media = new Media(); - - // generate file id which is used as the folder name on disk - media.FileId = Guid.NewGuid().ToString(); - media.FolderId = folderId; - - // generate file name - media.Name = Safenames.File(file.FileName); - - // build folder and full file path - string folderPath = Path.Combine(Paths.Media, media.FileId); - string filePath = Path.Combine(folderPath, media.Name); - string extension = Path.GetExtension(filePath); - - // find media type - media.Type = ImageExtensions.Contains(extension, StringComparer.InvariantCultureIgnoreCase) ? MediaType.Image : MediaType.File; - - Paths.Create(folderPath); - - // write media file to disk - using (var stream = File.Create(filePath)) - { - await file.CopyToAsync(stream, cancellationToken); - } - - // set new properties - media.Source = Path.Combine(PATH_PREFIX, media.FileId, media.Name).Replace(Path.DirectorySeparatorChar, PATH_SEPARATOR); - media.Size = file.Length; - - // write additional image data + thumbnail - if (media.Type == MediaType.Image) - { - using (Image image = Image.Load(filePath)) - { - media.ImageMeta = GetImageMeta(image); - - Image imageFrame = media.ImageMeta.Frames > 1 ? image.Frames.CloneFrame(0) : image; - - media.PreviewSource = 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 - }); - } - } - - return media; - } - - - /// - /// Saves a thumbnail of an image - /// - string SaveThumbnail(IMedia media, Image image, string extensionPrefix, ResizeOptions resizeOptions) - { - string extension = Path.GetExtension(media.Source); - - image.Mutate(x => x.Resize(resizeOptions)); - - string thumbFileName = media.Name.TrimEnd(extension) + extensionPrefix + extension; - image.Save(Path.Combine(Paths.Media, media.FileId, thumbFileName)); - return Path.Combine(PATH_PREFIX, media.FileId, thumbFileName).Replace(Path.DirectorySeparatorChar, PATH_SEPARATOR); - } - - - /// - /// Create image data if available - /// - MediaImageMeta GetImageMeta(Image image) - { - var pngMetadata = image.Metadata.GetPngMetadata(); - - return new MediaImageMeta() - { - Width = image.Width, - Height = image.Height, - CreatedDate = 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 - }; - } - } - - - public interface IMediaUploadApi - { - /// - /// Uploads a file to the media folder - /// - Task Upload(IFormFile file, string folderId, CancellationToken cancellationToken = default); - } -} diff --git a/zero.Core/Database/Indexes/Media_ByParent.cs b/zero.Core/Database/Indexes/Media_ByParent.cs new file mode 100644 index 00000000..8524ff5e --- /dev/null +++ b/zero.Core/Database/Indexes/Media_ByParent.cs @@ -0,0 +1,41 @@ +using Raven.Client.Documents.Indexes; +using System; +using System.Linq; +using zero.Core.Entities; + +namespace zero.Core.Database.Indexes +{ + public class Media_ByParent : AbstractMultiMapIndexCreationTask + { + public Media_ByParent() + { + AddMap(items => items.Select(item => new MediaListItem + { + Id = item.Id, + AppId = item.AppId, + ParentId = item.ParentId, + CreatedDate = item.CreatedDate, + IsFolder = true, + Name = item.Name, + Image = null, + Size = 0 + })); + + AddMap(items => items.Select(item => new MediaListItem + { + Id = item.Id, + AppId = item.AppId, + ParentId = item.FolderId, + CreatedDate = item.CreatedDate, + IsFolder = false, + Name = item.Name, + Image = item.PreviewSource, + Size = item.Size + })); + + StoreAllFields(FieldStorage.Yes); + Index(x => x.AppId, FieldIndexing.Exact); + Index(x => x.ParentId, FieldIndexing.Exact); + } + } +} diff --git a/zero.Core/Entities/Media/Media.cs b/zero.Core/Entities/Media/Media.cs index 92108915..2f6aaaf2 100644 --- a/zero.Core/Entities/Media/Media.cs +++ b/zero.Core/Entities/Media/Media.cs @@ -3,9 +3,7 @@ using zero.Core.Attributes; namespace zero.Core.Entities { - /// - /// A media file (can contain an image or other media like videos and documents) - /// + /// public class Media : ZeroEntity, IMedia { /// @@ -49,6 +47,9 @@ namespace zero.Core.Entities } + /// + /// A media file (can contain an image or other media like videos and documents) + /// [Collection("Media")] public interface IMedia : IZeroEntity, IAppAwareEntity, IZeroDbConventions { diff --git a/zero.Core/Entities/Media/MediaFolder.cs b/zero.Core/Entities/Media/MediaFolder.cs index d30c9b51..fb8c2678 100644 --- a/zero.Core/Entities/Media/MediaFolder.cs +++ b/zero.Core/Entities/Media/MediaFolder.cs @@ -1,16 +1,27 @@ -namespace zero.Core.Entities +using zero.Core.Attributes; + +namespace zero.Core.Entities { - /// - /// A media folder contains media and other folders - /// - public class MediaFolder : ZeroEntity, IAppAwareEntity, IZeroDbConventions + /// + public class MediaFolder : ZeroEntity, IMediaFolder { /// public string AppId { get; set; } + /// + public string ParentId { get; set; } + } + + + /// + /// A media folder contains media and other folders + /// + [Collection("MediaFolders")] + public interface IMediaFolder : IZeroEntity, IAppAwareEntity, IZeroDbConventions + { /// /// Parent folder id /// - public string ParentId { get; set; } + string ParentId { get; set; } } } \ No newline at end of file diff --git a/zero.Core/Entities/Media/MediaListItem.cs b/zero.Core/Entities/Media/MediaListItem.cs new file mode 100644 index 00000000..3c7c1ce6 --- /dev/null +++ b/zero.Core/Entities/Media/MediaListItem.cs @@ -0,0 +1,25 @@ +using System; +using zero.Core.Attributes; + +namespace zero.Core.Entities +{ + /// + public class MediaListItem : IZeroIdEntity, IAppAwareEntity, IZeroDbConventions + { + public string Id { get; set; } + + public string ParentId { get; set; } + + public string AppId { get; set; } + + public string Name { get; set; } + + public DateTimeOffset CreatedDate { get; set; } + + public bool IsFolder { get; set; } + + public string Image { get; set; } + + public long Size { get; set; } + } +} diff --git a/zero.Core/Entities/Media/MediaListQuery.cs b/zero.Core/Entities/Media/MediaListQuery.cs index cc842ece..a1da7aa5 100644 --- a/zero.Core/Entities/Media/MediaListQuery.cs +++ b/zero.Core/Entities/Media/MediaListQuery.cs @@ -1,6 +1,12 @@ namespace zero.Core.Entities { - public class MediaListQuery : ListQuery + public class MediaListQuery : ListQuery + { + public string FolderId { get; set; } + } + + + public class MediaListItemQuery : ListQuery { public string FolderId { get; set; } } diff --git a/zero.Core/Validation/MediaFolderValidator.cs b/zero.Core/Validation/MediaFolderValidator.cs index a9528b86..3d0bedac 100644 --- a/zero.Core/Validation/MediaFolderValidator.cs +++ b/zero.Core/Validation/MediaFolderValidator.cs @@ -3,7 +3,7 @@ using zero.Core.Entities; namespace zero.Core.Validation { - public class MediaFolderValidator : AbstractValidator + public class MediaFolderValidator : AbstractValidator { public MediaFolderValidator() { diff --git a/zero.Web.UI/App/components/datagrid/datagrid.vue b/zero.Web.UI/App/components/datagrid/datagrid.vue index a11294a4..e404a647 100644 --- a/zero.Web.UI/App/components/datagrid/datagrid.vue +++ b/zero.Web.UI/App/components/datagrid/datagrid.vue @@ -1,7 +1,7 @@