From 5f7b1aa7c19db624bf08a120b39ed743ba8529d1 Mon Sep 17 00:00:00 2001 From: markmental Date: Tue, 3 Feb 2026 13:19:20 -0500 Subject: [PATCH] adds MEDIA_ROOT environment variable for starting the server --- get_files.php | 54 ++++++++++++++++++++++++++++++------------------- serve_media.php | 33 +++++++++++++++++------------- start-server.sh | 53 ++++++++++++++++++++++-------------------------- 3 files changed, 76 insertions(+), 64 deletions(-) diff --git a/get_files.php b/get_files.php index 9c998bc..da42d56 100644 --- a/get_files.php +++ b/get_files.php @@ -3,19 +3,27 @@ * get_files.php - Recursively scans media directories and returns file structure as JSON */ declare(strict_types=1); + require_once __DIR__ . '/auth.php'; require_auth(true); - - header('Content-Type: application/json; charset=utf-8'); // Media type from query parameter (default: videos) $type = $_GET['type'] ?? 'videos'; -// Resolve base directory (make this configurable via env in production) -$homeDir = getenv('HOME') ?: (getenv('USERPROFILE') ?: ('/home/' . get_current_user())); -$baseDir = rtrim($homeDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'GDrive'; +// Resolve base directory via MEDIA_ROOT +$mediaRoot = getenv('MEDIA_ROOT'); +if ($mediaRoot === false || trim($mediaRoot) === '') { + echo json_encode(['error' => 'MEDIA_ROOT is not set']); + exit; +} + +$baseDir = realpath(trim($mediaRoot)); +if ($baseDir === false || !is_dir($baseDir)) { + echo json_encode(['error' => 'MEDIA_ROOT base directory not found']); + exit; +} $directories = [ 'videos' => $baseDir . DIRECTORY_SEPARATOR . 'Videos', @@ -50,6 +58,13 @@ function scanDirectory(string $dir, string $baseDir, array $validExtensions): ar { $result = []; + // Resolve base once (prevents per-file realpath overhead and handles normalization) + $realBase = realpath($baseDir); + if ($realBase === false) { + return []; + } + $realBase = rtrim($realBase, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; + // scandir() can return false on permissions $items = @scandir($dir); if ($items === false) return []; @@ -75,35 +90,32 @@ function scanDirectory(string $dir, string $baseDir, array $validExtensions): ar continue; } - // Build a relative path from baseDir. Use realpath() to normalize. - $realBase = realpath($baseDir); + // Normalize and enforce: file must live under baseDir $realFile = realpath($fullPath); - if ($realBase === false || $realFile === false) { + if ($realFile === false) { + continue; + } + if (strpos($realFile, $realBase) !== 0) { continue; } - // Enforce: file must live under baseDir - if (strpos($realFile, $realBase . DIRECTORY_SEPARATOR) !== 0) { - continue; - } - - $relativePath = substr($realFile, strlen($realBase) + 1); // +1 skips the slash + // Return relative path from baseDir + $relativePath = substr($realFile, strlen($realBase)); $result[$item] = $relativePath; } // Sort: folders first, then files, alphabetically uksort($result, function ($a, $b) use ($result) { - $aIsDir = is_array($result[$a]); - $bIsDir = is_array($result[$b]); + $aIsDir = is_array($result[$a]); + $bIsDir = is_array($result[$b]); - if ($aIsDir && !$bIsDir) return -1; - if (!$aIsDir && $bIsDir) return 1; + if ($aIsDir && !$bIsDir) return -1; + if (!$aIsDir && $bIsDir) return 1; - // IMPORTANT: keys can be ints if the filename is numeric (e.g. "01", "2026") - return strcasecmp((string)$a, (string)$b); + // IMPORTANT: keys can be ints if the filename is numeric (e.g. "01", "2026") + return strcasecmp((string)$a, (string)$b); }); - return $result; } diff --git a/serve_media.php b/serve_media.php index 1b32270..bc28816 100644 --- a/serve_media.php +++ b/serve_media.php @@ -3,18 +3,29 @@ * serve_media.php - Serves media files with proper headers for streaming + Range */ declare(strict_types=1); + require_once __DIR__ . '/auth.php'; require_auth(false); // plain text is fine for media endpoint set_time_limit(0); +// ---- Config ---- +$mediaRoot = getenv('MEDIA_ROOT'); +if ($mediaRoot === false || trim($mediaRoot) === '') { + http_response_code(500); + echo 'MEDIA_ROOT is not set'; + exit; +} -// ---- Config (prefer env vars in production) ---- -$homeDir = getenv('HOME') ?: (getenv('USERPROFILE') ?: ('/home/' . get_current_user())); -$baseDir = rtrim($homeDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'GDrive'; +$baseDir = realpath(trim($mediaRoot)); +if ($baseDir === false || !is_dir($baseDir)) { + http_response_code(500); + echo 'MEDIA_ROOT base directory not found'; + exit; +} // ---- Input ---- $requested = $_GET['file'] ?? ''; -if ($requested === '') { +if (!is_string($requested) || $requested === '') { http_response_code(400); echo 'No file specified'; exit; @@ -24,16 +35,15 @@ if ($requested === '') { $requested = str_replace(['\\'], '/', $requested); // Compute real paths and enforce base directory containment -$realBase = realpath($baseDir); -$realFile = realpath($baseDir . DIRECTORY_SEPARATOR . $requested); +$realBase = rtrim($baseDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; +$realFile = realpath($baseDir . DIRECTORY_SEPARATOR . ltrim($requested, "/")); -if ($realBase === false || $realFile === false) { +if ($realFile === false) { http_response_code(404); echo 'File not found'; exit; } -$realBase = rtrim($realBase, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; if (strpos($realFile, $realBase) !== 0) { http_response_code(403); echo 'Access denied'; @@ -82,7 +92,6 @@ if ($size === false) { // 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'); $rangeHeader = $_SERVER['HTTP_RANGE'] ?? ''; @@ -98,7 +107,7 @@ if ($rangeHeader === '') { exit; } - $buf = 1024 * 256; // 256KB chunks (fewer syscalls than 8KB) + $buf = 1024 * 256; // 256KB chunks while (!feof($fp) && !connection_aborted()) { $data = fread($fp, $buf); if ($data === false) break; @@ -109,10 +118,6 @@ if ($rangeHeader === '') { } // ---- 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}"); diff --git a/start-server.sh b/start-server.sh index 5aa6507..651ee43 100755 --- a/start-server.sh +++ b/start-server.sh @@ -1,23 +1,11 @@ #!/usr/bin/env bash set -euo pipefail -# start-media-server.sh -# -# Starts frankenphp with MEDIA_USER and MEDIA_PASS_HASH set. -# Hash is generated via: -# - system php (preferred) OR -# - frankenphp php-cli (fallback) -# -# Usage: -# ./start-media-server.sh --user admin --prompt [--port 9000] [--dir /path/to/docroot] -# -# Security: -# Avoid --pass on command line; use --prompt or --pass-stdin to keep it out of `ps`/history. - PORT="9000" USER="" PASS="" DOCROOT="" +MEDIA_ROOT_ARG="" read_pass_stdin=false prompt=false @@ -25,28 +13,27 @@ prompt=false usage() { cat <<'EOF' Usage: - start-media-server.sh --user [--pass | --pass-stdin | --prompt] [--port ] [--dir ] + start-media-server.sh --user [--pass | --pass-stdin | --prompt] + [--media-root ] [--port ] [--dir ] Options: - --user Username to set in MEDIA_USER (required) - --pass

