start media collection

This commit is contained in:
2020-11-20 17:38:01 +01:00
parent 5d9c828ddf
commit ba8cb46ac5
8 changed files with 291 additions and 29 deletions
+9
View File
@@ -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,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>
{
@@ -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);
}
}
+11 -23
View File
@@ -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"