file uploads for media management (not persisted yet)
This commit is contained in:
@@ -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<PageOptions>();
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
/// FOR MEDIA
|
||||
public virtual async Task<Paged<MediaListItem>> 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<MediaListItem> dbQuery = Session.Query<MediaListItem, Media_ByParent>().ProjectInto<MediaListItem>();
|
||||
|
||||
if (!hasSearch || !query.SearchIsGlobal)
|
||||
{
|
||||
dbQuery = dbQuery.WhereIf(x => x.ParentId == query.FolderId, !query.FolderId.IsNullOrEmpty(), x => x.ParentId == null);
|
||||
}
|
||||
|
||||
Paged<MediaListItem> result = await dbQuery.ToQueriedListAsyncX(query);
|
||||
|
||||
string[] ids = result.Items.Where(x => x.IsFolder).Select(x => x.Id).ToArray();
|
||||
|
||||
List<Media_ByChildren.Result> children = await Session.Query<Media_ByChildren.Result, Media_ByChildren>()
|
||||
.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;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
/// FOR MEDIA FOLDER
|
||||
public async Task<List<TreeItem>> LoadAsTree(string parentId = null, string activeId = null)
|
||||
{
|
||||
List<TreeItem> items = new();
|
||||
string[] openIds = Array.Empty<string>();
|
||||
|
||||
IList<MediaFolder> folders = await Session.Query<MediaFolder>()
|
||||
.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<MediaFolder_ByHierarchy.Result, MediaFolder_ByHierarchy>()
|
||||
.ProjectInto<MediaFolder_ByHierarchy.Result>()
|
||||
.Include<MediaFolder_ByHierarchy.Result, MediaFolder>(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<MediaFolders_WithChildren.Result> children = await Session.Query<MediaFolders_WithChildren.Result, MediaFolders_WithChildren>()
|
||||
.ProjectInto<MediaFolders_WithChildren.Result>()
|
||||
.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;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IList<TreeItem>> GetChildren(string parentId = null, string activeId = null, string search = null)
|
||||
{
|
||||
IList<TreeItem> items = new List<TreeItem>();
|
||||
IReadOnlyCollection<PageType> pageTypes = PageOptions.GetAllItems();
|
||||
string[] openIds = new string[0] { };
|
||||
Paged<Page> pages = null;
|
||||
IList<Pages_WithChildren.Result> 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<Pages_ByHierarchy.Result, Pages_ByHierarchy>()
|
||||
.ProjectInto<Pages_ByHierarchy.Result>()
|
||||
.Include<Pages_ByHierarchy.Result, Page>(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<Pages_WithChildren.Result, Pages_WithChildren>()
|
||||
.ProjectInto<Pages_WithChildren.Result>()
|
||||
.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
|
||||
{
|
||||
/// <summary>
|
||||
/// Get media children as tree items
|
||||
/// </summary>
|
||||
Task<IList<TreeItem>> GetChildren(string parentId = null, string activeId = null, string search = null);
|
||||
}
|
||||
@@ -18,6 +18,7 @@ public class PageTreeService : IPageTreeService
|
||||
Pages = pages;
|
||||
Routes = routes;
|
||||
PageOptions = options.For<PageOptions>();
|
||||
IMediaManagement media;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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<Media>, IMediaCollection
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public IMediaFolderCollection Folders { get; protected set; }
|
||||
|
||||
public MediaCollection(IStoreContext<Media> 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; }
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<EntityResult<Media>> Save(Media model)
|
||||
{
|
||||
model.IsActive = true;
|
||||
return base.Save(model);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual async Task<string> GetSource(string id, MediaSourceSize size = MediaSourceSize.Original)
|
||||
{
|
||||
Media media = await Session.LoadAsync<Media>(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;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual async Task<Paged<Media>> Load(MediaListQuery query)
|
||||
{
|
||||
query.SearchFor(entity => entity.Name);
|
||||
|
||||
return await Session.Query<Media>()
|
||||
.WhereIf(x => x.FolderId == query.FolderId, !query.FolderId.IsNullOrEmpty(), x => x.FolderId == null)
|
||||
.ToQueriedListAsync(query);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual async Task<EntityResult<Media>> Move(string id, string parentId)
|
||||
{
|
||||
Media model = await Load(id);
|
||||
MediaFolder parent = await Session.LoadAsync<MediaFolder>(parentId);
|
||||
|
||||
if (model == null || (!parentId.IsNullOrEmpty() && parent == null))
|
||||
{
|
||||
return EntityResult<Media>.Fail("@errors.idnotfound");
|
||||
}
|
||||
|
||||
model.FolderId = parent?.Id;
|
||||
|
||||
return await Save(model);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual async Task<Media> 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<Rgba32> image = Image.Load<Rgba32>(filePath);
|
||||
|
||||
media.ImageMeta = GetImageMeta(image);
|
||||
|
||||
Image<Rgba32> 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;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual async Task<Paged<MediaListItem>> 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<MediaListItem> dbQuery = Session.Query<MediaListItem, Media_ByParent>().ProjectInto<MediaListItem>();
|
||||
|
||||
if (!hasSearch || !query.SearchIsGlobal)
|
||||
{
|
||||
dbQuery = dbQuery.WhereIf(x => x.ParentId == query.FolderId, !query.FolderId.IsNullOrEmpty(), x => x.ParentId == null);
|
||||
}
|
||||
|
||||
Paged<MediaListItem> result = await dbQuery.ToQueriedListAsyncX(query);
|
||||
|
||||
string[] ids = result.Items.Where(x => x.IsFolder).Select(x => x.Id).ToArray();
|
||||
|
||||
List<Media_ByChildren.Result> children = await Session.Query<Media_ByChildren.Result, Media_ByChildren>()
|
||||
.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;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void ValidationRules(ZeroValidator<Media> validator)
|
||||
{
|
||||
validator.RuleFor(x => x.Name).Length(2, 80);
|
||||
validator.RuleFor(x => x.IsActive).Equal(true);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Saves a thumbnail of an image
|
||||
/// </summary>
|
||||
protected virtual string SaveThumbnail(Media media, Image<Rgba32> 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);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Create image data if available
|
||||
/// </summary>
|
||||
protected virtual MediaImageMetadata GetImageMeta(Image<Rgba32> 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>
|
||||
{
|
||||
/// <summary>
|
||||
/// Media folder collection
|
||||
/// </summary>
|
||||
IMediaFolderCollection Folders { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get media source by Id
|
||||
/// </summary>
|
||||
Task<string> GetSource(string id, MediaSourceSize size = MediaSourceSize.Original);
|
||||
|
||||
/// <summary>
|
||||
/// Get all available media items with query
|
||||
/// </summary>
|
||||
Task<Paged<Media>> Load(MediaListQuery query);
|
||||
|
||||
/// <summary>
|
||||
/// Get all available media items (including folders) with query
|
||||
/// </summary>
|
||||
Task<Paged<MediaListItem>> Load(MediaListItemQuery query);
|
||||
|
||||
/// <summary>
|
||||
/// Move a file to a new parent
|
||||
/// </summary>
|
||||
Task<EntityResult<Media>> Move(string id, string parentId);
|
||||
|
||||
/// <summary>
|
||||
/// Uploads a file to the media folder
|
||||
/// </summary>
|
||||
Task<Media> Upload(IFormFile file, string folderId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ public class MediaCreator : IMediaCreator
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Media> UploadFile(Stream fileStream, string filename, string folderId = null, CancellationToken cancellationToken = default)
|
||||
public async Task<EntityResult<Media>> 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<Media>.Fail("ERROR");
|
||||
}
|
||||
|
||||
Media model = await Store.Empty();
|
||||
@@ -67,7 +67,7 @@ public class MediaCreator : IMediaCreator
|
||||
// TODO save thumbnails
|
||||
}
|
||||
|
||||
return model;
|
||||
return EntityResult<Media>.Success(model);
|
||||
}
|
||||
|
||||
|
||||
@@ -162,5 +162,5 @@ public interface IMediaCreator
|
||||
/// Uploads a file by using the attached file system
|
||||
/// </summary>
|
||||
/// <returns>A temporary media file which can be persisted in a store</returns>
|
||||
Task<Media> UploadFile(Stream fileStream, string filename, string folderId = null, CancellationToken cancellationToken = default);
|
||||
Task<EntityResult<Media>> UploadFile(Stream fileStream, string filename, string folderId = null, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -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<MediaFolder>, IMediaFolderCollection
|
||||
{
|
||||
public MediaFolderCollection(IStoreContext<MediaFolder> context) : base(context)
|
||||
{
|
||||
Options = new(true);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<EntityResult<MediaFolder>> Save(MediaFolder model)
|
||||
{
|
||||
model.IsActive = true;
|
||||
return base.Save(model);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<List<MediaFolder>> LoadByParent(string parentId = null)
|
||||
{
|
||||
return await Session.Query<MediaFolder>()
|
||||
.WhereIf(x => x.ParentId == parentId, !parentId.IsNullOrEmpty(), x => x.ParentId == null)
|
||||
.OrderByDescending(x => x.Name)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<EntityResult<MediaFolder>> Move(string id, string parentId)
|
||||
{
|
||||
MediaFolder model = await Load(id);
|
||||
MediaFolder parent = await Load(parentId);
|
||||
|
||||
if (model == null || (!parentId.IsNullOrEmpty() && parent == null))
|
||||
{
|
||||
return EntityResult<MediaFolder>.Fail("@errors.idnotfound");
|
||||
}
|
||||
|
||||
model.ParentId = parent?.Id;
|
||||
|
||||
return await Save(model);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<List<TreeItem>> LoadAsTree(string parentId = null, string activeId = null)
|
||||
{
|
||||
List<TreeItem> items = new();
|
||||
string[] openIds = Array.Empty<string>();
|
||||
|
||||
IList<MediaFolder> folders = await Session.Query<MediaFolder>()
|
||||
.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<MediaFolder_ByHierarchy.Result, MediaFolder_ByHierarchy>()
|
||||
.ProjectInto<MediaFolder_ByHierarchy.Result>()
|
||||
.Include<MediaFolder_ByHierarchy.Result, MediaFolder>(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<MediaFolders_WithChildren.Result> children = await Session.Query<MediaFolders_WithChildren.Result, MediaFolders_WithChildren>()
|
||||
.ProjectInto<MediaFolders_WithChildren.Result>()
|
||||
.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;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<List<MediaFolder>> GetHierarchy(string id)
|
||||
{
|
||||
MediaFolder_ByHierarchy.Result result = await Session.Query<MediaFolder_ByHierarchy.Result, MediaFolder_ByHierarchy>()
|
||||
.ProjectInto<MediaFolder_ByHierarchy.Result>()
|
||||
.Include<MediaFolder_ByHierarchy.Result, MediaFolder>(x => x.Path.Select(p => p.Id))
|
||||
.FirstOrDefaultAsync(x => x.Id == id);
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
return new List<MediaFolder>();
|
||||
}
|
||||
|
||||
List<string> ids = result.Path.Select(x => x.Id).ToList();
|
||||
ids.Add(id);
|
||||
|
||||
return (await Session.LoadAsync<MediaFolder>(ids)).Select(x => x.Value).ToList();
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void ValidationRules(ZeroValidator<MediaFolder> 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<MediaFolder>
|
||||
{
|
||||
/// <summary>
|
||||
/// Get hierarchy for a folder
|
||||
/// </summary>
|
||||
Task<List<MediaFolder>> GetHierarchy(string id);
|
||||
|
||||
/// <summary>
|
||||
/// Get all folders with the specified parent or on root
|
||||
/// </summary>
|
||||
Task<List<MediaFolder>> LoadByParent(string parentId = null);
|
||||
|
||||
/// <summary>
|
||||
/// Get all folders with the specified parent or on root for tree output
|
||||
/// </summary>
|
||||
Task<List<TreeItem>> LoadAsTree(string parentId = null, string activeId = null);
|
||||
|
||||
/// <summary>
|
||||
/// Move a folder to a new parent
|
||||
/// </summary>
|
||||
Task<EntityResult<MediaFolder>> Move(string id, string parentId);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual async Task<EntityResult<Media>> UploadFile(Stream fileStream, string filename, string folderId = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await Creator.UploadFile(fileStream, filename, folderId, cancellationToken);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual async Task<Media> GetFolder(string id)
|
||||
{
|
||||
@@ -83,6 +115,18 @@ public class MediaManagement : IMediaManagement
|
||||
|
||||
public interface IMediaManagement
|
||||
{
|
||||
/// <summary>
|
||||
/// Get publicly accessible file path for a media file
|
||||
/// </summary>
|
||||
/// <param name="file">The media file</param>
|
||||
/// <param name="thumbnailKey">An optional thumbnail key which returns the path to a generated thumbnail</param>
|
||||
string GetPublicFilePath(Media file, string thumbnailKey = null);
|
||||
|
||||
/// <summary>
|
||||
/// Uploads a file and persists it
|
||||
/// </summary>
|
||||
Task<EntityResult<Media>> UploadFile(Stream fileStream, string filename, string folderId = null, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get a media folder by id
|
||||
/// </summary>
|
||||
|
||||
@@ -1,7 +1,30 @@
|
||||
namespace zero.Media
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using System.IO;
|
||||
|
||||
namespace zero.Media
|
||||
{
|
||||
public static class MediaManagementExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Uploads a file and persists it
|
||||
/// </summary>
|
||||
public static async Task<EntityResult<Media>> 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);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Uploads a file and persists it
|
||||
/// </summary>
|
||||
public static async Task<EntityResult<Media>> 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);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Rename and store a media folder
|
||||
/// </summary>
|
||||
@@ -55,12 +78,12 @@
|
||||
/// <summary>
|
||||
/// Deletes a folder by id
|
||||
/// </summary>
|
||||
public static async Task<EntityResult<Media>> DeleteFolder(this IMediaManagement media, string folderId)
|
||||
public static async Task<EntityResult<string[]>> DeleteFolder(this IMediaManagement media, string folderId)
|
||||
{
|
||||
Media folder = await media.GetFolder(folderId);
|
||||
if (folder == null)
|
||||
{
|
||||
return EntityResult<Media>.Fail("@errors.idnotfound");
|
||||
return EntityResult<string[]>.Fail("@errors.idnotfound");
|
||||
}
|
||||
|
||||
return await media.DeleteFolder(folder);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace zero.Media;
|
||||
using FluentValidation;
|
||||
|
||||
namespace zero.Media;
|
||||
|
||||
public class MediaStore : TreeEntityStore<Media>, IMediaStore
|
||||
{
|
||||
@@ -22,6 +24,19 @@ public class MediaStore : TreeEntityStore<Media>, IMediaStore
|
||||
Media parent = await Load(parentId);
|
||||
return parent != null && parent.Type == MediaType.Folder;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void ValidationRules(ZeroValidator<Media> 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,11 @@
|
||||
[RavenCollection("Media")]
|
||||
public class Media : ZeroEntity, IZeroTreeEntity
|
||||
{
|
||||
public Media()
|
||||
{
|
||||
IsActive = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Id/name of the phyiscal folder which is stored on disk/cloud
|
||||
/// </summary>
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
namespace zero.Media;
|
||||
|
||||
/// <summary>
|
||||
/// A media folder contains media and other folders
|
||||
/// </summary>
|
||||
[RavenCollection("MediaFolders")]
|
||||
public class MediaFolder : ZeroEntity, IZeroTreeEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Parent folder id
|
||||
/// </summary>
|
||||
public string ParentId { get; set; }
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
namespace zero.Media;
|
||||
|
||||
public class MediaListQuery : ListQuery<Media>
|
||||
{
|
||||
public string FolderId { get; set; }
|
||||
}
|
||||
|
||||
|
||||
public class MediaListItemQuery : ListQuery<MediaListItem>
|
||||
{
|
||||
public string FolderId { get; set; }
|
||||
|
||||
public bool SearchIsGlobal { get; set; }
|
||||
}
|
||||
@@ -11,10 +11,6 @@ internal static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddZeroMedia(this IServiceCollection services, IConfiguration config)
|
||||
{
|
||||
services.AddScoped<IMediaStore, MediaStore>();
|
||||
services.AddScoped<IMediaCreator, MediaCreator>();
|
||||
services.AddScoped<IMediaManagement, MediaManagement>();
|
||||
|
||||
services.AddSingleton<IMediaFileSystem, MediaFileSystem>(svc =>
|
||||
{
|
||||
IOptions<MediaOptions> options = svc.GetRequiredService<IOptions<MediaOptions>>();
|
||||
@@ -22,6 +18,10 @@ internal static class ServiceCollectionExtensions
|
||||
return new(Path.Combine(env.WebRootPath, options.Value.FolderPath), options.Value.PublicPathPrefix);
|
||||
});
|
||||
|
||||
services.AddScoped<IMediaStore, MediaStore>();
|
||||
services.AddScoped<IMediaCreator, MediaCreator>();
|
||||
services.AddScoped<IMediaManagement, MediaManagement>();
|
||||
|
||||
services.AddOptions<MediaOptions>().Bind(config.GetSection("Zero:Media")).Configure(opts =>
|
||||
{
|
||||
opts.FolderPath = "uploads";
|
||||
|
||||
Reference in New Issue
Block a user