serve files cleanup

This commit is contained in:
markmental 2026-02-02 12:26:55 -05:00
commit 8f545c3f67
2 changed files with 233 additions and 208 deletions

View file

@ -1,168 +1,182 @@
<?php
/**
* serve_media.php - Serves media files with proper headers for streaming
* serve_media.php - Serves media files with proper headers for streaming + Range
*/
// Get the requested file path
$requestedFile = isset($_GET['file']) ? $_GET['file'] : '';
declare(strict_types=1);
if (empty($requestedFile)) {
header('HTTP/1.1 400 Bad Request');
// ---- Config (prefer env vars in production) ----
$homeDir = getenv('HOME') ?: (getenv('USERPROFILE') ?: ('/home/' . get_current_user()));
$baseDir = rtrim($homeDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'GDrive';
// ---- Input ----
$requested = $_GET['file'] ?? '';
if ($requested === '') {
http_response_code(400);
echo 'No file specified';
exit;
}
// IMPORTANT: Set base directory to where media actually lives
// Using home directory + GDrive instead of __DIR__
$homeDir = getenv('HOME') ?: (getenv('USERPROFILE') ?: '/home/' . get_current_user());
$baseDir = $homeDir . '/GDrive';
// Normalize separators (avoid backslash weirdness)
$requested = str_replace(['\\'], '/', $requested);
// Construct full path
$filePath = $baseDir . DIRECTORY_SEPARATOR . $requestedFile;
// Security: Prevent directory traversal attacks
// Compute real paths and enforce base directory containment
$realBase = realpath($baseDir);
$realFile = realpath($filePath);
$realFile = realpath($baseDir . DIRECTORY_SEPARATOR . $requested);
// Additional debug info (remove in production)
if ($realFile === false) {
header('HTTP/1.1 404 Not Found');
echo 'File not found. Debug info:' . "\n";
echo 'Base dir: ' . $baseDir . "\n";
echo 'Requested: ' . $requestedFile . "\n";
echo 'Full path: ' . $filePath . "\n";
exit;
}
if (strpos($realFile, $realBase) !== 0) {
header('HTTP/1.1 403 Forbidden');
echo 'Access denied - path outside base directory';
exit;
}
// Check if file exists
if (!file_exists($filePath) || !is_file($filePath)) {
header('HTTP/1.1 404 Not Found');
if ($realBase === false || $realFile === false) {
http_response_code(404);
echo 'File not found';
exit;
}
// Get file information
clearstatcache(true, $filePath); // Clear file stat cache
$fileSize = filesize($filePath);
$fileExtension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
$realBase = rtrim($realBase, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
if (strpos($realFile, $realBase) !== 0) {
http_response_code(403);
echo 'Access denied';
exit;
}
// Disable output buffering to prevent Content-Length mismatches
if (ob_get_level()) {
if (!is_file($realFile) || !is_readable($realFile)) {
http_response_code(404);
echo 'File not found';
exit;
}
// Disable output buffering to prevent Content-Length mismatch
while (ob_get_level() > 0) {
ob_end_clean();
}
// Set content type based on extension
$mimeTypes = [
// Video
'mp4' => 'video/mp4',
'mkv' => 'video/x-matroska',
'avi' => 'video/x-msvideo',
'mov' => 'video/quicktime',
'wmv' => 'video/x-ms-wmv',
'flv' => 'video/x-flv',
// Mime types (extend as needed)
$ext = strtolower(pathinfo($realFile, PATHINFO_EXTENSION));
$mime = [
'mp4' => 'video/mp4',
'mkv' => 'video/x-matroska',
'avi' => 'video/x-msvideo',
'mov' => 'video/quicktime',
'wmv' => 'video/x-ms-wmv',
'flv' => 'video/x-flv',
'webm' => 'video/webm',
'm4v' => 'video/x-m4v',
// Audio
'mp3' => 'audio/mpeg',
'wav' => 'audio/wav',
'ogg' => 'audio/ogg',
'm4v' => 'video/x-m4v',
'mp3' => 'audio/mpeg',
'wav' => 'audio/wav',
'ogg' => 'audio/ogg',
'flac' => 'audio/flac',
'm4a' => 'audio/mp4',
'aac' => 'audio/aac',
'wma' => 'audio/x-ms-wma',
'opus' => 'audio/opus'
];
'm4a' => 'audio/mp4',
'aac' => 'audio/aac',
'wma' => 'audio/x-ms-wma',
'opus' => 'audio/opus',
][$ext] ?? 'application/octet-stream';
$contentType = isset($mimeTypes[$fileExtension]) ? $mimeTypes[$fileExtension] : 'application/octet-stream';
$size = filesize($realFile);
if ($size === false) {
http_response_code(500);
echo 'Failed to stat file';
exit;
}
// Handle range requests for seeking
$range = isset($_SERVER['HTTP_RANGE']) ? $_SERVER['HTTP_RANGE'] : '';
// Always advertise range support
header('Accept-Ranges: bytes');
header('Content-Type: ' . $mime);
// Optional caching (fine for local LAN usage)
header('Cache-Control: public, max-age=3600');
if (!empty($range)) {
// Parse range header
list($unit, $range) = explode('=', $range, 2);
if ($unit === 'bytes') {
// Parse range values
list($start, $end) = explode('-', $range, 2);
$start = intval($start);
$end = !empty($end) ? intval($end) : $fileSize - 1;
// Validate range
if ($start > $end || $start < 0 || $end >= $fileSize) {
header('HTTP/1.1 416 Requested Range Not Satisfiable');
header("Content-Range: bytes */$fileSize");
exit;
}
$length = $end - $start + 1;
// Set headers for partial content
header('HTTP/1.1 206 Partial Content');
header("Content-Range: bytes $start-$end/$fileSize");
header("Content-Length: $length");
header("Content-Type: $contentType");
header('Accept-Ranges: bytes');
// Open file and seek to start position
$file = fopen($filePath, 'rb');
if ($file === false) {
header('HTTP/1.1 500 Internal Server Error');
echo 'Failed to open file';
exit;
}
fseek($file, $start);
// Output the requested range
$buffer = 8192; // 8KB chunks
$bytesRemaining = $length;
while ($bytesRemaining > 0 && !feof($file)) {
$bytesToRead = min($buffer, $bytesRemaining);
$data = fread($file, $bytesToRead);
if ($data === false) {
break;
}
echo $data;
$bytesRemaining -= strlen($data);
if (connection_aborted()) {
break;
}
}
fclose($file);
exit; // Important: exit after streaming
}
} else {
// Normal full file response
header('HTTP/1.1 200 OK');
header("Content-Type: $contentType");
header("Content-Length: $fileSize");
header('Accept-Ranges: bytes');
header('Cache-Control: public, max-age=3600');
// Stream file in chunks to avoid memory issues
$file = fopen($filePath, 'rb');
if ($file === false) {
header('HTTP/1.1 500 Internal Server Error');
$rangeHeader = $_SERVER['HTTP_RANGE'] ?? '';
if ($rangeHeader === '') {
// Full response
header('Content-Length: ' . $size);
http_response_code(200);
$fp = fopen($realFile, 'rb');
if ($fp === false) {
http_response_code(500);
echo 'Failed to open file';
exit;
}
$buffer = 8192; // 8KB chunks
while (!feof($file) && !connection_aborted()) {
echo fread($file, $buffer);
}
fclose($file);
exit; // Important: exit after streaming
}
?>
$buf = 1024 * 256; // 256KB chunks (fewer syscalls than 8KB)
while (!feof($fp) && !connection_aborted()) {
$data = fread($fp, $buf);
if ($data === false) break;
echo $data;
}
fclose($fp);
exit;
}
// ---- Range response (single-range only) ----
// Examples:
// - bytes=0-999
// - bytes=1000-
// - bytes=-500 (suffix range)
if (!preg_match('/^bytes=(\d*)-(\d*)$/', trim($rangeHeader), $m)) {
http_response_code(416);
header("Content-Range: bytes */{$size}");
exit;
}
$startRaw = $m[1];
$endRaw = $m[2];
if ($startRaw === '' && $endRaw === '') {
http_response_code(416);
header("Content-Range: bytes */{$size}");
exit;
}
if ($startRaw === '') {
// Suffix range: last N bytes
$suffixLen = (int)$endRaw;
if ($suffixLen <= 0) {
http_response_code(416);
header("Content-Range: bytes */{$size}");
exit;
}
$start = max(0, $size - $suffixLen);
$end = $size - 1;
} else {
$start = (int)$startRaw;
$end = ($endRaw === '') ? ($size - 1) : (int)$endRaw;
}
if ($start < 0 || $end < $start || $end >= $size) {
http_response_code(416);
header("Content-Range: bytes */{$size}");
exit;
}
$length = $end - $start + 1;
http_response_code(206);
header("Content-Range: bytes {$start}-{$end}/{$size}");
header("Content-Length: {$length}");
$fp = fopen($realFile, 'rb');
if ($fp === false) {
http_response_code(500);
echo 'Failed to open file';
exit;
}
if (fseek($fp, $start) !== 0) {
fclose($fp);
http_response_code(500);
echo 'Failed to seek';
exit;
}
$buf = 1024 * 256;
$remaining = $length;
while ($remaining > 0 && !feof($fp) && !connection_aborted()) {
$read = min($buf, $remaining);
$data = fread($fp, $read);
if ($data === false || $data === '') break;
echo $data;
$remaining -= strlen($data);
}
fclose($fp);
exit;