using System.Globalization;
using System.Net;
namespace ScadaLink.CLI.Commands;
///
/// Filter + destination arguments for an audit export invocation. Mirrors the
/// Bundle B GET /api/audit/export parameters.
/// ///
/// are multi-valued — each supplied value becomes a repeated query-string param so
/// the server's multi-value IN (…) filter sees the full set, exactly like
/// the audit query subcommand.
///
public sealed class AuditExportArgs
{
///
/// Start timestamp for the export time window.
///
public string Since { get; set; } = string.Empty;
///
/// End timestamp for the export time window.
///
public string Until { get; set; } = string.Empty;
///
/// Export format (e.g., 'json', 'csv', 'parquet').
///
public string Format { get; set; } = string.Empty;
///
/// Output file path for the exported audit log.
///
public string Output { get; set; } = string.Empty;
///
/// Channel filter values (repeated query parameter).
///
public string[] Channel { get; set; } = Array.Empty();
///
/// Kind filter values (repeated query parameter).
///
public string[] Kind { get; set; } = Array.Empty();
///
/// Status filter values (repeated query parameter).
///
public string[] Status { get; set; } = Array.Empty();
///
/// Site identifier filter values (repeated query parameter).
///
public string[] Site { get; set; } = Array.Empty();
///
/// Optional target system filter.
///
public string? Target { get; set; }
///
/// Optional actor/user filter.
///
public string? Actor { get; set; }
}
///
/// Helpers for the audit export subcommand: builds the export query string and
/// streams the HTTP response body straight to the destination file without buffering
/// the (potentially multi-megabyte) export in memory.
///
public static class AuditExportHelpers
{
///
/// Builds the ?... query string for GET /api/audit/export: the required
/// time window + format, plus optional filters. Time-specs are resolved via
/// . The multi-valued
/// --channel/--kind/--status/--site filters each emit ONE
/// repeated query-string key per value (e.g. channel=A&channel=B) so the
/// server's multi-value IN (…) filter receives the full set — mirroring
/// .
///
/// The export arguments containing filters and format.
/// The current time for resolving relative time specifications.
public static string BuildQueryString(AuditExportArgs args, DateTimeOffset now)
{
var parts = new List();
void Add(string key, string? value)
{
if (!string.IsNullOrWhiteSpace(value))
parts.Add($"{key}={Uri.EscapeDataString(value)}");
}
void AddEach(string key, IReadOnlyList values)
{
foreach (var value in values)
{
Add(key, value);
}
}
Add("fromUtc", AuditQueryHelpers.ResolveTimeSpec(args.Since, now).ToString("o", CultureInfo.InvariantCulture));
Add("toUtc", AuditQueryHelpers.ResolveTimeSpec(args.Until, now).ToString("o", CultureInfo.InvariantCulture));
Add("format", args.Format);
AddEach("channel", args.Channel);
AddEach("kind", args.Kind);
AddEach("status", args.Status);
AddEach("sourceSiteId", args.Site);
Add("target", args.Target);
Add("actor", args.Actor);
return "?" + string.Join("&", parts);
}
///
/// Executes the export: GETs /api/audit/export and copies the response body
/// stream directly to . The body is never fully
/// buffered — streams in fixed-size chunks.
/// A 501 Not Implemented (parquet not yet supported server-side) prints the
/// server message and returns a non-zero exit code.
///
/// The management HTTP client for API communication.
/// The export arguments containing filters and output file path.
/// Text writer for command output messages.
/// The current time for resolving relative time specifications.
public static async Task RunExportAsync(
ManagementHttpClient client, AuditExportArgs args, TextWriter output, DateTimeOffset now)
{
var qs = BuildQueryString(args, now);
HttpResponseMessage response;
try
{
response = await client.SendGetStreamAsync("api/audit/export" + qs, CancellationToken.None);
}
catch (HttpRequestException ex)
{
OutputFormatter.WriteError($"Connection failed: {ex.Message}", "CONNECTION_FAILED");
return 1;
}
using (response)
{
if (response.StatusCode == HttpStatusCode.NotImplemented)
{
var message = await response.Content.ReadAsStringAsync();
OutputFormatter.WriteError(
string.IsNullOrWhiteSpace(message)
? "Export format not implemented by the server."
: message,
"NOT_IMPLEMENTED");
return 1;
}
if (!response.IsSuccessStatusCode)
{
var message = await response.Content.ReadAsStringAsync();
OutputFormatter.WriteError(
string.IsNullOrWhiteSpace(message) ? $"Export failed (HTTP {(int)response.StatusCode})." : message,
"ERROR");
return 1;
}
await using var source = await response.Content.ReadAsStreamAsync();
await using var destination = new FileStream(
args.Output, FileMode.Create, FileAccess.Write, FileShare.None,
bufferSize: 81920, useAsync: true);
await source.CopyToAsync(destination);
}
output.WriteLine($"Exported audit log to {args.Output}");
return 0;
}
}