// ==========================================================================
// 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;
}
}
}