Browse Source

Feature/range (#503)

* Range support.

* Asset range support.
pull/507/head
Sebastian Stehle 6 years ago
committed by GitHub
parent
commit
8c47ee1dc1
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs
  2. 2
      backend/src/Squidex.Domain.Apps.Entities/Apps/DefaultAppImageStore.cs
  3. 5
      backend/src/Squidex.Domain.Apps.Entities/Assets/DefaultAssetFileStore.cs
  4. 3
      backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetFileStore.cs
  5. 2
      backend/src/Squidex.Domain.Apps.Entities/Backup/DefaultBackupArchiveStore.cs
  6. 2
      backend/src/Squidex.Domain.Users/DefaultUserPictureStore.cs
  7. 16
      backend/src/Squidex.Infrastructure.Amazon/Assets/AmazonS3AssetStore.cs
  8. 8
      backend/src/Squidex.Infrastructure.Azure/Assets/AzureBlobAssetStore.cs
  9. 12
      backend/src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs
  10. 11
      backend/src/Squidex.Infrastructure.MongoDb/Assets/MongoGridFsAssetStore.cs
  11. 41
      backend/src/Squidex.Infrastructure/Assets/FTPAssetStore.cs
  12. 4
      backend/src/Squidex.Infrastructure/Assets/FolderAssetStore.cs
  13. 2
      backend/src/Squidex.Infrastructure/Assets/IAssetStore.cs
  14. 4
      backend/src/Squidex.Infrastructure/Assets/MemoryAssetStore.cs
  15. 2
      backend/src/Squidex.Infrastructure/Assets/NoopAssetStore.cs
  16. 64
      backend/src/Squidex.Infrastructure/Assets/StreamExtensions.cs
  17. 65
      backend/src/Squidex.Infrastructure/BytesRange.cs
  18. 19
      backend/src/Squidex.Web/FileCallbackResult.cs
  19. 12
      backend/src/Squidex.Web/Pipeline/FileCallbackResultExecutor.cs
  20. 11
      backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs
  21. 58
      backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs
  22. 11
      backend/src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs
  23. 13
      backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs
  24. 9
      backend/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs
  25. 20
      backend/src/Squidex/Config/Domain/LoggingServices.cs
  26. 5
      backend/src/Squidex/Config/Web/WebExtensions.cs
  27. 2
      backend/src/Squidex/appsettings.json
  28. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DefaultAppImageStoreTests.cs
  29. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/BackupAssetsTests.cs
  30. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DefaultAssetFileStoreTests.cs
  31. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/DefaultBackupArchiveStoreTests.cs
  32. 2
      backend/tests/Squidex.Domain.Users.Tests/DefaultUserPictureStoreTests.cs
  33. 12
      backend/tests/Squidex.Infrastructure.Tests/Assets/AssetStoreTests.cs
  34. 87
      backend/tests/Squidex.Infrastructure.Tests/BytesRangeTests.cs

2
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs

@ -12,14 +12,12 @@ using System.Linq;
using System.Reflection; using System.Reflection;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using NodaTime;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Security;
using Squidex.Shared.Identity; using Squidex.Shared.Identity;
using Squidex.Shared.Users; using Squidex.Shared.Users;

2
backend/src/Squidex.Domain.Apps.Entities/Apps/DefaultAppImageStore.cs

@ -29,7 +29,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
{ {
var fileName = GetFileName(backupId); var fileName = GetFileName(backupId);
return assetStore.DownloadAsync(fileName, stream, ct); return assetStore.DownloadAsync(fileName, stream, default, ct);
} }
public Task UploadAsync(Guid backupId, Stream stream, CancellationToken ct = default) public Task UploadAsync(Guid backupId, Stream stream, CancellationToken ct = default)

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

@ -9,6 +9,7 @@ using System;
using System.IO; using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Assets;
namespace Squidex.Domain.Apps.Entities.Assets namespace Squidex.Domain.Apps.Entities.Assets
@ -48,11 +49,11 @@ namespace Squidex.Domain.Apps.Entities.Assets
return assetStore.CopyAsync(tempFile, fileName, ct); return assetStore.CopyAsync(tempFile, fileName, ct);
} }
public Task DownloadAsync(Guid id, long fileVersion, Stream stream, CancellationToken ct = default) public Task DownloadAsync(Guid id, long fileVersion, Stream stream, BytesRange range = default, CancellationToken ct = default)
{ {
var fileName = GetFileName(id, fileVersion); var fileName = GetFileName(id, fileVersion);
return assetStore.DownloadAsync(fileName, stream, ct); return assetStore.DownloadAsync(fileName, stream, range, ct);
} }
public Task DeleteAsync(Guid id, long fileVersion) public Task DeleteAsync(Guid id, long fileVersion)

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

@ -9,6 +9,7 @@ using System;
using System.IO; using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Assets namespace Squidex.Domain.Apps.Entities.Assets
{ {
@ -22,7 +23,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
Task UploadAsync(Guid id, long fileVersion, Stream stream, CancellationToken ct = default); Task UploadAsync(Guid id, long fileVersion, Stream stream, CancellationToken ct = default);
Task DownloadAsync(Guid id, long fileVersion, Stream stream, CancellationToken ct = default); Task DownloadAsync(Guid id, long fileVersion, Stream stream, BytesRange range = default, CancellationToken ct = default);
Task DeleteAsync(string tempFile); Task DeleteAsync(string tempFile);

2
backend/src/Squidex.Domain.Apps.Entities/Backup/DefaultBackupArchiveStore.cs

@ -29,7 +29,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
{ {
var fileName = GetFileName(backupId); var fileName = GetFileName(backupId);
return assetStore.DownloadAsync(fileName, stream, ct); return assetStore.DownloadAsync(fileName, stream, default, ct);
} }
public Task UploadAsync(Guid backupId, Stream stream, CancellationToken ct = default) public Task UploadAsync(Guid backupId, Stream stream, CancellationToken ct = default)

2
backend/src/Squidex.Domain.Users/DefaultUserPictureStore.cs

@ -35,7 +35,7 @@ namespace Squidex.Domain.Users
{ {
var fileName = GetFileName(userId); var fileName = GetFileName(userId);
return assetStore.DownloadAsync(fileName, stream, ct); return assetStore.DownloadAsync(fileName, stream, default, ct);
} }
private static string GetFileName(string userId) private static string GetFileName(string userId)

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

@ -108,7 +108,7 @@ namespace Squidex.Infrastructure.Assets
} }
} }
public async Task DownloadAsync(string fileName, Stream stream, CancellationToken ct = default) public async Task DownloadAsync(string fileName, Stream stream, BytesRange range = default, CancellationToken ct = default)
{ {
Guard.NotNullOrEmpty(fileName); Guard.NotNullOrEmpty(fileName);
Guard.NotNull(stream); Guard.NotNull(stream);
@ -117,6 +117,11 @@ namespace Squidex.Infrastructure.Assets
{ {
var request = new GetObjectRequest { BucketName = options.Bucket, Key = GetKey(fileName) }; var request = new GetObjectRequest { BucketName = options.Bucket, Key = GetKey(fileName) };
if (range.IsDefined)
{
request.ByteRange = new ByteRange(range.ToString());
}
using (var response = await s3Client.GetObjectAsync(request, ct)) using (var response = await s3Client.GetObjectAsync(request, ct))
{ {
await response.ResponseStream.CopyToAsync(stream, BufferSize, ct); await response.ResponseStream.CopyToAsync(stream, BufferSize, ct);
@ -147,8 +152,7 @@ namespace Squidex.Infrastructure.Assets
ConfigureDefaults(request); ConfigureDefaults(request);
// Amazon S3 requires a seekable stream, but does not seek anything. SetStream(stream, request);
request.InputStream = new SeekFakerStream(stream);
await transferUtility.UploadAsync(request, ct); await transferUtility.UploadAsync(request, ct);
} }
@ -205,5 +209,11 @@ namespace Squidex.Infrastructure.Assets
request.AutoCloseStream = false; request.AutoCloseStream = false;
request.BucketName = options.Bucket; 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);
}
} }
} }

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

