adds MEDIA_ROOT environment variable for starting the server

This commit is contained in:
markmental 2026-02-03 13:19:20 -05:00
commit 5f7b1aa7c1
3 changed files with 76 additions and 64 deletions

View file

@ -3,19 +3,27 @@
* get_files.php - Recursively scans media directories and returns file structure as JSON * get_files.php - Recursively scans media directories and returns file structure as JSON
*/ */
declare(strict_types=1); declare(strict_types=1);
require_once __DIR__ . '/auth.php'; require_once __DIR__ . '/auth.php';
require_auth(true); require_auth(true);
header('Content-Type: application/json; charset=utf-8'); header('Content-Type: application/json; charset=utf-8');
// Media type from query parameter (default: videos) // Media type from query parameter (default: videos)
$type = $_GET['type'] ?? 'videos'; $type = $_GET['type'] ?? 'videos';
// Resolve base directory (make this configurable via env in production) // Resolve base directory via MEDIA_ROOT
$homeDir = getenv('HOME') ?: (getenv('USERPROFILE') ?: ('/home/' . get_current_user())); $mediaRoot = getenv('MEDIA_ROOT');
$baseDir = rtrim($homeDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'GDrive'; 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 = [ $directories = [
'videos' => $baseDir . DIRECTORY_SEPARATOR . 'Videos', 'videos' => $baseDir . DIRECTORY_SEPARATOR . 'Videos',
@ -50,6 +58,13 @@ function scanDirectory(string $dir, string $baseDir, array $validExtensions): ar
{ {
$result = []; $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 // scandir() can return false on permissions
$items = @scandir($dir); $items = @scandir($dir);
if ($items === false) return []; if ($items === false) return [];
@ -75,35 +90,32 @@ function scanDirectory(string $dir, string $baseDir, array $validExtensions): ar
continue; continue;
} }
// Build a relative path from baseDir. Use realpath() to normalize. // Normalize and enforce: file must live under baseDir
$realBase = realpath($baseDir);
$realFile = realpath($fullPath); $realFile = realpath($fullPath);
if ($realBase === false || $realFile === false) { if ($realFile === false) {
continue;
}
if (strpos($realFile, $realBase) !== 0) {
continue; continue;
} }
// Enforce: file must live under baseDir // Return relative path from baseDir
if (strpos($realFile, $realBase . DIRECTORY_SEPARATOR) !== 0) { $relativePath = substr($realFile, strlen($realBase));
continue;
}
$relativePath = substr($realFile, strlen($realBase) + 1); // +1 skips the slash
$result[$item] = $relativePath; $result[$item] = $relativePath;
} }
// Sort: folders first, then files, alphabetically // Sort: folders first, then files, alphabetically
uksort($result, function ($a, $b) use ($result) { uksort($result, function ($a, $b) use ($result) {
$aIsDir = is_array($result[$a]); $aIsDir = is_array($result[$a]);
$bIsDir = is_array($result[$b]); $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") // IMPORTANT: keys can be ints if the filename is numeric (e.g. "01", "2026")
return strcasecmp((string)$a, (string)$b); return strcasecmp((string)$a, (string)$b);
}); });
return $result; return $result;
} }

View file

