2023-09-16 09:06:56 +00:00
using System.IO.Compression ;
using System.Net ;
using Elsa.Extensions ;
using Elsa.Http.Contracts ;
using Elsa.Http.Models ;
using Elsa.Workflows.Core ;
using Elsa.Workflows.Core.Attributes ;
using Elsa.Workflows.Core.Exceptions ;
using Elsa.Workflows.Core.Models ;
using Microsoft.AspNetCore.Http ;
2023-09-16 17:02:07 +00:00
using Microsoft.AspNetCore.Mvc ;
using Microsoft.AspNetCore.Mvc.Abstractions ;
using Microsoft.AspNetCore.Routing ;
2023-09-16 09:06:56 +00:00
using Microsoft.AspNetCore.StaticFiles ;
2023-09-16 17:02:07 +00:00
using Microsoft.Extensions.Logging ;
2023-09-16 09:06:56 +00:00
namespace Elsa.Http ;
/// <summary>
/// Sends a file to the HTTP response.
/// </summary>
[Activity("Elsa", "HTTP", "Send one ore more files (zipped) to the HTTP response.", DisplayName = "HTTP File Response")]
public class WriteFileHttpResponse : Activity
{
/// <summary>
/// The MIME type of the file to serve.
/// </summary>
[Input(Description = "The content type of the file to serve. Leave empty to let the system determine the content type.")]
public Input < string? > ContentType { get ; set ; } = default ! ;
/// <summary>
/// The name of the file to serve.
/// </summary>
[Input(Description = "The name of the file to serve. Leave empty to let the system determine the file name.")]
public Input < string? > FileName { get ; set ; } = default ! ;
/// <summary>
/// The file content to serve. Supports byte array, streams, string, Uri and an array of the aforementioned types.
/// </summary>
[Input(Description = "The file content to serve. Supports various types, such as byte array, stream, string, Uri, Downloadable and a (mixed) array of the aforementioned types.")]
public Input < object > Content { get ; set ; } = default ! ;
/// <inheritdoc />
protected override async ValueTask ExecuteAsync ( ActivityExecutionContext context )
{
var httpContextAccessor = context . GetRequiredService < IHttpContextAccessor > ( ) ;
var httpContext = httpContextAccessor . HttpContext ;
if ( httpContext = = null )
{
// We're executing in a non-HTTP context (e.g. in a virtual actor).
// Create a bookmark to allow the invoker to export the state and resume execution from there.
context . CreateBookmark ( OnResumeAsync , BookmarkMetadata . HttpCrossBoundary ) ;
return ;
}
2023-09-16 17:02:07 +00:00
await WriteResponseAsync ( context , httpContext ) ;
2023-09-16 09:06:56 +00:00
}
2023-09-16 17:02:07 +00:00
private async Task WriteResponseAsync ( ActivityExecutionContext context , HttpContext httpContext )
2023-09-16 09:06:56 +00:00
{
// Set status code.
var statusCode = HttpStatusCode . OK ;
2023-09-16 17:02:07 +00:00
var response = httpContext . Response ;
2023-09-16 09:06:56 +00:00
response . StatusCode = ( int ) statusCode ;
// Get content and content type.
var content = context . Get ( Content ) ;
if ( content = = null )
return ;
// Write content.
var downloadables = await GetDownloadablesAsync ( context , content ) ;
2023-09-16 17:02:07 +00:00
await SendDownloadablesAsync ( context , httpContext , downloadables ) ;
2023-09-16 09:06:56 +00:00
// Complete activity.
await context . CompleteActivityAsync ( ) ;
}
2023-09-16 17:02:07 +00:00
private async Task SendDownloadablesAsync ( ActivityExecutionContext context , HttpContext httpContext , IEnumerable < Downloadable > downloadables )
2023-09-16 09:06:56 +00:00
{
var downloadableList = downloadables . ToList ( ) ;
switch ( downloadableList . Count )
{
case 0 :
return ;
case 1 :
{
var downloadable = downloadableList [ 0 ] ;
2023-09-16 17:02:07 +00:00
await SendSingleFileAsync ( context , httpContext , downloadable ) ;
2023-09-16 09:06:56 +00:00
return ;
}
default :
2023-09-16 17:02:07 +00:00
await SendMultipleFilesAsync ( context , httpContext , downloadableList ) ;
2023-09-16 09:06:56 +00:00
break ;
}
}
2023-09-16 17:02:07 +00:00
private async Task SendSingleFileAsync ( ActivityExecutionContext context , HttpContext httpContext , Downloadable downloadable )
2023-09-16 09:06:56 +00:00
{
var contentType = ContentType . GetOrDefault ( context ) ;
var filename = FileName . GetOrDefault ( context ) ;
filename = ! string . IsNullOrWhiteSpace ( filename ) ? filename : ! string . IsNullOrWhiteSpace ( downloadable . Filename ) ? downloadable . Filename : "file.bin" ;
contentType = ! string . IsNullOrWhiteSpace ( contentType ) ? contentType : ! string . IsNullOrWhiteSpace ( downloadable . ContentType ) ? downloadable . ContentType : GetContentType ( context , filename ) ;
2023-09-16 17:02:07 +00:00
await SendFileStream ( httpContext , downloadable . Stream , contentType , filename ) ;
2023-09-16 09:06:56 +00:00
}
2023-09-16 17:02:07 +00:00
private async Task SendMultipleFilesAsync ( ActivityExecutionContext context , HttpContext httpContext , ICollection < Downloadable > downloadables )
2023-09-16 09:06:56 +00:00
{
2023-09-16 17:02:07 +00:00
var logger = context . GetRequiredService < ILogger < WriteFileHttpResponse > > ( ) ;
// 1. Create a temporary file
var tempFilePath = Path . GetTempFileName ( ) ;
2023-09-16 09:06:56 +00:00
var currentFileIndex = 0 ;
2023-09-16 17:02:07 +00:00
// 2. Write the zip archive to the temporary file
await using var tempFileStream = new FileStream ( tempFilePath , FileMode . Create ) ;
using var zipArchive = new ZipArchive ( tempFileStream , ZipArchiveMode . Create , true ) ;
foreach ( var downloadable in downloadables )
2023-09-16 09:06:56 +00:00
{
2023-09-16 17:02:07 +00:00
var entryName = ! string . IsNullOrWhiteSpace ( downloadable . Filename ) ? downloadable . Filename : $"file-{currentFileIndex}.bin" ;
var entry = zipArchive . CreateEntry ( entryName ) ;
var fileStream = downloadable . Stream ;
await using var entryStream = entry . Open ( ) ;
await fileStream . CopyToAsync ( entryStream ) ;
await entryStream . FlushAsync ( ) ;
entryStream . Close ( ) ;
currentFileIndex + + ;
2023-09-16 09:06:56 +00:00
}
2023-09-16 17:02:07 +00:00
2023-09-16 09:06:56 +00:00
var contentType = ContentType . GetOrDefault ( context ) ;
var filename = FileName . GetOrDefault ( context ) ;
contentType = ! string . IsNullOrWhiteSpace ( contentType ) ? contentType : System . Net . Mime . MediaTypeNames . Application . Zip ;
filename = ! string . IsNullOrWhiteSpace ( filename ) ? filename : "download.zip" ;
2023-09-16 17:02:07 +00:00
// 3. Use FileStreamResult to stream the temporary file back to the client
var resultStream = new FileStream ( tempFilePath , FileMode . Open , FileAccess . Read , FileShare . Read , bufferSize : 4096 , useAsync : true ) ;
await SendFileStream ( httpContext , resultStream , contentType , filename ) ;
// 4. Delete the temporary file.
try
{
File . Delete ( tempFilePath ) ;
}
catch ( Exception e )
{
logger . LogWarning ( e , "Failed to delete temporary file {TempFilePath}" , tempFilePath ) ;
}
}
2023-09-16 09:06:56 +00:00
2023-09-16 17:02:07 +00:00
private async Task SendFileStream ( HttpContext httpContext , Stream source , string contentType , string filename )
{
source . Seek ( 0 , SeekOrigin . Begin ) ;
var result = new FileStreamResult ( source , contentType )
{
EnableRangeProcessing = true ,
FileDownloadName = filename
} ;
var actionContext = new ActionContext ( httpContext , httpContext . GetRouteData ( ) , new ActionDescriptor ( ) ) ;
await result . ExecuteResultAsync ( actionContext ) ;
2023-09-16 09:06:56 +00:00
}
/// <summary>
/// Leverages the <see cref="IDownloadableManager"/> to get a list of <see cref="Downloadable"/> instances from the <paramref name="content"/>.
/// </summary>
private async Task < IEnumerable < Downloadable > > GetDownloadablesAsync ( ActivityExecutionContext context , object content )
{
var manager = context . GetRequiredService < IDownloadableManager > ( ) ;
return await manager . GetDownloadablesAsync ( content , context . CancellationToken ) ;
}
2023-09-16 17:02:07 +00:00
2023-09-16 09:06:56 +00:00
private string GetContentType ( ActivityExecutionContext context , string filename )
{
2023-09-16 09:43:34 +00:00
var provider = context . GetRequiredService < IContentTypeProvider > ( ) ;
return provider . TryGetContentType ( filename , out var contentType ) ? contentType : System . Net . Mime . MediaTypeNames . Application . Octet ;
}
private static string CreateContentDisposition ( string filename )
{
var contentDisposition = new System . Net . Mime . ContentDisposition
{
FileName = filename
} ;
2023-09-16 17:02:07 +00:00
2023-09-16 09:43:34 +00:00
return contentDisposition . ToString ( ) ;
2023-09-16 09:06:56 +00:00
}
private async ValueTask OnResumeAsync ( ActivityExecutionContext context )
{
var httpContextAccessor = context . GetRequiredService < IHttpContextAccessor > ( ) ;
var httpContext = httpContextAccessor . HttpContext ;
if ( httpContext = = null )
{
// We're not in an HTTP context, so let's fail.
throw new FaultException ( "Cannot execute in a non-HTTP context" ) ;
}
2023-09-16 17:02:07 +00:00
await WriteResponseAsync ( context , httpContext ) ;
2023-09-16 09:06:56 +00:00
}
}