Browse Source

Tus support. (#839)

* Tus support.

* Save file.

* Fix registration.

* Fix local cache usage.

* Make assert more flexible.

* Cleanup

* Another cleanup.

* better tests.

* Fix tests?

* Update packages.

* Easier tests?

* Upload packages.

* Another attempt.

* Check for exception.

* Fix test
pull/840/head
Sebastian Stehle 5 years ago
committed by GitHub
parent
commit
23beef2904
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj
  2. 2
      backend/src/Migrations/Migrations.csproj
  3. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj
  4. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj
  5. 2
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj
  6. 85
      backend/src/Squidex.Domain.Apps.Entities/AppProvider.cs
  7. 40
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCleanupGrain.cs
  8. 15
      backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetCleanupGrain.cs
  9. 9
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLResolver.cs
  10. 53
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs
  11. 12
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetGraphType.cs
  12. 3
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs
  13. 5
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentResolvers.cs
  14. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Resolvers.cs
  15. 43
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs
  16. 3
      backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj
  17. 2
      backend/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj
  18. 2
      backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj
  19. 2
      backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj
  20. 2
      backend/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj
  21. 4
      backend/src/Squidex.Infrastructure.MongoDb/Diagnostics/MongoHealthCheck.cs
  22. 2
      backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj
  23. 2
      backend/src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.csproj
  24. 6
      backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
  25. 2
      backend/src/Squidex.Shared/Squidex.Shared.csproj
  26. 2
      backend/src/Squidex.Web/Squidex.Web.csproj
  27. 80
      backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
  28. 22
      backend/src/Squidex/Areas/Api/Controllers/Assets/Models/CreateAssetDto.cs
  29. 27
      backend/src/Squidex/Config/Domain/AssetServices.cs
  30. 2
      backend/src/Squidex/Config/Domain/ResizeServices.cs
  31. 18
      backend/src/Squidex/Config/Domain/StoreServices.cs
  32. 19
      backend/src/Squidex/Squidex.csproj
  33. 2
      backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj
  34. 39
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCleanupGrainTests.cs
  35. 4
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj
  36. 2
      backend/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj
  37. 2
      backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj
  38. 2
      backend/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj
  39. 1
      backend/tools/TestSuite/TestSuite.ApiTests/AppWorkflowsTest.cs
  40. 1
      backend/tools/TestSuite/TestSuite.ApiTests/AssetFormatTests.cs
  41. 305
      backend/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs
  42. 5
      backend/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj
  43. 2
      backend/tools/TestSuite/TestSuite.LoadTests/TestSuite.LoadTests.csproj
  44. 2
      backend/tools/TestSuite/TestSuite.Shared/Model/TestEntity.cs
  45. 2
      backend/tools/TestSuite/TestSuite.Shared/Model/TestEntityWithReferences.cs
  46. 4
      backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj
  47. 4
      backend/tools/TestSuite/TestSuite.sln

2
backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj

@ -18,7 +18,7 @@
<PackageReference Include="Elasticsearch.Net" Version="7.17.0" /> <PackageReference Include="Elasticsearch.Net" Version="7.17.0" />
<PackageReference Include="Google.Cloud.Diagnostics.Common" Version="4.4.0" /> <PackageReference Include="Google.Cloud.Diagnostics.Common" Version="4.4.0" />
<PackageReference Include="Google.Cloud.Logging.V2" Version="3.4.0" /> <PackageReference Include="Google.Cloud.Logging.V2" Version="3.4.0" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.689"> <PackageReference Include="Meziantou.Analyzer" Version="1.0.694">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

2
backend/src/Migrations/Migrations.csproj

@ -6,7 +6,7 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="1.0.689"> <PackageReference Include="Meziantou.Analyzer" Version="1.0.694">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

2
backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj

@ -12,7 +12,7 @@
<DebugSymbols>True</DebugSymbols> <DebugSymbols>True</DebugSymbols>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="1.0.689"> <PackageReference Include="Meziantou.Analyzer" Version="1.0.694">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

2
backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj

@ -21,7 +21,7 @@
<PackageReference Include="Fluid.Core.Squidex" Version="1.0.0-beta" /> <PackageReference Include="Fluid.Core.Squidex" Version="1.0.0-beta" />
<PackageReference Include="GeoJSON.Net" Version="1.2.19" /> <PackageReference Include="GeoJSON.Net" Version="1.2.19" />
<PackageReference Include="Jint" Version="3.0.0-beta-2036" /> <PackageReference Include="Jint" Version="3.0.0-beta-2036" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.689"> <PackageReference Include="Meziantou.Analyzer" Version="1.0.694">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

2
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj

@ -19,7 +19,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Lucene.Net.QueryParser" Version="4.8.0-beta00015" /> <PackageReference Include="Lucene.Net.QueryParser" Version="4.8.0-beta00015" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.689"> <PackageReference Include="Meziantou.Analyzer" Version="1.0.694">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

85
backend/src/Squidex.Domain.Apps.Entities/AppProvider.cs

@ -58,16 +58,13 @@ namespace Squidex.Domain.Apps.Entities
{ {
var cacheKey = AppCacheKey(appId); var cacheKey = AppCacheKey(appId);
if (localCache.TryGetValue(cacheKey, out var cached) && cached is IAppEntity found) var app = await GetOrCreate(cacheKey, () =>
{ {
return found; return indexForApps.GetAppAsync(appId, canCache, ct);
} });
var app = await indexForApps.GetAppAsync(appId, canCache, ct);
if (app != null) if (app != null)
{ {
localCache.Add(cacheKey, app);
localCache.Add(AppCacheKey(app.Name), app); localCache.Add(AppCacheKey(app.Name), app);
} }
@ -79,16 +76,13 @@ namespace Squidex.Domain.Apps.Entities
{ {
var cacheKey = AppCacheKey(appName); var cacheKey = AppCacheKey(appName);
if (localCache.TryGetValue(cacheKey, out var cached) && cached is IAppEntity found) var app = await GetOrCreate(cacheKey, () =>
{ {
return found; return indexForApps.GetAppAsync(appName, canCache, ct);
} });
var app = await indexForApps.GetAppAsync(appName, canCache, ct);
if (app != null) if (app != null)
{ {
localCache.Add(cacheKey, app);
localCache.Add(AppCacheKey(app.Id), app); localCache.Add(AppCacheKey(app.Id), app);
} }
@ -100,16 +94,13 @@ namespace Squidex.Domain.Apps.Entities
{ {
var cacheKey = SchemaCacheKey(appId, name); var cacheKey = SchemaCacheKey(appId, name);
if (localCache.TryGetValue(cacheKey, out var cached) && cached is ISchemaEntity found) var schema = await GetOrCreate(cacheKey, () =>
{ {
return found; return indexForSchemas.GetSchemaAsync(appId, name, canCache, ct);
} });
var schema = await indexForSchemas.GetSchemaAsync(appId, name, canCache, ct);
if (schema != null) if (schema != null)
{ {
localCache.Add(cacheKey, schema);
localCache.Add(SchemaCacheKey(appId, schema.Id), schema); localCache.Add(SchemaCacheKey(appId, schema.Id), schema);
} }
@ -121,16 +112,13 @@ namespace Squidex.Domain.Apps.Entities
{ {
var cacheKey = SchemaCacheKey(appId, id); var cacheKey = SchemaCacheKey(appId, id);
if (localCache.TryGetValue(cacheKey, out var cached) && cached is ISchemaEntity found) var schema = await GetOrCreate(cacheKey, () =>
{ {
return found; return indexForSchemas.GetSchemaAsync(appId, id, canCache, ct);
} });
var schema = await indexForSchemas.GetSchemaAsync(appId, id, canCache, ct);
if (schema != null) if (schema != null)
{ {
localCache.Add(cacheKey, schema);
localCache.Add(SchemaCacheKey(appId, schema.SchemaDef.Name), schema); localCache.Add(SchemaCacheKey(appId, schema.SchemaDef.Name), schema);
} }
@ -140,40 +128,43 @@ namespace Squidex.Domain.Apps.Entities
public async Task<List<IAppEntity>> GetUserAppsAsync(string userId, PermissionSet permissions, public async Task<List<IAppEntity>> GetUserAppsAsync(string userId, PermissionSet permissions,
CancellationToken ct = default) CancellationToken ct = default)
{ {
var apps = await localCache.GetOrCreateAsync($"GetUserApps({userId})", () => var apps = await GetOrCreate($"GetUserApps({userId})", () =>
{ {
return indexForApps.GetAppsForUserAsync(userId, permissions, ct); return indexForApps.GetAppsForUserAsync(userId, permissions, ct)!;
}); });
return apps; return apps?.ToList() ?? new List<IAppEntity>();
} }
public async Task<List<ISchemaEntity>> GetSchemasAsync(DomainId appId, public async Task<List<ISchemaEntity>> GetSchemasAsync(DomainId appId,
CancellationToken ct = default) CancellationToken ct = default)
{ {
var schemas = await localCache.GetOrCreateAsync($"GetSchemasAsync({appId})", () => var schemas = await GetOrCreate($"GetSchemasAsync({appId})", () =>
{ {
return indexForSchemas.GetSchemasAsync(appId, ct); return indexForSchemas.GetSchemasAsync(appId, ct)!;
}); });
foreach (var schema in schemas) if (schemas != null)
{ {
localCache.Add(SchemaCacheKey(appId, schema.Id), schema); foreach (var schema in schemas)
localCache.Add(SchemaCacheKey(appId, schema.SchemaDef.Name), schema); {
localCache.Add(SchemaCacheKey(appId, schema.Id), schema);
localCache.Add(SchemaCacheKey(appId, schema.SchemaDef.Name), schema);
}
} }
return schemas; return schemas?.ToList() ?? new List<ISchemaEntity>();
} }
public async Task<List<IRuleEntity>> GetRulesAsync(DomainId appId, public async Task<List<IRuleEntity>> GetRulesAsync(DomainId appId,
CancellationToken ct = default) CancellationToken ct = default)
{ {
var rules = await localCache.GetOrCreateAsync($"GetRulesAsync({appId})", () => var rules = await GetOrCreate($"GetRulesAsync({appId})", () =>
{ {
return indexForRules.GetRulesAsync(appId, ct); return indexForRules.GetRulesAsync(appId, ct)!;
}); });
return rules.ToList(); return rules?.ToList() ?? new List<IRuleEntity>();
} }
public async Task<IRuleEntity?> GetRuleAsync(DomainId appId, DomainId id, public async Task<IRuleEntity?> GetRuleAsync(DomainId appId, DomainId id,
@ -184,6 +175,28 @@ namespace Squidex.Domain.Apps.Entities
return rules.Find(x => x.Id == id); return rules.Find(x => x.Id == id);
} }
public async Task<T?> GetOrCreate<T>(object key, Func<Task<T?>> creator) where T : class
{
if (localCache.TryGetValue(key, out var value))
{
switch (value)
{
case T typed:
return typed;
case Task<T?> typedTask:
return await typedTask;
default:
return null;
}
}
var result = creator();
localCache.Add(key, result);
return await result;
}
private static string AppCacheKey(DomainId appId) private static string AppCacheKey(DomainId appId)
{ {
return $"APPS_ID_{appId}"; return $"APPS_ID_{appId}";

40
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCleanupGrain.cs

@ -0,0 +1,40 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Orleans;
using Orleans.Runtime;
using tusdotnet.Interfaces;
namespace Squidex.Domain.Apps.Entities.Assets
{
public sealed class AssetCleanupGrain : Grain, IRemindable, IAssetCleanupGrain
{
private readonly ITusExpirationStore expirationStore;
public AssetCleanupGrain(ITusExpirationStore expirationStore)
{
this.expirationStore = expirationStore;
}
public override Task OnActivateAsync()
{
RegisterOrUpdateReminder("Cleanup", TimeSpan.Zero, TimeSpan.FromMinutes(10));
return base.OnActivateAsync();
}
public Task ActivateAsync()
{
return Task.CompletedTask;
}
public Task ReceiveReminder(string reminderName, TickStatus status)
{
return expirationStore.RemoveExpiredFilesAsync(default);
}
}
}

15
backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetCleanupGrain.cs

@ -0,0 +1,15 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure.Orleans;
namespace Squidex.Domain.Apps.Entities.Assets
{
public interface IAssetCleanupGrain : IBackgroundGrain
{
}
}

9
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLResolver.cs

@ -30,6 +30,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
private readonly ISchemasHash schemasHash; private readonly ISchemasHash schemasHash;
private readonly IServiceProvider serviceProvider; private readonly IServiceProvider serviceProvider;
private readonly GraphQLOptions options; private readonly GraphQLOptions options;
private readonly SharedTypes sharedTypes;
private sealed record CacheEntry(GraphQLSchema Model, string Hash, Instant Created); private sealed record CacheEntry(GraphQLSchema Model, string Hash, Instant Created);
@ -45,6 +46,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
this.schemasHash = schemasHash; this.schemasHash = schemasHash;
this.serviceProvider = serviceProvider; this.serviceProvider = serviceProvider;
this.options = options.Value; this.options = options.Value;
sharedTypes = serviceProvider.GetRequiredService<SharedTypes>();
} }
public async Task ConfigureAsync(ExecutionOptions executionOptions) public async Task ConfigureAsync(ExecutionOptions executionOptions)
@ -88,10 +91,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var hash = await schemasHash.ComputeHashAsync(app, schemas); var hash = await schemasHash.ComputeHashAsync(app, schemas);
return new CacheEntry( return new CacheEntry(new Builder(app, sharedTypes).BuildSchema(schemas),
new Builder(app, serviceProvider.GetRequiredService<SharedTypes>()).BuildSchema(schemas), hash, SystemClock.Instance.GetCurrentInstant());
hash,
SystemClock.Instance.GetCurrentInstant());
} }
private static object CreateCacheKey(DomainId appId, string etag) private static object CreateCacheKey(DomainId appId, string etag)

53
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs

@ -6,13 +6,10 @@
// ========================================================================== // ==========================================================================
using GraphQL.DataLoader; using GraphQL.DataLoader;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Contents.Queries; using Squidex.Domain.Apps.Entities.Contents.Queries;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
using Squidex.Log;
using Squidex.Shared.Users; using Squidex.Shared.Users;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
@ -22,37 +19,17 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
private static readonly List<IEnrichedAssetEntity> EmptyAssets = new List<IEnrichedAssetEntity>(); private static readonly List<IEnrichedAssetEntity> EmptyAssets = new List<IEnrichedAssetEntity>();
private static readonly List<IEnrichedContentEntity> EmptyContents = new List<IEnrichedContentEntity>(); private static readonly List<IEnrichedContentEntity> EmptyContents = new List<IEnrichedContentEntity>();
private readonly IDataLoaderContextAccessor dataLoaders; private readonly IDataLoaderContextAccessor dataLoaders;
private readonly IUserResolver userResolver;
public IUrlGenerator UrlGenerator { get; }
public ICommandBus CommandBus { get; }
public ISemanticLog Log { get; }
public override Context Context { get; } public override Context Context { get; }
public GraphQLExecutionContext(IAssetQueryService assetQuery, IContentQueryService contentQuery, public GraphQLExecutionContext(IServiceProvider serviceProvider, IDataLoaderContextAccessor dataLoaders, Context context)
Context context, : base(serviceProvider)
IDataLoaderContextAccessor dataLoaders,
ICommandBus commandBus,
IUrlGenerator urlGenerator,
IUserResolver userResolver,
ISemanticLog log)
: base(assetQuery, contentQuery)
{ {
this.dataLoaders = dataLoaders; this.dataLoaders = dataLoaders;
this.userResolver = userResolver;
CommandBus = commandBus;
UrlGenerator = urlGenerator;
Context = context.Clone(b => b Context = context.Clone(b => b
.WithoutCleanup() .WithoutCleanup()
.WithoutContentEnrichment()); .WithoutContentEnrichment());
Log = log;
} }
public async Task<IUser?> FindUserAsync(RefToken refToken, public async Task<IUser?> FindUserAsync(RefToken refToken,
@ -93,34 +70,38 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
return content; return content;
} }
public Task<IReadOnlyList<IEnrichedAssetEntity>> GetReferencedAssetsAsync(IJsonValue value, public async Task<IReadOnlyList<IEnrichedAssetEntity>> GetReferencedAssetsAsync(IJsonValue value,
CancellationToken ct) CancellationToken ct)
{ {
var ids = ParseIds(value); var ids = ParseIds(value);
if (ids == null) if (ids == null)
{ {
return Task.FromResult<IReadOnlyList<IEnrichedAssetEntity>>(EmptyAssets); return EmptyAssets;
} }
var dataLoader = GetAssetsLoader(); var dataLoader = GetAssetsLoader();
return LoadManyAsync(dataLoader, ids, ct); var result = await dataLoader.LoadAsync(ids).GetResultAsync(ct);
return result?.NotNull().ToList() ?? EmptyAssets;
} }
public Task<IReadOnlyList<IEnrichedContentEntity>> GetReferencedContentsAsync(IJsonValue value, public async Task<IReadOnlyList<IEnrichedContentEntity>> GetReferencedContentsAsync(IJsonValue value,
CancellationToken ct) CancellationToken ct)
{ {
var ids = ParseIds(value); var ids = ParseIds(value);
if (ids == null) if (ids == null)
{ {
return Task.FromResult<IReadOnlyList<IEnrichedContentEntity>>(EmptyContents); return EmptyContents;
} }
var dataLoader = GetContentsLoader(); var dataLoader = GetContentsLoader();
return LoadManyAsync(dataLoader, ids, ct); var result = await dataLoader.LoadAsync(ids).GetResultAsync(ct);
return result?.NotNull().ToList() ?? EmptyContents;
} }
private IDataLoader<DomainId, IEnrichedAssetEntity> GetAssetsLoader() private IDataLoader<DomainId, IEnrichedAssetEntity> GetAssetsLoader()
@ -150,20 +131,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
return dataLoaders.Context.GetOrAddBatchLoader<string, IUser>(nameof(GetUserLoader), return dataLoaders.Context.GetOrAddBatchLoader<string, IUser>(nameof(GetUserLoader),
async (batch, ct) => async (batch, ct) =>
{ {
var result = await userResolver.QueryManyAsync(batch.ToArray(), ct); var result = await Resolve<IUserResolver>().QueryManyAsync(batch.ToArray(), ct);
return result; return result;
}); });
} }
private static async Task<IReadOnlyList<T>> LoadManyAsync<TKey, T>(IDataLoader<TKey, T> dataLoader, ICollection<TKey> keys,
CancellationToken ct) where T : class
{
var contents = await Task.WhenAll(keys.Select(x => dataLoader.LoadAsync(x).GetResultAsync(ct)));
return contents.NotNull().ToList();
}
private static ICollection<DomainId>? ParseIds(IJsonValue value) private static ICollection<DomainId>? ParseIds(IJsonValue value)
{ {
try try

12
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetGraphType.cs

@ -251,17 +251,23 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
private static readonly IFieldResolver Url = Resolve((asset, _, context) => private static readonly IFieldResolver Url = Resolve((asset, _, context) =>
{ {
return context.UrlGenerator.AssetContent(asset.AppId, asset.Id.ToString()); var urlGenerator = context.Resolve<IUrlGenerator>();
return urlGenerator.AssetContent(asset.AppId, asset.Id.ToString());
}); });
private static readonly IFieldResolver SourceUrl = Resolve((asset, _, context) => private static readonly IFieldResolver SourceUrl = Resolve((asset, _, context) =>
{ {
return context.UrlGenerator.AssetSource(asset.AppId, asset.Id, asset.FileVersion); var urlGenerator = context.Resolve<IUrlGenerator>();
return urlGenerator.AssetSource(asset.AppId, asset.Id, asset.FileVersion);
}); });
private static readonly IFieldResolver ThumbnailUrl = Resolve((asset, _, context) => private static readonly IFieldResolver ThumbnailUrl = Resolve((asset, _, context) =>
{ {
return context.UrlGenerator.AssetThumbnail(asset.AppId, asset.Id.ToString(), asset.Type); var urlGenerator = context.Resolve<IUrlGenerator>();
return urlGenerator.AssetThumbnail(asset.AppId, asset.Id.ToString(), asset.Type);
}); });
private static IFieldResolver Resolve<T>(Func<IEnrichedAssetEntity, IResolveFieldContext, GraphQLExecutionContext, T> resolver) private static IFieldResolver Resolve<T>(Func<IEnrichedAssetEntity, IResolveFieldContext, GraphQLExecutionContext, T> resolver)

3
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs

@ -13,6 +13,7 @@ using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Translations;
using Squidex.Shared; using Squidex.Shared;
@ -443,7 +444,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
contentCommand.SchemaId = schemaId; contentCommand.SchemaId = schemaId;
contentCommand.ExpectedVersion = fieldContext.GetArgument("expectedVersion", EtagVersion.Any); contentCommand.ExpectedVersion = fieldContext.GetArgument("expectedVersion", EtagVersion.Any);
var commandContext = await context.CommandBus.PublishAsync(contentCommand); var commandContext = await context.Resolve<ICommandBus>().PublishAsync(contentCommand);
return commandContext.PlainResult!; return commandContext.PlainResult!;
}); });

5
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentResolvers.cs

@ -7,6 +7,7 @@
using GraphQL; using GraphQL;
using GraphQL.Resolvers; using GraphQL.Resolvers;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ConvertContent; using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -24,9 +25,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
public static readonly IFieldResolver Url = Resolve((content, _, context) => public static readonly IFieldResolver Url = Resolve((content, _, context) =>
{ {
var appId = content.AppId; var urlGenerator = context.Resolve<IUrlGenerator>();
return context.UrlGenerator.ContentUI(appId, content.SchemaId, content.Id); return urlGenerator.ContentUI(content.AppId, content.SchemaId, content.Id);
}); });
public static readonly IFieldResolver FlatData = Resolve((content, c, context) => public static readonly IFieldResolver FlatData = Resolve((content, c, context) =>

4
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Resolvers.cs

@ -62,7 +62,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
} }
catch (Exception ex) catch (Exception ex)
{ {
executionContext.Log.LogWarning(ex, w => w executionContext.Resolve<ISemanticLog>().LogWarning(ex, w => w
.WriteProperty("action", "resolveField") .WriteProperty("action", "resolveField")
.WriteProperty("status", "failed") .WriteProperty("status", "failed")
.WriteProperty("field", context.FieldDefinition.Name)); .WriteProperty("field", context.FieldDefinition.Name));
@ -104,7 +104,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
} }
catch (Exception ex) catch (Exception ex)
{ {
executionContext.Log.LogWarning(ex, w => w executionContext.Resolve<ISemanticLog>().LogWarning(ex, w => w
.WriteProperty("action", "resolveField") .WriteProperty("action", "resolveField")
.WriteProperty("status", "failed") .WriteProperty("status", "failed")
.WriteProperty("field", context.FieldDefinition.Name)); .WriteProperty("field", context.FieldDefinition.Name));

43
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using System.Collections.Concurrent; using System.Collections.Concurrent;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -16,24 +17,22 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
private readonly SemaphoreSlim maxRequests = new SemaphoreSlim(10); private readonly SemaphoreSlim maxRequests = new SemaphoreSlim(10);
private readonly ConcurrentDictionary<DomainId, IEnrichedContentEntity?> cachedContents = new ConcurrentDictionary<DomainId, IEnrichedContentEntity?>(); private readonly ConcurrentDictionary<DomainId, IEnrichedContentEntity?> cachedContents = new ConcurrentDictionary<DomainId, IEnrichedContentEntity?>();
private readonly ConcurrentDictionary<DomainId, IEnrichedAssetEntity?> cachedAssets = new ConcurrentDictionary<DomainId, IEnrichedAssetEntity?>(); private readonly ConcurrentDictionary<DomainId, IEnrichedAssetEntity?> cachedAssets = new ConcurrentDictionary<DomainId, IEnrichedAssetEntity?>();
private readonly IContentQueryService contentQuery;
private readonly IAssetQueryService assetQuery;
public abstract Context Context { get; } public abstract Context Context { get; }
protected QueryExecutionContext(IAssetQueryService assetQuery, IContentQueryService contentQuery) public IServiceProvider Services { get; }
protected QueryExecutionContext(IServiceProvider serviceProvider)
{ {
Guard.NotNull(assetQuery); Guard.NotNull(serviceProvider);
Guard.NotNull(contentQuery);
this.assetQuery = assetQuery; Services = serviceProvider;
this.contentQuery = contentQuery;
} }
public virtual Task<IEnrichedContentEntity?> FindContentAsync(string schemaIdOrName, DomainId id, long version, public virtual Task<IEnrichedContentEntity?> FindContentAsync(string schemaIdOrName, DomainId id, long version,
CancellationToken ct) CancellationToken ct)
{ {
return contentQuery.FindAsync(Context, schemaIdOrName, id, version, ct); return Resolve<IContentQueryService>().FindAsync(Context, schemaIdOrName, id, version, ct);
} }
public virtual async Task<IResultList<IEnrichedAssetEntity>> QueryAssetsAsync(Q q, public virtual async Task<IResultList<IEnrichedAssetEntity>> QueryAssetsAsync(Q q,
@ -44,7 +43,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
await maxRequests.WaitAsync(ct); await maxRequests.WaitAsync(ct);
try try
{ {
assets = await assetQuery.QueryAsync(Context, null, q, ct); assets = await Resolve<IAssetQueryService>().QueryAsync(Context, null, q, ct);
} }
finally finally
{ {
@ -67,7 +66,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
await maxRequests.WaitAsync(ct); await maxRequests.WaitAsync(ct);
try try
{ {
contents = await contentQuery.QueryAsync(Context, schemaIdOrName, q, ct); contents = await Resolve<IContentQueryService>().QueryAsync(Context, schemaIdOrName, q, ct);
} }
finally finally
{ {
@ -96,7 +95,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
await maxRequests.WaitAsync(ct); await maxRequests.WaitAsync(ct);
try try
{ {
assets = await assetQuery.QueryAsync(Context, null, Q.Empty.WithIds(notLoadedAssets).WithoutTotal(), ct); var q = Q.Empty.WithIds(notLoadedAssets).WithoutTotal();
assets = await Resolve<IAssetQueryService>().QueryAsync(Context, null, q, ct);
} }
finally finally
{ {
@ -126,7 +127,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
await maxRequests.WaitAsync(ct); await maxRequests.WaitAsync(ct);
try try
{ {
contents = await contentQuery.QueryAsync(Context, Q.Empty.WithIds(notLoadedContents).WithoutTotal(), ct); var q = Q.Empty.WithIds(notLoadedContents).WithoutTotal();
contents = await Resolve<IContentQueryService>().QueryAsync(Context, q, ct);
} }
finally finally
{ {
@ -141,5 +144,21 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
return ids.Select(cachedContents.GetOrDefault).NotNull().ToList(); return ids.Select(cachedContents.GetOrDefault).NotNull().ToList();
} }
public T Resolve<T>() where T : class
{
var key = typeof(T).Name;
if (TryGetValue(key, out var stored) && stored is T typed)
{
return typed;
}
typed = Services.GetRequiredService<T>();
this[key] = typed;
return typed;
}
} }
} }

3
backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj

@ -22,7 +22,7 @@
<PackageReference Include="CsvHelper" Version="27.2.1" /> <PackageReference Include="CsvHelper" Version="27.2.1" />
<PackageReference Include="GraphQL" Version="4.7.1" /> <PackageReference Include="GraphQL" Version="4.7.1" />
<PackageReference Include="GraphQL.DataLoader" Version="4.7.1" /> <PackageReference Include="GraphQL.DataLoader" Version="4.7.1" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.689"> <PackageReference Include="Meziantou.Analyzer" Version="1.0.694">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
@ -38,6 +38,7 @@
<PackageReference Include="System.Collections.Immutable" Version="6.0.0" /> <PackageReference Include="System.Collections.Immutable" Version="6.0.0" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" /> <PackageReference Include="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="taglib-sharp-netstandard2.0" Version="2.1.0" /> <PackageReference Include="taglib-sharp-netstandard2.0" Version="2.1.0" />
<PackageReference Include="tusdotnet" Version="2.5.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<AdditionalFiles Include="..\..\stylecop.json" Link="stylecop.json" /> <AdditionalFiles Include="..\..\stylecop.json" Link="stylecop.json" />

2
backend/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj

@ -14,7 +14,7 @@
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" /> <ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="1.0.689"> <PackageReference Include="Meziantou.Analyzer" Version="1.0.694">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

2
backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj

@ -19,7 +19,7 @@
<ProjectReference Include="..\Squidex.Shared\Squidex.Shared.csproj" /> <ProjectReference Include="..\Squidex.Shared\Squidex.Shared.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="1.0.689"> <PackageReference Include="Meziantou.Analyzer" Version="1.0.694">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

2
backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj

@ -18,7 +18,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="IdentityModel" Version="6.0.0" /> <PackageReference Include="IdentityModel" Version="6.0.0" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.689"> <PackageReference Include="Meziantou.Analyzer" Version="1.0.694">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

2
backend/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj

@ -15,7 +15,7 @@
<PackageReference Include="EventStore.Client.Grpc.ProjectionManagement" Version="21.2.0" /> <PackageReference Include="EventStore.Client.Grpc.ProjectionManagement" Version="21.2.0" />
<PackageReference Include="EventStore.Client.Grpc.Streams" Version="21.2.0" /> <PackageReference Include="EventStore.Client.Grpc.Streams" Version="21.2.0" />
<PackageReference Include="Grpc.Net.Client" Version="2.42.0" /> <PackageReference Include="Grpc.Net.Client" Version="2.42.0" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.689"> <PackageReference Include="Meziantou.Analyzer" Version="1.0.694">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

4
backend/src/Squidex.Infrastructure.MongoDb/Diagnostics/MongoDBHealthCheck.cs → backend/src/Squidex.Infrastructure.MongoDb/Diagnostics/MongoHealthCheck.cs

@ -10,11 +10,11 @@ using MongoDB.Driver;
namespace Squidex.Infrastructure.Diagnostics namespace Squidex.Infrastructure.Diagnostics
{ {
public sealed class MongoDBHealthCheck : IHealthCheck public sealed class MongoHealthCheck : IHealthCheck
{ {
private readonly IMongoDatabase mongoDatabase; private readonly IMongoDatabase mongoDatabase;
public MongoDBHealthCheck(IMongoDatabase mongoDatabase) public MongoHealthCheck(IMongoDatabase mongoDatabase)
{ {
this.mongoDatabase = mongoDatabase; this.mongoDatabase = mongoDatabase;
} }

2
backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj

@ -14,7 +14,7 @@
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" /> <ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="1.0.689"> <PackageReference Include="Meziantou.Analyzer" Version="1.0.694">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

2
backend/src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.csproj

@ -11,7 +11,7 @@
<DebugSymbols>True</DebugSymbols> <DebugSymbols>True</DebugSymbols>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="1.0.689"> <PackageReference Include="Meziantou.Analyzer" Version="1.0.694">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

6
backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj

@ -14,7 +14,7 @@
<PackageReference Include="GeoJSON.Net" Version="1.2.19" /> <PackageReference Include="GeoJSON.Net" Version="1.2.19" />
<PackageReference Include="MailKit" Version="3.1.1" /> <PackageReference Include="MailKit" Version="3.1.1" />
<PackageReference Include="McMaster.NETCore.Plugins" Version="1.4.0" /> <PackageReference Include="McMaster.NETCore.Plugins" Version="1.4.0" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.689"> <PackageReference Include="Meziantou.Analyzer" Version="1.0.694">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
@ -31,8 +31,8 @@
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="OpenTelemetry.Api" Version="1.1.0" /> <PackageReference Include="OpenTelemetry.Api" Version="1.1.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.Assets" Version="2.6.0" /> <PackageReference Include="Squidex.Assets" Version="2.15.0" />
<PackageReference Include="Squidex.Caching" Version="1.8.0" /> <PackageReference Include="Squidex.Caching" Version="1.9.0" />
<PackageReference Include="Squidex.Hosting.Abstractions" Version="2.13.0" /> <PackageReference Include="Squidex.Hosting.Abstractions" Version="2.13.0" />
<PackageReference Include="Squidex.Log" Version="1.6.0" /> <PackageReference Include="Squidex.Log" Version="1.6.0" />
<PackageReference Include="Squidex.Text" Version="1.7.0" /> <PackageReference Include="Squidex.Text" Version="1.7.0" />

2
backend/src/Squidex.Shared/Squidex.Shared.csproj

@ -9,7 +9,7 @@
<DebugSymbols>True</DebugSymbols> <DebugSymbols>True</DebugSymbols>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="1.0.689"> <PackageReference Include="Meziantou.Analyzer" Version="1.0.694">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

2
backend/src/Squidex.Web/Squidex.Web.csproj

@ -19,7 +19,7 @@
</PackageReference> </PackageReference>
<PackageReference Include="GraphQL.Server.Transports.AspNetCore" Version="5.2.0" /> <PackageReference Include="GraphQL.Server.Transports.AspNetCore" Version="5.2.0" />
<PackageReference Include="Lazy.Fody" Version="1.11.0" PrivateAssets="all" /> <PackageReference Include="Lazy.Fody" Version="1.11.0" PrivateAssets="all" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.689"> <PackageReference Include="Meziantou.Analyzer" Version="1.0.694">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

80
backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs

@ -36,18 +36,21 @@ namespace Squidex.Areas.Api.Controllers.Assets
private readonly IAssetUsageTracker assetStatsRepository; private readonly IAssetUsageTracker assetStatsRepository;
private readonly IAppPlansProvider appPlansProvider; private readonly IAppPlansProvider appPlansProvider;
private readonly ITagService tagService; private readonly ITagService tagService;
private readonly AssetTusRunner assetTusRunner;
public AssetsController( public AssetsController(
ICommandBus commandBus, ICommandBus commandBus,
IAssetQueryService assetQuery, IAssetQueryService assetQuery,
IAssetUsageTracker assetStatsRepository, IAssetUsageTracker assetStatsRepository,
IAppPlansProvider appPlansProvider, IAppPlansProvider appPlansProvider,
ITagService tagService) ITagService tagService,
AssetTusRunner assetTusRunner)
: base(commandBus) : base(commandBus)
{ {
this.appPlansProvider = appPlansProvider;
this.assetQuery = assetQuery; this.assetQuery = assetQuery;
this.assetStatsRepository = assetStatsRepository; this.assetStatsRepository = assetStatsRepository;
this.appPlansProvider = appPlansProvider; this.assetTusRunner = assetTusRunner;
this.tagService = tagService; this.tagService = tagService;
} }
@ -218,6 +221,43 @@ namespace Squidex.Areas.Api.Controllers.Assets
return CreatedAtAction(nameof(GetAsset), new { app, id = response.Id }, response); return CreatedAtAction(nameof(GetAsset), new { app, id = response.Id }, response);
} }
/// <summary>
/// Upload a new asset using tus.io.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <returns>
/// 201 => Asset created.
/// 400 => Asset request not valid.
/// 413 => Asset exceeds the maximum upload size.
/// 404 => App not found.
/// </returns>
/// <remarks>
/// Use the tus protocol to upload an asset.
/// </remarks>
[OpenApiIgnore]
[Route("apps/{app}/assets/tus/{**fileId}")]
[ProducesResponseType(typeof(AssetDto), 201)]
[AssetRequestSizeLimit]
[ApiPermissionOrAnonymous(Permissions.AppAssetsCreate)]
[ApiCosts(1)]
public async Task<IActionResult> PostAssetTus(string app)
{
var url = Url.Action(null, new { app, fileId = (object?)null })!;
var (result, file) = await assetTusRunner.InvokeAsync(HttpContext, url);
if (file != null)
{
var command = CreateAssetDto.ToCommand(file);
var response = await InvokeCommandAsync(command);
return CreatedAtAction(nameof(GetAsset), new { app, id = response.Id }, response);
}
return result;
}
/// <summary> /// <summary>
/// Bulk update assets. /// Bulk update assets.
/// </summary> /// </summary>
@ -305,6 +345,42 @@ namespace Squidex.Areas.Api.Controllers.Assets
return Ok(response); return Ok(response);
} }
/// <summary>
/// Replace asset content using tus.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="id">The id of the asset.</param>
/// <returns>
/// 200 => Asset updated.
/// 400 => Asset request not valid.
/// 413 => Asset exceeds the maximum upload size.
/// 404 => Asset or app not found.
/// </returns>
/// <remarks>
/// Use the tus protocol to upload an asset.
/// </remarks>
[OpenApiIgnore]
[Route("apps/{app}/assets/{id}/content/tus/{**fileId}")]
[ProducesResponseType(typeof(AssetDto), StatusCodes.Status200OK)]
[AssetRequestSizeLimit]
[ApiPermissionOrAnonymous(Permissions.AppAssetsUpload)]
[ApiCosts(1)]
public async Task<IActionResult> PutAssetContentTus(string app, DomainId id)
{
var (result, file) = await assetTusRunner.InvokeAsync(HttpContext, Url.Action(null, new { app, id })!);
if (file != null)
{
var command = new UpdateAsset { File = file, AssetId = id };
var response = await InvokeCommandAsync(command);
return Ok(response);
}
return result;
}
/// <summary> /// <summary>
/// Update an asset. /// Update an asset.
/// </summary> /// </summary>

22
backend/src/Squidex/Areas/Api/Controllers/Assets/Models/CreateAssetDto.cs

@ -38,6 +38,28 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
[FromQuery] [FromQuery]
public bool Duplicate { get; set; } public bool Duplicate { get; set; }
public static CreateAsset ToCommand(AssetTusFile file)
{
var command = new CreateAsset { File = file };
if (file.Metadata.TryGetValue("id", out var id) && !string.IsNullOrWhiteSpace(id))
{
command.AssetId = DomainId.Create(id);
}
if (file.Metadata.TryGetValue("parentId", out var parentId) && !string.IsNullOrWhiteSpace(parentId))
{
command.ParentId = DomainId.Create(parentId);
}
if (file.Metadata.TryGetValue("duplicate", out var duplicate) && bool.TryParse(duplicate, out var parsed))
{
command.Duplicate = parsed;
}
return command;
}
public CreateAsset ToCommand(AssetFile file) public CreateAsset ToCommand(AssetFile file)
{ {
var command = SimpleMapper.Map(this, new CreateAsset { File = file }); var command = SimpleMapper.Map(this, new CreateAsset { File = file });

27
backend/src/Squidex/Config/Domain/AssetServices.cs

@ -18,6 +18,8 @@ using Squidex.Domain.Apps.Entities.Search;
using Squidex.Hosting; using Squidex.Hosting;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Orleans;
using tusdotnet.Interfaces;
namespace Squidex.Config.Domain namespace Squidex.Config.Domain
{ {
@ -49,12 +51,18 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<AssetQueryParser>() services.AddSingletonAs<AssetQueryParser>()
.AsSelf(); .AsSelf();
services.AddSingletonAs<RebuildFiles>()
.AsSelf();
services.AddTransientAs<AssetTagsDeleter>() services.AddTransientAs<AssetTagsDeleter>()
.As<IDeleter>(); .As<IDeleter>();
services.AddSingletonAs<AssetTusRunner>()
.AsSelf();
services.AddSingletonAs<AssetTusStore>()
.As<ITusStore>().As<ITusExpirationStore>();
services.AddSingletonAs<RebuildFiles>()
.AsSelf();
services.AddTransientAs<AssetHistoryEventsCreator>() services.AddTransientAs<AssetHistoryEventsCreator>()
.As<IHistoryEventsCreator>(); .As<IHistoryEventsCreator>();
@ -87,6 +95,9 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<SvgAssetMetadataSource>() services.AddSingletonAs<SvgAssetMetadataSource>()
.As<IAssetMetadataSource>(); .As<IAssetMetadataSource>();
services.AddSingletonAs<GrainBootstrap<IAssetCleanupGrain>>()
.AsSelf();
} }
public static void AddSquidexAssetInfrastructure(this IServiceCollection services, IConfiguration config) public static void AddSquidexAssetInfrastructure(this IServiceCollection services, IConfiguration config)
@ -176,10 +187,12 @@ namespace Squidex.Config.Domain
} }
}); });
services.AddSingletonAs(c => new DelegateInitializer( services.AddSingletonAs(c =>
c.GetRequiredService<IAssetStore>().GetType().Name, {
c.GetRequiredService<IAssetStore>().InitializeAsync)) var service = c.GetRequiredService<IAssetStore>();
.As<IInitializable>();
return new DelegateInitializer(service.GetType().Name, service.InitializeAsync);
}).As<IInitializable>();
} }
} }
} }