@ -98,15 +98,19 @@ namespace Squidex.Infrastructure.Assets
} }
} }
public async Task DownloadAsync(string fileName, Stream stream, CancellationToken ct = default) public async Task DownloadAsync(string fileName, Stream stream, BytesRange range = default, CancellationToken ct = default)
{ {
Guard.NotNullOrEmpty(fileName); Guard.NotNullOrEmpty(fileName);
Guard.NotNull(stream);
try try
{ {
var blob = blobContainer.GetBlockBlobReference(fileName); var blob = blobContainer.GetBlockBlobReference(fileName);
await blob.DownloadToStreamAsync(stream, null, null, null, ct); using (var blobStream = await blob.OpenReadAsync(null, null, null, ct))
{
await blobStream.CopyToAsync(stream, range, ct);
}
} }
catch (StorageException ex) when (ex.RequestInformation.HttpStatusCode == 404) catch (StorageException ex) when (ex.RequestInformation.HttpStatusCode == 404)
{ {

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

@ -8,6 +8,7 @@
using System; using System;
using System.IO; using System.IO;
using System.Net; using System.Net;
using System.Net.Http.Headers;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Google; using Google;
@ -67,13 +68,20 @@ namespace Squidex.Infrastructure.Assets
} }
} }
public async Task DownloadAsync(string fileName, Stream stream, CancellationToken ct = default) public async Task DownloadAsync(string fileName, Stream stream, BytesRange range = default, CancellationToken ct = default)
{ {
Guard.NotNullOrEmpty(fileName); Guard.NotNullOrEmpty(fileName);
try try
{ {
await storageClient.DownloadObjectAsync(bucketName, fileName, stream, cancellationToken: ct); var downloadOptions = new DownloadObjectOptions();
if (range.IsDefined)
{
downloadOptions.Range = new RangeHeaderValue(range.From, range.To);
}
await storageClient.DownloadObjectAsync(bucketName, fileName, stream, downloadOptions, ct);
} }
catch (GoogleApiException ex) when (ex.HttpStatusCode == HttpStatusCode.NotFound) catch (GoogleApiException ex) when (ex.HttpStatusCode == HttpStatusCode.NotFound)
{ {

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

@ -17,7 +17,8 @@ namespace Squidex.Infrastructure.Assets
{ {
public sealed class MongoGridFsAssetStore : IAssetStore, IInitializable public sealed class MongoGridFsAssetStore : IAssetStore, IInitializable
{ {
private const int BufferSize = 81920; private static readonly GridFSDownloadOptions DownloadDefault = new GridFSDownloadOptions();
private static readonly GridFSDownloadOptions DownloadSeekable = new GridFSDownloadOptions { Seekable = true };
private readonly IGridFSBucket<string> bucket; private readonly IGridFSBucket<string> bucket;
public MongoGridFsAssetStore(IGridFSBucket<string> bucket) public MongoGridFsAssetStore(IGridFSBucket<string> bucket)
@ -63,7 +64,7 @@ namespace Squidex.Infrastructure.Assets
} }
} }
public async Task DownloadAsync(string fileName, Stream stream, CancellationToken ct = default) public async Task DownloadAsync(string fileName, Stream stream, BytesRange range, CancellationToken ct = default)
{ {
Guard.NotNull(stream); Guard.NotNull(stream);
@ -71,9 +72,11 @@ namespace Squidex.Infrastructure.Assets
{ {
var name = GetFileName(fileName, nameof(fileName)); var name = GetFileName(fileName, nameof(fileName));
using (var readStream = await bucket.OpenDownloadStreamAsync(name, cancellationToken: ct)) var options = range.IsDefined ? DownloadSeekable : DownloadDefault;
using (var readStream = await bucket.OpenDownloadStreamAsync(name, options, ct))
{ {
await readStream.CopyToAsync(stream, BufferSize, ct); await readStream.CopyToAsync(stream, range, ct);
} }
} }
catch (GridFSFileNotFoundException ex) catch (GridFSFileNotFoundException ex)

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

@ -67,20 +67,43 @@ namespace Squidex.Infrastructure.Assets
using (var stream = new FileStream(tempPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None, 4096, FileOptions.DeleteOnClose)) using (var stream = new FileStream(tempPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None, 4096, FileOptions.DeleteOnClose))
{ {
await DownloadAsync(client, sourceFileName, stream, ct); try
{
var found = await client.DownloadAsync(stream, sourceFileName, token: ct);
if (!found)
{
throw new AssetNotFoundException(sourceFileName);
}
}
catch (FtpException ex) when (IsNotFound(ex))
{
throw new AssetNotFoundException(sourceFileName, ex);
}
await UploadAsync(client, targetFileName, stream, false, ct); await UploadAsync(client, targetFileName, stream, false, ct);
} }
} }
} }
public async Task DownloadAsync(string fileName, Stream stream, CancellationToken ct = default) public async Task DownloadAsync(string fileName, Stream stream, BytesRange range = default, CancellationToken ct = default)
{ {
Guard.NotNullOrEmpty(fileName); Guard.NotNullOrEmpty(fileName);
Guard.NotNull(stream); Guard.NotNull(stream);
using (var client = GetFtpClient()) using (var client = GetFtpClient())
{ {
await DownloadAsync(client, fileName, stream, ct); try
{
using (var ftpStream = await client.OpenReadAsync(fileName, range.From ?? 0, ct))
{
await ftpStream.CopyToAsync(stream, range, ct, false);
}
}
catch (FtpException ex) when (IsNotFound(ex))
{
throw new AssetNotFoundException(fileName, ex);
}
} }
} }
@ -95,18 +118,6 @@ namespace Squidex.Infrastructure.Assets
} }
} }
private static async Task DownloadAsync(IFtpClient client, string fileName, Stream stream, CancellationToken ct)
{
try
{
await client.DownloadAsync(stream, fileName, token: ct);
}
catch (FtpException ex) when (IsNotFound(ex))
{
throw new AssetNotFoundException(fileName, ex);
}
}
private static async Task UploadAsync(IFtpClient client, string fileName, Stream stream, bool overwrite, CancellationToken ct) private static async Task UploadAsync(IFtpClient client, string fileName, Stream stream, bool overwrite, CancellationToken ct)
{ {
if (!overwrite && await client.FileExistsAsync(fileName, ct)) if (!overwrite && await client.FileExistsAsync(fileName, ct))

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

@ -79,7 +79,7 @@ namespace Squidex.Infrastructure.Assets
} }
} }
public async Task DownloadAsync(string fileName, Stream stream, CancellationToken ct = default) public async Task DownloadAsync(string fileName, Stream stream, BytesRange range, CancellationToken ct = default)
{ {
Guard.NotNull(stream); Guard.NotNull(stream);
@ -89,7 +89,7 @@ namespace Squidex.Infrastructure.Assets
{ {
using (var fileStream = file.OpenRead()) using (var fileStream = file.OpenRead())
{ {
await fileStream.CopyToAsync(stream, BufferSize, ct); await fileStream.CopyToAsync(stream, range, ct);
} }
} }
catch (FileNotFoundException ex) catch (FileNotFoundException ex)

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

@ -17,7 +17,7 @@ namespace Squidex.Infrastructure.Assets
Task CopyAsync(string sourceFileName, string targetFileName, CancellationToken ct = default); Task CopyAsync(string sourceFileName, string targetFileName, CancellationToken ct = default);
Task DownloadAsync(string fileName, Stream stream, CancellationToken ct = default); Task DownloadAsync(string fileName, Stream stream, BytesRange range = default, CancellationToken ct = default);
Task UploadAsync(string fileName, Stream stream, bool overwrite = false, CancellationToken ct = default); Task UploadAsync(string fileName, Stream stream, bool overwrite = false, CancellationToken ct = default);

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

@ -40,7 +40,7 @@ namespace Squidex.Infrastructure.Assets
} }
} }
public virtual async Task DownloadAsync(string fileName, Stream stream, CancellationToken ct = default) public virtual async Task DownloadAsync(string fileName, Stream stream, BytesRange range = default, CancellationToken ct = default)
{ {
Guard.NotNullOrEmpty(fileName); Guard.NotNullOrEmpty(fileName);
Guard.NotNull(stream); Guard.NotNull(stream);
@ -54,7 +54,7 @@ namespace Squidex.Infrastructure.Assets
{ {
try try
{ {
await sourceStream.CopyToAsync(stream, 81920, ct); await sourceStream.CopyToAsync(stream, range, ct);
} }
finally finally
{ {

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

@ -24,7 +24,7 @@ namespace Squidex.Infrastructure.Assets
throw new NotSupportedException(); throw new NotSupportedException();
} }
public Task DownloadAsync(string fileName, Stream stream, CancellationToken ct = default) public Task DownloadAsync(string fileName, Stream stream, BytesRange range = default, CancellationToken ct = default)
{ {
throw new NotSupportedException(); throw new NotSupportedException();
} }

64
backend/src/Squidex.Infrastructure/Assets/StreamExtensions.cs

@ -0,0 +1,64 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Buffers;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace Squidex.Infrastructure.Assets
{
public static class StreamExtensions
{
private static readonly ArrayPool<byte> Pool = ArrayPool<byte>.Create();
public static async Task CopyToAsync(this Stream source, Stream target, BytesRange range, CancellationToken ct, bool skip = true)
{
var buffer = Pool.Rent(8192);
try
{
if (skip && range.From > 0)
{
source.Seek(range.From.Value, SeekOrigin.Begin);
}
var bytesLeft = range.Length;
while (true)
{
if (bytesLeft <= 0)
{
return;
}
ct.ThrowIfCancellationRequested();
var readLength = (int)Math.Min(buffer.Length, bytesLeft);
var read = await source.ReadAsync(buffer, 0, readLength, ct);
bytesLeft -= read;
if (read == 0)
{
return;
}
ct.ThrowIfCancellationRequested();
await target.WriteAsync(buffer, 0, read, ct);
}
}
finally
{
Pool.Return(buffer);
}
}
}
}

65
backend/src/Squidex.Infrastructure/BytesRange.cs

@ -0,0 +1,65 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
namespace Squidex.Infrastructure
{
public struct BytesRange
{
public readonly long? From;
public readonly long? To;
public long Length
{
get
{
if (To < 0 || From < 0)
{
return 0;
}
var result = (To ?? long.MaxValue) - (From ?? 0);
if (result == long.MaxValue)
{
return long.MaxValue;
}
return Math.Max(0, result + 1);
}
}
public bool IsDefined
{
get { return (From >= 0 || To >= 0) && Length > 0; }
}
public BytesRange(long? from, long? to)
{
From = from;
To = to;
}
public override string? ToString()
{
if (Length == 0)
{
return null;
}
if (From.HasValue || To.HasValue)
{
return $"bytes={From}-{To}";
}
return null;
}
}
}

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

@ -7,6 +7,7 @@
using System; using System;
using System.IO; using System.IO;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -17,18 +18,26 @@ namespace Squidex.Web
{ {
public sealed class FileCallbackResult : FileResult public sealed class FileCallbackResult : FileResult
{ {
public bool Send404 { get; set; } public bool ErrorAs404 { get; set; }
public bool SendInline { get; set; } public bool SendInline { get; set; }
public Func<Stream, Task> Callback { get; } public long? FileSize { get; set; }
public FileCallbackResult(string contentType, string? name, Func<Stream, Task> callback) public Func<Stream, BytesRange, CancellationToken, Task> Callback { get; }
public FileCallbackResult(string contentType, Func<Stream, CancellationToken, Task> callback)
: base(contentType) : base(contentType)
{ {
Guard.NotNull(callback); Guard.NotNull(callback);
FileDownloadName = name; Callback = (stream, _, ct) => callback(stream, ct);
}
public FileCallbackResult(string contentType, Func<Stream, BytesRange, CancellationToken, Task> callback)
: base(contentType)
{
Guard.NotNull(callback);
Callback = callback; Callback = callback;
} }
@ -41,5 +50,3 @@ namespace Squidex.Web
} }
} }
} }
#pragma warning restore 1573

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

@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
using Squidex.Infrastructure;
namespace Squidex.Web.Pipeline namespace Squidex.Web.Pipeline
{ {
@ -25,7 +26,7 @@ namespace Squidex.Web.Pipeline
{ {
try try
{ {
SetHeadersAndLog(context, result, null, false); var (range, _, serveBody) = SetHeadersAndLog(context, result, result.FileSize, result.FileSize.HasValue);
if (!string.IsNullOrWhiteSpace(result.FileDownloadName) && result.SendInline) if (!string.IsNullOrWhiteSpace(result.FileDownloadName) && result.SendInline)
{ {
@ -36,11 +37,16 @@ namespace Squidex.Web.Pipeline
context.HttpContext.Response.Headers[HeaderNames.ContentDisposition] = headerValue.ToString(); context.HttpContext.Response.Headers[HeaderNames.ContentDisposition] = headerValue.ToString();
} }
await result.Callback(context.HttpContext.Response.Body); if (serveBody)
{
var bytesRange = new BytesRange(range?.From, range?.To);
await result.Callback(context.HttpContext.Response.Body, bytesRange, context.HttpContext.RequestAborted);
}
} }
catch (Exception e) catch (Exception e)
{ {
if (!context.HttpContext.Response.HasStarted && result.Send404) if (!context.HttpContext.Response.HasStarted && result.ErrorAs404)
{ {
context.HttpContext.Response.Headers.Clear(); context.HttpContext.Response.Headers.Clear();
context.HttpContext.Response.StatusCode = 404; context.HttpContext.Response.StatusCode = 404;

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

@ -8,6 +8,7 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -209,13 +210,13 @@ namespace Squidex.Areas.Api.Controllers.Apps
Response.Headers[HeaderNames.ETag] = etag; Response.Headers[HeaderNames.ETag] = etag;
var handler = new Func<Stream, Task>(async bodyStream => var callback = new Func<Stream, CancellationToken, Task>(async (body, ct) =>
{ {
var resizedAsset = $"{App.Id}_{etag}_Resized"; var resizedAsset = $"{App.Id}_{etag}_Resized";
try try
{ {
await assetStore.DownloadAsync(resizedAsset, bodyStream); await assetStore.DownloadAsync(resizedAsset, body);
} }
catch (AssetNotFoundException) catch (AssetNotFoundException)
{ {
@ -243,16 +244,16 @@ namespace Squidex.Areas.Api.Controllers.Apps
destinationStream.Position = 0; destinationStream.Position = 0;
} }
await destinationStream.CopyToAsync(bodyStream); await destinationStream.CopyToAsync(body, ct);
} }
} }
} }
} }
}); });
return new FileCallbackResult(App.Image.MimeType, null, handler) return new FileCallbackResult(App.Image.MimeType, callback)
{ {
Send404 = true ErrorAs404 = true
}; };
} }

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

