$APP_NAME, 'version' => $APP_VERSION, 'date' => $APP_DATE, 'file' => $APP_FILE, 'build' => $APP_BUILD, ]; } function app_footer_text(string $extra = ''): string { $meta = app_meta(); $base = $meta['name'] . ' ' . $meta['version'] . ' · ' . $meta['date'] . ' · ' . $meta['file']; return $extra !== '' ? ($base . ' · ' . $extra) : $base; } function timestamp_label(): string { return date('Y-m-d_hisA') . '_' . INDEXER_VERSION; } function normalize_path(string $path): string { return rtrim(str_replace('\\', '/', $path), '/'); } function is_path_within_base(string $base, string $candidate): bool { $baseNorm = normalize_path($base); $candNorm = normalize_path($candidate); return $candNorm === $baseNorm || strpos($candNorm . '/', $baseNorm . '/') === 0; } function is_excluded_dir_name(string $name): bool { global $EXCLUDED_DIRS; if ($name === '.' || $name === '..') { return true; } if (strpos($name, '.') === 0) { return true; } return in_array($name, $EXCLUDED_DIRS, true); } function is_excluded_file_name(string $name): bool { global $EXCLUDED_FILES; if ($name === '.' || $name === '..') { return true; } if (strpos($name, '.') === 0) { return true; } return in_array($name, $EXCLUDED_FILES, true); } function ensure_scope_locked_path(string $candidatePath): string { global $BASE_PATH; $real = realpath($candidatePath); if ($real === false) { throw new RuntimeException('Selected path does not exist.'); } if (!is_path_within_base($BASE_PATH, $real)) { throw new RuntimeException('Selected path is outside /public_html scope.'); } return $real; } function get_folder_choices(string $basePath): array { $choices = [ [ 'label' => '/', 'path' => $basePath, ] ]; $items = @scandir($basePath); if ($items === false) { return $choices; } foreach ($items as $item) { if (is_excluded_dir_name($item)) { continue; } $full = $basePath . DIRECTORY_SEPARATOR . $item; if (is_dir($full)) { $choices[] = [ 'label' => '/' . $item, 'path' => realpath($full) ?: $full, ]; } } usort($choices, function (array $a, array $b): int { return strcasecmp($a['label'], $b['label']); }); return $choices; } function relative_from_base(string $absolutePath): string { global $BASE_PATH; $base = normalize_path($BASE_PATH); $abs = normalize_path($absolutePath); if ($abs === $base) { return '/'; } $rel = substr($abs, strlen($base)); if ($rel === false || $rel === '') { return '/'; } return $rel[0] === '/' ? $rel : '/' . $rel; } function site_name_for_root(string $selectedRoot): string { $relative = relative_from_base($selectedRoot); if ($relative === '/') { return basename($selectedRoot); } return ltrim($relative, '/'); } function web_path_for_dir(string $dir, string $selectedRoot): string { $root = normalize_path($selectedRoot); $dirNorm = normalize_path($dir); if ($dirNorm === $root) { return '/'; } $suffix = substr($dirNorm, strlen($root)); $suffix = str_replace('\\', '/', (string)$suffix); $suffix = trim($suffix, '/'); return '/' . $suffix . '/'; } function human_size(?int $bytes, bool $isDir = false): string { if ($isDir) { return '-'; } if ($bytes === null) { return '-'; } $units = ['B', 'K', 'M', 'G', 'T']; $size = (float)$bytes; $i = 0; while ($size >= 1024 && $i < count($units) - 1) { $size /= 1024; $i++; } if ($i === 0) { return (string)((int)$size); } return rtrim(rtrim(number_format($size, 1, '.', ''), '0'), '.') . $units[$i]; } function description_for_file(string $name): string { $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION)); if ($ext === 'html' || $ext === 'htm') { return 'HTML document'; } if ($ext === 'txt') { return 'Text file'; } if (in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'], true)) { return 'Image'; } if ($ext === 'pdf') { return 'PDF document'; } if ($ext === 'zip') { return 'ZIP archive'; } return 'File'; } function scan_directory_for_listing(string $dir): array { $items = @scandir($dir); if ($items === false) { return []; } $rows = []; foreach ($items as $name) { if ($name === '.' || $name === '..') { continue; } $full = $dir . DIRECTORY_SEPARATOR . $name; if (is_dir($full)) { if (is_excluded_dir_name($name)) { continue; } $rows[] = [ 'type' => 'dir', 'name' => $name, 'href' => rawurlencode($name) . '/', 'mtime' => @filemtime($full) ?: 0, 'size' => null, 'description' => 'Directory', ]; continue; } if (is_excluded_file_name($name)) { continue; } $rows[] = [ 'type' => 'file', 'name' => $name, 'href' => rawurlencode($name), 'mtime' => @filemtime($full) ?: 0, 'size' => @filesize($full) ?: 0, 'description' => description_for_file($name), ]; } return $rows; } function apache_sort_rows(array $rows, string $column, string $order): array { usort($rows, function (array $a, array $b) use ($column, $order): int { $result = 0; if ($column === 'N') { $aWeight = $a['type'] === 'file' ? 0 : 1; $bWeight = $b['type'] === 'file' ? 0 : 1; if ($aWeight !== $bWeight) { $result = $aWeight <=> $bWeight; } else { $result = strcasecmp($a['name'], $b['name']); } } elseif ($column === 'M') { $result = ($a['mtime'] <=> $b['mtime']); if ($result === 0) { $aWeight = $a['type'] === 'file' ? 0 : 1; $bWeight = $b['type'] === 'file' ? 0 : 1; if ($aWeight !== $bWeight) { $result = $aWeight <=> $bWeight; } else { $result = strcasecmp($a['name'], $b['name']); } } } elseif ($column === 'S') { $aSize = $a['type'] === 'dir' ? -1 : (int)$a['size']; $bSize = $b['type'] === 'dir' ? -1 : (int)$b['size']; $result = $aSize <=> $bSize; if ($result === 0) { $aWeight = $a['type'] === 'file' ? 0 : 1; $bWeight = $b['type'] === 'file' ? 0 : 1; if ($aWeight !== $bWeight) { $result = $aWeight <=> $bWeight; } else { $result = strcasecmp($a['name'], $b['name']); } } } else { $aWeight = $a['type'] === 'file' ? 0 : 1; $bWeight = $b['type'] === 'file' ? 0 : 1; if ($aWeight !== $bWeight) { $result = $aWeight <=> $bWeight; } else { $result = strcasecmp($a['name'], $b['name']); } } return $order === 'D' ? -$result : $result; }); return $rows; } function generate_index_html(string $dir, string $selectedRoot, string $generationStamp): string { $rows = scan_directory_for_listing($dir); $webPath = web_path_for_dir($dir, $selectedRoot); $rows = apache_sort_rows($rows, 'N', 'A'); $parentRow = ''; if (normalize_path($dir) !== normalize_path($selectedRoot)) { $parentRow = << Parent Directory - - HTML; } $htmlRows = ''; foreach ($rows as $row) { $nameDisplay = $row['type'] === 'dir' ? $row['name'] . '/' : $row['name']; $mtimeDisplay = $row['mtime'] > 0 ? date('Y-m-d H:i', $row['mtime']) : '-'; $sizeDisplay = human_size($row['size'], $row['type'] === 'dir'); $typeWeight = $row['type'] === 'file' ? '0' : '1'; $htmlRows .= '' . '' . h($nameDisplay) . '' . '' . h($mtimeDisplay) . '' . '' . h($sizeDisplay) . '' . '' . h($row['description']) . '' . "\n"; } $footerText = app_footer_text('Generated ' . $generationStamp); return << Index of {$webPath}

Index of {$webPath}

{$parentRow} {$htmlRows}
Name Last modified Size Description

HTML; } function recursive_directories(string $root): array { $dirs = [$root]; $items = @scandir($root); if ($items === false) { return $dirs; } foreach ($items as $name) { if ($name === '.' || $name === '..') { continue; } if (is_excluded_dir_name($name)) { continue; } $full = $root . DIRECTORY_SEPARATOR . $name; if (is_dir($full)) { $dirs = array_merge($dirs, recursive_directories($full)); } } return $dirs; } function create_temp_zip_path(): string { $temp = tempnam(sys_get_temp_dir(), 'apache-indexer-'); if ($temp === false) { throw new RuntimeException('Unable to create temporary ZIP file in the server temp directory.'); } $zipPath = $temp . '.zip'; if (!@rename($temp, $zipPath)) { @unlink($temp); throw new RuntimeException('Unable to prepare temporary ZIP file in the server temp directory.'); } register_shutdown_function(static function () use ($zipPath): void { if (is_file($zipPath)) { @unlink($zipPath); } }); return $zipPath; } function zip_internal_dir_from_absolute(string $absoluteDir, string $selectedRoot): string { $root = normalize_path($selectedRoot); $dir = normalize_path($absoluteDir); $siteFolder = basename($root); if ($dir === $root) { return $siteFolder; } $rel = substr($dir, strlen($root)); $rel = ltrim((string)$rel, '/'); return $siteFolder . '/' . $rel; } function build_zip_from_tree(string $selectedRoot): array { if (!class_exists('ZipArchive')) { throw new RuntimeException('ZipArchive is not available on this server.'); } $stamp = timestamp_label(); $zipPath = create_temp_zip_path(); $downloadName = 'indexes_' . $stamp . '.zip'; $zip = new ZipArchive(); if ($zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) { @unlink($zipPath); throw new RuntimeException('Unable to create ZIP archive.'); } $generatedCount = 0; $skippedExisting = 0; $directories = recursive_directories($selectedRoot); foreach ($directories as $dir) { $existingIndex = $dir . DIRECTORY_SEPARATOR . 'index.html'; if (is_file($existingIndex)) { $skippedExisting++; continue; } $html = generate_index_html($dir, $selectedRoot, $stamp); $internalDir = zip_internal_dir_from_absolute($dir, $selectedRoot); $internalPath = $internalDir . '/index.html'; $zip->addFromString($internalPath, $html); $generatedCount++; } $zip->close(); return [ 'zip_path' => $zipPath, 'download_name' => $downloadName, 'generated_count' => $generatedCount, 'skipped_existing' => $skippedExisting, 'stamp' => $stamp, ]; } function stream_zip_download_and_exit(array $resultMeta): void { $zipPath = $resultMeta['zip_path'] ?? ''; $downloadName = $resultMeta['download_name'] ?? 'indexes.zip'; if (!is_string($zipPath) || $zipPath === '' || !is_file($zipPath)) { throw new RuntimeException('Temporary ZIP file could not be found for download.'); } if (ob_get_level() > 0) { while (ob_get_level() > 0) { ob_end_clean(); } } header('Content-Description: File Transfer'); header('Content-Type: application/zip'); header('Content-Disposition: attachment; filename="' . str_replace('"', '', $downloadName) . '"'); header('Content-Length: ' . (string)filesize($zipPath)); header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0'); header('Pragma: public'); header('Expires: 0'); $fp = fopen($zipPath, 'rb'); if ($fp === false) { @unlink($zipPath); throw new RuntimeException('Unable to open temporary ZIP for download.'); } while (!feof($fp)) { echo fread($fp, 1048576); flush(); } fclose($fp); @unlink($zipPath); exit; } function write_generated_indexes_to_tree(string $selectedRoot): int { $count = 0; $stamp = timestamp_label(); foreach (recursive_directories($selectedRoot) as $dir) { $indexPath = $dir . DIRECTORY_SEPARATOR . 'index.html'; if (is_file($indexPath)) { continue; } $html = generate_index_html($dir, $selectedRoot, $stamp); file_put_contents($indexPath, $html); $count++; } return $count; } function remove_indexes(string $selectedRoot, bool $forceAll = false): int { $count = 0; foreach (recursive_directories($selectedRoot) as $dir) { $indexPath = $dir . DIRECTORY_SEPARATOR . 'index.html'; if (!is_file($indexPath)) { continue; } if ($forceAll) { @unlink($indexPath); if (!is_file($indexPath)) { $count++; } continue; } $content = @file_get_contents($indexPath); if ($content !== false && strpos($content, INDEX_MARKER) !== false) { @unlink($indexPath); if (!is_file($indexPath)) { $count++; } } } return $count; } function selected_path_from_request(array $choices): string { $allowed = []; foreach ($choices as $choice) { $allowed[] = normalize_path($choice['path']); } $requested = $_POST['folder'] ?? $choices[0]['path']; $selected = ensure_scope_locked_path((string)$requested); if (!in_array(normalize_path($selected), $allowed, true)) { throw new RuntimeException('Selected folder is not an allowed choice.'); } return $selected; } $message = ''; $messageType = 'info'; $resultMeta = []; $choices = get_folder_choices($BASE_PATH); $selectedPath = $choices[0]['path'] ?? $BASE_PATH; if ($_SERVER['REQUEST_METHOD'] === 'POST') { try { $selectedPath = selected_path_from_request($choices); $allowedActions = [ 'generate_zip', 'remove_generated', 'remove_all' ]; $action = $_POST['action'] ?? 'generate_zip'; if ($action === '' || !in_array($action, $allowedActions, true)) { $action = 'generate_zip'; } switch ($action) { case 'generate_zip': $resultMeta = build_zip_from_tree($selectedPath); stream_zip_download_and_exit($resultMeta); break; case 'remove_generated': $removed = remove_indexes($selectedPath, false); $message = '[' . timestamp_label() . '] Removed ' . $removed . ' generated index.html files from the selected subtree.'; $messageType = 'success'; break; case 'remove_all': $removed = remove_indexes($selectedPath, true); $message = '[' . timestamp_label() . '] Removed ' . $removed . ' total index.html files from the selected subtree.'; $messageType = 'warning'; break; } } catch (Throwable $e) { $message = $e->getMessage(); $messageType = 'error'; } } $selectedRelative = relative_from_base($selectedPath); $selectedAbsolute = $selectedPath; $selectedSiteName = site_name_for_root($selectedPath); $choicesForJs = []; foreach ($choices as $choice) { $path = $choice['path']; $choicesForJs[] = [ 'path' => $path, 'label' => $choice['label'], 'relative' => relative_from_base($path), 'siteName' => site_name_for_root($path), ]; } ?> <?php echo h(app_meta()['name']); ?> Control Panel <?php echo h(INDEXER_VERSION); ?>

Control Panel

Generate a ZIP download of self-contained Apache-style index.html files with no external assets, no support folders, and no forgotten ZIP files stored in your site tree.

Target Selection

Selected subtree
ZIP root folder:
Generated index web root: /
Each generated page is fully self-contained. No asset folders are created.
Build: dated
Absolute path that will be affected:
Scope lock is enforced. Actions only run inside the selected subtree.

Current Rules

Version
()
Marker
Default sort
Parent Directory, then files, then folders, all alphabetical where applicable
ZIP behavior
ZIP is created only in the server temp directory for the current request, streamed to your browser immediately, and deleted automatically. Nothing is written into your site folders.
Page behavior
Generated indexes display root as / and keep all CSS and JavaScript inline inside each index.html
Footer behavior
Every generated page footer includes apache-indexer.php YYYY-MM-DD_HHMMSSAM_v18
Skip behavior
Skips any folder that already has index.html

Excluded folders

Hidden files and hidden directories are ignored automatically.