2
backend/src/Squidex/Config/Domain/ResizeServices.cs

@ -6,8 +6,6 @@
// ========================================================================== // ==========================================================================
using Squidex.Assets; using Squidex.Assets;
using Squidex.Assets.ImageMagick;
using Squidex.Assets.ImageSharp;
using Squidex.Assets.Remote; using Squidex.Assets.Remote;
namespace Squidex.Config.Domain namespace Squidex.Config.Domain

18
backend/src/Squidex/Config/Domain/StoreServices.cs

@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Identity;
using Migrations.Migrations.MongoDb; using Migrations.Migrations.MongoDb;
using MongoDB.Driver; using MongoDB.Driver;
using Newtonsoft.Json; using Newtonsoft.Json;
using Squidex.Assets;
using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Apps.DomainObject; using Squidex.Domain.Apps.Entities.Apps.DomainObject;
using Squidex.Domain.Apps.Entities.Apps.Repositories; using Squidex.Domain.Apps.Entities.Apps.Repositories;
@ -34,6 +35,7 @@ using Squidex.Domain.Apps.Entities.Schemas.Repositories;
using Squidex.Domain.Users; using Squidex.Domain.Users;
using Squidex.Domain.Users.InMemory; using Squidex.Domain.Users.InMemory;
using Squidex.Domain.Users.MongoDb; using Squidex.Domain.Users.MongoDb;
using Squidex.Hosting;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Diagnostics; using Squidex.Infrastructure.Diagnostics;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
@ -96,7 +98,10 @@ namespace Squidex.Config.Domain
.As<IMigration>(); .As<IMigration>();
services.AddHealthChecks() services.AddHealthChecks()
.AddCheck<MongoDBHealthCheck>("MongoDB", tags: new[] { "node" }); .AddCheck<MongoHealthCheck>("MongoDB", tags: new[] { "node" });
services.AddSingletonAs<MongoAssetKeyValueStore<TusMetadata>>()
.As<IAssetKeyValueStore<TusMetadata>>();
services.AddSingletonAs<MongoRequestLogRepository>() services.AddSingletonAs<MongoRequestLogRepository>()
.As<IRequestLogRepository>(); .As<IRequestLogRepository>();
@ -175,6 +180,13 @@ namespace Squidex.Config.Domain
services.AddSingleton(typeof(IPersistenceFactory<>), services.AddSingleton(typeof(IPersistenceFactory<>),
typeof(Store<>)); typeof(Store<>));
services.AddSingletonAs(c =>
{
var service = c.GetRequiredService<IAssetKeyValueStore<TusMetadata>>();
return new DelegateInitializer(service.GetType().Name, service.InitializeAsync);
}).As<IInitializable>();
} }
private static IMongoClient GetClient(string configuration) private static IMongoClient GetClient(string configuration)
@ -182,9 +194,9 @@ namespace Squidex.Config.Domain
return Singletons<IMongoClient>.GetOrAdd(configuration, s => new MongoClient(s)); return Singletons<IMongoClient>.GetOrAdd(configuration, s => new MongoClient(s));
} }
private static IMongoDatabase GetDatabase(IServiceProvider service, string name) private static IMongoDatabase GetDatabase(IServiceProvider serviceProvider, string name)
{ {
return service.GetRequiredService<IMongoClient>().GetDatabase(name); return serviceProvider.GetRequiredService<IMongoClient>().GetDatabase(name);
} }
} }
} }

