核心函式:支援遞迴、排除、大目錄
<?php
/**
* 刪除目錄中建立/修改時間超過 N 分鐘的檔案
*
* @param string $dir 目標目錄 (絕對路徑)
* @param int $minutes 分鐘閾值
* @param bool $recursive 是否遞迴子目錄
* @param array $exclude 排除的檔名/副檔名 (如 ['.gitignore', '.log'])
* @return int 刪除的檔案數
*/
function deleteOldFiles(string $dir, int $minutes, bool $recursive = false, array $exclude = []): int {
$dir = rtrim(realpath($dir), DIRECTORY_SEPARATOR);
if (!$dir || !is_dir($dir)) {
throw new InvalidArgumentException("Directory not found: $dir");
}
$cutoff = time() - $minutes * 60;
$deleted = 0;
$flags = FilesystemIterator::SKIP_DOTS | FilesystemIterator::UNIX_PATHS;
$iterator = $recursive
? new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, $flags), RecursiveIteratorIterator::CHILD_FIRST)
: new FilesystemIterator($dir, $flags);
foreach ($iterator as $file) {
if (!$file->isFile()) continue;
// 排除檢查
$name = $file->getFilename();
if (in_array($name, $exclude, true)) continue;
$ext = $file->getExtension();
if ($ext && in_array($ext, $exclude, true)) continue;
// 時間判斷:使用 mtime (內容修改時間),ctime (inode 變更) 視需求選擇
if ($file->getMTime() < $cutoff) {
if (@unlink($file->getPathname())) {
$deleted++;
} else {
error_log("Delete failed: {$file->getPathname()}");
}
}
}
return $deleted;
}
// 使用範例
$count = deleteOldFiles('/var/www/uploads/temp', 30, true, ['.gitkeep', 'lock']);
echo "已刪除 $count 個 30 分鐘前的暫存檔\n";
關鍵設計決策解析
| 選項 | 建議值 | 理由 |
| 時間基準 | getMTime() (修改時間) | 上傳/寫入完成即更新;getCTime() 受權限變更影響,getATime() 系統常關閉 |
| 迭代器 | FilesystemIterator / RecursiveDirectoryIterator | SPL 原生、記憶體效率高、自動跳過 . .. |
| 遞迴順序 | CHILD_FIRST | 先刪檔案再刪目錄(若需清空目錄),避免「目錄非空」錯誤 |
| 錯誤處理 | @unlink() + error_log() | 單檔失敗不中斷整體,記錄供事後審計 |
| 路徑處理 | realpath() + rtrim(..., DIRECTORY_SEPARATOR) | 統一分隔符、解析符號連結、防目錄遍歷 |
進階:定時任務部署
Linux Cron (每 5 分鐘執行)
# /etc/cron.d/cleanup-temp
*/5 * * * * www-data /usr/bin/php /var/www/scripts/cleanup.php >> /var/log/cleanup.log 2>&1
<?php
// cleanup.php
require_once __DIR__ . '/../vendor/autoload.php';
$dirs = [
'/var/www/uploads/temp' => 30,
'/var/www/cache' => 60,
'/tmp/php_sessions' => 1440, // 24h
];
foreach ($dirs as $dir => $min) {
try {
$n = deleteOldFiles($dir, $min, true);
echo "[OK] $dir: deleted $n files\n";
} catch (Throwable $e) {
echo "[ERR] $dir: " . $e->getMessage() . "\n";
}
}
Windows 任務排程器
- 建立基本任務 → 觸發:每日重複每 5 分鐘
- 動作:啟動程式
php.exe,參數 C:\www\scripts\cleanup.php
- 勾選「使用最高權限執行」
大目錄效能優化 (10 萬+ 檔案)
<?php
// 1. 分批處理,避免長時間佔用 CPU/IO
function deleteOldFilesBatched(string $dir, int $minutes, int $batchSize = 500): int {
$cutoff = time() - $minutes * 60;
$deleted = 0;
$iterator = new FilesystemIterator($dir, FilesystemIterator::SKIP_DOTS);
$batch = [];
foreach ($iterator as $file) {
if (!$file->isFile() || $file->getMTime() >= $cutoff) continue;
$batch[] = $file->getPathname();
if (count($batch) >= $batchSize) {
$deleted += unlinkBatch($batch);
$batch = [];
usleep(10000); // 10ms 讓出 CPU
}
}
if ($batch) $deleted += unlinkBatch($batch);
return $deleted;
}
function unlinkBatch(array $paths): int {
$count = 0;
foreach ($paths as $p) { if (@unlink($p)) $count++; }
return $count;
}
// 2. 極大目錄改用 find + xargs (系統層面最快)
/*
find /var/www/uploads/temp -type f -mmin +30 -print0 | xargs -0 -r -P4 rm -f
# -P4 並行 4 條刪除
*/
替代方案對比
| 方案 | 優點 | 缺點 | 適用場景 |
| PHP SPL 迭代器 | 跨平台、可嵌入業務邏輯、易測試 | 大量檔案較慢 | 中小目錄、需業務判斷 |
| shell_exec('find ...') | 極快、系統原生 | 依賴 Linux、注入風險、難除錯 | 超大目錄、純清理無業務邏輯 |
| 系統 tmpwatch / systemd-tmpfiles | 宣告式、開機自動、標準化 | 僅限臨時目錄、靈活度低 | /tmp、/var/tmp、快取目錄 |
| Redis 掃描 Key TTL | 記憶體級、無檔案系統開銷 | 需資料在 Redis | Session、Token、暫存資料 |
安全與審計建議
- ✅ **白名單目錄**:僅允許預設清單目錄,禁止動態傳入
$dir
- ✅ **最小權限**:執行腳本的用戶僅擁有目標目錄
rwx,無 sudo
- ✅ **刪除日誌**:記錄
檔案路徑、mtime、刪除時間、執行 PID
- ✅ **乾跑模式**:上線前加
$dryRun 參數只列印不刪除
- ✅ **保護關鍵檔**:
$exclude = ['.gitkeep', '.htaccess', 'index.php', 'config.php']