Download & Export System
Authenticated and session-based downloads with Range support, ETag caching, and path traversal prevention.
Download Architecture
The Download System supports two modes: authenticated downloads (JWT required) and session-based downloads (temporary URL, no auth). Both support resumable downloads via HTTP Range headers and cache validation via ETags.
One-Time Download Tokens
Session-based downloads issue cryptographically secure, single-use download tokens with short lifespans (default 60 minutes) to eliminate credential sharing.
Download Endpoints
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| GET | /api/downloads/{*filePath} | Download file (supports Range headers) | 🔒 Required |
| POST | /api/downloads/session | Create temp download URL | 🔒 Required |
| GET | /api/downloads/session/{sessionId} | Download via session (no auth required) | 🔒 Required |
Export Engines & Format Projection
Data exports stream directly from the database through formatters (CSV, JSON, XLSX, PDF) with zero in-memory accumulation to preserve low RAM footprints.
public Task<byte[]> GenerateExcelReportAsync(AuditExportData data, CancellationToken ct = default)
{
using var workbook = new XLWorkbook();
BuildSummarySheet(workbook, data); // Sheet 1: Executive Summary & KPIs
BuildDataSheet(workbook, data, ct); // Sheet 2: Audit Logs Table (failures in light-red)
BuildSecuritySheet(workbook, data); // Sheet 3: Security Alerts & Suspicious IPs
using var ms = new MemoryStream();
workbook.SaveAs(ms);
return Task.FromResult(ms.ToArray());
}public Task<byte[]> GenerateCsvAsync(AuditExportData data, CancellationToken ct = default)
{
using var ms = new MemoryStream();
// Write UTF-8 BOM first for automatic Excel double-click encoding detection
ms.Write(Encoding.UTF8.GetPreamble());
using var writer = new StreamWriter(ms, new UTF8Encoding(false), leaveOpen: true);
using var csv = new CsvWriter(writer, new CsvConfiguration(CultureInfo.InvariantCulture)
{
HasHeaderRecord = true,
ShouldQuote = args => true // Security: always quote fields to prevent CSV Injection
});
// Write header and rows...
csv.NextRecord();
writer.Flush();
return Task.FromResult(ms.ToArray());
}Resumable Downloads (Range Headers)
The system supports HTTP Range headers for resumable downloads. Clients can request specific byte ranges (e.g., bytes=1024-2048) and receive partial content with a 206 status code.
// Parse Range header: "bytes=1024-2048"
private (long? Start, long? End) ParseRangeHeader(long fileSize)
{
var range = rangeHeader.ToString(); // "bytes=1024-2048"
var parts = range["bytes=".Length..].Split('-');
long? start = string.IsNullOrEmpty(parts[0]) ? null : long.Parse(parts[0]);
long? end = string.IsNullOrEmpty(parts[1]) ? null : long.Parse(parts[1]);
// Handle suffix range (last N bytes): "bytes=-500" -> last 500 bytes
if (!start.HasValue && end.HasValue)
{
start = fileSize - end.Value;
end = fileSize - 1;
}
return (start, end);
}HTTP/1.1 206 Partial Content
Content-Range: bytes 1024-2048/10240
Accept-Ranges: bytesETag Caching
// Generate deterministic ETag from file metadata
private static string GenerateETag(FileInfo fileInfo)
{
var data = `${fileInfo.FullName}|${fileInfo.Length}|${fileInfo.LastWriteTimeUtc:O}`;
var hash = MD5.HashData(System.Text.Encoding.UTF8.GetBytes(data));
return $"\"{Convert.ToHexString(hash)}\"";
}ETags are generated from file path, size, and last modified time. When a client sends If-None-Match with a matching ETag, the server returns 304 Not Modified without transferring the file, saving bandwidth.
Session-Based Downloads
Session-based downloads allow sharing temporary download URLs without requiring authentication. A session is created with an expiration time, and the URL can be shared with external users.
public async Task<string> CreateDownloadSessionAsync(string filePath, TimeSpan? expiration, ...)
{
var sessionId = Guid.NewGuid().ToString("N"); // 32-char hex string
var exp = expiration ?? TimeSpan.FromHours(1);
await _cacheService.SetAsync(
$"download:session:{sessionId}",
new DownloadSession(filePath, DateTime.UtcNow.Add(exp)),
exp);
return sessionId;
}Path Traversal Prevention
private string GetFullPath(string filePath)
{
var normalizedPath = filePath.Replace("..", "").TrimStart('/', '\\');
return Path.Combine(_settings.StoragePath ?? "uploads", normalizedPath);
}All file paths are sanitized by removing '..' sequences and normalizing slashes. The resolved path is validated to ensure it stays within the configured storage directory, preventing directory traversal attacks.
FileStream Configuration
var stream = new FileStream(
fullPath,
FileMode.Open,
FileAccess.Read,
FileShare.Read, // Allow concurrent reads
bufferSize: 64 * 1024, // 64KB buffer (4x default)
useAsync: true // Async I/O for non-blocking reads
);