freax-media/media_cache_lib.php

290 lines
7.5 KiB
PHP
Raw Permalink Normal View History

<?php
declare(strict_types=1);
function fm_extensions_by_type(): array
{
return [
'videos' => ['mp4', 'mkv', 'avi', 'mov', 'wmv', 'flv', 'webm', 'm4v'],
'music' => ['mp3', 'wav', 'ogg', 'flac', 'm4a', 'aac', 'wma', 'opus'],
];
}
function fm_is_valid_type(string $type): bool
{
return array_key_exists($type, fm_extensions_by_type());
}
function fm_directories_for_base(string $baseDir): array
{
return [
'videos' => $baseDir . DIRECTORY_SEPARATOR . 'Videos',
'music' => $baseDir . DIRECTORY_SEPARATOR . 'Music',
];
}
function fm_resolve_media_root(?string &$error = null): ?string
{
$mediaRoot = getenv('MEDIA_ROOT');
if ($mediaRoot === false || trim($mediaRoot) === '') {
$error = 'MEDIA_ROOT is not set';
return null;
}
$baseDir = realpath(trim($mediaRoot));
if ($baseDir === false || !is_dir($baseDir)) {
$error = 'MEDIA_ROOT base directory not found';
return null;
}
return $baseDir;
}
function fm_resolve_cache_dir(): string
{
$configured = getenv('MEDIA_CACHE_DIR');
if ($configured === false || trim($configured) === '') {
return __DIR__ . DIRECTORY_SEPARATOR . 'var' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR . 'media';
}
$configured = trim($configured);
if (strpos($configured, DIRECTORY_SEPARATOR) === 0) {
return rtrim($configured, DIRECTORY_SEPARATOR);
}
return rtrim(__DIR__ . DIRECTORY_SEPARATOR . $configured, DIRECTORY_SEPARATOR);
}
function fm_ensure_cache_dir(?string &$error = null): ?string
{
$cacheDir = fm_resolve_cache_dir();
if (is_dir($cacheDir)) {
return $cacheDir;
}
if (!@mkdir($cacheDir, 0775, true) && !is_dir($cacheDir)) {
$error = 'Failed to create cache directory';
return null;
}
return $cacheDir;
}
function fm_cache_paths(string $type, ?string &$error = null): ?array
{
if (!fm_is_valid_type($type)) {
$error = 'Invalid media type';
return null;
}
$cacheDir = fm_ensure_cache_dir($error);
if ($cacheDir === null) {
return null;
}
return [
'cache' => $cacheDir . DIRECTORY_SEPARATOR . $type . '.cache.json',
'lock' => $cacheDir . DIRECTORY_SEPARATOR . $type . '.cache.lock',
];
}
function fm_scan_directory_recursive(string $dir, string $realBase, array $validExtensions): array
{
$result = [];
$items = @scandir($dir);
if ($items === false) {
return [];
}
foreach ($items as $item) {
if ($item === '.' || $item === '..' || ($item !== '' && $item[0] === '.')) {
continue;
}
$fullPath = $dir . DIRECTORY_SEPARATOR . $item;
if (is_dir($fullPath)) {
$sub = fm_scan_directory_recursive($fullPath, $realBase, $validExtensions);
if (!empty($sub)) {
$result[$item] = $sub;
}
continue;
}
$ext = strtolower(pathinfo($item, PATHINFO_EXTENSION));
if (!in_array($ext, $validExtensions, true)) {
continue;
}
$realFile = realpath($fullPath);
if ($realFile === false || strpos($realFile, $realBase) !== 0) {
continue;
}
$result[$item] = substr($realFile, strlen($realBase));
}
uksort($result, function ($a, $b) use ($result) {
$aIsDir = is_array($result[$a]);
$bIsDir = is_array($result[$b]);
if ($aIsDir && !$bIsDir) {
return -1;
}
if (!$aIsDir && $bIsDir) {
return 1;
}
return strcasecmp((string)$a, (string)$b);
});
return $result;
}
function fm_scan_media_tree(string $type, string $baseDir, ?string &$error = null): ?array
{
if (!fm_is_valid_type($type)) {
$error = 'Invalid media type';
return null;
}
$directories = fm_directories_for_base($baseDir);
$targetDir = $directories[$type];
if (!is_dir($targetDir)) {
$error = 'Directory not found';
return null;
}
$realBase = rtrim($baseDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
$validExtensions = fm_extensions_by_type()[$type];
return fm_scan_directory_recursive($targetDir, $realBase, $validExtensions);
}
function fm_read_cache_payload(string $type, string $baseDir, ?string &$error = null): ?array
{
$paths = fm_cache_paths($type, $error);
if ($paths === null) {
return null;
}
if (!is_file($paths['cache']) || !is_readable($paths['cache'])) {
return null;
}
$json = @file_get_contents($paths['cache']);
if ($json === false) {
$error = 'Failed to read cache file';
return null;
}
$payload = json_decode($json, true);
if (!is_array($payload) || !isset($payload['tree']) || !is_array($payload['tree'])) {
$error = 'Invalid cache payload';
return null;
}
if (($payload['media_root'] ?? null) !== $baseDir) {
return null;
}
if (($payload['type'] ?? null) !== $type) {
return null;
}
return $payload;
}
function fm_write_cache_payload(string $type, array $payload, ?string &$error = null): bool
{
$paths = fm_cache_paths($type, $error);
if ($paths === null) {
return false;
}
$json = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
if ($json === false) {
$error = 'Failed to encode cache payload';
return false;
}
try {
$tmpSuffix = bin2hex(random_bytes(4));
} catch (Throwable $e) {
$tmpSuffix = uniqid('', true);
}
$tmpPath = $paths['cache'] . '.tmp.' . getmypid() . '.' . $tmpSuffix;
if (@file_put_contents($tmpPath, $json) === false) {
$error = 'Failed to write temporary cache file';
return false;
}
if (!@rename($tmpPath, $paths['cache'])) {
@unlink($tmpPath);
$error = 'Failed to finalize cache file';
return false;
}
return true;
}
function fm_with_type_lock(string $type, callable $callback, ?string &$error = null)
{
$paths = fm_cache_paths($type, $error);
if ($paths === null) {
return null;
}
$lockHandle = @fopen($paths['lock'], 'c');
if ($lockHandle === false) {
$error = 'Failed to open cache lock file';
return null;
}
if (!@flock($lockHandle, LOCK_EX)) {
@fclose($lockHandle);
$error = 'Failed to acquire cache lock';
return null;
}
try {
return $callback();
} finally {
@flock($lockHandle, LOCK_UN);
@fclose($lockHandle);
}
}
function fm_rebuild_cache(string $type, string $baseDir, bool $strictWrite, ?string &$error = null): ?array
{
return fm_with_type_lock($type, function () use ($type, $baseDir, $strictWrite, &$error) {
$scanError = null;
$tree = fm_scan_media_tree($type, $baseDir, $scanError);
if (!is_array($tree)) {
$error = $scanError ?? 'Failed to scan media directory';
return null;
}
$payload = [
'generated_at' => gmdate(DATE_ATOM),
'media_root' => $baseDir,
'type' => $type,
'tree' => $tree,
];
$writeError = null;
$written = fm_write_cache_payload($type, $payload, $writeError);
if (!$written && $strictWrite) {
$error = $writeError ?? 'Failed to write cache payload';
return null;
}
if (!$written) {
$error = $writeError;
}
return $tree;
}, $error);
}