核心函數對比
| 函數 | 適用場景 | 性能 | 特性 |
copy($src, $dst) | 中小文件 (<100MB)、簡單複製 | ⚡⚡⚡ (C 級內部優化) | 自動創建目標目錄? 不會;覆蓋存在文件 |
stream_copy_to_stream($src, $dst) | 大文件、流式、可控緩衝 | ⚡⚡⚡ (內核零拷貝機會) | 需手動 fopen、支援任意流包裝器 |
file_put_contents($dst, file_get_contents($src)) | 極小文件、簡潔 | 🐢 (全量進內存) | 不適合大文件 |
shell_exec('cp -p ...') | 需保持屬性/權限/時間戳 | ⚡⚡ (系統調用) | 依賴 OS、Windows 不通用 |
1. 基礎複製 (copy)
<?php
$src = 'image/a1.jpg';
$dst = 'backup/a1.jpg';
if (!file_exists($src)) {
die("源文件不存在: $src");
}
// 確保目標目錄存在
$dstDir = dirname($dst);
if (!is_dir($dstDir)) {
mkdir($dstDir, 0755, true);
}
if (copy($src, $dst)) {
echo "複製成功: $dst\n";
// 可選:保持原文件時間
touch($dst, filemtime($src));
} else {
echo "複製失敗\n";
}
2. 大文件流式複製 (stream_copy_to_stream)
<?php
function copyLargeFile(string $src, string $dst, int $bufferSize = 8192): bool
{
if (!file_exists($src)) return false;
$dstDir = dirname($dst);
if (!is_dir($dstDir)) mkdir($dstDir, 0755, true);
$in = fopen($src, 'rb');
$out = fopen($dst, 'wb');
if (!$in || !$out) {
$in && fclose($in);
$out && fclose($out);
return false;
}
// 可選:非阻塞/異步 (需 stream_set_blocking)
$bytes = stream_copy_to_stream($in, $out, $bufferSize);
fclose($in);
fclose($out);
// 保持元數據
if ($bytes !== false) {
$stat = stat($src);
touch($dst, $stat['mtime'], $stat['atime']);
chmod($dst, $stat['mode'] & 0777);
chown($dst, $stat['uid']);
chgrp($dst, $stat['gid']);
return true;
}
return false;
}
// 使用
copyLargeFile('/data/video/large.mp4', '/backup/video/large.mp4');
3. 異步/非阻塞複製 (Swoole/RoadRunner 環境)
<?php
// Swoole 異步文件複製 (不阻塞 Worker)
function asyncCopy(string $src, string $dst, callable $callback): void
{
Swoole\Async::copy($src, $dst, function (bool $success) use ($callback) {
$callback($success);
});
}
// 使用
asyncCopy('/big/file.iso', '/backup/file.iso', function ($ok) {
Log::info($ok ? '異步複製完成' : '異步複製失敗');
});
// Worker 可繼續處理其他請求
4. 目錄遞歸複製
<?php
function copyDir(string $src, string $dst, bool $preserveMeta = true): bool
{
if (!is_dir($src)) return false;
if (!is_dir($dst)) mkdir($dst, 0755, true);
$items = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($src, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($items as $item) {
$relPath = $items->getSubPathName();
$target = $dst . DIRECTORY_SEPARATOR . $relPath;
if ($item->isDir()) {
if (!is_dir($target)) mkdir($target, 0755, true);
if ($preserveMeta) {
touch($target, $item->getMTime());
chmod($target, $item->getPerms() & 0777);
}
} else {
copy($item->getPathname(), $target);
if ($preserveMeta) {
touch($target, $item->getMTime());
chmod($target, $item->getPerms() & 0777);
}
}
}
return true;
}
5. 跨服務器複製 (SFTP/SSH2)
<?php
// 需 php-ssh2 擴展
function copyRemote(string $host, int $port, string $user, string $pass, string $remoteSrc, string $localDst): bool
{
$conn = ssh2_connect($host, $port);
if (!$conn) return false;
if (!ssh2_auth_password($conn, $user, $pass)) return false;
$sftp = ssh2_sftp($conn);
$remoteStream = fopen("ssh2.sftp://{$sftp}{$remoteSrc}", 'rb');
$localStream = fopen($localDst, 'wb');
if (!$remoteStream || !$localStream) return false;
$ok = stream_copy_to_stream($remoteStream, $localStream);
fclose($remoteStream);
fclose($localStream);
return $ok !== false;
}
常見錯誤與排查
| 錯誤 | 原因 | 修復 |
failed to open stream: Permission denied | 目標目錄不可寫 | chown www:www /target/dir、chmod 755 |
copy(): The first argument to copy() function cannot be a directory | 誤傳目錄 | 使用 copyDir() 遞歸 |
| 大文件複製中斷 | PHP max_execution_time/內存限制 | 用 stream_copy_to_stream + set_time_limit(0) |
| Windows 路徑分隔符 | 反斜杠轉義 | 用 DIRECTORY_SEPARATOR 或正斜杠 / (PHP 自動兼容) |
總結
文件複製選型:<100MB 用 copy()、>100MB 用 stream_copy_to_stream()、需保持屬性用 stat()+touch()+chmod()、異步環境用 Swoole Async、跨機器用 SSH2 SFTP。始終檢查返回值、處理異常、確保目標目錄存在。