Add byte array converter and enhance HTTP file download
A new ByteArrayConverter class has been added to Elsa.JavaScript that converts byte arrays to Uint8Arrays. In Elsa.Http, the DownloadHttpFile activity now also stores the downloaded file's content in bytes. In addition, various new type aliases have been added to the Elsa.Http and Elsa.JavaScript modules, making it easier to handle non-string responses and the transfer of data.
This commit is contained in:
parent
43432cb70d
commit
615f30f0eb
|
|
@ -46,7 +46,7 @@ public class DownloadHttpFile : Activity<HttpFile>, IActivityPropertyDefaultValu
|
|||
UIHint = InputUIHints.DropDown
|
||||
)]
|
||||
public Input<string> Method { get; set; } = new("GET");
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A list of expected status codes to handle.
|
||||
/// </summary>
|
||||
|
|
@ -102,7 +102,7 @@ public class DownloadHttpFile : Activity<HttpFile>, IActivityPropertyDefaultValu
|
|||
/// </summary>
|
||||
[Output(IsSerializable = false)]
|
||||
public Output<HttpResponseMessage> Response { get; set; } = default!;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The HTTP response status code
|
||||
/// </summary>
|
||||
|
|
@ -110,17 +110,23 @@ public class DownloadHttpFile : Activity<HttpFile>, IActivityPropertyDefaultValu
|
|||
public Output<int> StatusCode { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The parsed content, if any.
|
||||
/// The downloaded content stream, if any.
|
||||
/// </summary>
|
||||
[Output(Description = "The parsed content, if any.", IsSerializable = false)]
|
||||
public Output<Stream?> ResponseContent { get; set; } = default!;
|
||||
[Output(Description = "The downloaded content stream, if any.", IsSerializable = false)]
|
||||
public Output<Stream?> ResponseContentStream { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The downloaded content bytes, if any.
|
||||
/// </summary>
|
||||
[Output(Description = "The downloaded content bytes, if any.", IsSerializable = false)]
|
||||
public Output<byte[]?> ResponseContentBytes { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The response headers that were received.
|
||||
/// </summary>
|
||||
[Output(Description = "The response headers that were received.")]
|
||||
public Output<HttpHeaders?> ResponseHeaders { get; set; } = default!;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The response content headers that were received.
|
||||
/// </summary>
|
||||
|
|
@ -150,11 +156,12 @@ public class DownloadHttpFile : Activity<HttpFile>, IActivityPropertyDefaultValu
|
|||
var responseContentHeaders = new HttpHeaders(response.Content.Headers);
|
||||
|
||||
context.Set(Response, response);
|
||||
context.Set(ResponseContent, file?.Stream);
|
||||
context.Set(ResponseContentStream, file?.Stream);
|
||||
context.Set(Result, file);
|
||||
context.Set(StatusCode, statusCode);
|
||||
context.Set(ResponseHeaders, responseHeaders);
|
||||
context.Set(ResponseContentHeaders, responseContentHeaders);
|
||||
if (ResponseContentBytes.HasTarget(context)) context.Set(ResponseContentBytes, file?.GetBytes());
|
||||
|
||||
await HandleResponseAsync(context, response);
|
||||
}
|
||||
|
|
@ -179,7 +186,7 @@ public class DownloadHttpFile : Activity<HttpFile>, IActivityPropertyDefaultValu
|
|||
await HandleTaskCanceledExceptionAsync(context, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Handles the response.
|
||||
/// </summary>
|
||||
|
|
@ -272,11 +279,14 @@ public class DownloadHttpFile : Activity<HttpFile>, IActivityPropertyDefaultValu
|
|||
var parsedContentType = new System.Net.Mime.ContentType(contentType);
|
||||
return factories.FirstOrDefault(httpContentFactory => httpContentFactory.SupportedContentTypes.Any(c => c == parsedContentType.MediaType)) ?? new JsonContentFactory();
|
||||
}
|
||||
|
||||
|
||||
object IActivityPropertyDefaultValueProvider.GetDefaultValue(PropertyInfo property)
|
||||
{
|
||||
if (property.Name == nameof(ExpectedStatusCodes))
|
||||
return new List<int> { 200 };
|
||||
return new List<int>
|
||||
{
|
||||
200
|
||||
};
|
||||
|
||||
return default!;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ namespace Elsa.Http.DownloadableContentHandlers;
|
|||
public class MultiDownloadableContentHandler : DownloadableContentHandlerBase
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override bool GetSupportsContent(object content) => content is IEnumerable enumerable and not string;
|
||||
public override bool GetSupportsContent(object content) => content is IEnumerable and not string and not byte[];
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override IEnumerable<Func<ValueTask<Downloadable>>> GetDownloadablesAsync(DownloadableContext context)
|
||||
|
|
|
|||
|
|
@ -219,6 +219,10 @@ public class HttpFeature : FeatureBase
|
|||
{
|
||||
options.AddTypeAlias<IFormFile>("FormFile");
|
||||
options.AddTypeAlias<IFormFile[]>("FormFile[]");
|
||||
options.AddTypeAlias<HttpFile>("HttpFile");
|
||||
options.AddTypeAlias<HttpFile[]>("HttpFile[]");
|
||||
options.AddTypeAlias<Downloadable>("Downloadable");
|
||||
options.AddTypeAlias<Downloadable[]>("Downloadable[]");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using Jint;
|
||||
using Jint.Native;
|
||||
using Jint.Runtime.Interop;
|
||||
|
||||
namespace Elsa.JavaScript.ObjectConverters;
|
||||
|
||||
/// <summary>
|
||||
/// Converts a byte array to a <see cref="JsValue"/> instance representing a Uint8Array.
|
||||
/// </summary>
|
||||
internal class ByteArrayConverter : IObjectConverter
|
||||
{
|
||||
public bool TryConvert(Engine engine, object value, [NotNullWhen(true)] out JsValue? result)
|
||||
{
|
||||
if (value is byte[] bytes)
|
||||
{
|
||||
// TODO: Temporary: Uint8Array creates a copy of the byte array. Instead, we want to create a view or a buffer referencing the byte array.
|
||||
// See also: https://github.com/sebastienros/jint/pull/1590
|
||||
result = engine.Intrinsics.Uint8Array.Construct(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
result = JsValue.Null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -12,10 +12,13 @@ using Elsa.Extensions;
|
|||
using Elsa.JavaScript.Contracts;
|
||||
using Elsa.JavaScript.Helpers;
|
||||
using Elsa.JavaScript.Notifications;
|
||||
using Elsa.JavaScript.ObjectConverters;
|
||||
using Elsa.JavaScript.Options;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Humanizer;
|
||||
using Jint;
|
||||
using Jint.Native;
|
||||
using Jint.Native.TypedArray;
|
||||
using Jint.Runtime.Interop;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
|
@ -27,23 +30,11 @@ namespace Elsa.JavaScript.Services;
|
|||
/// <summary>
|
||||
/// Provides a JavaScript evaluator using Jint.
|
||||
/// </summary>
|
||||
public class JintJavaScriptEvaluator : IJavaScriptEvaluator
|
||||
public class JintJavaScriptEvaluator(IConfiguration configuration, INotificationSender mediator, IOptions<JintOptions> scriptOptions, IMemoryCache memoryCache)
|
||||
: IJavaScriptEvaluator
|
||||
{
|
||||
private readonly INotificationSender _mediator;
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
private readonly JintOptions _jintOptions;
|
||||
private readonly IConfiguration _configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor.
|
||||
/// </summary>
|
||||
public JintJavaScriptEvaluator(IConfiguration configuration, INotificationSender mediator, IOptions<JintOptions> scriptOptions, IMemoryCache memoryCache)
|
||||
{
|
||||
_mediator = mediator;
|
||||
_memoryCache = memoryCache;
|
||||
_jintOptions = scriptOptions.Value;
|
||||
_configuration = configuration;
|
||||
}
|
||||
private readonly JintOptions _jintOptions = scriptOptions.Value;
|
||||
private readonly JsonSerializerOptions _jsonSerializerOptions = CreateJsonSerializerOptions();
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<object?> EvaluateAsync(string expression,
|
||||
|
|
@ -71,13 +62,15 @@ public class JintJavaScriptEvaluator : IJavaScriptEvaluator
|
|||
// Wrap objects in ObjectWrapper instances and set their prototype to Array.prototype if they are array-like.
|
||||
opts.SetWrapObjectHandler((engine, target, type) =>
|
||||
{
|
||||
var instance = new ObjectWrapper(engine, target);
|
||||
var instance = ObjectWrapper.Create(engine, target);
|
||||
|
||||
if (ObjectArrayHelper.DetermineIfObjectIsArrayLikeClrCollection(target.GetType()))
|
||||
instance.Prototype = engine.Intrinsics.Array.PrototypeObject;
|
||||
|
||||
return instance;
|
||||
});
|
||||
|
||||
opts.Interop.ObjectConverters.Add(new ByteArrayConverter());
|
||||
});
|
||||
|
||||
configureEngine?.Invoke(engine);
|
||||
|
|
@ -123,7 +116,7 @@ public class JintJavaScriptEvaluator : IJavaScriptEvaluator
|
|||
|
||||
// Create configuration value accessor
|
||||
if (_jintOptions.AllowConfigurationAccess)
|
||||
engine.SetValue("getConfig", (Func<string, object?>)(name => _configuration.GetSection(name).Value));
|
||||
engine.SetValue("getConfig", (Func<string, object?>)(name => configuration.GetSection(name).Value));
|
||||
|
||||
// Add common .NET types.
|
||||
engine.RegisterType<DateTime>();
|
||||
|
|
@ -135,7 +128,7 @@ public class JintJavaScriptEvaluator : IJavaScriptEvaluator
|
|||
_jintOptions.ConfigureEngineCallback(engine, context);
|
||||
|
||||
// Allow listeners invoked by the mediator to configure the engine.
|
||||
await _mediator.SendAsync(new EvaluatingJavaScript(engine, context), cancellationToken);
|
||||
await mediator.SendAsync(new EvaluatingJavaScript(engine, context), cancellationToken);
|
||||
|
||||
return engine;
|
||||
}
|
||||
|
|
@ -159,6 +152,7 @@ public class JintJavaScriptEvaluator : IJavaScriptEvaluator
|
|||
}
|
||||
}
|
||||
|
||||
[RequiresUnreferencedCode("Calls Jint.Engine.SetValue<T>(String, T)")]
|
||||
private static void CreateVariableAccessors(Engine engine, ExpressionExecutionContext context)
|
||||
{
|
||||
var variableNames = context.GetVariableNamesInScope().ToList();
|
||||
|
|
@ -184,7 +178,7 @@ public class JintJavaScriptEvaluator : IJavaScriptEvaluator
|
|||
{
|
||||
var cacheKey = "jint:script:" + Hash(expression);
|
||||
|
||||
var parsedScript = _memoryCache.GetOrCreate(cacheKey, entry =>
|
||||
var parsedScript = memoryCache.GetOrCreate(cacheKey, entry =>
|
||||
{
|
||||
if (_jintOptions.ScriptCacheTimeout.HasValue)
|
||||
entry.SetAbsoluteExpiration(_jintOptions.ScriptCacheTimeout.Value);
|
||||
|
|
@ -204,15 +198,19 @@ public class JintJavaScriptEvaluator : IJavaScriptEvaluator
|
|||
}
|
||||
|
||||
[RequiresUnreferencedCode("Calls System.Text.Json.JsonSerializer.Serialize<TValue>(TValue, JsonSerializerOptions)")]
|
||||
private static string Serialize(object value)
|
||||
private string Serialize(object value)
|
||||
{
|
||||
return JsonSerializer.Serialize(value, _jsonSerializerOptions);
|
||||
}
|
||||
|
||||
private static JsonSerializerOptions CreateJsonSerializerOptions()
|
||||
{
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
Encoder = JavaScriptEncoder.Create(UnicodeRanges.All)
|
||||
};
|
||||
options.Converters.Add(new JsonStringEnumConverter());
|
||||
|
||||
return JsonSerializer.Serialize(value, options);
|
||||
return options;
|
||||
}
|
||||
|
||||
private string Hash(string input)
|
||||
|
|
|
|||
|
|
@ -24,10 +24,14 @@ public class TypeAliasRegistry : ITypeAliasRegistry
|
|||
this.RegisterType<decimal>("Decimal");
|
||||
this.RegisterType<float>("Single");
|
||||
this.RegisterType<double>("Double");
|
||||
this.RegisterType<byte[]>("Buffer");
|
||||
this.RegisterType<Stream>("Stream");
|
||||
this.RegisterType<Guid>("Guid");
|
||||
this.RegisterType<DateTime>("Date");
|
||||
this.RegisterType<DateTimeOffset>("Date");
|
||||
this.RegisterType<DateOnly>("Date");
|
||||
this.RegisterType<TimeOnly>("Date");
|
||||
this.RegisterType<IDictionary<string, object>>("ObjectDictionary");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
|
|||
|
|
@ -53,4 +53,13 @@ public static class OutputExtensions
|
|||
var parsedContentVariableType = (memoryBlock.Metadata as VariableBlockMetadata)?.Variable.GetType();
|
||||
return parsedContentVariableType?.GenericTypeArguments.FirstOrDefault();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a value indicating whether the output has a target.
|
||||
/// </summary>
|
||||
public static bool HasTarget(this Output? output, ActivityExecutionContext context)
|
||||
{
|
||||
var memoryBlockReference = output?.MemoryBlockReference();
|
||||
return memoryBlockReference is not null && context.ExpressionExecutionContext.TryGetBlock(memoryBlockReference, out _);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue