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