Browse Source

Feature/resize server2 (#815)

* Configure Squidex as resize server.

* Make tests with resize server.

* Temp

* User image controller.
pull/816/head
Sebastian Stehle 4 years ago
committed by GitHub
parent
commit
4a948a00ac
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 48
      backend/src/Squidex.Domain.Apps.Entities/Assets/ImageAssetMetadataSource.cs
  2. 1
      backend/src/Squidex.Infrastructure/Orleans/LoggingFilter.cs
  3. 4
      backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
  4. 23
      backend/src/Squidex.Web/Pipeline/ActionContextLogAppender.cs
  5. 162
      backend/src/Squidex/Areas/Api/Controllers/Apps/AppImageController.cs
  6. 114
      backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs
  7. 2
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs
  8. 116
      backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs
  9. 14
      backend/src/Squidex/Areas/Api/Startup.cs
  10. 58
      backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs
  11. 14
      backend/src/Squidex/Areas/IdentityServer/Startup.cs
  12. 10
      backend/src/Squidex/Areas/OrleansDashboard/Startup.cs
  13. 10
      backend/src/Squidex/Areas/Portal/Startup.cs
  14. 4
      backend/src/Squidex/Config/Domain/AppsServices.cs
  15. 12
      backend/src/Squidex/Config/Domain/AssetServices.cs
  16. 4
      backend/src/Squidex/Config/Domain/InfrastructureServices.cs
  17. 8
      backend/src/Squidex/Config/Domain/LoggingServices.cs
  18. 45
      backend/src/Squidex/Config/Domain/ResizeServices.cs
  19. 6
      backend/src/Squidex/Config/Domain/RuleServices.cs
  20. 59
      backend/src/Squidex/Config/Domain/SerializationInitializer.cs
  21. 14
      backend/src/Squidex/Config/Domain/SerializationServices.cs
  22. 13
      backend/src/Squidex/Config/Domain/StoreServices.cs
  23. 16
      backend/src/Squidex/Config/Orleans/OrleansServices.cs
  24. 16
      backend/src/Squidex/Config/Web/WebExtensions.cs
  25. 3
      backend/src/Squidex/Config/Web/WebServices.cs
  26. 12
      backend/src/Squidex/Squidex.csproj
  27. 15
      backend/src/Squidex/Startup.cs
  28. 5
      backend/src/Squidex/appsettings.json
  29. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppCommandMiddlewareTests.cs
  30. 10
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageAssetMetadataSourceTests.cs
  31. 10
      backend/tests/docker-compose.yml

48
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;
}

1
backend/src/Squidex.Infrastructure/Orleans/LoggingFilter.cs

@ -6,7 +6,6 @@
// ==========================================================================
using Orleans;
using Squidex.Infrastructure.States;
using Squidex.Log;
namespace Squidex.Infrastructure.Orleans

4
backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj

@ -31,9 +31,9 @@
<PackageReference Include="NJsonSchema" Version="10.6.4" />
<PackageReference Include="OpenTelemetry.Api" Version="1.1.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.Assets" Version="2.1.0" />
<PackageReference Include="Squidex.Assets" Version="2.4.0" />
<PackageReference Include="Squidex.Caching" Version="1.8.0" />
<PackageReference Include="Squidex.Hosting.Abstractions" Version="2.3.0" />
<PackageReference Include="Squidex.Hosting.Abstractions" Version="2.4.0" />
<PackageReference Include="Squidex.Log" Version="1.6.0" />
<PackageReference Include="Squidex.Text" Version="1.7.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />

23
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<IHttpContextAccessor>();
if (string.IsNullOrEmpty(httpContextAccessor?.HttpContext?.Request?.Method))
{
return;
}
var actionContext = services.GetRequiredService<IActionContextAccessor>()?.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)
{

162
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
{
/// <summary>
/// Manages and configures apps.
/// </summary>
[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;
}
/// <summary>
/// Get the app image.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <returns>
/// 200 => App image found and content or (resized) image returned.
/// 404 => App not found.
/// </returns>
[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
}
}
}

