start media collection
This commit is contained in:
@@ -24,6 +24,8 @@ namespace zero.Core.Collections
|
||||
|
||||
protected ICollectionInterceptorHandler InterceptorHandler { get; private set; }
|
||||
|
||||
protected virtual Action<T> PreSave { get; set; }
|
||||
|
||||
|
||||
public CollectionBase(IZeroContext context, ICollectionInterceptorHandler interceptorHandler, IValidator<T> validator = null)
|
||||
{
|
||||
@@ -171,6 +173,13 @@ namespace zero.Core.Collections
|
||||
/// <inheritdoc />
|
||||
public virtual async Task<EntityResult<T>> Save(T model)
|
||||
{
|
||||
if (model == null)
|
||||
{
|
||||
return EntityResult<T>.Fail("@errors.onsave.empty");
|
||||
}
|
||||
|
||||
PreSave?.Invoke(model);
|
||||
|
||||
bool isCreate = false;
|
||||
|
||||
// find all Raven Ids
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
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.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using zero.Core.Api;
|
||||
using zero.Core.Database.Indexes;
|
||||
using zero.Core.Entities;
|
||||
using zero.Core.Extensions;
|
||||
|
||||
namespace zero.Core.Collections
|
||||
{
|
||||
public class MediaCollection : CollectionBase<IMedia>, IMediaCollection
|
||||
{
|
||||
public MediaCollection(IZeroContext context, ICollectionInterceptorHandler interceptor, IValidator<IMedia> validator, IPaths paths) : base(context, interceptor, validator)
|
||||
{
|
||||
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 async Task<string> GetSourceById(string id, bool thumb = false)
|
||||
{
|
||||
IMedia media = await Session.LoadAsync<IMedia>(id);
|
||||
|
||||
if (media == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (media.Source.StartsWith("url://"))
|
||||
{
|
||||
return media.Source.Substring(6) + "?preset=productMini"; // TODO remove this
|
||||
}
|
||||
|
||||
return thumb ? (media.ThumbnailSource ?? media.Source) : media.Source;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ListResult<IMedia>> GetByQuery(MediaListQuery query)
|
||||
{
|
||||
query.SearchFor(entity => entity.Name);
|
||||
|
||||
return await Query
|
||||
.WhereIf(x => x.FolderId == query.FolderId, !query.FolderId.IsNullOrEmpty(), x => x.FolderId == null)
|
||||
.ToQueriedListAsync(query);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<EntityResult<IMedia>> Move(string id, string parentId)
|
||||
{
|
||||
IMedia model = await GetById(id);
|
||||
IMediaFolder parent = await Session.LoadAsync<IMediaFolder>(parentId);
|
||||
|
||||
if (model == null || (!parentId.IsNullOrEmpty() && parent == null))
|
||||
{
|
||||
return EntityResult<IMedia>.Fail("@errors.idnotfound");
|
||||
}
|
||||
|
||||
model.FolderId = parent?.Id;
|
||||
|
||||
return await Save(model);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Media> 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<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 async Task<ListResult<MediaListItem>> GetListByQuery(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);
|
||||
|
||||
Session.Advanced.MaxNumberOfRequestsPerSession = query.PageSize + 1;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
ListResult<MediaListItem> result = await dbQuery.ToQueriedListAsync(query);
|
||||
|
||||
foreach (MediaListItem item in result.Items)
|
||||
{
|
||||
if (item.IsFolder)
|
||||
{
|
||||
item.Children = await Session.Query<MediaListItem, Media_ByParent>().CountAsync(x => x.ParentId == item.Id);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO this is only inserted when we have a core/shared database and a multi-tenancy setup
|
||||
if (isRoot)
|
||||
{
|
||||
result.Items.Insert(0, new MediaListItem()
|
||||
{
|
||||
Id = "shared",
|
||||
IsFolder = true
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Saves a thumbnail of an image
|
||||
/// </summary>
|
||||
string SaveThumbnail(IMedia 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>
|
||||
MediaImageMeta GetImageMeta(Image<Rgba32> 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 IMediaCollection : ICollectionBase<IMedia>
|
||||
{
|
||||
/// <summary>
|
||||
/// Get media source by Id
|
||||
/// </summary>
|
||||
Task<string> GetSourceById(string id, bool thumb = false);
|
||||
|
||||
/// <summary>
|
||||
/// Get all available media items with query
|
||||
/// </summary>
|
||||
Task<ListResult<IMedia>> GetByQuery(MediaListQuery query);
|
||||
|
||||
/// <summary>
|
||||
/// Get all available media items (including folders) with query
|
||||
/// </summary>
|
||||
Task<ListResult<MediaListItem>> GetListByQuery(MediaListItemQuery query);
|
||||
|
||||
/// <summary>
|
||||
/// Move a file to a new parent
|
||||
/// </summary>
|
||||
Task<EntityResult<IMedia>> Move(string id, string parentId);
|
||||
|
||||
/// <summary>
|
||||
/// Uploads a file to the media folder
|
||||
/// </summary>
|
||||
Task<Media> Upload(IFormFile file, string folderId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -2,8 +2,9 @@
|
||||
using zero.Core.Api;
|
||||
using zero.Core.Entities;
|
||||
using zero.Core.Extensions;
|
||||
using zero.Core.Validation;
|
||||
|
||||
namespace zero.Core.Validation
|
||||
namespace zero.Core.Collections
|
||||
{
|
||||
public class MediaFolderValidator : ZeroValidator<IMediaFolder>
|
||||
{
|
||||
+4
-4
@@ -1,15 +1,15 @@
|
||||
using FluentValidation;
|
||||
using zero.Core.Api;
|
||||
using zero.Core.Entities;
|
||||
using zero.Core.Validation;
|
||||
|
||||
namespace zero.Core.Validation
|
||||
namespace zero.Core.Collections
|
||||
{
|
||||
public class MediaValidator : ZeroValidator<IMedia>
|
||||
{
|
||||
public MediaValidator(IBackofficeStore store)
|
||||
public MediaValidator()
|
||||
{
|
||||
RuleFor(x => x.Name).Length(2, 80);
|
||||
RuleFor(x => x.IsActive).Equal(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,9 @@ namespace zero.Web.Controllers
|
||||
public virtual async Task<EditModel<TEntity>> GetById([FromQuery] string id) => Edit(await Collection.GetById(id));
|
||||
|
||||
|
||||
public virtual async Task<Dictionary<string, TEntity>> GetByIds([FromQuery] string[] ids) => await Collection.GetByIds(ids);
|
||||
|
||||
|
||||
public virtual EditModel<TEntity> GetEmpty([FromServices] TEntity blueprint) => Edit(blueprint);
|
||||
|
||||
|
||||
@@ -58,9 +61,11 @@ namespace zero.Web.Controllers
|
||||
public virtual async Task<IList<PreviewModel>> GetPreviews([FromQuery] List<string> ids) => Previews(await Collection.GetByIds(ids.ToArray()), PreviewTransform);
|
||||
|
||||
|
||||
[HttpPost]
|
||||
public virtual async Task<EntityResult<TEntity>> Save([FromBody] TEntity model) => await Collection.Save(model);
|
||||
|
||||
|
||||
[HttpDelete]
|
||||
public virtual async Task<EntityResult<TEntity>> Delete([FromQuery] string id) => await Collection.DeleteById(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using zero.Core;
|
||||
using zero.Core.Api;
|
||||
using zero.Core.Collections;
|
||||
using zero.Core.Entities;
|
||||
using zero.Core.Identity;
|
||||
using zero.Web.Models;
|
||||
@@ -16,48 +17,35 @@ using zero.Web.Models;
|
||||
namespace zero.Web.Controllers
|
||||
{
|
||||
[ZeroAuthorize(Permissions.Sections.Media, PermissionsValue.True)]
|
||||
public class MediaController : BackofficeController
|
||||
public class MediaController : BackofficeCollectionController<IMedia, IMediaCollection>
|
||||
{
|
||||
IMediaApi Api;
|
||||
IMediaFolderApi MediaFolderApi;
|
||||
IPaths Paths;
|
||||
|
||||
|
||||
public MediaController(IMediaApi api, IMediaFolderApi mediaFolderApi, IPaths paths)
|
||||
public MediaController(IMediaCollection collection, IMediaFolderApi mediaFolderApi, IPaths paths) : base(collection)
|
||||
{
|
||||
Api = api;
|
||||
MediaFolderApi = mediaFolderApi;
|
||||
Paths = paths;
|
||||
PreviewTransform = (item, model) => model.Icon = "fth-globe";
|
||||
}
|
||||
|
||||
|
||||
public async Task<EditModel<IMedia>> GetById([FromQuery] string id) => Edit(await Api.GetById(id));
|
||||
|
||||
public async Task<ListResult<MediaListItem>> GetListByQuery([FromQuery] MediaListItemQuery query) => await Collection.GetListByQuery(query);
|
||||
|
||||
public async Task<Dictionary<string, IMedia>> GetByIds([FromQuery] string[] ids) => await Api.GetById(ids);
|
||||
|
||||
|
||||
public async Task<ListResult<MediaListItem>> GetListByQuery([FromQuery] MediaListItemQuery query) => await Api.GetListByQuery(query);
|
||||
|
||||
|
||||
public async Task<EntityResult<IMedia>> Save([FromBody] IMedia model) => await Api.Save(model);
|
||||
|
||||
[HttpPost]
|
||||
public async Task<EntityResult<IMedia>> Upload(IFormFile file, string folderId) => await Api.Save(await Api.Upload(file, folderId));
|
||||
public async Task<EntityResult<IMedia>> Upload(IFormFile file, string folderId) => await Collection.Save(await Collection.Upload(file, folderId));
|
||||
|
||||
[HttpPost]
|
||||
public async Task<Media> UploadTemporary(IFormFile file, string folderId) => await Api.Upload(file, folderId);
|
||||
|
||||
[HttpDelete]
|
||||
public async Task<EntityResult<IMedia>> Delete([FromQuery] string id) => await Api.Delete(id);
|
||||
public async Task<Media> UploadTemporary(IFormFile file, string folderId) => await Collection.Upload(file, folderId);
|
||||
|
||||
[HttpPost]
|
||||
public async Task<EntityResult<IMedia>> Move([FromBody] ActionCopyModel model) => await Api.Move(model.Id, model.DestinationId);
|
||||
public async Task<EntityResult<IMedia>> Move([FromBody] ActionCopyModel model) => await Collection.Move(model.Id, model.DestinationId);
|
||||
|
||||
|
||||
public async Task<MediaListResultModel> GetAll([FromQuery] MediaListQuery query)
|
||||
{
|
||||
ListResult<MediaListModel> items = (await Api.GetByQuery(query)).MapTo(x => new MediaListModel()
|
||||
ListResult<MediaListModel> items = (await Collection.GetByQuery(query)).MapTo(x => new MediaListModel()
|
||||
{
|
||||
Id = x.Id,
|
||||
IsFolder = false,
|
||||
@@ -81,9 +69,9 @@ namespace zero.Web.Controllers
|
||||
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> StreamThumbnail([FromQuery] string id, [FromQuery] bool thumb = true, [FromQuery] bool core = false)
|
||||
public async Task<IActionResult> StreamThumbnail([FromQuery] string id, [FromQuery] bool thumb = true)
|
||||
{
|
||||
string path = await Api.GetSourceById(id, thumb, core);
|
||||
string path = await Collection.GetSourceById(id, thumb);
|
||||
|
||||
if (path == null)
|
||||
{
|
||||
|
||||
@@ -74,6 +74,7 @@ namespace zero.Web.Defaults
|
||||
services.AddTransient<ICountriesCollection, CountriesCollection>();
|
||||
services.AddTransient<ILanguagesCollection, LanguagesCollection>();
|
||||
services.AddTransient<ITranslationsCollection, TranslationsCollection>();
|
||||
services.AddTransient<IMediaCollection, MediaCollection>();
|
||||
services.AddTransient<IUserApi, UserApi>();
|
||||
services.AddTransient<IPagesApi, PagesApi>();
|
||||
services.AddTransient<IPageTreeApi, PageTreeApi>();
|
||||
|
||||
@@ -132,7 +132,8 @@
|
||||
"errors": {
|
||||
"idnotfound": "Could not find an entity for the given ID",
|
||||
"onsave": {
|
||||
"notallowed": "You are not allowed to edit this resource"
|
||||
"notallowed": "You are not allowed to edit this resource",
|
||||
"empty": "You need to pass a model"
|
||||
},
|
||||
"ondelete": {
|
||||
"idnotfound": "Could not find an entity for the given ID"
|
||||
|
||||
Reference in New Issue
Block a user