简介一套基于 Java 开发的临时文件上传、存储与分享系统完整源码标签为 Java 源码适合 Web 开发者、Java 学习者研究文件分享类项目的架构设计与后台管理实现。系统包含可配置域名下的 /admin 后台管理入口默认登录 key 需在部署后修改前后端分离或传统 MVC 结构涉及用户管理、文件管理、权限控制以及防 SQL 注入、XSS 等安全措施能够覆盖从上传、存储到限时分享的完整业务链路。压缩包共 133 个文件其中 gif 文件 75 个界面演示或操作截图js/php/css 等前端与逻辑文件 57 个辅以字体及说明文本整体体积约 13.69MB结构清晰便于按模块拆解。已有 71 人学习/下载。通过阅读源码可掌握临时文件系统的路由处理、数据持久化、云存储或本地存储对接方式以及分享链接时效控制等关键实现对理解 Java Web 后端与前端协作、提升实际项目构建能力有直接参考价值。1. 临时文件网盘系统源码它解决什么问题部署前先搞清楚这几点客户要你传一个 2GB 交付包邮件附件放不下、聊天工具传大文件又容易被压缩群聊里分享会议录像希望链接三天后自动失效、文件从服务器彻底消失。临时文件上传存储分享系统就是为这种“用完即走”的场景准备的免注册把文件传上去拿一条短链接走人有效期一到链接失效、磁盘里的文件被物理删除。这套源码一般是 PHP 写的单机方案自带上传页和定时清理任务搭一台普通 Linux 服务器就能跑。适合想快速搭轻量文件交换工具、又不想上对象存储和复杂权限系统的开发者、运维和个人站长。动手部署前先想清楚三件事过期策略怎么定、清理任务靠什么机制保证执行、匿名上传怎么防滥用。这三个问题想不明白系统上线两周内大概率会遇到磁盘打满或链接被刷爆。2. 把系统跑起来之前临时网盘的5个核心设计点与选型依据2.1 匿名上传还是用户体系临时网盘为什么多数选匿名临时网盘和普通网盘的第一道分水岭在用户体系。普通网盘的核心资产是账号里的文件要注册、要登录、要找回密码还要做容量配额和文件列表而临时网盘的核心资产是那条短链接用户传完文件拿链接就走根本不关心“我的文件在哪个目录”。绝大多数开源临时网盘源码都选择匿名上传原因很直接开发量小、使用门槛低、符合“不留长期数据”的定位。你不需要为用户体系写注册页、找回密码邮件、头像和权限模型省下来的时间应该花在过期清理和存储安全上。匿名带来的代价是滥用风险有人可能拿它传违规文件或者写脚本批量刷上传。常见的折中方案是保留一个管理端口令用固定密码进后台看统计、手动删文件而不是做完整用户体系。我自己的项目也倾向这种方案普通用户无感管理者有后门可进。如果你要做多租户、团队协作、按用户归档文件那就不该拿临时网盘源码去改直接换一套带账号体系的文件管理系统更划算。2.2 过期策略TTL、LRU还是下载次数临时文件的核心动作是“过期”。见过不少刚开始做的人只在文件表里加一个 expire_at 字段定时任务到点删掉就以为完事了。实际上过期策略至少有三种常见模型选哪种直接决定用户体感和服务器磁盘压力过期模型判定依据典型实现适用场景主要坑绝对时间过期TTLexpire_at 时间点页面上写“24小时有效”客服传附件用户拖延几天才下载链接已失效最后访问过期LRUlast_access_at 距今 N 天每次下载刷新时间内部文件交换热门文件可能长期不清理下载次数过期download_count 达到上限计数递减到 0 删除发送一次性报价单下载失败重试会重复计数常见做法是“以 TTL 为主、下载次数为辅”expire_at 一到就删除同时允许给个别链接设置下载次数上限。过期检查有两种触发方式一种是后台定时任务全表扫描一种是下载时惰性判断取文件前先看 expire_at 是否已过。生产环境我一般两个都留着——定时任务负责物理清理惰性判断负责兜底防止定时任务挂掉期间用户拿到已经过期的文件。这里有个代码习惯问题先删物理文件还是先删记录我一直建议先删物理文件、后删记录因为就算记录删失败了最坏结果是链接还能访问但文件已 404反过来如果先删记录物理文件就会变成数据库里查不到、以后永远没人管的孤儿文件。字段设计上表里至少要有 expire_at、download_count、max_downloads 三个字段别图省事只留一个 expire_at。2.3 存储布局本地磁盘还是对象存储临时文件网盘的存储层选择大多数单机部署都会用本地磁盘而不是一上来就接对象存储。原因有三个一是临时文件生命周期短文件本身不构成长期资产本地盘成本最低二是接对象存储要配 AK/SK、Bucket、CDN 回源复杂度上了一个台阶对“传了就删”的场景属于过度设计三是清理任务直接在本地磁盘上操作最直观删文件就是 unlink不需要调用 SDK 的删除接口。什么时候该上对象存储当你有多台应用服务器、需要共享同一份文件存储或者对外流量大、要用 CDN 分发时再说。即便上了对象存储也建议把临时网盘落在独立 Bucket 里开启生命周期规则让对象存储自己处理过期删除应用层只负责记录。本地磁盘布局常见做法是按“年/月/日/随机文件名”分层存放例如 storage/2025/07/12/aB3xYz9qR2mT4vW1.bin。这么做有两个作用一是单目录下文件数不会太多避免 ext4 在大目录下访问变慢二是把真实文件名从磁盘路径里剥离开用户上传时叫什么名字只存进数据库磁盘上只用随机 ID天然避免路径穿越和重名覆盖。storage 目录本身不要放在 Web 根目录里这个后面安全边界还会再说。2.4 分享链接与下载接口公开链接怎么生成才安全分享链接的核心是分享码file_key。最容易犯的错误是拿数据库自增 ID 当分享码——那意味着用户把 f?id1024 改成 f?id1025 就能看到别人的文件。正确做法是生成足够长的随机字符串常见参数是 16 位字符集只取英文字母和数字大小写敏感。生成方法用随机数源别用 date 加 mt_rand 这种可预测的方式PHP 里用 random_bytes() 再做 base64 编码并去掉 / 字符。下载接口要做三件事。第一响应头里带 Content-Disposition: attachment文件名用数据库里的 original_name第二Content-Type 使用白名单映射绝不直接用用户上传时自称的 MIME防止浏览器解析网页文件第三如果文件允许在线预览图片、视频、PDF预览和下载要走两个不同接口预览接口只输出 inline 内容且不暴露原始路径。对临时网盘来说最简单的安全模型是默认只允许下载需要预览的格式单独走预览白名单。还有一点容易被忽略公开链接和私有链接的区别。临时网盘的“公开”是指拿到链接就能下载不是指被搜索引擎收录。因此响应里要设置 X-Robots-Tag: noindex避免爬虫把临时链接索引起来。链接的有效期本来就是核心卖点被收录导致文件长期可访问就失去意义了。2.5 安全边界上传校验、路径穿越与滥用防护安全边界从上传那一刻就开始。接口层要校验文件大小上限和扩展名但要注意扩展名过滤只能挡最业余的攻击伪造扩展名太容易。真正可靠的是下面几条。第一上传文件落地后不允许被执行。存储目录在 Web 根目录之外并且不经过 PHP-FPM 解析如果不得不放在 Web 目录内要关闭该目录的 PHP 执行权限。第二文件名在存储层统一改为随机 ID原始文件名只保存在数据库前端展示用数据库值后端读取用存储路径两边隔离从根上消除路径穿越。第三上传接口必须限流按 IP 限制每小时上传次数和总流量否则脚本可以在一分钟内把磁盘打满。还有一类容易被忽略的滥用是“用你的服务器当跳板传文件”。虽然临时网盘传的文件不会被执行但内容本身仍可能包含恶意代码下载方打开后中招追查起来问题就出在你的链接上。所以上传时对压缩包做浅层检查是值得的至少禁止上传 HTML、SVG、JS 这类打开就可能执行脚本的格式。SVG 尤其危险它里面的脚本在浏览器里以你的域名上下文运行等于给你自己种了个 XSS。注意存储目录权限建议设置为 700属主改成 PHP-FPM 运行用户其他系统账号一律不可读。3. 在本地把临时文件网盘跑通安装、配置与启动实操3.1 环境准备与目录布局解压后先看这几个文件源码包是 .rar 压缩格式在 Linux 下先装 unrar 再解压。运行环境建议用 CentOS 7 或 Ubuntu 20.04 以上的系统PHP 7.4 以上兼容 PHP 8.1数据库用 MySQL 5.7 或 MariaDB 10.3 以上如果只是本地试用SQLite 也能跑但生产不建议。我一般把站点放在 /data/www/tmpnetdisk存储目录独立放在 /data/www/tmpnetdisk_storage两者分开方便后续设置权限和做清理。# 安装解压工具CentOS 用 yum install -y unrarUbuntu 用 apt install -y unrar mkdir -p /data/www/tmpnetdisk unrar x /tmp/tmpnetdisk.rar /data/www/tmpnetdisk/ cd /data/www/tmpnetdisk ls -la解压之后先看目录结构而不是急着改配置。典型的源码布局长这样目录/文件作用需要特别注意的地方public/Web 入口目录Nginx root 指到这里app/业务逻辑代码清理脚本通常在这里config/数据库与存储配置部署时第一处要改storage/上传文件落地目录必须移到 Web 根目录外sql/初始化建表脚本先导入再启动README.md部署说明先读这一份再动代码很多临时网盘源码的 storage 目录默认建在项目根目录下这是一开始就要改掉的。把它挪到 /data/www/tmpnetdisk_storage既能脱离 Web 根目录又方便给清理脚本单独授权。挪好之后通过 chown 设置目录属主为 PHP 运行用户通常是 www-data 或 www并给 storage 目录 700 权限禁止其他用户读取上传内容。3.2 数据库初始化导入建表语句并核对核心字段mysql -u root -p -e CREATE DATABASE tmpnetdisk DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; mysql -u root -p tmpnetdisk /data/www/tmpnetdisk/sql/init.sql建表脚本里最重要的就是文件记录表字段设计直接关系后续清理和分享能不能玩得转。典型的核心表结构如下CREATE TABLE upload_file ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT 自增主键仅内部使用, file_key CHAR(16) NOT NULL UNIQUE COMMENT 对外分享码随机字符串, original_name VARCHAR(255) NOT NULL COMMENT 用户上传时的原始文件名, storage_path VARCHAR(255) NOT NULL COMMENT 磁盘相对路径不含文件名原名, file_size BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 字节数, mime_type VARCHAR(100) NOT NULL DEFAULT COMMENT 落地时识别的类型, download_count INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 当前下载次数, max_downloads INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 0 表示不限次数, expire_at DATETIME NOT NULL COMMENT 过期时间, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, KEY idx_expire (expire_at), KEY idx_status_expire (status, expire_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT临时文件记录表;字段设计有几个地方容易忽略。file_key 的类型必须是 CHAR(16)长度和生成逻辑完全一致不要留成 VARCHAR(255)避免用户访问时因为尾部空格导致匹配失败。expire_at 用 DATETIME 而不用 TIMESTAMP是因为 TIMESTAMP 上限到 2038 年虽然临时文件活不到 2038 年但 DATETIME 没有隐式时区转换排错时更省心。表里还留了 status 字段的位置0 表示正常、1 表示已标记删除这样清理脚本可以分两步走先标记、再物理删除避免大表删除时锁住。导入之后必须验证一件事确认失败的文件上传不会向表中插入记录。很多源码的缺陷在于先存文件再写库文件写失败时记录没回滚导致数据库里有记录但磁盘上没有文件下载时永远 404。这一步可以在初始化之后手工插入一条测试记录再删掉确认事务是好的。3.3 修改配置并跑通上传下载用 curl 验证全链路// config/config.php 关键配置项 return [ db [ host 127.0.0.1, name tmpnetdisk, user tmpnetdisk_user, pass 换成自己的强密码, charset utf8mb4, ], storage [ path /data/www/tmpnetdisk_storage, // 一定要在 Web 根目录外 max_file_size 2 * 1024 * 1024 * 1024, // 单文件 2GB default_expire_hours 72, // 默认 72 小时后过期 allowed_ext [zip, rar, 7z, pdf, jpg, png, mp4, docx, xlsx], ], security [ file_key_length 16, max_uploads_per_ip_per_hour 30, max_bytes_per_ip_per_hour 5 * 1024 * 1024 * 1024, // 5GB ], ];数据库账号建议单独建一个别用 root授权只给 tmpnetdisk 库。存储路径如果不是绝对路径代码里要基于项目根目录做归一化防止相对路径引发读写位置错乱。allowed_ext 白名单只影响最外层的扩展名校验真正拦截还要靠上一节说的存储目录不可执行策略。提示config 里的 db.pass 一定不要提交到 Git 仓库部署时单独维护一份配置文件避免源码泄露导致数据库被连。本地开发时用 PHP 内置服务器快速试跑cd /data/www/tmpnetdisk php -S 0.0.0.0:8080 -t public public/index.php内置服务器只用于开发它的并发模型是单进程线上必须用 Nginx 加 PHP-FPM。试跑时用 curl 走一遍完整链路# 1. 上传一个本地测试文件 curl -s -F file./demo.zip http://127.0.0.1:8080/api/upload # 2. 预期返回 JSON类似 # {ok:true,link:http://127.0.0.1:8080/f/aB3xYz9qR2mT4vW1,expire_at:2025-07-15 18:30:00} # 3. 用返回的短链接下载确认 200 且文件名正确 curl -sI http://127.0.0.1:8080/f/aB3xYz9qR2mT4vW1 # 4. 把过期时间改成过去确认链接立即失效 mysql -e UPDATE tmpnetdisk.upload_file SET expire_atNOW()-INTERVAL 1 MINUTE WHERE file_keyaB3xYz9qR2mT4vW1; curl -sI http://127.0.0.1:8080/f/aB3xYz9qR2mT4vW1curl 上传的参数 -F 是 multipart/form-data对应网页表单的 name 属性。检查响应时先看 ok 字段再看 link 里的完整 URL不要自己拼 host。第 3 步用 HEAD 请求是为了不实际下载大文件、快速看响应头。第 4 步是把过期时间拨到过去来模拟过期生产环境不要手改数据库等定时清理任务跑完确认记录被删除、下载接口返回 404 或 410。3.4 挂到 Nginx 下伪静态与下载文件的内部转发配置server { listen 80; server_name tmp.example.com; root /data/www/tmpnetdisk/public; client_max_body_size 2g; location / { try_files $uri /index.php?$query_string; } location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } location /f/ { internal; alias /data/www/tmpnetdisk_storage/; } }这里把 /f/ 配成 Nginx internal 目录配合 PHP 端 X-Accel-Redirect 实现“先校验、再发文件”。思路是 PHP 收到下载请求后先检查分享码是否有效、是否过期校验通过后在响应头里输出 X-Accel-Redirect: /f/2025/07/aB3xYz9qR2mT4vW1Nginx 再从内部 location 读取文件返回给用户。文件内容不经过 PHP 进程下载大文件时内存占用极低而且存储目录不直接暴露给外部访问。client_max_body_size 设为 2g与 PHP 的 upload_max_filesize、post_max_size 保持一致少一层是坑多一层也是坑。4. 必须调整的四个关键参数与失效清理别让磁盘悄悄长满4.1 过期时间与清理周期决定磁盘会不会被打满的两个参数三个关键参数default_expire_hours 是每个文件的默认存活时长max_file_size 是单文件上限cleanup_interval 是清理任务的周期。它们彼此联动过期时间越长、文件上限越大对磁盘容量和清理频率的要求就越高。部署时别拍脑袋先按业务算一笔账假设一天上传 100 个文件平均 50MB默认 72 小时过期那么磁盘上稳定存在的临时文件大约 300 个、约 15GB。如果磁盘只有 40GB乘以两倍余量也不够就该把过期时间压到 24 小时或者限制单文件最大 200MB。清理任务建议用 crontab 挂一个 CLI 脚本而不是依赖 Web 请求触发。Web 触发的问题是清理代码能不能执行取决于有没有人访问系统流量低的时候磁盘悄悄涨满等发现时已经晚了# 每天凌晨 3 点执行先 dry-run 再真正执行 0 3 * * * php /data/www/tmpnetdisk/app/cleanup.php --dry-run /var/log/tmpnetdisk-cleanup.log 21 0 4 * * * php /data/www/tmpnetdisk/app/cleanup.php /var/log/tmpnetdisk-cleanup.log 21--dry-run 参数让脚本只输出将要删除的记录和文件不动真格。第一次上线时先跑几天 dry-run确认删除范围符合预期再放开执行。清理日志至少要记录三样删除了哪个记录、删除了哪个物理文件、删除失败的文件路径。日志不是给你看的是磁盘出问题时用来定位“为什么这个文件没了 / 为什么这个文件还在”的依据。还有一种情况容易被忽略清理脚本执行时如果刚好有用户正在下载那个文件直接 unlink 在 Linux 上不会报错因为文件句柄还开着下载能完成但文件成为临时孤儿需要靠后面的孤立文件扫描兜底。4.2 分享码长度与文件名存储改编码之前先想清楚file_key 长度建议固定 16 位。太短不安全10 位大小写加数字的空间是 62 的 10 次方看着够用但配合日志分析还能提高安全性太长则链接难记、难口头传播。16 位是体验和安全的折中。字符集用 [A-Za-z0-9]注意 URL 里不要出现 、/、否则要额外做 URL 编码。很多人喜欢用 bin2hex(random_bytes(8)) 生成 16 位十六进制串字符集少了 6 个字母等量容量不是不行只是没必要降低熵。另一个常被忽视的配置是文件名存储。数据库的 original_name 必须用 utf8mb4 字符集否则中文文件名存进去会变成问号下载时文件名通过响应头传回浏览器中文要按 RFC 5987 编码成 filename*UTF-8... 格式否则 Chrome 和 Firefox 显示乱码。源码里如果只做 filenamexxx下载中文文件大概率乱码。这个坑后面避坑章节还会单独展开。4.3 下载限制限速与并发控制放在哪一层临时网盘最容易被打爆的接口是下载接口。链接是公开的过期时间又长一旦被爬虫收录或者被人贴到群里一次热门事件就能把带宽打满。限速放 Nginx 层最省事limit_req_zone $binary_remote_addr zonedownload:10m rate5r/s; location /f/ { internal; alias /data/www/tmpnetdisk_storage/; limit_req zonedownload burst10 nodelay; limit_rate 1m; }这里 rate5r/s 限制每个 IP 每秒最多 5 个请求burst10 nodelay 允许瞬时 10 个请求排队但不堆积limit_rate 1m 把单连接下载速度限制在 1MB/s。参数要根据业务调如果使用场景是给客户传大文件限速 1MB/s 太慢可以放宽到 5MB/s如果是公开分享小文件反而可以更严格。应用层配合 download_count 做原子更新每下载一次计数加一达到 max_downloads 就删记录。注意计数更新要用 UPDATE upload_file SET download_countdownload_count1 WHERE file_key? 这种原子 SQL不要先 SELECT 再 UPDATE高并发下会数不准。4.4 自动扫描并删除孤立文件清理任务不能只删数据库记录清理任务删完过期记录之后磁盘上还可能残留两类垃圾一类是上面提到的“下载中硬删”产生的临时孤儿一类是上传过程中写了文件但数据库事务回滚失败留下的残片。这些文件在数据库里没有对应记录过期清理永远扫不到会一直躺在磁盘上。给系统加一层孤立文件扫描是必要的这也是“自动扫描并删除临时文件”思路在服务端的落地实践?php // app/scan_orphans.php —— 先扫描不删除 $storagePath /data/www/tmpnetdisk_storage; $pdo new PDO(mysql:host127.0.0.1;dbnametmpnetdisk;charsetutf8mb4, user, pass); $stmt $pdo-prepare(SELECT 1 FROM upload_file WHERE storage_path ?); $iter new RecursiveIteratorIterator( new RecursiveDirectoryIterator($storagePath, FilesystemIterator::SKIP_DOTS) ); $orphans []; foreach ($iter as $file) { if (!$file-isFile()) { continue; } $rel substr($file-getPathname(), strlen($storagePath) 1); // 路径分隔符统一为 /避免不同系统字符干扰匹配 $rel str_replace(DIRECTORY_SEPARATOR, /, $rel); $stmt-execute([$rel]); if (!$stmt-fetch()) { $orphans[] $rel; echo orphan: {$rel} ( . round($file-getSize() / 1024 / 1024, 2) . MB)\n; } } echo total orphans: . count($orphans) . \n;这个脚本先把 storage 目录下所有文件和数据库中 storage_path 一一对照凡是数据库里查不到的就是孤立文件。第一次跑的时候大概率会列出一批先人工检查文件名确认不是正在上传的半成品再改成删除模式。删除模式不要一条条 unlink 了事要把路径写进日志文件然后分批次删除每删一次记录一批方便事后审计。扫描频率不需要和过期清理一致一天一次就够而每日的过期清理本意是保证库里没有过期记录磁盘扫描是补漏两者职责不同不能合并成同一个任务。5. 部署临时网盘常见的5个坑上传翻车、链接失效、磁盘爆满的排查记录5.1 大文件上传直接报 413 或 504现象是小于 10MB 的文件正常超过 100MB 就失败Nginx 返回 413 Request Entity Too Large或者长时间上传后 504。原因通常是三层配置没同步Nginx 的 client_max_body_size 没设、PHP 的 upload_max_filesize 太小、PHP 的 post_max_size 太小。这三层任何一层卡住上传都会失败。解决方法是把三处调成一致的目标值注意 PHP 的 post_max_size 必须大于 upload_max_filesize因为除了文件本身multipart 表单还有额外的字段数据; php.ini 中需要同步修改的参数 upload_max_filesize 2048M post_max_size 2100M max_execution_time 600 max_input_time 600 memory_limit 256MNginx 侧的 client_max_body_size 设成 2g和 PHP 对齐。改完都要 reloadPHP-FPM 配置文件改了之后要重启 php-fpm 而不是只 reload因为 upload_max_filesize 是 PHP_INI_PERDIR 类型运行期修改不生效。还有一个容易漏的点如果前面还有一层 CDN 或负载均衡也要放开请求体大小限制否则客户端到 CDN 成功、CDN 回源失败浏览器报错还查不到原因。5.2 中文文件名下载乱码现象是浏览器下载文件时文件名变成一串百分号或者“___”。原因是响应头里的 Content-Disposition 只写了 filenamedemo.zip 这种形式遇到中文时不同浏览器对裸 UTF-8 的处理不一致。解决方式是同时输出两个文件名旧浏览器用 ASCII 兜底现代浏览器用 RFC 5987 的 filename*$encoded rawurlencode($originalName); // 中文文件名做百分号编码 header(Content-Disposition: attachment; filename\download.bin\; filename*UTF-8{$encoded});这里 filenamedownload.bin 是给非常老的浏览器用的兜底filename* 才是现代浏览器的标准写法。注意 rawurlencode 之后空格会变成 %20不要再用 urldecode 去还原直接传给响应头。这个坑踩过一次之后我把文件名编码逻辑抽成了一个公共函数所有下载接口统一调用不再让每个接口自己拼响应头。5.3 清理任务跑了磁盘空间却一点没降现象是 crontab 日志显示“delete 100 records”df -h 一看磁盘占用没变化。原因通常是两个一是清理脚本只删了数据库记录没删文件二是删文件时路径拼错了unlink 失败但脚本没报错。后一种更隐蔽因为 PHP 的 unlink 失败返回 false很多脚本不检查返回值日志还显示删除成功。解决办法是清理脚本里每个 unlink 都要判断返回值并记录失败原因// 清理脚本中的关键片段删除物理文件后检查返回值 if (!unlink($absolutePath)) { error_log(unlink failed: {$absolutePath} . error_get_last()[message]); continue; // 记录先不删等下次清理再试 } // unlink 成功后再删数据库记录 $deleteStmt-execute([$fileKey]);执行顺序也要养成习惯先 unlink成功后再删数据库记录。这样即使 unlink 失败记录还在库里下次清理会再尝试反过来先删库再删文件文件就成了没人管的孤儿。另一个常见场景是存储目录被别的进程占用了文件句柄Linux 下 unlink 仍然成功但空间要等句柄释放才回收这种问题用 lsof L1 可以查出来。5.4 分享链接被脚本批量遍历现象是访问日志里短时间内出现大量 /f/xxx 请求状态码大多是 404但偶尔有 200。原因是分享码长度不够长或者生成算法可预测更重要的是下载接口没有限流被字典遍历试出了有效链接。解决分三层分享码至少 16 位随机字符下载接口加 IP 限速前面第 4.3 节已经说过最关键的是把 max_downloads 默认值改成 3 或者 5而不是 0不限次数降低单条链接被刷后的影响面。遍历攻击没法完全阻止公开链接天然可以被猜到或抓到能做的只是把攻击成本抬高到不划算。还有一条实践下载接口的日志要记录 file_key 和响应状态码配合监控在出现批量 404 时告警。批量 404 是扫描器最典型的特征这个信号比磁盘使用率更能提前发现问题。5.5 手动执行清理正常定时任务却不执行现象是手动执行 php cleanup.php 一切正常crontab 里却从不执行日志文件里什么也没有。原因大多是 crontab 的环境变量和手动 shell 不一样尤其是 PATH 里没有 PHP 的绝对路径或者 CLI 模式加载的 PHP 扩展集和 Web 用的不一致。这跟玄学没关系就是环境差异。解决方法是 crontab 里写绝对路径并在脚本开头对 PHP 扩展做检查# 推荐写法全部用绝对路径 0 3 * * * /usr/bin/php /data/www/tmpnetdisk/app/cleanup.php /var/log/tmpnetdisk-cleanup.log 21如果清理脚本里用到 pdo_mysql、posix 这类扩展在 CLI 模式下未必和 FPM 模式加载的是同一份 php.ini先跑 php -m | grep pdo_mysql 确认。遇到“手动正常、定时不跑”下一步永远是看 crontab 的日志和脚本的 stderr 输出别猜。6. 上线前把清理逻辑验证一遍手工核对脚本与自检清单最后分享一个上线前必做的验证动作核对“数据库里能下载的记录”和“磁盘上真实存在的文件”是否一一对应。临时网盘系统的数据一致性根子就在这两边链接能访问但文件没了是 404文件在但数据库记录没了是孤儿两种都是事故。手工核对脚本和之前清理扫描的脚本正好是一对一个从数据库往磁盘查一个从磁盘往数据库查# 数据库角度查出存储路径不在磁盘上的记录会 404 的记录 mysql -N -e SELECT storage_path FROM tmpnetdisk.upload_file WHERE expire_at NOW() \ | while read rel; do if [ ! -f /data/www/tmpnetdisk_storage/$rel ]; then echo missing file: $rel fi done把查询结果和 scan_orphans.php 的输出放一起看missing file 说明文件丢了影响用户orphan 说明磁盘上有多余文件影响容量。我的习惯是每周跑一次这两条输出为空才表示系统是健康的。上线前按下面这个清单过一遍上传一个超过 100MB 的真实文件确认三层大小限制都放开下载时用带中文名的文件确认浏览器显示原文件名把 expire_at 改成过去确认下载接口立即返回 404跑清理脚本 dry-run确认输出里包含刚刚过期的那条记录和对应文件扫一次孤儿文件确认没有新增垃圾模拟连续下载 10 次确认 download_count 到上限后链接失效。这套验证动作不需要自动化框架看着繁琐但能避免上线后十天磁盘打满才发现清理任务没配好。我自己就翻过一次车清理脚本的 crontab 少写了一个绝对路径文件一直在产生、一直没清理等磁盘报警时 storage 目录已经堆到了几十 GB。后来就养成了两个习惯所有定时任务一律用绝对路径并重定向日志每周跑一次上面的双核对脚本日志里出现 missing 或 orphan 就立刻处理。临时文件网盘这类系统的成败不在上传页面做得多好而在过期删除是否始终可靠这条验证链路就是最后的后悔药。希望帮到你。本文还有配套的精品资源点击获取