diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/DefaultAssetFileStore.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/DefaultAssetFileStore.cs index e470ab706..f62251e16 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/DefaultAssetFileStore.cs +++ b/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 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); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetFileStore.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetFileStore.cs index 20a36af64..0e0d31e62 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetFileStore.cs +++ b/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 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); diff --git a/backend/src/Squidex.Infrastructure.Amazon/Assets/AmazonS3AssetStore.cs b/backend/src/Squidex.Infrastructure.Amazon/Assets/AmazonS3AssetStore.cs index c9a2115c9..2a829b752 100644 --- a/backend/src/Squidex.Infrastructure.Amazon/Assets/AmazonS3AssetStore.cs +++ b/backend/src/Squidex.Infrastructure.Amazon/Assets/AmazonS3AssetStore.cs @@ -79,21 +79,43 @@ namespace Squidex.Infrastructure.Assets return null; } + public async Task 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; } } } \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure.Azure/Assets/AzureBlobAssetStore.cs b/backend/src/Squidex.Infrastructure.Azure/Assets/AzureBlobAssetStore.cs index 33a98d03f..755105bac 100644 --- a/backend/src/Squidex.Infrastructure.Azure/Assets/AzureBlobAssetStore.cs +++ b/backend/src/Squidex.Infrastructure.Azure/Assets/AzureBlobAssetStore.cs @@ -62,6 +62,24 @@ namespace Squidex.Infrastructure.Assets return null; } + public async Task 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); } } diff --git a/backend/src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs b/backend/src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs index b2e12358d..d2c69f8c9 100644 --- a/backend/src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs +++ b/backend/src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs @@ -49,6 +49,27 @@ namespace Squidex.Infrastructure.Assets return null; } + public async Task 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); diff --git a/backend/src/Squidex.Infrastructure.MongoDb/Assets/MongoGridFsAssetStore.cs b/backend/src/Squidex.Infrastructure.MongoDb/Assets/MongoGridFsAssetStore.cs index 184ed219e..8b9f21b58 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/Assets/MongoGridFsAssetStore.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/Assets/MongoGridFsAssetStore.cs @@ -45,6 +45,20 @@ namespace Squidex.Infrastructure.Assets return null; } + public async Task GetSizeAsync(string fileName, CancellationToken ct = default) + { + var name = GetFileName(fileName, nameof(fileName)); + + var file = await bucket.Find(Builders>.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); diff --git a/backend/src/Squidex.Infrastructure/Assets/FTPAssetStore.cs b/backend/src/Squidex.Infrastructure/Assets/FTPAssetStore.cs index 881da5df1..66821ed9b 100644 --- a/backend/src/Squidex.Infrastructure/Assets/FTPAssetStore.cs +++ b/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 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); diff --git a/backend/src/Squidex.Infrastructure/Assets/FolderAssetStore.cs b/backend/src/Squidex.Infrastructure/Assets/FolderAssetStore.cs index 5038ca489..68861548b 100644 --- a/backend/src/Squidex.Infrastructure/Assets/FolderAssetStore.cs +++ b/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 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)); } diff --git a/backend/src/Squidex.Infrastructure/Assets/IAssetStore.cs b/backend/src/Squidex.Infrastructure/Assets/IAssetStore.cs index a00615636..6cc829504 100644 --- a/backend/src/Squidex.Infrastructure/Assets/IAssetStore.cs +++ b/backend/src/Squidex.Infrastructure/Assets/IAssetStore.cs @@ -15,6 +15,8 @@ namespace Squidex.Infrastructure.Assets { string? GeneratePublicUrl(string fileName); + Task 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); diff --git a/backend/src/Squidex.Infrastructure/Assets/MemoryAssetStore.cs b/backend/src/Squidex.Infrastructure/Assets/MemoryAssetStore.cs index 7fe53deda..8c92ffbae 100644 --- a/backend/src/Squidex.Infrastructure/Assets/MemoryAssetStore.cs +++ b/backend/src/Squidex.Infrastructure/Assets/MemoryAssetStore.cs @@ -24,6 +24,21 @@ namespace Squidex.Infrastructure.Assets return null; } + public async Task 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); diff --git a/backend/src/Squidex.Infrastructure/Assets/NoopAssetStore.cs b/backend/src/Squidex.Infrastructure/Assets/NoopAssetStore.cs index f2745cf69..ce9dad7dc 100644 --- a/backend/src/Squidex.Infrastructure/Assets/NoopAssetStore.cs +++ b/backend/src/Squidex.Infrastructure/Assets/NoopAssetStore.cs @@ -19,6 +19,11 @@ namespace Squidex.Infrastructure.Assets return null; } + public Task GetSizeAsync(string fileName, CancellationToken ct = default) + { + throw new NotSupportedException(); + } + public Task CopyAsync(string sourceFileName, string fileName, CancellationToken ct = default) { throw new NotSupportedException(); diff --git a/backend/src/Squidex.Web/FileCallbackResult.cs b/backend/src/Squidex.Web/FileCallbackResult.cs index 5a3c37523..0d200a3f8 100644 --- a/backend/src/Squidex.Web/FileCallbackResult.cs +++ b/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 Callback { get; } - - public FileCallbackResult(string contentType, Func callback) - : base(contentType) - { - Guard.NotNull(callback); - - Callback = (stream, _, ct) => callback(stream, ct); - } + public FileCallback Callback { get; } - public FileCallbackResult(string contentType, Func callback) + public FileCallbackResult(string contentType, FileCallback callback) : base(contentType) { Guard.NotNull(callback); diff --git a/backend/src/Squidex.Web/Pipeline/FileCallbackResultExecutor.cs b/backend/src/Squidex.Web/Pipeline/FileCallbackResultExecutor.cs index f176fbdd0..e84578fa3 100644 --- a/backend/src/Squidex.Web/Pipeline/FileCallbackResultExecutor.cs +++ b/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) diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs index 4e6bd062c..40f61c8e9 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs +++ b/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(async (body, ct) => + var callback = new FileCallback(async (body, range, ct) => { var resizedAsset = $"{App.Id}_{etag}_Resized"; diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs index bdc8578d5..5673038c0 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs +++ b/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); } /// @@ -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 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(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(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); diff --git a/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs b/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs index efad8cdc5..b76c72c91 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs +++ b/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((body, ct) => + var callback = new FileCallback((body, range, ct) => { return backupArchiveStore.DownloadAsync(id, body, ct); }); diff --git a/backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs index e3e0d1c6d..8f42efb86 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs +++ b/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((body, ct) => + var callback = new FileCallback((body, range, ct) => { return appLogStore.ReadLogAsync(Guid.Parse(appId), today.AddDays(-30), today, body, ct); }); diff --git a/backend/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs b/backend/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs index 9d9c9388f..82dfb8dbe 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs +++ b/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(async (body, ct) => + var callback = new FileCallback(async (body, range, ct) => { try { diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DefaultAssetFileStoreTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DefaultAssetFileStoreTests.cs index 9ccdad736..cde0f2b26 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DefaultAssetFileStoreTests.cs +++ b/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() { diff --git a/backend/tests/Squidex.Infrastructure.Tests/Assets/AssetStoreTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Assets/AssetStoreTests.cs index 07d262c91..9147b2888 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Assets/AssetStoreTests.cs +++ b/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(() => 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(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() { diff --git a/frontend/app/shared/components/assets/pipes.ts b/frontend/app/shared/components/assets/pipes.ts index 935e964de..506f1e672 100644 --- a/frontend/app/shared/components/assets/pipes.ts +++ b/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; } }