19
backend/src/Squidex/Squidex.csproj

@ -40,7 +40,7 @@
<PackageReference Include="GraphQL.Server.Core" Version="5.2.0" /> <PackageReference Include="GraphQL.Server.Core" Version="5.2.0" />
<PackageReference Include="GraphQL.Server.Transports.AspNetCore.NewtonsoftJson" Version="5.2.0" /> <PackageReference Include="GraphQL.Server.Transports.AspNetCore.NewtonsoftJson" Version="5.2.0" />
<PackageReference Include="GraphQL.Server.Transports.AspNetCore.SystemTextJson" Version="5.2.0" /> <PackageReference Include="GraphQL.Server.Transports.AspNetCore.SystemTextJson" Version="5.2.0" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.689"> <PackageReference Include="Meziantou.Analyzer" Version="1.0.694">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
@ -74,13 +74,16 @@
<PackageReference Include="OrleansDashboard.EmbeddedAssets" Version="3.6.1" /> <PackageReference Include="OrleansDashboard.EmbeddedAssets" Version="3.6.1" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="ReportGenerator" Version="5.0.3" PrivateAssets="all" /> <PackageReference Include="ReportGenerator" Version="5.0.3" PrivateAssets="all" />
<PackageReference Include="Squidex.Assets.Azure" Version="2.6.0" /> <PackageReference Include="Squidex.Assets.Azure" Version="2.15.0" />
<PackageReference Include="Squidex.Assets.GoogleCloud" Version="2.6.0" /> <PackageReference Include="Squidex.Assets.GoogleCloud" Version="2.15.0" />
<PackageReference Include="Squidex.Assets.FTP" Version="2.6.0" /> <PackageReference Include="Squidex.Assets.FTP" Version="2.15.0" />
<PackageReference Include="Squidex.Assets.Mongo" Version="2.6.0" /> <PackageReference Include="Squidex.Assets.ImageMagick" Version="2.15.0" />
<PackageReference Include="Squidex.Assets.S3" Version="2.6.0" /> <PackageReference Include="Squidex.Assets.ImageSharp" Version="2.15.0" />
<PackageReference Include="Squidex.Caching.Orleans" Version="1.8.0" /> <PackageReference Include="Squidex.Assets.Mongo" Version="2.15.0" />
<PackageReference Include="Squidex.ClientLibrary" Version="8.2.0" /> <PackageReference Include="Squidex.Assets.S3" Version="2.15.0" />
<PackageReference Include="Squidex.Assets.TusAdapter" Version="2.15.0" />
<PackageReference Include="Squidex.Caching.Orleans" Version="1.9.0" />
<PackageReference Include="Squidex.ClientLibrary" Version="8.5.0" />
<PackageReference Include="Squidex.Hosting" Version="2.13.0" /> <PackageReference Include="Squidex.Hosting" Version="2.13.0" />
<PackageReference Include="Squidex.OpenIddict.MongoDb" Version="4.0.1-dev" /> <PackageReference Include="Squidex.OpenIddict.MongoDb" Version="4.0.1-dev" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />

2
backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj

@ -15,7 +15,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="FakeItEasy" Version="7.3.0" /> <PackageReference Include="FakeItEasy" Version="7.3.0" />
<PackageReference Include="FluentAssertions" Version="6.4.0" /> <PackageReference Include="FluentAssertions" Version="6.4.0" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.689"> <PackageReference Include="Meziantou.Analyzer" Version="1.0.694">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

39
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCleanupGrainTests.cs

@ -0,0 +1,39 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using FakeItEasy;
using tusdotnet.Interfaces;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Assets
{
public class AssetCleanupGrainTests
{
private readonly ITusExpirationStore expirationStore = A.Fake<ITusExpirationStore>();
private readonly AssetCleanupGrain sut;
public AssetCleanupGrainTests()
{
sut = new AssetCleanupGrain(expirationStore);
}
[Fact]
public async Task Should_do_nothing_on_activate()
{
await sut.ActivateAsync();
}
[Fact]
public async Task Should_call_expiration_store_when_reminder_invoked()
{
await sut.ReceiveReminder("Reminder", default);
A.CallTo(() => expirationStore.RemoveExpiredFilesAsync(default))
.MustHaveHappened();
}
}
}

4
backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj

@ -25,7 +25,7 @@
<PackageReference Include="GraphQL" Version="4.7.1" /> <PackageReference Include="GraphQL" Version="4.7.1" />
<PackageReference Include="GraphQL.NewtonsoftJson" Version="4.7.1" /> <PackageReference Include="GraphQL.NewtonsoftJson" Version="4.7.1" />
<PackageReference Include="Lorem.Universal.Net" Version="4.0.80" /> <PackageReference Include="Lorem.Universal.Net" Version="4.0.80" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.689"> <PackageReference Include="Meziantou.Analyzer" Version="1.0.694">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
@ -34,7 +34,7 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="Microsoft.Orleans.TestingHost" Version="3.6.0" /> <PackageReference Include="Microsoft.Orleans.TestingHost" Version="3.6.0" />
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.0.0" /> <PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.0.0" />
<PackageReference Include="Squidex.Caching.Orleans" Version="1.8.0" /> <PackageReference Include="Squidex.Caching.Orleans" Version="1.9.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" /> <PackageReference Include="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit" Version="2.4.1" />

