目錄樹瀏覽器 - 安全地顯示指定文件夾的樹形結構

使用方法:
1. 將本文件放置於網站根目錄,命名爲 tree.php 或其他名稱
2. 修改下方的 TARGET_BASE 常量,設置您要瀏覽的根文件夾(相對於網站根目錄)
3. 通過瀏覽器訪問:http://您的網站/tree.php
4.可選參數 ?dir=子文件夾路徑 來瀏覽子目錄,例如:?dir=images/icons
安全特性:
- 所有路徑均經過 realpath 驗證,防止目錄遍歷攻擊
- 只允許訪問 TARGET_BASE 目錄及其子目錄
- 輸出使用 htmlspecialchars 轉義,防止 XSS 攻擊
<?php
/**
* 目錄樹瀏覽器(文本風格)- 使用樹形連線符號代替縮進空格
*
* 使用方法:
* 1. 將本文件放置於網站根目錄,命名爲 tree.php 或其他名稱
* 2. 修改下方的 TARGET_BASE 常量,設置您要瀏覽的根文件夾(相對於網站根目錄)
* 3. 通過瀏覽器訪問:http://您的網站/tree.php
* 4. 可選參數 ?dir=子文件夾路徑 來瀏覽子目錄,例如:?dir=images/icons
*
* 安全特性:
* - 所有路徑均經過 realpath 驗證,防止目錄遍歷攻擊
* - 只允許訪問 TARGET_BASE 目錄及其子目錄
* - 輸出使用 htmlspecialchars 轉義,防止 XSS 攻擊
*/
// ---------- 配置區域 ----------
// 定義要瀏覽的根文件夾(相對於網站根目錄 $_SERVER['DOCUMENT_ROOT'])
// 例如:'storage' 表示瀏覽 /網站根目錄/storage
define('TARGET_BASE', ''); // 請修改爲您需要顯示的文件夾名稱
// 是否顯示隱藏文件(以點開頭的文件/文件夾)
define('SHOW_HIDDEN', false);
// 圖標定義(仍保留,使輸出更友好)
define('ICON_FOLDER', '📁');
define('ICON_FILE', '📄');
// -----------------------------
// 設置時區與編碼
date_default_timezone_set('Asia/Shanghai');
header('Content-Type: text/html; charset=utf-8');
// 基礎路徑:網站根目錄下的目標文件夾
$basePath = rtrim($_SERVER['DOCUMENT_ROOT'], DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . TARGET_BASE;
$baseReal = realpath($basePath);
// 檢查基目錄是否存在且可讀
if ($baseReal === false) {
die("<h2>錯誤:基目錄不存在或不可讀</h2><p>請檢查配置:<code>" . htmlspecialchars($basePath) . "</code></p>");
}
// 獲取請求的子目錄參數,並清理
$requestDir = isset($_GET['dir']) ? trim($_GET['dir'], '/\\ ') : '';
$requestDir = str_replace(['../', '..\\'], '', $requestDir); // 額外過濾,但主要依賴 realpath
// 構建完整目標路徑
if ($requestDir !== '') {
// 替換目錄分隔符爲系統分隔符
$requestDir = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $requestDir);
$targetPath = $baseReal . DIRECTORY_SEPARATOR . $requestDir;
} else {
$targetPath = $baseReal;
}
// 獲取目標路徑的真實路徑(必須存在)
$targetReal = realpath($targetPath);
if ($targetReal === false) {
die("<h2>錯誤:請求的路徑不存在或無法訪問</h2><p>路徑:" . htmlspecialchars($targetPath) . "</p>");
}
// 安全檢查:確保目標路徑在基目錄之下
$baseRealWithSep = $baseReal . DIRECTORY_SEPARATOR;
if (strpos($targetReal . DIRECTORY_SEPARATOR, $baseRealWithSep) !== 0 && $targetReal !== $baseReal) {
die("<h2>錯誤:無權訪問指定目錄</h2>");
}
// 獲取當前目錄相對於基目錄的路徑(用於URL)
$relativePath = ($targetReal === $baseReal) ? '' : substr($targetReal, strlen($baseReal) + 1);
$relativePathForUrl = str_replace(DIRECTORY_SEPARATOR, '/', $relativePath);
/**
* 遞歸生成文本風格的目錄樹(帶連線符號)
*
* @param string $path 絕對路徑
* @param string $prefix 前綴字符串(用於遞歸構建連線)
* @return array 每行文本組成的數組
*/
function buildTreeLines($path, $prefix = '') {
$lines = [];
$items = @scandir($path);
if ($items === false) {
return ['<無法讀取目錄>'];
}
// 過濾掉 . 和 ..
$items = array_diff($items, ['.', '..']);
// 如果不顯示隱藏文件,過濾以點開頭的項目
if (!SHOW_HIDDEN) {
$items = array_filter($items, function($item) {
return $item[0] !== '.';
});
}
// 自然排序:文件夾在前,文件在後,各自按名稱排序
$folders = [];
$files = [];
foreach ($items as $item) {
$fullPath = $path . DIRECTORY_SEPARATOR . $item;
if (is_dir($fullPath)) {
$folders[] = $item;
} else {
$files[] = $item;
}
}
natcasesort($folders);
natcasesort($files);
$sortedItems = array_merge($folders, $files);
$count = count($sortedItems);
$i = 0;
foreach ($sortedItems as $item) {
$i++;
$isLast = ($i === $count);
$fullPath = $path . DIRECTORY_SEPARATOR . $item;
$safeName = htmlspecialchars($item);
// 選擇連接符
$connector = $isLast ? '└── ' : '├── ';
if (is_dir($fullPath)) {
// 文件夾行
$lines[] = $prefix . $connector . ICON_FOLDER . ' ' . $safeName;
// 遞歸子文件夾,生成新的前綴
$subPrefix = $prefix . ($isLast ? ' ' : '│ ');
$subLines = buildTreeLines($fullPath, $subPrefix);
$lines = array_merge($lines, $subLines);
} else {
// 文件行:附加文件大小
$size = filesize($fullPath);
$sizeFormatted = formatBytes($size);
$lines[] = $prefix . $connector . ICON_FILE . ' ' . $safeName . ' (' . $sizeFormatted . ')';
}
}
return $lines;
}
/**
* 格式化字節數
*/
function formatBytes($bytes, $precision = 2) {
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$bytes /= (1 << (10 * $pow));
return round($bytes, $precision) . ' ' . $units[$pow];
}
/**
* 獲取父目錄的相對路徑(用於返回上一級)
*/
function getParentRelative($currentRelative) {
if (empty($currentRelative)) {
return null; // 已經在根目錄,無上級
}
$parts = explode(DIRECTORY_SEPARATOR, $currentRelative);
array_pop($parts);
return implode(DIRECTORY_SEPARATOR, $parts);
}
// 生成當前目錄的文本樹行數組
$treeLines = buildTreeLines($targetReal);
$treeText = implode("\n", $treeLines);
// 獲取當前目錄名稱(用於顯示)
$currentDisplayName = ($targetReal === $baseReal) ? TARGET_BASE . ' (根)' : basename($targetReal);
?>
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>目錄樹瀏覽器(文本風格) - <?php echo htmlspecialchars($currentDisplayName); ?></title>
<style>
* {
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', 'SF Pro Text', 'Helvetica Neue', sans-serif;
background: #f8f9fa;
margin: 0;
padding: 20px;
color: #2c3e50;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
overflow: hidden;
padding: 25px 30px;
}
h1 {
margin-top: 0;
font-size: 1.8rem;
font-weight: 400;
border-bottom: 1px solid #e9ecef;
padding-bottom: 15px;
color: #1e3c5c;
}
h1 small {
font-size: 0.9rem;
font-weight: 300;
color: #6c757d;
margin-left: 10px;
}
.path-bar {
background: #e9ecef;
border-radius: 30px;
padding: 10px 20px;
margin-bottom: 25px;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
font-size: 0.95rem;
}
.path-label {
font-weight: 600;
color: #495057;
margin-right: 5px;
}
.path-links {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 5px;
}
.path-links a {
color: #0d6efd;
text-decoration: none;
padding: 2px 8px;
border-radius: 20px;
background: white;
border: 1px solid #dee2e6;
transition: 0.2s;
}
.path-links a:hover {
background: #0d6efd;
color: white;
border-color: #0d6efd;
}
.path-links span.sep {
color: #adb5bd;
margin: 0 2px;
}
.nav-form {
margin-left: auto;
display: flex;
gap: 8px;
}
.nav-form input {
padding: 8px 15px;
border-radius: 30px;
border: 1px solid #ced4da;
width: 250px;
font-size: 0.9rem;
}
.nav-form button {
background: #0d6efd;
color: white;
border: none;
border-radius: 30px;
padding: 8px 20px;
cursor: pointer;
font-weight: 500;
transition: 0.2s;
}
.nav-form button:hover {
background: #0b5ed7;
}
/* 文本樹樣式 */
.tree-container {
background: #fefefe;
border-radius: 12px;
padding: 20px;
font-family: 'SF Mono', 'Fira Code', 'Consolas', 'Courier New', monospace;
font-size: 1rem;
line-height: 1.5;
overflow-x: auto;
border: 1px solid #e9ecef;
white-space: pre;
margin: 15px 0;
}
.tree-text {
margin: 0;
color: #212529;
}
.footer {
margin-top: 30px;
text-align: center;
font-size: 0.85rem;
color: #6c757d;
border-top: 1px solid #e9ecef;
padding-top: 15px;
}
.footer code {
background: #e9ecef;
padding: 2px 6px;
border-radius: 6px;
}
.parent-link {
display: inline-block;
background: #e7f3ff;
color: #0d6efd;
padding: 8px 18px;
border-radius: 30px;
text-decoration: none;
font-weight: 500;
border: 1px solid #b8daff;
transition: 0.2s;
}
.parent-link:hover {
background: #0d6efd;
color: white;
border-color: #0d6efd;
}
</style>
</head>
<body>
<div class="container">
<h1>
📂 目錄樹瀏覽器(文本風格)
<small><?php echo htmlspecialchars(TARGET_BASE); ?> 及其子目錄</small>
</h1>
<!-- 路徑導航 -->
<div class="path-bar">
<span class="path-label">當前位置:</span>
<div class="path-links">
<a href="?">根目錄</a>
<?php
// 生成麪包屑導航
if (!empty($relativePath)) {
$parts = explode(DIRECTORY_SEPARATOR, $relativePath);
$cumulative = '';
echo '<span class="sep">/</span>';
foreach ($parts as $index => $part) {
$cumulative .= ($cumulative ? DIRECTORY_SEPARATOR : '') . $part;
$safePart = htmlspecialchars($part);
$url = '?dir=' . urlencode(str_replace(DIRECTORY_SEPARATOR, '/', $cumulative));
if ($index === count($parts) - 1) {
echo '<span>' . $safePart . '</span>';
} else {
echo '<a href="' . $url . '">' . $safePart . '</a><span class="sep">/</span>';
}
}
}
?>
</div>
<!-- 快速跳轉子目錄表單 -->
<form class="nav-form" method="get" action="">
<input type="text" name="dir" placeholder="輸入子目錄路徑 (如 images/icons)" value="<?php echo htmlspecialchars($relativePathForUrl); ?>">
<button type="submit">跳轉</button>
</form>
</div>
<!-- 文本樹輸出 -->
<div class="tree-container">
<pre class="tree-text"><?php
// 輸出根目錄行,然後輸出樹內容
echo ICON_FOLDER . ' ' . htmlspecialchars($currentDisplayName) . "\n";
echo $treeText;
?></pre>
</div>
<!-- 父目錄鏈接與說明 -->
<div style="margin-top: 20px;">
<?php
$parentRelative = getParentRelative($relativePath);
if ($parentRelative !== null):
$parentUrl = '?dir=' . urlencode(str_replace(DIRECTORY_SEPARATOR, '/', $parentRelative));
?>
<a href="<?php echo $parentUrl; ?>" class="parent-link">⬆ 返回上一級 (<?php echo htmlspecialchars(basename($parentRelative) ?: '根目錄'); ?>)</a>
<?php endif; ?>
</div>
<div class="footer">
<p>🔒 安全限制:只能瀏覽 <code><?php echo htmlspecialchars($baseReal); ?></code> 及其子目錄 | 隱藏文件<?php echo SHOW_HIDDEN ? '顯示' : '不顯示'; ?> | 連線符號: ├── └── │</p>
</div>
</div>
</body>
</html>