核心函数
/**
* 递归复制目录及所有内容
* @param string $src 源目录绝对路径
* @param string $dst 目标目录绝对路径
* @param bool $preservePermissions 是否保持权限/属主 (需 root 权限)
* @return bool 成功返回 true
* @throws RuntimeException 失败抛出异常
*/
function copyDirectory(string $src, string $dst, bool $preservePermissions = false): bool {
// 1. 规范化路径 (去除尾部斜杠)
$src = rtrim($src, '/\\');
$dst = rtrim($dst, '/\\');
// 2. 源目录存在性与可读性检查
if (!is_dir($src)) {
throw new InvalidArgumentException("源目录不存在: $src");
}
if (!is_readable($src)) {
throw new RuntimeException("源目录不可读: $src");
}
// 3. 创建目标目录 (递归创建,模式 0755)
if (!is_dir($dst)) {
if (!mkdir($dst, 0755, true)) {
throw new RuntimeException("创建目标目录失败: $dst");
}
} elseif (!is_writable($dst)) {
throw new RuntimeException("目标目录不可写: $dst");
}
// 4. 打开源目录句柄
$handle = opendir($src);
if ($handle === false) {
throw new RuntimeException("无法打开源目录: $src");
}
// 5. 遍历复制
while (($entry = readdir($handle)) !== false) {
// 跳过 . 和 ..
if ($entry === '.' || $entry === '..') continue;
$srcPath = "$src/$entry";
$dstPath = "$dst/$entry";
if (is_dir($srcPath)) {
// 递归复制子目录
copyDirectory($srcPath, $dstPath, $preservePermissions);
} elseif (is_file($srcPath)) {
// 复制文件
if (!copy($srcPath, $dstPath)) {
throw new RuntimeException("复制文件失败: $srcPath -> $dstPath");
}
// 可选:保持权限/时间
if ($preservePermissions) {
$stat = stat($srcPath);
@chmod($dstPath, $stat['mode'] & 0777);
@touch($dstPath, $stat['mtime'], $stat['atime']);
}
}
// 符号链接/特殊文件可按需扩展
}
closedir($handle);
return true;
}
使用示例
try {
copyDirectory('/var/www/html/project', '/backup/project_20240115', true);
echo "目录复制成功";
} catch (Exception $e) {
error_log("复制失败: " . $e->getMessage());
echo "操作失败: " . $e->getMessage();
}
进阶版:支持进度回调、大文件流式复制、符号链接处理
function copyDirectoryAdvanced(string $src, string $dst, array $options = []): bool {
$defaults = [
'preservePermissions' => false,
'preserveLinks' => false, // 复制链接而非目标文件
'skipHidden' => true, // 跳过 . 开头隐藏文件
'maxFileSize' => 0, // 0 无限制,单位字节
'progressCallback' => null, // function(int $copiedBytes, int $totalBytes): void
];
$opts = array_merge($defaults, $options);
// 预计算总大小 (用于进度)
$totalSize = 0;
if ($opts['progressCallback']) {
$totalSize = calculateDirSize($src, $opts);
}
$copied = 0;
$handle = opendir($src);
while (($entry = readdir($handle)) !== false) {
if ($entry === '.' || $entry === '..') continue;
if ($opts['skipHidden'] && $entry[0] === '.') continue;
$srcPath = "$src/$entry";
$dstPath = "$dst/$entry";
if (is_link($srcPath)) {
if ($opts['preserveLinks']) {
$target = readlink($srcPath);
symlink($target, $dstPath);
} else {
// 按普通文件/目录处理目标
copyDirectoryAdvanced($srcPath, $dstPath, $opts);
}
} elseif (is_dir($srcPath)) {
copyDirectoryAdvanced($srcPath, $dstPath, $opts);
} elseif (is_file($srcPath)) {
$size = filesize($srcPath);
if ($opts['maxFileSize'] && $size > $opts['maxFileSize']) continue;
// 大文件分块复制 (避免内存峰值)
if ($size > 10 * 1024 * 1024) { // >10MB
$srcFp = fopen($srcPath, 'rb');
$dstFp = fopen($dstPath, 'wb');
while (!feof($srcFp)) {
$chunk = fread($srcFp, 8192);
fwrite($dstFp, $chunk);
$copied += strlen($chunk);
if ($opts['progressCallback']) $opts['progressCallback']($copied, $totalSize);
}
fclose($srcFp);
fclose($dstFp);
} else {
copy($srcPath, $dstPath);
$copied += $size;
}
if ($opts['preservePermissions']) {
$stat = stat($srcPath);
@chmod($dstPath, $stat['mode'] & 0777);
@touch($dstPath, $stat['mtime'], $stat['atime']);
}
if ($opts['progressCallback']) $opts['progressCallback']($copied, $totalSize);
}
}
closedir($handle);
return true;
}
function calculateDirSize(string $dir, array $opts): int {
$size = 0;
$handle = opendir($dir);
while (($entry = readdir($handle)) !== false) {
if ($entry === '.' || $entry === '..') continue;
if ($opts['skipHidden'] && $entry[0] === '.') continue;
$path = "$dir/$entry";
if (is_file($path)) $size += filesize($path);
elseif (is_dir($path)) $size += calculateDirSize($path, $opts);
}
closedir($handle);
return $size;
}
关键点总结
- 路径规范化:
rtrim($path, '/\\') 防双斜杠
- 错误即异常:统一抛出
RuntimeException,上层统一捕获日志
- 权限保持可选:
chmod/touch 需对应权限,生产环境慎用
- 大文件流式复制:
fread/fwrite 分块,避免 copy() 大文件内存峰值
- 符号链接策略:保持链接或解析目标,按业务选择
- 进度回调:长耗时操作必备,前端轮询/WebSocket 推送进度
替代方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
| PHP 递归函数 | 纯 PHP、可定制、无依赖 | 大目录慢、受 max_execution_time 限制 | 中小目录、需定制逻辑 |
exec('cp -r ...') | 系统级极快、保持所有属性 | 命令注入风险、跨平台差异、需 shell 权限 | Linux 服务器、大目录、可信输入 |
rsync -a | 增量同步、断点续传、压缩传输 | 需 rsync、复杂参数 | 定时备份、跨服务器同步 |
| Swoole/Workerman 异步 | 非阻塞、高并发、可监控 | 需扩展、编程复杂 | 高性能服务端集成 |
- 上一篇文章: PHP 实现轻量级事件机制:观察者模式与回调管理
- 下一篇文章: PHP 基于前序遍历树(MPTT)的无限级分类实现:非递归高效方案
Copyright ©
程式碼之家 All Rights Reserved