// ========================================================================== // AssetsController.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 Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; using NSwag.Annotations; using Squidex.Controllers.Api.Assets.Models; using Squidex.Infrastructure; using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.Reflection; using Squidex.Pipeline; using Squidex.Read.Apps.Services; using Squidex.Read.Assets.Repositories; using Squidex.Write.Assets.Commands; namespace Squidex.Controllers.Api.Assets { /// /// Uploads and retrieves assets. /// [MustBeAppEditor] [ApiExceptionFilter] [AppApi] [SwaggerTag("Assets")] public class AssetsController : ControllerBase { private readonly IAssetRepository assetRepository; private readonly IAssetStatsRepository assetStatsRepository; private readonly IAppPlansProvider appPlanProvider; private readonly AssetConfig assetsConfig; public AssetsController( ICommandBus commandBus, IAssetRepository assetRepository, IAssetStatsRepository assetStatsRepository, IAppPlansProvider appPlanProvider, IOptions assetsConfig) : base(commandBus) { this.assetsConfig = assetsConfig.Value; this.assetRepository = assetRepository; this.assetStatsRepository = assetStatsRepository; this.appPlanProvider = appPlanProvider; } /// /// Get assets. /// /// The name of the app. /// The optional asset ids. /// The number of assets to skip. /// The number of assets to take (Default: 20). /// The query to limit the files by name. /// Comma separated list of mime types to get. /// /// 200 => Assets returned. /// 404 => App not found. /// /// /// Get all assets for the app. Mime types can be comma-separated, e.g. application/json,text/html. /// [HttpGet] [Route("apps/{app}/assets/")] [ProducesResponseType(typeof(AssetsDto), 200)] [ApiCosts(1)] public async Task GetAssets(string app, [FromQuery] string query = null, [FromQuery] string mimeTypes = null, [FromQuery] string ids = null, [FromQuery] int skip = 0, [FromQuery] int take = 10) { var mimeTypeList = new HashSet(); if (!string.IsNullOrWhiteSpace(mimeTypes)) { foreach (var mimeType in mimeTypes.Split(',')) { mimeTypeList.Add(mimeType.Trim()); } } var idsList = new HashSet(); if (!string.IsNullOrWhiteSpace(ids)) { foreach (var id in ids.Split(',')) { if (Guid.TryParse(id, out var guid)) { idsList.Add(guid); } } } var taskForAssets = assetRepository.QueryAsync(AppId, mimeTypeList, idsList, query, take, skip); var taskForCount = assetRepository.CountAsync(AppId, mimeTypeList, idsList, query); await Task.WhenAll(taskForAssets, taskForCount); var response = new AssetsDto { Total = taskForCount.Result, Items = taskForAssets.Result.Select(x => SimpleMapper.Map(x, new AssetDto())).ToArray() }; 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(AssetsDto), 200)] [ApiCosts(1)] public async Task GetAsset(string app, Guid id) { var entity = await assetRepository.FindAssetAsync(id); if (entity == null || entity.IsDeleted) { return NotFound(); } var response = SimpleMapper.Map(entity, new AssetDto()); Response.Headers["ETag"] = new StringValues(entity.Version.ToString()); return Ok(response); } /// /// Upload a new asset. /// /// The name of the app. /// 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 must be defined correctly. /// [HttpPost] [Route("apps/{app}/assets/")] [ProducesResponseType(typeof(AssetCreatedDto), 201)] [ProducesResponseType(typeof(ErrorDto), 400)] public async Task PostAsset(string app, List file) { var assetFile = await CheckAssetFileAsync(file); var command = new CreateAsset { File = assetFile }; var context = await CommandBus.PublishAsync(command); var result = context.Result>(); var response = AssetCreatedDto.Create(command, result); return StatusCode(201, response); } /// /// Replace asset content. /// /// The name of the app. /// The id of the asset. /// The file to upload. /// /// 201 => Asset updated. /// 404 => Asset or app not found. /// 400 => Asset exceeds the maximum size. /// [HttpPut] [Route("apps/{app}/assets/{id}/content")] [ProducesResponseType(typeof(AssetReplacedDto), 201)] [ProducesResponseType(typeof(ErrorDto), 400)] [ApiCosts(1)] public async Task PutAssetContent(string app, Guid id, List file) { var assetFile = await CheckAssetFileAsync(file); var command = new UpdateAsset { File = assetFile, AssetId = id }; var context = await CommandBus.PublishAsync(command); var result = context.Result(); var response = AssetReplacedDto.Create(command, result); return StatusCode(201, response); } /// /// Updates the asset. /// /// The name of the app. /// The id of the asset. /// The asset object that needs to updated. /// /// 204 => Asset updated. /// 400 => Asset name not valid. /// 404 => Asset or app not found. /// [HttpPut] [Route("apps/{app}/assets/{id}")] [ProducesResponseType(typeof(ErrorDto), 400)] [ApiCosts(1)] public async Task PutAsset(string app, Guid id, [FromBody] AssetUpdateDto request) { var command = SimpleMapper.Map(request, new RenameAsset { AssetId = id }); await CommandBus.PublishAsync(command); return NoContent(); } /// /// Delete an asset. /// /// The name of the app. /// The id of the asset to delete. /// /// 204 => Asset has been deleted. /// 404 => Asset or app not found. /// [HttpDelete] [Route("apps/{app}/assets/{id}/")] [ApiCosts(1)] public async Task DeleteAsset(string app, Guid id) { await CommandBus.PublishAsync(new DeleteAsset { AssetId = id }); return NoContent(); } private async Task CheckAssetFileAsync(IReadOnlyList file) { if (file.Count != 1) { var error = new ValidationError($"Can only upload one file, found {file.Count}."); throw new ValidationException("Cannot create asset.", error); } var formFile = file[0]; if (formFile.Length > assetsConfig.MaxSize) { var error = new ValidationError($"File size cannot be longer than ${assetsConfig.MaxSize}."); throw new ValidationException("Cannot create asset.", error); } var plan = appPlanProvider.GetPlanForApp(App); var currentSize = await assetStatsRepository.GetTotalSizeAsync(App.Id); if (plan.MaxAssetSize > 0 && plan.MaxAssetSize < currentSize + formFile.Length) { var error = new ValidationError("You have reached your max asset size."); throw new ValidationException("Cannot create asset.", error); } var assetFile = new AssetFile(formFile.FileName, formFile.ContentType, formFile.Length, formFile.OpenReadStream); return assetFile; } } }