Browse Source

Asset folder per app.

pull/732/head
Sebastian 5 years ago
parent
commit
76f496db91
  1. 1
      backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/ConfigAppPlansProvider.cs
  2. 2
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetExtensions.cs
  3. 2
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetPermanentDeleter.cs
  4. 4
      backend/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs
  5. 80
      backend/src/Squidex.Domain.Apps.Entities/Assets/DefaultAssetFileStore.cs
  6. 2
      backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetCommandMiddleware.cs
  7. 12
      backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetFileStore.cs
  8. 4
      backend/src/Squidex.Domain.Apps.Entities/Assets/RebuildFiles.cs
  9. 2
      backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj
  10. 2
      backend/src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.csproj
  11. 2
      backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
  12. 2
      backend/src/Squidex.Web/Services/UrlGenerator.cs
  13. 23
      backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs
  14. 12
      backend/src/Squidex/Squidex.csproj
  15. 14
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetPermanentDeleterTests.cs
  16. 8
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsFluidExtensionTests.cs
  17. 8
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs
  18. 8
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/BackupAssetsTests.cs
  19. 158
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DefaultAssetFileStoreTests.cs
  20. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetCommandMiddlewareTests.cs
  21. 18
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/RepairFilesTests.cs

1
backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/ConfigAppPlansProvider.cs

@ -68,7 +68,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans
{
Guard.NotNull(app, nameof(app));
return GetPlanUpgrade(app.Plan?.PlanId);
}

2
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetExtensions.cs

@ -31,7 +31,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
using (var stream = DefaultPools.MemoryStream.GetStream())
{
await assetFileStore.DownloadAsync(appId, id, fileVersion, stream);
await assetFileStore.DownloadAsync(appId, id, fileVersion, null, stream);
stream.Position = 0;

2
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetPermanentDeleter.cs

@ -53,7 +53,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
try
{
await assetFileStore.DeleteAsync(assetDeleted.AppId.Id, assetDeleted.AssetId, version);
await assetFileStore.DeleteAsync(assetDeleted.AppId.Id, assetDeleted.AssetId, version, null);
}
catch (AssetNotFoundException)
{

4
backend/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs

@ -127,7 +127,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
await writer.WriteBlobAsync(GetName(assetId, fileVersion), stream =>
{
return assetFileStore.DownloadAsync(appId, assetId, fileVersion, stream);
return assetFileStore.DownloadAsync(appId, assetId, fileVersion, null, stream);
});
}
catch (AssetNotFoundException)
@ -142,7 +142,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
await reader.ReadBlobAsync(GetName(assetId, fileVersion), stream =>
{
return assetFileStore.UploadAsync(appId, assetId, fileVersion, stream);
return assetFileStore.UploadAsync(appId, assetId, fileVersion, null, stream);
});
}
catch (FileNotFoundException)

80
backend/src/Squidex.Domain.Apps.Entities/Assets/DefaultAssetFileStore.cs

