Browse Source

New API structure

pull/65/head
Sebastian Stehle 10 years ago
parent
commit
247bfe3f36
  1. 44
      src/Squidex.Infrastructure/Assets/AssetFile.cs
  2. 20
      src/Squidex.Infrastructure/Assets/FolderAssetStore.cs
  3. 5
      src/Squidex.Infrastructure/Assets/IAssetStore.cs
  4. 5
      src/Squidex.Infrastructure/Assets/ImageInfo.cs
  5. 5
      src/Squidex.Read.MongoDb/Assets/MongoAssetRepository.cs
  6. 2
      src/Squidex.Write/Apps/AppDomainObject.cs
  7. 9
      src/Squidex.Write/Apps/Commands/CreateApp.cs
  8. 33
      src/Squidex.Write/Assets/AssetCommandHandler.cs
  9. 29
      src/Squidex.Write/Assets/AssetDomainObject.cs
  10. 14
      src/Squidex.Write/Assets/Commands/CreateAsset.cs
  11. 12
      src/Squidex.Write/Assets/Commands/UpdateAsset.cs
  12. 27
      src/Squidex/Controllers/Api/Assets/AssetContentController.cs
  13. 131
      src/Squidex/Controllers/Api/Assets/AssetController.cs
  14. 12
      src/Squidex/Controllers/Api/Assets/Models/AssetDto.cs
  15. 21
      src/Squidex/Controllers/Api/Assets/Models/AssetUpdateDto.cs
  16. 1
      src/Squidex/Squidex.csproj
  17. 14
      src/Squidex/app/features/assets/pages/asset.component.html
  18. 41
      src/Squidex/app/features/assets/pages/asset.component.scss
  19. 51
      src/Squidex/app/features/assets/pages/asset.component.ts
  20. 2
      src/Squidex/app/shared/services/assets.service.ts
  21. BIN
      src/Squidex/wwwroot/images/asset_doc.png
  22. BIN
      src/Squidex/wwwroot/images/asset_docx.png
  23. BIN
      src/Squidex/wwwroot/images/asset_generic.png
  24. BIN
      src/Squidex/wwwroot/images/asset_pdf.png
  25. BIN
      src/Squidex/wwwroot/images/asset_ppt.png
  26. BIN
      src/Squidex/wwwroot/images/asset_pptx.png
  27. BIN
      src/Squidex/wwwroot/images/asset_video.png
  28. BIN
      src/Squidex/wwwroot/images/asset_xls.png
  29. BIN
      src/Squidex/wwwroot/images/asset_xlsx.png
  30. 6
      tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs
  31. 2
      tests/Squidex.Write.Tests/Apps/AppDomainObjectTests.cs
  32. 36
      tests/Squidex.Write.Tests/Assets/AssetCommandHandlerTests.cs
  33. 42
      tests/Squidex.Write.Tests/Assets/AssetDomainObjectTests.cs
  34. 2
      tests/Squidex.Write.Tests/Contents/ContentCommandHandlerTests.cs
  35. 2
      tests/Squidex.Write.Tests/Schemas/SchemaCommandHandlerTests.cs

44
src/Squidex.Infrastructure/Assets/AssetFile.cs

@ -0,0 +1,44 @@
// ==========================================================================
// IAssetFile.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.IO;
namespace Squidex.Infrastructure.Assets
{
public sealed class AssetFile
{
private readonly Func<Stream> openAction;
public string FileName { get; }
public string MimeType { get; }
public long FileSize { get; }
public AssetFile(string fileName, string mimeType, long fileSize, Func<Stream> openAction)
{
Guard.NotNullOrEmpty(fileName, nameof(fileName));
Guard.NotNullOrEmpty(mimeType, nameof(mimeType));
Guard.NotNull(openAction, nameof(openAction));
Guard.GreaterThan(fileSize, 0, nameof(fileSize));
FileName = fileName;
FileSize = fileSize;
MimeType = mimeType;
this.openAction = openAction;
}
public Stream OpenRead()
{
return openAction();
}
}
}

20
src/Squidex.Infrastructure/Assets/FolderAssetStore.cs

@ -6,6 +6,7 @@
// All rights reserved. // All rights reserved.
// ========================================================================== // ==========================================================================
using System;
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Log;
@ -49,9 +50,9 @@ namespace Squidex.Infrastructure.Assets
} }
} }
public Task<Stream> GetAssetAsync(string name) public Task<Stream> GetAssetAsync(Guid id, long version, string suffix = null)
{ {
var file = GetFile(name); var file = GetFile(id, version, suffix);
Stream stream = null; Stream stream = null;
@ -70,9 +71,9 @@ namespace Squidex.Infrastructure.Assets
return Task.FromResult(stream); return Task.FromResult(stream);
} }
public async Task UploadAssetAsync(string name, Stream stream) public async Task UploadAssetAsync(Guid id, long version, Stream stream, string suffix = null)
{ {
var file = GetFile(name); var file = GetFile(id, version, suffix);
using (var fileStream = file.OpenWrite()) using (var fileStream = file.OpenWrite())
{ {
@ -80,13 +81,16 @@ namespace Squidex.Infrastructure.Assets
} }
} }
private FileInfo GetFile(string name) private FileInfo GetFile(Guid id, long version, string suffix)
{ {
Guard.ValidFileName(name, nameof(name)); var path = Path.Combine(directory.FullName, $"{id}_{version}");
var file = new FileInfo(Path.Combine(directory.FullName, name)); if (!string.IsNullOrWhiteSpace(suffix))
{
path += "_" + suffix;
}
return file; return new FileInfo(path);
} }
} }
} }

5
src/Squidex.Infrastructure/Assets/IAssetStore.cs

