copy()最直接但需检查返回值,目标目录须预创建,源路径应校验存在且可读;复制目录需用RecursiveDirectoryIterator递归遍历;大文件宜用stream_copy_to_stream()流式处理;批量操作前必须严格校验路径防穿越。

用 copy() 遍历复制最直接,但要注意失败不报错
copy() 是 PHP 原生函数,单次调用就能完成一个文件复制,适合小批量、路径明确的场景。但它默认不抛出异常,复制失败(比如权限不足、源文件不存在)只会返回 false,容易被忽略。
实操建议:
- 每次调用后必须检查返回值:
if (!copy($src, $dst)) { trigger_error("复制失败: $src → $dst", E_USER_WARNING); } - 目标目录必须已存在,
copy()不会自动创建父级路径,得提前用mkdir($dst_dir, 0755, true) - 源路径建议用
realpath($src)校验是否存在且可读,避免静默跳过
递归复制整个目录要用 RecursiveDirectoryIterator
如果要复制带子目录的整个文件夹(比如把 templates/ 批量拷到 backup/),不能只靠 copy()。PHP 没有内置的 cp -r 等价物,得手动遍历。
推荐用 SPL 的迭代器组合:
立即学习“PHP免费学习笔记(深入)”;
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($src_dir, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $item) {
$rel_path = $iterator->getSubPathName();
$dst_path = $dst_dir . '/' . $rel_path;
if ($item->isDir()) {
mkdir($dst_path, 0755, true);
} else {
copy($item->getPathname(), $dst_path);
}
}
关键点:
-
FilesystemIterator::SKIP_DOTS必须加,否则会把.和..当成目录处理,导致无限循环或路径错误 -
RecursiveIteratorIterator::SELF_FIRST确保先建目录再复制文件,否则文件复制时目标目录可能还不存在 - 注意
$dst_path的拼接方式,别漏掉/或多加斜杠,Windows 下反斜杠也得统一转为正斜杠或用dirname()/basename()
大文件或跨磁盘复制时,stream_copy_to_stream() 更稳
当单个文件超过几十 MB,或源/目标在不同挂载点(比如 NFS、Docker 卷),直接 copy() 可能因内存限制或底层 I/O 行为出问题。这时改用流式复制更可控。
示例:
$src = fopen($src_file, 'rb');
$dst = fopen($dst_file, 'wb');
if ($src && $dst) {
stream_copy_to_stream($src, $dst);
fclose($src);
fclose($dst);
}
优势和注意:
- 不会一次性把整个文件读进内存,适合大文件
- 返回实际复制的字节数,可用于校验完整性(对比
filesize($src_file)) - 必须确保
fopen()成功,尤其目标路径写权限、磁盘空间是否足够——这些错误不会由stream_copy_to_stream()报出,得自己检查$src和$dst是否为 resource
批量操作前务必校验路径合法性,防目录穿越
如果文件列表来自用户输入(比如表单传来的 files[]=a.php&files[]=../etc/passwd),不做过滤会触发目录穿越漏洞,把系统文件复制到 Web 可写目录下。
安全底线:
- 所有源路径必须用
realpath($user_input)解析,再判断是否落在允许的根目录内:strpos(realpath($src), $allowed_root) === 0 - 目标路径禁止包含
..、~、空字节等危险字符,可用basename($path)提取文件名,再拼到固定目标目录下 - 不要用
exec('cp')或shell_exec()调用系统命令——参数拼接极易被注入,且丧失 PHP 层的错误控制能力
路径校验这一步,90% 的批量复制脚本都漏掉,等真出事才发现不是文件没复制,而是复制到了不该去的地方。



















