diff --git a/.gitignore b/.gitignore index 4c398ec98..dc1259a51 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ node_modules/ **/wwwroot/scripts/**/*.* /src/Squidex/appsettings.Development.json +/src/Squidex/Assets diff --git a/src/Squidex.Core/Squidex.Core.csproj b/src/Squidex.Core/Squidex.Core.csproj index d5cb482ce..315417db2 100644 --- a/src/Squidex.Core/Squidex.Core.csproj +++ b/src/Squidex.Core/Squidex.Core.csproj @@ -14,7 +14,7 @@ - + diff --git a/src/Squidex.Infrastructure/Assets/IAssetThumbnailGenerator.cs b/src/Squidex.Infrastructure/Assets/IAssetThumbnailGenerator.cs index c15226c63..a572a853a 100644 --- a/src/Squidex.Infrastructure/Assets/IAssetThumbnailGenerator.cs +++ b/src/Squidex.Infrastructure/Assets/IAssetThumbnailGenerator.cs @@ -15,6 +15,6 @@ namespace Squidex.Infrastructure.Assets { Task GetImageInfoAsync(Stream input); - Task GetThumbnailOrNullAsync(Stream input, int dimension); + Task CreateThumbnailAsync(Stream input, int? width, int? height, string mode); } } diff --git a/src/Squidex.Infrastructure/Assets/ImageSharp/ImageSharpAssetThumbnailGenerator.cs b/src/Squidex.Infrastructure/Assets/ImageSharp/ImageSharpAssetThumbnailGenerator.cs index ab60f7b2f..a97e9ff58 100644 --- a/src/Squidex.Infrastructure/Assets/ImageSharp/ImageSharpAssetThumbnailGenerator.cs +++ b/src/Squidex.Infrastructure/Assets/ImageSharp/ImageSharpAssetThumbnailGenerator.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using System; using System.IO; using System.Threading.Tasks; using ImageSharp; @@ -22,24 +23,46 @@ namespace Squidex.Infrastructure.Assets.ImageSharp Configuration.Default.AddImageFormat(new PngFormat()); } - public Task GetThumbnailOrNullAsync(Stream input, int dimension) + public Task CreateThumbnailAsync(Stream input, int? width, int? height, string mode) { return Task.Run(() => { + if (width == null && height == null) + { + return input; + } + + if (!Enum.TryParse(mode, true, out var resizeMode)) + { + resizeMode = ResizeMode.Max; + } + + var w = width ?? int.MaxValue; + var h = height ?? int.MaxValue; + var result = new MemoryStream(); - var options = - new ResizeOptions + using (var sourceImage = Image.Load(input)) + { + if (w >= sourceImage.Width && h >= sourceImage.Height && resizeMode == ResizeMode.Crop) { - Size = new Size(dimension, dimension), - Mode = ResizeMode.Max - }; + resizeMode = ResizeMode.BoxPad; + } - var image = new Image(input).Resize(options); + var options = + new ResizeOptions + { + Size = new Size(w, h), + Mode = resizeMode + }; + + sourceImage.MetaData.Quality = 0; + sourceImage.Resize(options).Save(result); + } - image.Save(result); + result.Position = 0; - return (Stream)result; + return result; }); } @@ -47,23 +70,22 @@ namespace Squidex.Infrastructure.Assets.ImageSharp { return Task.Run(() => { + ImageInfo imageInfo = null; try { - var image = new Image(input); + var image = Image.Load(input); if (image.Width > 0 && image.Height > 0) { - return new ImageInfo(image.Width, image.Height); - } - else - { - return null; + imageInfo = new ImageInfo(image.Width, image.Height); } } catch { - return null; + imageInfo = null; } + + return imageInfo; }); } } diff --git a/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj b/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj index ebc95d857..a7f263d4b 100644 --- a/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj +++ b/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj @@ -8,7 +8,7 @@ True - + diff --git a/src/Squidex.Read.MongoDb/Assets/MongoAssetRepository.cs b/src/Squidex.Read.MongoDb/Assets/MongoAssetRepository.cs index 37c1493ab..9be371dbe 100644 --- a/src/Squidex.Read.MongoDb/Assets/MongoAssetRepository.cs +++ b/src/Squidex.Read.MongoDb/Assets/MongoAssetRepository.cs @@ -26,12 +26,17 @@ namespace Squidex.Read.MongoDb.Assets { } + protected override string CollectionName() + { + return "Projections_Assets"; + } + public async Task> QueryAsync(Guid appId, HashSet mimeTypes = null, string query = null, int take = 10, int skip = 0) { var filter = CreateFilter(appId, mimeTypes, query); var assets = - await Collection.Find(filter).Skip(skip).Limit(take).ToListAsync(); + await Collection.Find(filter).Skip(skip).Limit(take).SortByDescending(x => x.LastModified).ToListAsync(); return assets.OfType().ToList(); } diff --git a/src/Squidex/Assets/776fe199-0beb-4d64-b10c-01bdda742123_0 b/src/Squidex/Assets/776fe199-0beb-4d64-b10c-01bdda742123_0 deleted file mode 100644 index ebcf056cc..000000000 Binary files a/src/Squidex/Assets/776fe199-0beb-4d64-b10c-01bdda742123_0 and /dev/null differ diff --git a/src/Squidex/Assets/84ede32b-edd6-4859-8ca2-4de831a5d305_0 b/src/Squidex/Assets/84ede32b-edd6-4859-8ca2-4de831a5d305_0 deleted file mode 100644 index ebcf056cc..000000000 Binary files a/src/Squidex/Assets/84ede32b-edd6-4859-8ca2-4de831a5d305_0 and /dev/null differ diff --git a/src/Squidex/Assets/c1a2f5ee-df8e-4930-932d-031bcbdf959d_0 b/src/Squidex/Assets/c1a2f5ee-df8e-4930-932d-031bcbdf959d_0 deleted file mode 100644 index ebcf056cc..000000000 Binary files a/src/Squidex/Assets/c1a2f5ee-df8e-4930-932d-031bcbdf959d_0 and /dev/null differ diff --git a/src/Squidex/Config/Swagger/XmlTagProcessor.cs b/src/Squidex/Config/Swagger/XmlTagProcessor.cs index 19847d1bb..e2a13d080 100644 --- a/src/Squidex/Config/Swagger/XmlTagProcessor.cs +++ b/src/Squidex/Config/Swagger/XmlTagProcessor.cs @@ -20,7 +20,7 @@ namespace Squidex.Config.Swagger { public sealed class XmlTagProcessor : IOperationProcessor, IDocumentProcessor { - public void Process(DocumentProcessorContext context) + public Task ProcessAsync(DocumentProcessorContext context) { foreach (var controllerType in context.ControllerTypes) { @@ -46,6 +46,8 @@ namespace Squidex.Config.Swagger } } } + + return TaskHelper.Done; } public Task ProcessAsync(OperationProcessorContext context) diff --git a/src/Squidex/Controllers/Api/Assets/AssetContentController.cs b/src/Squidex/Controllers/Api/Assets/AssetContentController.cs new file mode 100644 index 000000000..59b90d435 --- /dev/null +++ b/src/Squidex/Controllers/Api/Assets/AssetContentController.cs @@ -0,0 +1,107 @@ +// ========================================================================== +// AssetContentController.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; +using Squidex.Infrastructure.Assets; +using Squidex.Infrastructure.CQRS.Commands; +using Squidex.Pipeline; +using Squidex.Read.Assets.Repositories; + +#pragma warning disable 1573 + +namespace Squidex.Controllers.Api.Assets +{ + /// + /// Uploads and retrieves assets. + /// + [ApiExceptionFilter] + [ServiceFilter(typeof(AppFilterAttribute))] + [SwaggerTag("Assets")] + public class AssetContentController : ControllerBase + { + private readonly IAssetStore assetStorage; + private readonly IAssetRepository assetRepository; + private readonly IAssetThumbnailGenerator assetThumbnailGenerator; + + public AssetContentController( + ICommandBus commandBus, + IAssetStore assetStorage, + IAssetRepository assetRepository, + IAssetThumbnailGenerator assetThumbnailGenerator) + : base(commandBus) + { + this.assetStorage = assetStorage; + this.assetRepository = assetRepository; + this.assetThumbnailGenerator = assetThumbnailGenerator; + } + + /// + /// Gets the content of the asset. + /// + /// The name of the app. + /// The id of the asset. + /// The resize mode. + /// The target width of the image. + /// The target width of the image. + /// + /// 200 => Asset content returned. + /// 404 => App or Asset not found. + /// + [HttpGet] + [Route("assets/{id}/")] + public async Task GetAssetContent(string app, Guid id, [FromQuery] int? width = null, [FromQuery] int? height = null, [FromQuery] string mode = null) + { + var asset = await assetRepository.FindAssetAsync(id); + + if (asset == null) + { + return NotFound(); + } + + Stream content; + + if (asset.IsImage && (width.HasValue || height.HasValue)) + { + var name = $"{asset.Id}_{asset.Version}_{width}_{height}_{mode}"; + + content = await assetStorage.GetAssetAsync(name); + + if (content == null) + { + var fullSizeContent = await assetStorage.GetAssetAsync($"{asset.Id}_{asset.Version}"); + + if (fullSizeContent == null) + { + return NotFound(); + } + + content = await assetThumbnailGenerator.CreateThumbnailAsync(fullSizeContent, width, height, mode); + + await assetStorage.UploadAssetAsync(name, content); + + content.Position = 0; + } + } + else + { + content = await assetStorage.GetAssetAsync($"{asset.Id}_{asset.Version}"); + } + + if (content == null) + { + return NotFound(); + } + + return new FileStreamResult(content, asset.MimeType); + } + } +} diff --git a/src/Squidex/Controllers/Api/Assets/AssetController.cs b/src/Squidex/Controllers/Api/Assets/AssetController.cs index ff7c734d4..71a4f4099 100644 --- a/src/Squidex/Controllers/Api/Assets/AssetController.cs +++ b/src/Squidex/Controllers/Api/Assets/AssetController.cs @@ -105,7 +105,6 @@ namespace Squidex.Controllers.Api.Assets [HttpPost] [Route("apps/{app}/assets/")] [ProducesResponseType(typeof(AssetDto), 201)] - [ProducesResponseType(typeof(ErrorDto), 409)] [ProducesResponseType(typeof(ErrorDto), 400)] public async Task PostAsset(string app, List files) { diff --git a/src/Squidex/Squidex.csproj b/src/Squidex/Squidex.csproj index 06046590b..a517add11 100644 --- a/src/Squidex/Squidex.csproj +++ b/src/Squidex/Squidex.csproj @@ -33,7 +33,7 @@ - + @@ -57,9 +57,9 @@ - + - + diff --git a/src/Squidex/app/features/assets/pages/asset.component.html b/src/Squidex/app/features/assets/pages/asset.component.html index b54fbdcb8..056377cbf 100644 --- a/src/Squidex/app/features/assets/pages/asset.component.html +++ b/src/Squidex/app/features/assets/pages/asset.component.html @@ -1,7 +1,12 @@ -
-
-
-