114
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;
}
/// <summary>
@ -185,84 +166,6 @@ namespace Squidex.Areas.Api.Controllers.Apps
return Ok(response);
}
/// <summary>
/// Get the app image.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <returns>
/// 200 => App image found and content or (resized) image returned.
/// 404 => App not found.
/// </returns>
[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
}
/// <summary>
/// Remove the app image.
/// </summary>
@ -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);
}
}
}

2
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<AppsController>(x => nameof(x.GetImage), values));
AddGetLink("image", resources.Url<AppImageController>(x => nameof(x.GetImage), values));
}
if (isContributor)

116
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
}
}
}

14
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();
});

58
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<IActionResult> MakeChangeAsync<TModel>(Func<string, Task> action, string successMessage, TModel? model = null) where TModel : class

14
backend/src/Squidex/Areas/IdentityServer/Startup.cs

@ -15,23 +15,23 @@ namespace Squidex.Areas.IdentityServer
{
var environment = app.ApplicationServices.GetRequiredService<IWebHostEnvironment>();
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();
});

10
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<OrleansDashboardAuthenticationMiddleware>();
orleansApp.UseOrleansDashboard();
builder.UseMiddleware<OrleansDashboardAuthenticationMiddleware>();
builder.UseOrleansDashboard();
});
}
}

10
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<PortalDashboardAuthenticationMiddleware>();
portalApp.UseMiddleware<PortalRedirectMiddleware>();
builder.UseMiddleware<PortalDashboardAuthenticationMiddleware>();
builder.UseMiddleware<PortalRedirectMiddleware>();
});
}
}

4
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<AppUsageDeleter>()
.As<IDeleter>();
services.AddSingletonAs<DefaultAppLogStore>()
.As<IAppLogStore>().As<IDeleter>();
services.AddSingletonAs<AppHistoryEventsCreator>()
.As<IHistoryEventsCreator>();

12
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<IAssetThumbnailGenerator>();
services.AddSingletonAs(c => new DelegateInitializer(
c.GetRequiredService<IAssetStore>().GetType().Name,
c.GetRequiredService<IAssetStore>().InitializeAsync))

4
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<GrainBootstrap<IEventConsumerManagerGrain>>()
.AsSelf();
services.AddSingletonAs<BackgroundRequestLogStore>()
.AsOptional<IRequestLogStore>();
services.AddSingletonAs<JintScriptEngine>()
.As<IScriptEngine>();

8
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<ActionContextLogAppender>()
.As<ILogAppender>();
services.AddSingletonAs<DefaultAppLogStore>()
.As<IAppLogStore>().As<IDeleter>();
services.AddSingletonAs<BackgroundRequestLogStore>()
.AsOptional<IRequestLogStore>();
}
private static void AddFilters(this ILoggingBuilder builder)

45
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<string>("assets:resizerUrl");
if (!string.IsNullOrWhiteSpace(resizerUrl))
{
services.AddHttpClient("Resize", options =>
{
options.BaseAddress = new Uri(resizerUrl);
});
services.AddSingletonAs(c => new RemoteThumbnailGenerator(c.GetRequiredService<IHttpClientFactory>(), thumbnailGenerator))
.As<IAssetThumbnailGenerator>();
}
else
{
services.AddSingletonAs(c => thumbnailGenerator)
.As<IAssetThumbnailGenerator>();
}
}
}
}

6
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<GrainBootstrap<IRuleDequeuerGrain>>()
.AsSelf();
services.AddInitializer<RuleRegistry>("Serializer (Rules)", registry =>
{
RuleActionConverter.Mapping = registry.Actions.ToDictionary(x => x.Key, x => x.Value.Type);
}, -1);
}
}
}

59
backend/src/Squidex/Config/Domain/SerializationInitializer.cs

@ -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;
}
}
}

14
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<NewtonsoftJsonSerializer>()
.As<IJsonSerializer>();
services.AddSingletonAs<SerializationInitializer>()
.AsSelf();
services.AddSingletonAs<TypeNameRegistry>()
.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<TypeNameRegistry>();
@ -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<IDocumentWriter>(c =>
{
var settings = ConfigureJson(new JsonSerializerSettings(), TypeNameHandling.None);
var serializer = new NewtonsoftJsonSerializer(settings);
var serializer = new NewtonsoftJsonSerializer(ConfigureJson(TypeNameHandling.None));
return new DefaultDocumentWriter(serializer);
});

13
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<MongoTextIndex>()
.AsOptional<ITextIndex>().As<IDeleter>();
}
services.AddInitializer<JsonSerializer>("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)

16
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<DefaultMongoClientFactory>()
services.AddScoped(typeof(IGrainState<>), typeof(Infrastructure.Orleans.GrainState<>));
services.AddSingletonAs<DefaultMongoClientFactory>()
.As<IMongoClientFactory>();
siloServices.AddSingletonAs<ActivationLimiter>()
services.AddSingletonAs<ActivationLimiter>()
.As<IActivationLimiter>();
siloServices.AddScopedAs<ActivationLimit>()
services.AddScopedAs<ActivationLimit>()
.As<IActivationLimit>();
siloServices.AddScoped(typeof(IGrainState<>), typeof(Infrastructure.Orleans.GrainState<>));
services.AddInitializer<IJsonSerializer>("Serializer (Orleans)", serializer =>
{
J.DefaultSerializer = serializer;
}, -1);
});
builder.ConfigureApplicationParts(parts =>

16
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<RequestExceptionMiddleware>();
app.UseMiddleware<RequestLogPerformanceMiddleware>();
return app;
}
public static IApplicationBuilder UseSquidexUsage(this IApplicationBuilder app)
{
app.UseMiddleware<UsageMiddleware>();
return app;
}
public static IApplicationBuilder UseSquidexExceptionHandling(this IApplicationBuilder app)
{
app.UseMiddleware<RequestExceptionMiddleware>();
return app;
}
public static IApplicationBuilder UseSquidexHealthCheck(this IApplicationBuilder app)
{
var serializer = app.ApplicationServices.GetRequiredService<IJsonSerializer>();

3
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<IOptions<ExposedConfiguration>>().Value, config, typeof(WebServices).Assembly))
.AsSelf();

12
backend/src/Squidex/Squidex.csproj

@ -72,14 +72,14 @@
<PackageReference Include="OrleansDashboard.EmbeddedAssets" Version="3.6.1" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="ReportGenerator" Version="5.0.0" PrivateAssets="all" />
<PackageReference Include="Squidex.Assets.Azure" Version="2.1.0" />
<PackageReference Include="Squidex.Assets.GoogleCloud" Version="2.1.0" />
<PackageReference Include="Squidex.Assets.FTP" Version="2.1.0" />
<PackageReference Include="Squidex.Assets.Mongo" Version="2.1.0" />
<PackageReference Include="Squidex.Assets.S3" Version="2.1.0" />
<PackageReference Include="Squidex.Assets.Azure" Version="2.4.0" />
<PackageReference Include="Squidex.Assets.GoogleCloud" Version="2.4.0" />
<PackageReference Include="Squidex.Assets.FTP" Version="2.4.0" />
<PackageReference Include="Squidex.Assets.Mongo" Version="2.4.0" />
<PackageReference Include="Squidex.Assets.S3" Version="2.4.0" />
<PackageReference Include="Squidex.Caching.Orleans" Version="1.8.0" />
<PackageReference Include="Squidex.ClientLibrary" Version="7.7.0" />
<PackageReference Include="Squidex.Hosting" Version="2.3.0" />
<PackageReference Include="Squidex.Hosting" Version="2.4.0" />
<PackageReference Include="Squidex.OpenIddict.MongoDb" Version="4.0.1-dev" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Linq" Version="4.3.0" />

15
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();

5
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": {

2
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<Stream>._, 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);

10
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<Stream>._, file.MimeType, default))
.Returns(new ImageInfo(800, 600, ImageOrientation.None, ImageFormat.PNG));
A.CallTo(() => assetThumbnailGenerator.FixOrientationAsync(stream, file.MimeType, A<Stream>._, 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);

10
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:

Loading…
Cancel
Save