2
backend/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj

@ -16,7 +16,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="FakeItEasy" Version="7.3.0" /> <PackageReference Include="FakeItEasy" Version="7.3.0" />
<PackageReference Include="FluentAssertions" Version="6.4.0" /> <PackageReference Include="FluentAssertions" Version="6.4.0" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.689"> <PackageReference Include="Meziantou.Analyzer" Version="1.0.694">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

2
backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj

@ -17,7 +17,7 @@
<PackageReference Include="FakeItEasy" Version="7.3.0" /> <PackageReference Include="FakeItEasy" Version="7.3.0" />
<PackageReference Include="FluentAssertions" Version="6.4.0" /> <PackageReference Include="FluentAssertions" Version="6.4.0" />
<PackageReference Include="Google.Cloud.Storage.V1" Version="3.7.0" /> <PackageReference Include="Google.Cloud.Storage.V1" Version="3.7.0" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.689"> <PackageReference Include="Meziantou.Analyzer" Version="1.0.694">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

2
backend/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj

@ -13,7 +13,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="FakeItEasy" Version="7.3.0" /> <PackageReference Include="FakeItEasy" Version="7.3.0" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.689"> <PackageReference Include="Meziantou.Analyzer" Version="1.0.694">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

1
backend/tools/TestSuite/TestSuite.ApiTests/AppWorkflowsTest.cs

