diff --git a/src/Squidex.Infrastructure/Assets/AssetFile.cs b/src/Squidex.Infrastructure/Assets/AssetFile.cs new file mode 100644 index 000000000..1c48a4f3d --- /dev/null +++ b/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 openAction; + + public string FileName { get; } + + public string MimeType { get; } + + public long FileSize { get; } + + public AssetFile(string fileName, string mimeType, long fileSize, Func 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(); + } + } +} diff --git a/src/Squidex.Infrastructure/Assets/FolderAssetStore.cs b/src/Squidex.Infrastructure/Assets/FolderAssetStore.cs index 1916cfc37..90e644c04 100644 --- a/src/Squidex.Infrastructure/Assets/FolderAssetStore.cs +++ b/src/Squidex.Infrastructure/Assets/FolderAssetStore.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using System; using System.IO; using System.Threading.Tasks; using Squidex.Infrastructure.Log; @@ -49,9 +50,9 @@ namespace Squidex.Infrastructure.Assets } } - public Task GetAssetAsync(string name) + public Task GetAssetAsync(Guid id, long version, string suffix = null) { - var file = GetFile(name); + var file = GetFile(id, version, suffix); Stream stream = null; @@ -70,9 +71,9 @@ namespace Squidex.Infrastructure.Assets 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()) { @@ -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); } } } diff --git a/src/Squidex.Infrastructure/Assets/IAssetStore.cs b/src/Squidex.Infrastructure/Assets/IAssetStore.cs index bf296eacc..e0926ad01 100644 --- a/src/Squidex.Infrastructure/Assets/IAssetStore.cs +++ b/src/Squidex.Infrastructure/Assets/IAssetStore.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using System; using System.IO; using System.Threading.Tasks; @@ -13,8 +14,8 @@ namespace Squidex.Infrastructure.Assets { public interface IAssetStore { - Task GetAssetAsync(string name); + Task 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); } } \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Assets/ImageInfo.cs b/src/Squidex.Infrastructure/Assets/ImageInfo.cs index 36bd06b53..6c30fb2d7 100644 --- a/src/Squidex.Infrastructure/Assets/ImageInfo.cs +++ b/src/Squidex.Infrastructure/Assets/ImageInfo.cs @@ -3,7 +3,7 @@ // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group -// All rights reserved. +// All rights reserved.d // ========================================================================== namespace Squidex.Infrastructure.Assets @@ -16,6 +16,9 @@ namespace Squidex.Infrastructure.Assets public ImageInfo(int pixelWidth, int pixelHeight) { + Guard.GreaterThan(pixelWidth, 0, nameof(pixelWidth)); + Guard.GreaterThan(pixelHeight, 0, nameof(pixelHeight)); + PixelWidth = pixelWidth; PixelHeight = pixelHeight; } diff --git a/src/Squidex.Read.MongoDb/Assets/MongoAssetRepository.cs b/src/Squidex.Read.MongoDb/Assets/MongoAssetRepository.cs index 9be371dbe..1d7ddc2bd 100644 --- a/src/Squidex.Read.MongoDb/Assets/MongoAssetRepository.cs +++ b/src/Squidex.Read.MongoDb/Assets/MongoAssetRepository.cs @@ -31,6 +31,11 @@ namespace Squidex.Read.MongoDb.Assets return "Projections_Assets"; } + protected override Task SetupCollectionAsync(IMongoCollection 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> QueryAsync(Guid appId, HashSet mimeTypes = null, string query = null, int take = 10, int skip = 0) { var filter = CreateFilter(appId, mimeTypes, query); diff --git a/src/Squidex.Write/Apps/AppDomainObject.cs b/src/Squidex.Write/Apps/AppDomainObject.cs index 52dc6ea60..35fbbdbe0 100644 --- a/src/Squidex.Write/Apps/AppDomainObject.cs +++ b/src/Squidex.Write/Apps/AppDomainObject.cs @@ -101,7 +101,7 @@ namespace Squidex.Write.Apps ThrowIfCreated(); - var appId = new NamedId(command.AggregateId, command.Name); + var appId = new NamedId(command.AppId, command.Name); RaiseEvent(SimpleMapper.Map(command, new AppCreated { AppId = appId })); diff --git a/src/Squidex.Write/Apps/Commands/CreateApp.cs b/src/Squidex.Write/Apps/Commands/CreateApp.cs index ae3bb8a5d..7d8e4dfc9 100644 --- a/src/Squidex.Write/Apps/Commands/CreateApp.cs +++ b/src/Squidex.Write/Apps/Commands/CreateApp.cs @@ -17,11 +17,16 @@ namespace Squidex.Write.Apps.Commands { public string Name { get; set; } - public Guid AggregateId { get; set; } + public Guid AppId { get; set; } + + Guid IAggregateCommand.AggregateId + { + get { return AppId; } + } public CreateApp() { - AggregateId = Guid.NewGuid(); + AppId = Guid.NewGuid(); } public void Validate(IList errors) diff --git a/src/Squidex.Write/Assets/AssetCommandHandler.cs b/src/Squidex.Write/Assets/AssetCommandHandler.cs index 60d1aa496..90d5876ff 100644 --- a/src/Squidex.Write/Assets/AssetCommandHandler.cs +++ b/src/Squidex.Write/Assets/AssetCommandHandler.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Squidex.Infrastructure; +using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.Dispatching; using Squidex.Infrastructure.Tasks; @@ -18,32 +19,52 @@ namespace Squidex.Write.Assets public class AssetCommandHandler : ICommandHandler { 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(assetStore, nameof(assetStore)); + Guard.NotNull(assetThumbnailGenerator, nameof(assetThumbnailGenerator)); this.handler = handler; + this.assetStore = assetStore; + this.assetThumbnailGenerator = assetThumbnailGenerator; } protected async Task On(CreateAsset command, CommandContext context) { - await handler.CreateAsync(context, c => + await handler.CreateAsync(context, async c => { + command.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(command.File.OpenRead()); + c.Create(command); + await assetStore.UploadAssetAsync(c.Id, c.Version, command.File.OpenRead()); + 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(context, c => c.Rename(command)); + await handler.UpdateAsync(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(context, c => c.Update(command)); + return handler.UpdateAsync(context, c => c.Rename(command)); } protected Task On(DeleteAsset command, CommandContext context) diff --git a/src/Squidex.Write/Assets/AssetDomainObject.cs b/src/Squidex.Write/Assets/AssetDomainObject.cs index eca149e2f..aff28018f 100644 --- a/src/Squidex.Write/Assets/AssetDomainObject.cs +++ b/src/Squidex.Write/Assets/AssetDomainObject.cs @@ -60,29 +60,48 @@ namespace Squidex.Write.Assets 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; } - public AssetDomainObject Delete(DeleteAsset command) + public AssetDomainObject Update(UpdateAsset command) { Guard.NotNull(command, nameof(command)); 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; } - public AssetDomainObject Update(UpdateAsset command) + public AssetDomainObject Delete(DeleteAsset command) { Guard.NotNull(command, nameof(command)); VerifyCreatedAndNotDeleted(); - RaiseEvent(SimpleMapper.Map(command, new AssetUpdated())); + RaiseEvent(SimpleMapper.Map(command, new AssetDeleted())); return this; } diff --git a/src/Squidex.Write/Assets/Commands/CreateAsset.cs b/src/Squidex.Write/Assets/Commands/CreateAsset.cs index 501502b14..66f321237 100644 --- a/src/Squidex.Write/Assets/Commands/CreateAsset.cs +++ b/src/Squidex.Write/Assets/Commands/CreateAsset.cs @@ -6,20 +6,14 @@ // All rights reserved. // ========================================================================== +using Squidex.Infrastructure.Assets; + namespace Squidex.Write.Assets.Commands { public sealed class CreateAsset : AssetAggregateCommand { - public string FileName { get; set; } - - public string MimeType { get; set; } - - public long FileSize { get; set; } - - public bool IsImage { get; set; } - - public int? PixelWidth { get; set; } + public AssetFile File { get; set; } - public int? PixelHeight { get; set; } + public ImageInfo ImageInfo { get; set; } } } diff --git a/src/Squidex.Write/Assets/Commands/UpdateAsset.cs b/src/Squidex.Write/Assets/Commands/UpdateAsset.cs index 754b8ca0d..0c835e30a 100644 --- a/src/Squidex.Write/Assets/Commands/UpdateAsset.cs +++ b/src/Squidex.Write/Assets/Commands/UpdateAsset.cs @@ -6,18 +6,14 @@ // All rights reserved. // ========================================================================== +using Squidex.Infrastructure.Assets; + namespace Squidex.Write.Assets.Commands { public class UpdateAsset : AssetAggregateCommand { - public string MimeType { get; set; } - - public long FileSize { get; set; } - - public bool IsImage { get; set; } - - public int? PixelWidth { get; set; } + public AssetFile File { get; set; } - public int? PixelHeight { get; set; } + public ImageInfo ImageInfo { get; set; } } } diff --git a/src/Squidex/Controllers/Api/Assets/AssetContentController.cs b/src/Squidex/Controllers/Api/Assets/AssetContentController.cs index 59b90d435..a68716528 100644 --- a/src/Squidex/Controllers/Api/Assets/AssetContentController.cs +++ b/src/Squidex/Controllers/Api/Assets/AssetContentController.cs @@ -20,12 +20,9 @@ using Squidex.Read.Assets.Repositories; namespace Squidex.Controllers.Api.Assets { - /// - /// Uploads and retrieves assets. - /// [ApiExceptionFilter] [ServiceFilter(typeof(AppFilterAttribute))] - [SwaggerTag("Assets")] + [SwaggerIgnore] public class AssetContentController : ControllerBase { private readonly IAssetStore assetStorage; @@ -44,18 +41,6 @@ namespace Squidex.Controllers.Api.Assets this.assetThumbnailGenerator = assetThumbnailGenerator; } - /// - /// Gets the content of the asset. - /// - /// The name of the app. - /// The id of the asset. - /// The resize mode. - /// The target width of the image. - /// The target width of the image. - /// - /// 200 => Asset content returned. - /// 404 => App or Asset not found. - /// [HttpGet] [Route("assets/{id}/")] public async Task 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)) { - 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) { - var fullSizeContent = await assetStorage.GetAssetAsync($"{asset.Id}_{asset.Version}"); + var fullSizeContent = await assetStorage.GetAssetAsync(asset.Id, asset.Version); if (fullSizeContent == null) { @@ -86,14 +71,14 @@ namespace Squidex.Controllers.Api.Assets 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; } } else { - content = await assetStorage.GetAssetAsync($"{asset.Id}_{asset.Version}"); + content = await assetStorage.GetAssetAsync(asset.Id, asset.Version); } if (content == null) diff --git a/src/Squidex/Controllers/Api/Assets/AssetController.cs b/src/Squidex/Controllers/Api/Assets/AssetController.cs index 71a4f4099..e69f62578 100644 --- a/src/Squidex/Controllers/Api/Assets/AssetController.cs +++ b/src/Squidex/Controllers/Api/Assets/AssetController.cs @@ -8,7 +8,6 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; @@ -26,8 +25,6 @@ using Squidex.Pipeline; using Squidex.Read.Assets.Repositories; using Squidex.Write.Assets.Commands; -#pragma warning disable 1573 - namespace Squidex.Controllers.Api.Assets { /// @@ -39,28 +36,26 @@ namespace Squidex.Controllers.Api.Assets [SwaggerTag("Assets")] public class AssetController : ControllerBase { - private readonly IAssetStore assetStorage; private readonly IAssetRepository assetRepository; - private readonly IAssetThumbnailGenerator assetThumbnailGenerator; private readonly AssetConfig assetsConfig; public AssetController( ICommandBus commandBus, - IAssetStore assetStorage, IAssetRepository assetRepository, - IAssetThumbnailGenerator assetThumbnailGenerator, IOptions assetsConfig) : base(commandBus) { - this.assetStorage = assetStorage; this.assetsConfig = assetsConfig.Value; this.assetRepository = assetRepository; - this.assetThumbnailGenerator = assetThumbnailGenerator; } /// /// Get assets. /// + /// The number of assets to skip. + /// The number of assets to take. + /// The query to limit the files by name. + /// Comma separated list of mime types to get. /// /// 200 => assets returned. /// @@ -96,7 +91,8 @@ namespace Squidex.Controllers.Api.Assets /// /// Creates and uploads a new asset. /// - /// The name of the app. + /// The app where the asset is a part of. + /// The file to upload. /// /// 201 => Asset created. /// 404 => App not found. @@ -106,53 +102,104 @@ namespace Squidex.Controllers.Api.Assets [Route("apps/{app}/assets/")] [ProducesResponseType(typeof(AssetDto), 201)] [ProducesResponseType(typeof(ErrorDto), 400)] - public async Task PostAsset(string app, List files) + public async Task PostAsset(string app, List file) { - if (files.Count != 1) - { - var error = new ValidationError($"Can only upload one file, found ${files.Count}."); + var assetFile = GetAssetFile(file); - 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>(); + var response = AssetDto.Create(command, result); - if (file.Length > assetsConfig.MaxSize) - { - var error = new ValidationError($"File size cannot be longer than ${assetsConfig.MaxSize}."); + return StatusCode(201, response); + } - throw new ValidationException("Cannot create asset.", error); - } + /// + /// Replaces the content of the asset with a newer version. + /// + /// The app where the asset is a part of. + /// The id of the asset. + /// The file to upload. + /// + /// 201 => Asset updated. + /// 404 => App or Asset not found. + /// 400 => Asset exceeds the maximum size. + /// + [HttpPut] + [Route("apps/{app}/assets/{id}/content")] + [ProducesResponseType(typeof(AssetDto), 201)] + [ProducesResponseType(typeof(ErrorDto), 400)] + public async Task PutAssetContent(string app, Guid id, List file) + { + var assetFile = GetAssetFile(file); + + await CommandBus.PublishAsync(new UpdateAsset { File = assetFile }); - var fileContent = new MemoryStream(); + return NoContent(); + } - await file.OpenReadStream().CopyToAsync(fileContent); + /// + /// Updates the asset. + /// + /// The app where the asset is a part of. + /// The id of the asset. + /// The asset object that needs to updated. + /// + /// 201 => Asset updated. + /// 404 => App or Asset not found. + /// + [HttpPost] + [Route("apps/{app}/assets/{id}/content")] + [ProducesResponseType(typeof(AssetDto), 201)] + [ProducesResponseType(typeof(ErrorDto), 400)] + public async Task PutAsset(string app, Guid id, [FromBody] AssetUpdateDto request) + { + var command = SimpleMapper.Map(request, new RenameAsset()); + + await CommandBus.PublishAsync(command); - fileContent.Position = 0; + return NoContent(); + } + + /// + /// Delete an asset. + /// + /// The app where the schema is a part of. + /// The id of the asset to delete. + /// + /// 204 => Asset has been deleted. + /// + [HttpDelete] + [Route("apps/{app}/schemas/{name}/")] + public async Task 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 file) + { + if (file.Count != 1) { - AssetId = Guid.NewGuid(), - FileSize = file.Length, - FileName = file.FileName, - MimeType = file.ContentType, - IsImage = imageInfo != null, - PixelWidth = imageInfo?.PixelWidth, - PixelHeight = imageInfo?.PixelHeight - }; - - fileContent.Position = 0; + var error = new ValidationError($"Can only upload one file, found ${file.Count}."); - 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>(); - var response = AssetDto.Create(command, result); + if (formFile.Length > assetsConfig.MaxSize) + { + 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; } } } diff --git a/src/Squidex/Controllers/Api/Assets/Models/AssetDto.cs b/src/Squidex/Controllers/Api/Assets/Models/AssetDto.cs index 5f64319bd..21d5dbf5e 100644 --- a/src/Squidex/Controllers/Api/Assets/Models/AssetDto.cs +++ b/src/Squidex/Controllers/Api/Assets/Models/AssetDto.cs @@ -93,12 +93,12 @@ namespace Squidex.Controllers.Api.Assets.Models CreatedBy = command.Actor, LastModified = now, LastModifiedBy = command.Actor, - FileName = command.FileName, - FileSize = command.FileSize, - MimeType = command.MimeType, - IsImage = command.IsImage, - PixelWidth = command.PixelWidth, - PixelHeight = command.PixelHeight + FileName = command.File.FileName, + FileSize = command.File.FileSize, + MimeType = command.File.MimeType, + IsImage = command.ImageInfo != null, + PixelWidth = command.ImageInfo?.PixelWidth, + PixelHeight = command.ImageInfo?.PixelHeight }; return response; diff --git a/src/Squidex/Controllers/Api/Assets/Models/AssetUpdateDto.cs b/src/Squidex/Controllers/Api/Assets/Models/AssetUpdateDto.cs new file mode 100644 index 000000000..d627c24c4 --- /dev/null +++ b/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 + { + /// + /// The new name of the asset. + /// + [Required] + public string Name { get; set; } + } +} diff --git a/src/Squidex/Squidex.csproj b/src/Squidex/Squidex.csproj index a517add11..4a36a74d5 100644 --- a/src/Squidex/Squidex.csproj +++ b/src/Squidex/Squidex.csproj @@ -64,6 +64,7 @@ + diff --git a/src/Squidex/app/features/assets/pages/asset.component.html b/src/Squidex/app/features/assets/pages/asset.component.html index 91669dc4b..56ac6f642 100644 --- a/src/Squidex/app/features/assets/pages/asset.component.html +++ b/src/Squidex/app/features/assets/pages/asset.component.html @@ -1,12 +1,15 @@
-
+
-
+
{{fileType}}
-
+
+
+ +
+
+
+ + Drop to update +
\ No newline at end of file diff --git a/src/Squidex/app/features/assets/pages/asset.component.scss b/src/Squidex/app/features/assets/pages/asset.component.scss index 54771de50..8671a25d4 100644 --- a/src/Squidex/app/features/assets/pages/asset.component.scss +++ b/src/Squidex/app/features/assets/pages/asset.component.scss @@ -10,11 +10,42 @@ $color-type-foreground: #fff; 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 { & { + position: relative; height: $card-size; } + &.drag { + .drop-overlay { + @include opacity(1); + } + } + &-block { padding: .8rem .8rem 0; position: relative; @@ -33,6 +64,13 @@ $color-type-foreground: #fff; font-size: .8rem; } + &-icon { + &-container { + background: $color-border; + height: 155px; + } + } + &-name { @include truncate; font-size: 1rem; @@ -47,7 +85,8 @@ $color-type-foreground: #fff; background: $color-type-background; border: 0; padding: .1rem .3rem; - font-size: .8rem; + text-transform: uppercase; + font-size: .7rem; font-weight: normal; color: $color-type-foreground; } diff --git a/src/Squidex/app/features/assets/pages/asset.component.ts b/src/Squidex/app/features/assets/pages/asset.component.ts index 31c419099..714670290 100644 --- a/src/Squidex/app/features/assets/pages/asset.component.ts +++ b/src/Squidex/app/features/assets/pages/asset.component.ts @@ -48,6 +48,16 @@ export class AssetComponent extends AppComponentBase implements OnInit { return result; } + public get fileIcon(): string { + let result = ''; + + if (this.asset != null) { + result = fileIcon(this.fileType); + } + + return result; + } + public get fileName(): string { let result = ''; @@ -108,4 +118,45 @@ function fileSize(b: number) { } 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`; } \ No newline at end of file diff --git a/src/Squidex/app/shared/services/assets.service.ts b/src/Squidex/app/shared/services/assets.service.ts index 4a53f21ec..ad9b8e4fb 100644 --- a/src/Squidex/app/shared/services/assets.service.ts +++ b/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}` }); - content.append('files', file); + content.append('file', file); this.http .post(url, content, { headers }) diff --git a/src/Squidex/wwwroot/images/asset_doc.png b/src/Squidex/wwwroot/images/asset_doc.png new file mode 100644 index 000000000..08fb7de74 Binary files /dev/null and b/src/Squidex/wwwroot/images/asset_doc.png differ diff --git a/src/Squidex/wwwroot/images/asset_docx.png b/src/Squidex/wwwroot/images/asset_docx.png new file mode 100644 index 000000000..5a31c92da Binary files /dev/null and b/src/Squidex/wwwroot/images/asset_docx.png differ diff --git a/src/Squidex/wwwroot/images/asset_generic.png b/src/Squidex/wwwroot/images/asset_generic.png new file mode 100644 index 000000000..576117903 Binary files /dev/null and b/src/Squidex/wwwroot/images/asset_generic.png differ diff --git a/src/Squidex/wwwroot/images/asset_pdf.png b/src/Squidex/wwwroot/images/asset_pdf.png new file mode 100644 index 000000000..1aa38c9f1 Binary files /dev/null and b/src/Squidex/wwwroot/images/asset_pdf.png differ diff --git a/src/Squidex/wwwroot/images/asset_ppt.png b/src/Squidex/wwwroot/images/asset_ppt.png new file mode 100644 index 000000000..c2c7b7e62 Binary files /dev/null and b/src/Squidex/wwwroot/images/asset_ppt.png differ diff --git a/src/Squidex/wwwroot/images/asset_pptx.png b/src/Squidex/wwwroot/images/asset_pptx.png new file mode 100644 index 000000000..1d4832111 Binary files /dev/null and b/src/Squidex/wwwroot/images/asset_pptx.png differ diff --git a/src/Squidex/wwwroot/images/asset_video.png b/src/Squidex/wwwroot/images/asset_video.png new file mode 100644 index 000000000..0889275a0 Binary files /dev/null and b/src/Squidex/wwwroot/images/asset_video.png differ diff --git a/src/Squidex/wwwroot/images/asset_xls.png b/src/Squidex/wwwroot/images/asset_xls.png new file mode 100644 index 000000000..3e7849d3e Binary files /dev/null and b/src/Squidex/wwwroot/images/asset_xls.png differ diff --git a/src/Squidex/wwwroot/images/asset_xlsx.png b/src/Squidex/wwwroot/images/asset_xlsx.png new file mode 100644 index 000000000..d28ad0789 Binary files /dev/null and b/src/Squidex/wwwroot/images/asset_xlsx.png differ diff --git a/tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs b/tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs index c99271606..367911618 100644 --- a/tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs +++ b/tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs @@ -39,7 +39,7 @@ namespace Squidex.Write.Apps public AppCommandHandlerTests() { - app = new AppDomainObject(AppId, 0); + app = new AppDomainObject(AppId, -1); sut = new AppCommandHandler(Handler, appRepository.Object, userRepository.Object, keyGenerator.Object); } @@ -47,7 +47,7 @@ namespace Squidex.Write.Apps [Fact] 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)) .Returns(Task.FromResult(new Mock().Object)) @@ -64,7 +64,7 @@ namespace Squidex.Write.Apps [Fact] 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)) .Returns(Task.FromResult(null)) diff --git a/tests/Squidex.Write.Tests/Apps/AppDomainObjectTests.cs b/tests/Squidex.Write.Tests/Apps/AppDomainObjectTests.cs index 121ab3787..750f1c8c2 100644 --- a/tests/Squidex.Write.Tests/Apps/AppDomainObjectTests.cs +++ b/tests/Squidex.Write.Tests/Apps/AppDomainObjectTests.cs @@ -56,7 +56,7 @@ namespace Squidex.Write.Apps [Fact] 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); diff --git a/tests/Squidex.Write.Tests/Assets/AssetCommandHandlerTests.cs b/tests/Squidex.Write.Tests/Assets/AssetCommandHandlerTests.cs index 26367ea4a..18a770f2d 100644 --- a/tests/Squidex.Write.Tests/Assets/AssetCommandHandlerTests.cs +++ b/tests/Squidex.Write.Tests/Assets/AssetCommandHandlerTests.cs @@ -7,11 +7,15 @@ // ========================================================================== using System; +using System.IO; using System.Threading.Tasks; +using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Write.Assets.Commands; using Squidex.Write.TestHelpers; using Xunit; +using Moq; +using Squidex.Infrastructure.Tasks; // ReSharper disable ConvertToConstant.Local @@ -19,24 +23,31 @@ namespace Squidex.Write.Assets { public class AssetCommandHandlerTests : HandlerTestBase { + private readonly Mock assetThumbnailGenerator = new Mock(); + private readonly Mock assetStore = new Mock(); private readonly AssetCommandHandler sut; private readonly AssetDomainObject asset; private readonly Guid assetId = Guid.NewGuid(); - private readonly string fileName = "my-image.png"; - private readonly string mimeType = "image/png"; - private readonly long fileSize = 1024; + private readonly Stream stream = new MemoryStream(); + private readonly ImageInfo image = new ImageInfo(2048, 2048); + private readonly AssetFile file; 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] 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 _ => { @@ -44,19 +55,28 @@ namespace Squidex.Write.Assets }); Assert.Equal(assetId, context.Result>().IdOrValue); + + assetStore.VerifyAll(); + assetThumbnailGenerator.VerifyAll(); } [Fact] 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(); - 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 sut.HandleAsync(context); }); + + assetStore.VerifyAll(); + assetThumbnailGenerator.VerifyAll(); } [Fact] @@ -87,7 +107,7 @@ namespace Squidex.Write.Assets private void CreateAsset() { - asset.Create(new CreateAsset { FileName = fileName, FileSize = fileSize, MimeType = mimeType }); + asset.Create(new CreateAsset { File = file }); } } } diff --git a/tests/Squidex.Write.Tests/Assets/AssetDomainObjectTests.cs b/tests/Squidex.Write.Tests/Assets/AssetDomainObjectTests.cs index e6ef96af5..571987b7b 100644 --- a/tests/Squidex.Write.Tests/Assets/AssetDomainObjectTests.cs +++ b/tests/Squidex.Write.Tests/Assets/AssetDomainObjectTests.cs @@ -7,8 +7,10 @@ // ========================================================================== using System; +using System.IO; using Squidex.Events.Assets; using Squidex.Infrastructure; +using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.CQRS; using Squidex.Write.Assets.Commands; using Squidex.Write.TestHelpers; @@ -21,9 +23,8 @@ namespace Squidex.Write.Assets public class AssetDomainObjectTests : HandlerTestBase { private readonly AssetDomainObject sut; - private readonly string fileName = "my-image.png"; - private readonly string mimeType = "image/png"; - private readonly long fileSize = 1024; + private readonly ImageInfo image = new ImageInfo(2048, 2048); + private readonly AssetFile file = new AssetFile("my-image.png", "image/png", 1024, () => new MemoryStream()); public Guid AssetId { get; } = Guid.NewGuid(); @@ -35,22 +36,30 @@ namespace Squidex.Write.Assets [Fact] 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(() => { - sut.Create(CreateAssetCommand(new CreateAsset { FileName = fileName, FileSize = fileSize, MimeType = mimeType })); + sut.Create(CreateAssetCommand(new CreateAsset { File = file })); }); } [Fact] 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() .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(() => { - 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(); - sut.Update(CreateAssetCommand(new UpdateAsset { FileSize = fileSize, MimeType = mimeType })); + sut.Update(CreateAssetCommand(new UpdateAsset { File = file, ImageInfo = image })); sut.GetUncomittedEvents() .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(() => { - 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(() => { - 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() { - sut.Create(CreateAssetCommand(new CreateAsset { FileName = fileName, FileSize = fileSize, MimeType = mimeType })); + sut.Create(CreateAssetCommand(new CreateAsset { File = file })); ((IAggregate)sut).ClearUncommittedEvents(); } diff --git a/tests/Squidex.Write.Tests/Contents/ContentCommandHandlerTests.cs b/tests/Squidex.Write.Tests/Contents/ContentCommandHandlerTests.cs index b530a12f8..d7867095f 100644 --- a/tests/Squidex.Write.Tests/Contents/ContentCommandHandlerTests.cs +++ b/tests/Squidex.Write.Tests/Contents/ContentCommandHandlerTests.cs @@ -43,7 +43,7 @@ namespace Squidex.Write.Contents .AddOrUpdateField(new NumberField(1, "my-field", new NumberFieldProperties { IsRequired = true })); - content = new ContentDomainObject(contentId, 0); + content = new ContentDomainObject(contentId, -1); sut = new ContentCommandHandler(Handler, appProvider.Object, schemaProvider.Object); diff --git a/tests/Squidex.Write.Tests/Schemas/SchemaCommandHandlerTests.cs b/tests/Squidex.Write.Tests/Schemas/SchemaCommandHandlerTests.cs index 80cd0b6a5..c6e4bfe10 100644 --- a/tests/Squidex.Write.Tests/Schemas/SchemaCommandHandlerTests.cs +++ b/tests/Squidex.Write.Tests/Schemas/SchemaCommandHandlerTests.cs @@ -32,7 +32,7 @@ namespace Squidex.Write.Schemas public SchemaCommandHandlerTests() { - schema = new SchemaDomainObject(SchemaId, 0, registry); + schema = new SchemaDomainObject(SchemaId, -1, registry); sut = new SchemaCommandHandler(Handler, schemaProvider.Object); }