save development progress
This commit is contained in:
256
src/Cli/StellaOps.Cli/Commands/CommandHandlers.Federation.cs
Normal file
256
src/Cli/StellaOps.Cli/Commands/CommandHandlers.Federation.cs
Normal file
@@ -0,0 +1,256 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// CommandHandlers.Federation.cs
|
||||
// Sprint: SPRINT_8200_0014_0002 (Delta Bundle Export)
|
||||
// Description: Command handlers for federation bundle operations.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Spectre.Console;
|
||||
|
||||
namespace StellaOps.Cli.Commands;
|
||||
|
||||
internal static partial class CommandHandlers
|
||||
{
|
||||
internal static async Task<int> HandleFederationBundleExportAsync(
|
||||
IServiceProvider services,
|
||||
string? sinceCursor,
|
||||
string? output,
|
||||
bool sign,
|
||||
int compressLevel,
|
||||
int maxItems,
|
||||
bool json,
|
||||
bool verbose,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (verbose)
|
||||
{
|
||||
AnsiConsole.MarkupLine("[blue]Exporting federation bundle...[/]");
|
||||
AnsiConsole.MarkupLine($" Since Cursor: [bold]{Markup.Escape(sinceCursor ?? "(none - full export)")}[/]");
|
||||
AnsiConsole.MarkupLine($" Sign: {sign}");
|
||||
AnsiConsole.MarkupLine($" Compression Level: {compressLevel}");
|
||||
AnsiConsole.MarkupLine($" Max Items: {maxItems}");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var httpClientFactory = services.GetService<IHttpClientFactory>();
|
||||
if (httpClientFactory == null)
|
||||
{
|
||||
AnsiConsole.MarkupLine("[red]Error: HTTP client factory not available.[/]");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var client = httpClientFactory.CreateClient("Concelier");
|
||||
|
||||
// Build query string
|
||||
var queryParams = new List<string>();
|
||||
if (!string.IsNullOrEmpty(sinceCursor))
|
||||
queryParams.Add($"since_cursor={Uri.EscapeDataString(sinceCursor)}");
|
||||
queryParams.Add($"sign={sign.ToString().ToLowerInvariant()}");
|
||||
queryParams.Add($"max_items={maxItems}");
|
||||
queryParams.Add($"compress_level={compressLevel}");
|
||||
|
||||
var url = $"/api/v1/federation/export?{string.Join("&", queryParams)}";
|
||||
|
||||
using var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var error = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
AnsiConsole.MarkupLine($"[red]Error: {response.StatusCode}[/]");
|
||||
if (verbose)
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[grey]{Markup.Escape(error)}[/]");
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Extract metadata from headers
|
||||
var bundleHash = response.Headers.TryGetValues("X-Bundle-Hash", out var hashValues)
|
||||
? hashValues.FirstOrDefault()
|
||||
: null;
|
||||
var exportCursor = response.Headers.TryGetValues("X-Export-Cursor", out var cursorValues)
|
||||
? cursorValues.FirstOrDefault()
|
||||
: null;
|
||||
var itemsCount = response.Headers.TryGetValues("X-Items-Count", out var countValues)
|
||||
? countValues.FirstOrDefault()
|
||||
: null;
|
||||
|
||||
// Determine output stream
|
||||
Stream outputStream;
|
||||
bool disposeStream;
|
||||
|
||||
if (string.IsNullOrEmpty(output))
|
||||
{
|
||||
outputStream = Console.OpenStandardOutput();
|
||||
disposeStream = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
outputStream = File.Create(output);
|
||||
disposeStream = true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await using var contentStream = await response.Content.ReadAsStreamAsync(cancellationToken);
|
||||
await contentStream.CopyToAsync(outputStream, cancellationToken);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (disposeStream)
|
||||
{
|
||||
await outputStream.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
// Output metadata
|
||||
if (!string.IsNullOrEmpty(output))
|
||||
{
|
||||
if (json)
|
||||
{
|
||||
var metadata = new
|
||||
{
|
||||
bundle_hash = bundleHash,
|
||||
export_cursor = exportCursor,
|
||||
since_cursor = sinceCursor,
|
||||
items_count = int.TryParse(itemsCount, out var count) ? count : 0,
|
||||
output_path = output
|
||||
};
|
||||
AnsiConsole.WriteLine(JsonSerializer.Serialize(metadata, new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true
|
||||
}));
|
||||
}
|
||||
else
|
||||
{
|
||||
AnsiConsole.MarkupLine("[green]Bundle exported successfully.[/]");
|
||||
AnsiConsole.MarkupLine($" Output: [bold]{Markup.Escape(output)}[/]");
|
||||
if (bundleHash != null)
|
||||
AnsiConsole.MarkupLine($" Hash: [dim]{bundleHash}[/]");
|
||||
if (exportCursor != null)
|
||||
AnsiConsole.MarkupLine($" New Cursor: [bold]{exportCursor}[/]");
|
||||
if (itemsCount != null)
|
||||
AnsiConsole.MarkupLine($" Items: {itemsCount}");
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[red]Connection error: {Markup.Escape(ex.Message)}[/]");
|
||||
return 1;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[red]Error: {Markup.Escape(ex.Message)}[/]");
|
||||
if (verbose)
|
||||
{
|
||||
AnsiConsole.WriteException(ex);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
internal static async Task<int> HandleFederationBundlePreviewAsync(
|
||||
IServiceProvider services,
|
||||
string? sinceCursor,
|
||||
bool json,
|
||||
bool verbose,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (verbose)
|
||||
{
|
||||
AnsiConsole.MarkupLine("[blue]Previewing federation export...[/]");
|
||||
AnsiConsole.MarkupLine($" Since Cursor: [bold]{Markup.Escape(sinceCursor ?? "(none - full export)")}[/]");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var httpClientFactory = services.GetService<IHttpClientFactory>();
|
||||
if (httpClientFactory == null)
|
||||
{
|
||||
AnsiConsole.MarkupLine("[red]Error: HTTP client factory not available.[/]");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var client = httpClientFactory.CreateClient("Concelier");
|
||||
|
||||
var url = "/api/v1/federation/export/preview";
|
||||
if (!string.IsNullOrEmpty(sinceCursor))
|
||||
{
|
||||
url += $"?since_cursor={Uri.EscapeDataString(sinceCursor)}";
|
||||
}
|
||||
|
||||
using var response = await client.GetAsync(url, cancellationToken);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var error = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
AnsiConsole.MarkupLine($"[red]Error: {response.StatusCode}[/]");
|
||||
if (verbose)
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[grey]{Markup.Escape(error)}[/]");
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
var content = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
|
||||
if (json)
|
||||
{
|
||||
AnsiConsole.WriteLine(content);
|
||||
}
|
||||
else
|
||||
{
|
||||
var preview = JsonSerializer.Deserialize<PreviewResponse>(content, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
});
|
||||
|
||||
if (preview != null)
|
||||
{
|
||||
AnsiConsole.MarkupLine("[green]Export Preview[/]");
|
||||
AnsiConsole.MarkupLine($" Since Cursor: [dim]{sinceCursor ?? "(full export)"}[/]");
|
||||
AnsiConsole.MarkupLine($" Estimated Canonicals: [bold]{preview.EstimatedCanonicals:N0}[/]");
|
||||
AnsiConsole.MarkupLine($" Estimated Edges: [bold]{preview.EstimatedEdges:N0}[/]");
|
||||
AnsiConsole.MarkupLine($" Estimated Deletions: [bold]{preview.EstimatedDeletions:N0}[/]");
|
||||
AnsiConsole.MarkupLine($" Estimated Size: [bold]{preview.EstimatedSizeMb:F2} MB[/]");
|
||||
}
|
||||
else
|
||||
{
|
||||
AnsiConsole.WriteLine(content);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[red]Connection error: {Markup.Escape(ex.Message)}[/]");
|
||||
return 1;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[red]Error: {Markup.Escape(ex.Message)}[/]");
|
||||
if (verbose)
|
||||
{
|
||||
AnsiConsole.WriteException(ex);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class PreviewResponse
|
||||
{
|
||||
public string? SinceCursor { get; set; }
|
||||
public int EstimatedCanonicals { get; set; }
|
||||
public int EstimatedEdges { get; set; }
|
||||
public int EstimatedDeletions { get; set; }
|
||||
public long EstimatedSizeBytes { get; set; }
|
||||
public double EstimatedSizeMb { get; set; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user