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 4 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="Google.Cloud.Diagnostics.Common" Version="4.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>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

2
backend/src/Migrations/Migrations.csproj

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

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

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

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

@ -19,7 +19,7 @@
</ItemGroup>
<ItemGroup>
<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>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

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

@ -58,16 +58,13 @@ namespace Squidex.Domain.Apps.Entities
{
var cacheKey = AppCacheKey(appId);
if (localCache.TryGetValue(cacheKey, out var cached) && cached is IAppEntity found)
var app = await GetOrCreate(cacheKey, () =>
{
return found;
}
var app = await indexForApps.GetAppAsync(appId, canCache, ct);
return indexForApps.GetAppAsync(appId, canCache, ct);
});
if (app != null)
{
localCache.Add(cacheKey, app);
localCache.Add(AppCacheKey(app.Name), app);
}
@ -79,16 +76,13 @@ namespace Squidex.Domain.Apps.Entities
{
var cacheKey = AppCacheKey(appName);
if (localCache.TryGetValue(cacheKey, out var cached) && cached is IAppEntity found)
var app = await GetOrCreate(cacheKey, () =>
{
return found;
}
var app = await indexForApps.GetAppAsync(appName, canCache, ct);
return indexForApps.GetAppAsync(appName, canCache, ct);
});
if (app != null)
{
localCache.Add(cacheKey, app);
localCache.Add(AppCacheKey(app.Id), app);
}
@ -100,16 +94,13 @@ namespace Squidex.Domain.Apps.Entities
{
var cacheKey = SchemaCacheKey(appId, name);
if (localCache.TryGetValue(cacheKey, out var cached) && cached is ISchemaEntity found)
var schema = await GetOrCreate(cacheKey, () =>
{
return found;
}
var schema = await indexForSchemas.GetSchemaAsync(appId, name, canCache, ct);
return indexForSchemas.GetSchemaAsync(appId, name, canCache, ct);
});
if (schema != null)
{
localCache.Add(cacheKey, schema);
localCache.Add(SchemaCacheKey(appId, schema.Id), schema);
}
@ -121,16 +112,13 @@ namespace Squidex.Domain.Apps.Entities
{
var cacheKey = SchemaCacheKey(appId, id);
if (localCache.TryGetValue(cacheKey, out var cached) && cached is ISchemaEntity found)
var schema = await GetOrCreate(cacheKey, () =>
{
return found;
}
var schema = await indexForSchemas.GetSchemaAsync(appId, id, canCache, ct);
return indexForSchemas.GetSchemaAsync(appId, id, canCache, ct);
});
if (schema != null)
{
localCache.Add(cacheKey, 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,
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,
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);
localCache.Add(SchemaCacheKey(appId, schema.SchemaDef.Name), schema);
foreach (var schema in schemas)
{
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,
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,
@ -184,6 +175,28 @@ namespace Squidex.Domain.Apps.Entities
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)
{
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 IServiceProvider serviceProvider;
private readonly GraphQLOptions options;
private readonly SharedTypes sharedTypes;
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.serviceProvider = serviceProvider;
this.options = options.Value;
sharedTypes = serviceProvider.GetRequiredService<SharedTypes>();
}
public async Task ConfigureAsync(ExecutionOptions executionOptions)
@ -88,10 +91,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var hash = await schemasHash.ComputeHashAsync(app, schemas);
return new CacheEntry(
new Builder(app, serviceProvider.GetRequiredService<SharedTypes>()).BuildSchema(schemas),
hash,
SystemClock.Instance.GetCurrentInstant());
return new CacheEntry(new Builder(app, sharedTypes).BuildSchema(schemas),
hash, SystemClock.Instance.GetCurrentInstant());
}
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 Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Contents.Queries;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Log;
using Squidex.Shared.Users;
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<IEnrichedContentEntity> EmptyContents = new List<IEnrichedContentEntity>();
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 GraphQLExecutionContext(IAssetQueryService assetQuery, IContentQueryService contentQuery,
Context context,
IDataLoaderContextAccessor dataLoaders,
ICommandBus commandBus,
IUrlGenerator urlGenerator,
IUserResolver userResolver,
ISemanticLog log)
: base(assetQuery, contentQuery)
public GraphQLExecutionContext(IServiceProvider serviceProvider, IDataLoaderContextAccessor dataLoaders, Context context)
: base(serviceProvider)
{
this.dataLoaders = dataLoaders;
this.userResolver = userResolver;
CommandBus = commandBus;
UrlGenerator = urlGenerator;
Context = context.Clone(b => b
.WithoutCleanup()
.WithoutContentEnrichment());
Log = log;
}
public async Task<IUser?> FindUserAsync(RefToken refToken,
@ -93,34 +70,38 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
return content;
}
public Task<IReadOnlyList<IEnrichedAssetEntity>> GetReferencedAssetsAsync(IJsonValue value,
public async Task<IReadOnlyList<IEnrichedAssetEntity>> GetReferencedAssetsAsync(IJsonValue value,
CancellationToken ct)
{
var ids = ParseIds(value);
if (ids == null)
{
return Task.FromResult<IReadOnlyList<IEnrichedAssetEntity>>(EmptyAssets);
return EmptyAssets;
}
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)
{
var ids = ParseIds(value);
if (ids == null)
{
return Task.FromResult<IReadOnlyList<IEnrichedContentEntity>>(EmptyContents);
return EmptyContents;
}
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()
@ -150,20 +131,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
return dataLoaders.Context.GetOrAddBatchLoader<string, IUser>(nameof(GetUserLoader),
async (batch, ct) =>
{
var result = await userResolver.QueryManyAsync(batch.ToArray(), ct);
var result = await Resolve<IUserResolver>().QueryManyAsync(batch.ToArray(), ct);
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)
{
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) =>
{
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) =>
{
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) =>
{
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)

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.Entities.Contents.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Translations;
using Squidex.Shared;
@ -443,7 +444,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
contentCommand.SchemaId = schemaId;
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!;
});

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

@ -7,6 +7,7 @@
using GraphQL;
using GraphQL.Resolvers;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Infrastructure;
@ -24,9 +25,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
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) =>

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)
{
executionContext.Log.LogWarning(ex, w => w
executionContext.Resolve<ISemanticLog>().LogWarning(ex, w => w
.WriteProperty("action", "resolveField")
.WriteProperty("status", "failed")
.WriteProperty("field", context.FieldDefinition.Name));
@ -104,7 +104,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
}
catch (Exception ex)
{
executionContext.Log.LogWarning(ex, w => w
executionContext.Resolve<ISemanticLog>().LogWarning(ex, w => w
.WriteProperty("action", "resolveField")
.WriteProperty("status", "failed")
.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 Microsoft.Extensions.DependencyInjection;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure;
@ -16,24 +17,22 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
private readonly SemaphoreSlim maxRequests = new SemaphoreSlim(10);
private readonly ConcurrentDictionary<DomainId, IEnrichedContentEntity?> cachedContents = new ConcurrentDictionary<DomainId, IEnrichedContentEntity?>();
private readonly ConcurrentDictionary<DomainId, IEnrichedAssetEntity?> cachedAssets = new ConcurrentDictionary<DomainId, IEnrichedAssetEntity?>();
private readonly IContentQueryService contentQuery;
private readonly IAssetQueryService assetQuery;
public abstract Context Context { get; }
protected QueryExecutionContext(IAssetQueryService assetQuery, IContentQueryService contentQuery)
public IServiceProvider Services { get; }
protected QueryExecutionContext(IServiceProvider serviceProvider)
{
Guard.NotNull(assetQuery);
Guard.NotNull(contentQuery);
Guard.NotNull(serviceProvider);
this.assetQuery = assetQuery;
this.contentQuery = contentQuery;
Services = serviceProvider;
}
public virtual Task<IEnrichedContentEntity?> FindContentAsync(string schemaIdOrName, DomainId id, long version,
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,
@ -44,7 +43,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
await maxRequests.WaitAsync(ct);
try
{
assets = await assetQuery.QueryAsync(Context, null, q, ct);
assets = await Resolve<IAssetQueryService>().QueryAsync(Context, null, q, ct);
}
finally
{
@ -67,7 +66,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
await maxRequests.WaitAsync(ct);
try
{
contents = await contentQuery.QueryAsync(Context, schemaIdOrName, q, ct);
contents = await Resolve<IContentQueryService>().QueryAsync(Context, schemaIdOrName, q, ct);
}
finally
{
@ -96,7 +95,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
await maxRequests.WaitAsync(ct);
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
{
@ -126,7 +127,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
await maxRequests.WaitAsync(ct);
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
{
@ -141,5 +144,21 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
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="GraphQL" 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>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@ -38,6 +38,7 @@
<PackageReference Include="System.Collections.Immutable" Version="6.0.0" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="taglib-sharp-netstandard2.0" Version="2.1.0" />
<PackageReference Include="tusdotnet" Version="2.5.0" />
</ItemGroup>
<ItemGroup>
<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" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="1.0.689">
<PackageReference Include="Meziantou.Analyzer" Version="1.0.694">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

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

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

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

@ -18,7 +18,7 @@
</ItemGroup>
<ItemGroup>
<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>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</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.Streams" Version="21.2.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>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</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
{
public sealed class MongoDBHealthCheck : IHealthCheck
public sealed class MongoHealthCheck : IHealthCheck
{
private readonly IMongoDatabase mongoDatabase;
public MongoDBHealthCheck(IMongoDatabase mongoDatabase)
public MongoHealthCheck(IMongoDatabase 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" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="1.0.689">
<PackageReference Include="Meziantou.Analyzer" Version="1.0.694">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

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

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

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

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

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

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

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

@ -19,7 +19,7 @@
</PackageReference>
<PackageReference Include="GraphQL.Server.Transports.AspNetCore" Version="5.2.0" />
<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>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</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 IAppPlansProvider appPlansProvider;
private readonly ITagService tagService;
private readonly AssetTusRunner assetTusRunner;
public AssetsController(
ICommandBus commandBus,
IAssetQueryService assetQuery,
IAssetUsageTracker assetStatsRepository,
IAppPlansProvider appPlansProvider,
ITagService tagService)
ITagService tagService,
AssetTusRunner assetTusRunner)
: base(commandBus)
{
this.appPlansProvider = appPlansProvider;
this.assetQuery = assetQuery;
this.assetStatsRepository = assetStatsRepository;
this.appPlansProvider = appPlansProvider;
this.assetTusRunner = assetTusRunner;
this.tagService = tagService;
}
@ -218,6 +221,43 @@ namespace Squidex.Areas.Api.Controllers.Assets
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>
/// Bulk update assets.
/// </summary>
@ -305,6 +345,42 @@ namespace Squidex.Areas.Api.Controllers.Assets
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>
/// Update an asset.
/// </summary>

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

@ -38,6 +38,28 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
[FromQuery]
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)
{
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.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Orleans;
using tusdotnet.Interfaces;
namespace Squidex.Config.Domain
{
@ -49,12 +51,18 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<AssetQueryParser>()
.AsSelf();
services.AddSingletonAs<RebuildFiles>()
.AsSelf();
services.AddTransientAs<AssetTagsDeleter>()
.As<IDeleter>();
services.AddSingletonAs<AssetTusRunner>()
.AsSelf();
services.AddSingletonAs<AssetTusStore>()
.As<ITusStore>().As<ITusExpirationStore>();
services.AddSingletonAs<RebuildFiles>()
.AsSelf();
services.AddTransientAs<AssetHistoryEventsCreator>()
.As<IHistoryEventsCreator>();
@ -87,6 +95,9 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<SvgAssetMetadataSource>()
.As<IAssetMetadataSource>();
services.AddSingletonAs<GrainBootstrap<IAssetCleanupGrain>>()
.AsSelf();
}
public static void AddSquidexAssetInfrastructure(this IServiceCollection services, IConfiguration config)
@ -176,10 +187,12 @@ namespace Squidex.Config.Domain
}
});
services.AddSingletonAs(c => new DelegateInitializer(
c.GetRequiredService<IAssetStore>().GetType().Name,
c.GetRequiredService<IAssetStore>().InitializeAsync))
.As<IInitializable>();
services.AddSingletonAs(c =>
{
var service = c.GetRequiredService<IAssetStore>();
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.ImageMagick;
using Squidex.Assets.ImageSharp;
using Squidex.Assets.Remote;
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 MongoDB.Driver;
using Newtonsoft.Json;
using Squidex.Assets;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Apps.DomainObject;
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.InMemory;
using Squidex.Domain.Users.MongoDb;
using Squidex.Hosting;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Diagnostics;
using Squidex.Infrastructure.EventSourcing;
@ -96,7 +98,10 @@ namespace Squidex.Config.Domain
.As<IMigration>();
services.AddHealthChecks()
.AddCheck<MongoDBHealthCheck>("MongoDB", tags: new[] { "node" });
.AddCheck<MongoHealthCheck>("MongoDB", tags: new[] { "node" });
services.AddSingletonAs<MongoAssetKeyValueStore<TusMetadata>>()
.As<IAssetKeyValueStore<TusMetadata>>();
services.AddSingletonAs<MongoRequestLogRepository>()
.As<IRequestLogRepository>();
@ -175,6 +180,13 @@ namespace Squidex.Config.Domain
services.AddSingleton(typeof(IPersistenceFactory<>),
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)
@ -182,9 +194,9 @@ namespace Squidex.Config.Domain
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.Transports.AspNetCore.NewtonsoftJson" 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>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@ -74,13 +74,16 @@
<PackageReference Include="OrleansDashboard.EmbeddedAssets" Version="3.6.1" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="ReportGenerator" Version="5.0.3" PrivateAssets="all" />
<PackageReference Include="Squidex.Assets.Azure" Version="2.6.0" />
<PackageReference Include="Squidex.Assets.GoogleCloud" Version="2.6.0" />
<PackageReference Include="Squidex.Assets.FTP" Version="2.6.0" />
<PackageReference Include="Squidex.Assets.Mongo" Version="2.6.0" />
<PackageReference Include="Squidex.Assets.S3" Version="2.6.0" />
<PackageReference Include="Squidex.Caching.Orleans" Version="1.8.0" />
<PackageReference Include="Squidex.ClientLibrary" Version="8.2.0" />
<PackageReference Include="Squidex.Assets.Azure" Version="2.15.0" />
<PackageReference Include="Squidex.Assets.GoogleCloud" Version="2.15.0" />
<PackageReference Include="Squidex.Assets.FTP" Version="2.15.0" />
<PackageReference Include="Squidex.Assets.ImageMagick" Version="2.15.0" />
<PackageReference Include="Squidex.Assets.ImageSharp" Version="2.15.0" />
<PackageReference Include="Squidex.Assets.Mongo" Version="2.15.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.OpenIddict.MongoDb" Version="4.0.1-dev" />
<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>
<PackageReference Include="FakeItEasy" Version="7.3.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>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</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.NewtonsoftJson" Version="4.7.1" />
<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>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@ -34,7 +34,7 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="Microsoft.Orleans.TestingHost" Version="3.6.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="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="xunit" Version="2.4.1" />

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

@ -16,7 +16,7 @@
<ItemGroup>
<PackageReference Include="FakeItEasy" Version="7.3.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>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

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

@ -17,7 +17,7 @@
<PackageReference Include="FakeItEasy" Version="7.3.0" />
<PackageReference Include="FluentAssertions" Version="6.4.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>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

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

@ -13,7 +13,7 @@
</ItemGroup>
<ItemGroup>
<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>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

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

@ -9,6 +9,7 @@ using Squidex.ClientLibrary.Management;
using TestSuite.Fixtures;
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 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 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 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.
// ==========================================================================
using Squidex.Assets;
using Squidex.ClientLibrary.Management;
using TestSuite.Fixtures;
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]
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]
public async Task Should_annote_asset()
{
@ -356,5 +624,42 @@ namespace TestSuite.ApiTests
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" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="1.0.688">
<PackageReference Include="Meziantou.Analyzer" Version="1.0.694">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<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="xunit" Version="2.4.1" />
<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>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="1.0.688">
<PackageReference Include="Meziantou.Analyzer" Version="1.0.694">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

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

@ -10,6 +10,8 @@ using Newtonsoft.Json.Linq;
using Squidex.ClientLibrary;
using Squidex.ClientLibrary.Management;
#pragma warning disable MA0048 // File name must match type name
namespace TestSuite.Model
{
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.Management;
#pragma warning disable MA0048 // File name must match type name
namespace TestSuite.Model
{
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>
</PackageReference>
<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>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@ -21,7 +21,7 @@
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" 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="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="xunit" Version="2.4.1" />
</ItemGroup>

4
backend/tools/TestSuite/TestSuite.sln

@ -1,7 +1,7 @@

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

Loading…
Cancel
Save