mirror of https://github.com/Squidex/squidex.git
18 changed files with 564 additions and 25 deletions
@ -0,0 +1,6 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<configuration> |
|||
<packageSources> |
|||
<add key="ImageSharp Nightly" value="https://www.myget.org/F/imagesharp/api/v3/index.json" /> |
|||
</packageSources> |
|||
</configuration> |
|||
@ -0,0 +1,22 @@ |
|||
// ==========================================================================
|
|||
// AssetUpdated.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Events.Assets |
|||
{ |
|||
[TypeName("AssetUpdated")] |
|||
public class AssetUpdated : AssetEvent |
|||
{ |
|||
public string MimeType { get; set; } |
|||
|
|||
public long FileSize { get; set; } |
|||
|
|||
public bool IsImage { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
// ==========================================================================
|
|||
// IAssetStorage.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.IO; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Squidex.Infrastructure.Images |
|||
{ |
|||
public interface IAssetStorage |
|||
{ |
|||
Task<Stream> GetAssetAsync(Guid id, string tags = null); |
|||
|
|||
Task UploadAssetAsync(Guid id, Stream stream, string tags = null); |
|||
} |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
// ==========================================================================
|
|||
// IThumbnailGenerator.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System.IO; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Squidex.Infrastructure.Images |
|||
{ |
|||
public interface IThumbnailGenerator |
|||
{ |
|||
Task<Stream> GetThumbnailOrNullAsync(Stream input, int dimension); |
|||
} |
|||
} |
|||
@ -0,0 +1,46 @@ |
|||
// ==========================================================================
|
|||
// ImageSharpThumbnailGenerator.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System.IO; |
|||
using System.Threading.Tasks; |
|||
using ImageSharp; |
|||
using ImageSharp.Formats; |
|||
using ImageSharp.Processing; |
|||
|
|||
namespace Squidex.Infrastructure.Images.ImageSharp |
|||
{ |
|||
public sealed class ImageSharpThumbnailGenerator : IThumbnailGenerator |
|||
{ |
|||
public ImageSharpThumbnailGenerator() |
|||
{ |
|||
Configuration.Default.AddImageFormat(new JpegFormat()); |
|||
Configuration.Default.AddImageFormat(new PngFormat()); |
|||
} |
|||
|
|||
public Task<Stream> GetThumbnailOrNullAsync(Stream input, int dimension) |
|||
{ |
|||
return Task.Run<Stream>(() => |
|||
{ |
|||
var result = new MemoryStream(); |
|||
|
|||
var options = |
|||
new ResizeOptions |
|||
{ |
|||
Size = new Size(dimension, dimension), |
|||
Mode = ResizeMode.Max |
|||
}; |
|||
|
|||
var image = new Image(input).Resize(options); |
|||
|
|||
image.Save(result); |
|||
|
|||
return result; |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,84 @@ |
|||
// ==========================================================================
|
|||
// PhysicalAssetStorage.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.IO; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Squidex.Infrastructure.Images.Physical |
|||
{ |
|||
public sealed class PhysicalAssetStorage : IAssetStorage, IExternalSystem |
|||
{ |
|||
private readonly DirectoryInfo directory; |
|||
|
|||
public PhysicalAssetStorage(string path) |
|||
{ |
|||
Guard.NotNullOrEmpty(path, nameof(path)); |
|||
|
|||
directory = new DirectoryInfo(path); |
|||
} |
|||
|
|||
public void Connect() |
|||
{ |
|||
try |
|||
{ |
|||
if (!directory.Exists) |
|||
{ |
|||
directory.Create(); |
|||
} |
|||
} |
|||
catch |
|||
{ |
|||
if (!directory.Exists) |
|||
{ |
|||
throw new ConfigurationException($"Cannot access directory {directory.FullName}"); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public Task<Stream> GetAssetAsync(Guid id, string tags = null) |
|||
{ |
|||
var file = GetFile(id, tags); |
|||
|
|||
Stream stream = null; |
|||
|
|||
if (file.Exists) |
|||
{ |
|||
stream = file.OpenRead(); |
|||
} |
|||
|
|||
return Task.FromResult(stream); |
|||
} |
|||
|
|||
public async Task UploadAssetAsync(Guid id, Stream stream, string tags = null) |
|||
{ |
|||
var file = GetFile(id, tags); |
|||
|
|||
using (var fileStream = file.OpenWrite()) |
|||
{ |
|||
await stream.CopyToAsync(fileStream); |
|||
} |
|||
} |
|||
|
|||
private FileInfo GetFile(Guid id, string tags) |
|||
{ |
|||
var fileName = id.ToString(); |
|||
|
|||
if (!string.IsNullOrWhiteSpace(tags)) |
|||
{ |
|||
fileName += tags; |
|||
} |
|||
|
|||
Guard.ValidFileName(fileName, tags); |
|||
|
|||
var file = new FileInfo(Path.Combine(directory.FullName, fileName)); |
|||
|
|||
return file; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,41 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Text; |
|||
using MongoDB.Bson.Serialization.Attributes; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.MongoDb; |
|||
using Squidex.Read.Assets; |
|||
|
|||
namespace Squidex.Read.MongoDb.Assets |
|||
{ |
|||
public sealed class MongoAssetEntity : MongoEntity, IAssetEntity |
|||
{ |
|||
[BsonRequired] |
|||
[BsonElement] |
|||
public string MimeType { get; set; } |
|||
|
|||
[BsonRequired] |
|||
[BsonElement] |
|||
public string Name { get; set; } |
|||
|
|||
[BsonRequired] |
|||
[BsonElement] |
|||
public long FileSize { get; set; } |
|||
|
|||
[BsonRequired] |
|||
[BsonElement] |
|||
public long Version { get; set; } |
|||
|
|||
[BsonRequired] |
|||
[BsonElement] |
|||
public Guid AppId { get; set; } |
|||
|
|||
[BsonRequired] |
|||
[BsonElement] |
|||
public RefToken CreatedBy { get; set; } |
|||
|
|||
[BsonRequired] |
|||
[BsonElement] |
|||
public RefToken LastModifiedBy { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,78 @@ |
|||
// ==========================================================================
|
|||
// MongoAssetRepository.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using MongoDB.Bson; |
|||
using MongoDB.Driver; |
|||
using Squidex.Infrastructure.MongoDb; |
|||
using Squidex.Read.Assets; |
|||
using Squidex.Read.Assets.Repositories; |
|||
|
|||
namespace Squidex.Read.MongoDb.Assets |
|||
{ |
|||
public partial class MongoAssetRepository : MongoRepositoryBase<MongoAssetEntity>, IAssetRepository |
|||
{ |
|||
public MongoAssetRepository(IMongoDatabase database) |
|||
: base(database) |
|||
{ |
|||
} |
|||
|
|||
public async Task<IReadOnlyList<IAssetEntity>> QueryAsync(Guid appId, HashSet<string> mimeTypes = null, string query = null, int take = 10, int skip = 0) |
|||
{ |
|||
var filter = CreateFilter(appId, mimeTypes, query); |
|||
|
|||
var assets = |
|||
await Collection.Find(filter).Skip(skip).Limit(take).ToListAsync(); |
|||
|
|||
return assets.OfType<IAssetEntity>().ToList(); |
|||
} |
|||
|
|||
public async Task<long> CountAsync(Guid appId, HashSet<string> mimeTypes = null, string query = null) |
|||
{ |
|||
var filter = CreateFilter(appId, mimeTypes, query); |
|||
|
|||
var count = |
|||
await Collection.Find(filter).CountAsync(); |
|||
|
|||
return count; |
|||
} |
|||
|
|||
public async Task<IAssetEntity> FindAssetAsync(Guid id) |
|||
{ |
|||
var entity = |
|||
await Collection.Find(s => s.Id == id).FirstOrDefaultAsync(); |
|||
|
|||
return entity; |
|||
} |
|||
|
|||
private static FilterDefinition<MongoAssetEntity> CreateFilter(Guid appId, HashSet<string> mimeTypes, string query) |
|||
{ |
|||
var filters = new List<FilterDefinition<MongoAssetEntity>> |
|||
{ |
|||
Filter.Eq(x => x.AppId, appId) |
|||
}; |
|||
|
|||
if (mimeTypes != null && mimeTypes.Count > 0) |
|||
{ |
|||
filters.Add(Filter.In(x => x.MimeType, mimeTypes)); |
|||
} |
|||
|
|||
if (!string.IsNullOrWhiteSpace(query)) |
|||
{ |
|||
filters.Add(Filter.Regex(x => x.Name, new BsonRegularExpression(query, "i"))); |
|||
} |
|||
|
|||
var filter = Filter.And(filters); |
|||
|
|||
return filter; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,50 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
using Squidex.Events.Apps; |
|||
using Squidex.Events.Assets; |
|||
using Squidex.Infrastructure.CQRS.Events; |
|||
using Squidex.Infrastructure.Dispatching; |
|||
using Squidex.Infrastructure.Reflection; |
|||
using Squidex.Read.MongoDb.Utils; |
|||
|
|||
namespace Squidex.Read.MongoDb.Assets |
|||
{ |
|||
public partial class MongoAssetRepository |
|||
{ |
|||
public Task On(Envelope<IEvent> @event) |
|||
{ |
|||
return this.DispatchActionAsync(@event.Payload, @event.Headers); |
|||
} |
|||
|
|||
protected Task On(AssetCreated @event, EnvelopeHeaders headers) |
|||
{ |
|||
return Collection.CreateAsync(@event, headers, a => |
|||
{ |
|||
SimpleMapper.Map(@event, a); |
|||
}); |
|||
} |
|||
|
|||
protected Task On(AssetUpdated @event, EnvelopeHeaders headers) |
|||
{ |
|||
return Collection.UpdateAsync(@event, headers, a => |
|||
{ |
|||
SimpleMapper.Map(@event, a); |
|||
}); |
|||
} |
|||
|
|||
protected Task On(AssetRenamed @event, EnvelopeHeaders headers) |
|||
{ |
|||
return Collection.UpdateAsync(@event, headers, a => |
|||
{ |
|||
SimpleMapper.Map(@event, a); |
|||
}); |
|||
} |
|||
|
|||
protected Task On(AssetDeleted @event, EnvelopeHeaders headers) |
|||
{ |
|||
return Collection.DeleteOneAsync(Filter.Eq(x => x.Id, @event.AssetId)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
// ==========================================================================
|
|||
// IAssetEntity.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Read.Assets |
|||
{ |
|||
public interface IAssetEntity : IAppRefEntity, IEntityWithCreatedBy, IEntityWithLastModifiedBy, IEntityWithVersion |
|||
{ |
|||
string MimeType { get; } |
|||
|
|||
string Name { get; } |
|||
|
|||
long FileSize { get; } |
|||
} |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
// ==========================================================================
|
|||
// IAssetRepository.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Squidex.Read.Assets.Repositories |
|||
{ |
|||
public interface IAssetRepository |
|||
{ |
|||
Task<IReadOnlyList<IAssetEntity>> QueryAsync(Guid appId, HashSet<string> mimeTypes = null, string query = null, int take = 10, int skip = 0); |
|||
|
|||
Task<long> CountAsync(Guid appId, HashSet<string> mimeTypes = null, string query = null); |
|||
|
|||
Task<IAssetEntity> FindAssetAsync(Guid id); |
|||
} |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
// ==========================================================================
|
|||
// AssetConfig.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Controllers.Api.Assets |
|||
{ |
|||
public sealed class AssetConfig |
|||
{ |
|||
public long MaxSize { get; set; } = 5 * 1024 * 1024; |
|||
} |
|||
} |
|||
@ -0,0 +1,110 @@ |
|||
// ==========================================================================
|
|||
// AssetsController.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.IO; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Authorization; |
|||
using Microsoft.AspNetCore.Http; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Microsoft.Extensions.Options; |
|||
using NSwag.Annotations; |
|||
using Squidex.Core.Identity; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.CQRS.Commands; |
|||
using Squidex.Infrastructure.Images; |
|||
using Squidex.Pipeline; |
|||
using Squidex.Write.Assets.Commands; |
|||
|
|||
namespace Squidex.Controllers.Api.Assets |
|||
{ |
|||
/// <summary>
|
|||
/// Uploads and retrieves assets.
|
|||
/// </summary>
|
|||
[Authorize(Roles = SquidexRoles.AppEditor)] |
|||
[ApiExceptionFilter] |
|||
[ServiceFilter(typeof(AppFilterAttribute))] |
|||
[SwaggerTag("Assets")] |
|||
public class AssetsController : ControllerBase |
|||
{ |
|||
private readonly IAssetStorage assetStorage; |
|||
private readonly AssetConfig assetsConfig; |
|||
private readonly IThumbnailGenerator thumbnailGenerator; |
|||
|
|||
public AssetsController( |
|||
ICommandBus commandBus, |
|||
IAssetStorage assetStorage, |
|||
IThumbnailGenerator thumbnailGenerator, |
|||
IOptions<AssetConfig> assetsConfig) |
|||
: base(commandBus) |
|||
{ |
|||
this.assetStorage = assetStorage; |
|||
this.assetsConfig = assetsConfig.Value; |
|||
this.thumbnailGenerator = thumbnailGenerator; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates and uploads a new asset.
|
|||
/// </summary>
|
|||
/// <param name="app">The name of the app.</param>
|
|||
/// <param name="file">The name of the schema.</param>
|
|||
/// <returns>
|
|||
/// 201 => Asset created.
|
|||
/// 404 => App not found.
|
|||
/// 400 => Asset exceeds the maximum size.
|
|||
/// </returns>
|
|||
[HttpPost] |
|||
[Route("apps/{app}/schemas/{name}/fields/")] |
|||
[ProducesResponseType(typeof(EntityCreatedDto), 201)] |
|||
[ProducesResponseType(typeof(ErrorDto), 409)] |
|||
[ProducesResponseType(typeof(ErrorDto), 400)] |
|||
public async Task<IActionResult> PostAsset(string app, IFormFile file) |
|||
{ |
|||
if (file.Length > assetsConfig.MaxSize) |
|||
{ |
|||
var error = new ValidationError($"File size cannot be longer than ${assetsConfig.MaxSize}."); |
|||
|
|||
throw new ValidationException("Cannot create asset.", error); |
|||
} |
|||
|
|||
var command = new CreateAsset |
|||
{ |
|||
AssetId = Guid.NewGuid(), |
|||
FileSize = file.Length, |
|||
FileName = file.Name, |
|||
MimeType = file.ContentType |
|||
}; |
|||
|
|||
var fileContent = new MemoryStream(); |
|||
|
|||
await file.CopyToAsync(fileContent); |
|||
|
|||
fileContent.Position = 0; |
|||
|
|||
var fileThumbnail = await thumbnailGenerator.GetThumbnailOrNullAsync(fileContent, 200); |
|||
|
|||
if (fileThumbnail != null) |
|||
{ |
|||
command.IsImage = true; |
|||
|
|||
await assetStorage.UploadAssetAsync(command.AssetId, fileThumbnail, "thumbnail"); |
|||
} |
|||
|
|||
fileContent.Position = 0; |
|||
|
|||
await assetStorage.UploadAssetAsync(command.AssetId, fileContent); |
|||
|
|||
var context = await CommandBus.PublishAsync(command); |
|||
|
|||
var result = context.Result<EntityCreatedResult<Guid>>().IdOrValue; |
|||
var response = new EntityCreatedDto { Id = result.ToString() }; |
|||
|
|||
return StatusCode(201, response); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue