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.

Download Architecture
GET /api/downloads/{filePath} (JWT Required)
POST /api/downloads/session (Returns sessionId)
GET /api/downloads/session/{sessionId} (No Auth)
DownloadService
Cache: ETag check -> 304
Range: partial -> 206
FileStream: 64KB buffer -> 200

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 Session Token Generation
Request Token (POST /api/downloads/session)
Verify JWT & Permissions
Verify File Existence
Generate unique Session ID (GUID N)
Store in Redis cache (TTL: ExpirationHours)
Return sessionId to Client
Share URL with external users
Request File (GET /api/downloads/session/{sessionId})
Fetch Session from Redis Cache
If exists & not expired
Stream file to client with 64KB buffer

Download Endpoints

MethodEndpointDescriptionAuth
GET/api/downloads/{*filePath}Download file (supports Range headers)🔒 Required
POST/api/downloads/sessionCreate 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.

ExcelExportService.csC#
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());
}
CsvExportService.csC#
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.

DownloadService.csC#
// 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);
}
Response with RangeHTTP
HTTP/1.1 206 Partial Content
Content-Range: bytes 1024-2048/10240
Accept-Ranges: bytes

ETag Caching

DownloadService.csC#
// 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.

DownloadService.csC#
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

DownloadService.csC#
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

DownloadService.csC#
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
);