mirror of https://github.com/Squidex/squidex.git
241 changed files with 222 additions and 5564 deletions
@ -1,164 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using System.IO; |
|
||||
using System.Threading; |
|
||||
using System.Threading.Tasks; |
|
||||
using Microsoft.Azure.Storage; |
|
||||
using Microsoft.Azure.Storage.Blob; |
|
||||
|
|
||||
namespace Squidex.Infrastructure.Assets |
|
||||
{ |
|
||||
public class AzureBlobAssetStore : IAssetStore, IInitializable |
|
||||
{ |
|
||||
private readonly string containerName; |
|
||||
private readonly string connectionString; |
|
||||
private CloudBlobContainer blobContainer; |
|
||||
|
|
||||
public AzureBlobAssetStore(string connectionString, string containerName) |
|
||||
{ |
|
||||
Guard.NotNullOrEmpty(containerName, nameof(containerName)); |
|
||||
Guard.NotNullOrEmpty(connectionString, nameof(connectionString)); |
|
||||
|
|
||||
this.connectionString = connectionString; |
|
||||
this.containerName = containerName; |
|
||||
} |
|
||||
|
|
||||
public async Task InitializeAsync(CancellationToken ct = default) |
|
||||
{ |
|
||||
try |
|
||||
{ |
|
||||
var storageAccount = CloudStorageAccount.Parse(connectionString); |
|
||||
|
|
||||
var blobClient = storageAccount.CreateCloudBlobClient(); |
|
||||
var blobReference = blobClient.GetContainerReference(containerName); |
|
||||
|
|
||||
await blobReference.CreateIfNotExistsAsync(ct); |
|
||||
|
|
||||
blobContainer = blobReference; |
|
||||
} |
|
||||
catch (Exception ex) |
|
||||
{ |
|
||||
throw new ConfigurationException($"Cannot connect to blob container '{containerName}'.", ex); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public string? GeneratePublicUrl(string fileName) |
|
||||
{ |
|
||||
Guard.NotNullOrEmpty(fileName, nameof(fileName)); |
|
||||
|
|
||||
if (blobContainer.Properties.PublicAccess != BlobContainerPublicAccessType.Blob) |
|
||||
{ |
|
||||
var blob = blobContainer.GetBlockBlobReference(fileName); |
|
||||
|
|
||||
return blob.Uri.ToString(); |
|
||||
} |
|
||||
|
|
||||
return null; |
|
||||
} |
|
||||
|
|
||||
public async Task<long> GetSizeAsync(string fileName, CancellationToken ct = default) |
|
||||
{ |
|
||||
Guard.NotNullOrEmpty(fileName, nameof(fileName)); |
|
||||
|
|
||||
try |
|
||||
{ |
|
||||
var blob = blobContainer.GetBlockBlobReference(fileName); |
|
||||
|
|
||||
await blob.FetchAttributesAsync(ct); |
|
||||
|
|
||||
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, nameof(sourceFileName)); |
|
||||
Guard.NotNullOrEmpty(targetFileName, nameof(targetFileName)); |
|
||||
|
|
||||
try |
|
||||
{ |
|
||||
var sourceBlob = blobContainer.GetBlockBlobReference(sourceFileName); |
|
||||
|
|
||||
var targetBlob = blobContainer.GetBlobReference(targetFileName); |
|
||||
|
|
||||
await targetBlob.StartCopyAsync(sourceBlob.Uri, null, AccessCondition.GenerateIfNotExistsCondition(), null, null, ct); |
|
||||
|
|
||||
while (targetBlob.CopyState.Status == CopyStatus.Pending) |
|
||||
{ |
|
||||
ct.ThrowIfCancellationRequested(); |
|
||||
|
|
||||
await Task.Delay(50, ct); |
|
||||
await targetBlob.FetchAttributesAsync(null, null, null, ct); |
|
||||
} |
|
||||
|
|
||||
if (targetBlob.CopyState.Status != CopyStatus.Success) |
|
||||
{ |
|
||||
throw new StorageException($"Copy of temporary file failed: {targetBlob.CopyState.Status}"); |
|
||||
} |
|
||||
} |
|
||||
catch (StorageException ex) when (ex.RequestInformation.HttpStatusCode == 409) |
|
||||
{ |
|
||||
throw new AssetAlreadyExistsException(targetFileName, ex); |
|
||||
} |
|
||||
catch (StorageException ex) when (ex.RequestInformation.HttpStatusCode == 404) |
|
||||
{ |
|
||||
throw new AssetNotFoundException(sourceFileName, ex); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public async Task DownloadAsync(string fileName, Stream stream, BytesRange range = default, CancellationToken ct = default) |
|
||||
{ |
|
||||
Guard.NotNullOrEmpty(fileName, nameof(fileName)); |
|
||||
Guard.NotNull(stream, nameof(stream)); |
|
||||
|
|
||||
try |
|
||||
{ |
|
||||
var blob = blobContainer.GetBlockBlobReference(fileName); |
|
||||
|
|
||||
using (var blobStream = await blob.OpenReadAsync(null, null, null, ct)) |
|
||||
{ |
|
||||
await blobStream.CopyToAsync(stream, range, ct); |
|
||||
} |
|
||||
} |
|
||||
catch (StorageException ex) when (ex.RequestInformation.HttpStatusCode == 404) |
|
||||
{ |
|
||||
throw new AssetNotFoundException(fileName, ex); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public async Task UploadAsync(string fileName, Stream stream, bool overwrite = false, CancellationToken ct = default) |
|
||||
{ |
|
||||
Guard.NotNullOrEmpty(fileName, nameof(fileName)); |
|
||||
|
|
||||
try |
|
||||
{ |
|
||||
var tempBlob = blobContainer.GetBlockBlobReference(fileName); |
|
||||
|
|
||||
await tempBlob.UploadFromStreamAsync(stream, overwrite ? null : AccessCondition.GenerateIfNotExistsCondition(), null, null, ct); |
|
||||
} |
|
||||
catch (StorageException ex) when (ex.RequestInformation.HttpStatusCode == 409) |
|
||||
{ |
|
||||
throw new AssetAlreadyExistsException(fileName, ex); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public Task DeleteAsync(string fileName) |
|
||||
{ |
|
||||
Guard.NotNullOrEmpty(fileName, nameof(fileName)); |
|
||||
|
|
||||
var blob = blobContainer.GetBlockBlobReference(fileName); |
|
||||
|
|
||||
return blob.DeleteIfExistsAsync(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,150 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System.IO; |
|
||||
using System.Linq; |
|
||||
using System.Threading; |
|
||||
using System.Threading.Tasks; |
|
||||
using MongoDB.Bson; |
|
||||
using MongoDB.Driver; |
|
||||
using MongoDB.Driver.GridFS; |
|
||||
|
|
||||
namespace Squidex.Infrastructure.Assets |
|
||||
{ |
|
||||
public sealed class MongoGridFsAssetStore : IAssetStore, IInitializable |
|
||||
{ |
|
||||
private static readonly GridFSDownloadOptions DownloadDefault = new GridFSDownloadOptions(); |
|
||||
private static readonly GridFSDownloadOptions DownloadSeekable = new GridFSDownloadOptions { Seekable = true }; |
|
||||
private readonly IGridFSBucket<string> bucket; |
|
||||
|
|
||||
public MongoGridFsAssetStore(IGridFSBucket<string> bucket) |
|
||||
{ |
|
||||
Guard.NotNull(bucket, nameof(bucket)); |
|
||||
|
|
||||
this.bucket = bucket; |
|
||||
} |
|
||||
|
|
||||
public async Task InitializeAsync(CancellationToken ct = default) |
|
||||
{ |
|
||||
try |
|
||||
{ |
|
||||
await bucket.Database.ListCollectionsAsync(cancellationToken: ct); |
|
||||
} |
|
||||
catch (MongoException ex) |
|
||||
{ |
|
||||
throw new ConfigurationException($"Cannot connect to Mongo GridFS bucket '{bucket.Options.BucketName}'.", ex); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public string? GeneratePublicUrl(string fileName) |
|
||||
{ |
|
||||
return null; |
|
||||
} |
|
||||
|
|
||||
public async Task<long> GetSizeAsync(string fileName, CancellationToken ct = default) |
|
||||
{ |
|
||||
var name = GetFileName(fileName, nameof(fileName)); |
|
||||
|
|
||||
var query = await bucket.FindAsync(Builders<GridFSFileInfo<string>>.Filter.Eq(x => x.Id, name), cancellationToken: ct); |
|
||||
|
|
||||
var file = await query.FirstOrDefaultAsync(cancellationToken: ct); |
|
||||
|
|
||||
if (file == null) |
|
||||
{ |
|
||||
throw new AssetNotFoundException(fileName); |
|
||||
} |
|
||||
|
|
||||
return file.Length; |
|
||||
} |
|
||||
|
|
||||
public async Task CopyAsync(string sourceFileName, string targetFileName, CancellationToken ct = default) |
|
||||
{ |
|
||||
Guard.NotNullOrEmpty(targetFileName, nameof(targetFileName)); |
|
||||
|
|
||||
try |
|
||||
{ |
|
||||
var sourceName = GetFileName(sourceFileName, nameof(sourceFileName)); |
|
||||
|
|
||||
await using (var readStream = await bucket.OpenDownloadStreamAsync(sourceName, cancellationToken: ct)) |
|
||||
{ |
|
||||
await UploadAsync(targetFileName, readStream, false, ct); |
|
||||
} |
|
||||
} |
|
||||
catch (GridFSFileNotFoundException ex) |
|
||||
{ |
|
||||
throw new AssetNotFoundException(sourceFileName, ex); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public async Task DownloadAsync(string fileName, Stream stream, BytesRange range, CancellationToken ct = default) |
|
||||
{ |
|
||||
Guard.NotNull(stream, nameof(stream)); |
|
||||
|
|
||||
try |
|
||||
{ |
|
||||
var name = GetFileName(fileName, nameof(fileName)); |
|
||||
|
|
||||
var options = range.IsDefined ? DownloadSeekable : DownloadDefault; |
|
||||
|
|
||||
using (var readStream = await bucket.OpenDownloadStreamAsync(name, options, ct)) |
|
||||
{ |
|
||||
await readStream.CopyToAsync(stream, range, ct); |
|
||||
} |
|
||||
} |
|
||||
catch (GridFSFileNotFoundException ex) |
|
||||
{ |
|
||||
throw new AssetNotFoundException(fileName, ex); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public async Task UploadAsync(string fileName, Stream stream, bool overwrite = false, CancellationToken ct = default) |
|
||||
{ |
|
||||
Guard.NotNull(stream, nameof(stream)); |
|
||||
|
|
||||
try |
|
||||
{ |
|
||||
var name = GetFileName(fileName, nameof(fileName)); |
|
||||
|
|
||||
if (overwrite) |
|
||||
{ |
|
||||
await DeleteAsync(fileName); |
|
||||
} |
|
||||
|
|
||||
await bucket.UploadFromStreamAsync(name, name, stream, cancellationToken: ct); |
|
||||
} |
|
||||
catch (MongoWriteException ex) when (ex.WriteError?.Category == ServerErrorCategory.DuplicateKey) |
|
||||
{ |
|
||||
throw new AssetAlreadyExistsException(fileName); |
|
||||
} |
|
||||
catch (MongoBulkWriteException<BsonDocument> ex) when (ex.WriteErrors.Any(x => x.Category == ServerErrorCategory.DuplicateKey)) |
|
||||
{ |
|
||||
throw new AssetAlreadyExistsException(fileName); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public async Task DeleteAsync(string fileName) |
|
||||
{ |
|
||||
try |
|
||||
{ |
|
||||
var name = GetFileName(fileName, nameof(fileName)); |
|
||||
|
|
||||
await bucket.DeleteAsync(name); |
|
||||
} |
|
||||
catch (GridFSFileNotFoundException) |
|
||||
{ |
|
||||
return; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private static string GetFileName(string fileName, string parameterName) |
|
||||
{ |
|
||||
Guard.NotNullOrEmpty(fileName, parameterName); |
|
||||
|
|
||||
return fileName; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,33 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using System.Runtime.Serialization; |
|
||||
|
|
||||
namespace Squidex.Infrastructure.Assets |
|
||||
{ |
|
||||
[Serializable] |
|
||||
public class AssetAlreadyExistsException : Exception |
|
||||
{ |
|
||||
public AssetAlreadyExistsException(string fileName, Exception? inner = null) |
|
||||
: base(FormatMessage(fileName), inner) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
protected AssetAlreadyExistsException(SerializationInfo info, StreamingContext context) |
|
||||
: base(info, context) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
private static string FormatMessage(string fileName) |
|
||||
{ |
|
||||
Guard.NotNullOrEmpty(fileName, nameof(fileName)); |
|
||||
|
|
||||
return $"An asset with name '{fileName}' already exists."; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,39 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using System.IO; |
|
||||
|
|
||||
namespace Squidex.Infrastructure.Assets |
|
||||
{ |
|
||||
public abstract class AssetFile : IDisposable |
|
||||
{ |
|
||||
public string FileName { get; } |
|
||||
|
|
||||
public string MimeType { get; } |
|
||||
|
|
||||
public long FileSize { get; } |
|
||||
|
|
||||
protected AssetFile(string fileName, string mimeType, long fileSize) |
|
||||
{ |
|
||||
Guard.NotNullOrEmpty(fileName, nameof(fileName)); |
|
||||
Guard.NotNullOrEmpty(mimeType, nameof(mimeType)); |
|
||||
Guard.GreaterEquals(fileSize, 0, nameof(fileSize)); |
|
||||
|
|
||||
FileName = fileName; |
|
||||
FileSize = fileSize; |
|
||||
|
|
||||
MimeType = mimeType; |
|
||||
} |
|
||||
|
|
||||
public virtual void Dispose() |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public abstract Stream OpenRead(); |
|
||||
} |
|
||||
} |
|
||||
@ -1,33 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using System.Runtime.Serialization; |
|
||||
|
|
||||
namespace Squidex.Infrastructure.Assets |
|
||||
{ |
|
||||
[Serializable] |
|
||||
public class AssetNotFoundException : Exception |
|
||||
{ |
|
||||
public AssetNotFoundException(string fileName, Exception? inner = null) |
|
||||
: base(FormatMessage(fileName), inner) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
protected AssetNotFoundException(SerializationInfo info, StreamingContext context) |
|
||||
: base(info, context) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
private static string FormatMessage(string fileName) |
|
||||
{ |
|
||||
Guard.NotNullOrEmpty(fileName, nameof(fileName)); |
|
||||
|
|
||||
return $"An asset with name '{fileName}' does not exist."; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,28 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using System.IO; |
|
||||
|
|
||||
namespace Squidex.Infrastructure.Assets |
|
||||
{ |
|
||||
public sealed class DelegateAssetFile : AssetFile |
|
||||
{ |
|
||||
private readonly Func<Stream> openStream; |
|
||||
|
|
||||
public DelegateAssetFile(string fileName, string mimeType, long fileSize, Func<Stream> openStream) |
|
||||
: base(fileName, mimeType, fileSize) |
|
||||
{ |
|
||||
this.openStream = openStream; |
|
||||
} |
|
||||
|
|
||||
public override Stream OpenRead() |
|
||||
{ |
|
||||
return openStream(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,197 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using System.Diagnostics.CodeAnalysis; |
|
||||
using System.IO; |
|
||||
using System.Threading; |
|
||||
using System.Threading.Tasks; |
|
||||
using FluentFTP; |
|
||||
using Squidex.Infrastructure.Log; |
|
||||
|
|
||||
namespace Squidex.Infrastructure.Assets |
|
||||
{ |
|
||||
[ExcludeFromCodeCoverage] |
|
||||
public sealed class FTPAssetStore : IAssetStore, IInitializable |
|
||||
{ |
|
||||
private readonly string path; |
|
||||
private readonly ISemanticLog log; |
|
||||
private readonly Func<IFtpClient> factory; |
|
||||
|
|
||||
public FTPAssetStore(Func<IFtpClient> factory, string path, ISemanticLog log) |
|
||||
{ |
|
||||
Guard.NotNull(factory, nameof(factory)); |
|
||||
Guard.NotNullOrEmpty(path, nameof(path)); |
|
||||
Guard.NotNull(log, nameof(log)); |
|
||||
|
|
||||
this.factory = factory; |
|
||||
this.path = path; |
|
||||
|
|
||||
this.log = log; |
|
||||
} |
|
||||
|
|
||||
public async Task InitializeAsync(CancellationToken ct = default) |
|
||||
{ |
|
||||
using (var client = factory()) |
|
||||
{ |
|
||||
await client.ConnectAsync(ct); |
|
||||
|
|
||||
if (!await client.DirectoryExistsAsync(path, ct)) |
|
||||
{ |
|
||||
await client.CreateDirectoryAsync(path, ct); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
log.LogInformation(w => w |
|
||||
.WriteProperty("action", "FTPAssetStoreConfigured") |
|
||||
.WriteProperty("path", path)); |
|
||||
} |
|
||||
|
|
||||
public string? GeneratePublicUrl(string fileName) |
|
||||
{ |
|
||||
return null; |
|
||||
} |
|
||||
|
|
||||
public async Task<long> GetSizeAsync(string fileName, CancellationToken ct = default) |
|
||||
{ |
|
||||
Guard.NotNullOrEmpty(fileName, nameof(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, nameof(sourceFileName)); |
|
||||
Guard.NotNullOrEmpty(targetFileName, nameof(targetFileName)); |
|
||||
|
|
||||
using (var client = GetFtpClient()) |
|
||||
{ |
|
||||
var tempPath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); |
|
||||
|
|
||||
using (var stream = new FileStream(tempPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None, 4096, FileOptions.DeleteOnClose)) |
|
||||
{ |
|
||||
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); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public async Task DownloadAsync(string fileName, Stream stream, BytesRange range = default, CancellationToken ct = default) |
|
||||
{ |
|
||||
Guard.NotNullOrEmpty(fileName, nameof(fileName)); |
|
||||
Guard.NotNull(stream, nameof(stream)); |
|
||||
|
|
||||
using (var client = GetFtpClient()) |
|
||||
{ |
|
||||
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); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public async Task UploadAsync(string fileName, Stream stream, bool overwrite = false, CancellationToken ct = default) |
|
||||
{ |
|
||||
Guard.NotNullOrEmpty(fileName, nameof(fileName)); |
|
||||
Guard.NotNull(stream, nameof(stream)); |
|
||||
|
|
||||
using (var client = GetFtpClient()) |
|
||||
{ |
|
||||
await UploadAsync(client, fileName, stream, overwrite, ct); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private static async Task UploadAsync(IFtpClient client, string fileName, Stream stream, bool overwrite, CancellationToken ct) |
|
||||
{ |
|
||||
if (!overwrite && await client.FileExistsAsync(fileName, ct)) |
|
||||
{ |
|
||||
throw new AssetAlreadyExistsException(fileName); |
|
||||
} |
|
||||
|
|
||||
var mode = overwrite ? FtpRemoteExists.Overwrite : FtpRemoteExists.Skip; |
|
||||
|
|
||||
await client.UploadAsync(stream, fileName, mode, true, null, ct); |
|
||||
} |
|
||||
|
|
||||
public async Task DeleteAsync(string fileName) |
|
||||
{ |
|
||||
Guard.NotNullOrEmpty(fileName, nameof(fileName)); |
|
||||
|
|
||||
using (var client = GetFtpClient()) |
|
||||
{ |
|
||||
try |
|
||||
{ |
|
||||
await client.DeleteFileAsync(fileName); |
|
||||
} |
|
||||
catch (FtpException ex) |
|
||||
{ |
|
||||
if (!IsNotFound(ex)) |
|
||||
{ |
|
||||
throw; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private IFtpClient GetFtpClient() |
|
||||
{ |
|
||||
var client = factory(); |
|
||||
|
|
||||
client.Connect(); |
|
||||
client.SetWorkingDirectory(path); |
|
||||
|
|
||||
return client; |
|
||||
} |
|
||||
|
|
||||
private static bool IsNotFound(Exception exception) |
|
||||
{ |
|
||||
if (exception is FtpCommandException command) |
|
||||
{ |
|
||||
return command.CompletionCode == "550"; |
|
||||
} |
|
||||
|
|
||||
return exception.InnerException != null && IsNotFound(exception.InnerException); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,152 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using System.IO; |
|
||||
using System.Threading; |
|
||||
using System.Threading.Tasks; |
|
||||
using Squidex.Infrastructure.Log; |
|
||||
|
|
||||
namespace Squidex.Infrastructure.Assets |
|
||||
{ |
|
||||
public sealed class FolderAssetStore : IAssetStore, IInitializable |
|
||||
{ |
|
||||
private const int BufferSize = 81920; |
|
||||
private readonly ISemanticLog log; |
|
||||
private readonly DirectoryInfo directory; |
|
||||
|
|
||||
public FolderAssetStore(string path, ISemanticLog log) |
|
||||
{ |
|
||||
Guard.NotNullOrEmpty(path, nameof(path)); |
|
||||
Guard.NotNull(log, nameof(log)); |
|
||||
|
|
||||
this.log = log; |
|
||||
|
|
||||
directory = new DirectoryInfo(path); |
|
||||
} |
|
||||
|
|
||||
public Task InitializeAsync(CancellationToken ct = default) |
|
||||
{ |
|
||||
try |
|
||||
{ |
|
||||
if (!directory.Exists) |
|
||||
{ |
|
||||
directory.Create(); |
|
||||
} |
|
||||
|
|
||||
log.LogInformation(w => w |
|
||||
.WriteProperty("action", "FolderAssetStoreConfigured") |
|
||||
.WriteProperty("path", directory.FullName)); |
|
||||
|
|
||||
return Task.CompletedTask; |
|
||||
} |
|
||||
catch (Exception ex) |
|
||||
{ |
|
||||
throw new ConfigurationException($"Cannot access directory {directory.FullName}", ex); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public string? GeneratePublicUrl(string fileName) |
|
||||
{ |
|
||||
return null; |
|
||||
} |
|
||||
|
|
||||
public Task<long> GetSizeAsync(string fileName, CancellationToken ct = default) |
|
||||
{ |
|
||||
var file = GetFile(fileName, nameof(fileName)); |
|
||||
|
|
||||
try |
|
||||
{ |
|
||||
return Task.FromResult(file.Length); |
|
||||
} |
|
||||
catch (FileNotFoundException ex) |
|
||||
{ |
|
||||
throw new AssetNotFoundException(fileName, ex); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public Task CopyAsync(string sourceFileName, string targetFileName, CancellationToken ct = default) |
|
||||
{ |
|
||||
var targetFile = GetFile(targetFileName, nameof(targetFileName)); |
|
||||
var sourceFile = GetFile(sourceFileName, nameof(sourceFileName)); |
|
||||
|
|
||||
try |
|
||||
{ |
|
||||
sourceFile.CopyTo(targetFile.FullName); |
|
||||
|
|
||||
return Task.CompletedTask; |
|
||||
} |
|
||||
catch (IOException) when (targetFile.Exists) |
|
||||
{ |
|
||||
throw new AssetAlreadyExistsException(targetFileName); |
|
||||
} |
|
||||
catch (FileNotFoundException ex) |
|
||||
{ |
|
||||
throw new AssetNotFoundException(sourceFileName, ex); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public async Task DownloadAsync(string fileName, Stream stream, BytesRange range, CancellationToken ct = default) |
|
||||
{ |
|
||||
Guard.NotNull(stream, nameof(stream)); |
|
||||
|
|
||||
var file = GetFile(fileName, nameof(fileName)); |
|
||||
|
|
||||
try |
|
||||
{ |
|
||||
using (var fileStream = file.OpenRead()) |
|
||||
{ |
|
||||
await fileStream.CopyToAsync(stream, range, ct); |
|
||||
} |
|
||||
} |
|
||||
catch (FileNotFoundException ex) |
|
||||
{ |
|
||||
throw new AssetNotFoundException(fileName, ex); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public async Task UploadAsync(string fileName, Stream stream, bool overwrite = false, CancellationToken ct = default) |
|
||||
{ |
|
||||
Guard.NotNull(stream, nameof(stream)); |
|
||||
|
|
||||
var file = GetFile(fileName, nameof(fileName)); |
|
||||
|
|
||||
try |
|
||||
{ |
|
||||
using (var fileStream = file.Open(overwrite ? FileMode.Create : FileMode.CreateNew, FileAccess.Write)) |
|
||||
{ |
|
||||
await stream.CopyToAsync(fileStream, BufferSize, ct); |
|
||||
} |
|
||||
} |
|
||||
catch (IOException) when (file.Exists) |
|
||||
{ |
|
||||
throw new AssetAlreadyExistsException(file.Name); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public Task DeleteAsync(string fileName) |
|
||||
{ |
|
||||
var file = GetFile(fileName, nameof(fileName)); |
|
||||
|
|
||||
file.Delete(); |
|
||||
|
|
||||
return Task.CompletedTask; |
|
||||
} |
|
||||
|
|
||||
private FileInfo GetFile(string fileName, string parameterName) |
|
||||
{ |
|
||||
Guard.NotNullOrEmpty(fileName, parameterName); |
|
||||
|
|
||||
return new FileInfo(GetPath(fileName)); |
|
||||
} |
|
||||
|
|
||||
private string GetPath(string name) |
|
||||
{ |
|
||||
return Path.Combine(directory.FullName, name); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,101 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using System.IO; |
|
||||
using System.Security.Cryptography; |
|
||||
|
|
||||
namespace Squidex.Infrastructure.Assets |
|
||||
{ |
|
||||
public sealed class HasherStream : Stream |
|
||||
{ |
|
||||
private readonly Stream inner; |
|
||||
private readonly IncrementalHash hasher; |
|
||||
|
|
||||
public override bool CanRead |
|
||||
{ |
|
||||
get { return inner.CanRead; } |
|
||||
} |
|
||||
|
|
||||
public override bool CanSeek |
|
||||
{ |
|
||||
get { return false; } |
|
||||
} |
|
||||
|
|
||||
public override bool CanWrite |
|
||||
{ |
|
||||
get { return false; } |
|
||||
} |
|
||||
|
|
||||
public override long Length |
|
||||
{ |
|
||||
get { return inner.Length; } |
|
||||
} |
|
||||
|
|
||||
public override long Position |
|
||||
{ |
|
||||
get { return inner.Position; } |
|
||||
set { throw new NotSupportedException(); } |
|
||||
} |
|
||||
|
|
||||
public HasherStream(Stream inner, HashAlgorithmName hashAlgorithmName) |
|
||||
{ |
|
||||
Guard.NotNull(inner, nameof(inner)); |
|
||||
|
|
||||
if (!inner.CanRead) |
|
||||
{ |
|
||||
throw new ArgumentException("Inner stream must be readable."); |
|
||||
} |
|
||||
|
|
||||
this.inner = inner; |
|
||||
|
|
||||
hasher = IncrementalHash.CreateHash(hashAlgorithmName); |
|
||||
} |
|
||||
|
|
||||
public override int Read(byte[] buffer, int offset, int count) |
|
||||
{ |
|
||||
var read = inner.Read(buffer, offset, count); |
|
||||
|
|
||||
if (read > 0) |
|
||||
{ |
|
||||
hasher.AppendData(buffer, offset, read); |
|
||||
} |
|
||||
|
|
||||
return read; |
|
||||
} |
|
||||
|
|
||||
public byte[] GetHashAndReset() |
|
||||
{ |
|
||||
return hasher.GetHashAndReset(); |
|
||||
} |
|
||||
|
|
||||
public string GetHashStringAndReset() |
|
||||
{ |
|
||||
return Convert.ToBase64String(GetHashAndReset()); |
|
||||
} |
|
||||
|
|
||||
public override void Flush() |
|
||||
{ |
|
||||
throw new NotSupportedException(); |
|
||||
} |
|
||||
|
|
||||
public override long Seek(long offset, SeekOrigin origin) |
|
||||
{ |
|
||||
throw new NotSupportedException(); |
|
||||
} |
|
||||
|
|
||||
public override void SetLength(long value) |
|
||||
{ |
|
||||
throw new NotSupportedException(); |
|
||||
} |
|
||||
|
|
||||
public override void Write(byte[] buffer, int offset, int count) |
|
||||
{ |
|
||||
throw new NotSupportedException(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,28 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System.IO; |
|
||||
using System.Threading; |
|
||||
using System.Threading.Tasks; |
|
||||
|
|
||||
namespace Squidex.Infrastructure.Assets |
|
||||
{ |
|
||||
public interface IAssetStore |
|
||||
{ |
|
||||
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); |
|
||||
|
|
||||
Task UploadAsync(string fileName, Stream stream, bool overwrite = false, CancellationToken ct = default); |
|
||||
|
|
||||
Task DeleteAsync(string fileName); |
|
||||
} |
|
||||
} |
|
||||
@ -1,21 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System.IO; |
|
||||
using System.Threading.Tasks; |
|
||||
|
|
||||
namespace Squidex.Infrastructure.Assets |
|
||||
{ |
|
||||
public interface IAssetThumbnailGenerator |
|
||||
{ |
|
||||
Task<ImageInfo?> GetImageInfoAsync(Stream source); |
|
||||
|
|
||||
Task<ImageInfo> FixOrientationAsync(Stream source, Stream destination); |
|
||||
|
|
||||
Task CreateThumbnailAsync(Stream source, Stream destination, ResizeOptions options); |
|
||||
} |
|
||||
} |
|
||||
@ -1,18 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
namespace Squidex.Infrastructure.Assets |
|
||||
{ |
|
||||
public enum ImageFormat |
|
||||
{ |
|
||||
Auto, |
|
||||
PNG, |
|
||||
JPEG, |
|
||||
TGA, |
|
||||
GIF |
|
||||
} |
|
||||
} |
|
||||
@ -1,31 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
namespace Squidex.Infrastructure.Assets |
|
||||
{ |
|
||||
public sealed class ImageInfo |
|
||||
{ |
|
||||
public string? Format { get; set; } |
|
||||
|
|
||||
public int PixelWidth { get; } |
|
||||
|
|
||||
public int PixelHeight { get; } |
|
||||
|
|
||||
public bool IsRotatedOrSwapped { get; } |
|
||||
|
|
||||
public ImageInfo(int pixelWidth, int pixelHeight, bool isRotatedOrSwapped) |
|
||||
{ |
|
||||
Guard.GreaterThan(pixelWidth, 0, nameof(pixelWidth)); |
|
||||
Guard.GreaterThan(pixelHeight, 0, nameof(pixelHeight)); |
|
||||
|
|
||||
PixelWidth = pixelWidth; |
|
||||
PixelHeight = pixelHeight; |
|
||||
|
|
||||
IsRotatedOrSwapped = isRotatedOrSwapped; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,197 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using System.IO; |
|
||||
using System.Threading; |
|
||||
using System.Threading.Tasks; |
|
||||
using SixLabors.ImageSharp; |
|
||||
using SixLabors.ImageSharp.Formats; |
|
||||
using SixLabors.ImageSharp.Formats.Gif; |
|
||||
using SixLabors.ImageSharp.Formats.Jpeg; |
|
||||
using SixLabors.ImageSharp.Formats.Png; |
|
||||
using SixLabors.ImageSharp.Formats.Tga; |
|
||||
using SixLabors.ImageSharp.Metadata.Profiles.Exif; |
|
||||
using SixLabors.ImageSharp.Processing; |
|
||||
using ISResizeMode = SixLabors.ImageSharp.Processing.ResizeMode; |
|
||||
using ISResizeOptions = SixLabors.ImageSharp.Processing.ResizeOptions; |
|
||||
|
|
||||
namespace Squidex.Infrastructure.Assets.ImageSharp |
|
||||
{ |
|
||||
public sealed class ImageSharpAssetThumbnailGenerator : IAssetThumbnailGenerator |
|
||||
{ |
|
||||
private readonly SemaphoreSlim maxTasks = new SemaphoreSlim(Math.Max(Environment.ProcessorCount / 4, 1)); |
|
||||
|
|
||||
public async Task CreateThumbnailAsync(Stream source, Stream destination, ResizeOptions options) |
|
||||
{ |
|
||||
Guard.NotNull(source, nameof(source)); |
|
||||
Guard.NotNull(destination, nameof(destination)); |
|
||||
Guard.NotNull(options, nameof(options)); |
|
||||
|
|
||||
if (!options.IsValid) |
|
||||
{ |
|
||||
await source.CopyToAsync(destination); |
|
||||
|
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
var w = options.Width ?? 0; |
|
||||
var h = options.Height ?? 0; |
|
||||
|
|
||||
await maxTasks.WaitAsync(); |
|
||||
try |
|
||||
{ |
|
||||
using (var image = Image.Load(source, out var format)) |
|
||||
{ |
|
||||
var encoder = GetEncoder(options, format); |
|
||||
|
|
||||
image.Mutate(x => x.AutoOrient()); |
|
||||
|
|
||||
if (w > 0 || h > 0) |
|
||||
{ |
|
||||
var isCropUpsize = options.Mode == ResizeMode.CropUpsize; |
|
||||
|
|
||||
if (!Enum.TryParse<ISResizeMode>(options.Mode.ToString(), true, out var resizeMode)) |
|
||||
{ |
|
||||
resizeMode = ISResizeMode.Max; |
|
||||
} |
|
||||
|
|
||||
if (isCropUpsize) |
|
||||
{ |
|
||||
resizeMode = ISResizeMode.Crop; |
|
||||
} |
|
||||
|
|
||||
if (w >= image.Width && h >= image.Height && resizeMode == ISResizeMode.Crop && !isCropUpsize) |
|
||||
{ |
|
||||
resizeMode = ISResizeMode.BoxPad; |
|
||||
} |
|
||||
|
|
||||
var resizeOptions = new ISResizeOptions { Size = new Size(w, h), Mode = resizeMode }; |
|
||||
|
|
||||
if (options.FocusX.HasValue && options.FocusY.HasValue) |
|
||||
{ |
|
||||
resizeOptions.CenterCoordinates = new PointF( |
|
||||
+(options.FocusX.Value / 2f) + 0.5f, |
|
||||
-(options.FocusY.Value / 2f) + 0.5f |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
image.Mutate(x => x.Resize(resizeOptions)); |
|
||||
} |
|
||||
|
|
||||
await image.SaveAsync(destination, encoder); |
|
||||
} |
|
||||
} |
|
||||
finally |
|
||||
{ |
|
||||
maxTasks.Release(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private static IImageEncoder GetEncoder(ResizeOptions options, IImageFormat? format) |
|
||||
{ |
|
||||
var encoder = Configuration.Default.ImageFormatsManager.FindEncoder(format); |
|
||||
|
|
||||
if (encoder == null) |
|
||||
{ |
|
||||
throw new NotSupportedException(); |
|
||||
} |
|
||||
|
|
||||
if (options.Quality.HasValue && (encoder is JpegEncoder || !options.KeepFormat) && options.Format == ImageFormat.Auto) |
|
||||
{ |
|
||||
encoder = new JpegEncoder { Quality = options.Quality.Value }; |
|
||||
} |
|
||||
else if (options.Format == ImageFormat.JPEG) |
|
||||
{ |
|
||||
encoder = new JpegEncoder(); |
|
||||
} |
|
||||
else if (options.Format == ImageFormat.PNG) |
|
||||
{ |
|
||||
encoder = new PngEncoder(); |
|
||||
} |
|
||||
else if (options.Format == ImageFormat.TGA) |
|
||||
{ |
|
||||
encoder = new TgaEncoder(); |
|
||||
} |
|
||||
else if (options.Format == ImageFormat.GIF) |
|
||||
{ |
|
||||
encoder = new GifEncoder(); |
|
||||
} |
|
||||
|
|
||||
return encoder; |
|
||||
} |
|
||||
|
|
||||
public Task<ImageInfo?> GetImageInfoAsync(Stream source) |
|
||||
{ |
|
||||
Guard.NotNull(source, nameof(source)); |
|
||||
|
|
||||
ImageInfo? result = null; |
|
||||
|
|
||||
try |
|
||||
{ |
|
||||
var image = Image.Identify(source, out var format); |
|
||||
|
|
||||
if (image != null) |
|
||||
{ |
|
||||
result = GetImageInfo(image); |
|
||||
|
|
||||
result.Format = format.Name; |
|
||||
} |
|
||||
} |
|
||||
catch |
|
||||
{ |
|
||||
result = null; |
|
||||
} |
|
||||
|
|
||||
return Task.FromResult(result); |
|
||||
} |
|
||||
|
|
||||
public async Task<ImageInfo> FixOrientationAsync(Stream source, Stream destination) |
|
||||
{ |
|
||||
Guard.NotNull(source, nameof(source)); |
|
||||
Guard.NotNull(destination, nameof(destination)); |
|
||||
|
|
||||
await maxTasks.WaitAsync(); |
|
||||
try |
|
||||
{ |
|
||||
using (var image = Image.Load(source, out var format)) |
|
||||
{ |
|
||||
var encoder = Configuration.Default.ImageFormatsManager.FindEncoder(format); |
|
||||
|
|
||||
if (encoder == null) |
|
||||
{ |
|
||||
throw new NotSupportedException(); |
|
||||
} |
|
||||
|
|
||||
image.Mutate(x => x.AutoOrient()); |
|
||||
|
|
||||
await image.SaveAsync(destination, encoder); |
|
||||
|
|
||||
return GetImageInfo(image); |
|
||||
} |
|
||||
} |
|
||||
finally |
|
||||
{ |
|
||||
maxTasks.Release(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private static ImageInfo GetImageInfo(IImageInfo image) |
|
||||
{ |
|
||||
var isRotatedOrSwapped = false; |
|
||||
|
|
||||
if (image.Metadata.ExifProfile != null) |
|
||||
{ |
|
||||
var value = image.Metadata.ExifProfile.GetValue(ExifTag.Orientation); |
|
||||
|
|
||||
isRotatedOrSwapped = value?.Value > 1; |
|
||||
} |
|
||||
|
|
||||
return new ImageInfo(image.Width, image.Height, isRotatedOrSwapped); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,128 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System.Collections.Concurrent; |
|
||||
using System.IO; |
|
||||
using System.Threading; |
|
||||
using System.Threading.Tasks; |
|
||||
using Squidex.Infrastructure.Tasks; |
|
||||
|
|
||||
namespace Squidex.Infrastructure.Assets |
|
||||
{ |
|
||||
public class MemoryAssetStore : IAssetStore |
|
||||
{ |
|
||||
private readonly ConcurrentDictionary<string, MemoryStream> streams = new ConcurrentDictionary<string, MemoryStream>(); |
|
||||
private readonly AsyncLock readerLock = new AsyncLock(); |
|
||||
private readonly AsyncLock writerLock = new AsyncLock(); |
|
||||
|
|
||||
public string? GeneratePublicUrl(string fileName) |
|
||||
{ |
|
||||
return null; |
|
||||
} |
|
||||
|
|
||||
public async Task<long> GetSizeAsync(string fileName, CancellationToken ct = default) |
|
||||
{ |
|
||||
Guard.NotNullOrEmpty(fileName, nameof(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, nameof(sourceFileName)); |
|
||||
Guard.NotNullOrEmpty(targetFileName, nameof(targetFileName)); |
|
||||
|
|
||||
if (!streams.TryGetValue(sourceFileName, out var sourceStream)) |
|
||||
{ |
|
||||
throw new AssetNotFoundException(sourceFileName); |
|
||||
} |
|
||||
|
|
||||
using (await readerLock.LockAsync()) |
|
||||
{ |
|
||||
await UploadAsync(targetFileName, sourceStream, false, ct); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public virtual async Task DownloadAsync(string fileName, Stream stream, BytesRange range = default, CancellationToken ct = default) |
|
||||
{ |
|
||||
Guard.NotNullOrEmpty(fileName, nameof(fileName)); |
|
||||
Guard.NotNull(stream, nameof(stream)); |
|
||||
|
|
||||
if (!streams.TryGetValue(fileName, out var sourceStream)) |
|
||||
{ |
|
||||
throw new AssetNotFoundException(fileName); |
|
||||
} |
|
||||
|
|
||||
using (await readerLock.LockAsync()) |
|
||||
{ |
|
||||
try |
|
||||
{ |
|
||||
await sourceStream.CopyToAsync(stream, range, ct); |
|
||||
} |
|
||||
finally |
|
||||
{ |
|
||||
sourceStream.Position = 0; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public virtual async Task UploadAsync(string fileName, Stream stream, bool overwrite = false, CancellationToken ct = default) |
|
||||
{ |
|
||||
Guard.NotNullOrEmpty(fileName, nameof(fileName)); |
|
||||
Guard.NotNull(stream, nameof(stream)); |
|
||||
|
|
||||
var memoryStream = new MemoryStream(); |
|
||||
|
|
||||
async Task CopyAsync() |
|
||||
{ |
|
||||
using (await writerLock.LockAsync()) |
|
||||
{ |
|
||||
try |
|
||||
{ |
|
||||
await stream.CopyToAsync(memoryStream, 81920, ct); |
|
||||
} |
|
||||
finally |
|
||||
{ |
|
||||
memoryStream.Position = 0; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
if (overwrite) |
|
||||
{ |
|
||||
await CopyAsync(); |
|
||||
|
|
||||
streams[fileName] = memoryStream; |
|
||||
} |
|
||||
else if (streams.TryAdd(fileName, memoryStream)) |
|
||||
{ |
|
||||
await CopyAsync(); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
throw new AssetAlreadyExistsException(fileName); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public virtual Task DeleteAsync(string fileName) |
|
||||
{ |
|
||||
Guard.NotNullOrEmpty(fileName, nameof(fileName)); |
|
||||
|
|
||||
streams.TryRemove(fileName, out _); |
|
||||
|
|
||||
return Task.CompletedTask; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,47 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using System.IO; |
|
||||
using System.Threading; |
|
||||
using System.Threading.Tasks; |
|
||||
|
|
||||
namespace Squidex.Infrastructure.Assets |
|
||||
{ |
|
||||
public sealed class NoopAssetStore : IAssetStore |
|
||||
{ |
|
||||
public string? GeneratePublicUrl(string fileName) |
|
||||
{ |
|
||||
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(); |
|
||||
} |
|
||||
|
|
||||
public Task DownloadAsync(string fileName, Stream stream, BytesRange range = default, CancellationToken ct = default) |
|
||||
{ |
|
||||
throw new NotSupportedException(); |
|
||||
} |
|
||||
|
|
||||
public Task UploadAsync(string fileName, Stream stream, bool overwrite = false, CancellationToken ct = default) |
|
||||
{ |
|
||||
throw new NotSupportedException(); |
|
||||
} |
|
||||
|
|
||||
public Task DeleteAsync(string fileName) |
|
||||
{ |
|
||||
throw new NotSupportedException(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,20 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
namespace Squidex.Infrastructure.Assets |
|
||||
{ |
|
||||
public enum ResizeMode |
|
||||
{ |
|
||||
Crop, |
|
||||
CropUpsize, |
|
||||
Pad, |
|
||||
BoxPad, |
|
||||
Max, |
|
||||
Min, |
|
||||
Stretch |
|
||||
} |
|
||||
} |
|
||||
@ -1,72 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System.Text; |
|
||||
|
|
||||
namespace Squidex.Infrastructure.Assets |
|
||||
{ |
|
||||
public sealed class ResizeOptions |
|
||||
{ |
|
||||
public ImageFormat Format { get; set; } |
|
||||
|
|
||||
public ResizeMode Mode { get; set; } |
|
||||
|
|
||||
public int? Width { get; set; } |
|
||||
|
|
||||
public int? Height { get; set; } |
|
||||
|
|
||||
public int? Quality { get; set; } |
|
||||
|
|
||||
public float? FocusX { get; set; } |
|
||||
|
|
||||
public float? FocusY { get; set; } |
|
||||
|
|
||||
public bool KeepFormat { get; set; } |
|
||||
|
|
||||
public bool IsValid |
|
||||
{ |
|
||||
get { return Width > 0 || Height > 0 || Quality > 0 || Format != ImageFormat.Auto; } |
|
||||
} |
|
||||
|
|
||||
public override string ToString() |
|
||||
{ |
|
||||
var sb = new StringBuilder(); |
|
||||
|
|
||||
sb.Append(Width); |
|
||||
sb.Append("_"); |
|
||||
sb.Append(Height); |
|
||||
sb.Append("_"); |
|
||||
sb.Append(Mode); |
|
||||
|
|
||||
if (Quality.HasValue) |
|
||||
{ |
|
||||
sb.Append("_"); |
|
||||
sb.Append(Quality); |
|
||||
} |
|
||||
|
|
||||
if (FocusX.HasValue) |
|
||||
{ |
|
||||
sb.Append("_focusX_"); |
|
||||
sb.Append(FocusX); |
|
||||
} |
|
||||
|
|
||||
if (FocusY.HasValue) |
|
||||
{ |
|
||||
sb.Append("_focusY_"); |
|
||||
sb.Append(FocusY); |
|
||||
} |
|
||||
|
|
||||
if (Format != ImageFormat.Auto) |
|
||||
{ |
|
||||
sb.Append("_format_"); |
|
||||
sb.Append(Format.ToString()); |
|
||||
} |
|
||||
|
|
||||
return sb.ToString(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,64 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// 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); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,65 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
|
|
||||
namespace Squidex.Infrastructure |
|
||||
{ |
|
||||
public readonly 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; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue