本文目录导读:

在PHP项目中实现增量同步时,对比文件修改时间是最基础、最高效的策略之一,以下是几种常见的实现方法和完整的代码示例,涵盖了从简单到健壮的方案。
基础实现:比较 filemtime
这是最直接的方法,利用 PHP 的 filemtime() 函数获取文件最后修改时间。
代码示例
<?php
$sourceDir = '/path/to/source/';
$targetDir = '/path/to/target/';
function syncByMtime($source, $target, $exclude = ['.', '..', '.git', '.svn']) {
$files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($source, RecursiveDirectoryIterator::SKIP_DOTS)
);
$synced = 0;
$skipped = 0;
foreach ($files as $file) {
$relativePath = $files->getSubPathname();
// 排除不需要同步的文件
$skip = false;
foreach ($exclude as $pattern) {
if (strpos($relativePath, $pattern) !== false) {
$skip = true;
break;
}
}
if ($skip) {
continue;
}
$sourceFile = $file->getPathname();
$targetFile = $target . $relativePath;
// 获取源文件修改时间
$sourceTime = filemtime($sourceFile);
// 检查目标文件是否存在
if (file_exists($targetFile)) {
$targetTime = filemtime($targetFile);
// 只同步源文件更新的情况
if ($sourceTime > $targetTime) {
copy($sourceFile, $targetFile);
touch($targetFile, $sourceTime); // 保留原始修改时间
echo "Updated: $relativePath\n";
$synced++;
} else {
$skipped++;
}
} else {
// 目标文件不存在,创建目录并复制
$dir = dirname($targetFile);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
copy($sourceFile, $targetFile);
touch($targetFile, $sourceTime);
echo "Created: $relativePath\n";
$synced++;
}
}
echo "Synced: $synced, Skipped: $skipped\n";
return ['synced' => $synced, 'skipped' => $skipped];
}
// 执行同步
syncByMtime($sourceDir, $targetDir);
?>
带缓存的高效实现
对于大型项目,每次扫描所有文件性能较差,可以维护一个修改时间缓存文件。
<?php
class IncrementalSync {
private $sourceDir;
private $targetDir;
private $cacheFile;
private $cache = [];
public function __construct($sourceDir, $targetDir, $cacheFile = null) {
$this->sourceDir = rtrim($sourceDir, '/') . '/';
$this->targetDir = rtrim($targetDir, '/') . '/';
$this->cacheFile = $cacheFile ?? __DIR__ . '/sync_cache.json';
}
public function sync() {
$this->loadCache();
$changes = 0;
$files = $this->scanFiles();
foreach ($files as $relativePath => $sourceMtime) {
$targetFile = $this->targetDir . $relativePath;
$cachedMtime = $this->cache[$relativePath] ?? null;
// 检查是否需要同步
if ($cachedMtime === null || $sourceMtime > $cachedMtime) {
$this->syncFile($relativePath, $sourceMtime);
$this->cache[$relativePath] = $sourceMtime;
$changes++;
}
}
// 清理:删除目标中多余的缓存项
$this->cleanupCache($files);
$this->saveCache();
return $changes;
}
private function scanFiles() {
$files = [];
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($this->sourceDir, RecursiveDirectoryIterator::SKIP_DOTS)
);
foreach ($iterator as $file) {
if ($file->isFile()) {
$relativePath = $iterator->getSubPathname();
$files[$relativePath] = filemtime($file->getPathname());
}
}
return $files;
}
private function syncFile($relativePath, $mtime) {
$sourceFile = $this->sourceDir . $relativePath;
$targetFile = $this->targetDir . $relativePath;
$dir = dirname($targetFile);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
copy($sourceFile, $targetFile);
touch($targetFile, $mtime);
echo "Synced: $relativePath\n";
}
private function cleanupCache($currentFiles) {
// 删除缓存中但源文件已不存在的记录
foreach ($this->cache as $path => $mtime) {
if (!isset($currentFiles[$path])) {
unset($this->cache[$path]);
}
}
}
private function loadCache() {
if (file_exists($this->cacheFile)) {
$this->cache = json_decode(file_get_contents($this->cacheFile), true) ?? [];
}
}
private function saveCache() {
file_put_contents(
$this->cacheFile,
json_encode($this->cache, JSON_PRETTY_PRINT)
);
}
}
// 使用示例
$sync = new IncrementalSync('/path/to/source', '/path/to/target');
$syncedCount = $sync->sync();
echo "Total synced files: $syncedCount\n";
?>
使用 md5_file 结合修改时间的混合方案
修改时间可能被篡改,结合文件内容哈希可以更准确。
<?php
function smartSync($sourceDir, $targetDir, $hashDbFile = 'sync_hash.db') {
// 加载已有哈希数据库
$hashDb = file_exists($hashDbFile) ? unserialize(file_get_contents($hashDbFile)) : [];
$changedFiles = 0;
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($sourceDir, RecursiveDirectoryIterator::SKIP_DOTS)
);
$currentHashes = [];
foreach ($iterator as $file) {
$relativePath = str_replace($sourceDir, '', $file->getPathname());
$sourceFile = $file->getPathname();
$targetFile = $targetDir . $relativePath;
// 方法1: 先比较修改时间(快速筛选)
$sourceMtime = filemtime($sourceFile);
$needCheck = false;
if (!file_exists($targetFile)) {
$needCheck = true;
} elseif ($sourceMtime > filemtime($targetFile)) {
$needCheck = true;
}
// 方法2: 再用哈希验证(防止时间戳欺骗)
if ($needCheck) {
$hash = md5_file($sourceFile);
$currentHashes[$relativePath] = $hash;
if (!isset($hashDb[$relativePath]) || $hashDb[$relativePath] !== $hash) {
// 文件已更改,进行同步
$targetDirPath = dirname($targetFile);
if (!is_dir($targetDirPath)) {
mkdir($targetDirPath, 0755, true);
}
if (copy($sourceFile, $targetFile)) {
$hashDb[$relativePath] = $hash;
touch($targetFile, $sourceMtime);
$changedFiles++;
echo "Changed: $relativePath\n";
}
} else {
// 哈希相同但时间戳不同,更新时间戳
touch($targetFile, $sourceMtime);
}
} else {
// 文件未变化,保留哈希值
$currentHashes[$relativePath] = $hashDb[$relativePath] ?? md5_file($sourceFile);
}
}
// 更新哈希数据库
file_put_contents($hashDbFile, serialize($currentHashes));
return $changedFiles;
}
?>
命令行使用 rsync(推荐生产环境)
PHP 可以调用系统命令来使用更专业的工具:
<?php
function rsyncSync($source, $target, $options = '-avz') {
$source = rtrim($source, '/') . '/';
$target = rtrim($target, '/') . '/';
// 构建 rsync 命令
$cmd = sprintf(
'rsync %s --delete --times "%s" "%s" 2>&1',
$options,
$source,
$target
);
$output = [];
$returnVar = 0;
exec($cmd, $output, $returnVar);
if ($returnVar === 0) {
echo "Rsync completed successfully\n";
return implode("\n", $output);
} else {
echo "Rsync error: " . implode("\n", $output) . "\n";
return false;
}
}
// 使用示例
rsyncSync('/path/to/source', '/path/to/target');
?>
性能优化建议
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 小型项目 (<1000文件) | 基础 filemtime | 简单直接,足够快 |
| 中型项目 (1K-10K文件) | 带缓存的实现 | 减少重复扫描 |
| 大型项目 (>10K文件) | rsync 或 inotify | 专有工具更高效 |
| 实时同步 | inotify (Linux) + PHP | 事件驱动,无需轮询 |
注意事项
- 时区问题:
filemtime()返回 Unix 时间戳,不受时区影响 - 权限问题:确保 PHP 有读取源文件和写入目标文件的权限
- 符号链接:默认
RecursiveDirectoryIterator会跟随符号链接,可使用SKIP_LINKS跳过 - 大文件处理:对于大文件,考虑使用
stream_copy_to_stream避免内存溢出 - 并发处理:多进程同步时要注意文件锁
选择哪种方案取决于你的具体需求,对于大多数 PHP 项目,带缓存的 filemtime 方案已经足够高效和可靠。