using Coscine.ApiCommons; using Coscine.Configuration; using Coscine.Database.DataModel; using Coscine.Database.Models; using Coscine.Database.Util; using Coscine.Logging; using Coscine.Metadata; using Coscine.ResourceTypes; using Coscine.ResourceTypes.Base.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Net; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Web; namespace Coscine.Api.Blob.Controllers { /// <summary> /// This controller represents the actions which can be taken with a Blob object. /// </summary> [Authorize] public class BlobController : Controller { private readonly IConfiguration _configuration; private readonly Authenticator _authenticator; private readonly ResourceModel _resourceModel; private readonly ProjectResourceModel _projectResourceModel; private readonly ProjectRoleModel _projectRoleModel; private readonly CoscineLogger _coscineLogger; private readonly AnalyticsLogObject _analyticsLogObject; private readonly RdfStoreConnector _rdfStoreConnector; private readonly CoscineLDPHelper _coscineLDPHelper; /// <summary> /// Blob controller constructor /// </summary> /// <param name="logger">Logger</param> public BlobController(ILogger<BlobController> logger) { _configuration = Program.Configuration; _authenticator = new Authenticator(this, _configuration); _resourceModel = new ResourceModel(); _projectResourceModel = new ProjectResourceModel(); _projectRoleModel = new ProjectRoleModel(); _rdfStoreConnector = new RdfStoreConnector(_configuration.GetStringAndWait("coscine/local/virtuoso/additional/url")); _coscineLogger = new CoscineLogger(logger); _analyticsLogObject = new AnalyticsLogObject(); _coscineLDPHelper = new CoscineLDPHelper(_rdfStoreConnector); } /// <summary> /// This method checks if the given file exists and returns it /// </summary> /// <param name="resourceId">Id of the resource</param> /// <param name="path"> Path to the file </param> /// <returns>File if file exists otherwise status code 204, 400, 401 or 404 </returns> [HttpGet("[controller]/{resourceId}/{*path}")] [DisableRequestSizeLimit] [ApiExplorerSettings(IgnoreApi = true)] public async Task<IActionResult> GetFileWithPath(string resourceId, string path) { return await GetFile(resourceId, path); } /// <summary> /// This method checks if the given file exists and returns it /// </summary> /// <param name="resourceId">Id of the resource</param> /// <param name="path"> Path to the file </param> /// <returns>File if file exists otherwise status code 204, 400, 401 or 404 </returns> [HttpGet("[controller]/{resourceId}/")] [DisableRequestSizeLimit] public async Task<IActionResult> GetFileWithParameter(string resourceId, [FromQuery] string path) { // Strip the first slash, to reuse the previous implementation. if (path.StartsWith("/")) { path = path[1..]; } return await GetFile(resourceId, path); } /// <summary> /// This method checks if the given file exists and returns it /// </summary> /// <param name="resourceId">Id of the resource</param> /// <param name="path"> Path to the file </param> /// <returns>File if file exists otherwise status code 204, 400, 401 or 404 </returns> public async Task<IActionResult> GetFile(string resourceId, string path) { var user = _authenticator.GetUser(); //path = $"/{path}"; var checkPath = CheckPath(path); if (checkPath != null) { return checkPath; } var checkResourceId = CheckResource(resourceId, out Resource resource); if (checkResourceId != null) { return checkResourceId; } // Rights Matrix (https://git.rwth-aachen.de/coscine/docs/private/internal-wiki/-/blob/master/coscine/Definition%20of%20rights%20Matrix.md) // - Resource: View Resource (RCV, Metadatamanager) if (user is null || !_resourceModel.HasAccess(user, resource, UserRoles.Owner, UserRoles.Member, UserRoles.Guest)) { return Forbid("User does not have permission to download files from the resource."); } try { var resourceTypeDefinition = ResourceTypeFactory.Instance.GetResourceType(resource); if (resourceTypeDefinition == null) { return BadRequest($"No provider for: \"{resource.Type.DisplayName}\"."); } var infos = await resourceTypeDefinition.GetEntry(resource.Id.ToString(), path); var response = await resourceTypeDefinition.LoadEntry(resource.Id.ToString(), path); new FileExtensionContentTypeProvider().TryGetContentType(path, out string contentType); LogAnalytics("Download File", resourceId, path, user); return File(response, contentType ?? "application/octet-stream"); } catch (Exception e) { _coscineLogger.Log(LogType.High, "Get File failed", e); return BadRequest("Error in communication with the resource"); } } /// <summary> /// This method uploads a given File /// </summary> /// <param name="resourceId">Id of the resource </param> /// <param name="path">Path to the file</param> /// <param name="files">List of files.</param> /// <returns>status code 204 if file is uploaded otherwise status code 400 or 403</returns> [DisableRequestSizeLimit] [RequestFormLimits(MultipartBodyLengthLimit = long.MaxValue)] [HttpPut("[controller]/{resourceId}/{*path}")] [ApiExplorerSettings(IgnoreApi = true)] public async Task<IActionResult> UploadFileWithPath(string resourceId, string path, List<IFormFile> files) { return await UploadFile(resourceId, path, files); } /// <summary> /// This method uploads a given File /// </summary> /// <param name="resourceId">Id of the resource </param> /// <param name="path">Path to the file</param> /// <param name="files">List of files.</param> /// <returns>status code 204 if file is uploaded otherwise status code 400 or 403</returns> [DisableRequestSizeLimit] [RequestFormLimits(MultipartBodyLengthLimit = long.MaxValue)] [HttpPut("[controller]/{resourceId}/")] public async Task<IActionResult> UploadFileWithParameter(string resourceId, [FromQuery] string path, List<IFormFile> files) { // Strip the first slash, to reuse the previous implementation. if (path.StartsWith("/")) { path = path[1..]; } return await UploadFile(resourceId, path, files); } /// <summary> /// This method uploads a given File /// </summary> /// <param name="resourceId">Id of the resource </param> /// <param name="path">Path to the file</param> /// <param name="files">List of files.</param> /// <returns>status code 204 if file is uploaded otherwise status code 400 or 403</returns> public async Task<IActionResult> UploadFile(string resourceId, string path, List<IFormFile> files) { var user = _authenticator.GetUser(); var checkPath = CheckPath(path); if (checkPath != null) { return checkPath; } var checkResourceId = CheckResource(resourceId, out Resource resource); if (checkResourceId != null) { return checkResourceId; } // Rights Matrix (https://git.rwth-aachen.de/coscine/docs/private/internal-wiki/-/blob/master/coscine/Definition%20of%20rights%20Matrix.md) // - Resource: Change Resource (RCV, Metadatamanager) if (user is null || !_resourceModel.HasAccess(user, resource, UserRoles.Owner, UserRoles.Member)) { return Forbid("User does not have permission to upload files in the resource."); } if (resource.Archived == "1") { return BadRequest("The resource is read only!"); } if (files.Count != 1) { return BadRequest("Only one file can be uploaded per request."); } var id = _coscineLDPHelper.GetId(resourceId, $"/{path}", true); if (!_rdfStoreConnector.HasGraph(id)) { return StatusCode((int)HttpStatusCode.Forbidden, "No metadata set has been added for this file."); } try { var stream = files[0].OpenReadStream(); var resourceTypeDefinition = ResourceTypeFactory.Instance.GetResourceType(resource); if (resourceTypeDefinition == null) { return BadRequest($"No provider for: \"{resource.Type.DisplayName}\"."); } ResourceEntry infos = null; try { infos = await resourceTypeDefinition.GetEntry(resource.Id.ToString(), path, null); } catch { // do nothing } await resourceTypeDefinition.StoreEntry(resource.Id.ToString(), path, stream); LogAnalytics(infos == null ? "Upload File" : "Update File", resourceId, path, user); return NoContent(); } catch (Exception e) { _coscineLogger.Log(LogType.High, "Upload File failed", e); return BadRequest("Error in communication with the resource"); } } /// <summary> /// This method deletes a given file /// </summary> /// <param name="resourceId">Id of the resource </param> /// <param name="path">Path to the file</param> /// <returns>status code 204 if deletion successful otherwise status code 400, 401 or 404 </returns> [HttpDelete("[controller]/{resourceId}/{*path}")] [ApiExplorerSettings(IgnoreApi = true)] public async Task<IActionResult> DeleteFileWithPath(string resourceId, string path) { return await DeleteFile(resourceId, path); } /// <summary> /// This method deletes a given file /// </summary> /// <param name="resourceId">Id of the resource </param> /// <param name="path">Path to the file</param> /// <returns>status code 204 if deletion successful otherwise status code 400, 401 or 404 </returns> [HttpDelete("[controller]/{resourceId}/")] public async Task<IActionResult> DeleteFileWithParameter(string resourceId, [FromQuery] string path) { // Strip the first slash, to reuse the previous implementation. if (path.StartsWith("/")) { path = path[1..]; } return await DeleteFile(resourceId, path); } /// <summary> /// This method deletes a given file /// </summary> /// <param name="resourceId">Id of the resource </param> /// <param name="path">Path to the file</param> /// <returns>status code 204 if deletion successful otherwise status code 400, 401 or 404 </returns> public async Task<IActionResult> DeleteFile(string resourceId, string path) { var user = _authenticator.GetUser(); var checkPath = CheckPath(path); if (checkPath != null) { return checkPath; } var checkResourceId = CheckResource(resourceId, out Resource resource); if (checkResourceId != null) { return checkResourceId; } // Rights Matrix (https://git.rwth-aachen.de/coscine/docs/private/internal-wiki/-/blob/master/coscine/Definition%20of%20rights%20Matrix.md) // - Resource: Change Resource (RCV, Metadatamanager) if (user is null || !_resourceModel.HasAccess(user, resource, UserRoles.Owner, UserRoles.Member)) { return Forbid("User does not have permission to delete from the resource."); } if (resource.Archived == "1") { return BadRequest("The resource is read only!"); } try { var resourceTypeDefinition = ResourceTypeFactory.Instance.GetResourceType(resource); if (resourceTypeDefinition == null) { return BadRequest($"No provider for: \"{resource.Type.DisplayName}\"."); } await resourceTypeDefinition.DeleteEntry(resource.Id.ToString(), path); var userGraphName = $"{_rdfStoreConnector.UserUrlPrefix}/{user.Id}"; if (resourceTypeDefinition.ResourceTypeConfiguration.SpecificType?.Type != "linked") { _rdfStoreConnector.SetInvalidation(resource.Id.ToString(), path, "data", new Uri(userGraphName)); } _rdfStoreConnector.SetInvalidation(resource.Id.ToString(), path, "metadata", new Uri(userGraphName)); LogAnalytics("Delete File", resourceId, path, user); return NoContent(); } catch (Exception e) { _coscineLogger.Log(LogType.High, "Delete failed", e); return BadRequest("Error in communication with the resource"); } } /// <summary> /// Tries to establish connection with resource and validates whether the given file/folder exists /// </summary> private IActionResult CheckResource(string resourceId, out Resource resource) { resource = null; if (!Guid.TryParse(resourceId, out Guid resourceGuid)) { return BadRequest($"{resourceId} is not a GUID."); } try { resource = _resourceModel.GetById(resourceGuid); if (resource == null) { return NotFound($"Could not find resource with id: {resourceId}"); } } catch (Exception) { return NotFound($"Could not find resource with id: {resourceId}"); } if (resource.Type == null) { var resourceTypeModel = new ResourceTypeModel(); resource.Type = resourceTypeModel.GetById(resource.TypeId); } // All good return null; } /// <summary> /// Checks if the path is valid /// </summary> /// <param name="path">path</param> /// <returns>status code 400 if the given path is not valid</returns> public IActionResult CheckPath(string path) { if (string.IsNullOrWhiteSpace(path)) { return BadRequest($"Your path \"{path}\" is empty."); } var rgx = new Regex(@"[\:?*<>|]+"); if (rgx.IsMatch(path)) { return BadRequest($"Your path \"{path}\" contains bad characters. The following characters are not permissible: {@"\/:?*<>|"}."); } if (path.Contains("%2F") || path.Contains("%2f")) { return BadRequest("Path can not contain the sequence %2F."); } return null; } /// <summary> /// Writes an analytics log entry /// </summary> private void LogAnalytics(string operation, string resourceId, string path, User user) { if (CoscineLoggerConfiguration.IsLogLevelActivated(LogType.Analytics)) { _analyticsLogObject.Type = "Action"; _analyticsLogObject.FileId = resourceId + "/" + HttpUtility.UrlDecode(path); _analyticsLogObject.ResourceId = resourceId; _analyticsLogObject.ProjectId = _projectResourceModel.GetProjectForResource(new Guid(resourceId)).ToString(); _analyticsLogObject.RoleId = _projectRoleModel.GetGetUserRoleForProject(new Guid(_analyticsLogObject.ProjectId), user.Id).ToString(); _analyticsLogObject.Operation = operation; _coscineLogger.AnalyticsLog(_analyticsLogObject); } } } }