// ========================================================================== // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex UG (haftungsbeschränkt) // All rights reserved. Licensed under the MIT license. // ========================================================================== using System; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using Microsoft.Net.Http.Headers; using Squidex.Areas.Api.Controllers.Assets.Models; using Squidex.Areas.Api.Controllers.Contents; using Squidex.Domain.Apps.Core.Tags; using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Apps.Services; using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Validation; using Squidex.Shared; using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Assets { /// /// Uploads and retrieves assets. /// [ApiExplorerSettings(GroupName = nameof(Assets))] public sealed class AssetsController : ApiController { private readonly IAssetQueryService assetQuery; private readonly IAssetUsageTracker assetStatsRepository; private readonly IAppPlansProvider appPlansProvider; private readonly MyContentsControllerOptions controllerOptions; private readonly ITagService tagService; public AssetsController( ICommandBus commandBus, IAssetQueryService assetQuery, IAssetUsageTracker assetStatsRepository, IAppPlansProvider appPlansProvider, IOptions controllerOptions, ITagService tagService) : base(commandBus) { this.assetQuery = assetQuery; this.assetStatsRepository = assetStatsRepository; this.appPlansProvider = appPlansProvider; this.controllerOptions = controllerOptions.Value; this.tagService = tagService; } /// /// Get assets tags. /// /// The name of the app. /// /// 200 => Assets returned. /// 404 => App not found. /// /// /// Get all tags for assets. /// [HttpGet] [Route("apps/{app}/assets/tags")] [ProducesResponseType(typeof(Dictionary), 200)] [ApiPermission(Permissions.AppAssetsRead)] [ApiCosts(1)] public async Task GetTags(string app) { var tags = await tagService.GetTagsAsync(AppId, TagGroups.Assets); Response.Headers[HeaderNames.ETag] = tags.Version.ToString(); return Ok(tags); } /// /// Get assets. /// /// The name of the app. /// The optional parent folder id. /// The optional asset ids. /// The optional json query. /// /// 200 => Assets returned. /// 404 => App not found. /// /// /// Get all assets for the app. /// [HttpGet] [Route("apps/{app}/assets/")] [ProducesResponseType(typeof(AssetsDto), 200)] [ApiPermission(Permissions.AppAssetsRead)] [ApiCosts(1)] public async Task GetAssets(string app, [FromQuery] Guid? parentId, [FromQuery] string? ids = null, [FromQuery] string? q = null) { var assets = await assetQuery.QueryAsync(Context, parentId, Q.Empty .WithIds(ids) .WithJsonQuery(q) .WithODataQuery(Request.QueryString.ToString())); var response = Deferred.Response(() => { return AssetsDto.FromAssets(assets, this, app); }); if (controllerOptions.EnableSurrogateKeys && assets.Count <= controllerOptions.MaxItemsForSurrogateKeys) { Response.Headers["Surrogate-Key"] = assets.ToSurrogateKeys(); } Response.Headers[HeaderNames.ETag] = assets.ToEtag(); return Ok(response); } /// /// Get an asset by id. /// /// The name of the app. /// The id of the asset to retrieve. /// /// 200 => Asset found. /// 404 => Asset or app not found. /// [HttpGet] [Route("apps/{app}/assets/{id}/")] [ProducesResponseType(typeof(AssetDto), 200)] [ApiPermission(Permissions.AppAssetsRead)] [ApiCosts(1)] public async Task GetAsset(string app, Guid id) { var asset = await assetQuery.FindAssetAsync(Context, id); if (asset == null) { return NotFound(); } var response = Deferred.Response(() => { return AssetDto.FromAsset(asset, this, app); }); if (controllerOptions.EnableSurrogateKeys) { Response.Headers["Surrogate-Key"] = asset.ToSurrogateKey(); } Response.Headers[HeaderNames.ETag] = asset.ToEtag(); return Ok(response); } /// /// Upload a new asset. /// /// The name of the app. /// The optional parent folder id. /// The file to upload. /// /// 201 => Asset created. /// 404 => App not found. /// 400 => Asset exceeds the maximum size. /// /// /// You can only upload one file at a time. The mime type of the file is not calculated by Squidex and is required correctly. /// [HttpPost] [Route("apps/{app}/assets/")] [ProducesResponseType(typeof(AssetDto), 201)] [AssetRequestSizeLimit] [ApiPermission(Permissions.AppAssetsCreate)] [ApiCosts(1)] public async Task PostAsset(string app, [FromQuery] Guid parentId, IFormFile file) { var assetFile = await CheckAssetFileAsync(file); var command = new CreateAsset { File = assetFile, ParentId = parentId }; var response = await InvokeCommandAsync(app, command); return CreatedAtAction(nameof(GetAsset), new { app, id = response.Id }, response); } /// /// Replace asset content. /// /// The name of the app. /// The id of the asset. /// The file to upload. /// /// 200 => Asset updated. /// 404 => Asset or app not found. /// 400 => Asset exceeds the maximum size. /// /// /// Use multipart request to upload an asset. /// [HttpPut] [Route("apps/{app}/assets/{id}/content/")] [ProducesResponseType(typeof(AssetDto), 200)] [ApiPermission(Permissions.AppAssetsUpload)] [ApiCosts(1)] public async Task PutAssetContent(string app, Guid id, IFormFile file) { var assetFile = await CheckAssetFileAsync(file); var command = new UpdateAsset { File = assetFile, AssetId = id }; var response = await InvokeCommandAsync(app, command); return Ok(response); } /// /// Updates the asset. /// /// The name of the app. /// The id of the asset. /// The asset object that needs to updated. /// /// 200 => Asset updated. /// 400 => Asset name not valid. /// 404 => Asset or app not found. /// [HttpPut] [Route("apps/{app}/assets/{id}/")] [ProducesResponseType(typeof(AssetDto), 200)] [AssetRequestSizeLimit] [ApiPermission(Permissions.AppAssetsUpdate)] [ApiCosts(1)] public async Task PutAsset(string app, Guid id, [FromBody] AnnotateAssetDto request) { var command = request.ToCommand(id); var response = await InvokeCommandAsync(app, command); return Ok(response); } /// /// Moves the asset. /// /// The name of the app. /// The id of the asset. /// The asset object that needs to updated. /// /// 200 => Asset moved. /// 404 => Asset or app not found. /// [HttpPut] [Route("apps/{app}/assets/{id}/parent")] [ProducesResponseType(typeof(AssetDto), 200)] [AssetRequestSizeLimit] [ApiPermission(Permissions.AppAssetsUpdate)] [ApiCosts(1)] public async Task PutAssetParent(string app, Guid id, [FromBody] MoveAssetItemDto request) { var command = request.ToCommand(id); var response = await InvokeCommandAsync(app, command); return Ok(response); } /// /// Delete an asset. /// /// The name of the app. /// The id of the asset to delete. /// /// 204 => Asset deleted. /// 404 => Asset or app not found. /// [HttpDelete] [Route("apps/{app}/assets/{id}/")] [ApiPermission(Permissions.AppAssetsDelete)] [ApiCosts(1)] public async Task DeleteAsset(string app, Guid id) { await CommandBus.PublishAsync(new DeleteAsset { AssetId = id }); return NoContent(); } private async Task InvokeCommandAsync(string app, ICommand command) { var context = await CommandBus.PublishAsync(command); if (context.PlainResult is AssetCreatedResult created) { return AssetDto.FromAsset(created.Asset, this, app, created.IsDuplicate); } else { return AssetDto.FromAsset(context.Result(), this, app); } } private async Task CheckAssetFileAsync(IFormFile? file) { if (file == null || Request.Form.Files.Count != 1) { var error = new ValidationError($"Can only upload one file, found {Request.Form.Files.Count} files."); throw new ValidationException("Cannot create asset.", error); } var plan = appPlansProvider.GetPlanForApp(App); var currentSize = await assetStatsRepository.GetTotalSizeAsync(AppId); if (plan.MaxAssetSize > 0 && plan.MaxAssetSize < currentSize + file.Length) { var error = new ValidationError("You have reached your max asset size."); throw new ValidationException("Cannot create asset.", error); } return file.ToAssetFile(); } } }