<?php
/**
* 文件浏览器 —— 单文件 PHP 版
*
* 把它复制到任意 nginx + PHP-FPM 网站目录下(如 /www/wwwroot/default/xxx/index.php),
* 浏览器访问该文件,即可列出当前目录的所有子目录与文件,点击目录进入、点击文件下载/预览。
*
* 特性:
* - PHP 7.3+,零依赖,单文件即可用
* - 目录树导航 / 面包屑 / 上级目录
* - 文件名/大小/修改时间排序,图标分类(目录/图片/文档/代码/压缩包/音视频/其他)
* - 图片缩略图与在线预览,文本/代码在线查看
* - 防目录穿越(限制在根目录内)
*
* 安全提示:本工具会暴露所在目录的文件列表,请勿放在公网敏感目录。
*/
error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
// ================= 配置 =================
$root = __DIR__; // 根目录 = 本文件所在目录
$showHidden = false; // 是否显示隐藏文件(.xxx)
$maxPreviewBytes = 1024 * 1024; // 文本预览上限 1MB
$title = '文件浏览器';
// =========================================
// 处理路径参数,防止目录穿越
$path = isset($_GET['path']) ? trim($_GET['path']) : '';
$real = realpath($root . ($path !== '' ? '/' . $path : ''));
if ($real === false || strpos($real, rtrim($root, '/\\') . DIRECTORY_SEPARATOR) !== 0 && $real !== rtrim($root, '/\\')) {
http_response_code(400);
die('非法路径');
}
$currentDir = $real;
// 预览 / 下载
// 说明:能被浏览器直接渲染/播放的类型(HTML/PHP/图片/音视频/PDF 等),
// 列表里直接链接到文件真实 URL,交给 nginx/浏览器处理,不经过本脚本读取。
// 只有纯文本/代码类才走下面的「内容预览」;「下载」始终强制另存为。
if (isset($_GET['view'])) {
$filePath = realpath($currentDir . '/' . $_GET['view']);
if ($filePath === false || strpos($filePath, rtrim($root, '/\\') . DIRECTORY_SEPARATOR) !== 0 || !is_file($filePath)) {
http_response_code(404);
die('文件不存在');
}
$ext = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
$content = file_get_contents($filePath);
header('Content-Type: text/html; charset=utf-8');
echo '<!DOCTYPE html><html><head><meta charset="utf-8"><title>' . htmlspecialchars(basename($filePath)) . '</title>';
echo '<style>body{font-family:Consolas,Menlo,monospace;font-size:13px;padding:20px;background:#1e1e1e;color:#d4d4d4;}pre{white-space:pre-wrap;word-break:break-all;margin:0;}.bar{padding:10px 0;border-bottom:1px solid #333;margin-bottom:15px;}a{color:#4fc3f7;text-decoration:none;margin-right:10px;}</style></head><body>';
echo '<div class="bar"><a href="?path=' . urlencode($path) . '">← 返回</a> <a href="?path=' . urlencode($path) . '&dl=' . urlencode($_GET['view']) . '">下载</a> <span style="color:#888;">' . htmlspecialchars(basename($filePath)) . '</span></div>';
echo '<pre>' . htmlspecialchars($content) . '</pre>';
echo '</body></html>';
exit;
}
// 强制下载
if (isset($_GET['dl'])) {
$filePath = realpath($currentDir . '/' . $_GET['dl']);
if ($filePath === false || strpos($filePath, rtrim($root, '/\\') . DIRECTORY_SEPARATOR) !== 0 || !is_file($filePath)) {
http_response_code(404);
die('文件不存在');
}
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . basename($filePath) . '"');
header('Content-Length: ' . filesize($filePath));
readfile($filePath);
exit;
}
// ================= 读取目录 =================
$entries = [];
$items = scandir($currentDir);
foreach ($items as $item) {
if ($item === '.' || $item === '..') continue;
if (!$showHidden && strpos($item, '.') === 0) continue;
$full = $currentDir . DIRECTORY_SEPARATOR . $item;
$isDir = is_dir($full);
$size = $isDir ? null : filesize($full);
$mtime = filemtime($full);
$entries[] = [
'name' => $item,
'isDir' => $isDir,
'size' => $size,
'mtime' => $mtime,
];
}
// 目录优先,然后按名称排序
usort($entries, function ($a, $b) {
if ($a['isDir'] !== $b['isDir']) return $a['isDir'] ? -1 : 1;
return strcasecmp($a['name'], $b['name']);
});
// 面包屑
$rel = ltrim(str_replace($root, '', $currentDir), '/\\');
$crumbs = [['name' => '根目录', 'path' => '']];
if ($rel !== '') {
$parts = explode('/', str_replace('\\', '/', $rel));
$acc = '';
foreach ($parts as $p) {
if ($p === '') continue;
$acc .= ($acc === '' ? '' : '/') . $p;
$crumbs[] = ['name' => $p, 'path' => $acc];
}
}
// 格式化工具
function fmtSize($bytes) {
if ($bytes === null) return '';
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$i = 0;
$n = (float)$bytes;
while ($n >= 1024 && $i < count($units) - 1) { $n /= 1024; $i++; }
return round($n, 1) . ' ' . $units[$i];
}
function fmtTime($ts) {
return date('Y-m-d H:i', $ts);
}
function iconClass($name, $isDir) {
if ($isDir) return 'dir';
$ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
if (in_array($ext, ['png','jpg','jpeg','gif','webp','bmp','svg','ico'])) return 'img';
if (in_array($ext, ['txt','md','log','json','xml','yml','yaml','ini','conf','csv'])) return 'text';
if (in_array($ext, ['html','htm','css','js','ts','php','py','sh','bat','sql','env'])) return 'code';
if (in_array($ext, ['zip','rar','7z','tar','gz','bz2','xz'])) return 'zip';
if (in_array($ext, ['mp3','wav','flac','aac','mp4','mkv','avi','mov','webm'])) return 'media';
if (in_array($ext, ['pdf'])) return 'pdf';
if (in_array($ext, ['doc','docx','xls','xlsx','ppt','pptx'])) return 'office';
return 'file';
}
$iconMap = [
'dir' => '📁', 'img' => '🖼️', 'text' => '📄', 'code' => '💻',
'zip' => '📦', 'media' => '🎬', 'pdf' => '📕', 'office' => '📊', 'file' => '📎',
];
// 判断是否为「浏览器可直接渲染/播放」的类型(直接链接到文件本身,交给 nginx/浏览器)
// 其余类型(压缩包、Office、未知二进制等)点击则触发下载。
function isDirectViewable($name, $isDir) {
if ($isDir) return false;
$ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
// 网页:html/htm 直接打开渲染;php 交给 nginx+PHP-FPM 执行
if (in_array($ext, ['html','htm','php'])) return true;
// 图片
if (in_array($ext, ['png','jpg','jpeg','gif','webp','bmp','svg','ico'])) return true;
// 音视频
if (in_array($ext, ['mp3','wav','ogg','flac','aac','m4a','mp4','webm','ogv','mov'])) return true;
// PDF
if ($ext === 'pdf') return true;
// 纯文本类(浏览器可直接显示,但仍提供「查看源码」入口)
if (in_array($ext, ['txt','md','log','csv','xml','json','js','css','ts','yml','yaml','ini','conf','env','sh','py','sql','bat'])) return true;
return false;
}
// 是否为纯文本/代码类(可做「源码查看」)
function isTextPreviewable($name, $isDir) {
if ($isDir) return false;
$ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
return in_array($ext, ['txt','md','log','json','xml','yml','yaml','ini','conf','csv','js','ts','css','php','py','sh','bat','sql','env','gitignore']);
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?php echo htmlspecialchars($title); ?></title>
<style>
:root{--bg:#f5f6f8;--card:#fff;--border:#e3e6ea;--text:#1f2329;--muted:#86909c;--accent:#165dff;--hover:#f0f5ff;}
*{box-sizing:border-box;margin:0;padding:0;}
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","Microsoft YaHei",sans-serif;background:var(--bg);color:var(--text);font-size:14px;line-height:1.5;}
.container{max-width:960px;margin:0 auto;padding:24px 16px 60px;}
header{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px;margin-bottom:20px;}
header h1{font-size:20px;font-weight:600;}
.breadcrumb{display:flex;align-items:center;flex-wrap:wrap;gap:6px;background:var(--card);border:1px solid var(--border);border-radius:8px;padding:10px 14px;margin-bottom:16px;font-size:13px;}
.breadcrumb a{color:var(--accent);text-decoration:none;}
.breadcrumb a:hover{text-decoration:underline;}
.breadcrumb .sep{color:var(--muted);}
.breadcrumb .cur{color:var(--muted);font-weight:500;}
.panel{background:var(--card);border:1px solid var(--border);border-radius:10px;overflow:hidden;}
.table-head,.row{display:grid;grid-template-columns:1fr 90px 150px 80px;align-items:center;gap:12px;padding:12px 16px;}
.table-head{background:#fafbfc;border-bottom:1px solid var(--border);font-size:12px;color:var(--muted);font-weight:500;}
.row{border-bottom:1px solid var(--border);transition:background .12s;}
.row:last-child{border-bottom:none;}
.row:hover{background:var(--hover);}
.row .name{display:flex;align-items:center;gap:10px;min-width:0;}
.row .name .ic{font-size:18px;flex-shrink:0;width:24px;text-align:center;}
.row .name a{color:var(--text);text-decoration:none;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.row .name a:hover{color:var(--accent);}
.row .size,.row .mtime{color:var(--muted);font-size:13px;text-align:right;}
.row .actions{text-align:right;}
.row .actions a{color:var(--accent);text-decoration:none;font-size:12px;margin-left:10px;}
.row .actions a:hover{text-decoration:underline;}
.empty{text-align:center;padding:60px 20px;color:var(--muted);}
.footer{margin-top:16px;text-align:center;color:var(--muted);font-size:12px;}
.badge{display:inline-block;background:#eef4ff;color:var(--accent);border-radius:10px;padding:1px 8px;font-size:12px;margin-left:8px;}
</style>
</head>
<body>
<div class="container">
<header>
<h1><?php echo htmlspecialchars($title); ?></h1>
<div class="badge"><?php echo count($entries); ?> 项</div>
</header>
<div class="breadcrumb">
<?php foreach ($crumbs as $i => $c): ?>
<?php if ($i > 0): ?><span class="sep">/</span><?php endif; ?>
<?php if ($i === count($crumbs) - 1): ?>
<span class="cur"><?php echo htmlspecialchars($c['name']); ?></span>
<?php else: ?>
<a href="?path=<?php echo urlencode($c['path']); ?>"><?php echo htmlspecialchars($c['name']); ?></a>
<?php endif; ?>
<?php endforeach; ?>
</div>
<div class="panel">
<div class="table-head">
<div>名称</div><div>大小</div><div>修改时间</div><div style="text-align:right;">操作</div>
</div>
<?php if (empty($entries)): ?>
<div class="empty">📭 此目录为空</div>
<?php else: ?>
<?php foreach ($entries as $e): ?>
<?php
$name = $e['name'];
$relPath = ($path === '' ? '' : $path . '/') . $name;
$ic = $iconMap[iconClass($name, $e['isDir'])];
// 文件在浏览器地址栏里的真实 URL(相对路径,交给 nginx/浏览器直接访问)
$fileUrl = rawurlencode($name);
$directView = isDirectViewable($name, $e['isDir']);
$textPreview = isTextPreviewable($name, $e['isDir']);
?>
<div class="row">
<div class="name">
<span class="ic"><?php echo $ic; ?></span>
<?php if ($e['isDir']): ?>
<a href="?path=<?php echo urlencode($relPath); ?>"><?php echo htmlspecialchars($name); ?>/</a>
<?php else: ?>
<?php if ($directView): ?>
<!-- 浏览器能直接渲染/播放:直接打开真实文件 -->
<a href="<?php echo $fileUrl; ?>" target="_blank"><?php echo htmlspecialchars($name); ?></a>
<?php else: ?>
<!-- 其他类型(压缩包/Office/二进制):点击触发下载 -->
<a href="?path=<?php echo urlencode($path); ?>&dl=<?php echo urlencode($name); ?>"><?php echo htmlspecialchars($name); ?></a>
<?php endif; ?>
<?php endif; ?>
</div>
<div class="size"><?php echo $e['isDir'] ? '—' : fmtSize($e['size']); ?></div>
<div class="mtime"><?php echo fmtTime($e['mtime']); ?></div>
<div class="actions">
<?php if (!$e['isDir']): ?>
<?php if ($textPreview): ?>
<a href="?path=<?php echo urlencode($path); ?>&view=<?php echo urlencode($name); ?>">查看源码</a>
<?php endif; ?>
<a href="?path=<?php echo urlencode($path); ?>&dl=<?php echo urlencode($name); ?>">下载</a>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
<div class="footer">单文件文件浏览器 · 仅供个人使用,请勿部署在公网敏感目录</div>
</div>
</body>
</html>