Select Git revision
requirements.txt
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
This project manages its dependencies using pip.
Learn more
BlobController.cs 17.88 KiB
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.ResourceLoader;
using Coscine.ResourceTypeBase;
using Coscine.WaterbutlerHelper.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
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 string _prefix;
/// <summary>
/// Blob controller constructor
/// </summary>
/// <param name="logger">Logger</param>
/// <param name="dataSourceService">Source service for data</param>
public BlobController(ILogger<BlobController> logger, IDataSourceService dataSourceService)
{
_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();
_prefix = _configuration.GetStringAndWait("coscine/global/epic/prefix");
}
/// <summary>
/// Generates Id
/// </summary>
/// <param name="resourceId">Id of the resource</param>
/// <param name="path"> Path to file</param>
/// <returns> Uri </returns>
public Uri GenerateId(string resourceId, string path)
{
return new Uri($"https://hdl.handle.net/{_prefix}/{resourceId}@path={Uri.EscapeDataString(path)}");
}
/// <summary>
/// This method returns the amount of allocated space for the given resource
/// </summary>
/// <param name="resourceId">Id of a resource</param>
/// <returns>Data, file count and bytesize used or Status Code 400, 404, 401 or 500 </returns>
[HttpGet("[controller]/{resourceId}/quota")]
public IActionResult GetQuota(string resourceId)
{
if (!Guid.TryParse(resourceId, out Guid resourceGuid))
{
return BadRequest($"{resourceId} is not a guid.");
}
var user = _authenticator.GetUser();
Resource resource;
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)
{
ResourceTypeModel resourceTypeModel = new ResourceTypeModel();
resource.Type = resourceTypeModel.GetById(resource.TypeId);
}
if (user == null || !_resourceModel.HasAccess(user, resource, UserRoles.Owner, UserRoles.Member))
{
return BadRequest("User does not have permission to the resource.");
}
if ((resource.Type.DisplayName.ToLower() == "rds" || resource.Type.DisplayName.ToLower() == "rdss3") && resource.ResourceTypeOptionId.HasValue)
{
try
{
var resourceTypeOptions = _resourceModel.GetResourceTypeOptions(resource.Id);
var resourceTypeDefinition = ResourceTypeFactory.CreateResourceTypeObject(resource.Type.DisplayName, _configuration);
if (resourceTypeDefinition == null)
{
return BadRequest($"No provider for: \"{resource.Type.DisplayName}\".");
}
var totalFileSize = resourceTypeDefinition.GetResourceQuotaUsed(resourceId, resourceTypeOptions).Result;
return Ok($"{{ \"data\": {{ \"usedSizeByte\": {totalFileSize} }}}}");
}
catch
{
return BadRequest($"Error in communication with the resource");
}
}
else
{
return BadRequest("The resource type must be rds.");
}
}
/// <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 Statuscode 204, 400, 401 or 404 </returns>
[HttpGet("[controller]/{resourceId}/{*path}")]
[DisableRequestSizeLimit]
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;
}
var checkUser = CheckUser(user, resource);
if (checkUser != null)
{
return checkUser;
}
var resourceTypeOptions = _resourceModel.GetResourceTypeOptions(resource.Id);
try
{
var resourceTypeDefinition = ResourceTypeFactory.CreateResourceTypeObject(resource.Type.DisplayName, _configuration);
if (resourceTypeDefinition == null)
{
return BadRequest($"No provider for: \"{resource.Type.DisplayName}\".");
}
var infos = await resourceTypeDefinition.GetEntry(resource.Id.ToString(), path, null, resourceTypeOptions);
var response = await resourceTypeDefinition.LoadEntry(resource.Id.ToString(), path, null, resourceTypeOptions);
new FileExtensionContentTypeProvider().TryGetContentType(path.Substring(path.LastIndexOf("/")), out string contentType);
LogAnalytics("Download File", resourceId, path.Substring(1), user);
return File(response, contentType ?? "application/octet-stream");
}
catch
{
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>Statuscode 204 if file is uploaded otherwise Statuscode 400 or 403</returns>
[DisableRequestSizeLimit]
[RequestFormLimits(MultipartBodyLengthLimit = long.MaxValue)]
[HttpPut("[controller]/{resourceId}/{*path}")]
public async Task<IActionResult> UploadFile(string resourceId, string path, List<IFormFile> files)
{
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;
}
var checkUser = CheckUser(user, resource);
if (checkUser != null)
{
return checkUser;
}
if (resource.Archived == "1")
{
return BadRequest("The resource is readonly!");
}
if (files.Count != 1)
{
return BadRequest($"Only one file can be uploaded per request.");
}
var id = GenerateId(resourceId, path);
if (!_rdfStoreConnector.HasGraph(id.AbsoluteUri))
{
return StatusCode((int)HttpStatusCode.Forbidden,
"No metadataset has been added for this file.");
}
try
{
var resourceTypeOptions = _resourceModel.GetResourceTypeOptions(resource.Id);
var stream = files[0].OpenReadStream();
resourceTypeOptions.Add("ContentLength", stream.Length.ToString());
var resourceTypeDefinition = ResourceTypeFactory.CreateResourceTypeObject(resource.Type.DisplayName, _configuration);
if (resourceTypeDefinition == null)
{
return BadRequest($"No provider for: \"{resource.Type.DisplayName}\".");
}
ResourceEntry infos = null;
try
{
infos = await resourceTypeDefinition.GetEntry(resource.Id.ToString(), path, null, resourceTypeOptions);
}
catch
{
// do nothing
}
await resourceTypeDefinition.StoreEntry(resource.Id.ToString(), path, stream, resourceTypeOptions);
LogAnalytics(infos == null ? "Upload File" : "Update File", resourceId, path, user);
return NoContent();
}
catch (Exception 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>Statuscode 204 if deletion successful otherwise Statuscode 400, 401 or 404 </returns>
[HttpDelete("[controller]/{resourceId}/{*path}")]
public async Task<IActionResult> DeleteFile(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;
}
var checkUser = CheckUser(user, resource);
if (checkUser != null)
{
return checkUser;
}
if (resource.Archived == "1")
{
return BadRequest("The resource is readonly!");
}
try
{
var resourceTypeOptions = _resourceModel.GetResourceTypeOptions(resource.Id);
var resourceTypeDefinition = ResourceTypeFactory.CreateResourceTypeObject(resource.Type.DisplayName, _configuration);
if (resourceTypeDefinition == null)
{
return BadRequest($"No provider for: \"{resource.Type.DisplayName}\".");
}
await resourceTypeDefinition.DeleteEntry(resource.Id.ToString(), path, resourceTypeOptions);
LogAnalytics("Delete File", resourceId, path, user);
return NoContent();
}
catch
{
return BadRequest($"Error in communication with the resource");
}
}
/// <summary>
/// This method checks if the resource is valid
/// </summary>
/// <returns>Statuscode 204 if resource is valid otherwise Statuscode 400 or 404</returns>
[HttpPost("[controller]/validate")]
public async Task<IActionResult> IsResourceValid([FromBody] JToken resource)
{
var displayName = resource["type"]["displayName"].ToString().ToLower();
var resourceTypeOptions = new Dictionary<string, string>();
if (displayName == "s3")
{
resourceTypeOptions.Add("accessKey", resource["resourceTypeOption"]["AccessKey"].ToString());
resourceTypeOptions.Add("secretKey", resource["resourceTypeOption"]["SecretKey"].ToString());
resourceTypeOptions.Add("bucketname", resource["resourceTypeOption"]["BucketName"].ToString());
resourceTypeOptions.Add("resourceUrl", resource["resourceTypeOption"]["ResourceUrl"].ToString());
}
else if (displayName == "gitlab")
{
resourceTypeOptions.Add("token", resource["resourceTypeOption"]["Token"].ToString());
resourceTypeOptions.Add("repositoryUrl", resource["resourceTypeOption"]["RepositoryUrl"].ToString());
resourceTypeOptions.Add("repositoryNumber", resource["resourceTypeOption"]["RepositoryNumber"].ToString());
}
try
{
var resourceTypeDefinition = ResourceTypeFactory.CreateResourceTypeObject(displayName, _configuration);
if (resourceTypeDefinition == null)
{
return BadRequest($"No provider for: \"{displayName}\".");
}
await resourceTypeDefinition.IsResourceCreated("", resourceTypeOptions);
return NoContent();
}
catch
{
return BadRequest($"Error in communication with the resource");
}
}
/// <summary>
/// Tries to establish connection with resource and validates wether 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)
{
ResourceTypeModel 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>Statuscode 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>
/// Checks if the user has access to the resource
/// </summary>
/// <param name="user">user</param>
/// <param name="resource">resource</param>
/// <returns>Statuscode 403 if the user has no access</returns>
public IActionResult CheckUser(User user, Resource resource)
{
if (user == null || !_resourceModel.HasAccess(user, resource, UserRoles.Owner, UserRoles.Member))
{
return Forbid("User does not have permission to the resource.");
}
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)[1..];
_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);
}
}
}
}