@ -27,53 +27,53 @@ namespace Squidex.Domain.Apps.Entities.Assets
this.options = options.Value;
}
public string? GeneratePublicUrl(DomainId appId, DomainId id, long fileVersion)
public string? GeneratePublicUrl(DomainId appId, DomainId id, long fileVersion, string? suffix)
{
var fileName = GetFileName(appId, id, fileVersion);
var fileName = GetFileName(appId, id, fileVersion, suffix);
return assetStore.GeneratePublicUrl(fileName);
}
public async Task<long> GetFileSizeAsync(DomainId appId, DomainId id, long fileVersion,
public async Task<long> GetFileSizeAsync(DomainId appId, DomainId id, long fileVersion, string? suffix,
CancellationToken ct = default)
{
try
{
var fileNameNew = GetFileName(appId, id, fileVersion);
var fileNameNew = GetFileName(appId, id, fileVersion, suffix);
return await assetStore.GetSizeAsync(fileNameNew, ct);
}
catch (AssetNotFoundException)
catch (AssetNotFoundException) when (!options.FolderPerApp)
{
var fileNameOld = GetFileName(id, fileVersion);
var fileNameOld = GetFileName(id, fileVersion, suffix);
return await assetStore.GetSizeAsync(fileNameOld, ct);
}
}
public async Task DownloadAsync(DomainId appId, DomainId id, long fileVersion, Stream stream, BytesRange range = default,
public async Task DownloadAsync(DomainId appId, DomainId id, long fileVersion, string? suffix, Stream stream, BytesRange range = default,
CancellationToken ct = default)
{
try
{
var fileNameNew = GetFileName(appId, id, fileVersion);
var fileNameNew = GetFileName(appId, id, fileVersion, suffix);
await assetStore.DownloadAsync(fileNameNew, stream, range, ct);
}
catch (AssetNotFoundException)
catch (AssetNotFoundException) when (!options.FolderPerApp)
{
var fileNameOld = GetFileName(id, fileVersion);
var fileNameOld = GetFileName(id, fileVersion, suffix);
await assetStore.DownloadAsync(fileNameOld, stream, range, ct);
}
}
public Task UploadAsync(DomainId appId, DomainId id, long fileVersion, Stream stream,
public Task UploadAsync(DomainId appId, DomainId id, long fileVersion, string? suffix, Stream stream, bool overwrite = true,
CancellationToken ct = default)
{
var fileName = GetFileName(appId, id, fileVersion);
var fileName = GetFileName(appId, id, fileVersion, suffix);
return assetStore.UploadAsync(fileName, stream, true, ct);
return assetStore.UploadAsync(fileName, stream, overwrite, ct);
}
public Task UploadAsync(string tempFile, Stream stream,
@ -82,22 +82,29 @@ namespace Squidex.Domain.Apps.Entities.Assets
return assetStore.UploadAsync(tempFile, stream, false, ct);
}
public Task CopyAsync(string tempFile, DomainId appId, DomainId id, long fileVersion,
public Task CopyAsync(string tempFile, DomainId appId, DomainId id, long fileVersion, string? suffix,
CancellationToken ct = default)
{
var fileName = GetFileName(appId, id, fileVersion);
var fileName = GetFileName(appId, id, fileVersion, suffix);
return assetStore.CopyAsync(tempFile, fileName, ct);
}
public async Task DeleteAsync(DomainId appId, DomainId id, long fileVersion)
public Task DeleteAsync(DomainId appId, DomainId id, long fileVersion, string? suffix)
{
var fileNameOld = GetFileName(id, fileVersion);
var fileNameNew = GetFileName(appId, id, fileVersion);
var fileNameOld = GetFileName(id, fileVersion, suffix);
var fileNameNew = GetFileName(appId, id, fileVersion, suffix);
await Task.WhenAll(
assetStore.DeleteAsync(fileNameOld),
assetStore.DeleteAsync(fileNameNew));
if (options.FolderPerApp)
{
return assetStore.DeleteAsync(fileNameNew);
}
else
{
return Task.WhenAll(
assetStore.DeleteAsync(fileNameOld),
assetStore.DeleteAsync(fileNameNew));
}
}
public Task DeleteAsync(string tempFile)
@ -105,20 +112,41 @@ namespace Squidex.Domain.Apps.Entities.Assets
return assetStore.DeleteAsync(tempFile);
}
private static string GetFileName(DomainId id, long fileVersion)
private static string GetFileName(DomainId id, long fileVersion, string? suffix)
{
return $"{id}_{fileVersion}";
if (!string.IsNullOrWhiteSpace(suffix))
{
return $"{id}_{fileVersion}_{suffix}";
}
else
{
return $"{id}_{fileVersion}";
}
}
private string GetFileName(DomainId appId, DomainId id, long fileVersion)
private string GetFileName(DomainId appId, DomainId id, long fileVersion, string? suffix)
{
if (options.FolderPerApp)
{
return $"{appId}/{id}_{fileVersion}";
if (!string.IsNullOrWhiteSpace(suffix))
{
return $"derived/{appId}/{id}_{fileVersion}_{suffix}";
}
else
{
return $"{appId}/{id}_{fileVersion}";
}
}
else
{
return $"{appId}_{id}_{fileVersion}";
if (!string.IsNullOrWhiteSpace(suffix))
{
return $"{appId}_{id}_{fileVersion}_{suffix}";
}
else
{
return $"{appId}_{id}_{fileVersion}";
}
}
}
}

2
backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetCommandMiddleware.cs

@ -142,7 +142,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
try
{
await assetFileStore.CopyAsync(tempFile, asset.AppId.Id, asset.AssetId, asset.FileVersion);
await assetFileStore.CopyAsync(tempFile, asset.AppId.Id, asset.AssetId, asset.FileVersion, null);
}
catch (AssetAlreadyExistsException) when (context.Command is not UpsertAsset)
{

12
backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetFileStore.cs

@ -15,20 +15,20 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
public interface IAssetFileStore
{
string? GeneratePublicUrl(DomainId appId, DomainId id, long fileVersion);
string? GeneratePublicUrl(DomainId appId, DomainId id, long fileVersion, string? suffix);
Task<long> GetFileSizeAsync(DomainId appId, DomainId id, long fileVersion, CancellationToken ct = default);
Task<long> GetFileSizeAsync(DomainId appId, DomainId id, long fileVersion, string? suffix, CancellationToken ct = default);
Task CopyAsync(string tempFile, DomainId appId, DomainId id, long fileVersion, CancellationToken ct = default);
Task CopyAsync(string tempFile, DomainId appId, DomainId id, long fileVersion, string? suffix, CancellationToken ct = default);
Task UploadAsync(string tempFile, Stream stream, CancellationToken ct = default);
Task UploadAsync(DomainId appId, DomainId id, long fileVersion, Stream stream, CancellationToken ct = default);
Task UploadAsync(DomainId appId, DomainId id, long fileVersion, string? suffix, Stream stream, bool overwrite = true, CancellationToken ct = default);
Task DownloadAsync(DomainId appId, DomainId id, long fileVersion, Stream stream, BytesRange range = default, CancellationToken ct = default);
Task DownloadAsync(DomainId appId, DomainId id, long fileVersion, string? suffix, Stream stream, BytesRange range = default, CancellationToken ct = default);
Task DeleteAsync(string tempFile);
Task DeleteAsync(DomainId appId, DomainId id, long fileVersion);
Task DeleteAsync(DomainId appId, DomainId id, long fileVersion, string? suffix);
}
}

4
backend/src/Squidex.Domain.Apps.Entities/Assets/RebuildFiles.cs

@ -58,13 +58,13 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
try
{
await assetFileStore.GetFileSizeAsync(appId.Id, id, fileVersion, ct);
await assetFileStore.GetFileSizeAsync(appId.Id, id, fileVersion, null, ct);
}
catch (AssetNotFoundException)
{
DummyStream.Position = 0;
await assetFileStore.UploadAsync(appId.Id, id, fileVersion, DummyStream, ct);
await assetFileStore.UploadAsync(appId.Id, id, fileVersion, null, DummyStream, ct: ct);
}
}
}

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

