Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion source/backend/api/Areas/Documents/DocumentController.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using MapsterMapper;
using Microsoft.AspNetCore.Authorization;
Expand Down Expand Up @@ -125,6 +126,25 @@ public async Task<IActionResult> DownloadFile(long mayanDocumentId, long mayanFi
return new FileContentResult(fileBytes, result.Payload.Mimetype) { FileDownloadName = result.Payload.FileName };
}

/// <summary>
/// Stream the file for the corresponding file and document id and return a stream.
/// </summary>
[HttpGet("storage/{mayanDocumentId}/files/{mayanFileId}/stream")]
[HasPermission(Permissions.DocumentView)]
[ProducesResponseType(typeof(File), 200)]
[SwaggerOperation(Tags = new[] { "storage-documents" })]
public async Task<IActionResult> StreamFile(long mayanDocumentId, long mayanFileId)
{
var result = await _documentService.StreamFileAsync(mayanDocumentId, mayanFileId);

if (result?.Payload == null)
{
return new NotFoundResult();
}

return File(result.Payload.FilePayload, "application/octet-stream", $"{result.Payload.FileName}");
}

/// <summary>
/// Retrieves a list of documents.
/// </summary>
Expand Down Expand Up @@ -208,7 +228,7 @@ public async Task<IActionResult> DownloadWrappedFile(long mayanDocumentId)
[ProducesResponseType(typeof(FileContentResult), 200)]
[SwaggerOperation(Tags = new[] { "storage-documents" })]
[TypeFilter(typeof(NullJsonResultFilter))]
public async Task<IActionResult> DownloadFile(long mayanDocumentId)
public async Task<IActionResult> DownloadFileLatest(long mayanDocumentId)
{
var result = await _documentService.DownloadFileLatestAsync(mayanDocumentId);
if (result?.Payload == null)
Expand All @@ -220,6 +240,25 @@ public async Task<IActionResult> DownloadFile(long mayanDocumentId)
return new FileContentResult(fileBytes, result.Payload.Mimetype) { FileDownloadName = result.Payload.FileName };
}

/// <summary>
/// Streams the latest file for the corresponding document id.
/// </summary>
[HttpGet("storage/{mayanDocumentId}/stream")]
[HasPermission(Permissions.DocumentView)]
[ProducesResponseType(typeof(File), 200)]
[SwaggerOperation(Tags = new[] { "storage-documents" })]
[TypeFilter(typeof(NullJsonResultFilter))]
public async Task<IActionResult> StreamFileLatest(long mayanDocumentId)
{
var result = await _documentService.StreamFileLatestAsync(mayanDocumentId);
if (result?.Payload == null)
{
return new NotFoundResult();
}

return File(result.Payload.FilePayload, "application/octet-stream", $"{result.Payload.FileName}");
}

/// <summary>
/// Retrieves a external document metadata.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;

using Pims.Api.Models;
using Pims.Api.Models.Mayan;
using Pims.Api.Models.Mayan.Document;
using Pims.Api.Models.Requests.Http;
Expand Down Expand Up @@ -31,6 +31,8 @@ public interface IEdmsDocumentRepository

Task<ExternalResponse<FileDownloadResponse>> TryDownloadFileAsync(long documentId, long fileId);

Task<ExternalResponse<FileStreamResponse>> TryStreamFileAsync(long documentId, long fileId);

Task<ExternalResponse<string>> TryDeleteDocument(long documentId);

Task<ExternalResponse<DocumentDetailModel>> TryUploadDocumentAsync(long documentType, IFormFile file);
Expand Down
53 changes: 40 additions & 13 deletions source/backend/api/Repositories/Mayan/MayanDocumentRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Pims.Api.Models;
using Pims.Api.Models.CodeTypes;
using Pims.Api.Models.Mayan;
using Pims.Api.Models.Mayan.Document;
Expand Down Expand Up @@ -170,25 +171,14 @@ public async Task<ExternalResponse<QueryResponse<DocumentMetadataModel>>> TryGet
public async Task<ExternalResponse<FileDownloadResponse>> TryDownloadFileAsync(long documentId, long fileId)
{
_logger.LogDebug("Downloading file {documentId}, {fileId}...", documentId, fileId);
string authenticationToken = await _authRepository.GetTokenAsync();

using HttpClient client = _httpClientFactory.CreateClient("Pims.Api.Logging");
client.DefaultRequestHeaders.Accept.Clear();
AddAuthentication(client, authenticationToken);
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json));

ExternalResponse<FileDownloadResponse> result = new ExternalResponse<FileDownloadResponse>()
ExternalResponse<FileDownloadResponse> result = new()
{
Status = ExternalResponseStatus.Error,
};

try
{
Uri endpoint = new($"{this._config.BaseUri}/documents/{documentId}/files/{fileId}/download/");
HttpResponseMessage httpResponse = await client.GetAsync(endpoint).ConfigureAwait(true);

var response = await ProcessDownloadResponse(httpResponse);

var response = await ProcessDownloadResponse(await GetFileAsync(documentId, fileId));
return response;
}
catch (Exception e)
Expand All @@ -202,6 +192,30 @@ public async Task<ExternalResponse<FileDownloadResponse>> TryDownloadFileAsync(l
return result;
}

public async Task<ExternalResponse<FileStreamResponse>> TryStreamFileAsync(long documentId, long fileId)
{
_logger.LogDebug("Streaming file {documentId}, {fileId}...", documentId, fileId);
ExternalResponse<FileStreamResponse> result = new()
{
Status = ExternalResponseStatus.Error,
};

try
{
var stream = await ProcessStreamResponse(await GetFileAsync(documentId, fileId));
return stream;
}
catch (Exception e)
{
result.Status = ExternalResponseStatus.Error;
result.Message = "Exception downloading file";
_logger.LogError("Unexpected exception streaming file {e}", e);
}

_logger.LogDebug($"Finished streaming file");
return result;
}

public async Task<ExternalResponse<string>> TryDeleteDocument(long documentId)
{
_logger.LogDebug("Deleting document {documentId}...", documentId);
Expand Down Expand Up @@ -381,5 +395,18 @@ public async Task<HttpResponseMessage> TryGetFilePageImage(long documentId, long
_logger.LogDebug("Finished retrieving mayan file page");
return response;
}

private async Task<HttpResponseMessage> GetFileAsync(long documentId, long fileId)
{
string authenticationToken = await _authRepository.GetTokenAsync();

using HttpClient client = _httpClientFactory.CreateClient("Pims.Api.Logging");
client.DefaultRequestHeaders.Accept.Clear();
AddAuthentication(client, authenticationToken);
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json));

Uri endpoint = new($"{this._config.BaseUri}/documents/{documentId}/files/{fileId}/download/");
return await client.GetAsync(endpoint).ConfigureAwait(true);
}
}
}
58 changes: 57 additions & 1 deletion source/backend/api/Repositories/RestCommon/BaseRestRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Pims.Api.Models;
using Pims.Api.Models.CodeTypes;
using Pims.Api.Models.Requests.Http;

Expand Down Expand Up @@ -196,7 +197,11 @@ protected async Task<ExternalResponse<FileDownloadResponse>> ProcessDownloadResp
byte[] responsePayload = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(true);
_logger.LogTrace("Response: {response}", response);
response.Content.Headers.TryGetValues("Content-Length", out IEnumerable<string> contentLengthHeaders);
long contentLength = contentLengthHeaders?.FirstOrDefault() != null ? int.Parse(contentLengthHeaders.FirstOrDefault(), CultureInfo.InvariantCulture) : responsePayload.Length;
long contentLength = responsePayload.Length;
if (contentLengthHeaders?.FirstOrDefault() != null)
{
contentLength = int.Parse(contentLengthHeaders.FirstOrDefault(), CultureInfo.InvariantCulture);
}
result.HttpStatusCode = response.StatusCode;
switch (response.StatusCode)
{
Expand Down Expand Up @@ -233,6 +238,57 @@ protected async Task<ExternalResponse<FileDownloadResponse>> ProcessDownloadResp
return result;
}

protected async Task<ExternalResponse<FileStreamResponse>> ProcessStreamResponse(HttpResponseMessage response)
{
ExternalResponse<FileStreamResponse> result = new ExternalResponse<FileStreamResponse>()
{
Status = ExternalResponseStatus.Error,
};

Stream responsePayload = await response.Content.ReadAsStreamAsync().ConfigureAwait(true);
_logger.LogTrace("Response: {response}", response);
response.Content.Headers.TryGetValues("Content-Length", out IEnumerable<string> contentLengthHeaders);
long contentLength = responsePayload.Length;
if (contentLengthHeaders?.FirstOrDefault() != null)
{
contentLength = int.Parse(contentLengthHeaders.FirstOrDefault(), CultureInfo.InvariantCulture);
}
result.HttpStatusCode = response.StatusCode;
switch (response.StatusCode)
{
case HttpStatusCode.OK:
string contentDisposition = response.Content.Headers.GetValues("Content-Disposition").FirstOrDefault();
string fileName = GetFileNameFromContentDisposition(contentDisposition);

result.Status = ExternalResponseStatus.Success;
result.Payload = new FileStreamResponse()
{
FilePayload = responsePayload,
Size = contentLength,
Mimetype = response.Content.Headers.GetValues("Content-Type").FirstOrDefault(),
FileName = fileName,
FileNameExtension = Path.GetExtension(fileName).Replace(".", string.Empty),
FileNameWithoutExtension = Path.GetFileNameWithoutExtension(fileName),
};

break;
case HttpStatusCode.NoContent:
result.Status = ExternalResponseStatus.Success;
result.Message = "No content found";
break;
case HttpStatusCode.Forbidden:
result.Status = ExternalResponseStatus.Error;
result.Message = "Forbidden";
break;
default:
result.Status = ExternalResponseStatus.Error;
result.Message = $"Unable to contact endpoint {response.RequestMessage.RequestUri}. Http status {response.StatusCode}";
break;
}

return result;
}

