// Kyoo - A portable and vast media library solution. // Copyright (c) Kyoo. // // See AUTHORS.md and LICENSE file in the project root for full license information. // // Kyoo is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // any later version. // // Kyoo is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Kyoo. If not, see . using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models.Exceptions; using Kyoo.Abstractions.Models.Permissions; using Kyoo.Abstractions.Models.Utils; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace Kyoo.Core.Api { /// /// A base class to handle CRUD operations on a specific resource type . /// /// The type of resource to make CRUD apis for. [ApiController] [ResourceView] public class CrudApi : ControllerBase where T : class, IResource { /// /// The repository of the resource, used to retrieve, save and do operations on the baking store. /// private readonly IRepository _repository; /// /// The base URL of Kyoo. This will be used to create links for images and . /// protected Uri BaseURL { get; } /// /// Create a new using the given repository and base url. /// /// /// The repository to use as a baking store for the type . /// /// /// The base URL of Kyoo to use to create links. /// public CrudApi(IRepository repository, Uri baseURL) { _repository = repository; BaseURL = baseURL; } /// /// Construct and return a page from an api. /// /// The list of resources that should be included in the current page. /// /// The max number of items that should be present per page. This should be the same as in the request, /// it is used to calculate if this is the last page and so on. /// /// The type of items on the page. /// A Page representing the response. protected Page Page(ICollection resources, int limit) where TResult : IResource { return new Page(resources, new Uri(BaseURL, Request.Path), Request.Query.ToDictionary(x => x.Key, x => x.Value.ToString(), StringComparer.InvariantCultureIgnoreCase), limit); } /// /// Get by ID /// /// /// Get a specific resource via it's ID. /// /// The ID of the resource to retrieve. /// The retrieved resource. /// A resource with the given ID does not exist. [HttpGet("{id:int}")] [PartialPermission(Kind.Read)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> Get(int id) { T ret = await _repository.GetOrDefault(id); if (ret == null) return NotFound(); return ret; } /// /// Get by slug /// /// /// Get a specific resource via it's slug (a unique, human readable identifier). /// /// The slug of the resource to retrieve. /// The retrieved resource. /// A resource with the given ID does not exist. [HttpGet("{slug}")] [PartialPermission(Kind.Read)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> Get(string slug) { T ret = await _repository.GetOrDefault(slug); if (ret == null) return NotFound(); return ret; } /// /// Get count /// /// /// Get the number of resources that match the filters. /// /// A list of filters to respect. /// How many resources matched that filter. /// Invalid filters. [HttpGet("count")] [PartialPermission(Kind.Read)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] public async Task> GetCount([FromQuery] Dictionary where) { try { return await _repository.GetCount(ApiHelper.ParseWhere(where)); } catch (ArgumentException ex) { return BadRequest(new RequestError(ex.Message)); } } /// /// Get all /// /// /// Get all resources that match the given filter. /// /// Sort information about the query (sort by, sort order). /// Where the pagination should start. /// Filter the returned items. /// How many items per page should be returned. /// A list of resources that match every filters. /// Invalid filters or sort information. [HttpGet] [PartialPermission(Kind.Read)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] public async Task>> GetAll([FromQuery] string sortBy, [FromQuery] int afterID, [FromQuery] Dictionary where, [FromQuery] int limit = 20) { try { ICollection resources = await _repository.GetAll(ApiHelper.ParseWhere(where), new Sort(sortBy), new Pagination(limit, afterID)); return Page(resources, limit); } catch (ArgumentException ex) { return BadRequest(new RequestError(ex.Message)); } } /// /// Create new /// /// /// Create a new item and store it. You may leave the ID unspecified, it will be filed by Kyoo. /// /// The resource to create. /// The created resource. /// The resource in the request body is invalid. /// This item already exists (maybe a duplicated slug). [HttpPost] [PartialPermission(Kind.Create)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] [ProducesResponseType(StatusCodes.Status409Conflict, Type = typeof(ActionResult<>))] public virtual async Task> Create([FromBody] T resource) { try { return await _repository.Create(resource); } catch (ArgumentException ex) { return BadRequest(new RequestError(ex.Message)); } catch (DuplicatedItemException) { T existing = await _repository.GetOrDefault(resource.Slug); return Conflict(existing); } } /// /// Edit /// /// /// Edit an item. If the ID is specified it will be used to identify the resource. /// If not, the slug will be used to identify it. /// /// The resource to edit. /// /// Should old properties of the resource be discarded or should null values considered as not changed? /// /// The created resource. /// The resource in the request body is invalid. /// No item found with the specified ID (or slug). [HttpPut] [PartialPermission(Kind.Write)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> Edit([FromBody] T resource, [FromQuery] bool resetOld = true) { try { if (resource.ID > 0) return await _repository.Edit(resource, resetOld); T old = await _repository.Get(resource.Slug); resource.ID = old.ID; return await _repository.Edit(resource, resetOld); } catch (ItemNotFoundException) { return NotFound(); } } [HttpDelete("{id:int}")] [PartialPermission(Kind.Delete)] public virtual async Task Delete(int id) { try { await _repository.Delete(id); } catch (ItemNotFoundException) { return NotFound(); } return Ok(); } [HttpDelete("{slug}")] [PartialPermission(Kind.Delete)] public virtual async Task Delete(string slug) { try { await _repository.Delete(slug); } catch (ItemNotFoundException) { return NotFound(); } return Ok(); } [HttpDelete] [PartialPermission(Kind.Delete)] public virtual async Task Delete(Dictionary where) { try { await _repository.DeleteAll(ApiHelper.ParseWhere(where)); } catch (ItemNotFoundException) { return NotFound(); } return Ok(); } } }