@ -7,6 +7,7 @@
using System; using System;
using System.IO; using System.IO;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -134,17 +135,19 @@ namespace Squidex.Areas.Api.Controllers.Assets
Response.Headers[HeaderNames.CacheControl] = $"public,max-age={query.CacheDuration}"; Response.Headers[HeaderNames.CacheControl] = $"public,max-age={query.CacheDuration}";
} }
var handler = new Func<Stream, Task>(async bodyStream => var inline = query.Download != 1;
{
var resizeOptions = query.ToResizeOptions(asset);
if (asset.Type == AssetType.Image && resizeOptions.IsValid) var resizeOptions = query.ToResizeOptions(asset);
if (asset.Type == AssetType.Image && resizeOptions.IsValid)
{
var handler = new Func<Stream, CancellationToken, Task>(async (bodyStream, ct) =>
{ {
var resizedAsset = $"{asset.Id}_{asset.FileVersion}_{resizeOptions}"; var resizedAsset = $"{asset.Id}_{asset.FileVersion}_{resizeOptions}";
if (query.ForceResize) if (query.ForceResize)
{ {
await ResizeAsync(asset, bodyStream, resizedAsset, fileVersion, resizeOptions, true); await ResizeAsync(asset, bodyStream, resizedAsset, fileVersion, resizeOptions, true, ct);
} }
else else
{ {
@ -154,27 +157,40 @@ namespace Squidex.Areas.Api.Controllers.Assets
} }
catch (AssetNotFoundException) catch (AssetNotFoundException)
{ {
await ResizeAsync(asset, bodyStream, resizedAsset, fileVersion, resizeOptions, false); await ResizeAsync(asset, bodyStream, resizedAsset, fileVersion, resizeOptions, false, ct);
} }
} }
} });
else
{
await assetFileStore.DownloadAsync(asset.Id, fileVersion, bodyStream);
}
});
var inline = query.Download != 1;
return new FileCallbackResult(asset.MimeType, asset.FileName, handler) return new FileCallbackResult(asset.MimeType, handler)
{
ErrorAs404 = true,
FileDownloadName = asset.FileName,
FileSize = null,
LastModified = asset.LastModified.ToDateTimeOffset(),
SendInline = inline
};
}
else
{ {
LastModified = asset.LastModified.ToDateTimeOffset(), var handler = new Func<Stream, BytesRange, CancellationToken, Task>(async (bodyStream, range, ct) =>
Send404 = true, {
SendInline = inline, await assetFileStore.DownloadAsync(asset.Id, fileVersion, bodyStream, range, ct);
}; });
return new FileCallbackResult(asset.MimeType, handler)
{
EnableRangeProcessing = true,
ErrorAs404 = true,
FileDownloadName = asset.FileName,
FileSize = asset.FileSize,
LastModified = asset.LastModified.ToDateTimeOffset(),
SendInline = inline
};
}
} }
private async Task ResizeAsync(IAssetEntity asset, Stream bodyStream, string fileName, long fileVersion, ResizeOptions resizeOptions, bool overwrite) private async Task ResizeAsync(IAssetEntity asset, Stream bodyStream, string fileName, long fileVersion, ResizeOptions resizeOptions, bool overwrite, CancellationToken ct)
{ {
using (Profiler.Trace("Resize")) using (Profiler.Trace("Resize"))
{ {
@ -200,7 +216,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
destinationStream.Position = 0; destinationStream.Position = 0;
} }
await destinationStream.CopyToAsync(bodyStream); await destinationStream.CopyToAsync(bodyStream, ct);
} }
} }
} }

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

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

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

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

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

