using System.IO; using System.Text; namespace SharepointToolbox.Services.Export; /// /// Central file-write plumbing for export services so every CSV and HTML /// artefact gets a consistent encoding: CSV files are written with a UTF-8 /// BOM (required for Excel to detect the encoding when opening a /// double-clicked .csv), HTML files are written without a BOM (some browsers /// and iframe srcdoc paths render the BOM as a visible character). /// Export services should call these helpers rather than constructing /// inline. /// internal static class ExportFileWriter { private static readonly UTF8Encoding Utf8WithBom = new(encoderShouldEmitUTF8Identifier: true); private static readonly UTF8Encoding Utf8NoBom = new(encoderShouldEmitUTF8Identifier: false); /// Writes to as UTF-8 with BOM. public static Task WriteCsvAsync(string filePath, string csv, CancellationToken ct) => File.WriteAllTextAsync(filePath, csv, Utf8WithBom, ct); /// Writes to as UTF-8 without BOM. public static Task WriteHtmlAsync(string filePath, string html, CancellationToken ct) => File.WriteAllTextAsync(filePath, html, Utf8NoBom, ct); /// /// Streams a directly to disk as UTF-8 with /// BOM, chunk by chunk. Avoids the full-document ToString() copy /// and the separate UTF-8 byte buffer that /// would otherwise allocate — meaningful for large CSV exports. /// public static Task WriteCsvChunksAsync(string filePath, StringBuilder builder, CancellationToken ct) => WriteChunksAsync(filePath, builder, Utf8WithBom, ct); /// /// Streams a directly to disk as UTF-8 without /// BOM. Same rationale as — for large /// HTML reports it halves peak memory by skipping the intermediate string. /// public static Task WriteHtmlChunksAsync(string filePath, StringBuilder builder, CancellationToken ct) => WriteChunksAsync(filePath, builder, Utf8NoBom, ct); private static async Task WriteChunksAsync(string filePath, StringBuilder builder, Encoding encoding, CancellationToken ct) { // FileOptions.Asynchronous lets StreamWriter use true async I/O. await using var fs = new FileStream( filePath, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 64 * 1024, options: FileOptions.Asynchronous | FileOptions.SequentialScan); await using var sw = new StreamWriter(fs, encoding, bufferSize: 64 * 1024); foreach (var chunk in builder.GetChunks()) { ct.ThrowIfCancellationRequested(); await sw.WriteAsync(chunk, ct); } await sw.FlushAsync(ct); } }