@ -9,6 +9,7 @@ using Squidex.ClientLibrary.Management;
using TestSuite.Fixtures; using TestSuite.Fixtures;
using Xunit; using Xunit;
#pragma warning disable MA0048 // File name must match type name
#pragma warning disable SA1300 // Element should begin with upper-case letter #pragma warning disable SA1300 // Element should begin with upper-case letter
#pragma warning disable SA1507 // Code should not contain multiple blank lines in a row #pragma warning disable SA1507 // Code should not contain multiple blank lines in a row

1
backend/tools/TestSuite/TestSuite.ApiTests/AssetFormatTests.cs

@ -9,7 +9,6 @@ using Squidex.ClientLibrary.Management;
using TestSuite.Fixtures; using TestSuite.Fixtures;
using Xunit; using Xunit;
#pragma warning disable xUnit1004 // Test methods should not be skipped
#pragma warning disable SA1300 // Element should begin with upper-case letter #pragma warning disable SA1300 // Element should begin with upper-case letter
#pragma warning disable CS0618 // Type or member is obsolete #pragma warning disable CS0618 // Type or member is obsolete

305
backend/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using Squidex.Assets;
using Squidex.ClientLibrary.Management; using Squidex.ClientLibrary.Management;
using TestSuite.Fixtures; using TestSuite.Fixtures;
using Xunit; using Xunit;
@ -39,6 +40,136 @@ namespace TestSuite.ApiTests
} }
} }
[Fact]
public async Task Should_upload_asset_using_tus()
{
// STEP 1: Create asset
var fileParameter = FileParameter.FromPath("Assets/SampleVideo_1280x720_1mb.mp4");
var reportedException = (Exception)null;
var reportedProgress = new List<int>();
var reportedAsset = (AssetDto)null;
await using (fileParameter.Data)
{
await _.Assets.UploadNewAssetAsync(_.AppName, fileParameter, new AssetUploadOptions
{
ProgressHandler = new AssetDelegatingProgressHandler
{
OnProgressAsync = (@event, _) =>
{
reportedProgress.Add(@event.Progress);
return Task.CompletedTask;
},
OnCompletedAsync = (@event, _) =>
{
reportedAsset = @event.Asset;
return Task.CompletedTask;
},
OnFailedAsync = (@event, _) =>
{
reportedException = @event.Exception;
return Task.CompletedTask;
}
}
});
}
Assert.NotEmpty(reportedProgress);
Assert.NotNull(reportedAsset);
Assert.Null(reportedException);
await using (var stream = new FileStream("Assets/SampleVideo_1280x720_1mb.mp4", FileMode.Open))
{
var downloaded = await _.DownloadAsync(reportedAsset);
// Should dowload with correct size.
Assert.Equal(stream.Length, downloaded.Length);
}
}
[Fact]
public async Task Should_upload_asset_using_tus_in_chunks()
{
// STEP 1: Create asset
var fileParameter = FileParameter.FromPath("Assets/SampleVideo_1280x720_1mb.mp4");
var pausingStream = new PauseStream(fileParameter.Data, 0.25);
var pausingFile = new FileParameter(pausingStream, fileParameter.FileName, fileParameter.ContentType);
var numUploads = 0;
var reportedException = (Exception)null;
var reportedProgress = new List<int>();
var reportedAsset = (AssetDto)null;
var fileId = (string)null;
await using (pausingFile.Data)
{
using var cts = new CancellationTokenSource(5000);
while (reportedAsset == null)
{
pausingStream.Reset();
if (pausingStream.Position == pausingStream.Length)
{
throw new InvalidOperationException("Stream end reached.");
}
await _.Assets.UploadNewAssetAsync(_.AppName, pausingFile, new AssetUploadOptions
{
ProgressHandler = new AssetDelegatingProgressHandler
{
OnCreatedAsync = (@event, _) =>
{
fileId = @event.FileId;
return Task.CompletedTask;
},
OnProgressAsync = (@event, _) =>
{
reportedProgress.Add(@event.Progress);
return Task.CompletedTask;
},
OnCompletedAsync = (@event, _) =>
{
reportedAsset = @event.Asset;
return Task.CompletedTask;
},
OnFailedAsync = (@event, _) =>
{
if (!@event.Exception.ToString().Contains("PAUSED", StringComparison.OrdinalIgnoreCase))
{
reportedException = @event.Exception;
}
return Task.CompletedTask;
}
},
FileId = fileId
}, cts.Token);
Assert.Null(reportedException);
await Task.Delay(50, cts.Token);
numUploads++;
}
}
Assert.NotEmpty(reportedProgress);
Assert.NotNull(reportedAsset);
Assert.Null(reportedException);
Assert.True(numUploads > 1);
await using (var stream = new FileStream("Assets/SampleVideo_1280x720_1mb.mp4", FileMode.Open))
{
var downloaded = await _.DownloadAsync(reportedAsset);
// Should dowload with correct size.
Assert.Equal(stream.Length, downloaded.Length);
}
}
[Fact] [Fact]
public async Task Should_upload_asset_with_custom_id() public async Task Should_upload_asset_with_custom_id()
{ {
@ -98,6 +229,143 @@ namespace TestSuite.ApiTests
} }
} }
[Fact]
public async Task Should_replace_asset_using_tus()
{
// STEP 1: Create asset
var asset_1 = await _.UploadFileAsync("Assets/logo-squared.png", "image/png");
// STEP 2: Reupload asset
var fileParameter = FileParameter.FromPath("Assets/SampleVideo_1280x720_1mb.mp4");
var reportedException = (Exception)null;
var reportedProgress = new List<int>();
var reportedAsset = (AssetDto)null;
await using (fileParameter.Data)
{
await _.Assets.UploadExistingAssetAsync(_.AppName, asset_1.Id, fileParameter, new AssetUploadOptions
{
ProgressHandler = new AssetDelegatingProgressHandler
{
OnProgressAsync = (@event, _) =>
{
reportedProgress.Add(@event.Progress);
return Task.CompletedTask;
},
OnCompletedAsync = (@event, _) =>
{
reportedAsset = @event.Asset;
return Task.CompletedTask;
},
OnFailedAsync = (@event, _) =>
{
reportedException = @event.Exception;
return Task.CompletedTask;
}
}
});
}
Assert.NotNull(reportedAsset);
Assert.Equal(Enumerable.Range(1, 100).ToArray(), reportedProgress.ToArray());
await using (var stream = new FileStream("Assets/SampleVideo_1280x720_1mb.mp4", FileMode.Open))
{
var downloaded = await _.DownloadAsync(reportedAsset);
// Should dowload with correct size.
Assert.Equal(stream.Length, downloaded.Length);
}
}
[Fact]
public async Task Should_replace_asset_using_tus_in_chunks()
{
// STEP 1: Create asset
var asset_1 = await _.UploadFileAsync("Assets/logo-squared.png", "image/png");
// STEP 2: Reupload asset
var fileParameter = FileParameter.FromPath("Assets/SampleVideo_1280x720_1mb.mp4");
var pausingStream = new PauseStream(fileParameter.Data, 0.25);
var pausingFile = new FileParameter(pausingStream, fileParameter.FileName, fileParameter.ContentType);
var numUploads = 0;
var reportedException = (Exception)null;
var reportedProgress = new List<int>();
var reportedAsset = (AssetDto)null;
var fileId = (string)null;
await using (pausingFile.Data)
{
using var cts = new CancellationTokenSource(5000);
while (reportedAsset == null)
{
pausingStream.Reset();
if (pausingStream.Position == pausingStream.Length)
{
throw new InvalidOperationException("Stream end reached.");
}
await _.Assets.UploadExistingAssetAsync(_.AppName, asset_1.Id, pausingFile, new AssetUploadOptions
{
ProgressHandler = new AssetDelegatingProgressHandler
{
OnCreatedAsync = (@event, _) =>
{
fileId = @event.FileId;
return Task.CompletedTask;
},
OnProgressAsync = (@event, _) =>
{
reportedProgress.Add(@event.Progress);
return Task.CompletedTask;
},
OnCompletedAsync = (@event, _) =>
{
reportedAsset = @event.Asset;
return Task.CompletedTask;
},
OnFailedAsync = (@event, _) =>
{
if (!@event.Exception.ToString().Contains("PAUSED", StringComparison.OrdinalIgnoreCase))
{
reportedException = @event.Exception;
}
return Task.CompletedTask;
}
},
FileId = fileId
}, cts.Token);
Assert.Null(reportedException);
await Task.Delay(50, cts.Token);
numUploads++;
}
}
Assert.NotEmpty(reportedProgress);
Assert.NotNull(reportedAsset);
Assert.Null(reportedException);
Assert.True(numUploads > 1);
await using (var stream = new FileStream("Assets/SampleVideo_1280x720_1mb.mp4", FileMode.Open))
{
var downloaded = await _.DownloadAsync(reportedAsset);
// Should dowload with correct size.
Assert.Equal(stream.Length, downloaded.Length);
}
}
[Fact] [Fact]
public async Task Should_annote_asset() public async Task Should_annote_asset()
{ {
@ -356,5 +624,42 @@ namespace TestSuite.ApiTests
Assert.NotEqual(asset_1.FileSize, asset_2.FileSize); Assert.NotEqual(asset_1.FileSize, asset_2.FileSize);
} }
public class PauseStream : DelegateStream
{
private readonly double pauseAfter = 1;
private int totalRead;
public PauseStream(Stream innerStream, double pauseAfter)
: base(innerStream)
{
this.pauseAfter = pauseAfter;
}
public void Reset()
{
totalRead = 0;
}
public override async ValueTask<int> ReadAsync(Memory<byte> buffer,
CancellationToken cancellationToken = default)
{
if (Position >= Length)
{
return 0;
}
if (totalRead >= Length * pauseAfter)
{
throw new InvalidOperationException("PAUSED");
}
var bytesRead = await base.ReadAsync(buffer, cancellationToken);
totalRead += bytesRead;
return bytesRead;
}
}
} }
} }

5
backend/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj

@ -14,12 +14,13 @@
<None Remove="Assets\SampleImage_WEBP_350kb - Copy.webp" /> <None Remove="Assets\SampleImage_WEBP_350kb - Copy.webp" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="1.0.688"> <PackageReference Include="Meziantou.Analyzer" Version="1.0.694">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="NSwag.Core" Version="13.15.5" /> <PackageReference Include="NSwag.Core" Version="13.15.7" />
<PackageReference Include="Squidex.Assets" Version="2.15.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3"> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">

2
backend/tools/TestSuite/TestSuite.LoadTests/TestSuite.LoadTests.csproj

@ -6,7 +6,7 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="1.0.688"> <PackageReference Include="Meziantou.Analyzer" Version="1.0.694">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

2
backend/tools/TestSuite/TestSuite.Shared/Model/TestEntity.cs

@ -10,6 +10,8 @@ using Newtonsoft.Json.Linq;
using Squidex.ClientLibrary; using Squidex.ClientLibrary;
using Squidex.ClientLibrary.Management; using Squidex.ClientLibrary.Management;
#pragma warning disable MA0048 // File name must match type name
namespace TestSuite.Model namespace TestSuite.Model
{ {
public sealed class TestEntity : Content<TestEntityData> public sealed class TestEntity : Content<TestEntityData>

2
backend/tools/TestSuite/TestSuite.Shared/Model/TestEntityWithReferences.cs

@ -9,6 +9,8 @@ using Newtonsoft.Json;
using Squidex.ClientLibrary; using Squidex.ClientLibrary;
using Squidex.ClientLibrary.Management; using Squidex.ClientLibrary.Management;
#pragma warning disable MA0048 // File name must match type name
namespace TestSuite.Model namespace TestSuite.Model
{ {
public sealed class TestEntityWithReferences : Content<TestEntityWithReferencesData> public sealed class TestEntityWithReferences : Content<TestEntityWithReferencesData>

4
backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj

@ -11,7 +11,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Lazy.Fody" Version="1.11.0" PrivateAssets="all" /> <PackageReference Include="Lazy.Fody" Version="1.11.0" PrivateAssets="all" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.688"> <PackageReference Include="Meziantou.Analyzer" Version="1.0.694">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
@ -21,7 +21,7 @@
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.ClientLibrary" Version="8.2.0" /> <PackageReference Include="Squidex.ClientLibrary" Version="8.5.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit" Version="2.4.1" />
</ItemGroup> </ItemGroup>

4
backend/tools/TestSuite/TestSuite.sln

@ -1,7 +1,7 @@
 
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16 # Visual Studio Version 17
VisualStudioVersion = 16.0.29613.14 VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestSuite.Shared", "TestSuite.Shared\TestSuite.Shared.csproj", "{37484845-5542-4E52-AB00-C4576B84FE75}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestSuite.Shared", "TestSuite.Shared\TestSuite.Shared.csproj", "{37484845-5542-4E52-AB00-C4576B84FE75}"
EndProject EndProject

Loading…
Cancel
Save