<?php
/**
* 简易文件管理器 —— 纯 PHP 实现 ZIP 打包下载
* 不依赖任何扩展 / 插件 / 组件
*/
define('ROOT_DIR', realpath(__DIR__));
$currentPath = '';
// 安全检查
if (isset($_GET['path'])) {
$requested = $_GET['path'];
$realPath = realpath(ROOT_DIR . '/' . $requested);
if ($realPath === false || strpos($realPath, ROOT_DIR) !== 0) {
exit('Access denied.');
}
$currentPath = trim($requested, '/');
}
$currentDir = ROOT_DIR . ($currentPath ? '/' . $currentPath : '');
// 允许编辑的文本类型
$editableExts = [
'txt','php','html','htm','css','js','json','xml','md','ini',
'cfg','log','csv','sql','yaml','yml','sh','bat','py','rb',
'java','c','cpp','h','conf','env'
];
// ============ 动作处理 ============
if (isset($_GET['action'])) {
// 1. 打包下载(纯 PHP ZIP)
if ($_GET['action'] === 'download_zip') {
$zipFileName = 'backup_' . date('YmdHis') . '.zip';
// 创建临时文件
$tmp = tmpfile();
if ($tmp === false) {
$tmpPath = tempnam(sys_get_temp_dir(), 'zip_');
$tmp = fopen($tmpPath, 'w+b');
} else {
$tmpPath = null; // tmpfile 自动删除
}
// 生成 ZIP 并写入临时文件
buildZipStream($currentDir, $tmp);
// 获取大小
fseek($tmp, 0, SEEK_END);
$size = ftell($tmp);
rewind($tmp);
// 输出头
header('Content-Type: application/zip');
header('Content-Disposition: attachment; filename="' . $zipFileName . '"');
header('Content-Length: ' . $size);
fpassthru($tmp);
// 清理
fclose($tmp);
if ($tmpPath) @unlink($tmpPath);
exit;
}
// 2. 编辑文件界面
if ($_GET['action'] === 'edit' && !empty($_GET['file'])) {
$fileName = basename($_GET['file']);
$filePath = realpath($currentDir . '/' . $fileName);
if ($filePath === false || strpos($filePath, ROOT_DIR) !== 0 || !is_file($filePath)) {
exit('File not found.');
}
if (!in_array(strtolower(pathinfo($filePath, PATHINFO_EXTENSION)), $editableExts)) {
exit('Editing this file type is not allowed.');
}
$content = file_get_contents($filePath);
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>编辑:<?php echo htmlspecialchars(basename($filePath)); ?></title>
<style>
body { font-family: system-ui; margin: 30px; background: #f4f6f9; color: #333; }
h2 { color: #007bff; }
textarea { width: 100%; height: 60vh; font-family: monospace; font-size: 14px; padding: 10px; border: 1px solid #ccc; border-radius: 6px; }
.btn { padding: 10px 22px; border: none; border-radius: 6px; background: #007bff; color: white; cursor: pointer; margin-right: 10px; text-decoration: none; display: inline-block; }
.btn:hover { background: #0056b3; }
.cancel { background: #6c757d; } .cancel:hover { background: #5a6268; }
</style>
</head>
<body>
<h2>✏️ 编辑:<?php echo htmlspecialchars(basename($filePath)); ?></h2>
<form method="POST" action="?action=save&file=<?php echo urlencode($fileName); ?>&path=<?php echo urlencode($currentPath); ?>">
<textarea name="content"><?php echo htmlspecialchars($content); ?></textarea><br><br>
<button type="submit">? 保存</button>
<a href="?path=<?php echo urlencode($currentPath); ?>" class="btn cancel">取消</a>
</form>
</body>
</html>
<?php
exit;
}
// 3. 保存编辑
if ($_GET['action'] === 'save' && $_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_GET['file'])) {
$fileName = basename($_GET['file']);
$filePath = realpath($currentDir . '/' . $fileName);
if ($filePath === false || strpos($filePath, ROOT_DIR) !== 0 || !is_file($filePath)) exit('File not found.');
if (!in_array(strtolower(pathinfo($filePath, PATHINFO_EXTENSION)), $editableExts)) exit('Not allowed.');
file_put_contents($filePath, $_POST['content'] ?? '');
header('Location: ?path=' . urlencode($currentPath));
exit;
}
}
// ============ ZIP 构建函数(纯 PHP,无扩展)============
function buildZipStream($sourceDir, $outputHandle) {
$entries = [];
$basePath = rtrim(realpath($sourceDir), '/') . '/';
// 递归收集文件/目录
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($sourceDir, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $item) {
$realPath = $item->getRealPath();
$relative = substr($realPath, strlen($basePath));
if ($relative === '') continue; // 根目录自身跳过
if ($item->isDir()) {
$entries[] = [
'type' => 'dir',
'path' => $relative . '/',
'real' => null,
'size' => 0,
'mtime' => $item->getMTime()
];
} else {
$entries[] = [
'type' => 'file',
'path' => $relative,
'real' => $realPath,
'size' => $item->getSize(),
'mtime' => $item->getMTime()
];
}
}
// 收集中央目录信息
$centralEntries = [];
$offset = 0;
foreach ($entries as $entry) {
$name = $entry['path'];
$nameBytes = $name; // 字节串
$mtime = $entry['mtime'];
$date = date('Y-m-d H:i:s', $mtime);
$dostime = dosTime($mtime);
if ($entry['type'] === 'dir') {
$data = '';
$crc = 0;
$compSize = 0;
$uncompSize = 0;
} else {
$data = file_get_contents($entry['real']);
$uncompSize = strlen($data);
$compSize = $uncompSize; // STORE 模式
$crc = crc32($data);
}
// 本地文件头
$localHeader = pack('V', 0x04034b50); // 签名
$localHeader .= pack('v', 20); // 解压版本 2.0
$localHeader .= pack('v', 0); // 通用标志位
$localHeader .= pack('v', 0); // 压缩方法 0=STORE
$localHeader .= pack('V', $dostime); // 修改时间/日期
$localHeader .= pack('V', $crc); // CRC32
$localHeader .= pack('V', $compSize); // 压缩大小
$localHeader .= pack('V', $uncompSize);// 未压缩大小
$localHeader .= pack('v', strlen($nameBytes)); // 文件名长度
$localHeader .= pack('v', 0); // 额外字段长度
$localHeader .= $nameBytes;
fwrite($outputHandle, $localHeader);
fwrite($outputHandle, $data);
// 记录中央目录信息
$centralEntries[] = [
'name' => $nameBytes,
'crc' => $crc,
'compSize' => $compSize,
'uncompSize' => $uncompSize,
'dostime' => $dostime,
'offset' => $offset
];
$offset += strlen($localHeader) + strlen($data);
}
// 中央目录起始位置
$centralStart = $offset;
foreach ($centralEntries as $e) {
$centralHeader = pack('V', 0x02014b50); // 中央目录签名
$centralHeader .= pack('v', 20); // 创建版本
$centralHeader .= pack('v', 20); // 解压版本
$centralHeader .= pack('v', 0); // 标志位
$centralHeader .= pack('v', 0); // 压缩方法
$centralHeader .= pack('V', $e['dostime']);
$centralHeader .= pack('V', $e['crc']);
$centralHeader .= pack('V', $e['compSize']);
$centralHeader .= pack('V', $e['uncompSize']);
$centralHeader .= pack('v', strlen($e['name'])); // 文件名长
$centralHeader .= pack('v', 0); // 额外字段
$centralHeader .= pack('v', 0); // 注释长度
$centralHeader .= pack('v', 0); // 磁盘起始
$centralHeader .= pack('v', 0); // 内部属性
$centralHeader .= pack('V', 0); // 外部属性
$centralHeader .= pack('V', $e['offset']);
$centralHeader .= $e['name'];
fwrite($outputHandle, $centralHeader);
$offset += strlen($centralHeader);
}
// 结束记录
$endRecord = pack('V', 0x06054b50);
$endRecord .= pack('v', 0); // 磁盘编号
$endRecord .= pack('v', 0); // 起始磁盘
$endRecord .= pack('v', count($centralEntries)); // 磁盘条目数
$endRecord .= pack('v', count($centralEntries)); // 总条目数
$endRecord .= pack('V', $offset - $centralStart); // 中央目录大小
$endRecord .= pack('V', $centralStart); // 中央目录偏移
$endRecord .= pack('v', 0); // 注释长度
fwrite($outputHandle, $endRecord);
}
function dosTime($timestamp) {
$time = localtime($timestamp, true);
$dosTime = ($time['tm_sec'] >> 1) & 0x1F; // 秒/2
$dosTime |= ($time['tm_min'] & 0x3F) << 5;
$dosTime |= ($time['tm_hour'] & 0x1F) << 11;
$dosDate = ($time['tm_mday'] & 0x1F);
$dosDate |= (($time['tm_mon'] + 1) & 0x0F) << 5;
$dosDate |= (($time['tm_year'] + 1900 - 1980) & 0x7F) << 9;
return ($dosDate << 16) | $dosTime;
}
// ============ 目录列表 ============
$items = @scandir($currentDir);
if ($items === false) exit('Cannot read directory.');
$dirs = [];
$files = [];
foreach ($items as $item) {
if ($item === '.') continue;
if ($item === '..') {
if ($currentPath !== '') {
$parentPath = dirname($currentPath);
if ($parentPath === '.') $parentPath = '';
$dirs[] = ['name' => '..', 'path' => $parentPath, 'isDir' => true];
}
continue;
}
$full = $currentDir . '/' . $item;
if (is_dir($full)) {
$dirs[] = ['name' => $item, 'path' => ($currentPath ? $currentPath . '/' : '') . $item, 'isDir' => true];
} else {
$files[] = [
'name' => $item,
'path' => ($currentPath ? $currentPath . '/' : '') . $item,
'isDir' => false,
'size' => filesize($full),
'ext' => pathinfo($item, PATHINFO_EXTENSION)
];
}
}
usort($dirs, function($a,$b){ return strcasecmp($a['name'],$b['name']); });
usort($files, function($a,$b){ return strcasecmp($a['name'],$b['name']); });
$allItems = array_merge($dirs, $files);
// 面包屑
$crumbs = [['label' => '? 根目录', 'path' => '']];
$parts = $currentPath ? explode('/', $currentPath) : [];
$build = '';
foreach ($parts as $part) {
if ($part === '') continue;
$build .= ($build ? '/' : '') . $part;
$crumbs[] = ['label' => $part, 'path' => $build];
}
function formatBytes($b) {
if ($b >= 1073741824) return number_format($b/1073741824,2).' GB';
if ($b >= 1048576) return number_format($b/1048576,2).' MB';
if ($b >= 1024) return number_format($b/1024,2).' KB';
return $b.' B';
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>简易文件管理器</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; background: #f0f2f5; color: #222; padding: 30px; }
.container { max-width: 960px; margin: auto; background: white; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.08); padding: 25px 30px; }
h1 { font-size: 24px; color: #007bff; margin-bottom: 15px; }
.breadcrumb { margin-bottom: 20px; color: #555; }
.breadcrumb a { color: #007bff; text-decoration: none; }
.breadcrumb a:hover { text-decoration: underline; }
.breadcrumb span { margin: 0 6px; }
.toolbar { margin-bottom: 18px; }
.btn { display: inline-block; padding: 8px 18px; border-radius: 6px; background: #007bff; color: #fff; text-decoration: none; border: none; cursor: pointer; }
.btn:hover { background: #0056b3; }
table { width: 100%; border-collapse: collapse; }
th, td { text-align: left; padding: 12px 15px; border-bottom: 1px solid #e9ecef; }
th { background: #f8f9fa; color: #495057; }
tr:hover td { background: #f1f3f5; }
.icon { margin-right: 6px; }
.actions a { color: #007bff; text-decoration: none; margin-right: 10px; }
.actions a:hover { text-decoration: underline; }
.size { color: #6c757d; }
.empty { text-align: center; color: #6c757d; padding: 30px; }
</style>
</head>
<body>
<div>
<h1>? 文件管理器</h1>
<div>
<?php foreach ($crumbs as $i => $c): ?>
<?php if ($i > 0): ?><span>/</span><?php endif; ?>
<?php if ($i === count($crumbs)-1): ?>
<strong><?php echo htmlspecialchars($c['label']); ?></strong>
<?php else: ?>
<a href="?path=<?php echo urlencode($c['path']); ?>"><?php echo htmlspecialchars($c['label']); ?></a>
<?php endif; ?>
<?php endforeach; ?>
</div>
<div>
<a href="?action=download_zip&path=<?php echo urlencode($currentPath); ?>">⬇️ 打包下载此目录</a>
</div>
<table>
<thead><tr><th>名称</th><th>大小</th><th>操作</th></tr></thead>
<tbody>
<?php if (empty($allItems)): ?>
<tr><td colspan="3">此目录为空</td></tr>
<?php else: ?>
<?php foreach ($allItems as $item): ?>
<tr>
<td>
<?php if ($item['isDir']): ?>
<span>?</span>
<a href="?path=<?php echo urlencode($item['path']); ?>"><?php echo htmlspecialchars($item['name']); ?></a>
<?php else: ?>
<span>?</span>
<?php echo htmlspecialchars($item['name']); ?>
<?php endif; ?>
</td>
<td><?php echo $item['isDir'] ? '—' : formatBytes($item['size']); ?></td>
<td>
<?php if (!$item['isDir'] && in_array(strtolower($item['ext']), $editableExts)): ?>
<a href="?action=edit&file=<?php echo urlencode($item['name']); ?>&path=<?php echo urlencode($currentPath); ?>">✏️ 编辑</a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</body>
</html>