去年帮一个做独立站的备份朋友搬家,他服务器上跑了三年的文件网站备份脚本,攒了将近400GB的下载网站快照。换服务器的源码时候他问我:这些备份文件能不能直接放个目录让人下载?我说能是能,但你得先想清楚两件事——带宽扛不扛得住,备份以及别人拿到你的文件网站源码之后会发生什么。
他最后还是下载搭了个简单的下载站。用的源码就是最朴素的PHP目录列表脚本,套了个Bootstrap的备份壳。我看了下代码,文件网站核心逻辑不到五十行:扫描指定目录,下载过滤掉非zip和tar.gz的源码文件,按修改时间倒序输出文件名、备份大小和下载链接。文件网站没有任何数据库,下载没有用户系统,连验证码都没加。
这种站点的源码在网上其实一抓一大把。GitHub上搜“file index”或者“directory listing”,star过千的项目少说十几个。我比较推荐的是用PHP的glob()函数配合filesize()和filemtime()来生成列表,比用scandir()再循环判断要干净得多。下面这段是我自己常用的骨架:
<?php $dir = './backups/'; $files = glob($dir . '*.{ zip,gz,tar}', GLOB_BRACE); usort($files, function($a, $b) { return filemtime($b) - filemtime($a); }); foreach ($files as $file) { $name = basename($file); $size = round(filesize($file) / 1048576, 2) . ' MB'; $time = date('Y-m-d H:i', filemtime($file)); echo "<li><a href='$file'>$name</a> <span>$size / $time</span></li>"; } ?> 就这么几行,扔到支持PHP的虚拟主机上就能跑。但我要说的是,能跑和能用是两码事。我那个朋友上线第三天就出问题了——有人用迅雷多线程拉一个2GB的包,直接把他的1核1G小机器拖到负载爆表。后来加了Nginx的limit_rate限速,又套了Cloudflare的免费CDN,才算稳住。
另一个坑是文件名暴露。很多人写下载站的时候不注意,直接把服务器上的真实路径输出到页面上。比如/home/user/backups/2024-01-15-full-site.tar.gz这种,有心人一看就知道你的目录结构、备份周期、甚至服务器用户名。我一般会在输出前用basename()剥掉路径,或者干脆把文件重命名成哈希值存到另一个目录,下载时再通过readfile()推送。
说到readfile(),这又是个分水岭。直接给文件URL让Web服务器处理,和用PHP读取后输出,体验完全不同。前者支持断点续传、Range请求,但会暴露真实文件地址;后者可以藏在脚本后面做权限校验,但大文件容易撑爆内存。折中方案是用Nginx的X-Accel-Redirect,PHP只负责鉴权,实际传输交给Nginx,两全其美。
我见过最离谱的一个备份下载站,是把整个网站根目录打了个包扔在/download/下面,连wp-config.php里的数据库密码都没删。这种站被扫到就是分分钟的事。所以如果你真要搭这类站点,至少做三件事:把备份文件放在Web根目录之外、给下载链接加个随机token、定期清理旧文件。
至于源码本身,网上那些现成的“备份文件下载网站源码”大多带一堆用不上的功能——用户注册、积分下载、广告位管理。我的建议是别用。自己写个百来行的脚本,逻辑透明,出问题好查,也不用担心别人在代码里埋后门。真要图省事,就用Python的http.server加个--directory参数,一行命令的事,虽然丑但足够应付临时需求。
最后说个观察:这类下载站真正的问题从来不是技术,而是内容管理。我朋友那个站跑了半年,上面堆了七十多个历史版本,最早的还是2022年的。他自己都说不清哪个包对应哪个版本。后来我帮他加了个简单的manifest.json,每次备份时记录版本号和commit hash,下载页面上直接显示。这才算把“备份”这件事做完整了。