Skip to content

Commit

Permalink
Add download handler (#22)
Browse files Browse the repository at this point in the history
* Add download handler

* Update blob name for jobs

also add missing variables for integration test

* Update download

Also ensure that the func list method only updates status of non job reports

* Mark failed status
  • Loading branch information
FirestarJes authored Sep 16, 2024
1 parent c4cc8a8 commit d9e97ce
Show file tree
Hide file tree
Showing 13 changed files with 194 additions and 50 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,13 @@ private FunctionAppHostSettings CreateAppHostSettings(ref int port)
$"{SettlementReportStorageOptions.SectionName}__{nameof(SettlementReportStorageOptions.StorageAccountUri)}",
AzuriteManager.BlobStorageServiceUri + "/");

appHostSettings.ProcessEnvironmentVariables.Add(
$"{SettlementReportStorageOptions.SectionName}__{nameof(SettlementReportStorageOptions.StorageContainerForJobsName)}",
"settlement-report-container-jobs");
appHostSettings.ProcessEnvironmentVariables.Add(
$"{SettlementReportStorageOptions.SectionName}__{nameof(SettlementReportStorageOptions.StorageAccountForJobsUri)}",
AzuriteManager.BlobStorageServiceUri + "/");

return appHostSettings;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ private async Task<IEnumerable<RequestedSettlementReportDto>> CheckStatusOfSettl
{
var updatedReport = settlementReport;

if (settlementReport.Status == SettlementReportStatus.InProgress && settlementReport.RequestId != null)
if (settlementReport.Status == SettlementReportStatus.InProgress && settlementReport.JobId == null)
{
var instanceInfo = await durableTaskClient
.GetInstanceAsync(settlementReport.RequestId.Id, getInputsAndOutputs: true)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright 2020 Energinet DataHub A/S
//
// Licensed under the Apache License, Version 2.0 (the "License2");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using Energinet.DataHub.SettlementReport.Interfaces.SettlementReports_v2.Models;

namespace Energinet.DataHub.SettlementReport.Application.Handlers;

public interface ISettlementReportJobsDownloadHandler
{
Task DownloadReportAsync(SettlementReportRequestId requestId, Func<Stream> outputStreamProvider, Guid actorId, bool isMultitenancy);
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,14 @@ private async Task<IEnumerable<RequestedSettlementReportDto>> GetSettlementRepor
{
var jobStatus = await _jobHelper.GetSettlementReportsJobStatusAsync(settlementReportDto.JobId!.Id)
.ConfigureAwait(false);
if (jobStatus == JobRunStatus.Completed)
switch (jobStatus)
{
await MarkAsCompletedAsync(settlementReportDto).ConfigureAwait(false);
case JobRunStatus.Completed:
await MarkAsCompletedAsync(settlementReportDto).ConfigureAwait(false);
break;
case JobRunStatus.Canceled or JobRunStatus.Failed:
await MarkAsFailedAsync(settlementReportDto).ConfigureAwait(false);
break;
}

results.Add(settlementReportDto with { Status = MapFromJobStatus(jobStatus) });
Expand All @@ -85,7 +90,23 @@ private async Task MarkAsCompletedAsync(RequestedSettlementReportDto settlementR
.GetAsync(settlementReportDto.JobId.Id)
.ConfigureAwait(false);

request.MarkAsCompleted(_clock, settlementReportDto.JobId);
request.MarkAsCompleted(_clock, settlementReportDto.RequestId);

await _repository
.AddOrUpdateAsync(request)
.ConfigureAwait(false);
}

private async Task MarkAsFailedAsync(RequestedSettlementReportDto settlementReportDto)
{
ArgumentNullException.ThrowIfNull(settlementReportDto);
ArgumentNullException.ThrowIfNull(settlementReportDto.JobId);

var request = await _repository
.GetAsync(settlementReportDto.JobId.Id)
.ConfigureAwait(false);

request.MarkAsFailed();

await _repository
.AddOrUpdateAsync(request)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright 2020 Energinet DataHub A/S
//
// Licensed under the Apache License, Version 2.0 (the "License2");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using Energinet.DataHub.SettlementReport.Application.SettlementReports_v2;
using Energinet.DataHub.SettlementReport.Interfaces.SettlementReports_v2.Models;

namespace Energinet.DataHub.SettlementReport.Application.Handlers;

public sealed class SettlementReportJobsDownloadHandler : ISettlementReportJobsDownloadHandler
{
private readonly ISettlementReportJobsFileRepository _fileRepository;
private readonly ISettlementReportRepository _repository;

public SettlementReportJobsDownloadHandler(
ISettlementReportJobsFileRepository fileRepository,
ISettlementReportRepository repository)
{
_fileRepository = fileRepository;
_repository = repository;
}

public async Task DownloadReportAsync(
SettlementReportRequestId requestId,
Func<Stream> outputStreamProvider,
Guid actorId,
bool isMultitenancy)
{
var report = await _repository
.GetAsync(requestId.Id)
.ConfigureAwait(false) ?? throw new InvalidOperationException("Report not found.");

if (!isMultitenancy && (report.ActorId != actorId || report.IsHiddenFromActor))
{
throw new InvalidOperationException("User does not have access to the report.");
}

if (string.IsNullOrEmpty(report.BlobFileName))
throw new InvalidOperationException("Report does not have a Blob file name.");

await _fileRepository
.DownloadAsync(report.BlobFileName, outputStreamProvider())
.ConfigureAwait(false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,5 @@ namespace Energinet.DataHub.SettlementReport.Application.SettlementReports_v2;

public interface ISettlementReportJobsFileRepository
{
Task DownloadAsync(JobRunId jobRunId, string fileName, Stream downloadStream);

Task DeleteAsync(JobRunId reportRequestId, string fileName);
Task DownloadAsync(string blobFileName, Stream downloadStream);
}
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,10 @@ public void MarkAsCompleted(IClock clock, GeneratedSettlementReportDto generated
EndedDateTime = clock.GetCurrentInstant();
}

public void MarkAsCompleted(IClock clock, JobRunId jobRunId)
public void MarkAsCompleted(IClock clock, SettlementReportRequestId requestId)
{
Status = SettlementReportStatus.Completed;
BlobFileName = jobRunId.Id.ToString();
BlobFileName = requestId.Id + ".zip";
EndedDateTime = clock.GetCurrentInstant();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,25 +45,43 @@ public static IServiceCollection AddSettlementReportBlobStorage(this IServiceCol
{
var blobSettings = serviceProvider.GetRequiredService<IOptions<SettlementReportStorageOptions>>().Value;

var blobContainerUri = new Uri(blobSettings.StorageAccountUri, blobSettings.StorageContainerName);
var blobContainerUri = new Uri(blobSettings.StorageAccountForJobsUri, blobSettings.StorageContainerForJobsName);
var blobContainerClient = new BlobContainerClient(blobContainerUri, new DefaultAzureCredential());

return new SettlementReportJobsFileBlobStorage(blobContainerClient);
});

// Health checks
services.AddHealthChecks().AddAzureBlobStorage(
serviceProvider =>
{
var blobSettings = serviceProvider.GetRequiredService<IOptions<SettlementReportStorageOptions>>().Value;
return new BlobServiceClient(blobSettings.StorageAccountUri, new DefaultAzureCredential());
},
(serviceProvider, options) =>
{
var blobSettings = serviceProvider.GetRequiredService<IOptions<SettlementReportStorageOptions>>().Value;
options.ContainerName = blobSettings.StorageContainerName;
},
"SettlementReportBlobStorage");
services
.AddHealthChecks()
.AddAzureBlobStorage(
serviceProvider =>
{
var blobSettings = serviceProvider.GetRequiredService<IOptions<SettlementReportStorageOptions>>()
.Value;
return new BlobServiceClient(blobSettings.StorageAccountUri, new DefaultAzureCredential());
},
(serviceProvider, options) =>
{
var blobSettings = serviceProvider.GetRequiredService<IOptions<SettlementReportStorageOptions>>()
.Value;
options.ContainerName = blobSettings.StorageContainerName;
},
"SettlementReportBlobStorage")
.AddAzureBlobStorage(
serviceProvider =>
{
var blobSettings = serviceProvider.GetRequiredService<IOptions<SettlementReportStorageOptions>>()
.Value;
return new BlobServiceClient(blobSettings.StorageAccountForJobsUri, new DefaultAzureCredential());
},
(serviceProvider, options) =>
{
var blobSettings = serviceProvider.GetRequiredService<IOptions<SettlementReportStorageOptions>>()
.Value;
options.ContainerName = blobSettings.StorageContainerForJobsName;
},
"SettlementReportBlobStorageJobs");

return services;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,10 @@ public class SettlementReportStorageOptions

[Required]
public string StorageContainerName { get; set; } = string.Empty;

[Required]
public Uri StorageAccountForJobsUri { get; set; } = null!;

[Required]
public string StorageContainerForJobsName { get; set; } = string.Empty;
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,24 +47,14 @@ public async Task RemoveExpiredAsync(IList<Application.SettlementReports_v2.Sett
if (!IsExpired(settlementReport))
continue;

if (settlementReport.BlobFileName != null)
// Delete the blob file if it exists and the job id is null, because on the shared blob storage we use retention to clean-up
if (settlementReport is { BlobFileName: not null, JobId: null })
{
if (settlementReport.JobId is not null)
{
await _settlementReportJobFileRepository
.DeleteAsync(
new JobRunId(settlementReport.JobId.GetValueOrDefault()),
settlementReport.BlobFileName)
.ConfigureAwait(false);
}
else
{
await _settlementReportFileRepository
.DeleteAsync(
new SettlementReportRequestId(settlementReport.RequestId),
settlementReport.BlobFileName)
.ConfigureAwait(false);
}
await _settlementReportFileRepository
.DeleteAsync(
new SettlementReportRequestId(settlementReport.RequestId),
settlementReport.BlobFileName)
.ConfigureAwait(false);
}

await _settlementReportRepository
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@

using Azure.Storage.Blobs;
using Energinet.DataHub.SettlementReport.Application.SettlementReports_v2;
using Energinet.DataHub.SettlementReport.Interfaces.SettlementReports_v2.Models;

namespace Energinet.DataHub.SettlementReport.Infrastructure.SettlementReports_v2;

Expand All @@ -27,20 +26,15 @@ public SettlementReportJobsFileBlobStorage(BlobContainerClient blobContainerClie
_blobContainerClient = blobContainerClient;
}

public async Task DownloadAsync(JobRunId jobRunId, string fileName, Stream downloadStream)
public async Task DownloadAsync(string fileName, Stream downloadStream)
{
var blobName = GetBlobName(jobRunId, fileName);
var blobName = GetBlobName(fileName);
var blobClient = _blobContainerClient.GetBlobClient(blobName);
await blobClient.DownloadToAsync(downloadStream).ConfigureAwait(false);
}

public Task DeleteAsync(JobRunId reportRequestId, string fileName)
private static string GetBlobName(string fileName)
{
return Task.CompletedTask;
}

private static string GetBlobName(JobRunId jobRunId, string fileName)
{
return string.Join('/', "settlement-reports", "reports", jobRunId.Id, fileName);
return string.Join('/', "reports", fileName);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.

using System.Net;
using System.Net.Mime;
using Azure;
using Energinet.DataHub.Core.App.Common.Abstractions.Users;
using Energinet.DataHub.SettlementReport.Application.Commands;
using Energinet.DataHub.SettlementReport.Application.Handlers;
Expand All @@ -30,16 +33,19 @@ public class SettlementReportsController
{
private readonly IRequestSettlementReportJobHandler _requestSettlementReportJobHandler;
private readonly IListSettlementReportJobsHandler _listSettlementReportJobsHandler;
private readonly ISettlementReportJobsDownloadHandler _downloadHandler;
private readonly IUserContext<FrontendUser> _userContext;

public SettlementReportsController(
IRequestSettlementReportJobHandler requestSettlementReportJobHandler,
IUserContext<FrontendUser> userContext,
IListSettlementReportJobsHandler listSettlementReportJobsHandler)
IListSettlementReportJobsHandler listSettlementReportJobsHandler,
ISettlementReportJobsDownloadHandler downloadHandler)
{
_requestSettlementReportJobHandler = requestSettlementReportJobHandler;
_userContext = userContext;
_listSettlementReportJobsHandler = listSettlementReportJobsHandler;
_downloadHandler = downloadHandler;
}

[HttpPost]
Expand Down Expand Up @@ -107,6 +113,32 @@ public async Task<IEnumerable<RequestedSettlementReportDto>> ListSettlementRepor
return await _listSettlementReportJobsHandler.HandleAsync(_userContext.CurrentUser.Actor.ActorId).ConfigureAwait(false);
}

[HttpGet]
[Route("download")]
[Authorize]
[Produces("application/octet-stream")]
[ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)]
public async Task<ActionResult> DownloadFileAsync(SettlementReportRequestId requestId)
{
try
{
var stream = new MemoryStream();
await _downloadHandler
.DownloadReportAsync(
requestId,
() => stream,
_userContext.CurrentUser.Actor.ActorId,
_userContext.CurrentUser.MultiTenancy)
.ConfigureAwait(false);

return File(stream.GetBuffer(), MediaTypeNames.Application.Zip);
}
catch (Exception ex) when (ex is InvalidOperationException or RequestFailedException)
{
return NotFound();
}
}

private bool IsValid(SettlementReportRequestDto req)
{
if (_userContext.CurrentUser.MultiTenancy)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public static IServiceCollection AddSettlementReportApiModule(this IServiceColle
services.AddScoped<ISettlementReportInitializeHandler, SettlementReportInitializeHandler>();
services.AddScoped<IListSettlementReportJobsHandler, ListSettlementReportJobsHandler>();
services.AddScoped<IRequestSettlementReportJobHandler, RequestSettlementReportHandler>();
services.AddScoped<ISettlementReportJobsDownloadHandler, SettlementReportJobsDownloadHandler>();
services.AddSettlementReportBlobStorage();

// Database Health check
Expand Down

0 comments on commit d9e97ce

Please sign in to comment.