diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/ImageAssetMetadataSource.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/ImageAssetMetadataSource.cs index 40a0621c0..bc977433c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/ImageAssetMetadataSource.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/ImageAssetMetadataSource.cs @@ -20,37 +20,6 @@ namespace Squidex.Domain.Apps.Entities.Assets this.assetThumbnailGenerator = assetThumbnailGenerator; } - private sealed class TempAssetFile : AssetFile, IDisposable - { - public Stream Stream { get; } - - public TempAssetFile(AssetFile source) - : base(source.FileName, source.MimeType, source.FileSize) - { - var tempPath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); - - var tempStream = new FileStream(tempPath, - FileMode.Create, - FileAccess.ReadWrite, - FileShare.None, 4096, - FileOptions.DeleteOnClose); - - Stream = tempStream; - } - - public override void Dispose() - { - Stream.Dispose(); - } - - public override Stream OpenRead() - { - Stream.Position = 0; - - return Stream; - } - } - public async Task EnhanceAsync(UploadAssetCommand command) { if (command.Type == AssetType.Unknown || command.Type == AssetType.Image) @@ -66,18 +35,27 @@ namespace Squidex.Domain.Apps.Entities.Assets if (imageInfo != null) { - var isSwapped = imageInfo.IsRotatedOrSwapped; + var isSwapped = imageInfo.Orientation > ImageOrientation.TopLeft; - if (isSwapped) + if (command.File != null && isSwapped) { var tempFile = new TempAssetFile(command.File); await using (var uploadStream = command.File.OpenRead()) { - imageInfo = await assetThumbnailGenerator.FixOrientationAsync(uploadStream, mimeType, tempFile.Stream); + await using (var tempStream = tempFile.OpenWrite()) + { + await assetThumbnailGenerator.FixOrientationAsync(uploadStream, mimeType, tempStream); + } } - command.File.Dispose(); + await using (var tempStream = tempFile.OpenRead()) + { + imageInfo = await assetThumbnailGenerator.GetImageInfoAsync(tempStream, mimeType) ?? imageInfo; + } + + await command.File.DisposeAsync(); + command.File = tempFile; } diff --git a/backend/src/Squidex.Infrastructure/Orleans/LoggingFilter.cs b/backend/src/Squidex.Infrastructure/Orleans/LoggingFilter.cs index ccde0b8d4..49ba98fc2 100644 --- a/backend/src/Squidex.Infrastructure/Orleans/LoggingFilter.cs +++ b/backend/src/Squidex.Infrastructure/Orleans/LoggingFilter.cs @@ -6,7 +6,6 @@ // ========================================================================== using Orleans; -using Squidex.Infrastructure.States; using Squidex.Log; namespace Squidex.Infrastructure.Orleans diff --git a/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj b/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj index 3cba22f72..e1a45f2e3 100644 --- a/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj +++ b/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj @@ -31,9 +31,9 @@ - + - + diff --git a/backend/src/Squidex.Web/Pipeline/ActionContextLogAppender.cs b/backend/src/Squidex.Web/Pipeline/ActionContextLogAppender.cs index 057e81231..e28772c28 100644 --- a/backend/src/Squidex.Web/Pipeline/ActionContextLogAppender.cs +++ b/backend/src/Squidex.Web/Pipeline/ActionContextLogAppender.cs @@ -8,24 +8,31 @@ using System.Diagnostics; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.Extensions.DependencyInjection; using Squidex.Log; namespace Squidex.Web.Pipeline { public sealed class ActionContextLogAppender : ILogAppender { - private readonly IActionContextAccessor actionContextAccessor; - private readonly IHttpContextAccessor httpContextAccessor; + private readonly IServiceProvider services; - public ActionContextLogAppender(IActionContextAccessor actionContextAccessor, IHttpContextAccessor httpContextAccessor) + public ActionContextLogAppender(IServiceProvider services) { - this.actionContextAccessor = actionContextAccessor; - - this.httpContextAccessor = httpContextAccessor; + this.services = services; } public void Append(IObjectWriter writer, SemanticLogLevel logLevel, Exception? exception) { + var httpContextAccessor = services.GetService(); + + if (string.IsNullOrEmpty(httpContextAccessor?.HttpContext?.Request?.Method)) + { + return; + } + + var actionContext = services.GetRequiredService()?.ActionContext; + try { var httpContext = httpContextAccessor.HttpContext; @@ -37,7 +44,7 @@ namespace Squidex.Web.Pipeline var requestId = GetRequestId(httpContext); - var logContext = (requestId, context: httpContext, actionContextAccessor); + var logContext = (requestId, context: httpContext, actionContext); writer.WriteObject("web", logContext, (ctx, w) => { @@ -45,7 +52,7 @@ namespace Squidex.Web.Pipeline w.WriteProperty("requestPath", ctx.context.Request.Path); w.WriteProperty("requestMethod", ctx.context.Request.Method); - var actionContext = ctx.actionContextAccessor.ActionContext; + var actionContext = ctx.actionContext; if (actionContext != null) { diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppImageController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppImageController.cs new file mode 100644 index 000000000..5881f08d4 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppImageController.cs @@ -0,0 +1,162 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Net.Http.Headers; +using Squidex.Assets; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Web; + +namespace Squidex.Areas.Api.Controllers.Apps +{ + /// + /// Manages and configures apps. + /// + [ApiExplorerSettings(GroupName = nameof(Apps))] + public sealed class AppImageController : ApiController + { + private readonly IAppImageStore appImageStore; + private readonly IAppProvider appProvider; + private readonly IAssetStore assetStore; + private readonly IAssetThumbnailGenerator assetThumbnailGenerator; + + public AppImageController(ICommandBus commandBus, + IAppImageStore appImageStore, + IAppProvider appProvider, + IAssetStore assetStore, + IAssetThumbnailGenerator assetThumbnailGenerator) + : base(commandBus) + { + this.appImageStore = appImageStore; + this.appProvider = appProvider; + this.assetStore = assetStore; + this.assetThumbnailGenerator = assetThumbnailGenerator; + } + + /// + /// Get the app image. + /// + /// The name of the app. + /// + /// 200 => App image found and content or (resized) image returned. + /// 404 => App not found. + /// + [HttpGet] + [Route("apps/{app}/image")] + [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)] + [AllowAnonymous] + [ApiCosts(0)] + public IActionResult GetImage(string app) + { + if (App.Image == null) + { + return NotFound(); + } + + var etag = App.Image.Etag; + + Response.Headers[HeaderNames.ETag] = etag; + + var callback = new FileCallback(async (body, range, ct) => + { + var resizedAsset = $"{App.Id}_{etag}_Resized"; + + try + { + await assetStore.DownloadAsync(resizedAsset, body, ct: ct); + } + catch (AssetNotFoundException) + { + await ResizeAsync(resizedAsset, App.Image.MimeType, body, ct); + } + }); + + return new FileCallbackResult(App.Image.MimeType, callback) + { + ErrorAs404 = true + }; + } + + private async Task ResizeAsync(string resizedAsset, string mimeType, Stream target, + CancellationToken ct) + { +#pragma warning disable CA2016 // Forward the 'CancellationToken' parameter to methods +#pragma warning disable MA0040 // Flow the cancellation token + using var activity = Telemetry.Activities.StartActivity("Resize"); + + await using var assetOriginal = new TempAssetFile(resizedAsset, mimeType, 0); + await using var assetResized = new TempAssetFile(resizedAsset, mimeType, 0); + + var resizeOptions = new ResizeOptions + { + TargetWidth = 50, + TargetHeight = 50 + }; + + using (Telemetry.Activities.StartActivity("Read")) + { + await using (var originalStream = assetOriginal.OpenWrite()) + { + await appImageStore.DownloadAsync(App.Id, originalStream); + } + } + + using (Telemetry.Activities.StartActivity("Resize")) + { + try + { + await using (var originalStream = assetOriginal.OpenRead()) + { + await using (var resizeStream = assetResized.OpenWrite()) + { + await assetThumbnailGenerator.CreateThumbnailAsync(originalStream, mimeType, resizeStream, resizeOptions); + } + } + } + catch + { + await using (var originalStream = assetOriginal.OpenRead()) + { + await using (var resizeStream = assetResized.OpenWrite()) + { + await originalStream.CopyToAsync(resizeStream); + } + } + } + } + + using (Telemetry.Activities.StartActivity("Save")) + { + try + { + await using (var resizeStream = assetResized.OpenRead()) + { + await assetStore.UploadAsync(resizedAsset, resizeStream); + } + } + catch (AssetAlreadyExistsException) + { + return; + } + } + + using (Telemetry.Activities.StartActivity("Write")) + { + await using (var resizeStream = assetResized.OpenRead()) + { + await resizeStream.CopyToAsync(target, ct); + } + } +#pragma warning restore CA2016 // Forward the 'CancellationToken' parameter to methods +#pragma warning restore MA0040 // Flow the cancellation token + } + } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs index 635eb0ffc..a5b0713f8 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs @@ -5,15 +5,12 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Net.Http.Headers; using Squidex.Areas.Api.Controllers.Apps.Models; -using Squidex.Assets; using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Translations; @@ -29,28 +26,12 @@ namespace Squidex.Areas.Api.Controllers.Apps [ApiExplorerSettings(GroupName = nameof(Apps))] public sealed class AppsController : ApiController { - private static readonly ResizeOptions ResizeOptions = new ResizeOptions - { - TargetWidth = 50, - TargetHeight = 50, - Mode = ResizeMode.Crop - }; - private readonly IAppImageStore appImageStore; private readonly IAppProvider appProvider; - private readonly IAssetStore assetStore; - private readonly IAssetThumbnailGenerator assetThumbnailGenerator; - - public AppsController(ICommandBus commandBus, - IAppImageStore appImageStore, - IAppProvider appProvider, - IAssetStore assetStore, - IAssetThumbnailGenerator assetThumbnailGenerator) + + public AppsController(ICommandBus commandBus, IAppProvider appProvider) : base(commandBus) { - this.appImageStore = appImageStore; this.appProvider = appProvider; - this.assetStore = assetStore; - this.assetThumbnailGenerator = assetThumbnailGenerator; } /// @@ -185,84 +166,6 @@ namespace Squidex.Areas.Api.Controllers.Apps return Ok(response); } - /// - /// Get the app image. - /// - /// The name of the app. - /// - /// 200 => App image found and content or (resized) image returned. - /// 404 => App not found. - /// - [HttpGet] - [Route("apps/{app}/image")] - [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)] - [AllowAnonymous] - [ApiCosts(0)] - public IActionResult GetImage(string app) - { - if (App.Image == null) - { - return NotFound(); - } - - var etag = App.Image.Etag; - - Response.Headers[HeaderNames.ETag] = etag; - - var callback = new FileCallback(async (body, range, ct) => - { - var resizedAsset = $"{App.Id}_{etag}_Resized"; - - try - { - await assetStore.DownloadAsync(resizedAsset, body, ct: ct); - } - catch (AssetNotFoundException) - { - using (Telemetry.Activities.StartActivity("Resize")) - { - await using (var destinationStream = GetTempStream()) - { - await ResizeAsync(resizedAsset, App.Image.MimeType, destinationStream); - - await destinationStream.CopyToAsync(body, ct); - } - } - } - }); - - return new FileCallbackResult(App.Image.MimeType, callback) - { - ErrorAs404 = true - }; - } - - private async Task ResizeAsync(string resizedAsset, string mimeType, FileStream destinationStream) - { -#pragma warning disable MA0040 // Flow the cancellation token - await using (var sourceStream = GetTempStream()) - { - using (Telemetry.Activities.StartActivity("ResizeDownload")) - { - await appImageStore.DownloadAsync(App.Id, sourceStream); - sourceStream.Position = 0; - } - - using (Telemetry.Activities.StartActivity("ResizeImage")) - { - await assetThumbnailGenerator.CreateThumbnailAsync(sourceStream, mimeType, destinationStream, ResizeOptions); - destinationStream.Position = 0; - } - - using (Telemetry.Activities.StartActivity("ResizeUpload")) - { - await assetStore.UploadAsync(resizedAsset, destinationStream); - destinationStream.Position = 0; - } - } -#pragma warning restore MA0040 // Flow the cancellation token - } - /// /// Remove the app image. /// @@ -335,18 +238,5 @@ namespace Squidex.Areas.Api.Controllers.Apps return new UploadAppImage { File = file.ToAssetFile() }; } - - private static FileStream GetTempStream() - { - var tempFileName = Path.GetTempFileName(); - - return new FileStream(tempFileName, - FileMode.Create, - FileAccess.ReadWrite, - FileShare.Delete, 1024 * 16, - FileOptions.Asynchronous | - FileOptions.DeleteOnClose | - FileOptions.SequentialScan); - } } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs index bdf942b4f..c3321a22b 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs @@ -135,7 +135,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models if (app.Image != null) { - AddGetLink("image", resources.Url(x => nameof(x.GetImage), values)); + AddGetLink("image", resources.Url(x => nameof(x.GetImage), values)); } if (isContributor) diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs index 87add2bd5..f2f5627da 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs @@ -135,10 +135,6 @@ namespace Squidex.Areas.Api.Controllers.Assets return NotFound(); } - var resizeOptions = request.ToResizeOptions(asset); - - FileCallback callback; - Response.Headers[HeaderNames.ETag] = asset.FileVersion.ToString(CultureInfo.InvariantCulture); if (request.CacheDuration > 0) @@ -146,27 +142,33 @@ namespace Squidex.Areas.Api.Controllers.Assets Response.Headers[HeaderNames.CacheControl] = $"public,max-age={request.CacheDuration}"; } + var resizeOptions = request.ToResizeOptions(asset); + var contentLength = (long?)null; + var contentCallback = (FileCallback?)null; if (asset.Type == AssetType.Image && resizeOptions.IsValid) { - callback = async (bodyStream, range, ct) => + contentCallback = async (body, range, ct) => { + var suffix = resizeOptions.ToString(); + if (request.ForceResize) { - await ResizeAsync(asset, bodyStream, resizeOptions, true, ct); + using (Telemetry.Activities.StartActivity("Resize")) + { + await ResizeAsync(asset, suffix, body, resizeOptions, true, ct); + } } else { try { - var suffix = resizeOptions.ToString(); - - await assetFileStore.DownloadAsync(asset.AppId.Id, asset.Id, asset.FileVersion, suffix, bodyStream, ct: ct); + await DownloadAsync(asset, body, suffix, range, ct); } catch (AssetNotFoundException) { - await ResizeAsync(asset, bodyStream, resizeOptions, false, ct); + await ResizeAsync(asset, suffix, body, resizeOptions, false, ct); } } }; @@ -175,13 +177,13 @@ namespace Squidex.Areas.Api.Controllers.Assets { contentLength = asset.FileSize; - callback = async (bodyStream, range, ct) => + contentCallback = async (body, range, ct) => { - await assetFileStore.DownloadAsync(asset.AppId.Id, asset.Id, asset.FileVersion, null, bodyStream, range, ct); + await DownloadAsync(asset, body, null, range, ct); }; } - return new FileCallbackResult(asset.MimeType, callback) + return new FileCallbackResult(asset.MimeType, contentCallback) { EnableRangeProcessing = contentLength > 0, ErrorAs404 = true, @@ -192,78 +194,78 @@ namespace Squidex.Areas.Api.Controllers.Assets }; } - private async Task ResizeAsync(IAssetEntity asset, Stream bodyStream, ResizeOptions resizeOptions, bool overwrite, + private async Task DownloadAsync(IAssetEntity asset, Stream bodyStream, string? suffix, BytesRange range, CancellationToken ct) { - using (Telemetry.Activities.StartActivity("Resize")) - { - await using (var destinationStream = GetTempStream()) - { - // Do not use cancellation for the resize process because it is valuable to complete it. - await ResizeAsync(asset, resizeOptions, destinationStream, overwrite); - - await destinationStream.CopyToAsync(bodyStream, ct); - } - } + await assetFileStore.DownloadAsync(asset.AppId.Id, asset.Id, asset.FileVersion, suffix, bodyStream, range, ct); } - private async Task ResizeAsync(IAssetEntity asset, ResizeOptions resizeOptions, FileStream stream, bool overwrite) + private async Task ResizeAsync(IAssetEntity asset, string suffix, Stream target, ResizeOptions resizeOptions, bool overwrite, + CancellationToken ct) { +#pragma warning disable CA2016 // Forward the 'CancellationToken' parameter to methods #pragma warning disable MA0040 // Flow the cancellation token - var suffix = resizeOptions.ToString(); + using var activity = Telemetry.Activities.StartActivity("Resize"); + + await using var assetOriginal = new TempAssetFile(asset.FileName, asset.MimeType, 0); + await using var assetResized = new TempAssetFile(asset.FileName, asset.MimeType, 0); - await using (var sourceStream = GetTempStream()) + using (Telemetry.Activities.StartActivity("Read")) { - using (Telemetry.Activities.StartActivity("ResizeDownload")) + await using (var originalStream = assetOriginal.OpenWrite()) { - await assetFileStore.DownloadAsync(asset.AppId.Id, asset.Id, asset.FileVersion, null, sourceStream); - sourceStream.Position = 0; + await assetFileStore.DownloadAsync(asset.AppId.Id, asset.Id, asset.FileVersion, null, originalStream); } + } - using (Telemetry.Activities.StartActivity("ResizeImage")) + using (Telemetry.Activities.StartActivity("Resize")) + { + try { - try + await using (var originalStream = assetOriginal.OpenRead()) { - await assetThumbnailGenerator.CreateThumbnailAsync(sourceStream, asset.MimeType, stream, resizeOptions); - stream.Position = 0; + await using (var resizeStream = assetResized.OpenWrite()) + { + await assetThumbnailGenerator.CreateThumbnailAsync(originalStream, asset.MimeType, resizeStream, resizeOptions); + } } - catch + } + catch + { + await using (var originalStream = assetOriginal.OpenRead()) { - sourceStream.Position = 0; - await sourceStream.CopyToAsync(stream); + await using (var resizeStream = assetResized.OpenWrite()) + { + await originalStream.CopyToAsync(resizeStream); + } } } + } + using (Telemetry.Activities.StartActivity("Save")) + { try { - using (Telemetry.Activities.StartActivity("ResizeUpload")) + await using (var resizeStream = assetResized.OpenRead()) { - await assetFileStore.UploadAsync(asset.AppId.Id, asset.Id, asset.FileVersion, suffix, stream, overwrite); - stream.Position = 0; + await assetFileStore.UploadAsync(asset.AppId.Id, asset.Id, asset.FileVersion, suffix, resizeStream, overwrite); } } catch (AssetAlreadyExistsException) { - stream.Position = 0; + return; } } -#pragma warning restore MA0040 // Flow the cancellation token - } - - private static FileStream GetTempStream() - { - var tempFileName = Path.GetTempFileName(); - const int bufferSize = 16 * 1024; - - return new FileStream(tempFileName, - FileMode.Create, - FileAccess.ReadWrite, - FileShare.Delete, - bufferSize, - FileOptions.Asynchronous | - FileOptions.DeleteOnClose | - FileOptions.SequentialScan); + using (Telemetry.Activities.StartActivity("Write")) + { + await using (var resizeStream = assetResized.OpenRead()) + { + await resizeStream.CopyToAsync(target, ct); + } + } +#pragma warning restore CA2016 // Forward the 'CancellationToken' parameter to methods +#pragma warning restore MA0040 // Flow the cancellation token } } } diff --git a/backend/src/Squidex/Areas/Api/Startup.cs b/backend/src/Squidex/Areas/Api/Startup.cs index 6c5e0f33e..480252943 100644 --- a/backend/src/Squidex/Areas/Api/Startup.cs +++ b/backend/src/Squidex/Areas/Api/Startup.cs @@ -15,18 +15,18 @@ namespace Squidex.Areas.Api { public static void ConfigureApi(this IApplicationBuilder app) { - app.Map(Constants.PrefixApi, appApi => + app.Map(Constants.PrefixApi, builder => { - appApi.UseAccessTokenQueryString(); + builder.UseAccessTokenQueryString(); - appApi.UseRouting(); + builder.UseRouting(); - appApi.UseAuthentication(); - appApi.UseAuthorization(); + builder.UseAuthentication(); + builder.UseAuthorization(); - appApi.UseSquidexOpenApi(); + builder.UseSquidexOpenApi(); - appApi.UseEndpoints(endpoints => + builder.UseEndpoints(endpoints => { endpoints.MapControllers(); }); diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs index 258da762e..9cecb17da 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs @@ -20,18 +20,13 @@ using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Validation; using Squidex.Shared.Identity; using Squidex.Shared.Users; +using Squidex.Web; namespace Squidex.Areas.IdentityServer.Controllers.Profile { [Authorize] public sealed class ProfileController : IdentityServerController { - private static readonly ResizeOptions ResizeOptions = new ResizeOptions - { - TargetWidth = 128, - TargetHeight = 128, - Mode = ResizeMode.Crop - }; private readonly IUserPictureStore userPictureStore; private readonly IUserService userService; private readonly IAssetThumbnailGenerator assetThumbnailGenerator; @@ -156,31 +151,46 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile throw new ValidationException(T.Get("validation.onlyOneFile")); } - using (var thumbnailStream = new MemoryStream()) + await UploadResizedAsync(files[0], id, HttpContext.RequestAborted); + + var update = new UserValues { - try - { - var file = files[0]; + PictureUrl = SquidexClaimTypes.PictureUrlStore + }; - await using (var stream = file.OpenReadStream()) - { - await assetThumbnailGenerator.CreateThumbnailAsync(stream, file.ContentType, thumbnailStream, ResizeOptions, - HttpContext.RequestAborted); - } + await userService.UpdateAsync(id, update, ct: HttpContext.RequestAborted); + } - thumbnailStream.Position = 0; - } - catch + private async Task UploadResizedAsync(IFormFile file, string id, + CancellationToken ct) + { + await using var assetResized = new TempAssetFile(file.ToAssetFile()); + + var resizeOptions = new ResizeOptions + { + TargetWidth = 128, + TargetHeight = 128 + }; + + try + { + await using (var originalStream = file.OpenReadStream()) { - throw new ValidationException(T.Get("validation.notAnImage")); + await using (var resizeStream = assetResized.OpenWrite()) + { + await assetThumbnailGenerator.CreateThumbnailAsync(originalStream, file.ContentType, resizeStream, resizeOptions, ct); + } } - - await userPictureStore.UploadAsync(id, thumbnailStream, HttpContext.RequestAborted); + } + catch + { + throw new ValidationException(T.Get("validation.notAnImage")); } - var update = new UserValues { PictureUrl = SquidexClaimTypes.PictureUrlStore }; - - await userService.UpdateAsync(id, update, ct: HttpContext.RequestAborted); + await using (var resizeStream = assetResized.OpenWrite()) + { + await userPictureStore.UploadAsync(id, resizeStream, ct); + } } private async Task MakeChangeAsync(Func action, string successMessage, TModel? model = null) where TModel : class diff --git a/backend/src/Squidex/Areas/IdentityServer/Startup.cs b/backend/src/Squidex/Areas/IdentityServer/Startup.cs index 8f6b895e2..77dd04cee 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Startup.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Startup.cs @@ -15,23 +15,23 @@ namespace Squidex.Areas.IdentityServer { var environment = app.ApplicationServices.GetRequiredService(); - app.Map(Constants.PrefixIdentityServer, identityApp => + app.Map(Constants.PrefixIdentityServer, builder => { if (environment.IsDevelopment()) { - identityApp.UseDeveloperExceptionPage(); + builder.UseDeveloperExceptionPage(); } else { - identityApp.UseExceptionHandler("/error"); + builder.UseExceptionHandler("/error"); } - identityApp.UseRouting(); + builder.UseRouting(); - identityApp.UseAuthentication(); - identityApp.UseAuthorization(); + builder.UseAuthentication(); + builder.UseAuthorization(); - identityApp.UseEndpoints(endpoints => + builder.UseEndpoints(endpoints => { endpoints.MapControllers(); }); diff --git a/backend/src/Squidex/Areas/OrleansDashboard/Startup.cs b/backend/src/Squidex/Areas/OrleansDashboard/Startup.cs index 37a292eb1..a4f51fb59 100644 --- a/backend/src/Squidex/Areas/OrleansDashboard/Startup.cs +++ b/backend/src/Squidex/Areas/OrleansDashboard/Startup.cs @@ -15,13 +15,13 @@ namespace Squidex.Areas.OrleansDashboard { public static void ConfigureOrleansDashboard(this IApplicationBuilder app) { - app.Map(Constants.PrefixOrleans, orleansApp => + app.Map(Constants.PrefixOrleans, builder => { - orleansApp.UseAuthentication(); - orleansApp.UseAuthorization(); + builder.UseAuthentication(); + builder.UseAuthorization(); - orleansApp.UseMiddleware(); - orleansApp.UseOrleansDashboard(); + builder.UseMiddleware(); + builder.UseOrleansDashboard(); }); } } diff --git a/backend/src/Squidex/Areas/Portal/Startup.cs b/backend/src/Squidex/Areas/Portal/Startup.cs index b1a5933e7..9c94d2137 100644 --- a/backend/src/Squidex/Areas/Portal/Startup.cs +++ b/backend/src/Squidex/Areas/Portal/Startup.cs @@ -14,13 +14,13 @@ namespace Squidex.Areas.Portal { public static void ConfigurePortal(this IApplicationBuilder app) { - app.Map(Constants.PrefixPortal, portalApp => + app.Map(Constants.PrefixPortal, builder => { - portalApp.UseAuthentication(); - portalApp.UseAuthorization(); + builder.UseAuthentication(); + builder.UseAuthorization(); - portalApp.UseMiddleware(); - portalApp.UseMiddleware(); + builder.UseMiddleware(); + builder.UseMiddleware(); }); } } diff --git a/backend/src/Squidex/Config/Domain/AppsServices.cs b/backend/src/Squidex/Config/Domain/AppsServices.cs index a5d5a52f9..62bc19d53 100644 --- a/backend/src/Squidex/Config/Domain/AppsServices.cs +++ b/backend/src/Squidex/Config/Domain/AppsServices.cs @@ -15,6 +15,7 @@ using Squidex.Domain.Apps.Entities.History; using Squidex.Domain.Apps.Entities.Search; using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Log; namespace Squidex.Config.Domain { @@ -40,6 +41,9 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); + services.AddSingletonAs() + .As().As(); + services.AddSingletonAs() .As(); diff --git a/backend/src/Squidex/Config/Domain/AssetServices.cs b/backend/src/Squidex/Config/Domain/AssetServices.cs index 00e26f5af..5d0789ecb 100644 --- a/backend/src/Squidex/Config/Domain/AssetServices.cs +++ b/backend/src/Squidex/Config/Domain/AssetServices.cs @@ -9,8 +9,6 @@ using FluentFTP; using MongoDB.Driver; using MongoDB.Driver.GridFS; using Squidex.Assets; -using Squidex.Assets.ImageMagick; -using Squidex.Assets.ImageSharp; using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets.DomainObject; @@ -178,16 +176,6 @@ namespace Squidex.Config.Domain } }); - var thumbnailGenerator = new CompositeThumbnailGenerator( - new IAssetThumbnailGenerator[] - { - new ImageSharpThumbnailGenerator(), - new ImageMagickThumbnailGenerator() - }); - - services.AddSingletonAs(c => thumbnailGenerator) - .As(); - services.AddSingletonAs(c => new DelegateInitializer( c.GetRequiredService().GetType().Name, c.GetRequiredService().InitializeAsync)) diff --git a/backend/src/Squidex/Config/Domain/InfrastructureServices.cs b/backend/src/Squidex/Config/Domain/InfrastructureServices.cs index 888e386b0..b22f6d456 100644 --- a/backend/src/Squidex/Config/Domain/InfrastructureServices.cs +++ b/backend/src/Squidex/Config/Domain/InfrastructureServices.cs @@ -25,6 +25,7 @@ using Squidex.Domain.Apps.Entities.Tags; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.EventSourcing.Grains; +using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.UsageTracking; @@ -60,6 +61,9 @@ namespace Squidex.Config.Domain services.AddSingletonAs>() .AsSelf(); + services.AddSingletonAs() + .AsOptional(); + services.AddSingletonAs() .As(); diff --git a/backend/src/Squidex/Config/Domain/LoggingServices.cs b/backend/src/Squidex/Config/Domain/LoggingServices.cs index b2a41a590..f03497357 100644 --- a/backend/src/Squidex/Config/Domain/LoggingServices.cs +++ b/backend/src/Squidex/Config/Domain/LoggingServices.cs @@ -7,8 +7,6 @@ #define LOG_ALL_IDENTITY_SERVER_NONE -using Squidex.Domain.Apps.Entities; -using Squidex.Domain.Apps.Entities.Apps; using Squidex.Infrastructure.Log; using Squidex.Log; using Squidex.Web.Pipeline; @@ -43,12 +41,6 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); - - services.AddSingletonAs() - .As().As(); - - services.AddSingletonAs() - .AsOptional(); } private static void AddFilters(this ILoggingBuilder builder) diff --git a/backend/src/Squidex/Config/Domain/ResizeServices.cs b/backend/src/Squidex/Config/Domain/ResizeServices.cs new file mode 100644 index 000000000..dd456814e --- /dev/null +++ b/backend/src/Squidex/Config/Domain/ResizeServices.cs @@ -0,0 +1,45 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Assets; +using Squidex.Assets.ImageMagick; +using Squidex.Assets.ImageSharp; +using Squidex.Assets.Remote; + +namespace Squidex.Config.Domain +{ + public static class ResizeServices + { + public static void AddSquidexImageResizing(this IServiceCollection services, IConfiguration config) + { + var thumbnailGenerator = new CompositeThumbnailGenerator( + new IAssetThumbnailGenerator[] + { + new ImageSharpThumbnailGenerator(), + new ImageMagickThumbnailGenerator() + }); + + var resizerUrl = config.GetValue("assets:resizerUrl"); + + if (!string.IsNullOrWhiteSpace(resizerUrl)) + { + services.AddHttpClient("Resize", options => + { + options.BaseAddress = new Uri(resizerUrl); + }); + + services.AddSingletonAs(c => new RemoteThumbnailGenerator(c.GetRequiredService(), thumbnailGenerator)) + .As(); + } + else + { + services.AddSingletonAs(c => thumbnailGenerator) + .As(); + } + } + } +} diff --git a/backend/src/Squidex/Config/Domain/RuleServices.cs b/backend/src/Squidex/Config/Domain/RuleServices.cs index b2389fce7..539bd4294 100644 --- a/backend/src/Squidex/Config/Domain/RuleServices.cs +++ b/backend/src/Squidex/Config/Domain/RuleServices.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Squidex.Areas.Api.Controllers.Rules.Models; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules.Extensions; using Squidex.Domain.Apps.Core.Scripting; @@ -102,6 +103,11 @@ namespace Squidex.Config.Domain services.AddSingletonAs>() .AsSelf(); + + services.AddInitializer("Serializer (Rules)", registry => + { + RuleActionConverter.Mapping = registry.Actions.ToDictionary(x => x.Key, x => x.Value.Type); + }, -1); } } } diff --git a/backend/src/Squidex/Config/Domain/SerializationInitializer.cs b/backend/src/Squidex/Config/Domain/SerializationInitializer.cs deleted file mode 100644 index 310af8162..000000000 --- a/backend/src/Squidex/Config/Domain/SerializationInitializer.cs +++ /dev/null @@ -1,59 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Newtonsoft.Json; -using Squidex.Areas.Api.Controllers.Rules.Models; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Hosting; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.MongoDb; -using Squidex.Infrastructure.Orleans; - -namespace Squidex.Config.Domain -{ - public sealed class SerializationInitializer : IInitializable - { - private readonly JsonSerializer jsonNetSerializer; - private readonly IJsonSerializer jsonSerializer; - private readonly RuleRegistry ruleRegistry; - - public int Order => -1; - - public SerializationInitializer(JsonSerializer jsonNetSerializer, IJsonSerializer jsonSerializer, RuleRegistry ruleRegistry) - { - this.jsonNetSerializer = jsonNetSerializer; - this.jsonSerializer = jsonSerializer; - - this.ruleRegistry = ruleRegistry; - } - - public Task InitializeAsync( - CancellationToken ct) - { - SetupBson(); - SetupOrleans(); - SetupActions(); - - return Task.CompletedTask; - } - - private void SetupActions() - { - RuleActionConverter.Mapping = ruleRegistry.Actions.ToDictionary(x => x.Key, x => x.Value.Type); - } - - private void SetupBson() - { - BsonJsonConvention.Register(jsonNetSerializer); - } - - private void SetupOrleans() - { - J.DefaultSerializer = jsonSerializer; - } - } -} diff --git a/backend/src/Squidex/Config/Domain/SerializationServices.cs b/backend/src/Squidex/Config/Domain/SerializationServices.cs index b507e1914..f88e3e59f 100644 --- a/backend/src/Squidex/Config/Domain/SerializationServices.cs +++ b/backend/src/Squidex/Config/Domain/SerializationServices.cs @@ -35,8 +35,9 @@ namespace Squidex.Config.Domain { public static class SerializationServices { - private static JsonSerializerSettings ConfigureJson(JsonSerializerSettings settings, TypeNameHandling typeNameHandling) + private static JsonSerializerSettings ConfigureJson(TypeNameHandling typeNameHandling, JsonSerializerSettings? settings = null) { + settings ??= new JsonSerializerSettings(); settings.Converters.Add(new StringEnumConverter()); settings.ContractResolver = new ConverterContractResolver( @@ -86,9 +87,6 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); - services.AddSingletonAs() - .AsSelf(); - services.AddSingletonAs() .AsSelf(); @@ -97,7 +95,7 @@ namespace Squidex.Config.Domain services.AddSingletonAs(c => { - var serializerSettings = ConfigureJson(new JsonSerializerSettings(), TypeNameHandling.Auto); + var serializerSettings = ConfigureJson(TypeNameHandling.Auto, new JsonSerializerSettings()); var typeNameRegistry = c.GetService(); @@ -118,7 +116,7 @@ namespace Squidex.Config.Domain { options.AllowInputFormatterExceptionMessages = false; - ConfigureJson(options.SerializerSettings, TypeNameHandling.None); + ConfigureJson(TypeNameHandling.None, options.SerializerSettings); }); return builder; @@ -128,9 +126,7 @@ namespace Squidex.Config.Domain { builder.Services.AddSingleton(c => { - var settings = ConfigureJson(new JsonSerializerSettings(), TypeNameHandling.None); - - var serializer = new NewtonsoftJsonSerializer(settings); + var serializer = new NewtonsoftJsonSerializer(ConfigureJson(TypeNameHandling.None)); return new DefaultDocumentWriter(serializer); }); diff --git a/backend/src/Squidex/Config/Domain/StoreServices.cs b/backend/src/Squidex/Config/Domain/StoreServices.cs index 7716dda5b..831a71d32 100644 --- a/backend/src/Squidex/Config/Domain/StoreServices.cs +++ b/backend/src/Squidex/Config/Domain/StoreServices.cs @@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Identity; using Migrations.Migrations.MongoDb; using MongoDB.Driver; +using Newtonsoft.Json; using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Apps.DomainObject; using Squidex.Domain.Apps.Entities.Apps.Repositories; @@ -38,6 +39,7 @@ using Squidex.Infrastructure.Diagnostics; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Migrations; +using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.States; using Squidex.Infrastructure.UsageTracking; @@ -160,12 +162,19 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .AsOptional().As(); } + + services.AddInitializer("Serializer (BSON)", jsonNetSerializer => + { + BsonJsonConvention.Register(jsonNetSerializer); + }, -1); } }); - services.AddSingleton(typeof(IStore<>), typeof(Store<>)); + services.AddSingleton(typeof(IStore<>), + typeof(Store<>)); - services.AddSingleton(typeof(IPersistenceFactory<>), typeof(Store<>)); + services.AddSingleton(typeof(IPersistenceFactory<>), + typeof(Store<>)); } private static IMongoClient GetClient(string configuration) diff --git a/backend/src/Squidex/Config/Orleans/OrleansServices.cs b/backend/src/Squidex/Config/Orleans/OrleansServices.cs index 02fa456bb..6ada3e54b 100644 --- a/backend/src/Squidex/Config/Orleans/OrleansServices.cs +++ b/backend/src/Squidex/Config/Orleans/OrleansServices.cs @@ -17,6 +17,7 @@ using OrleansDashboard; using Squidex.Domain.Apps.Entities; using Squidex.Hosting.Configuration; using Squidex.Infrastructure; +using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Orleans; using Squidex.Web; @@ -28,18 +29,23 @@ namespace Squidex.Config.Orleans { builder.AddOrleansPubSub(); - builder.ConfigureServices(siloServices => + builder.ConfigureServices(services => { - siloServices.AddSingletonAs() + services.AddScoped(typeof(IGrainState<>), typeof(Infrastructure.Orleans.GrainState<>)); + + services.AddSingletonAs() .As(); - siloServices.AddSingletonAs() + services.AddSingletonAs() .As(); - siloServices.AddScopedAs() + services.AddScopedAs() .As(); - siloServices.AddScoped(typeof(IGrainState<>), typeof(Infrastructure.Orleans.GrainState<>)); + services.AddInitializer("Serializer (Orleans)", serializer => + { + J.DefaultSerializer = serializer; + }, -1); }); builder.ConfigureApplicationParts(parts => diff --git a/backend/src/Squidex/Config/Web/WebExtensions.cs b/backend/src/Squidex/Config/Web/WebExtensions.cs index 7b362c930..27225cc18 100644 --- a/backend/src/Squidex/Config/Web/WebExtensions.cs +++ b/backend/src/Squidex/Config/Web/WebExtensions.cs @@ -44,15 +44,27 @@ namespace Squidex.Config.Web return app; } - public static IApplicationBuilder UseSquidexTracking(this IApplicationBuilder app) + public static IApplicationBuilder UseSquidexLogging(this IApplicationBuilder app) { - app.UseMiddleware(); app.UseMiddleware(); + + return app; + } + + public static IApplicationBuilder UseSquidexUsage(this IApplicationBuilder app) + { app.UseMiddleware(); return app; } + public static IApplicationBuilder UseSquidexExceptionHandling(this IApplicationBuilder app) + { + app.UseMiddleware(); + + return app; + } + public static IApplicationBuilder UseSquidexHealthCheck(this IApplicationBuilder app) { var serializer = app.ApplicationServices.GetRequiredService(); diff --git a/backend/src/Squidex/Config/Web/WebServices.cs b/backend/src/Squidex/Config/Web/WebServices.cs index 23edaef7f..33a7be3ac 100644 --- a/backend/src/Squidex/Config/Web/WebServices.cs +++ b/backend/src/Squidex/Config/Web/WebServices.cs @@ -27,9 +27,6 @@ namespace Squidex.Config.Web { public static void AddSquidexMvcWithPlugins(this IServiceCollection services, IConfiguration config) { - services.AddDefaultWebServices(config); - services.AddDefaultForwardRules(); - services.AddSingletonAs(c => new ExposedValues(c.GetRequiredService>().Value, config, typeof(WebServices).Assembly)) .AsSelf(); diff --git a/backend/src/Squidex/Squidex.csproj b/backend/src/Squidex/Squidex.csproj index f55a965cf..be2c518ed 100644 --- a/backend/src/Squidex/Squidex.csproj +++ b/backend/src/Squidex/Squidex.csproj @@ -72,14 +72,14 @@ - - - - - + + + + + - + diff --git a/backend/src/Squidex/Startup.cs b/backend/src/Squidex/Startup.cs index 4823d21a5..224d63364 100644 --- a/backend/src/Squidex/Startup.cs +++ b/backend/src/Squidex/Startup.cs @@ -33,12 +33,16 @@ namespace Squidex { services.AddHttpClient(); services.AddMemoryCache(); + services.AddHealthChecks(); services.AddNonBreakingSameSiteCookies(); + services.AddDefaultWebServices(config); + services.AddDefaultForwardRules(); + services.AddSquidexImageResizing(config); + services.AddSquidexAssetInfrastructure(config); + services.AddSquidexSerializers(); services.AddSquidexMvcWithPlugins(config); - services.AddSquidexApps(config); - services.AddSquidexAssetInfrastructure(config); services.AddSquidexAssets(config); services.AddSquidexAuthentication(config); services.AddSquidexBackups(); @@ -62,7 +66,6 @@ namespace Squidex services.AddSquidexRules(config); services.AddSquidexSchemas(); services.AddSquidexSearch(); - services.AddSquidexSerializers(); services.AddSquidexStoreServices(config); services.AddSquidexSubscriptions(config); services.AddSquidexTelemetry(config); @@ -77,10 +80,12 @@ namespace Squidex app.UseDefaultPathBase(); app.UseDefaultForwardRules(); - app.UseSquidexCacheKeys(); app.UseSquidexHealthCheck(); app.UseSquidexRobotsTxt(); - app.UseSquidexTracking(); + app.UseSquidexCacheKeys(); + app.UseSquidexExceptionHandling(); + app.UseSquidexUsage(); + app.UseSquidexLogging(); app.UseSquidexLocalization(); app.UseSquidexLocalCache(); app.UseSquidexCors(); diff --git a/backend/src/Squidex/appsettings.json b/backend/src/Squidex/appsettings.json index 4d1538ab7..e35b1cf3a 100644 --- a/backend/src/Squidex/appsettings.json +++ b/backend/src/Squidex/appsettings.json @@ -254,7 +254,10 @@ // Create one folder per app. // // WARNING: If you change this parameter, previous assets are not available anymore. - "folderPerApp": false + "folderPerApp": false, + + // Points to another Squidex instance, which should be configured as resizer. + "resizerUrl": "" }, "logging": { diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppCommandMiddlewareTests.cs index 12d723367..0a18ddf28 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppCommandMiddlewareTests.cs @@ -63,7 +63,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject var file = new NoopAssetFile(); A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(A._, file.MimeType, default)) - .Returns(new ImageInfo(100, 100, false, ImageFormat.PNG)); + .Returns(new ImageInfo(100, 100, ImageOrientation.None, ImageFormat.PNG)); await HandleAsync(new UploadAppImage { File = file }, None.Value); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageAssetMetadataSourceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageAssetMetadataSourceTests.cs index 5ae393426..0942d8555 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageAssetMetadataSourceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageAssetMetadataSourceTests.cs @@ -55,7 +55,7 @@ namespace Squidex.Domain.Apps.Entities.Assets var command = new CreateAsset { File = file }; A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream, file.MimeType, default)) - .Returns(new ImageInfo(800, 600, false, ImageFormat.PNG)); + .Returns(new ImageInfo(800, 600, ImageOrientation.None, ImageFormat.PNG)); await sut.EnhanceAsync(command); @@ -72,11 +72,11 @@ namespace Squidex.Domain.Apps.Entities.Assets { var command = new CreateAsset { File = file }; - A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream, file.MimeType, default)) - .Returns(new ImageInfo(600, 800, true, ImageFormat.PNG)); + A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(A._, file.MimeType, default)) + .Returns(new ImageInfo(800, 600, ImageOrientation.None, ImageFormat.PNG)); - A.CallTo(() => assetThumbnailGenerator.FixOrientationAsync(stream, file.MimeType, A._, default)) - .Returns(new ImageInfo(800, 600, true, ImageFormat.PNG)); + A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream, file.MimeType, default)) + .Returns(new ImageInfo(600, 800, ImageOrientation.BottomRight, ImageFormat.PNG)).Once(); await sut.EnhanceAsync(command); diff --git a/backend/tests/docker-compose.yml b/backend/tests/docker-compose.yml index ff834456a..f343019e8 100644 --- a/backend/tests/docker-compose.yml +++ b/backend/tests/docker-compose.yml @@ -21,6 +21,16 @@ services: - SCRIPTING__TIMEOUTSCRIPT=00:00:10 - SCRIPTING__TIMEOUTEXECUTION=00:00:10 - GRAPHQL__CACHEDURATION=0 + - ASSETS_RESIZERURL=http://resizer:8081 + networks: + - internal + depends_on: + - mongo + + resizer: + image: squidex/resizer:dev-2 + ports: + - "8081:80" networks: - internal depends_on: