mirror of https://github.com/Squidex/squidex.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
346 lines
12 KiB
346 lines
12 KiB
// ==========================================================================
|
|
// 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.Net.Http.Headers;
|
|
using Squidex.Areas.Api.Controllers.Assets.Models;
|
|
using Squidex.Domain.Apps.Core.Tags;
|
|
using Squidex.Domain.Apps.Entities;
|
|
using Squidex.Domain.Apps.Entities.Apps.Plans;
|
|
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
|
|
{
|
|
/// <summary>
|
|
/// Uploads and retrieves assets.
|
|
/// </summary>
|
|
[ApiExplorerSettings(GroupName = nameof(Assets))]
|
|
public sealed class AssetsController : ApiController
|
|
{
|
|
private readonly IAssetQueryService assetQuery;
|
|
private readonly IAssetUsageTracker assetStatsRepository;
|
|
private readonly IAppPlansProvider appPlansProvider;
|
|
private readonly ITagService tagService;
|
|
|
|
public AssetsController(
|
|
ICommandBus commandBus,
|
|
IAssetQueryService assetQuery,
|
|
IAssetUsageTracker assetStatsRepository,
|
|
IAppPlansProvider appPlansProvider,
|
|
ITagService tagService)
|
|
: base(commandBus)
|
|
{
|
|
this.assetQuery = assetQuery;
|
|
this.assetStatsRepository = assetStatsRepository;
|
|
this.appPlansProvider = appPlansProvider;
|
|
this.tagService = tagService;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get assets tags.
|
|
/// </summary>
|
|
/// <param name="app">The name of the app.</param>
|
|
/// <returns>
|
|
/// 200 => Assets returned.
|
|
/// 404 => App not found.
|
|
/// </returns>
|
|
/// <remarks>
|
|
/// Get all tags for assets.
|
|
/// </remarks>
|
|
[HttpGet]
|
|
[Route("apps/{app}/assets/tags")]
|
|
[ProducesResponseType(typeof(Dictionary<string, int>), 200)]
|
|
[ApiPermission(Permissions.AppAssetsRead)]
|
|
[ApiCosts(1)]
|
|
public async Task<IActionResult> GetTags(string app)
|
|
{
|
|
var tags = await tagService.GetTagsAsync(AppId, TagGroups.Assets);
|
|
|
|
Response.Headers[HeaderNames.ETag] = tags.Version.ToString();
|
|
|
|
return Ok(tags);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get assets.
|
|
/// </summary>
|
|
/// <param name="app">The name of the app.</param>
|
|
/// <param name="parentId">The optional parent folder id.</param>
|
|
/// <param name="ids">The optional asset ids.</param>
|
|
/// <param name="q">The optional json query.</param>
|
|
/// <returns>
|
|
/// 200 => Assets returned.
|
|
/// 404 => App not found.
|
|
/// </returns>
|
|
/// <remarks>
|
|
/// Get all assets for the app.
|
|
/// </remarks>
|
|
[HttpGet]
|
|
[Route("apps/{app}/assets/")]
|
|
[ProducesResponseType(typeof(AssetsDto), 200)]
|
|
[ApiPermission(Permissions.AppAssetsRead)]
|
|
[ApiCosts(1)]
|
|
public async Task<IActionResult> GetAssets(string app, [FromQuery] Guid? parentId, [FromQuery] string? ids = null, [FromQuery] string? q = null)
|
|
{
|
|
var assets = await assetQuery.QueryAsync(Context, parentId, CreateQuery(ids, q));
|
|
|
|
var response = Deferred.Response(() =>
|
|
{
|
|
return AssetsDto.FromAssets(assets, this, app);
|
|
});
|
|
|
|
return Ok(response);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get assets.
|
|
/// </summary>
|
|
/// <param name="app">The name of the app.</param>
|
|
/// <param name="query">The required query object.</param>
|
|
/// <returns>
|
|
/// 200 => Assets returned.
|
|
/// 404 => App not found.
|
|
/// </returns>
|
|
/// <remarks>
|
|
/// Get all assets for the app.
|
|
/// </remarks>
|
|
[HttpPost]
|
|
[Route("apps/{app}/assets/query")]
|
|
[ProducesResponseType(typeof(AssetsDto), 200)]
|
|
[ApiPermission(Permissions.AppAssetsRead)]
|
|
[ApiCosts(1)]
|
|
public async Task<IActionResult> GetAssetsPost(string app, [FromBody] QueryDto query)
|
|
{
|
|
var assets = await assetQuery.QueryAsync(Context, query?.ParentId, query?.ToQuery() ?? Q.Empty);
|
|
|
|
var response = Deferred.Response(() =>
|
|
{
|
|
return AssetsDto.FromAssets(assets, this, app);
|
|
});
|
|
|
|
return Ok(response);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get an asset by id.
|
|
/// </summary>
|
|
/// <param name="app">The name of the app.</param>
|
|
/// <param name="id">The id of the asset to retrieve.</param>
|
|
/// <returns>
|
|
/// 200 => Asset found.
|
|
/// 404 => Asset or app not found.
|
|
/// </returns>
|
|
[HttpGet]
|
|
[Route("apps/{app}/assets/{id}/")]
|
|
[ProducesResponseType(typeof(AssetDto), 200)]
|
|
[ApiPermission(Permissions.AppAssetsRead)]
|
|
[ApiCosts(1)]
|
|
public async Task<IActionResult> 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);
|
|
});
|
|
|
|
return Ok(response);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Upload a new asset.
|
|
/// </summary>
|
|
/// <param name="app">The name of the app.</param>
|
|
/// <param name="parentId">The optional parent folder id.</param>
|
|
/// <param name="file">The file to upload.</param>
|
|
/// <returns>
|
|
/// 201 => Asset created.
|
|
/// 404 => App not found.
|
|
/// 400 => Asset exceeds the maximum size.
|
|
/// </returns>
|
|
/// <remarks>
|
|
/// You can only upload one file at a time. The mime type of the file is not calculated by Squidex and is required correctly.
|
|
/// </remarks>
|
|
[HttpPost]
|
|
[Route("apps/{app}/assets/")]
|
|
[ProducesResponseType(typeof(AssetDto), 201)]
|
|
[AssetRequestSizeLimit]
|
|
[ApiPermission(Permissions.AppAssetsCreate)]
|
|
[ApiCosts(1)]
|
|
public async Task<IActionResult> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Replace asset content.
|
|
/// </summary>
|
|
/// <param name="app">The name of the app.</param>
|
|
/// <param name="id">The id of the asset.</param>
|
|
/// <param name="file">The file to upload.</param>
|
|
/// <returns>
|
|
/// 200 => Asset updated.
|
|
/// 404 => Asset or app not found.
|
|
/// 400 => Asset exceeds the maximum size.
|
|
/// </returns>
|
|
/// <remarks>
|
|
/// Use multipart request to upload an asset.
|
|
/// </remarks>
|
|
[HttpPut]
|
|
[Route("apps/{app}/assets/{id}/content/")]
|
|
[ProducesResponseType(typeof(AssetDto), 200)]
|
|
[ApiPermission(Permissions.AppAssetsUpload)]
|
|
[ApiCosts(1)]
|
|
public async Task<IActionResult> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the asset.
|
|
/// </summary>
|
|
/// <param name="app">The name of the app.</param>
|
|
/// <param name="id">The id of the asset.</param>
|
|
/// <param name="request">The asset object that needs to updated.</param>
|
|
/// <returns>
|
|
/// 200 => Asset updated.
|
|
/// 400 => Asset name not valid.
|
|
/// 404 => Asset or app not found.
|
|
/// </returns>
|
|
[HttpPut]
|
|
[Route("apps/{app}/assets/{id}/")]
|
|
[ProducesResponseType(typeof(AssetDto), 200)]
|
|
[AssetRequestSizeLimit]
|
|
[ApiPermission(Permissions.AppAssetsUpdate)]
|
|
[ApiCosts(1)]
|
|
public async Task<IActionResult> PutAsset(string app, Guid id, [FromBody] AnnotateAssetDto request)
|
|
{
|
|
var command = request.ToCommand(id);
|
|
|
|
var response = await InvokeCommandAsync(app, command);
|
|
|
|
return Ok(response);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Moves the asset.
|
|
/// </summary>
|
|
/// <param name="app">The name of the app.</param>
|
|
/// <param name="id">The id of the asset.</param>
|
|
/// <param name="request">The asset object that needs to updated.</param>
|
|
/// <returns>
|
|
/// 200 => Asset moved.
|
|
/// 404 => Asset or app not found.
|
|
/// </returns>
|
|
[HttpPut]
|
|
[Route("apps/{app}/assets/{id}/parent")]
|
|
[ProducesResponseType(typeof(AssetDto), 200)]
|
|
[AssetRequestSizeLimit]
|
|
[ApiPermission(Permissions.AppAssetsUpdate)]
|
|
[ApiCosts(1)]
|
|
public async Task<IActionResult> PutAssetParent(string app, Guid id, [FromBody] MoveAssetItemDto request)
|
|
{
|
|
var command = request.ToCommand(id);
|
|
|
|
var response = await InvokeCommandAsync(app, command);
|
|
|
|
return Ok(response);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Delete an asset.
|
|
/// </summary>
|
|
/// <param name="app">The name of the app.</param>
|
|
/// <param name="id">The id of the asset to delete.</param>
|
|
/// <returns>
|
|
/// 204 => Asset deleted.
|
|
/// 404 => Asset or app not found.
|
|
/// </returns>
|
|
[HttpDelete]
|
|
[Route("apps/{app}/assets/{id}/")]
|
|
[ApiPermission(Permissions.AppAssetsDelete)]
|
|
[ApiCosts(1)]
|
|
public async Task<IActionResult> DeleteAsset(string app, Guid id)
|
|
{
|
|
await CommandBus.PublishAsync(new DeleteAsset { AssetId = id });
|
|
|
|
return NoContent();
|
|
}
|
|
|
|
private async Task<AssetDto> 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<IEnrichedAssetEntity>(), this, app);
|
|
}
|
|
}
|
|
|
|
private async Task<AssetFile> 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();
|
|
}
|
|
|
|
private Q CreateQuery(string? ids, string? q)
|
|
{
|
|
return Q.Empty
|
|
.WithIds(ids)
|
|
.WithJsonQuery(q)
|
|
.WithODataQuery(Request.QueryString.ToString());
|
|
}
|
|
}
|
|
}
|
|
|