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 byte size 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."); } 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 resourceTypeInformation = resourceTypeDefinition.GetResourceTypeInformation().Result; if (resource.ResourceTypeOptionId.HasValue && resourceTypeInformation.IsQuotaAvailable) { try { var totalFileSize = resourceTypeDefinition.GetResourceQuotaUsed(resourceId, resourceTypeOptions).Result; return Ok($"{{ \"data\": {{ \"usedSizeByte\": {totalFileSize} }}}}"); } catch (Exception e) { _coscineLogger.Log("Get Quota failed", e); return BadRequest("Error in communication with the resource"); } } else { return BadRequest("The resource quota must be adjustable."); } } /// <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, [System.Web.Http.FromUri] 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; } 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[path.LastIndexOf("/")..], out string contentType); LogAnalytics("Download File", resourceId, path[1..], user); return File(response, contentType ?? "application/octet-stream"); } catch (Exception e) { _coscineLogger.Log("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, [System.Web.Http.FromUri] 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(); 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 read only!"); } 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 metadata set 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) { _coscineLogger.Log("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, [System.Web.Http.FromUri] 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(); 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 read only!"); } 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 (Exception e) { _coscineLogger.Log("Delete failed", e); return BadRequest("Error in communication with the resource"); } } /// <summary> /// This method checks if the resource is valid /// </summary> /// <returns>status code 204 if resource is valid otherwise status code 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 (Exception e) { _coscineLogger.Log("Resource validation 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) { 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>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> /// Checks if the user has access to the resource /// </summary> /// <param name="user">user</param> /// <param name="resource">resource</param> /// <returns>status code 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); _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); } } } }