Browse Source

Some progress with asset management API

pull/65/head
Sebastian 9 years ago
parent
commit
9cabed3c90
  1. 6
      Nuget.config
  2. 6
      src/Squidex.Events/Assets/AssetCreated.cs
  3. 3
      src/Squidex.Events/Assets/AssetEvent.cs
  4. 22
      src/Squidex.Events/Assets/AssetUpdated.cs
  5. 21
      src/Squidex.Infrastructure/Images/IAssetStorage.cs
  6. 18
      src/Squidex.Infrastructure/Images/IThumbnailGenerator.cs
  7. 46
      src/Squidex.Infrastructure/Images/ImageSharp/ImageSharpThumbnailGenerator.cs
  8. 84
      src/Squidex.Infrastructure/Images/Physical/PhysicalAssetStorage.cs
  9. 4
      src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
  10. 41
      src/Squidex.Read.MongoDb/Assets/MongoAssetEntity.cs
  11. 78
      src/Squidex.Read.MongoDb/Assets/MongoAssetRepository.cs
  12. 50
      src/Squidex.Read.MongoDb/Assets/MongoAssetRepository_EventHandling.cs
  13. 19
      src/Squidex.Read/Assets/IAssetEntity.cs
  14. 23
      src/Squidex.Read/Assets/Repositories/IAssetRepository.cs
  15. 24
      src/Squidex.Write/Assets/AssetDomainObject.cs
  16. 19
      src/Squidex.Write/Assets/Commands/CreateAsset.cs
  17. 15
      src/Squidex/Controllers/Api/Assets/AssetConfig.cs
  18. 110
      src/Squidex/Controllers/Api/Assets/AssetsController.cs

6
Nuget.config

@ -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>

6
src/Squidex.Events/Assets/AssetCreated.cs

@ -14,5 +14,11 @@ namespace Squidex.Events.Assets
public class AssetCreated : AssetEvent
{
public string Name { get; set; }
public string MimeType { get; set; }
public long FileSize { get; set; }
public bool IsImage { get; set; }
}
}

3
src/Squidex.Events/Assets/AssetEvent.cs

@ -7,11 +7,10 @@
// ==========================================================================
using System;
using Squidex.Infrastructure.CQRS.Events;
namespace Squidex.Events.Assets
{
public abstract class AssetEvent : IEvent
public abstract class AssetEvent : AppEvent
{
public Guid AssetId { get; set; }
}

22
src/Squidex.Events/Assets/AssetUpdated.cs

@ -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; }
}
}

21
src/Squidex.Infrastructure/Images/IAssetStorage.cs

@ -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);
}
}

18
src/Squidex.Infrastructure/Images/IThumbnailGenerator.cs

@ -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);
}
}

46
src/Squidex.Infrastructure/Images/ImageSharp/ImageSharpThumbnailGenerator.cs

@ -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;
});
}
}
}

84
src/Squidex.Infrastructure/Images/Physical/PhysicalAssetStorage.cs

@ -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;
}
}
}

4
src/Squidex.Infrastructure/Squidex.Infrastructure.csproj

@ -7,6 +7,10 @@
<DebugSymbols>True</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ImageSharp" Version="1.0.0-alpha4-00031" />
<PackageReference Include="ImageSharp.Formats.Jpeg" Version="1.0.0-alpha2-00158" />
<PackageReference Include="ImageSharp.Formats.Png" Version="1.0.0-alpha2-00154" />
<PackageReference Include="ImageSharp.Processing" Version="1.0.0-alpha2-00146" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="1.1.1" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="1.1.1" />
<PackageReference Include="Newtonsoft.Json" Version="9.0.2-beta2" />

41
src/Squidex.Read.MongoDb/Assets/MongoAssetEntity.cs

@ -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; }
}
}

78
src/Squidex.Read.MongoDb/Assets/MongoAssetRepository.cs

@ -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;
}
}
}

50
src/Squidex.Read.MongoDb/Assets/MongoAssetRepository_EventHandling.cs

@ -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));
}
}
}

19
src/Squidex.Read/Assets/IAssetEntity.cs

@ -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; }
}
}

23
src/Squidex.Read/Assets/Repositories/IAssetRepository.cs

@ -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);
}
}

24
src/Squidex.Write/Assets/AssetDomainObject.cs

@ -15,21 +15,23 @@ using Squidex.Infrastructure.Dispatching;
using Squidex.Infrastructure.Reflection;
using Squidex.Write.Assets.Commands;
// ReSharper disable UnusedParameter.Local
namespace Squidex.Write.Assets
{
public class AssetDomainObject : DomainObject
public class AssetDomainObject : DomainObjectBase
{
private bool isDeleted;
private string name;
private string fileName;
public bool IsDeleted
{
get { return isDeleted; }
}
public string Name
public string FileName
{
get { return name; }
get { return fileName; }
}
public AssetDomainObject(Guid id, int version)
@ -39,12 +41,12 @@ namespace Squidex.Write.Assets
protected void On(AssetCreated @event)
{
name = @event.Name;
fileName = @event.Name;
}
protected void On(AssetRenamed @event)
{
name = @event.Name;
fileName = @event.Name;
}
protected void On(AssetDeleted @event)
@ -54,11 +56,11 @@ namespace Squidex.Write.Assets
public AssetDomainObject Create(CreateAsset command)
{
Guard.Valid(command, nameof(command), () => "Cannot create content");
Guard.NotNull(command, nameof(command));
VerifyNotCreated();
RaiseEvent(SimpleMapper.Map(command, new AssetCreated()));
RaiseEvent(SimpleMapper.Map(command, new AssetCreated { Name = command.FileName }));
return this;
}
@ -88,7 +90,7 @@ namespace Squidex.Write.Assets
private void VerifyDifferentNames(string newName, Func<string> message)
{
if (string.Equals(name, newName))
if (string.Equals(fileName, newName))
{
throw new ValidationException(message(), new ValidationError("The asset already has this name.", "Name"));
}
@ -96,7 +98,7 @@ namespace Squidex.Write.Assets
private void VerifyNotCreated()
{
if (!string.IsNullOrWhiteSpace(name))
if (!string.IsNullOrWhiteSpace(fileName))
{
throw new DomainException("Asset has already been created.");
}
@ -104,7 +106,7 @@ namespace Squidex.Write.Assets
private void VerifyCreatedAndNotDeleted()
{
if (isDeleted || !string.IsNullOrWhiteSpace(name))
if (isDeleted || !string.IsNullOrWhiteSpace(fileName))
{
throw new DomainException("Asset has already been deleted or not created yet.");
}

19
src/Squidex.Write/Assets/Commands/CreateAsset.cs

@ -6,21 +6,16 @@
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using Squidex.Infrastructure;
namespace Squidex.Write.Assets.Commands
{
public sealed class CreateAsset : AssetAggregateCommand, IValidatable
public sealed class CreateAsset : AssetAggregateCommand
{
public string Name { get; set; }
public string FileName { get; set; }
public string MimeType { get; set; }
public long FileSize { get; set; }
public void Validate(IList<ValidationError> errors)
{
if (!Name.IsSlug())
{
errors.Add(new ValidationError("Name must be a valid slug", nameof(Name)));
}
}
public bool IsImage { get; set; }
}
}

15
src/Squidex/Controllers/Api/Assets/AssetConfig.cs

@ -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;
}
}

110
src/Squidex/Controllers/Api/Assets/AssetsController.cs

@ -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…
Cancel
Save