Password to hash (insecure: visible in process list & shell history) - --pass-stdin Read password from stdin (safer) - --prompt Prompt for password (safer; hidden input) - --port

Listen port (default: 9000) - --dir cd into docroot before starting (optional) - -h, --help Show help + --user Username to set in MEDIA_USER (required) + --pass

Password to hash (insecure: visible in process list & shell history) + --pass-stdin Read password from stdin (safer) + --prompt Prompt for password (safer; hidden input) + --media-root

Base directory containing Videos/ and Music/ (recommended) + --port

Listen port (default: 9000) + --dir cd into docroot before starting (optional) + -h, --help Show help Examples: - ./start-media-server.sh --user admin --prompt --port 9000 --dir /home/me/samba-serv - printf '%s\n' 'test123' | ./start-media-server.sh --user admin --pass-stdin + ./start-media-server.sh --user admin --prompt --media-root /mnt/media --port 9000 --dir /home/me/samba-serv + printf '%s\n' 'test123' | ./start-media-server.sh --user admin --pass-stdin --media-root /mnt/media EOF } have_cmd() { command -v "$1" >/dev/null 2>&1; } -# Pick a PHP hashing command: -# - If `php` exists: use it. -# - Else if `frankenphp` exists and supports php-cli: use it. pick_php_runner() { if have_cmd php; then echo "php" @@ -54,7 +41,6 @@ pick_php_runner() { fi if have_cmd frankenphp; then - # Verify php-cli is available (frankenphp subcommand) if frankenphp php-cli -v >/dev/null 2>&1; then echo "frankenphp php-cli" return 0 @@ -70,6 +56,7 @@ while (($#)); do --pass) PASS="${2-}"; shift 2 ;; --pass-stdin) read_pass_stdin=true; shift ;; --prompt) prompt=true; shift ;; + --media-root) MEDIA_ROOT_ARG="${2-}"; shift 2 ;; --port) PORT="${2-}"; shift 2 ;; --dir) DOCROOT="${2-}"; shift 2 ;; -h|--help) usage; exit 0 ;; @@ -117,12 +104,10 @@ fi PHP_RUNNER="$(pick_php_runner)" || { echo "Error: neither 'php' nor 'frankenphp php-cli' is available for hashing" >&2 - echo "Install php OR ensure frankenphp supports 'php-cli'." >&2 exit 127 } # Generate bcrypt hash without needing to escape $. -# We pass the password via an env var P to avoid quoting surprises in -r strings. MEDIA_PASS_HASH="$( P="$PASS" $PHP_RUNNER -r 'echo password_hash(getenv("P"), PASSWORD_BCRYPT), PHP_EOL;' )" @@ -130,6 +115,11 @@ MEDIA_PASS_HASH="$( export MEDIA_USER="$USER" export MEDIA_PASS_HASH +# Export MEDIA_ROOT if provided +if [[ -n "${MEDIA_ROOT_ARG}" ]]; then + export MEDIA_ROOT="${MEDIA_ROOT_ARG}" +fi + if [[ -n "$DOCROOT" ]]; then cd "$DOCROOT" fi @@ -138,6 +128,11 @@ echo "Using PHP runner: $PHP_RUNNER" echo "Starting frankenphp on :$PORT" echo "MEDIA_USER=$MEDIA_USER" echo "MEDIA_PASS_HASH set (bcrypt)" +if [[ -n "${MEDIA_ROOT:-}" ]]; then + echo "MEDIA_ROOT=$MEDIA_ROOT" +else + echo "MEDIA_ROOT not set (get_files.php / serve_media.php will error)" +fi echo exec frankenphp php-server --listen ":$PORT"