@ -6,6 +6,7 @@
// All rights reserved. // All rights reserved.
// ========================================================================== // ==========================================================================
using System;
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -13,8 +14,8 @@ namespace Squidex.Infrastructure.Assets
{ {
public interface IAssetStore public interface IAssetStore
{ {
Task<Stream> GetAssetAsync(string name); Task<Stream> GetAssetAsync(Guid id, long version, string suffix = null);
Task UploadAssetAsync(string name, Stream stream); Task UploadAssetAsync(Guid id, long version, Stream stream, string suffix = null);
} }
} }

5
src/Squidex.Infrastructure/Assets/ImageInfo.cs

@ -3,7 +3,7 @@
// Squidex Headless CMS // Squidex Headless CMS
// ========================================================================== // ==========================================================================
// Copyright (c) Squidex Group // Copyright (c) Squidex Group
// All rights reserved. // All rights reserved.d
// ========================================================================== // ==========================================================================
namespace Squidex.Infrastructure.Assets namespace Squidex.Infrastructure.Assets
@ -16,6 +16,9 @@ namespace Squidex.Infrastructure.Assets
public ImageInfo(int pixelWidth, int pixelHeight) public ImageInfo(int pixelWidth, int pixelHeight)
{ {
Guard.GreaterThan(pixelWidth, 0, nameof(pixelWidth));
Guard.GreaterThan(pixelHeight, 0, nameof(pixelHeight));
PixelWidth = pixelWidth; PixelWidth = pixelWidth;
PixelHeight = pixelHeight; PixelHeight = pixelHeight;
} }

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

@ -31,6 +31,11 @@ namespace Squidex.Read.MongoDb.Assets
return "Projections_Assets"; return "Projections_Assets";
} }
protected override Task SetupCollectionAsync(IMongoCollection<MongoAssetEntity> collection)
{
return Collection.Indexes.CreateOneAsync(IndexKeys.Descending(x => x.LastModified).Ascending(x => x.AppId).Ascending(x => x.FileName).Ascending(x => x.MimeType));
}
public async Task<IReadOnlyList<IAssetEntity>> QueryAsync(Guid appId, HashSet<string> mimeTypes = null, string query = null, int take = 10, int skip = 0) 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 filter = CreateFilter(appId, mimeTypes, query);

2
src/Squidex.Write/Apps/AppDomainObject.cs

@ -101,7 +101,7 @@ namespace Squidex.Write.Apps
ThrowIfCreated(); ThrowIfCreated();
var appId = new NamedId<Guid>(command.AggregateId, command.Name); var appId = new NamedId<Guid>(command.AppId, command.Name);
RaiseEvent(SimpleMapper.Map(command, new AppCreated { AppId = appId })); RaiseEvent(SimpleMapper.Map(command, new AppCreated { AppId = appId }));

9
src/Squidex.Write/Apps/Commands/CreateApp.cs

