using System.Text; namespace MeterVault.App.Analysis; /// /// GET /export/analysis.csv (D-55): the analysis table of the URL keys the pages use — scope/id/ /// ids, metric, period, from, to, bucket, compare — as a CSV download. /// Build links to it with . /// /// /// A UI endpoint, not part of the versioned REST API: it serves the same reader a signed-in browser already sees and /// needs no API key, like the pages themselves. Invalid input is a 400 with a short plain-text message; nothing a user /// can type into the URL makes it a 500. /// public static class AnalysisExportEndpoints { public static IEndpointRouteBuilder MapAnalysisExport(this IEndpointRouteBuilder endpoints) { ArgumentNullException.ThrowIfNull(endpoints); endpoints.MapGet(AnalysisLinks.ExportPath, async (HttpContext http, AnalysisExport export, ILogger logger, CancellationToken ct) => { var query = AnalysisQuery.Parse(http.Request.Query, AnalysisDefaults.Export); AnalysisExportResult prepared; try { prepared = await export.PrepareAsync(query, ct); } catch (ArgumentException ex) { // The readers reject a combination they cannot answer with an ArgumentException; that is the request's // fault, not the server's. logger.LogWarning(ex, "Analysis export refused {Query}", query); return Results.Text("This combination of keys cannot be exported.", "text/plain; charset=utf-8", Encoding.UTF8, StatusCodes.Status400BadRequest); } if (prepared.Error is { } error) { return Results.Text(error, "text/plain; charset=utf-8", Encoding.UTF8, StatusCodes.Status400BadRequest); } return Results.Stream( async stream => { // A byte-order mark, so spreadsheet programs read the umlauts of meter names as UTF-8. await using var writer = new StreamWriter(stream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true), leaveOpen: true); await AnalysisCsvWriter.WriteAsync(writer, prepared.Rows, ct); }, "text/csv; charset=utf-8", prepared.FileName); }) .ExcludeFromDescription(); return endpoints; } }