函數簽名與參數
/**
* 遞迴複製目錄內容
* @param string $source 來源目錄
* @param string $destination 目的目錄
* @param bool $child 是否包含子目錄
* @return int 1=成功, 0=失敗
*/
function xCopy($source, $destination, $child) { ... }
完整實作代碼
<?php
function xCopy($source, $destination, $child) {
// 1. 來源必須是目錄
if (!is_dir($source)) {
trigger_error("Error: \$source ($source) is not a directory!", E_USER_WARNING);
return 0;
}
// 2. 目的目錄不存在則建立
if (!is_dir($destination)) {
if (!mkdir($destination, 0777, true)) {
trigger_error("Error: Cannot create directory $destination", E_USER_WARNING);
return 0;
}
}
$handle = @opendir($source);
if (!$handle) {
trigger_error("Error: Cannot open directory $source", E_USER_WARNING);
return 0;
}
while (($entry = readdir($handle)) !== false) {
if ($entry === '.' || $entry === '..') continue;
$srcPath = $source . '/' . $entry;
$dstPath = $destination . '/' . $entry;
if (is_dir($srcPath)) {
// 3. 子目錄:遞迴呼叫
if ($child) {
$result = xCopy($srcPath, $dstPath, $child);
if ($result === 0) return 0; // 向上傳遞失敗
}
} else {
// 4. 檔案:直接複製
if (!@copy($srcPath, $dstPath)) {
trigger_error("Warning: Failed to copy $srcPath to $dstPath", E_USER_WARNING);
// 可選:決定是否繼續或中止
}
}
}
closedir($handle);
return 1;
}
?>
使用範例
// 複製完整目錄樹(含子目錄)
xCopy("/var/www/html/project", "/backup/project_".date("Ymd"), true);
// 僅複製第一層檔案(不含子目錄)
xCopy("/var/www/html/project", "/backup/project_files_only", false);
進階強化版
function xCopyAdvanced($source, $destination, $options = []) {
$defaults = [
'recursive' => true, // 遞迴子目錄
'overwrite' => true, // 覆蓋既有檔案
'preserve_permissions' => true, // 保留權限/擁有者/時間
'exclude_patterns' => [], // 排除模式 ['.git','*.log','tmp/']
'callback' => null, // 進度回呼 function($src,$dst,$status)
'max_file_size' => 0, // 0=不限制
];
$opts = array_merge($defaults, $options);
// 正規化路徑
$source = realpath($source);
if (!$source || !is_dir($source)) throw new InvalidArgumentException("Invalid source");
$destination = realpath($destination) ?: $destination;
return _xCopyRecursive($source, $destination, $opts, 0);
}
function _xCopyRecursive($src, $dst, $opts, $depth) {
$handle = @opendir($src);
if (!$handle) return false;
if (!is_dir($dst)) {
if (!mkdir($dst, 0777, true)) return false;
if ($opts['preserve_permissions']) {
$stat = stat($src);
@chmod($dst, $stat['mode']);
@chown($dst, $stat['uid']);
@chgrp($dst, $stat['gid']);
}
}
while (($entry = readdir($handle)) !== false) {
if ($entry === '.' || $entry === '..') continue;
// 排除模式匹配
if ($opts['exclude_patterns']) {
$matched = false;
foreach ($opts['exclude_patterns'] as $pattern) {
if (fnmatch($pattern, $entry) || fnmatch($pattern, $src.'/'.$entry)) {
$matched = true; break;
}
}
if ($matched) continue;
}
$srcPath = $src . '/' . $entry;
$dstPath = $dst . '/' . $entry;
if ($file->isDir()) {
if ($opts['recursive']) {
if (!_xCopyRecursive($srcPath, $dstPath, $opts, $depth+1)) return false;
}
} else {
// 檔案大小限制
if ($opts['max_file_size'] > 0 && filesize($srcPath) > $opts['max_file_size']) {
if ($opts['callback']) $opts['callback']($srcPath, $dstPath, 'skipped_size');
continue;
}
// 覆蓋檢查
if (file_exists($dstPath) && !$opts['overwrite']) {
if ($opts['callback']) $opts['callback']($srcPath, $dstPath, 'skipped_exists');
continue;
}
if (@copy($srcPath, $dstPath)) {
if ($opts['preserve_permissions']) {
$stat = stat($srcPath);
@chmod($dstPath, $stat['mode']);
@touch($dstPath, $stat['mtime']);
}
if ($opts['callback']) $opts['callback']($srcPath, $dstPath, 'copied');
} else {
if ($opts['callback']) $opts['callback']($srcPath, $dstPath, 'failed');
return false;
}
}
}
closedir($handle);
return true;
}
現代替代方案
| 方案 | 優點 | 適用場景 |
cp -r (shell_exec) | 系統級、最快、保留所有屬性 | Linux/Unix 環境、大量資料 |
rsync -a | 增量同步、斷點續傳、網路傳輸 | 備份、部署、跨伺服器同步 |
| Symfony Finder + Filesystem | OOP、測試友善、跨平台 | 現代 PHP 專案、Composer 專案 |
PHP 內建 copy() + RecursiveIterator | 無依賴、細粒度控制 | 需自訂邏輯、過濾、進度回報 |
常見坑
- Windows 路徑分隔符
\ vs / → 用 DIRECTORY_SEPARATOR 或統一 /
- 權限不足:PHP 進程需具備來源讀取、目的寫入權限
- 符號連結:
is_dir() 返回 true,copy() 會複製連結而非目標 → 需 is_link() 特殊處理
- 開啟檔案過多:大目錄遞迴可能耗盡檔案描述符 → 分批或增加
ulimit -n