PHP项目增量同步如何对比文件修改时间

wen PHP项目 27

本文目录导读:

PHP项目增量同步如何对比文件修改时间

  1. 基础实现:比较 filemtime
  2. 带缓存的高效实现
  3. 使用 md5_file 结合修改时间的混合方案
  4. 命令行使用 rsync(推荐生产环境)
  5. 性能优化建议
  6. 注意事项

在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 事件驱动,无需轮询

注意事项

  1. 时区问题filemtime() 返回 Unix 时间戳,不受时区影响
  2. 权限问题:确保 PHP 有读取源文件和写入目标文件的权限
  3. 符号链接:默认 RecursiveDirectoryIterator 会跟随符号链接,可使用 SKIP_LINKS 跳过
  4. 大文件处理:对于大文件,考虑使用 stream_copy_to_stream 避免内存溢出
  5. 并发处理:多进程同步时要注意文件锁

选择哪种方案取决于你的具体需求,对于大多数 PHP 项目,带缓存的 filemtime 方案已经足够高效和可靠。

抱歉,评论功能暂时关闭!