@ -9,6 +9,7 @@ using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
@ -168,17 +169,19 @@ namespace Squidex.Areas.Api.Controllers.Users
{ {
if (entity.IsPictureUrlStored()) if (entity.IsPictureUrlStored())
{ {
return new FileCallbackResult("image/png", null, async stream => var callback = new Func<Stream, CancellationToken, Task>(async (body, ct) =>
{ {
try try
{ {
await userPictureStore.DownloadAsync(entity.Id, stream); await userPictureStore.DownloadAsync(entity.Id, body, ct);
} }
catch catch
{ {
await stream.WriteAsync(AvatarBytes); await body.WriteAsync(AvatarBytes);
} }
}); });
return new FileCallbackResult("image/png", callback);
} }
using (var client = httpClientFactory.CreateClient()) using (var client = httpClientFactory.CreateClient())

20
backend/src/Squidex/Config/Domain/LoggingServices.cs

@ -5,6 +5,8 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
#define LOG_ALL_IDENTITY_SERVER_NONE
using System; using System;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -92,6 +94,17 @@ namespace Squidex.Config.Domain
{ {
builder.AddFilter((category, level) => builder.AddFilter((category, level) =>
{ {
#if LOG_ALL_IDENTITY_SERVER
if (category.StartsWith("Microsoft.AspNetCore.", StringComparison.OrdinalIgnoreCase))
{
return true;
}
if (category.StartsWith("IdentityServer4.", StringComparison.OrdinalIgnoreCase))
{
return true;
}
#endif
if (level < LogLevel.Information) if (level < LogLevel.Information)
{ {
return false; return false;
@ -126,12 +139,7 @@ namespace Squidex.Config.Domain
{ {
return level >= LogLevel.Warning; return level >= LogLevel.Warning;
} }
#if LOG_ALL_IDENTITY_SERVER
if (category.StartsWith("IdentityServer4.", StringComparison.OrdinalIgnoreCase))
{
return true;
}
#endif
return true; return true;
}); });
} }

5
backend/src/Squidex/Config/Web/WebExtensions.cs

@ -126,7 +126,10 @@ namespace Squidex.Config.Web
{ {
return new ForwardedHeadersOptions return new ForwardedHeadersOptions
{ {
AllowedHosts = new List<string> { new Uri(urlsOptions.BaseUrl).Host }, AllowedHosts = new List<string>
{
new Uri(urlsOptions.BaseUrl).Host
},
ForwardedHeaders = ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost, ForwardedHeaders = ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost,
ForwardLimit = null, ForwardLimit = null,
RequireHeaderSymmetry = false RequireHeaderSymmetry = false

2
backend/src/Squidex/appsettings.json

@ -10,7 +10,7 @@
/* /*
* Set the base url of your application, to generate correct urls in background process. * Set the base url of your application, to generate correct urls in background process.
*/ */
"baseUrl": "http://localhost:5000", "baseUrl": "https://localhost:5001",
/* /*
* Set it to true to redirect the user from http to https permanently. * Set it to true to redirect the user from http to https permanently.

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DefaultAppImageStoreTests.cs

@ -47,7 +47,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
await sut.DownloadAsync(appId, stream); await sut.DownloadAsync(appId, stream);
A.CallTo(() => assetStore.DownloadAsync(fileName, stream, CancellationToken.None)) A.CallTo(() => assetStore.DownloadAsync(fileName, stream, default, CancellationToken.None))
.MustHaveHappened(); .MustHaveHappened();
} }
} }

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

@ -104,7 +104,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
await sut.BackupEventAsync(Envelope.Create(@event), context); await sut.BackupEventAsync(Envelope.Create(@event), context);
A.CallTo(() => assetFileStore.DownloadAsync(assetId, version, assetStream, default)) A.CallTo(() => assetFileStore.DownloadAsync(assetId, version, assetStream, default, default))
.MustHaveHappened(); .MustHaveHappened();
} }

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

@ -72,7 +72,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
await sut.DownloadAsync(assetId, assetFileVersion, stream); await sut.DownloadAsync(assetId, assetFileVersion, stream);
A.CallTo(() => assetStore.DownloadAsync(fileName, stream, CancellationToken.None)) A.CallTo(() => assetStore.DownloadAsync(fileName, stream, default, CancellationToken.None))
.MustHaveHappened(); .MustHaveHappened();
} }

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/DefaultBackupArchiveStoreTests.cs

@ -47,7 +47,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
await sut.DownloadAsync(backupId, stream); await sut.DownloadAsync(backupId, stream);
A.CallTo(() => assetStore.DownloadAsync(fileName, stream, CancellationToken.None)) A.CallTo(() => assetStore.DownloadAsync(fileName, stream, default, CancellationToken.None))
.MustHaveHappened(); .MustHaveHappened();
} }

2
backend/tests/Squidex.Domain.Users.Tests/DefaultUserPictureStoreTests.cs

@ -47,7 +47,7 @@ namespace Squidex.Domain.Users
await sut.DownloadAsync(userId, stream); await sut.DownloadAsync(userId, stream);
A.CallTo(() => assetStore.DownloadAsync(file, stream, CancellationToken.None)) A.CallTo(() => assetStore.DownloadAsync(file, stream, default, CancellationToken.None))
.MustHaveHappened(); .MustHaveHappened();
} }
} }

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

@ -102,6 +102,18 @@ namespace Squidex.Infrastructure.Assets
Assert.Equal(assetData.ToArray(), readData.ToArray()); Assert.Equal(assetData.ToArray(), readData.ToArray());
} }
[Fact]
public async Task Should_write_and_read_file_with_range()
{
await Sut.UploadAsync(fileName, assetData, true);
var readData = new MemoryStream();
await Sut.DownloadAsync(fileName, readData, new BytesRange(1, 2));
Assert.Equal(new Span<byte>(assetData.ToArray()).Slice(1, 2).ToArray(), readData.ToArray());
}
[Fact] [Fact]
public async Task Should_write_and_read_file_and_overwrite_non_existing() public async Task Should_write_and_read_file_and_overwrite_non_existing()
{ {

87
backend/tests/Squidex.Infrastructure.Tests/BytesRangeTests.cs

@ -0,0 +1,87 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Xunit;
namespace Squidex.Infrastructure
{
public class BytesRangeTests
{
[Fact]
public void Should_create_default()
{
var sut = (BytesRange)default;
TestBytesRange(sut, null, null, long.MaxValue, false, null);
}
[Fact]
public void Should_create_default_manually()
{
var sut = new BytesRange(null, null);
TestBytesRange(sut, null, null, long.MaxValue, false, null);
}
[Fact]
public void Should_create_with_from()
{
var sut = new BytesRange(12, null);
TestBytesRange(sut, 12, null, long.MaxValue - 11, true, "bytes=12-");
}
[Fact]
public void Should_create_with_to()
{
var sut = new BytesRange(null, 12);
TestBytesRange(sut, null, 12, 13, true, "bytes=-12");
}
[Fact]
public void Should_create_with_from_and_to()
{
var sut = new BytesRange(3, 15);
TestBytesRange(sut, 3, 15, 13, true, "bytes=3-15");
}
[Fact]
public void Should_create_with_single_byte()
{
var sut = new BytesRange(3, 3);
TestBytesRange(sut, 3, 3, 1, true, "bytes=3-3");
}
[Fact]
public void Should_fix_length()
{
var sut = new BytesRange(5, 3);
TestBytesRange(sut, 5, 3, 0, false, null);
}
[Fact]
public void Should_fix_length_for_negative_range()
{
var sut = new BytesRange(-5, -3);
TestBytesRange(sut, -5, -3, 0, false, null);
}
private void TestBytesRange(BytesRange sut, long? from, long? to, long length, bool defined, string? formatted)
{
Assert.Equal(from, sut.From);
Assert.Equal(to, sut.To);
Assert.Equal(length, sut.Length);
Assert.Equal(defined, sut.IsDefined);
Assert.Equal(formatted, sut.ToString());
}
}
}
Loading…
Cancel
Save