Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: GAWR-5717 add pre-signed url endpoints to download files #1587

Merged
merged 6 commits into from
Jan 14, 2025
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
namespace RoadRegistry.BackOffice.Abstractions.Extracts;

public sealed record GetDownloadFilePreSignedUrlRequest(string DownloadId, int DefaultRetryAfter, int RetryAfterAverageWindowInDays) : EndpointRequest<GetDownloadFilePreSignedUrlResponse>, IEndpointRetryableRequest
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
namespace RoadRegistry.BackOffice.Abstractions.Extracts;

public sealed record GetDownloadFilePreSignedUrlResponse(string PreSignedUrl) : EndpointResponse
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
namespace RoadRegistry.BackOffice.Abstractions.Uploads;

public sealed record GetUploadFilePreSignedUrlRequest(string Identifier) : EndpointRequest<GetUploadFilePreSignedUrlResponse>
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
namespace RoadRegistry.BackOffice.Abstractions.Uploads;

public sealed record GetUploadFilePreSignedUrlResponse(string PreSignedUrl, string FileName) : EndpointResponse
{
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace RoadRegistry.BackOffice.Api.Extracts;

using System;
using System.Threading;
using System.Threading.Tasks;
using Abstractions.Extracts;
Expand All @@ -19,6 +20,7 @@ public partial class ExtractsController
/// of cancellation.
/// </param>
/// <returns>IActionResult.</returns>
[Obsolete("Use endpoint /grb/extracts/bycontour instead")]
[HttpPost(PostDownloadRequestRoute, Name = nameof(RequestDownload))]
[SwaggerOperation(OperationId = nameof(RequestDownload), Description = "")]
public async Task<IActionResult> RequestDownload([FromBody] DownloadExtractRequestBody body, CancellationToken cancellationToken)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
namespace RoadRegistry.BackOffice.Api.Extracts;

using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Abstractions;
using Abstractions.Exceptions;
using Abstractions.Extracts;
using Be.Vlaanderen.Basisregisters.BlobStore;
using Exceptions;
using Microsoft.AspNetCore.Mvc;
using Swashbuckle.AspNetCore.Annotations;

public partial class ExtractsController
{
private const string GetDownloadPreSignedUrlRoute = "download/{downloadId}/presignedurl";

/// <summary>
/// Gets the pre-signed url to download the extract.
/// </summary>
/// <param name="downloadId">The download identifier.</param>
/// <param name="options">The options.</param>
/// <param name="cancellationToken">
/// The cancellation token that can be used by other objects or threads to receive notice
/// of cancellation.
/// </param>
/// <returns>ActionResult.</returns>
[HttpGet(GetDownloadPreSignedUrlRoute, Name = nameof(GetDownloadPreSignedUrl))]
[SwaggerOperation(OperationId = nameof(GetDownloadPreSignedUrl), Description = "")]
public async Task<ActionResult> GetDownloadPreSignedUrl(
[FromRoute] string downloadId,
[FromServices] ExtractDownloadsOptions options,
CancellationToken cancellationToken)
{
try
{
var request = new GetDownloadFilePreSignedUrlRequest(downloadId, options.DefaultRetryAfter, options.RetryAfterAverageWindowInDays);
var response = await _mediator.Send(request, cancellationToken);
return new RedirectResult(response.PreSignedUrl, false);
}
catch (ExtractArchiveNotCreatedException)
{
return StatusCode((int)HttpStatusCode.Gone);
}
catch (BlobNotFoundException) // This condition can only occur if the blob no longer exists in the bucket
{
return StatusCode((int)HttpStatusCode.Gone);
}
catch (DownloadExtractNotFoundException exception)
{
AddHeaderRetryAfter(exception.RetryAfterSeconds);
return NotFound();
}
catch (ExtractDownloadNotFoundException exception)
{
AddHeaderRetryAfter(exception.RetryAfterSeconds);
return NotFound();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace RoadRegistry.BackOffice.Api.Extracts;

using System;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
Expand All @@ -23,6 +24,7 @@ public partial class ExtractsController
/// of cancellation.
/// </param>
/// <returns>IActionResult.</returns>
[Obsolete("Status of upload in new way is tracked using the ticketing service")]
[HttpGet(GetStatusRoute, Name = nameof(GetStatus))]
[SwaggerOperation(OperationId = nameof(GetStatus), Description = "")]
public async Task<IActionResult> GetStatus(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace RoadRegistry.BackOffice.Api.Extracts;

using System;
using Be.Vlaanderen.Basisregisters.Api.Exceptions;
using Infrastructure;
using Microsoft.AspNetCore.Http;
Expand All @@ -24,6 +25,7 @@ public partial class ExtractsController
/// <response code="200">Als de url is aangemaakt.</response>
/// <response code="400">Als de url is aangemaakt.</response>
/// <response code="500">Als er een interne fout is opgetreden.</response>
[Obsolete("Use endpoint /grb/download/{downloadId}/upload instead")]
[ProducesResponseType(typeof(GetPresignedUploadUrlResponse), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
[SwaggerResponseExample(StatusCodes.Status200OK, typeof(FileCallbackResultExamples))]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
namespace RoadRegistry.BackOffice.Api.Grb;

using System.Threading;
using System.Threading.Tasks;
using Abstractions.Extracts;
using Microsoft.AspNetCore.Mvc;
using Swashbuckle.AspNetCore.Annotations;

public partial class GrbController
{
private const string PostDownloadRequestRoute = "extracts/bycontour";

/// <summary>
/// Requests the download.
/// </summary>
/// <param name="body">The body.</param>
/// <param name="cancellationToken">
/// The cancellation token that can be used by other objects or threads to receive notice
/// of cancellation.
/// </param>
/// <returns>IActionResult.</returns>
[HttpPost(PostDownloadRequestRoute, Name = nameof(ExtractByContour))]
[SwaggerOperation(OperationId = nameof(ExtractByContour), Description = "")]
public async Task<IActionResult> ExtractByContour([FromBody] DownloadExtractRequestBody body, CancellationToken cancellationToken)
{
var isInformative = body.IsInformative ??
body.RequestId?.StartsWith("INF_")
?? false;

var request = new DownloadExtractRequest(body.RequestId, body.Contour, isInformative);
var response = await _mediator.Send(request, cancellationToken);
return Accepted(new DownloadExtractResponseBody(response.DownloadId, response.IsInformative));
}
}

public record DownloadExtractRequestBody(string Contour, string RequestId, bool? IsInformative);

public record DownloadExtractResponseBody(string DownloadId, bool IsInformative);
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
namespace RoadRegistry.BackOffice.Api.Grb;

using Be.Vlaanderen.Basisregisters.Api.Exceptions;
using Infrastructure;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Swashbuckle.AspNetCore.Annotations;
using Swashbuckle.AspNetCore.Filters;
using System.Threading;
using System.Threading.Tasks;
using Abstractions.Exceptions;
using Abstractions.Jobs;
using Editor.Schema;
using Exceptions;

public partial class GrbController
{
/// <summary>
/// Vraag een pre-signed url aan voor een zip van een extract download te uploaden.
/// </summary>
/// <param name="downloadId"></param>
/// <param name="editorContext"></param>
/// <param name="cancellationToken"></param>
/// <response code="200">Als de url is aangemaakt.</response>
/// <response code="400">Als de url is aangemaakt.</response>
/// <response code="500">Als er een interne fout is opgetreden.</response>
[ProducesResponseType(typeof(GetPresignedUploadUrlResponse), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
[SwaggerResponseExample(StatusCodes.Status200OK, typeof(FileCallbackResultExamples))]
[SwaggerResponseExample(StatusCodes.Status400BadRequest, typeof(BadRequestResponseExamples))]
[SwaggerResponseExample(StatusCodes.Status500InternalServerError, typeof(InternalServerErrorResponseExamples))]
[SwaggerOperation(OperationId = nameof(UploadForDownload), Description = "")]
[HttpPost("download/{downloadId}/upload", Name = nameof(UploadForDownload))]
public async Task<IActionResult> UploadForDownload(
[FromRoute] string downloadId,
[FromServices] EditorContext editorContext,
CancellationToken cancellationToken)
{
if (!DownloadId.TryParse(downloadId, out var parsedDownloadId))
{
throw new InvalidGuidValidationException("DownloadId");
}

var record = await editorContext.ExtractRequests.FindAsync(new object[] { parsedDownloadId.ToGuid() }, cancellationToken);
if (record is null)
{
throw new ExtractRequestNotFoundException(parsedDownloadId);
}

return Ok(await _mediator.Send(GetPresignedUploadUrlRequest.ForExtracts(parsedDownloadId), cancellationToken));
}
}
26 changes: 26 additions & 0 deletions src/RoadRegistry.BackOffice.Api/Grb/GrbController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
namespace RoadRegistry.BackOffice.Api.Grb;

using Asp.Versioning;
using Be.Vlaanderen.Basisregisters.Api;
using Be.Vlaanderen.Basisregisters.Auth.AcmIdm;
using Infrastructure.Authentication;
using Infrastructure.Controllers;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Version = Infrastructure.Version;

[ApiVersion(Version.Current)]
[AdvertiseApiVersions(Version.CurrentAdvertised)]
[ApiRoute("grb")]
[ApiExplorerSettings(GroupName = "Grb")]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.AllSchemes, Policy = PolicyNames.IngemetenWeg.Beheerder)]
public partial class GrbController : BackofficeApiController
{
private readonly IMediator _mediator;

public GrbController(IMediator mediator)
{
_mediator = mediator;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public partial class UploadController
private const string GetUploadRoute = "{identifier}";

/// <summary>
/// Download een aangevraagd extract
/// Download het geupload extract
/// </summary>
/// <param name="identifier">De identificator van het wegsegment.</param>
/// <param name="cancellationToken"></param>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
namespace RoadRegistry.BackOffice.Api.Uploads;

using System.Threading;
using System.Threading.Tasks;
using Abstractions.Uploads;
using Exceptions;
using Microsoft.AspNetCore.Mvc;
using Swashbuckle.AspNetCore.Annotations;

public partial class UploadController
{
private const string GetDownloadPreSignedUrlRoute = "{identifier}/presignedurl";

/// <summary>
/// Gets the pre-signed url to download the uploaded extract.
/// </summary>
/// <param name="identifier">De identificator van de upload.</param>
/// <param name="cancellationToken"></param>
/// <response code="200">Als de upload gevonden is.</response>
/// <response code="404">Als de upload niet gevonden kan worden.</response>
/// <response code="500">Als er een interne fout is opgetreden.</response>
[HttpGet(GetDownloadPreSignedUrlRoute, Name = nameof(GetDownloadPreSignedUrl))]
[SwaggerOperation(OperationId = nameof(GetDownloadPreSignedUrl), Description = "")]
public async Task<IActionResult> GetDownloadPreSignedUrl(string identifier, CancellationToken cancellationToken)
{
try
{
var request = new GetUploadFilePreSignedUrlRequest(identifier);
var response = await _mediator.Send(request, cancellationToken);

return Ok(new GetDownloadPreSignedUrlResponse
{
DownloadUrl = response.PreSignedUrl,
FileName = response.FileName
});
}
catch (ExtractDownloadNotFoundException)
{
return NotFound();
}
}

public class GetDownloadPreSignedUrlResponse
{
public string DownloadUrl { get; init; }
public string FileName { get; init; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@ public abstract class EndpointRetryableRequestHandler<TRequest, TResponse> : End
where TResponse : EndpointResponse
{
protected EditorContext Context { get; }
protected IClock Clock { get; }
private readonly IStreamStore _streamStore;
private readonly IClock _clock;

protected EndpointRetryableRequestHandler(CommandHandlerDispatcher dispatcher, EditorContext context, IStreamStore streamStore, IClock clock, ILogger logger) : base(dispatcher, logger)
protected EndpointRetryableRequestHandler(CommandHandlerDispatcher dispatcher, EditorContext context, IStreamStore streamStore, IClock clock, ILogger logger)
: base(dispatcher, logger)
{
Context = context ?? throw new ArgumentNullException(nameof(context));
Clock = clock ?? throw new ArgumentNullException(nameof(clock));
_streamStore = streamStore ?? throw new ArgumentNullException(nameof(streamStore));
_clock = clock ?? throw new ArgumentNullException(nameof(clock));
}

protected async Task<TimeSpan> CalculateRetryAfterAsync(IEndpointRetryableRequest request, CancellationToken cancellationToken)
Expand All @@ -46,7 +47,7 @@ protected async Task<TimeSpan> CalculateRetryAfterAsync(IEndpointRetryableReques
}

return await Context.ExtractUploads
.TookAverageProcessDuration(_clock
.TookAverageProcessDuration(Clock
.GetCurrentInstant()
.Minus(Duration.FromDays(request.RetryAfterAverageWindowInDays)), new TimeSpan(0, 0, request.DefaultRetryAfter));
}
Expand Down
Loading
Loading