From d81992d2cd4fa674082eb475f783be53ee7660b7 Mon Sep 17 00:00:00 2001 From: Tobias Klika Date: Thu, 25 Nov 2021 16:01:38 +0100 Subject: [PATCH] file uploads for media management (not persisted yet) --- .../Modules/Media/MediaTreeService.cs | 250 ++++++++++++++++ .../Modules/Pages/PageTreeService.cs | 1 + zero.Core/Media/MediaCollection.cs | 275 ------------------ zero.Core/Media/MediaCreator.cs | 8 +- zero.Core/Media/MediaFolderCollection.cs | 171 ----------- zero.Core/Media/MediaManagement.cs | 48 ++- zero.Core/Media/MediaManagementExtensions.cs | 29 +- zero.Core/Media/MediaStore.cs | 17 +- zero.Core/Media/Models/Media.cs | 5 + zero.Core/Media/Models/MediaFolder.cs | 13 - zero.Core/Media/Models/MediaListQuery.cs | 14 - .../Media/ServiceCollectionExtensions.cs | 8 +- 12 files changed, 352 insertions(+), 487 deletions(-) create mode 100644 zero.Backoffice/Modules/Media/MediaTreeService.cs delete mode 100644 zero.Core/Media/MediaCollection.cs delete mode 100644 zero.Core/Media/MediaFolderCollection.cs delete mode 100644 zero.Core/Media/Models/MediaFolder.cs delete mode 100644 zero.Core/Media/Models/MediaListQuery.cs diff --git a/zero.Backoffice/Modules/Media/MediaTreeService.cs b/zero.Backoffice/Modules/Media/MediaTreeService.cs new file mode 100644 index 00000000..23a239de --- /dev/null +++ b/zero.Backoffice/Modules/Media/MediaTreeService.cs @@ -0,0 +1,250 @@ +using Raven.Client.Documents; +using Raven.Client.Documents.Linq; +using Raven.Client.Documents.Session; + +namespace zero.Backoffice.Modules; + +public class MediaTreeService : IMediaTreeService +{ + protected IMediaStore Media { get; private set; } + + protected PageOptions PageOptions { get; set; } + + + public PageTreeService(IPagesStore pages, IZeroOptions options, IRoutes routes) + { + Media = pages; + Routes = routes; + PageOptions = options.For(); + } + + + /// + /// FOR MEDIA + public virtual async Task> Load(MediaListItemQuery query) + { + bool hasSearch = !query.Search.IsNullOrWhiteSpace(); + bool isRoot = query.FolderId.IsNullOrWhiteSpace(); + + 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); + + IRavenQueryable dbQuery = Session.Query().ProjectInto(); + + if (!hasSearch || !query.SearchIsGlobal) + { + dbQuery = dbQuery.WhereIf(x => x.ParentId == query.FolderId, !query.FolderId.IsNullOrEmpty(), x => x.ParentId == null); + } + + Paged result = await dbQuery.ToQueriedListAsyncX(query); + + string[] ids = result.Items.Where(x => x.IsFolder).Select(x => x.Id).ToArray(); + + List children = await Session.Query() + .Where(x => x.ParentId.In(ids)) + .ToListAsync(); + + foreach (MediaListItem item in result.Items) + { + item.Children = children.FirstOrDefault(x => x.ParentId == item.Id)?.ChildrenCount ?? 0; + } + + return result; + } + + + /// + /// FOR MEDIA FOLDER + public async Task> LoadAsTree(string parentId = null, string activeId = null) + { + List items = new(); + string[] openIds = Array.Empty(); + + IList folders = await Session.Query() + .WhereIf(x => x.ParentId == parentId, !parentId.IsNullOrEmpty(), x => x.ParentId == null) + .OrderByDescending(x => x.CreatedDate).ThenBy(x => x.Name) + .ToListAsync(); + + + // get hierarchy so we know if we should set the folder to open + if (!activeId.IsNullOrEmpty()) + { + MediaFolder_ByHierarchy.Result result = await Session.Query() + .ProjectInto() + .Include(x => x.Path.Select(p => p.Id)) + .FirstOrDefaultAsync(x => x.Id == activeId); + + if (result != null) + { + openIds = result.Path.Select(x => x.Id).ToArray(); + } + } + + + // get children for all folders + string[] folderIds = folders.Select(x => x.Id).ToArray(); + + IList children = await Session.Query() + .ProjectInto() + .Where(x => x.Id.In(folderIds)) + .ToListAsync(); + + + foreach (MediaFolder folder in folders) + { + int childCount = children.Count(x => x.Id == folder.Id); + + items.Add(new TreeItem() + { + Id = folder.Id, + Name = folder.Name, + HasChildren = childCount > 0, + ChildCount = childCount, + ParentId = folder.ParentId, + Sort = folder.Sort, + Icon = "fth-folder", + IsOpen = openIds.Contains(folder.Id), + IsInactive = !folder.IsActive, + HasActions = true, + Modifier = !folder.IsActive ? new TreeItemModifier() + { + Icon = "fth-minus-circle color-yellow", + Name = "Inactive" + } : null + }); + } + + return items; + } + + + /// + public async Task> GetChildren(string parentId = null, string activeId = null, string search = null) + { + IList items = new List(); + IReadOnlyCollection pageTypes = PageOptions.GetAllItems(); + string[] openIds = new string[0] { }; + Paged pages = null; + IList children = null; + bool isSearch = !search.IsNullOrWhiteSpace(); + + if (isSearch) + { + pages = await Media.Load(1, Int32.MaxValue, q => q.SearchIf(x => x.Name, search, "*").OrderBy(x => x.Sort, OrderingType.Long)); + + var urls = await Routes.GetUrls(pages.Items.ToArray()); + + foreach (Page page in pages.Items) + { + if (urls.TryGetValue(page, out string url)) + { + page.Url = url; + } + } + } + else + { + pages = await Media.Load(1, Int32.MaxValue, q => q + .WhereIf(x => x.ParentId == parentId, !parentId.IsNullOrEmpty(), x => x.ParentId == null) + .OrderBy(x => x.Sort, OrderingType.Long)); + + + // get hierarchy so we know if we should set the page to open + if (!activeId.IsNullOrEmpty()) + { + Pages_ByHierarchy.Result result = await Media.Session.Query() + .ProjectInto() + .Include(x => x.Path.Select(p => p.Id)) + .FirstOrDefaultAsync(x => x.Id == activeId); + + if (result != null) + { + openIds = result.Path.Select(x => x.Id).ToArray(); // .Union(new string[1] { activeId }) + } + } + + + // get children for all pages + string[] pageIds = pages.Items.Select(x => x.Id).ToArray(); + + children = await Media.Session.Query() + .ProjectInto() + .Where(x => x.Id.In(pageIds)) + .ToListAsync(); + } + + + // function to get modifier icon + TreeItemModifier GetModifier(Page page) + { + if (page.PublishDate > DateTimeOffset.Now || page.UnpublishDate > DateTimeOffset.Now) + { + return new TreeItemModifier("@page.schedule.scheduled", "fth-clock"); + } + if (!page.IsActive) + { + return new TreeItemModifier("@ui.inactive", "fth-minus-circle color-red"); + } + return null; + } + + + // build tree + foreach (Page page in pages.Items) + { + PageType pageType = pageTypes.FirstOrDefault(x => x.Alias == page.PageTypeAlias); + + if (pageType == null) + { + continue; + // TODO the page type does not exist anymore + } + + int childCount = isSearch ? 0 : children.Count(x => x.Id == page.Id); + + items.Add(new TreeItem() + { + Id = page.Id, + Name = page.Name, + HasChildren = childCount > 0, + ChildCount = childCount, + ParentId = page.ParentId, + Sort = page.Sort, + Icon = pageType.Icon, + IsOpen = openIds.Contains(page.Id), + IsInactive = !page.IsActive, + HasActions = true, + Modifier = GetModifier(page), + Description = isSearch ? page.Url : null + }); + } + + if (parentId.IsNullOrEmpty()) + { + items.Add(new TreeItem() + { + Id = "recyclebin", + ParentId = null, + Sort = 99999, + Name = "@recyclebin.name", + Icon = "fth-trash", + HasChildren = false, + HasActions = true + }); + } + + return items; + } +} + + +public interface IMediaTreeService +{ + /// + /// Get media children as tree items + /// + Task> GetChildren(string parentId = null, string activeId = null, string search = null); +} \ No newline at end of file diff --git a/zero.Backoffice/Modules/Pages/PageTreeService.cs b/zero.Backoffice/Modules/Pages/PageTreeService.cs index 51f4085e..46fd3899 100644 --- a/zero.Backoffice/Modules/Pages/PageTreeService.cs +++ b/zero.Backoffice/Modules/Pages/PageTreeService.cs @@ -18,6 +18,7 @@ public class PageTreeService : IPageTreeService Pages = pages; Routes = routes; PageOptions = options.For(); + IMediaManagement media; } diff --git a/zero.Core/Media/MediaCollection.cs b/zero.Core/Media/MediaCollection.cs deleted file mode 100644 index df247156..00000000 --- a/zero.Core/Media/MediaCollection.cs +++ /dev/null @@ -1,275 +0,0 @@ -using FluentValidation; -using Microsoft.AspNetCore.Http; -using Raven.Client.Documents; -using Raven.Client.Documents.Linq; -using Raven.Client.Documents.Session; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using zero.Core.Database.Indexes; - -namespace zero.Core.Collections -{ - public class MediaCollection : EntityStore, IMediaCollection - { - /// - public IMediaFolderCollection Folders { get; protected set; } - - public MediaCollection(IStoreContext context, IMediaFolderCollection folders, IPaths paths) : base(context) - { - Options = new(true); - Folders = folders; - //PreSave = model => model.IsActive = true; - Paths = paths; - } - - - 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 override Task> Save(Media model) - { - model.IsActive = true; - return base.Save(model); - } - - - /// - public virtual async Task GetSource(string id, MediaSourceSize size = MediaSourceSize.Original) - { - Media media = await Session.LoadAsync(id); - - if (media == null) - { - return null; - } - - if (size == MediaSourceSize.Thumbnail && media.ThumbnailSource.HasValue()) - { - return media.ThumbnailSource; - } - if (size == MediaSourceSize.Preview && media.PreviewSource.HasValue()) - { - return media.PreviewSource; - } - - return media.Source; - } - - - /// - public virtual async Task> Load(MediaListQuery query) - { - query.SearchFor(entity => entity.Name); - - return await Session.Query() - .WhereIf(x => x.FolderId == query.FolderId, !query.FolderId.IsNullOrEmpty(), x => x.FolderId == null) - .ToQueriedListAsync(query); - } - - - /// - public virtual async Task> Move(string id, string parentId) - { - Media model = await Load(id); - MediaFolder parent = await Session.LoadAsync(parentId); - - if (model == null || (!parentId.IsNullOrEmpty() && parent == null)) - { - return EntityResult.Fail("@errors.idnotfound"); - } - - model.FolderId = parent?.Id; - - return await Save(model); - } - - - /// - public virtual async Task Upload(IFormFile file, string folderId, CancellationToken cancellationToken = default) - { - Media media = await Empty(); - - // 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; - } - - - /// - public virtual async Task> Load(MediaListItemQuery query) - { - bool hasSearch = !query.Search.IsNullOrWhiteSpace(); - bool isRoot = query.FolderId.IsNullOrWhiteSpace(); - - 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); - - IRavenQueryable dbQuery = Session.Query().ProjectInto(); - - if (!hasSearch || !query.SearchIsGlobal) - { - dbQuery = dbQuery.WhereIf(x => x.ParentId == query.FolderId, !query.FolderId.IsNullOrEmpty(), x => x.ParentId == null); - } - - Paged result = await dbQuery.ToQueriedListAsyncX(query); - - string[] ids = result.Items.Where(x => x.IsFolder).Select(x => x.Id).ToArray(); - - List children = await Session.Query() - .Where(x => x.ParentId.In(ids)) - .ToListAsync(); - - foreach (MediaListItem item in result.Items) - { - item.Children = children.FirstOrDefault(x => x.ParentId == item.Id)?.ChildrenCount ?? 0; - } - - return result; - } - - - /// - protected override void ValidationRules(ZeroValidator validator) - { - validator.RuleFor(x => x.Name).Length(2, 80); - validator.RuleFor(x => x.IsActive).Equal(true); - } - - - /// - /// Saves a thumbnail of an image - /// - protected virtual string SaveThumbnail(Media 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 - /// - protected virtual MediaImageMetadata GetImageMeta(Image image) - { - var 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 - }; - } - } - - - public interface IMediaCollection : IEntityStore - { - /// - /// Media folder collection - /// - IMediaFolderCollection Folders { get; } - - /// - /// Get media source by Id - /// - Task GetSource(string id, MediaSourceSize size = MediaSourceSize.Original); - - /// - /// Get all available media items with query - /// - Task> Load(MediaListQuery query); - - /// - /// Get all available media items (including folders) with query - /// - Task> Load(MediaListItemQuery query); - - /// - /// Move a file to a new parent - /// - Task> Move(string id, string parentId); - - /// - /// Uploads a file to the media folder - /// - Task Upload(IFormFile file, string folderId, CancellationToken cancellationToken = default); - } -} diff --git a/zero.Core/Media/MediaCreator.cs b/zero.Core/Media/MediaCreator.cs index 10275f0d..18a5d63d 100644 --- a/zero.Core/Media/MediaCreator.cs +++ b/zero.Core/Media/MediaCreator.cs @@ -23,7 +23,7 @@ public class MediaCreator : IMediaCreator /// - public async Task UploadFile(Stream fileStream, string filename, string folderId = null, CancellationToken cancellationToken = default) + public async Task> UploadFile(Stream fileStream, string filename, string folderId = null, CancellationToken cancellationToken = default) { string fileExtension = Path.GetExtension(filename); string normalizedFilename = Safenames.File(filename); @@ -34,7 +34,7 @@ public class MediaCreator : IMediaCreator if (!isImage && !isDocument) { // TODO error - return null; + return EntityResult.Fail("ERROR"); } Media model = await Store.Empty(); @@ -67,7 +67,7 @@ public class MediaCreator : IMediaCreator // TODO save thumbnails } - return model; + return EntityResult.Success(model); } @@ -162,5 +162,5 @@ 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); + Task> UploadFile(Stream fileStream, string filename, string folderId = null, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/zero.Core/Media/MediaFolderCollection.cs b/zero.Core/Media/MediaFolderCollection.cs deleted file mode 100644 index a2d61132..00000000 --- a/zero.Core/Media/MediaFolderCollection.cs +++ /dev/null @@ -1,171 +0,0 @@ -using FluentValidation; -using Raven.Client.Documents; -using Raven.Client.Documents.Linq; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using zero.Core.Database.Indexes; - -namespace zero.Core.Collections -{ - public class MediaFolderCollection : EntityStore, IMediaFolderCollection - { - public MediaFolderCollection(IStoreContext context) : base(context) - { - Options = new(true); - } - - - /// - public override Task> Save(MediaFolder model) - { - model.IsActive = true; - return base.Save(model); - } - - - /// - public async Task> LoadByParent(string parentId = null) - { - return await Session.Query() - .WhereIf(x => x.ParentId == parentId, !parentId.IsNullOrEmpty(), x => x.ParentId == null) - .OrderByDescending(x => x.Name) - .ToListAsync(); - } - - - /// - public async Task> Move(string id, string parentId) - { - MediaFolder model = await Load(id); - MediaFolder parent = await Load(parentId); - - if (model == null || (!parentId.IsNullOrEmpty() && parent == null)) - { - return EntityResult.Fail("@errors.idnotfound"); - } - - model.ParentId = parent?.Id; - - return await Save(model); - } - - - /// - public async Task> LoadAsTree(string parentId = null, string activeId = null) - { - List items = new(); - string[] openIds = Array.Empty(); - - IList folders = await Session.Query() - .WhereIf(x => x.ParentId == parentId, !parentId.IsNullOrEmpty(), x => x.ParentId == null) - .OrderByDescending(x => x.CreatedDate).ThenBy(x => x.Name) - .ToListAsync(); - - - // get hierarchy so we know if we should set the folder to open - if (!activeId.IsNullOrEmpty()) - { - MediaFolder_ByHierarchy.Result result = await Session.Query() - .ProjectInto() - .Include(x => x.Path.Select(p => p.Id)) - .FirstOrDefaultAsync(x => x.Id == activeId); - - if (result != null) - { - openIds = result.Path.Select(x => x.Id).ToArray(); - } - } - - - // get children for all folders - string[] folderIds = folders.Select(x => x.Id).ToArray(); - - IList children = await Session.Query() - .ProjectInto() - .Where(x => x.Id.In(folderIds)) - .ToListAsync(); - - - foreach (MediaFolder folder in folders) - { - int childCount = children.Count(x => x.Id == folder.Id); - - items.Add(new TreeItem() - { - Id = folder.Id, - Name = folder.Name, - HasChildren = childCount > 0, - ChildCount = childCount, - ParentId = folder.ParentId, - Sort = folder.Sort, - Icon = "fth-folder", - IsOpen = openIds.Contains(folder.Id), - IsInactive = !folder.IsActive, - HasActions = true, - Modifier = !folder.IsActive ? new TreeItemModifier() - { - Icon = "fth-minus-circle color-yellow", - Name = "Inactive" - } : null - }); - } - - return items; - } - - - /// - public async Task> GetHierarchy(string id) - { - MediaFolder_ByHierarchy.Result result = await Session.Query() - .ProjectInto() - .Include(x => x.Path.Select(p => p.Id)) - .FirstOrDefaultAsync(x => x.Id == id); - - if (result == null) - { - return new List(); - } - - List ids = result.Path.Select(x => x.Id).ToList(); - ids.Add(id); - - return (await Session.LoadAsync(ids)).Select(x => x.Value).ToList(); - } - - - /// - protected override void ValidationRules(ZeroValidator validator) - { - validator.RuleFor(x => x.Name).Length(2, 80); - validator.RuleFor(x => x.IsActive).Equal(true); - validator.RuleFor(x => x.ParentId).Exists(Context.Store); - } - } - - - public interface IMediaFolderCollection : IEntityStore - { - /// - /// Get hierarchy for a folder - /// - Task> GetHierarchy(string id); - - /// - /// Get all folders with the specified parent or on root - /// - Task> LoadByParent(string parentId = null); - - /// - /// Get all folders with the specified parent or on root for tree output - /// - Task> LoadAsTree(string parentId = null, string activeId = null); - - /// - /// Move a folder to a new parent - /// - Task> Move(string id, string parentId); - } -} diff --git a/zero.Core/Media/MediaManagement.cs b/zero.Core/Media/MediaManagement.cs index ec548c20..0633dda8 100644 --- a/zero.Core/Media/MediaManagement.cs +++ b/zero.Core/Media/MediaManagement.cs @@ -1,4 +1,6 @@ -namespace zero.Media; +using System.IO; + +namespace zero.Media; public class MediaManagement : IMediaManagement { @@ -6,11 +8,34 @@ public class MediaManagement : IMediaManagement protected IMediaStore Store { get; set; } + protected IMediaCreator Creator { get; set; } - public MediaManagement(IMediaFileSystem fileSystem, IMediaStore store) + + public MediaManagement(IMediaFileSystem fileSystem, IMediaStore store, IMediaCreator creator) { FileSystem = fileSystem; Store = store; + Creator = creator; + } + + + + /// + public virtual string GetPublicFilePath(Media file, string thumbnailKey = null) + { + string path = file?.Path; + + if (!thumbnailKey.IsNullOrWhiteSpace()) + { + path = file?.Thumbnails?.GetValueOrDefault(thumbnailKey); + } + + if (path.IsNullOrEmpty()) + { + return null; + } + + return FileSystem.MapToPublicPath(path); } @@ -38,6 +63,13 @@ public class MediaManagement : IMediaManagement } + /// + public virtual async Task> UploadFile(Stream fileStream, string filename, string folderId = null, CancellationToken cancellationToken = default) + { + return await Creator.UploadFile(fileStream, filename, folderId, cancellationToken); + } + + /// public virtual async Task GetFolder(string id) { @@ -83,6 +115,18 @@ public class MediaManagement : IMediaManagement public interface IMediaManagement { + /// + /// Get publicly accessible file path for a media file + /// + /// The media file + /// An optional thumbnail key which returns the path to a generated thumbnail + string GetPublicFilePath(Media file, string thumbnailKey = null); + + /// + /// Uploads a file and persists it + /// + Task> UploadFile(Stream fileStream, string filename, string folderId = null, CancellationToken cancellationToken = default); + /// /// Get a media folder by id /// diff --git a/zero.Core/Media/MediaManagementExtensions.cs b/zero.Core/Media/MediaManagementExtensions.cs index 40853b56..1f235461 100644 --- a/zero.Core/Media/MediaManagementExtensions.cs +++ b/zero.Core/Media/MediaManagementExtensions.cs @@ -1,7 +1,30 @@ -namespace zero.Media +using Microsoft.AspNetCore.Http; +using System.IO; + +namespace zero.Media { public static class MediaManagementExtensions { + /// + /// Uploads a file and persists it + /// + public static async Task> UploadFile(this IMediaManagement media, IFormFile formFile, string folderId = null, CancellationToken cancellationToken = default) + { + using Stream stream = formFile.OpenReadStream(); + return await media.UploadFile(stream, formFile.FileName, folderId, cancellationToken); + } + + + /// + /// Uploads a file and persists it + /// + public static async Task> UploadFile(this IMediaManagement media, byte[] fileBytes, string filename, string folderId = null, CancellationToken cancellationToken = default) + { + using Stream stream = new MemoryStream(fileBytes); + return await media.UploadFile(stream, filename, folderId, cancellationToken); + } + + /// /// Rename and store a media folder /// @@ -55,12 +78,12 @@ /// /// Deletes a folder by id /// - public static async Task> DeleteFolder(this IMediaManagement media, string folderId) + 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 EntityResult.Fail("@errors.idnotfound"); } return await media.DeleteFolder(folder); diff --git a/zero.Core/Media/MediaStore.cs b/zero.Core/Media/MediaStore.cs index 3fbb88a8..51ff5b6c 100644 --- a/zero.Core/Media/MediaStore.cs +++ b/zero.Core/Media/MediaStore.cs @@ -1,4 +1,6 @@ -namespace zero.Media; +using FluentValidation; + +namespace zero.Media; public class MediaStore : TreeEntityStore, IMediaStore { @@ -22,6 +24,19 @@ public class MediaStore : TreeEntityStore, IMediaStore Media parent = await Load(parentId); return parent != null && parent.Type == MediaType.Folder; } + + + /// + protected override void ValidationRules(ZeroValidator validator) + { + validator.RuleFor(x => x.Name).Length(2, 120); + validator.RuleFor(x => x.IsActive).Equal(true); + + validator.When(x => x.Type == MediaType.Folder, () => + { + validator.RuleFor(x => x.ParentId).Exists(Context.Store); + }); + } } diff --git a/zero.Core/Media/Models/Media.cs b/zero.Core/Media/Models/Media.cs index f5454586..fb38417f 100644 --- a/zero.Core/Media/Models/Media.cs +++ b/zero.Core/Media/Models/Media.cs @@ -6,6 +6,11 @@ [RavenCollection("Media")] public class Media : ZeroEntity, IZeroTreeEntity { + public Media() + { + IsActive = true; + } + /// /// Id/name of the phyiscal folder which is stored on disk/cloud /// diff --git a/zero.Core/Media/Models/MediaFolder.cs b/zero.Core/Media/Models/MediaFolder.cs deleted file mode 100644 index 79f18e4f..00000000 --- a/zero.Core/Media/Models/MediaFolder.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace zero.Media; - -/// -/// A media folder contains media and other folders -/// -[RavenCollection("MediaFolders")] -public class MediaFolder : ZeroEntity, IZeroTreeEntity -{ - /// - /// Parent folder id - /// - public string ParentId { get; set; } -} \ No newline at end of file diff --git a/zero.Core/Media/Models/MediaListQuery.cs b/zero.Core/Media/Models/MediaListQuery.cs deleted file mode 100644 index 9dea5458..00000000 --- a/zero.Core/Media/Models/MediaListQuery.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace zero.Media; - -public class MediaListQuery : ListQuery -{ - public string FolderId { get; set; } -} - - -public class MediaListItemQuery : ListQuery -{ - public string FolderId { get; set; } - - public bool SearchIsGlobal { get; set; } -} \ No newline at end of file diff --git a/zero.Core/Media/ServiceCollectionExtensions.cs b/zero.Core/Media/ServiceCollectionExtensions.cs index 58f8c137..d265074e 100644 --- a/zero.Core/Media/ServiceCollectionExtensions.cs +++ b/zero.Core/Media/ServiceCollectionExtensions.cs @@ -11,10 +11,6 @@ internal static class ServiceCollectionExtensions { public static IServiceCollection AddZeroMedia(this IServiceCollection services, IConfiguration config) { - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddSingleton(svc => { IOptions options = svc.GetRequiredService>(); @@ -22,6 +18,10 @@ internal static class ServiceCollectionExtensions return new(Path.Combine(env.WebRootPath, options.Value.FolderPath), options.Value.PublicPathPrefix); }); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddOptions().Bind(config.GetSection("Zero:Media")).Configure(opts => { opts.FolderPath = "uploads";