Browse Source

Feature/range (#504)

* Range support.

* Asset range support.

* Fix for older asset versions.

* Final fixes.
pull/507/head
Sebastian Stehle 6 years ago
committed by GitHub
parent
commit
3ecc89681a
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      backend/src/Squidex.Domain.Apps.Entities/Assets/DefaultAssetFileStore.cs
  2. 2
      backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetFileStore.cs
  3. 74
      backend/src/Squidex.Infrastructure.Amazon/Assets/AmazonS3AssetStore.cs
  4. 22
      backend/src/Squidex.Infrastructure.Azure/Assets/AzureBlobAssetStore.cs
  5. 21
      backend/src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs
  6. 14
      backend/src/Squidex.Infrastructure.MongoDb/Assets/MongoGridFsAssetStore.cs
  7. 34
      backend/src/Squidex.Infrastructure/Assets/FTPAssetStore.cs
  8. 31
      backend/src/Squidex.Infrastructure/Assets/FolderAssetStore.cs
  9. 2
      backend/src/Squidex.Infrastructure/Assets/IAssetStore.cs
  10. 15
      backend/src/Squidex.Infrastructure/Assets/MemoryAssetStore.cs
  11. 5
      backend/src/Squidex.Infrastructure/Assets/NoopAssetStore.cs
  12. 15
      backend/src/Squidex.Web/FileCallbackResult.cs
  13. 4
      backend/src/Squidex.Web/Pipeline/FileCallbackResultExecutor.cs
  14. 4
      backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs
  15. 77
      backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs
  16. 4
      backend/src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs
  17. 4
      backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs
  18. 3
      backend/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs
  19. 13
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DefaultAssetFileStoreTests.cs
  20. 16
      backend/tests/Squidex.Infrastructure.Tests/Assets/AssetStoreTests.cs
  21. 6
      frontend/app/shared/components/assets/pipes.ts

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

@ -30,6 +30,13 @@ namespace Squidex.Domain.Apps.Entities.Assets
return assetStore.GeneratePublicUrl(fileName);
}
public Task<long> GetFileSizeAsync(Guid id, long fileVersion, CancellationToken ct = default)
{
var fileName = GetFileName(id, fileVersion);
return assetStore.GetSizeAsync(fileName, ct);
}
public Task UploadAsync(Guid id, long fileVersion, Stream stream, CancellationToken ct = default)
{
var fileName = GetFileName(id, fileVersion);

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

@ -17,6 +17,8 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
string? GeneratePublicUrl(Guid id, long fileVersion);
Task<long> GetFileSizeAsync(Guid id, long fileVersion, CancellationToken ct = default);
Task CopyAsync(string tempFile, Guid id, long fileVersion, CancellationToken ct = default);
Task UploadAsync(string tempFile, Stream stream, CancellationToken ct = default);

74
backend/src/Squidex.Infrastructure.Amazon/Assets/AmazonS3AssetStore.cs

@ -79,21 +79,43 @@ namespace Squidex.Infrastructure.Assets
return null;
}
public async Task<long> GetSizeAsync(string fileName, CancellationToken ct = default)
{
var key = GetKey(fileName, nameof(fileName));
try
{
var request = new GetObjectMetadataRequest
{
BucketName = options.Bucket,
Key = key
};
var metadata = await s3Client.GetObjectMetadataAsync(request, ct);
return metadata.ContentLength;
}
catch (AmazonS3Exception ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
throw new AssetNotFoundException(fileName, ex);
}
}
public async Task CopyAsync(string sourceFileName, string targetFileName, CancellationToken ct = default)
{
Guard.NotNullOrEmpty(sourceFileName);
Guard.NotNullOrEmpty(targetFileName);
var sourceKey = GetKey(sourceFileName, nameof(sourceFileName));
var targetKey = GetKey(targetFileName, nameof(targetFileName));
try
{
await EnsureNotExistsAsync(targetFileName, ct);
await EnsureNotExistsAsync(targetKey, targetFileName, ct);
var request = new CopyObjectRequest
{
SourceBucket = options.Bucket,
SourceKey = GetKey(sourceFileName),
SourceKey = sourceKey,
DestinationBucket = options.Bucket,
DestinationKey = GetKey(targetFileName)
DestinationKey = targetKey
};
await s3Client.CopyObjectAsync(request, ct);
@ -110,12 +132,17 @@ namespace Squidex.Infrastructure.Assets
public async Task DownloadAsync(string fileName, Stream stream, BytesRange range = default, CancellationToken ct = default)
{
Guard.NotNullOrEmpty(fileName);
Guard.NotNull(stream);
var key = GetKey(fileName, nameof(fileName));
try
{
var request = new GetObjectRequest { BucketName = options.Bucket, Key = GetKey(fileName) };
var request = new GetObjectRequest
{
BucketName = options.Bucket,
Key = key
};
if (range.IsDefined)
{
@ -135,23 +162,23 @@ namespace Squidex.Infrastructure.Assets
public async Task UploadAsync(string fileName, Stream stream, bool overwrite = false, CancellationToken ct = default)
{
Guard.NotNullOrEmpty(fileName);
Guard.NotNull(stream);
var key = GetKey(fileName, nameof(fileName));
try
{
if (!overwrite)
{
await EnsureNotExistsAsync(fileName, ct);
await EnsureNotExistsAsync(key, fileName, ct);
}
var request = new TransferUtilityUploadRequest
{
Key = GetKey(fileName)
BucketName = options.Bucket,
Key = key
};
ConfigureDefaults(request);
SetStream(stream, request);
await transferUtility.UploadAsync(request, ct);
@ -164,11 +191,15 @@ namespace Squidex.Infrastructure.Assets
public async Task DeleteAsync(string fileName)
{
Guard.NotNullOrEmpty(fileName);
var key = GetKey(fileName, nameof(fileName));
try
{
var request = new DeleteObjectRequest { BucketName = options.Bucket, Key = fileName };
var request = new DeleteObjectRequest
{
BucketName = options.Bucket,
Key = key
};
await s3Client.DeleteObjectAsync(request);
}
@ -178,8 +209,10 @@ namespace Squidex.Infrastructure.Assets
}
}
private string GetKey(string fileName)
private string GetKey(string fileName, string parameterName)
{
Guard.NotNullOrEmpty(fileName, parameterName);
if (!string.IsNullOrWhiteSpace(options.BucketFolder))
{
return $"{options.BucketFolder}/{fileName}";
@ -190,11 +223,11 @@ namespace Squidex.Infrastructure.Assets
}
}
private async Task EnsureNotExistsAsync(string fileName, CancellationToken ct)
private async Task EnsureNotExistsAsync(string key, string fileName, CancellationToken ct)
{
try
{
await s3Client.GetObjectAsync(options.Bucket, GetKey(fileName), ct);
await s3Client.GetObjectAsync(options.Bucket, key, ct);
}
catch
{
@ -204,16 +237,11 @@ namespace Squidex.Infrastructure.Assets
throw new AssetAlreadyExistsException(fileName);
}
private void ConfigureDefaults(TransferUtilityUploadRequest request)
{
request.AutoCloseStream = false;
request.BucketName = options.Bucket;
}
private static void SetStream(Stream stream, TransferUtilityUploadRequest request)
{
// Amazon S3 requires a seekable stream, but does not seek anything.
request.InputStream = new SeekFakerStream(stream);
request.AutoCloseStream = false;
}
}
}

22
backend/src/Squidex.Infrastructure.Azure/Assets/AzureBlobAssetStore.cs

@ -62,6 +62,24 @@ namespace Squidex.Infrastructure.Assets
return null;
}
public async Task<long> GetSizeAsync(string fileName, CancellationToken ct = default)
{
Guard.NotNullOrEmpty(fileName);
try
{
var blob = blobContainer.GetBlockBlobReference(fileName);
await blob.FetchAttributesAsync();
return blob.Properties.Length;
}
catch (StorageException ex) when (ex.RequestInformation.HttpStatusCode == 404)
{
throw new AssetNotFoundException(fileName, ex);
}
}
public async Task CopyAsync(string sourceFileName, string targetFileName, CancellationToken ct = default)
{
Guard.NotNullOrEmpty(sourceFileName);
@ -90,7 +108,7 @@ namespace Squidex.Infrastructure.Assets
}
catch (StorageException ex) when (ex.RequestInformation.HttpStatusCode == 409)
{
throw new AssetAlreadyExistsException(targetFileName);
throw new AssetAlreadyExistsException(targetFileName, ex);
}
catch (StorageException ex) when (ex.RequestInformation.HttpStatusCode == 404)
{
@ -130,7 +148,7 @@ namespace Squidex.Infrastructure.Assets
}
catch (StorageException ex) when (ex.RequestInformation.HttpStatusCode == 409)
{
throw new AssetAlreadyExistsException(fileName);
throw new AssetAlreadyExistsException(fileName, ex);
}
}

21
backend/src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs

@ -49,6 +49,27 @@ namespace Squidex.Infrastructure.Assets
return null;
}
public async Task<long> GetSizeAsync(string fileName, CancellationToken ct = default)
{
Guard.NotNullOrEmpty(fileName);
try
{
var obj = await storageClient.GetObjectAsync(bucketName, fileName, null, ct);
if (!obj.Size.HasValue)
{
throw new AssetNotFoundException(fileName);
}
return (long)obj.Size.Value;
}
catch (GoogleApiException ex) when (ex.HttpStatusCode == HttpStatusCode.NotFound)
{
throw new AssetNotFoundException(fileName, ex);
}
}
public async Task CopyAsync(string sourceFileName, string targetFileName, CancellationToken ct = default)
{
Guard.NotNullOrEmpty(sourceFileName);

14
backend/src/Squidex.Infrastructure.MongoDb/Assets/MongoGridFsAssetStore.cs

@ -45,6 +45,20 @@ namespace Squidex.Infrastructure.Assets
return null;
}
public async Task<long> GetSizeAsync(string fileName, CancellationToken ct = default)
{
var name = GetFileName(fileName, nameof(fileName));
var file = await bucket.Find(Builders<GridFSFileInfo<string>>.Filter.Eq(x => x.Id, name)).FirstOrDefaultAsync();
if (file == null)
{
throw new AssetNotFoundException(fileName);
}
return file.Length;
}
public async Task CopyAsync(string sourceFileName, string targetFileName, CancellationToken ct = default)
{
Guard.NotNullOrEmpty(targetFileName);

34
backend/src/Squidex.Infrastructure/Assets/FTPAssetStore.cs

@ -34,11 +34,6 @@ namespace Squidex.Infrastructure.Assets
this.log = log;
}
public string? GeneratePublicUrl(string fileName)
{
return null;
}
public async Task InitializeAsync(CancellationToken ct = default)
{
using (var client = factory())
@ -56,6 +51,35 @@ namespace Squidex.Infrastructure.Assets
.WriteProperty("path", path));
}
public string? GeneratePublicUrl(string fileName)
{
return null;
}
public async Task<long> GetSizeAsync(string fileName, CancellationToken ct = default)
{
Guard.NotNullOrEmpty(fileName);
using (var client = GetFtpClient())
{
try
{
var size = await client.GetFileSizeAsync(fileName, ct);
if (size < 0)
{
throw new AssetNotFoundException(fileName);
}
return size;
}
catch (FtpException ex) when (IsNotFound(ex))
{
throw new AssetNotFoundException(fileName, ex);
}
}
}
public async Task CopyAsync(string sourceFileName, string targetFileName, CancellationToken ct = default)
{
Guard.NotNullOrEmpty(sourceFileName);

31
backend/src/Squidex.Infrastructure/Assets/FolderAssetStore.cs

@ -55,13 +55,24 @@ namespace Squidex.Infrastructure.Assets
return null;
}
public Task CopyAsync(string sourceFileName, string targetFileName, CancellationToken ct = default)
public Task<long> GetSizeAsync(string fileName, CancellationToken ct = default)
{
Guard.NotNullOrEmpty(sourceFileName);
Guard.NotNullOrEmpty(targetFileName);
var file = GetFile(fileName, nameof(fileName));
try
{
return Task.FromResult(file.Length);
}
catch (FileNotFoundException ex)
{
throw new AssetNotFoundException(fileName, ex);
}
}
var targetFile = GetFile(targetFileName);
var sourceFile = GetFile(sourceFileName);
public Task CopyAsync(string sourceFileName, string targetFileName, CancellationToken ct = default)
{
var targetFile = GetFile(targetFileName, nameof(targetFileName));
var sourceFile = GetFile(sourceFileName, nameof(sourceFileName));
try
{
@ -83,7 +94,7 @@ namespace Squidex.Infrastructure.Assets
{
Guard.NotNull(stream);
var file = GetFile(fileName);
var file = GetFile(fileName, nameof(fileName));
try
{
@ -102,7 +113,7 @@ namespace Squidex.Infrastructure.Assets
{
Guard.NotNull(stream);
var file = GetFile(fileName);
var file = GetFile(fileName, nameof(fileName));
try
{
@ -119,16 +130,16 @@ namespace Squidex.Infrastructure.Assets
public Task DeleteAsync(string fileName)
{
var file = GetFile(fileName);
var file = GetFile(fileName, nameof(fileName));
file.Delete();
return Task.CompletedTask;
}
private FileInfo GetFile(string fileName)
private FileInfo GetFile(string fileName, string parameterName)
{
Guard.NotNullOrEmpty(fileName);
Guard.NotNullOrEmpty(fileName, parameterName);
return new FileInfo(GetPath(fileName));
}

2
backend/src/Squidex.Infrastructure/Assets/IAssetStore.cs

@ -15,6 +15,8 @@ namespace Squidex.Infrastructure.Assets
{
string? GeneratePublicUrl(string fileName);
Task<long> GetSizeAsync(string fileName, CancellationToken ct = default);
Task CopyAsync(string sourceFileName, string targetFileName, CancellationToken ct = default);
Task DownloadAsync(string fileName, Stream stream, BytesRange range = default, CancellationToken ct = default);

15
backend/src/Squidex.Infrastructure/Assets/MemoryAssetStore.cs

@ -24,6 +24,21 @@ namespace Squidex.Infrastructure.Assets
return null;
}
public async Task<long> GetSizeAsync(string fileName, CancellationToken ct = default)
{
Guard.NotNullOrEmpty(fileName);
if (!streams.TryGetValue(fileName, out var sourceStream))
{
throw new AssetNotFoundException(fileName);
}
using (await readerLock.LockAsync())
{
return sourceStream.Length;
}
}
public virtual async Task CopyAsync(string sourceFileName, string targetFileName, CancellationToken ct = default)
{
Guard.NotNullOrEmpty(sourceFileName);

5
backend/src/Squidex.Infrastructure/Assets/NoopAssetStore.cs

@ -19,6 +19,11 @@ namespace Squidex.Infrastructure.Assets
return null;
}
public Task<long> GetSizeAsync(string fileName, CancellationToken ct = default)
{
throw new NotSupportedException();
}
public Task CopyAsync(string sourceFileName, string fileName, CancellationToken ct = default)
{
throw new NotSupportedException();

15
backend/src/Squidex.Web/FileCallbackResult.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
@ -16,6 +15,8 @@ using Squidex.Web.Pipeline;
namespace Squidex.Web
{
public delegate Task FileCallback(Stream body, BytesRange range, CancellationToken ct);
public sealed class FileCallbackResult : FileResult
{
public bool ErrorAs404 { get; set; }
@ -24,17 +25,9 @@ namespace Squidex.Web
public long? FileSize { get; set; }
public Func<Stream, BytesRange, CancellationToken, Task> Callback { get; }
public FileCallbackResult(string contentType, Func<Stream, CancellationToken, Task> callback)
: base(contentType)
{
Guard.NotNull(callback);
Callback = (stream, _, ct) => callback(stream, ct);
}
public FileCallback Callback { get; }
public FileCallbackResult(string contentType, Func<Stream, BytesRange, CancellationToken, Task> callback)
public FileCallbackResult(string contentType, FileCallback callback)
: base(contentType)
{
Guard.NotNull(callback);

4
backend/src/Squidex.Web/Pipeline/FileCallbackResultExecutor.cs

@ -44,6 +44,10 @@ namespace Squidex.Web.Pipeline
await result.Callback(context.HttpContext.Response.Body, bytesRange, context.HttpContext.RequestAborted);
}
}
catch (OperationCanceledException)
{
return;
}
catch (Exception e)
{
if (!context.HttpContext.Response.HasStarted && result.ErrorAs404)

4
backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs

@ -5,10 +5,8 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@ -210,7 +208,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
Response.Headers[HeaderNames.ETag] = etag;
var callback = new Func<Stream, CancellationToken, Task>(async (body, ct) =>
var callback = new FileCallback(async (body, range, ct) =>
{
var resizedAsset = $"{App.Id}_{etag}_Resized";

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

@ -35,6 +35,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
{
private readonly IAssetFileStore assetFileStore;
private readonly IAssetRepository assetRepository;
private readonly IAssetLoader assetLoader;
private readonly IAssetStore assetStore;
private readonly IAssetThumbnailGenerator assetThumbnailGenerator;
@ -42,12 +43,14 @@ namespace Squidex.Areas.Api.Controllers.Assets
ICommandBus commandBus,
IAssetFileStore assetFileStore,
IAssetRepository assetRepository,
IAssetLoader assetLoader,
IAssetStore assetStore,
IAssetThumbnailGenerator assetThumbnailGenerator)
: base(commandBus)
{
this.assetFileStore = assetFileStore;
this.assetRepository = assetRepository;
this.assetLoader = assetLoader;
this.assetStore = assetStore;
this.assetThumbnailGenerator = assetThumbnailGenerator;
}
@ -82,7 +85,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
asset = await assetRepository.FindAssetBySlugAsync(App.Id, idOrSlug);
}
return DeliverAsset(asset, query);
return await DeliverAssetAsync(asset, query);
}
/// <summary>
@ -104,14 +107,14 @@ namespace Squidex.Areas.Api.Controllers.Assets
{
var asset = await assetRepository.FindAssetAsync(id);
return DeliverAsset(asset, query);
return await DeliverAssetAsync(asset, query);
}
private IActionResult DeliverAsset(IAssetEntity? asset, AssetContentQueryDto query)
private async Task<IActionResult> DeliverAssetAsync(IAssetEntity? asset, AssetContentQueryDto query)
{
query ??= new AssetContentQueryDto();
if (asset == null || asset.FileVersion < query.Version)
if (asset == null)
{
return NotFound();
}
@ -121,33 +124,33 @@ namespace Squidex.Areas.Api.Controllers.Assets
return StatusCode(403);
}
var fileVersion = query.Version;
if (fileVersion <= EtagVersion.Any)
if (query.Version > EtagVersion.Any && asset.Version != query.Version)
{
fileVersion = asset.FileVersion;
asset = await assetLoader.GetAsync(asset.Id, query.Version);
}
Response.Headers[HeaderNames.ETag] = fileVersion.ToString();
var resizeOptions = query.ToResizeOptions(asset);
FileCallback callback;
Response.Headers[HeaderNames.ETag] = asset.FileVersion.ToString();
if (query.CacheDuration > 0)
{
Response.Headers[HeaderNames.CacheControl] = $"public,max-age={query.CacheDuration}";
}
var inline = query.Download != 1;
var resizeOptions = query.ToResizeOptions(asset);
var contentLength = (long?)null;
if (asset.Type == AssetType.Image && resizeOptions.IsValid)
{
var handler = new Func<Stream, CancellationToken, Task>(async (bodyStream, ct) =>
callback = new FileCallback(async (bodyStream, range, ct) =>
{
var resizedAsset = $"{asset.Id}_{asset.FileVersion}_{resizeOptions}";
if (query.ForceResize)
{
await ResizeAsync(asset, bodyStream, resizedAsset, fileVersion, resizeOptions, true, ct);
await ResizeAsync(asset, bodyStream, resizedAsset, resizeOptions, true, ct);
}
else
{
@ -157,40 +160,33 @@ namespace Squidex.Areas.Api.Controllers.Assets
}
catch (AssetNotFoundException)
{
await ResizeAsync(asset, bodyStream, resizedAsset, fileVersion, resizeOptions, false, ct);
await ResizeAsync(asset, bodyStream, resizedAsset, resizeOptions, false, ct);
}
}
});
return new FileCallbackResult(asset.MimeType, handler)
{
ErrorAs404 = true,
FileDownloadName = asset.FileName,
FileSize = null,
LastModified = asset.LastModified.ToDateTimeOffset(),
SendInline = inline
};
}
else
{
var handler = new Func<Stream, BytesRange, CancellationToken, Task>(async (bodyStream, range, ct) =>
{
await assetFileStore.DownloadAsync(asset.Id, fileVersion, bodyStream, range, ct);
});
contentLength = asset.FileSize;
return new FileCallbackResult(asset.MimeType, handler)
callback = new FileCallback(async (bodyStream, range, ct) =>
{
EnableRangeProcessing = true,
ErrorAs404 = true,
FileDownloadName = asset.FileName,
FileSize = asset.FileSize,
LastModified = asset.LastModified.ToDateTimeOffset(),
SendInline = inline
};
await assetFileStore.DownloadAsync(asset.Id, asset.FileVersion, bodyStream, range, ct);
});
}
return new FileCallbackResult(asset.MimeType, callback)
{
EnableRangeProcessing = contentLength > 0,
ErrorAs404 = true,
FileDownloadName = asset.FileName,
FileSize = contentLength,
LastModified = asset.LastModified.ToDateTimeOffset(),
SendInline = query.Download != 1
};
}
private async Task ResizeAsync(IAssetEntity asset, Stream bodyStream, string fileName, long fileVersion, ResizeOptions resizeOptions, bool overwrite, CancellationToken ct)
private async Task ResizeAsync(IAssetEntity asset, Stream bodyStream, string fileName, ResizeOptions resizeOptions, bool overwrite, CancellationToken ct)
{
using (Profiler.Trace("Resize"))
{
@ -200,7 +196,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
{
using (Profiler.Trace("ResizeDownload"))
{
await assetFileStore.DownloadAsync(asset.Id, fileVersion, sourceStream);
await assetFileStore.DownloadAsync(asset.Id, asset.FileVersion, sourceStream);
sourceStream.Position = 0;
}
@ -226,10 +222,13 @@ namespace Squidex.Areas.Api.Controllers.Assets
{
var tempFileName = Path.GetTempFileName();
const int bufferSize = 16 * 1024;
return new FileStream(tempFileName,
FileMode.Create,
FileAccess.ReadWrite,
FileShare.Delete, 1024 * 16,
FileShare.Delete,
bufferSize,
FileOptions.Asynchronous |
FileOptions.DeleteOnClose |
FileOptions.SequentialScan);

4
backend/src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs

@ -6,8 +6,6 @@
// ==========================================================================
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@ -61,7 +59,7 @@ namespace Squidex.Areas.Api.Controllers.Backups
var fileName = $"backup-{app}-{backup.Started:yyyy-MM-dd_HH-mm-ss}.zip";
var callback = new Func<Stream, CancellationToken, Task>((body, ct) =>
var callback = new FileCallback((body, range, ct) =>
{
return backupArchiveStore.DownloadAsync(id, body, ct);
});

4
backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs

@ -6,9 +6,7 @@
// ==========================================================================
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Mvc;
@ -178,7 +176,7 @@ namespace Squidex.Areas.Api.Controllers.Statistics
var fileName = $"Usage-{today:yyy-MM-dd}.csv";
var callback = new Func<Stream, CancellationToken, Task>((body, ct) =>
var callback = new FileCallback((body, range, ct) =>
{
return appLogStore.ReadLogAsync(Guid.Parse(appId), today.AddDays(-30), today, body, ct);
});

3
backend/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs

@ -9,7 +9,6 @@ using System;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
@ -169,7 +168,7 @@ namespace Squidex.Areas.Api.Controllers.Users
{
if (entity.IsPictureUrlStored())
{
var callback = new Func<Stream, CancellationToken, Task>(async (body, ct) =>
var callback = new FileCallback(async (body, range, ct) =>
{
try
{

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

@ -43,6 +43,19 @@ namespace Squidex.Domain.Apps.Entities.Assets
Assert.Equal(url, result);
}
[Fact]
public async Task Should_invoke_asset_store_to_get_file_size()
{
var size = 1024L;
A.CallTo(() => assetStore.GetSizeAsync(fileName, default))
.Returns(size);
var result = await sut.GetFileSizeAsync(assetId, assetFileVersion);
Assert.Equal(size, result);
}
[Fact]
public async Task Should_invoke_asset_store_to_temporary_upload_file()
{

16
backend/tests/Squidex.Infrastructure.Tests/Assets/AssetStoreTests.cs

@ -36,6 +36,12 @@ namespace Squidex.Infrastructure.Assets
public abstract T CreateStore();
[Fact]
public virtual async Task Should_throw_exception_if_asset_to_get_size_is_not_found()
{
await Assert.ThrowsAsync<AssetNotFoundException>(() => Sut.GetSizeAsync(fileName));
}
[Fact]
public virtual async Task Should_throw_exception_if_asset_to_download_is_not_found()
{
@ -114,6 +120,16 @@ namespace Squidex.Infrastructure.Assets
Assert.Equal(new Span<byte>(assetData.ToArray()).Slice(1, 2).ToArray(), readData.ToArray());
}
[Fact]
public async Task Should_write_and_and_get_size()
{
await Sut.UploadAsync(fileName, assetData, true);
var size = await Sut.GetSizeAsync(fileName);
Assert.Equal(assetData.Length, size);
}
[Fact]
public async Task Should_write_and_read_file_and_overwrite_non_existing()
{

6
frontend/app/shared/components/assets/pipes.ts

@ -51,7 +51,11 @@ export class AssetPreviewUrlPipe implements PipeTransform {
}
public transform(asset: AssetDto): string {
return asset.fullUrl(this.apiUrl, this.authService);
let url = asset.fullUrl(this.apiUrl, this.authService);
url = StringHelper.appendToUrl(url, 'version', asset.fileVersion);
return url;
}
}

Loading…
Cancel
Save