ThinkPHP 擴展庫提供的 Dir 類別 (ORG.Dir) 封裝了目錄遞迴掃描、檔案刪除、空目錄判斷等常用操作。本文透過實際專案「後台快取清理功能」展示其實戰應用,並補充進階用法與注意事項。
| 方法 | 說明 | 回傳值 |
|---|---|---|
Dir::isEmpty($path) | 檢查目錄是否為空 (無檔案、無子目錄) | bool |
Dir::del($path) | 遞迴刪除目錄及其所有內容 | bool |
Dir::getFiles($path, $pattern='*') | 取得目錄下所有檔案 (支援 glob 模式) | array |
Dir::getDirs($path) | 取得目錄下所有子目錄 | array |
Dir::create($path, $mode=0777) | 遞迴建立目錄 | bool |
Dir::copy($src, $dst) | 遞迴複製目錄 | bool |
Dir::getSize($path) | 計算目錄總大小 (bytes) | int |
專案運行久後 Runtime/ 目錄膨脹,需提供後台介面讓管理員按類型清理:
Runtime/Cache/)Runtime/Data/、Runtime/Temp/)Runtime/Logs/)<?php
class CacheAction extends Action {
// 快取類型對應路徑映射
private $cachePaths = [
'all_front' => ['path' => 'WEB_PATH.Runtime', 'label' => '前台全部快取'],
'template_f' => ['path' => 'WEB_PATH.Runtime/Cache', 'label' => '前台模板快取'],
'data_f' => ['path' => 'WEB_PATH.Runtime/Data', 'label' => '前台資料快取'],
'temp_f' => ['path' => 'WEB_PATH.Runtime/Temp', 'label' => '前台臨時檔案'],
'logs_f' => ['path' => 'WEB_PATH.Runtime/Logs', 'label' => '前台日誌檔案'],
'all_admin' => ['path' => 'APP_PATH.Runtime', 'label' => '後台全部快取'],
'template_a' => ['path' => 'APP_PATH.Runtime/Cache', 'label' => '後台模板快取'],
'data_a' => ['path' => 'APP_PATH.Runtime/Data', 'label' => '後台資料快取'],
'temp_a' => ['path' => 'APP_PATH.Runtime/Temp', 'label' => '後台臨時檔案'],
'logs_a' => ['path' => 'APP_PATH.Runtime/Logs', 'label' => '後台日誌檔案'],
];
public function index() {
// 顯示快取狀態:大小、檔案數
$status = [];
foreach ($this->cachePaths as $key => $info) {
$path = $info['path'];
$status[$key] = [
'label' => $info['label'],
'exists' => is_dir($path),
'size' => is_dir($path) ? $this->formatBytes(Dir::getSize($path)) : 'N/A',
'files' => is_dir($path) ? count(Dir::getFiles($path)) : 0,
];
}
$this->assign('cacheStatus', $status);
$this->display();
}
public function clear() {
$type = I('get.type', '', 'trim');
if (empty($type) || !isset($this->cachePaths[$type])) {
$this->error('無效的快取類型');
}
$path = $this->cachePaths[$type]['path'];
$label = $this->cachePaths[$type]['label'];
// 載入 Dir 類別 (放在 ORG 目錄下)
import('@.ORG.Dir');
if (!Dir::isEmpty($path)) {
if (Dir::del($path)) {
// 重建目錄結構 (避免 ThinkPHP 找不到目錄報錯)
$this->rebuildRuntimeStructure($path);
$this->success("{$label} 清理成功");
} else {
$this->error("{$label} 清理失敗,請檢查權限");
}
} else {
$this->success("{$label} 本身就是空的");
}
}
private function rebuildRuntimeStructure($clearedPath) {
// 根據清理的路徑重建必要子目錄
$subDirs = ['Cache', 'Data', 'Temp', 'Logs', 'Data/_fields'];
$base = str_replace(['WEB_PATH', 'APP_PATH'], ['', ''], $clearedPath);
// 實際專案中根據 WEB_PATH/APP_PATH 判斷基準路徑
foreach ($subDirs as $dir) {
$fullPath = dirname($clearedPath) . '/' . $dir;
if (!is_dir($fullPath)) Dir::create($fullPath);
}
}
private function formatBytes($bytes, $precision = 2) {
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
return round($bytes / (1024 ** $pow), $precision) . ' ' . $units[$pow];
}
}
?>
<table class="table table-striped">
<thead>
<tr><th>快取類型</th><th>狀態</th><th>大小</th><th>檔案數</th><th>操作</th></tr>
</thead>
<tbody>
<volist name="cacheStatus" id="item">
<tr>
<td>{$item.label}</td>
<td>
<eq name="item.exists" value="true">
<span class="badge badge-success">存在</span>
<else/>
<span class="badge badge-secondary">不存在</span>
</eq>
</td>
<td>{$item.size}</td>
<td>{$item.files}</td>
<td>
<eq name="item.exists" value="true">
<a href="<?php echo U('clear', ['type'=>$key]); ?>"
class="btn btn-sm btn-danger"
onclick="return confirm('確定清理 {$item.label} 嗎?')"
>清理</a>
<else/>
<span class="text-muted">無需清理</span>
</eq>
</td>
</tr>
</volist>
</tbody>
</table>
結合 Dir::getFiles() 實作模板列表、編輯、備份功能:
<?php
class TemplateAction extends Action {
public function listTemplates() {
import('@.ORG.Dir');
$tplPath = TMPL_PATH; // ThinkPHP 模板常數
// 遞迴取得所有 .html 檔案
$files = $this->getTemplateFiles($tplPath);
$list = [];
foreach ($files as $file) {
$relPath = str_replace($tplPath, '', $file);
$list[] = [
'path' => $relPath,
'size' => $this->formatBytes(filesize($file)),
'mtime' => date('Y-m-d H:i:s', filemtime($file)),
'url' => U('edit', ['file'=>urlencode($relPath)]),
];
}
$this->assign('templates', $list);
$this->display();
}
private function getTemplateFiles($dir) {
$files = [];
$items = Dir::getFiles($dir, '*.html');
foreach ($items as $item) {
if (is_dir($item)) {
$files = array_merge($files, $this->getTemplateFiles($item));
} else {
$files[] = $item;
}
}
return $files;
}
public function edit() {
$file = urldecode(I('get.file'));
$fullPath = TMPL_PATH . $file;
if (!file_exists($fullPath) || is_dir($fullPath)) {
$this->error('模板檔案不存在');
}
// 安全檢查:防止目錄遍歷
if (strpos($file, '..') !== false) {
$this->error('非法路徑');
}
$content = file_get_contents($fullPath);
$this->assign(['file'=>$file, 'content'=>$content]);
$this->display();
}
public function save() {
$file = urldecode(I('post.file'));
$content = I('post.content');
$fullPath = TMPL_PATH . $file;
// 備份原檔
$backupDir = RUNTIME_PATH . 'TemplateBackup/' . date('Ymd');
Dir::create($backupDir);
copy($fullPath, $backupDir . '/' . basename($file) . '_' . time() . '.bak');
if (file_put_contents($fullPath, $content) !== false) {
$this->success('儲存成功,已自動備份');
} else {
$this->error('儲存失敗');
}
}
}
?>
<?php
// 簡易版 Dir 類核心邏輯
class Dir {
public static function isEmpty($path) {
if (!is_dir($path)) return true;
$handle = opendir($path);
while (($entry = readdir($handle)) !== false) {
if ($entry !== '.' && $entry !== '..') {
closedir($handle);
return false;
}
}
closedir($handle);
return true;
}
public static function del($path) {
if (!is_dir($path)) return false;
$items = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($items as $item) {
$item->isDir() ? rmdir($item->getPathname()) : unlink($item->getPathname());
}
return rmdir($path);
}
public static function getFiles($path, $pattern = '*') {
$files = [];
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS)
);
foreach ($iterator as $file) {
if ($file->isFile() && ($pattern === '*' || fnmatch($pattern, $file->getFilename()))) {
$files[] = $file->getPathname();
}
}
return $files;
}
public static function getSize($path) {
$size = 0;
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS)
);
foreach ($iterator as $file) {
if ($file->isFile()) $size += $file->getSize();
}
return $size;
}
public static function create($path, $mode = 0777) {
return is_dir($path) || mkdir($path, $mode, true);
}
}
?>
Runtime/ 需 Web 使用者寫入權限 (通常 755 或 775),Dir::create() 預設 0777 受 umask 影響basename()、realpath() 驗證,禁止 ../ 遍歷Runtime/Trash/ 保留 7 天exec('rm -rf ' . escapeshellarg($path)) (需權限評估)getSize() 快取結果,避免重複掃描