@ -17,7 +17,7 @@
<ProjectReference Include="..\Squidex.Shared\Squidex.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="CsvHelper" Version="27.1.0" />
<PackageReference Include="CsvHelper" Version="27.1.1" />
<PackageReference Include="Elasticsearch.Net" Version="7.13.1" />
<PackageReference Include="Equals.Fody" Version="4.0.1" PrivateAssets="all" />
<PackageReference Include="Fody" Version="6.5.1">

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

@ -10,7 +10,7 @@
<DebugSymbols>True</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RabbitMQ.Client" Version="6.2.1" />
<PackageReference Include="RabbitMQ.Client" Version="6.2.2" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />

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

@ -25,7 +25,7 @@
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="NJsonSchema" Version="10.4.4" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.Assets" Version="1.5.0" />
<PackageReference Include="Squidex.Assets" Version="1.8.0" />
<PackageReference Include="Squidex.Caching" Version="1.8.0" />
<PackageReference Include="Squidex.Hosting.Abstractions" Version="1.9.0" />
<PackageReference Include="Squidex.Log" Version="1.4.0" />

2
backend/src/Squidex.Web/Services/UrlGenerator.cs

@ -61,7 +61,7 @@ namespace Squidex.Web.Services
public string? AssetSource(NamedId<DomainId> appId, DomainId assetId, long fileVersion)
{
return assetFileStore.GeneratePublicUrl(appId.Id, assetId, fileVersion);
return assetFileStore.GeneratePublicUrl(appId.Id, assetId, fileVersion, null);
}
public string AssetsUI(NamedId<DomainId> appId, string? query = null)

23
backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs

@ -36,7 +36,6 @@ namespace Squidex.Areas.Api.Controllers.Assets
private readonly IAssetFileStore assetFileStore;
private readonly IAssetQueryService assetQuery;
private readonly IAssetLoader assetLoader;
private readonly IAssetStore assetStore;
private readonly IAssetThumbnailGenerator assetThumbnailGenerator;
public AssetContentController(
@ -44,14 +43,12 @@ namespace Squidex.Areas.Api.Controllers.Assets
IAssetFileStore assetFileStore,
IAssetQueryService assetQuery,
IAssetLoader assetLoader,
IAssetStore assetStore,
IAssetThumbnailGenerator assetThumbnailGenerator)
: base(commandBus)
{
this.assetFileStore = assetFileStore;
this.assetQuery = assetQuery;
this.assetLoader = assetLoader;
this.assetStore = assetStore;
this.assetThumbnailGenerator = assetThumbnailGenerator;
}
@ -162,21 +159,21 @@ namespace Squidex.Areas.Api.Controllers.Assets
{
callback = async (bodyStream, range, ct) =>
{
var resizedAsset = $"{asset.AppId.Id}_{asset.Id}_{asset.FileVersion}_{resizeOptions}";
if (request.ForceResize)
{
await ResizeAsync(asset, bodyStream, resizedAsset, resizeOptions, true, ct);
await ResizeAsync(asset, bodyStream, resizeOptions, true, ct);
}
else
{
try
{
await assetStore.DownloadAsync(resizedAsset, bodyStream, ct: ct);
var suffix = resizeOptions.ToString();
await assetFileStore.DownloadAsync(asset.AppId.Id, asset.Id, asset.FileVersion, suffix, bodyStream, ct: ct);
}
catch (AssetNotFoundException)
{
await ResizeAsync(asset, bodyStream, resizedAsset, resizeOptions, false, ct);
await ResizeAsync(asset, bodyStream, resizeOptions, false, ct);
}
}
};
@ -187,7 +184,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
callback = async (bodyStream, range, ct) =>
{
await assetFileStore.DownloadAsync(asset.AppId.Id, asset.Id, asset.FileVersion, bodyStream, range, ct);
await assetFileStore.DownloadAsync(asset.AppId.Id, asset.Id, asset.FileVersion, null, bodyStream, range, ct);
};
}
@ -202,8 +199,10 @@ namespace Squidex.Areas.Api.Controllers.Assets
};
}
private async Task ResizeAsync(IAssetEntity asset, Stream bodyStream, string fileName, ResizeOptions resizeOptions, bool overwrite, CancellationToken ct)
private async Task ResizeAsync(IAssetEntity asset, Stream bodyStream, ResizeOptions resizeOptions, bool overwrite, CancellationToken ct)
{
var suffix = resizeOptions.ToString();
using (Profiler.Trace("Resize"))
{
using (var sourceStream = GetTempStream())
@ -212,7 +211,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
{
using (Profiler.Trace("ResizeDownload"))
{
await assetFileStore.DownloadAsync(asset.AppId.Id, asset.Id, asset.FileVersion, sourceStream);
await assetFileStore.DownloadAsync(asset.AppId.Id, asset.Id, asset.FileVersion, null, sourceStream);
sourceStream.Position = 0;
}
@ -234,7 +233,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
{
using (Profiler.Trace("ResizeUpload"))
{
await assetStore.UploadAsync(fileName, destinationStream, overwrite);
await assetFileStore.UploadAsync(asset.AppId.Id, asset.Id, asset.FileVersion, suffix, destinationStream, overwrite);
destinationStream.Position = 0;
}
}

12
backend/src/Squidex/Squidex.csproj

@ -59,12 +59,12 @@
<PackageReference Include="Orleans.Providers.MongoDB" Version="3.3.1" />
<PackageReference Include="OrleansDashboard" Version="3.1.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="ReportGenerator" Version="4.8.10" PrivateAssets="all" />
<PackageReference Include="Squidex.Assets.Azure" Version="1.5.0" />
<PackageReference Include="Squidex.Assets.GoogleCloud" Version="1.5.0" />
<PackageReference Include="Squidex.Assets.FTP" Version="1.5.0" />
<PackageReference Include="Squidex.Assets.Mongo" Version="1.5.0" />
<PackageReference Include="Squidex.Assets.S3" Version="1.5.0" />
<PackageReference Include="ReportGenerator" Version="4.8.11" PrivateAssets="all" />
<PackageReference Include="Squidex.Assets.Azure" Version="1.8.0" />
<PackageReference Include="Squidex.Assets.GoogleCloud" Version="1.8.0" />
<PackageReference Include="Squidex.Assets.FTP" Version="1.8.0" />
<PackageReference Include="Squidex.Assets.Mongo" Version="1.8.0" />
<PackageReference Include="Squidex.Assets.S3" Version="1.8.0" />
<PackageReference Include="Squidex.Caching.Orleans" Version="1.8.0" />
<PackageReference Include="Squidex.ClientLibrary" Version="7.4.0" />
<PackageReference Include="Squidex.Hosting" Version="1.9.0" />

14
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetPermanentDeleterTests.cs

@ -61,7 +61,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
await sut.On(Envelope.Create(@event).SetRestored());
A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, A<long>._))
A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, A<long>._, null))
.MustNotHaveHappened();
}
@ -72,10 +72,10 @@ namespace Squidex.Domain.Apps.Entities.Assets
await sut.On(Envelope.Create(@event).SetEventStreamNumber(2));
A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, 0))
A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, 0, null))
.MustHaveHappened();
A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, 1))
A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, 1, null))
.MustHaveHappened();
}
@ -84,12 +84,12 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
var @event = new AssetDeleted { AppId = appId, AssetId = DomainId.NewGuid() };
A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, 0))
A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, 0, null))
.Throws(new AssetNotFoundException("fileName"));
await sut.On(Envelope.Create(@event).SetEventStreamNumber(2));
A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, 1))
A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, 1, null))
.MustHaveHappened();
}
@ -98,12 +98,12 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
var @event = new AssetDeleted { AppId = appId, AssetId = DomainId.NewGuid() };
A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, 0))
A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, 0, null))
.Throws(new InvalidOperationException());
await Assert.ThrowsAsync<InvalidOperationException>(() => sut.On(Envelope.Create(@event).SetEventStreamNumber(2)));
A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, 1))
A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, 1, null))
.MustNotHaveHappened();
}
}

