231 lines
7.6 KiB
C#
231 lines
7.6 KiB
C#
|
|
using System.Net.Http.Headers;
|
||
|
|
using System.Text;
|
||
|
|
using System.Text.Json.Nodes;
|
||
|
|
using w4c_workflows.Models.Credentials;
|
||
|
|
using w4c_workflows.Models.Nodes;
|
||
|
|
using w4c_workflows.Services.Credentials;
|
||
|
|
using w4c_workflows.Services.Nodes.Binary;
|
||
|
|
using w4c_workflows.Services.Security;
|
||
|
|
|
||
|
|
namespace w4c_workflows.Services.Nodes.Executors;
|
||
|
|
|
||
|
|
// Response half of the HTTP Request node: maps a page body to output items,
|
||
|
|
// including the file/binary case and the envelope-unwrapping JSON path.
|
||
|
|
|
||
|
|
public sealed partial class HttpRequestNodeExecutor
|
||
|
|
{
|
||
|
|
// ------------------------------------------------------------------ response
|
||
|
|
|
||
|
|
private sealed record BuildResult(List<FlowItem> Items, string? Error);
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Maps one page's body to items. <c>responseFormat: file</c> is the only
|
||
|
|
/// asynchronous case: it stores the payload and emits a binary-backed item.
|
||
|
|
/// </summary>
|
||
|
|
private async Task<BuildResult> BuildItemsAsync(
|
||
|
|
PageResult page,
|
||
|
|
string responseFormat,
|
||
|
|
string? putOutputInField,
|
||
|
|
string? outputPath,
|
||
|
|
CancellationToken ct)
|
||
|
|
{
|
||
|
|
if (responseFormat == "file")
|
||
|
|
{
|
||
|
|
BinaryAttachment attachment;
|
||
|
|
try
|
||
|
|
{
|
||
|
|
await using var content = new MemoryStream(page.Bytes, writable: false);
|
||
|
|
attachment = await _binary.SaveAsync(content, page.SuggestedFileName, page.ContentType, ct);
|
||
|
|
}
|
||
|
|
catch (OperationCanceledException)
|
||
|
|
{
|
||
|
|
throw;
|
||
|
|
}
|
||
|
|
catch (Exception ex) when (ex is InvalidOperationException or IOException)
|
||
|
|
{
|
||
|
|
return new BuildResult(new List<FlowItem>(), ex.Message);
|
||
|
|
}
|
||
|
|
|
||
|
|
return new BuildResult(new List<FlowItem> { FileItem(attachment, putOutputInField) }, null);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!TryBuildItems(
|
||
|
|
page.Body, page.Parsed, responseFormat, putOutputInField, outputPath,
|
||
|
|
out var items, out var error))
|
||
|
|
{
|
||
|
|
return new BuildResult(new List<FlowItem>(), error);
|
||
|
|
}
|
||
|
|
|
||
|
|
return new BuildResult(items, null);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>Builds a one-item result whose payload lives in the binary store.</summary>
|
||
|
|
private static FlowItem FileItem(BinaryAttachment attachment, string? putOutputInField)
|
||
|
|
{
|
||
|
|
var meta = new JsonObject
|
||
|
|
{
|
||
|
|
["fileName"] = attachment.FileName,
|
||
|
|
["mimeType"] = attachment.MimeType,
|
||
|
|
["sizeBytes"] = attachment.SizeBytes,
|
||
|
|
};
|
||
|
|
|
||
|
|
var extension = Path.GetExtension(attachment.FileName);
|
||
|
|
if (!string.IsNullOrEmpty(extension))
|
||
|
|
meta["fileExtension"] = extension.TrimStart('.');
|
||
|
|
|
||
|
|
return new FlowItem
|
||
|
|
{
|
||
|
|
Json = string.IsNullOrWhiteSpace(putOutputInField)
|
||
|
|
? meta
|
||
|
|
: new JsonObject { [putOutputInField!] = meta },
|
||
|
|
Binary = new Dictionary<string, BinaryAttachment>(StringComparer.Ordinal)
|
||
|
|
{
|
||
|
|
[DefaultBinaryProperty] = attachment,
|
||
|
|
},
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// File name for a downloaded response: the Content-Disposition name, else the
|
||
|
|
/// last URL path segment, else null. Directory separators are stripped so a
|
||
|
|
/// provider cannot smuggle a path into the stored reference.
|
||
|
|
/// </summary>
|
||
|
|
private static string? ResolveFileName(HttpResponseMessage response, Uri requestUri)
|
||
|
|
{
|
||
|
|
try
|
||
|
|
{
|
||
|
|
var disposition = response.Content.Headers.ContentDisposition;
|
||
|
|
var name = TrimName(disposition?.FileNameStar ?? disposition?.FileName);
|
||
|
|
if (name != null)
|
||
|
|
return name;
|
||
|
|
}
|
||
|
|
catch (FormatException)
|
||
|
|
{
|
||
|
|
// Malformed Content-Disposition: fall through to the URL segment.
|
||
|
|
}
|
||
|
|
|
||
|
|
if (requestUri.Segments.Length > 0)
|
||
|
|
{
|
||
|
|
var name = TrimName(Uri.UnescapeDataString(requestUri.Segments[^1]));
|
||
|
|
if (name != null)
|
||
|
|
return name;
|
||
|
|
}
|
||
|
|
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static string? TrimName(string? name)
|
||
|
|
{
|
||
|
|
if (string.IsNullOrWhiteSpace(name))
|
||
|
|
return null;
|
||
|
|
|
||
|
|
var trimmed = name.Trim().Trim('"');
|
||
|
|
var separator = trimmed.LastIndexOfAny(new[] { '/', '\\' });
|
||
|
|
if (separator >= 0)
|
||
|
|
trimmed = trimmed[(separator + 1)..];
|
||
|
|
return string.IsNullOrWhiteSpace(trimmed) ? null : trimmed;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static bool TryBuildItems(
|
||
|
|
string body,
|
||
|
|
JsonNode? parsed,
|
||
|
|
string responseFormat,
|
||
|
|
string? putOutputInField,
|
||
|
|
string? outputPath,
|
||
|
|
out List<FlowItem> items,
|
||
|
|
out string? error)
|
||
|
|
{
|
||
|
|
items = new List<FlowItem>();
|
||
|
|
error = null;
|
||
|
|
|
||
|
|
switch (responseFormat)
|
||
|
|
{
|
||
|
|
case "text":
|
||
|
|
items.Add(TextItem(body, putOutputInField));
|
||
|
|
return true;
|
||
|
|
|
||
|
|
case "json":
|
||
|
|
if (parsed == null)
|
||
|
|
{
|
||
|
|
error = "the response is not valid JSON";
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
|
||
|
|
case "file":
|
||
|
|
// File responses are handled by BuildItemsAsync before this point.
|
||
|
|
error = "responseFormat 'file' must be built through the binary store";
|
||
|
|
return false;
|
||
|
|
|
||
|
|
default: // autodetect
|
||
|
|
if (parsed == null)
|
||
|
|
{
|
||
|
|
items.Add(TextItem(body, putOutputInField));
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Envelope unwrapping: read the page's items from a JSON path so the
|
||
|
|
// cursor/next fields that live beside them do not become items.
|
||
|
|
if (!string.IsNullOrWhiteSpace(outputPath) && parsed is JsonObject envelope)
|
||
|
|
{
|
||
|
|
parsed = NodePath.Read(envelope, outputPath, dotNotation: true);
|
||
|
|
if (parsed == null)
|
||
|
|
return true; // a page without the data field simply contributes nothing
|
||
|
|
}
|
||
|
|
|
||
|
|
items.AddRange(ToItems(parsed, putOutputInField));
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static List<FlowItem> ToItems(JsonNode parsed, string? putOutputInField)
|
||
|
|
{
|
||
|
|
var items = new List<FlowItem>();
|
||
|
|
if (!string.IsNullOrWhiteSpace(putOutputInField))
|
||
|
|
{
|
||
|
|
if (parsed is JsonArray wrapped)
|
||
|
|
{
|
||
|
|
foreach (var element in wrapped)
|
||
|
|
items.Add(Wrap(new JsonObject { [putOutputInField] = element?.DeepClone() }));
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
items.Add(Wrap(new JsonObject { [putOutputInField] = parsed.DeepClone() }));
|
||
|
|
}
|
||
|
|
return items;
|
||
|
|
}
|
||
|
|
|
||
|
|
switch (parsed)
|
||
|
|
{
|
||
|
|
case JsonArray array:
|
||
|
|
foreach (var element in array)
|
||
|
|
{
|
||
|
|
if (element is JsonObject obj)
|
||
|
|
items.Add(Wrap(obj));
|
||
|
|
else
|
||
|
|
items.Add(Wrap(new JsonObject { ["value"] = element?.DeepClone() }));
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
|
||
|
|
case JsonObject obj:
|
||
|
|
items.Add(Wrap(obj));
|
||
|
|
break;
|
||
|
|
|
||
|
|
default:
|
||
|
|
items.Add(Wrap(new JsonObject { ["value"] = parsed.DeepClone() }));
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
return items;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static FlowItem TextItem(string body, string? putOutputInField)
|
||
|
|
=> string.IsNullOrWhiteSpace(putOutputInField)
|
||
|
|
? Wrap(new JsonObject { ["data"] = body })
|
||
|
|
: Wrap(new JsonObject { [putOutputInField] = body });
|
||
|
|
|
||
|
|
private static FlowItem Wrap(JsonNode json)
|
||
|
|
=> new() { Json = json is JsonObject obj ? (JsonObject)obj.DeepClone() : new JsonObject { ["value"] = json } };
|
||
|
|
}
|