diff --git a/api/download_attachment.php b/api/download_attachment.php index c4f14cf..3665d8b 100644 --- a/api/download_attachment.php +++ b/api/download_attachment.php @@ -102,12 +102,50 @@ try { // Sanitize filename for Content-Disposition $safeFilename = preg_replace('/[^\w\s\-\.]/', '_', $attachment['original_filename']); + $fileSize = filesize($filePath); + + // Parse a single-range "Range: bytes=start-end" request header (RFC 7233). + // Multi-range requests aren't supported; they fall through to a full 200 response. + $rangeStart = 0; + $rangeEnd = $fileSize - 1; + $isRangeRequest = false; + + if (isset($_SERVER['HTTP_RANGE']) && preg_match('/^bytes=(\d*)-(\d*)$/', trim($_SERVER['HTTP_RANGE']), $m)) { + if ($m[1] === '' && $m[2] === '') { + // Malformed ("bytes=-") — ignore and serve the full file. + } elseif ($m[1] === '') { + // Suffix range: last N bytes + $suffixLength = (int)$m[2]; + $rangeStart = max(0, $fileSize - $suffixLength); + $rangeEnd = $fileSize - 1; + $isRangeRequest = true; + } else { + $rangeStart = (int)$m[1]; + $rangeEnd = ($m[2] === '') ? $fileSize - 1 : min((int)$m[2], $fileSize - 1); + $isRangeRequest = true; + } + + if ($isRangeRequest && ($rangeStart > $rangeEnd || $rangeStart >= $fileSize)) { + http_response_code(416); + header('Content-Range: bytes */' . $fileSize); + exit; + } + } + + $rangeLength = $rangeEnd - $rangeStart + 1; + + header('Accept-Ranges: bytes'); header('Content-Type: ' . $attachment['mime_type']); header('Content-Disposition: ' . $disposition . '; filename="' . $safeFilename . '"'); - header('Content-Length: ' . $attachment['file_size']); header('Cache-Control: private, max-age=3600'); header('X-Content-Type-Options: nosniff'); + if ($isRangeRequest) { + http_response_code(206); + header('Content-Range: bytes ' . $rangeStart . '-' . $rangeEnd . '/' . $fileSize); + } + header('Content-Length: ' . $rangeLength); + // Prevent PHP from timing out on large files set_time_limit(0); @@ -125,9 +163,18 @@ try { exit; } - while (!feof($handle)) { - echo fread($handle, 8192); + fseek($handle, $rangeStart); + $remaining = $rangeLength; + $chunkSize = 8192; + while ($remaining > 0 && !feof($handle)) { + $read = ($remaining < $chunkSize) ? $remaining : $chunkSize; + $data = fread($handle, $read); + if ($data === false) { + break; + } + echo $data; flush(); + $remaining -= strlen($data); } fclose($handle);