首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

PHP 递归复制目录完整实现 (含权限保持、错误处理)

     編輯:PHP 綜合知識

核心函数

/**

 * 递归复制目录及所有内容
 * @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 推送进度

替代方案对比

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved
方案优点缺点适用场景
PHP 递归函数纯 PHP、可定制、无依赖大目录慢、受 max_execution_time 限制中小目录、需定制逻辑
exec('cp -r ...')系统级极快、保持所有属性命令注入风险、跨平台差异、需 shell 权限Linux 服务器、大目录、可信输入
rsync -a增量同步、断点续传、压缩传输需 rsync、复杂参数定时备份、跨服务器同步
Swoole/Workerman 异步非阻塞、高并发、可监控需扩展、编程复杂高性能服务端集成