8
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsFluidExtensionTests.cs

@ -217,7 +217,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
Assert.Equal(Cleanup(expected), Cleanup(result));
A.CallTo(() => assetFileStore.DownloadAsync(A<DomainId>._, A<DomainId>._, A<long>._, A<Stream>._, A<BytesRange>._, A<CancellationToken>._))
A.CallTo(() => assetFileStore.DownloadAsync(A<DomainId>._, A<DomainId>._, A<long>._, null, A<Stream>._, A<BytesRange>._, A<CancellationToken>._))
.MustNotHaveHappened();
}
@ -280,16 +280,16 @@ namespace Squidex.Domain.Apps.Entities.Assets
Assert.Equal(Cleanup(expected), Cleanup(result));
A.CallTo(() => assetFileStore.DownloadAsync(A<DomainId>._, A<DomainId>._, A<long>._, A<Stream>._, A<BytesRange>._, A<CancellationToken>._))
A.CallTo(() => assetFileStore.DownloadAsync(A<DomainId>._, A<DomainId>._, A<long>._, null, A<Stream>._, A<BytesRange>._, A<CancellationToken>._))
.MustNotHaveHappened();
}
private void SetupText(DomainId id, byte[] bytes)
{
A.CallTo(() => assetFileStore.DownloadAsync(appId.Id, id, 0, A<Stream>._, A<BytesRange>._, A<CancellationToken>._))
A.CallTo(() => assetFileStore.DownloadAsync(appId.Id, id, 0, null, A<Stream>._, A<BytesRange>._, A<CancellationToken>._))
.Invokes(x =>
{
var stream = x.GetArgument<Stream>(3)!;
var stream = x.GetArgument<Stream>(4)!;
stream.Write(bytes);
});

8
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs

@ -250,7 +250,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
Assert.Equal(Cleanup(expected), Cleanup(result));
A.CallTo(() => assetFileStore.DownloadAsync(A<DomainId>._, A<DomainId>._, A<long>._, A<Stream>._, A<BytesRange>._, A<CancellationToken>._))
A.CallTo(() => assetFileStore.DownloadAsync(A<DomainId>._, A<DomainId>._, A<long>._, null, A<Stream>._, A<BytesRange>._, A<CancellationToken>._))
.MustNotHaveHappened();
}
@ -319,16 +319,16 @@ namespace Squidex.Domain.Apps.Entities.Assets
Assert.Equal(Cleanup(expected), Cleanup(result));
A.CallTo(() => assetFileStore.DownloadAsync(A<DomainId>._, A<DomainId>._, A<long>._, A<Stream>._, A<BytesRange>._, A<CancellationToken>._))
A.CallTo(() => assetFileStore.DownloadAsync(A<DomainId>._, A<DomainId>._, A<long>._, null, A<Stream>._, A<BytesRange>._, A<CancellationToken>._))
.MustNotHaveHappened();
}
private void SetupText(DomainId id, byte[] bytes)
{
A.CallTo(() => assetFileStore.DownloadAsync(appId.Id, id, 0, A<Stream>._, A<BytesRange>._, A<CancellationToken>._))
A.CallTo(() => assetFileStore.DownloadAsync(appId.Id, id, 0, null, A<Stream>._, A<BytesRange>._, A<CancellationToken>._))
.Invokes(x =>
{
var stream = x.GetArgument<Stream>(3)!;
var stream = x.GetArgument<Stream>(4)!;
stream.Write(bytes);
});

8
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/BackupAssetsTests.cs

@ -119,7 +119,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
await sut.BackupEventAsync(AppEvent(@event), context);
A.CallTo(() => assetFileStore.DownloadAsync(appId.Id, assetId, version, assetStream, default, default))
A.CallTo(() => assetFileStore.DownloadAsync(appId.Id, assetId, version, null, assetStream, default, default))
.MustHaveHappened();
}
@ -133,7 +133,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
A.CallTo(() => context.Writer.WriteBlobAsync($"{assetId}_{version}.asset", A<Func<Stream, Task>>._))
.Invokes((string _, Func<Stream, Task> handler) => handler(assetStream));
A.CallTo(() => assetFileStore.DownloadAsync(appId.Id, assetId, version, assetStream, default, default))
A.CallTo(() => assetFileStore.DownloadAsync(appId.Id, assetId, version, null, assetStream, default, default))
.Throws(new AssetNotFoundException(assetId.ToString()));
await sut.BackupEventAsync(AppEvent(@event), context);
@ -183,7 +183,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
await sut.RestoreEventAsync(AppEvent(@event), context);
A.CallTo(() => assetFileStore.UploadAsync(appId.Id, assetId, version, assetStream, default))
A.CallTo(() => assetFileStore.UploadAsync(appId.Id, assetId, version, null, assetStream, true, default))
.MustHaveHappened();
}
@ -199,7 +199,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
await sut.RestoreEventAsync(AppEvent(@event), context);
A.CallTo(() => assetFileStore.UploadAsync(appId.Id, assetId, version, assetStream, default))
A.CallTo(() => assetFileStore.UploadAsync(appId.Id, assetId, version, null, assetStream, true, default))
.MustNotHaveHappened();
}