@ -3,18 +3,29 @@
* serve_media.php - Serves media files with proper headers for streaming + Range * serve_media.php - Serves media files with proper headers for streaming + Range
*/ */
declare(strict_types=1); declare(strict_types=1);
require_once __DIR__ . '/auth.php'; require_once __DIR__ . '/auth.php';
require_auth(false); // plain text is fine for media endpoint require_auth(false); // plain text is fine for media endpoint
set_time_limit(0); 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) ---- $baseDir = realpath(trim($mediaRoot));
$homeDir = getenv('HOME') ?: (getenv('USERPROFILE') ?: ('/home/' . get_current_user())); if ($baseDir === false || !is_dir($baseDir)) {
$baseDir = rtrim($homeDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'GDrive'; http_response_code(500);
echo 'MEDIA_ROOT base directory not found';
exit;
}
// ---- Input ---- // ---- Input ----
$requested = $_GET['file'] ?? ''; $requested = $_GET['file'] ?? '';
if ($requested === '') { if (!is_string($requested) || $requested === '') {
http_response_code(400); http_response_code(400);
echo 'No file specified'; echo 'No file specified';
exit; exit;
@ -24,16 +35,15 @@ if ($requested === '') {
$requested = str_replace(['\\'], '/', $requested); $requested = str_replace(['\\'], '/', $requested);
// Compute real paths and enforce base directory containment // Compute real paths and enforce base directory containment
$realBase = realpath($baseDir); $realBase = rtrim($baseDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
$realFile = realpath($baseDir . DIRECTORY_SEPARATOR . $requested); $realFile = realpath($baseDir . DIRECTORY_SEPARATOR . ltrim($requested, "/"));
if ($realBase === false || $realFile === false) { if ($realFile === false) {
http_response_code(404); http_response_code(404);
echo 'File not found'; echo 'File not found';
exit; exit;
} }
$realBase = rtrim($realBase, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
if (strpos($realFile, $realBase) !== 0) { if (strpos($realFile, $realBase) !== 0) {
http_response_code(403); http_response_code(403);
echo 'Access denied'; echo 'Access denied';
@ -82,7 +92,6 @@ if ($size === false) {
// Always advertise range support // Always advertise range support
header('Accept-Ranges: bytes'); header('Accept-Ranges: bytes');
header('Content-Type: ' . $mime); header('Content-Type: ' . $mime);
// Optional caching (fine for local LAN usage)
header('Cache-Control: public, max-age=3600'); header('Cache-Control: public, max-age=3600');
$rangeHeader = $_SERVER['HTTP_RANGE'] ?? ''; $rangeHeader = $_SERVER['HTTP_RANGE'] ?? '';
@ -98,7 +107,7 @@ if ($rangeHeader === '') {
exit; exit;
} }
$buf = 1024 * 256; // 256KB chunks (fewer syscalls than 8KB) $buf = 1024 * 256; // 256KB chunks
while (!feof($fp) && !connection_aborted()) { while (!feof($fp) && !connection_aborted()) {
$data = fread($fp, $buf); $data = fread($fp, $buf);
if ($data === false) break; if ($data === false) break;
@ -109,10 +118,6 @@ if ($rangeHeader === '') {
} }
// ---- Range response (single-range only) ---- // ---- Range response (single-range only) ----
// Examples:
// - bytes=0-999
// - bytes=1000-
// - bytes=-500 (suffix range)
if (!preg_match('/^bytes=(\d*)-(\d*)$/', trim($rangeHeader), $m)) { if (!preg_match('/^bytes=(\d*)-(\d*)$/', trim($rangeHeader), $m)) {
http_response_code(416); http_response_code(416);
header("Content-Range: bytes */{$size}"); header("Content-Range: bytes */{$size}");

View file

@ -1,23 +1,11 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail 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" PORT="9000"
USER="" USER=""
PASS="" PASS=""
DOCROOT="" DOCROOT=""
MEDIA_ROOT_ARG=""
read_pass_stdin=false read_pass_stdin=false
prompt=false prompt=false
@ -25,28 +13,27 @@ prompt=false
usage() { usage() {
cat <<'EOF' cat <<'EOF'
Usage: Usage:
start-media-server.sh --user <username> [--pass <password> | --pass-stdin | --prompt] [--port <port>] [--dir <docroot>] start-media-server.sh --user <username> [--pass <password> | --pass-stdin | --prompt]
[--media-root <path>] [--port <port>] [--dir <docroot>]
Options: Options:
--user <u> Username to set in MEDIA_USER (required) --user <u> Username to set in MEDIA_USER (required)
--pass <p> Password to hash (insecure: visible in process list & shell history) --pass <p> Password to hash (insecure: visible in process list & shell history)
--pass-stdin Read password from stdin (safer) --pass-stdin Read password from stdin (safer)
--prompt Prompt for password (safer; hidden input) --prompt Prompt for password (safer; hidden input)
--port <p> Listen port (default: 9000) --media-root <p> Base directory containing Videos/ and Music/ (recommended)
--dir <path> cd into docroot before starting (optional) --port <p> Listen port (default: 9000)
-h, --help Show help --dir <path> cd into docroot before starting (optional)
-h, --help Show help
Examples: Examples:
./start-media-server.sh --user admin --prompt --port 9000 --dir /home/me/samba-serv ./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 printf '%s\n' 'test123' | ./start-media-server.sh --user admin --pass-stdin --media-root /mnt/media
EOF EOF
} }
have_cmd() { command -v "$1" >/dev/null 2>&1; } 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() { pick_php_runner() {
if have_cmd php; then if have_cmd php; then
echo "php" echo "php"
@ -54,7 +41,6 @@ pick_php_runner() {
fi fi
if have_cmd frankenphp; then if have_cmd frankenphp; then
# Verify php-cli is available (frankenphp subcommand)
if frankenphp php-cli -v >/dev/null 2>&1; then if frankenphp php-cli -v >/dev/null 2>&1; then
echo "frankenphp php-cli" echo "frankenphp php-cli"
return 0 return 0
@ -70,6 +56,7 @@ while (($#)); do
--pass) PASS="${2-}"; shift 2 ;; --pass) PASS="${2-}"; shift 2 ;;
--pass-stdin) read_pass_stdin=true; shift ;; --pass-stdin) read_pass_stdin=true; shift ;;
--prompt) prompt=true; shift ;; --prompt) prompt=true; shift ;;
--media-root) MEDIA_ROOT_ARG="${2-}"; shift 2 ;;
--port) PORT="${2-}"; shift 2 ;; --port) PORT="${2-}"; shift 2 ;;
--dir) DOCROOT="${2-}"; shift 2 ;; --dir) DOCROOT="${2-}"; shift 2 ;;
-h|--help) usage; exit 0 ;; -h|--help) usage; exit 0 ;;
@ -117,12 +104,10 @@ fi
PHP_RUNNER="$(pick_php_runner)" || { PHP_RUNNER="$(pick_php_runner)" || {
echo "Error: neither 'php' nor 'frankenphp php-cli' is available for hashing" >&2 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 exit 127
} }
# Generate bcrypt hash without needing to escape $. # 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="$( MEDIA_PASS_HASH="$(
P="$PASS" $PHP_RUNNER -r 'echo password_hash(getenv("P"), PASSWORD_BCRYPT), PHP_EOL;' 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_USER="$USER"
export MEDIA_PASS_HASH 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 if [[ -n "$DOCROOT" ]]; then
cd "$DOCROOT" cd "$DOCROOT"
fi fi
@ -138,6 +128,11 @@ echo "Using PHP runner: $PHP_RUNNER"
echo "Starting frankenphp on :$PORT" echo "Starting frankenphp on :$PORT"
echo "MEDIA_USER=$MEDIA_USER" echo "MEDIA_USER=$MEDIA_USER"
echo "MEDIA_PASS_HASH set (bcrypt)" 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 echo
exec frankenphp php-server --listen ":$PORT" exec frankenphp php-server --listen ":$PORT"