['php'], 'public' => ['php', 'html', 'js', 'css', 'webmanifest', 'png', 'ico'], ]; function collect_asset_files(string $root, array $scanRoots): array { $files = []; $rootPath = realpath($root); if (!$rootPath) { return $files; } foreach ($scanRoots as $relativeRoot => $extensions) { $base = realpath($rootPath . DIRECTORY_SEPARATOR . $relativeRoot); if (!$base || strpos($base, $rootPath . DIRECTORY_SEPARATOR) !== 0) { continue; } $allowed = array_fill_keys(array_map('strtolower', $extensions), true); $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($base, FilesystemIterator::SKIP_DOTS) ); foreach ($iterator as $file) { if (!$file instanceof SplFileInfo || !$file->isFile()) { continue; } $extension = strtolower($file->getExtension()); if (!isset($allowed[$extension])) { continue; } $path = $file->getRealPath(); if ($path && strpos($path, $rootPath . DIRECTORY_SEPARATOR) === 0) { $files[] = $path; } } } sort($files, SORT_STRING); return array_values(array_unique($files)); } clearstatcache(); $rootPath = realpath($root); $fingerprints = []; $assets = []; foreach (collect_asset_files($root, $scanRoots) as $fullPath) { $relativePath = ltrim(str_replace($rootPath ?: $root, '', $fullPath), DIRECTORY_SEPARATOR); $fileHash = is_readable($fullPath) ? (hash_file('sha256', $fullPath) ?: 'hash-failed') : 'unreadable'; $fileVersion = (string)filemtime($fullPath) . '-' . substr($fileHash, 0, 10); $fingerprints[] = implode('|', [ str_replace(DIRECTORY_SEPARATOR, '/', $relativePath), (string)filesize($fullPath), (string)filemtime($fullPath), $fileHash, ]); if (str_starts_with($relativePath, 'public' . DIRECTORY_SEPARATOR)) { $publicPath = substr($relativePath, strlen('public')); $assets[str_replace(DIRECTORY_SEPARATOR, '/', $publicPath)] = $fileVersion; } } echo json_encode([ 'result' => 'ok', 'service' => $service, 'version' => hash('sha256', implode("\n", $fingerprints)), 'assets' => $assets, 'count' => count($fingerprints), 'generatedAt' => time(), ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);