@ -17,11 +17,16 @@ namespace Squidex.Write.Apps.Commands
{ {
public string Name { get; set; } public string Name { get; set; }
public Guid AggregateId { get; set; } public Guid AppId { get; set; }
Guid IAggregateCommand.AggregateId
{
get { return AppId; }
}
public CreateApp() public CreateApp()
{ {
AggregateId = Guid.NewGuid(); AppId = Guid.NewGuid();
} }
public void Validate(IList<ValidationError> errors) public void Validate(IList<ValidationError> errors)

33
src/Squidex.Write/Assets/AssetCommandHandler.cs

@ -8,6 +8,7 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Infrastructure.Dispatching; using Squidex.Infrastructure.Dispatching;
using Squidex.Infrastructure.Tasks; using Squidex.Infrastructure.Tasks;
@ -18,32 +19,52 @@ namespace Squidex.Write.Assets
public class AssetCommandHandler : ICommandHandler public class AssetCommandHandler : ICommandHandler
{ {
private readonly IAggregateHandler handler; private readonly IAggregateHandler handler;
private readonly IAssetStore assetStore;
private readonly IAssetThumbnailGenerator assetThumbnailGenerator;
public AssetCommandHandler(IAggregateHandler handler) public AssetCommandHandler(
IAggregateHandler handler,
IAssetStore assetStore,
IAssetThumbnailGenerator assetThumbnailGenerator)
{ {
Guard.NotNull(handler, nameof(handler)); Guard.NotNull(handler, nameof(handler));
Guard.NotNull(assetStore, nameof(assetStore));
Guard.NotNull(assetThumbnailGenerator, nameof(assetThumbnailGenerator));
this.handler = handler; this.handler = handler;
this.assetStore = assetStore;
this.assetThumbnailGenerator = assetThumbnailGenerator;
} }
protected async Task On(CreateAsset command, CommandContext context) protected async Task On(CreateAsset command, CommandContext context)
{ {
await handler.CreateAsync<AssetDomainObject>(context, c => await handler.CreateAsync<AssetDomainObject>(context, async c =>
{ {
command.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(command.File.OpenRead());
c.Create(command); c.Create(command);
await assetStore.UploadAssetAsync(c.Id, c.Version, command.File.OpenRead());
context.Succeed(EntityCreatedResult.Create(c.Id, c.Version)); context.Succeed(EntityCreatedResult.Create(c.Id, c.Version));
}); });
} }
protected async Task On(RenameAsset command, CommandContext context) protected async Task On(UpdateAsset command, CommandContext context)
{ {
await handler.UpdateAsync<AssetDomainObject>(context, c => c.Rename(command)); await handler.UpdateAsync<AssetDomainObject>(context, async c =>
{
command.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(command.File.OpenRead());
c.Update(command);
await assetStore.UploadAssetAsync(c.Id, c.Version, command.File.OpenRead());
});
} }
protected async Task On(UpdateAsset command, CommandContext context) protected Task On(RenameAsset command, CommandContext context)
{ {
await handler.UpdateAsync<AssetDomainObject>(context, c => c.Update(command)); return handler.UpdateAsync<AssetDomainObject>(context, c => c.Rename(command));
} }
protected Task On(DeleteAsset command, CommandContext context) protected Task On(DeleteAsset command, CommandContext context)

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

@ -60,29 +60,48 @@ namespace Squidex.Write.Assets
VerifyNotCreated(); VerifyNotCreated();
RaiseEvent(SimpleMapper.Map(command, new AssetCreated())); var @event = SimpleMapper.Map(command, new AssetCreated
{
FileName = command.File.FileName,
FileSize = command.File.FileSize,
MimeType = command.File.MimeType,
PixelWidth = command.ImageInfo?.PixelWidth,
PixelHeight = command.ImageInfo?.PixelHeight,
IsImage = command.ImageInfo != null
});
RaiseEvent(@event);
return this; return this;
} }
public AssetDomainObject Delete(DeleteAsset command) public AssetDomainObject Update(UpdateAsset command)
{ {
Guard.NotNull(command, nameof(command)); Guard.NotNull(command, nameof(command));
VerifyCreatedAndNotDeleted(); VerifyCreatedAndNotDeleted();
RaiseEvent(SimpleMapper.Map(command, new AssetDeleted())); var @event = SimpleMapper.Map(command, new AssetUpdated
{
FileSize = command.File.FileSize,
MimeType = command.File.MimeType,
PixelWidth = command.ImageInfo?.PixelWidth,
PixelHeight = command.ImageInfo?.PixelHeight,
IsImage = command.ImageInfo != null
});
RaiseEvent(@event);
return this; return this;
} }
public AssetDomainObject Update(UpdateAsset command) public AssetDomainObject Delete(DeleteAsset command)
{ {
Guard.NotNull(command, nameof(command)); Guard.NotNull(command, nameof(command));
VerifyCreatedAndNotDeleted(); VerifyCreatedAndNotDeleted();
RaiseEvent(SimpleMapper.Map(command, new AssetUpdated())); RaiseEvent(SimpleMapper.Map(command, new AssetDeleted()));
return this; return this;
} }

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

@ -6,20 +6,14 @@
// All rights reserved. // All rights reserved.
// ========================================================================== // ==========================================================================
using Squidex.Infrastructure.Assets;
namespace Squidex.Write.Assets.Commands namespace Squidex.Write.Assets.Commands
{ {
public sealed class CreateAsset : AssetAggregateCommand public sealed class CreateAsset : AssetAggregateCommand
{ {
public string FileName { get; set; } public AssetFile File { get; set; }
public string MimeType { get; set; }
public long FileSize { get; set; }
public bool IsImage { get; set; }
public int? PixelWidth { get; set; }
public int? PixelHeight { get; set; } public ImageInfo ImageInfo { get; set; }
} }
} }

12
src/Squidex.Write/Assets/Commands/UpdateAsset.cs

@ -6,18 +6,14 @@
// All rights reserved. // All rights reserved.
// ========================================================================== // ==========================================================================
using Squidex.Infrastructure.Assets;
namespace Squidex.Write.Assets.Commands namespace Squidex.Write.Assets.Commands
{ {
public class UpdateAsset : AssetAggregateCommand public class UpdateAsset : AssetAggregateCommand
{ {
public string MimeType { get; set; } public AssetFile File { get; set; }
public long FileSize { get; set; }
public bool IsImage { get; set; }
public int? PixelWidth { get; set; }
public int? PixelHeight { get; set; } public ImageInfo ImageInfo { get; set; }
} }
} }

27
src/Squidex/Controllers/Api/Assets/AssetContentController.cs

@ -20,12 +20,9 @@ using Squidex.Read.Assets.Repositories;
namespace Squidex.Controllers.Api.Assets namespace Squidex.Controllers.Api.Assets
{ {
/// <summary>
/// Uploads and retrieves assets.
/// </summary>
[ApiExceptionFilter] [ApiExceptionFilter]
[ServiceFilter(typeof(AppFilterAttribute))] [ServiceFilter(typeof(AppFilterAttribute))]
[SwaggerTag("Assets")] [SwaggerIgnore]
public class AssetContentController : ControllerBase public class AssetContentController : ControllerBase
{ {
private readonly IAssetStore assetStorage; private readonly IAssetStore assetStorage;
@ -44,18 +41,6 @@ namespace Squidex.Controllers.Api.Assets
this.assetThumbnailGenerator = assetThumbnailGenerator; this.assetThumbnailGenerator = assetThumbnailGenerator;
} }
/// <summary>
/// Gets the content of the asset.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="id">The id of the asset.</param>
/// <param name="mode">The resize mode.</param>
/// <param name="width">The target width of the image.</param>
/// <param name="height">The target width of the image.</param>
/// <returns>
/// 200 => Asset content returned.
/// 404 => App or Asset not found.
/// </returns>
[HttpGet] [HttpGet]
[Route("assets/{id}/")] [Route("assets/{id}/")]
public async Task<IActionResult> GetAssetContent(string app, Guid id, [FromQuery] int? width = null, [FromQuery] int? height = null, [FromQuery] string mode = null) public async Task<IActionResult> GetAssetContent(string app, Guid id, [FromQuery] int? width = null, [FromQuery] int? height = null, [FromQuery] string mode = null)
@ -71,13 +56,13 @@ namespace Squidex.Controllers.Api.Assets
if (asset.IsImage && (width.HasValue || height.HasValue)) if (asset.IsImage && (width.HasValue || height.HasValue))
{ {
var name = $"{asset.Id}_{asset.Version}_{width}_{height}_{mode}"; var suffix = $"{width}_{height}_{mode}";
content = await assetStorage.GetAssetAsync(name); content = await assetStorage.GetAssetAsync(asset.Id, asset.Version, suffix);
if (content == null) if (content == null)
{ {
var fullSizeContent = await assetStorage.GetAssetAsync($"{asset.Id}_{asset.Version}"); var fullSizeContent = await assetStorage.GetAssetAsync(asset.Id, asset.Version);
if (fullSizeContent == null) if (fullSizeContent == null)
{ {
@ -86,14 +71,14 @@ namespace Squidex.Controllers.Api.Assets
content = await assetThumbnailGenerator.CreateThumbnailAsync(fullSizeContent, width, height, mode); content = await assetThumbnailGenerator.CreateThumbnailAsync(fullSizeContent, width, height, mode);
await assetStorage.UploadAssetAsync(name, content); await assetStorage.UploadAssetAsync(asset.Id, asset.Version, content, suffix);
content.Position = 0; content.Position = 0;
} }
} }
else else
{ {
content = await assetStorage.GetAssetAsync($"{asset.Id}_{asset.Version}"); content = await assetStorage.GetAssetAsync(asset.Id, asset.Version);
} }
if (content == null) if (content == null)

131
src/Squidex/Controllers/Api/Assets/AssetController.cs

@ -8,7 +8,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@ -26,8 +25,6 @@ using Squidex.Pipeline;
using Squidex.Read.Assets.Repositories; using Squidex.Read.Assets.Repositories;
using Squidex.Write.Assets.Commands; using Squidex.Write.Assets.Commands;
#pragma warning disable 1573
namespace Squidex.Controllers.Api.Assets namespace Squidex.Controllers.Api.Assets
{ {
/// <summary> /// <summary>
@ -39,28 +36,26 @@ namespace Squidex.Controllers.Api.Assets
[SwaggerTag("Assets")] [SwaggerTag("Assets")]
public class AssetController : ControllerBase public class AssetController : ControllerBase
{ {
private readonly IAssetStore assetStorage;
private readonly IAssetRepository assetRepository; private readonly IAssetRepository assetRepository;
private readonly IAssetThumbnailGenerator assetThumbnailGenerator;
private readonly AssetConfig assetsConfig; private readonly AssetConfig assetsConfig;
public AssetController( public AssetController(
ICommandBus commandBus, ICommandBus commandBus,
IAssetStore assetStorage,
IAssetRepository assetRepository, IAssetRepository assetRepository,
IAssetThumbnailGenerator assetThumbnailGenerator,
IOptions<AssetConfig> assetsConfig) IOptions<AssetConfig> assetsConfig)
: base(commandBus) : base(commandBus)
{ {
this.assetStorage = assetStorage;
this.assetsConfig = assetsConfig.Value; this.assetsConfig = assetsConfig.Value;
this.assetRepository = assetRepository; this.assetRepository = assetRepository;
this.assetThumbnailGenerator = assetThumbnailGenerator;
} }
/// <summary> /// <summary>
/// Get assets. /// Get assets.
/// </summary> /// </summary>
/// <param name="skip">The number of assets to skip.</param>
/// <param name="take">The number of assets to take.</param>
/// <param name="query">The query to limit the files by name.</param>
/// <param name="mimeTypes">Comma separated list of mime types to get.</param>
/// <returns> /// <returns>
/// 200 => assets returned. /// 200 => assets returned.
/// </returns> /// </returns>
@ -96,7 +91,8 @@ namespace Squidex.Controllers.Api.Assets
/// <summary> /// <summary>
/// Creates and uploads a new asset. /// Creates and uploads a new asset.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The app where the asset is a part of.</param>
/// <param name="file">The file to upload.</param>
/// <returns> /// <returns>
/// 201 => Asset created. /// 201 => Asset created.
/// 404 => App not found. /// 404 => App not found.
@ -106,53 +102,104 @@ namespace Squidex.Controllers.Api.Assets
[Route("apps/{app}/assets/")] [Route("apps/{app}/assets/")]
[ProducesResponseType(typeof(AssetDto), 201)] [ProducesResponseType(typeof(AssetDto), 201)]
[ProducesResponseType(typeof(ErrorDto), 400)] [ProducesResponseType(typeof(ErrorDto), 400)]
public async Task<IActionResult> PostAsset(string app, List<IFormFile> files) public async Task<IActionResult> PostAsset(string app, List<IFormFile> file)
{ {
if (files.Count != 1) var assetFile = GetAssetFile(file);
{
var error = new ValidationError($"Can only upload one file, found ${files.Count}.");
throw new ValidationException("Cannot create asset.", error); var command = new CreateAsset { File = assetFile };
} var context = await CommandBus.PublishAsync(command);
var file = files[0]; var result = context.Result<EntityCreatedResult<Guid>>();
var response = AssetDto.Create(command, result);
if (file.Length > assetsConfig.MaxSize) return StatusCode(201, response);
{ }
var error = new ValidationError($"File size cannot be longer than ${assetsConfig.MaxSize}.");
throw new ValidationException("Cannot create asset.", error); /// <summary>
} /// Replaces the content of the asset with a newer version.
/// </summary>
/// <param name="app">The app where the asset is a part of.</param>
/// <param name="id">The id of the asset.</param>
/// <param name="file">The file to upload.</param>
/// <returns>
/// 201 => Asset updated.
/// 404 => App or Asset not found.
/// 400 => Asset exceeds the maximum size.
/// </returns>
[HttpPut]
[Route("apps/{app}/assets/{id}/content")]
[ProducesResponseType(typeof(AssetDto), 201)]
[ProducesResponseType(typeof(ErrorDto), 400)]
public async Task<IActionResult> PutAssetContent(string app, Guid id, List<IFormFile> file)
{
var assetFile = GetAssetFile(file);
await CommandBus.PublishAsync(new UpdateAsset { File = assetFile });
var fileContent = new MemoryStream(); return NoContent();
}
await file.OpenReadStream().CopyToAsync(fileContent); /// <summary>
/// Updates the asset.
/// </summary>
/// <param name="app">The app where the asset is a part of.</param>
/// <param name="id">The id of the asset.</param>
/// <param name="request">The asset object that needs to updated.</param>
/// <returns>
/// 201 => Asset updated.
/// 404 => App or Asset not found.
/// </returns>
[HttpPost]
[Route("apps/{app}/assets/{id}/content")]
[ProducesResponseType(typeof(AssetDto), 201)]
[ProducesResponseType(typeof(ErrorDto), 400)]
public async Task<IActionResult> PutAsset(string app, Guid id, [FromBody] AssetUpdateDto request)
{
var command = SimpleMapper.Map(request, new RenameAsset());
await CommandBus.PublishAsync(command);
fileContent.Position = 0; return NoContent();
}
/// <summary>
/// Delete an asset.
/// </summary>
/// <param name="app">The app where the schema is a part of.</param>
/// <param name="id">The id of the asset to delete.</param>
/// <returns>
/// 204 => Asset has been deleted.
/// </returns>
[HttpDelete]
[Route("apps/{app}/schemas/{name}/")]
public async Task<IActionResult> DeleteSchema(string app, Guid id)
{
await CommandBus.PublishAsync(new DeleteAsset { AssetId = id });
var imageInfo = await assetThumbnailGenerator.GetImageInfoAsync(fileContent); return NoContent();
}
var command = new CreateAsset private AssetFile GetAssetFile(IReadOnlyList<IFormFile> file)
{
if (file.Count != 1)
{ {
AssetId = Guid.NewGuid(), var error = new ValidationError($"Can only upload one file, found ${file.Count}.");
FileSize = file.Length,
FileName = file.FileName,
MimeType = file.ContentType,
IsImage = imageInfo != null,
PixelWidth = imageInfo?.PixelWidth,
PixelHeight = imageInfo?.PixelHeight
};
fileContent.Position = 0;
await assetStorage.UploadAssetAsync($"{command.AssetId}_0", fileContent); throw new ValidationException("Cannot create asset.", error);
}
var context = await CommandBus.PublishAsync(command); var formFile = file[0];
var result = context.Result<EntityCreatedResult<Guid>>(); if (formFile.Length > assetsConfig.MaxSize)
var response = AssetDto.Create(command, result); {
var error = new ValidationError($"File size cannot be longer than ${assetsConfig.MaxSize}.");
return StatusCode(201, response); throw new ValidationException("Cannot create asset.", error);
}
var assetFile = new AssetFile(formFile.FileName, formFile.ContentType, formFile.Length, formFile.OpenReadStream);
return assetFile;
} }
} }
} }

12
src/Squidex/Controllers/Api/Assets/Models/AssetDto.cs

@ -93,12 +93,12 @@ namespace Squidex.Controllers.Api.Assets.Models
CreatedBy = command.Actor, CreatedBy = command.Actor,
LastModified = now, LastModified = now,
LastModifiedBy = command.Actor, LastModifiedBy = command.Actor,
FileName = command.FileName, FileName = command.File.FileName,
FileSize = command.FileSize, FileSize = command.File.FileSize,
MimeType = command.MimeType, MimeType = command.File.MimeType,
IsImage = command.IsImage, IsImage = command.ImageInfo != null,
PixelWidth = command.PixelWidth, PixelWidth = command.ImageInfo?.PixelWidth,
PixelHeight = command.PixelHeight PixelHeight = command.ImageInfo?.PixelHeight
}; };
return response; return response;

21
src/Squidex/Controllers/Api/Assets/Models/AssetUpdateDto.cs

@ -0,0 +1,21 @@
// ==========================================================================
// AssetUpdateDto.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.ComponentModel.DataAnnotations;
namespace Squidex.Controllers.Api.Assets.Models
{
public class AssetUpdateDto
{
/// <summary>
/// The new name of the asset.
/// </summary>
[Required]
public string Name { get; set; }
}
}

1
src/Squidex/Squidex.csproj

@ -64,6 +64,7 @@
<PackageReference Include="ReportGenerator" Version="2.5.6" /> <PackageReference Include="ReportGenerator" Version="2.5.6" />
<PackageReference Include="StackExchange.Redis.StrongName" Version="1.2.1" /> <PackageReference Include="StackExchange.Redis.StrongName" Version="1.2.1" />
<PackageReference Include="System.Linq" Version="4.3.0" /> <PackageReference Include="System.Linq" Version="4.3.0" />
<PackageReference Include="System.ValueTuple" Version="4.3.0" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'netcoreapp1.1' "> <ItemGroup Condition=" '$(TargetFramework)' == 'netcoreapp1.1' ">

14
src/Squidex/app/features/assets/pages/asset.component.html

@ -1,12 +1,15 @@
<div class="asset"> <div class="asset">
<div class="card"> <div class="card" (sqxFileDrop)="updateFile($event)">
<div class="card-block"> <div class="card-block">
<div *ngIf="asset"> <div *ngIf="asset" [@fade]>
<div class="file-type">{{fileType}}</div> <div class="file-type">{{fileType}}</div>
<div *ngIf="asset.isImage" [@fade]> <div *ngIf="asset.isImage">
<img [attr.src]="previewUrl | async" sqxHideInvalidImage> <img [attr.src]="previewUrl | async" sqxHideInvalidImage>
</div> </div>
<div *ngIf="!asset.isImage" class="file-icon-container">
<img [attr.src]="fileIcon" sqxHideInvalidImage>
</div>
</div> </div>
</div> </div>
<div class="card-footer"> <div class="card-footer">
@ -17,5 +20,10 @@
{{fileInfo}} {{fileInfo}}
</div> </div>
</div> </div>
<div class="drop-overlay">
<div class="drop-overlay-background"></div>
<span class="drop-overlay-text">Drop to update</span>
</div>
</div> </div>
</div> </div>

41
src/Squidex/app/features/assets/pages/asset.component.scss

@ -10,11 +10,42 @@ $color-type-foreground: #fff;
padding-bottom: 1rem; padding-bottom: 1rem;
} }
.drop-overlay {
& {
@include transition(opacity.4s ease);
@include absolute(0, 0, 0, 0);
@include opacity(0);
pointer-events: none;
}
&-text {
@include absolute(0, 0, 0, 0);
text-align: center;
line-height: 14rem;
font-size: 1.2rem;
font-weight: lighter;
color: $color-type-foreground;
}
&-background {
@include absolute(0, 0, 0, 0);
@include opacity(.7);
background: $color-type-background;
}
}
.card { .card {
& { & {
position: relative;
height: $card-size; height: $card-size;
} }
&.drag {
.drop-overlay {
@include opacity(1);
}
}
&-block { &-block {
padding: .8rem .8rem 0; padding: .8rem .8rem 0;
position: relative; position: relative;
@ -33,6 +64,13 @@ $color-type-foreground: #fff;
font-size: .8rem; font-size: .8rem;
} }
&-icon {
&-container {
background: $color-border;
height: 155px;
}
}
&-name { &-name {
@include truncate; @include truncate;
font-size: 1rem; font-size: 1rem;
@ -47,7 +85,8 @@ $color-type-foreground: #fff;
background: $color-type-background; background: $color-type-background;
border: 0; border: 0;
padding: .1rem .3rem; padding: .1rem .3rem;
font-size: .8rem; text-transform: uppercase;
font-size: .7rem;
font-weight: normal; font-weight: normal;
color: $color-type-foreground; color: $color-type-foreground;
} }

51
src/Squidex/app/features/assets/pages/asset.component.ts

@ -48,6 +48,16 @@ export class AssetComponent extends AppComponentBase implements OnInit {
return result; return result;
} }
public get fileIcon(): string {
let result = '';
if (this.asset != null) {
result = fileIcon(this.fileType);
}
return result;
}
public get fileName(): string { public get fileName(): string {
let result = ''; let result = '';
@ -108,4 +118,45 @@ function fileSize(b: number) {
} }
return (u ? b.toFixed(1) + ' ' : b) + ' kMGTPEZY'[u] + 'B'; return (u ? b.toFixed(1) + ' ' : b) + ' kMGTPEZY'[u] + 'B';
}
const mimeMapping = {
'pdf': 'pdf',
'vnd.openxmlformats-officedocument.wordprocessingml.document': 'docx',
'vnd.openxmlformats-officedocument.wordprocessingml.template': 'docx',
'vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'xlsx',
'vnd.openxmlformats-officedocument.spreadsheetml.template': 'xlsx',
'vnd.openxmlformats-officedocument.presentationml.presentation': 'pptx',
'vnd.openxmlformats-officedocument.presentationml.template': 'pptx',
'vnd.openxmlformats-officedocument.presentationml.slideshow': 'pptx',
'msword': 'doc',
'vnd.ms-word': 'doc',
'vnd.ms-word.document.macroEnabled.12': 'docx',
'vnd.ms-word.template.macroEnabled.12': 'docx',
'vnd.ms-excel': 'xls',
'vnd.ms-excel.sheet.macroEnabled.12': 'xlsx',
'vnd.ms-excel.template.macroEnabled.12': 'xlsx',
'vnd.ms-excel.addin.macroEnabled.12': 'xlsx',
'vnd.ms-excel.sheet.binary.macroEnabled.12': 'xlsx',
'vnd.ms-powerpoint': 'ppt',
'vnd.ms-powerpoint.addin.macroEnabled.12': 'pptx',
'vnd.ms-powerpoint.presentation.macroEnabled.12': 'pptx',
'vnd.ms-powerpoint.template.macroEnabled.12': 'pptx',
'vnd.ms-powerpoint.slideshow.macroEnabled.12': 'pptx'
};
function fileIcon(mimeType: string) {
const mimeParts = mimeType.split('/');
const mimePrefix = mimeParts[0].toLowerCase();
const mimeSuffix = mimeParts[1].toLowerCase();
let mimeIcon = 'generic';
if (mimePrefix === 'video') {
mimeIcon = 'video';
} else {
mimeIcon = mimeMapping[mimeSuffix] || 'generic';;
}
return `/images/asset_${mimeIcon}.png`;
} }

2
src/Squidex/app/shared/services/assets.service.ts

@ -116,7 +116,7 @@ export class AssetsService {
'Authorization': `${this.authService.user.user.token_type} ${this.authService.user.user.access_token}` 'Authorization': `${this.authService.user.user.token_type} ${this.authService.user.user.access_token}`
}); });
content.append('files', file); content.append('file', file);
this.http this.http
.post(url, content, { headers }) .post(url, content, { headers })

BIN
src/Squidex/wwwroot/images/asset_doc.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 911 B

BIN
src/Squidex/wwwroot/images/asset_docx.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 958 B

BIN
src/Squidex/wwwroot/images/asset_generic.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 514 B

BIN
src/Squidex/wwwroot/images/asset_pdf.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
src/Squidex/wwwroot/images/asset_ppt.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
src/Squidex/wwwroot/images/asset_pptx.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
src/Squidex/wwwroot/images/asset_video.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 723 B

BIN
src/Squidex/wwwroot/images/asset_xls.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

BIN
src/Squidex/wwwroot/images/asset_xlsx.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 905 B

6
tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs

@ -39,7 +39,7 @@ namespace Squidex.Write.Apps
public AppCommandHandlerTests() public AppCommandHandlerTests()
{ {
app = new AppDomainObject(AppId, 0); app = new AppDomainObject(AppId, -1);
sut = new AppCommandHandler(Handler, appRepository.Object, userRepository.Object, keyGenerator.Object); sut = new AppCommandHandler(Handler, appRepository.Object, userRepository.Object, keyGenerator.Object);
} }
@ -47,7 +47,7 @@ namespace Squidex.Write.Apps
[Fact] [Fact]
public async Task Create_should_throw_if_a_name_with_same_name_already_exists() public async Task Create_should_throw_if_a_name_with_same_name_already_exists()
{ {
var context = CreateContextForCommand(new CreateApp { Name = AppName, AggregateId = AppId }); var context = CreateContextForCommand(new CreateApp { Name = AppName, AppId = AppId });
appRepository.Setup(x => x.FindAppAsync(AppName)) appRepository.Setup(x => x.FindAppAsync(AppName))
.Returns(Task.FromResult(new Mock<IAppEntity>().Object)) .Returns(Task.FromResult(new Mock<IAppEntity>().Object))
@ -64,7 +64,7 @@ namespace Squidex.Write.Apps
[Fact] [Fact]
public async Task Create_should_create_app_if_name_is_free() public async Task Create_should_create_app_if_name_is_free()
{ {
var context = CreateContextForCommand(new CreateApp { Name = AppName, AggregateId = AppId }); var context = CreateContextForCommand(new CreateApp { Name = AppName, AppId = AppId });
appRepository.Setup(x => x.FindAppAsync(AppName)) appRepository.Setup(x => x.FindAppAsync(AppName))
.Returns(Task.FromResult<IAppEntity>(null)) .Returns(Task.FromResult<IAppEntity>(null))

2
tests/Squidex.Write.Tests/Apps/AppDomainObjectTests.cs

@ -56,7 +56,7 @@ namespace Squidex.Write.Apps
[Fact] [Fact]
public void Create_should_specify_name_and_owner() public void Create_should_specify_name_and_owner()
{ {
sut.Create(CreateCommand(new CreateApp { Name = AppName, Actor = User, AggregateId = AppId })); sut.Create(CreateCommand(new CreateApp { Name = AppName, Actor = User, AppId = AppId }));
Assert.Equal(AppName, sut.Name); Assert.Equal(AppName, sut.Name);

36
tests/Squidex.Write.Tests/Assets/AssetCommandHandlerTests.cs

@ -7,11 +7,15 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Write.Assets.Commands; using Squidex.Write.Assets.Commands;
using Squidex.Write.TestHelpers; using Squidex.Write.TestHelpers;
using Xunit; using Xunit;
using Moq;
using Squidex.Infrastructure.Tasks;
// ReSharper disable ConvertToConstant.Local // ReSharper disable ConvertToConstant.Local
@ -19,24 +23,31 @@ namespace Squidex.Write.Assets
{ {
public class AssetCommandHandlerTests : HandlerTestBase<AssetDomainObject> public class AssetCommandHandlerTests : HandlerTestBase<AssetDomainObject>
{ {
private readonly Mock<IAssetThumbnailGenerator> assetThumbnailGenerator = new Mock<IAssetThumbnailGenerator>();
private readonly Mock<IAssetStore> assetStore = new Mock<IAssetStore>();
private readonly AssetCommandHandler sut; private readonly AssetCommandHandler sut;
private readonly AssetDomainObject asset; private readonly AssetDomainObject asset;
private readonly Guid assetId = Guid.NewGuid(); private readonly Guid assetId = Guid.NewGuid();
private readonly string fileName = "my-image.png"; private readonly Stream stream = new MemoryStream();
private readonly string mimeType = "image/png"; private readonly ImageInfo image = new ImageInfo(2048, 2048);
private readonly long fileSize = 1024; private readonly AssetFile file;
public AssetCommandHandlerTests() public AssetCommandHandlerTests()
{ {
asset = new AssetDomainObject(assetId, 0); file = new AssetFile("my-image.png", "image/png", 1024, () => stream);
sut = new AssetCommandHandler(Handler); asset = new AssetDomainObject(assetId, -1);
sut = new AssetCommandHandler(Handler, assetStore.Object, assetThumbnailGenerator.Object);
} }
[Fact] [Fact]
public async Task Create_should_create_asset() public async Task Create_should_create_asset()
{ {
var context = CreateContextForCommand(new CreateAsset { AssetId = assetId, FileName = fileName, FileSize = fileSize, MimeType = mimeType }); assetStore.Setup(x => x.UploadAssetAsync(assetId, 0, stream, null)).Returns(TaskHelper.Done).Verifiable();
assetThumbnailGenerator.Setup(x => x.GetImageInfoAsync(stream)).Returns(Task.FromResult(image)).Verifiable();
var context = CreateContextForCommand(new CreateAsset { AssetId = assetId, File = file });
await TestCreate(asset, async _ => await TestCreate(asset, async _ =>
{ {
@ -44,19 +55,28 @@ namespace Squidex.Write.Assets
}); });
Assert.Equal(assetId, context.Result<EntityCreatedResult<Guid>>().IdOrValue); Assert.Equal(assetId, context.Result<EntityCreatedResult<Guid>>().IdOrValue);
assetStore.VerifyAll();
assetThumbnailGenerator.VerifyAll();
} }
[Fact] [Fact]
public async Task Update_should_update_domain_object() public async Task Update_should_update_domain_object()
{ {
assetStore.Setup(x => x.UploadAssetAsync(assetId, 1, stream, null)).Returns(TaskHelper.Done).Verifiable();
assetThumbnailGenerator.Setup(x => x.GetImageInfoAsync(stream)).Returns(Task.FromResult(image)).Verifiable();
CreateAsset(); CreateAsset();
var context = CreateContextForCommand(new UpdateAsset { AssetId = assetId, FileSize = fileSize, MimeType = mimeType }); var context = CreateContextForCommand(new UpdateAsset { AssetId = assetId, File = file });
await TestUpdate(asset, async _ => await TestUpdate(asset, async _ =>
{ {
await sut.HandleAsync(context); await sut.HandleAsync(context);
}); });
assetStore.VerifyAll();
assetThumbnailGenerator.VerifyAll();
} }
[Fact] [Fact]
@ -87,7 +107,7 @@ namespace Squidex.Write.Assets
private void CreateAsset() private void CreateAsset()
{ {
asset.Create(new CreateAsset { FileName = fileName, FileSize = fileSize, MimeType = mimeType }); asset.Create(new CreateAsset { File = file });
} }
} }
} }

42
tests/Squidex.Write.Tests/Assets/AssetDomainObjectTests.cs

@ -7,8 +7,10 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.IO;
using Squidex.Events.Assets; using Squidex.Events.Assets;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.CQRS; using Squidex.Infrastructure.CQRS;
using Squidex.Write.Assets.Commands; using Squidex.Write.Assets.Commands;
using Squidex.Write.TestHelpers; using Squidex.Write.TestHelpers;
@ -21,9 +23,8 @@ namespace Squidex.Write.Assets
public class AssetDomainObjectTests : HandlerTestBase<AssetDomainObject> public class AssetDomainObjectTests : HandlerTestBase<AssetDomainObject>
{ {
private readonly AssetDomainObject sut; private readonly AssetDomainObject sut;
private readonly string fileName = "my-image.png"; private readonly ImageInfo image = new ImageInfo(2048, 2048);
private readonly string mimeType = "image/png"; private readonly AssetFile file = new AssetFile("my-image.png", "image/png", 1024, () => new MemoryStream());
private readonly long fileSize = 1024;
public Guid AssetId { get; } = Guid.NewGuid(); public Guid AssetId { get; } = Guid.NewGuid();
@ -35,22 +36,30 @@ namespace Squidex.Write.Assets
[Fact] [Fact]
public void Create_should_throw_if_created() public void Create_should_throw_if_created()
{ {
sut.Create(new CreateAsset { FileName = fileName, FileSize = fileSize, MimeType = mimeType }); sut.Create(new CreateAsset { File = file });
Assert.Throws<DomainException>(() => Assert.Throws<DomainException>(() =>
{ {
sut.Create(CreateAssetCommand(new CreateAsset { FileName = fileName, FileSize = fileSize, MimeType = mimeType })); sut.Create(CreateAssetCommand(new CreateAsset { File = file }));
}); });
} }
[Fact] [Fact]
public void Create_should_create_events() public void Create_should_create_events()
{ {
sut.Create(CreateAssetCommand(new CreateAsset { FileName = fileName, FileSize = fileSize, MimeType = mimeType })); sut.Create(CreateAssetCommand(new CreateAsset { File = file, ImageInfo = image }));
sut.GetUncomittedEvents() sut.GetUncomittedEvents()
.ShouldHaveSameEvents( .ShouldHaveSameEvents(
CreateAssetEvent(new AssetCreated { FileName = fileName, FileSize = fileSize, MimeType = mimeType }) CreateAssetEvent(new AssetCreated
{
IsImage = true,
FileName = file.FileName,
FileSize = file.FileSize,
MimeType = file.MimeType,
PixelWidth = image.PixelWidth,
PixelHeight = image.PixelHeight
})
); );
} }
@ -59,7 +68,7 @@ namespace Squidex.Write.Assets
{ {
Assert.Throws<DomainException>(() => Assert.Throws<DomainException>(() =>
{ {
sut.Update(CreateAssetCommand(new UpdateAsset { FileSize = fileSize, MimeType = mimeType })); sut.Update(CreateAssetCommand(new UpdateAsset { File = file }));
}); });
} }
@ -80,11 +89,18 @@ namespace Squidex.Write.Assets
{ {
CreateAsset(); CreateAsset();
sut.Update(CreateAssetCommand(new UpdateAsset { FileSize = fileSize, MimeType = mimeType })); sut.Update(CreateAssetCommand(new UpdateAsset { File = file, ImageInfo = image }));
sut.GetUncomittedEvents() sut.GetUncomittedEvents()
.ShouldHaveSameEvents( .ShouldHaveSameEvents(
CreateAssetEvent(new AssetUpdated { FileSize = fileSize, MimeType = mimeType }) CreateAssetEvent(new AssetUpdated
{
IsImage = true,
FileSize = file.FileSize,
MimeType = file.MimeType,
PixelWidth = image.PixelWidth,
PixelHeight = image.PixelHeight
})
); );
} }
@ -93,7 +109,7 @@ namespace Squidex.Write.Assets
{ {
Assert.Throws<DomainException>(() => Assert.Throws<DomainException>(() =>
{ {
sut.Update(CreateAssetCommand(new UpdateAsset { FileSize = fileSize, MimeType = mimeType })); sut.Rename(CreateAssetCommand(new RenameAsset { FileName = "new-file.png" }));
}); });
} }
@ -127,7 +143,7 @@ namespace Squidex.Write.Assets
Assert.Throws<ValidationException>(() => Assert.Throws<ValidationException>(() =>
{ {
sut.Rename(CreateAssetCommand(new RenameAsset { FileName = fileName })); sut.Rename(CreateAssetCommand(new RenameAsset { FileName = file.FileName }));
}); });
} }
@ -182,7 +198,7 @@ namespace Squidex.Write.Assets
private void CreateAsset() private void CreateAsset()
{ {
sut.Create(CreateAssetCommand(new CreateAsset { FileName = fileName, FileSize = fileSize, MimeType = mimeType })); sut.Create(CreateAssetCommand(new CreateAsset { File = file }));
((IAggregate)sut).ClearUncommittedEvents(); ((IAggregate)sut).ClearUncommittedEvents();
} }

2
tests/Squidex.Write.Tests/Contents/ContentCommandHandlerTests.cs

@ -43,7 +43,7 @@ namespace Squidex.Write.Contents
.AddOrUpdateField(new NumberField(1, "my-field", .AddOrUpdateField(new NumberField(1, "my-field",
new NumberFieldProperties { IsRequired = true })); new NumberFieldProperties { IsRequired = true }));
content = new ContentDomainObject(contentId, 0); content = new ContentDomainObject(contentId, -1);
sut = new ContentCommandHandler(Handler, appProvider.Object, schemaProvider.Object); sut = new ContentCommandHandler(Handler, appProvider.Object, schemaProvider.Object);

2
tests/Squidex.Write.Tests/Schemas/SchemaCommandHandlerTests.cs

@ -32,7 +32,7 @@ namespace Squidex.Write.Schemas
public SchemaCommandHandlerTests() public SchemaCommandHandlerTests()
{ {
schema = new SchemaDomainObject(SchemaId, 0, registry); schema = new SchemaDomainObject(SchemaId, -1, registry);
sut = new SchemaCommandHandler(Handler, schemaProvider.Object); sut = new SchemaCommandHandler(Handler, schemaProvider.Object);
} }

Loading…
Cancel
Save