158
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DefaultAssetFileStoreTests.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
@ -22,59 +23,79 @@ namespace Squidex.Domain.Apps.Entities.Assets
private readonly DomainId appId = DomainId.NewGuid();
private readonly DomainId assetId = DomainId.NewGuid();
private readonly long assetFileVersion = 21;
private readonly string fileNameOld;
private readonly string fileNameNew;
private readonly string fileNameFolder;
private readonly AssetOptions options = new AssetOptions();
private readonly DefaultAssetFileStore sut;
public DefaultAssetFileStoreTests()
{
fileNameOld = $"{assetId}_{assetFileVersion}";
fileNameNew = $"{appId}_{assetId}_{assetFileVersion}";
fileNameFolder = $"{appId}/{assetId}_{assetFileVersion}";
sut = new DefaultAssetFileStore(assetStore, Options.Create(options));
}
[Fact]
public void Should_get_public_url_from_store()
public static IEnumerable<object[]> PathCases()
{
yield return new object[] { true, "resize=100", "derived/{appId}/{assetId}_{assetFileVersion}_resize=100" };
yield return new object[] { true, string.Empty, "{appId}/{assetId}_{assetFileVersion}" };
yield return new object[] { false, "resize=100", "{appId}_{assetId}_{assetFileVersion}_resize=100" };
yield return new object[] { false, string.Empty, "{appId}_{assetId}_{assetFileVersion}" };
}
public static IEnumerable<object?[]> PathCasesOld()
{
yield return new object?[] { "resize=100", "{assetId}_{assetFileVersion}_resize=100" };
yield return new object?[] { string.Empty, "{assetId}_{assetFileVersion}" };
}
[Theory]
[MemberData(nameof(PathCases))]
public void Should_get_public_url_from_store(bool folderPerApp, string? suffix, string fileName)
{
var fullName = GetFullName(fileName);
options.FolderPerApp = folderPerApp;
var url = "http_//squidex.io/assets";
A.CallTo(() => assetStore.GeneratePublicUrl(fileNameNew))
A.CallTo(() => assetStore.GeneratePublicUrl(fullName))
.Returns(url);
var result = sut.GeneratePublicUrl(appId, assetId, assetFileVersion);
var result = sut.GeneratePublicUrl(appId, assetId, assetFileVersion, suffix);
Assert.Equal(url, result);
}
[Fact]
public async Task Should_get_file_size_from_store()
[Theory]
[MemberData(nameof(PathCases))]
public async Task Should_get_file_size_from_store(bool folderPerApp, string? suffix, string fileName)
{
var fullName = GetFullName(fileName);
options.FolderPerApp = folderPerApp;
var size = 1024L;
A.CallTo(() => assetStore.GetSizeAsync(fileNameNew, default))
A.CallTo(() => assetStore.GetSizeAsync(fullName, default))
.Returns(size);
var result = await sut.GetFileSizeAsync(appId, assetId, assetFileVersion);
var result = await sut.GetFileSizeAsync(appId, assetId, assetFileVersion, suffix);
Assert.Equal(size, result);
}
[Fact]
public async Task Should_get_file_size_from_store_with_old_file_name_if_new_name_not_found()
[Theory]
[MemberData(nameof(PathCasesOld))]
public async Task Should_get_file_size_from_store_with_old_file_name_if_new_name_not_found(string? suffix, string fileName)
{
var fullName = GetFullName(fileName);
var size = 1024L;
A.CallTo(() => assetStore.GetSizeAsync(fileNameOld, default))
.Throws(new AssetNotFoundException(fileNameOld));
A.CallTo(() => assetStore.GetSizeAsync(A<string>._, default))
.Throws(new AssetNotFoundException(assetId.ToString()));
A.CallTo(() => assetStore.GetSizeAsync(fileNameNew, default))
A.CallTo(() => assetStore.GetSizeAsync(fullName, default))
.Returns(size);
var result = await sut.GetFileSizeAsync(appId, assetId, assetFileVersion);
var result = await sut.GetFileSizeAsync(appId, assetId, assetFileVersion, suffix);
Assert.Equal(size, result);
}
@ -90,66 +111,82 @@ namespace Squidex.Domain.Apps.Entities.Assets
.MustHaveHappened();
}
[Fact]
public async Task Should_upload_file_to_store()
[Theory]
[MemberData(nameof(PathCases))]
public async Task Should_upload_file_to_store(bool folderPerApp, string? suffix, string fileName)
{
options.FolderPerApp = false;
var fullName = GetFullName(fileName);
options.FolderPerApp = folderPerApp;
var stream = new MemoryStream();
await sut.UploadAsync(appId, assetId, assetFileVersion, stream);
await sut.UploadAsync(appId, assetId, assetFileVersion, suffix, stream);
A.CallTo(() => assetStore.UploadAsync(fileNameNew, stream, true, CancellationToken.None))
A.CallTo(() => assetStore.UploadAsync(fullName, stream, true, CancellationToken.None))
.MustHaveHappened();
}
[Fact]
public async Task Should_upload_file_to_store_with_folder()
[Theory]
[MemberData(nameof(PathCases))]
public async Task Should_download_file_from_store(bool folderPerApp, string? suffix, string fileName)
{
options.FolderPerApp = true;
var fullName = GetFullName(fileName);
options.FolderPerApp = folderPerApp;
var stream = new MemoryStream();
await sut.UploadAsync(appId, assetId, assetFileVersion, stream);
await sut.DownloadAsync(appId, assetId, assetFileVersion, suffix, stream);
A.CallTo(() => assetStore.UploadAsync(fileNameFolder, stream, true, CancellationToken.None))
A.CallTo(() => assetStore.DownloadAsync(fullName, stream, default, CancellationToken.None))
.MustHaveHappened();
}
[Fact]
public async Task Should_download_file_from_store()
public async Task Should_download_file_from_store_with_folder_only_if_configured()
{
options.FolderPerApp = true;
var stream = new MemoryStream();
await sut.DownloadAsync(appId, assetId, assetFileVersion, stream);
A.CallTo(() => assetStore.DownloadAsync(A<string>._, stream, default, CancellationToken.None))
.Throws(new AssetNotFoundException(assetId.ToString())).Once();
A.CallTo(() => assetStore.DownloadAsync(fileNameNew, stream, default, CancellationToken.None))
.MustHaveHappened();
await Assert.ThrowsAsync<AssetNotFoundException>(() => sut.DownloadAsync(appId, assetId, assetFileVersion, null, stream));
A.CallTo(() => assetStore.DownloadAsync(A<string>._, stream, default, CancellationToken.None))
.MustHaveHappenedOnceExactly();
}
[Fact]
public async Task Should_download_file_from_store_with_old_file_name_if_new_name_not_found()
[Theory]
[MemberData(nameof(PathCasesOld))]
public async Task Should_download_file_from_store_with_old_file_name_if_new_name_not_found(string suffix, string fileName)
{
var fullName = GetFullName(fileName);
var stream = new MemoryStream();
A.CallTo(() => assetStore.DownloadAsync(fileNameNew, stream, default, CancellationToken.None))
.Throws(new AssetNotFoundException(fileNameNew));
A.CallTo(() => assetStore.DownloadAsync(A<string>.That.Matches(x => x != fileName), stream, default, CancellationToken.None))
.Throws(new AssetNotFoundException(assetId.ToString())).Once();
await sut.DownloadAsync(appId, assetId, assetFileVersion, stream);
await sut.DownloadAsync(appId, assetId, assetFileVersion, suffix, stream);
A.CallTo(() => assetStore.DownloadAsync(fileNameOld, stream, default, CancellationToken.None))
.MustHaveHappened();
A.CallTo(() => assetStore.DownloadAsync(fileNameNew, stream, default, CancellationToken.None))
A.CallTo(() => assetStore.DownloadAsync(fullName, stream, default, CancellationToken.None))
.MustHaveHappened();
}
[Fact]
public async Task Should_copy_file_to_store()
[Theory]
[MemberData(nameof(PathCases))]
public async Task Should_copy_file_to_store(bool folderPerApp, string? suffix, string fileName)
{
await sut.CopyAsync("Temp", appId, assetId, assetFileVersion);
var fullName = GetFullName(fileName);
A.CallTo(() => assetStore.CopyAsync("Temp", fileNameNew, CancellationToken.None))
options.FolderPerApp = folderPerApp;
await sut.CopyAsync("Temp", appId, assetId, assetFileVersion, suffix);
A.CallTo(() => assetStore.CopyAsync("Temp", fullName, CancellationToken.None))
.MustHaveHappened();
}
@ -162,16 +199,29 @@ namespace Squidex.Domain.Apps.Entities.Assets
.MustHaveHappened();
}
[Fact]
public async Task Should_delete_file_from_store()
[Theory]
[MemberData(nameof(PathCases))]
public async Task Should_delete_file_from_store(bool folderPerApp, string? suffix, string fileName)
{
await sut.DeleteAsync(appId, assetId, assetFileVersion);
var fullName = GetFullName(fileName);
A.CallTo(() => assetStore.DeleteAsync(fileNameNew))
.MustHaveHappened();
options.FolderPerApp = folderPerApp;
await sut.DeleteAsync(appId, assetId, assetFileVersion, suffix);
A.CallTo(() => assetStore.DeleteAsync(fileNameOld))
A.CallTo(() => assetStore.DeleteAsync(fullName))
.MustHaveHappened();
A.CallTo(() => assetStore.DeleteAsync(A<string>._))
.MustHaveHappenedANumberOfTimesMatching(x => x == (folderPerApp ? 1 : 2));
}
private string GetFullName(string fileName)
{
return fileName
.Replace("{appId}", appId.ToString())
.Replace("{assetId}", assetId.ToString())
.Replace("{assetFileVersion}", assetFileVersion.ToString());
}
}
}

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetCommandMiddlewareTests.cs

