Select Git revision
Code owners
Assign users and groups as approvers for specific file changes. 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);
}
}
}
}