private static string GetFileNameFromContentDisposition(string contentDisposition)
{
const string fileNameFlag = "filename";
Expand Down
63 changes: 63 additions & 0 deletions source/backend/api/Services/DocumentService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Pims.Api.Helpers.Exceptions;
using Pims.Api.Models;
using Pims.Api.Models.CodeTypes;
using Pims.Api.Models.Concepts.Document;
using Pims.Api.Models.Config;
Expand Down Expand Up @@ -405,6 +406,30 @@ public async Task<ExternalResponse<FileDownloadResponse>> DownloadFileAsync(long
}
}

public async Task<ExternalResponse<FileStreamResponse>> StreamFileAsync(long mayanDocumentId, long mayanFileId)
{
this.Logger.LogInformation("Streaming storage document {mayanDocumentId} {mayanFileId}", mayanDocumentId, mayanFileId);
this.User.ThrowIfNotAuthorized(Permissions.DocumentView);

ExternalResponse<FileStreamResponse> downloadResult = await documentStorageRepository.TryStreamFileAsync(mayanDocumentId, mayanFileId);
if (IsValidDocumentExtension(downloadResult.Payload.FileName))
{
if (downloadResult.Status != ExternalResponseStatus.Success)
{
throw GetMayanResponseError(downloadResult.Message);
}
return downloadResult;
}
else
{
return new ExternalResponse<FileStreamResponse>()
{
Status = ExternalResponseStatus.Error,
Message = $"Document with id ${mayanDocumentId} has an invalid extension",
};
}
}

public async Task<ExternalResponse<FileDownloadResponse>> DownloadFileLatestAsync(long mayanDocumentId)
{
this.Logger.LogInformation("Downloading storage document latest {mayanDocumentId}", mayanDocumentId);
Expand Down Expand Up @@ -443,6 +468,44 @@ public async Task<ExternalResponse<FileDownloadResponse>> DownloadFileLatestAsyn
}
}

public async Task<ExternalResponse<FileStreamResponse>> StreamFileLatestAsync(long mayanDocumentId)
{
this.Logger.LogInformation("Streaming storage document latest {mayanDocumentId}", mayanDocumentId);

ExternalResponse<DocumentDetailModel> documentResult = await documentStorageRepository.TryGetDocumentAsync(mayanDocumentId);
if (documentResult.Status == ExternalResponseStatus.Success)
{
if (documentResult.Payload != null)
{
if (IsValidDocumentExtension(documentResult.Payload.FileLatest.FileName))
{
ExternalResponse<FileStreamResponse> downloadResult = await documentStorageRepository.TryStreamFileAsync(documentResult.Payload.Id, documentResult.Payload.FileLatest.Id);
return downloadResult;
}
else
{
return new ExternalResponse<FileStreamResponse>()
{
Status = ExternalResponseStatus.Error,
Message = $"Document with id ${mayanDocumentId} has an invalid extension",
};
}
}
else
{
return new ExternalResponse<FileStreamResponse>()
{
Status = ExternalResponseStatus.Error,
Message = $"No document with id ${mayanDocumentId} found in the storage",
};
}
}
else
{
throw GetMayanResponseError(documentResult.Message);
}
}

public async Task<ExternalResponse<QueryResponse<FilePageModel>>> GetDocumentFilePageListAsync(long documentId, long documentFileId)
{
this.Logger.LogInformation("Retrieving pages for document: {documentId} file: {documentFileId}", documentId, documentFileId);
Expand Down
5 changes: 5 additions & 0 deletions source/backend/api/Services/IDocumentService.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Pims.Api.Models;
using Pims.Api.Models.CodeTypes;

using Pims.Api.Models.Mayan;
Expand All @@ -27,8 +28,12 @@ public interface IDocumentService

Task<ExternalResponse<FileDownloadResponse>> DownloadFileAsync(long mayanDocumentId, long mayanFileId);

Task<ExternalResponse<FileStreamResponse>> StreamFileAsync(long mayanDocumentId, long mayanFileId);

Task<ExternalResponse<FileDownloadResponse>> DownloadFileLatestAsync(long mayanDocumentId);

Task<ExternalResponse<FileStreamResponse>> StreamFileLatestAsync(long mayanDocumentId);

IList<PimsDocumentTyp> GetPimsDocumentTypes();

IList<PimsDocumentTyp> GetPimsDocumentTypes(DocumentRelationType relationshipType);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System.IO;

namespace Pims.Api.Models
{
public class FileStreamResponse
{
/// <summary>
/// get/set - The file contents. Could be encoded.
/// </summary>
public Stream FilePayload { get; set; }

/// <summary>
/// get/set - The file size.
/// </summary>
public long Size { get; set; }

/// <summary>
/// get/set - Name of the file.
/// </summary>
public string FileName { get; set; }

/// <summary>
/// get/set - The extension of the file (pdf, docx, etc).
/// </summary>
public string FileNameExtension { get; set; }

/// <summary>
/// get/set - Complement of FileNameExtension.
/// </summary>
public string FileNameWithoutExtension { get; set; }

/// <summary>
/// get/set - The Mime-Type that the file uses.
/// </summary>
public string Mimetype { get; set; }

/// <summary>
/// get/set - The encoding type that the file uses.
/// </summary>
public string EncodingType { get; set; } = "base64";
}
}
Loading