@ -192,7 +192,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
{
A.CallTo(() => assetFileStore.UploadAsync(A<string>._, A<HasherStream>._, CancellationToken.None))
.MustHaveHappened();
A.CallTo(() => assetFileStore.CopyAsync(A<string>._, AppId, assetId, fileVersion, CancellationToken.None))
A.CallTo(() => assetFileStore.CopyAsync(A<string>._, AppId, assetId, fileVersion, null, CancellationToken.None))
.MustHaveHappened();
A.CallTo(() => assetFileStore.DeleteAsync(A<string>._))
.MustHaveHappened();

18
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/RepairFilesTests.cs

@ -38,12 +38,12 @@ namespace Squidex.Domain.Apps.Entities.Assets
SetupEvent(@event);
A.CallTo(() => assetFileStore.GetFileSizeAsync(appId.Id, @event.AssetId, 0, default))
A.CallTo(() => assetFileStore.GetFileSizeAsync(appId.Id, @event.AssetId, 0, null, default))
.Throws(new AssetNotFoundException("file"));
await sut.RepairAsync();
A.CallTo(() => assetFileStore.UploadAsync(appId.Id, @event.AssetId, 0, A<Stream>._, default))
A.CallTo(() => assetFileStore.UploadAsync(appId.Id, @event.AssetId, 0, null, A<Stream>._, true, default))
.MustHaveHappened();
}
@ -54,12 +54,12 @@ namespace Squidex.Domain.Apps.Entities.Assets
SetupEvent(@event);
A.CallTo(() => assetFileStore.GetFileSizeAsync(appId.Id, @event.AssetId, 0, default))
A.CallTo(() => assetFileStore.GetFileSizeAsync(appId.Id, @event.AssetId, 0, null, default))
.Returns(100);
await sut.RepairAsync();
A.CallTo(() => assetFileStore.UploadAsync(appId.Id, @event.AssetId, 0, A<Stream>._, default))
A.CallTo(() => assetFileStore.UploadAsync(appId.Id, @event.AssetId, 0, null, A<Stream>._, true, default))
.MustNotHaveHappened();
}
@ -70,12 +70,12 @@ namespace Squidex.Domain.Apps.Entities.Assets
SetupEvent(@event);
A.CallTo(() => assetFileStore.GetFileSizeAsync(appId.Id, @event.AssetId, 3, default))
A.CallTo(() => assetFileStore.GetFileSizeAsync(appId.Id, @event.AssetId, 3, null, default))
.Throws(new AssetNotFoundException("file"));
await sut.RepairAsync();
A.CallTo(() => assetFileStore.UploadAsync(appId.Id, @event.AssetId, 3, A<Stream>._, default))
A.CallTo(() => assetFileStore.UploadAsync(appId.Id, @event.AssetId, 3, null, A<Stream>._, true, default))
.MustHaveHappened();
}
@ -86,12 +86,12 @@ namespace Squidex.Domain.Apps.Entities.Assets
SetupEvent(@event);
A.CallTo(() => assetFileStore.GetFileSizeAsync(appId.Id, @event.AssetId, 3, default))
A.CallTo(() => assetFileStore.GetFileSizeAsync(appId.Id, @event.AssetId, 3, null, default))
.Returns(100);
await sut.RepairAsync();
A.CallTo(() => assetFileStore.UploadAsync(appId.Id, @event.AssetId, 3, A<Stream>._, default))
A.CallTo(() => assetFileStore.UploadAsync(appId.Id, @event.AssetId, 3, null, A<Stream>._, true, default))
.MustNotHaveHappened();
}
@ -102,7 +102,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
await sut.RepairAsync();
A.CallTo(() => assetFileStore.GetFileSizeAsync(A<DomainId>._, A<DomainId>._, A<long>._, default))
A.CallTo(() => assetFileStore.GetFileSizeAsync(A<DomainId>._, A<DomainId>._, A<long>._, null, default))
.MustNotHaveHappened();
}

Loading…
Cancel
Save