在日常开发和数据管理中,文件同步工具扮演着至关重要的角色。Syncovery Premium 作为一款专业的文件同步软件,支持多平台、多协议的文件同步与备份,其核心功能包括差异同步、定时任务、双向同步等。本文将使用 PHP 实现一个简易的文件同步工具,模拟 Syncovery 的核心逻辑,并详细解析实现思路。

一、文件同步的核心原理

文件同步的本质是保持两个或多个文件夹(源目录与目标目录)的文件一致性。主要分为以下几种同步模式:

  • 单向同步:将源目录的文件更新同步到目标目录
  • 双向同步:双方的文件变更相互同步
  • 镜像同步:使目标目录完全镜像源目录(删除目标目录中源目录不存在的文件)

实现文件同步的关键技术点包括:

  1. 文件差异检测(通过修改时间、文件大小、哈希值)
  2. 文件操作(复制、删除、创建目录)
  3. 同步策略管理
  4. 错误处理与日志记录

二、PHP 实现文件同步工具

下面我们将编写一个 PHP 类FileSyncer,实现基础的文件同步功能。

php

<?php
/**
 * 简易文件同步工具类
 * 模拟Syncovery Premium的核心同步功能
 */
class FileSyncer {
    // 源目录路径
    private $sourceDir;
    // 目标目录路径
    private $targetDir;
    // 同步模式:'one_way' 单向, 'two_way' 双向, 'mirror' 镜像
    private $syncMode;
    // 同步日志
    private $log = [];
    // 是否启用详细日志
    private $verbose = true;

    /**
     * 构造函数
     * @param string $sourceDir 源目录
     * @param string $targetDir 目标目录
     * @param string $syncMode 同步模式
     */
    public function __construct(string $sourceDir, string $targetDir, string $syncMode = 'one_way') {
        // 标准化目录路径(确保以斜杠结尾)
        $this->sourceDir = rtrim($sourceDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
        $this->targetDir = rtrim($targetDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
        $this->syncMode = $syncMode;
        
        // 验证目录有效性
        $this->validateDirectories();
    }

    /**
     * 验证目录是否存在且可访问
     * @throws Exception 当目录无效时抛出异常
     */
    private function validateDirectories() {
        if (!is_dir($this->sourceDir) || !is_readable($this->sourceDir)) {
            throw new Exception("源目录不存在或不可读: {$this->sourceDir}");
        }
        
        // 目标目录不存在则尝试创建
        if (!is_dir($this->targetDir)) {
            if (!mkdir($this->targetDir, 0755, true)) {
                throw new Exception("无法创建目标目录: {$this->targetDir}");
            }
            $this->log("已创建目标目录: {$this->targetDir}");
        } elseif (!is_writable($this->targetDir)) {
            throw new Exception("目标目录不可写: {$this->targetDir}");
        }
    }

    /**
     * 执行文件同步
     */
    public function sync() {
        $this->log("开始同步 - 源目录: {$this->sourceDir} 目标目录: {$this->targetDir} 模式: {$this->syncMode}");
        
        // 获取源目录和目标目录的文件列表
        $sourceFiles = $this->scanDirectory($this->sourceDir);
        $targetFiles = $this->scanDirectory($this->targetDir);
        
        // 根据同步模式执行不同的同步逻辑
        switch ($this->syncMode) {
            case 'one_way':
                $this->oneWaySync($sourceFiles, $targetFiles);
                break;
            case 'mirror':
                $this->mirrorSync($sourceFiles, $targetFiles);
                break;
            case 'two_way':
                $this->twoWaySync($sourceFiles, $targetFiles);
                break;
            default:
                throw new Exception("不支持的同步模式: {$this->syncMode}");
        }
        
        $this->log("同步完成");
        return $this->log;
    }

    /**
     * 扫描目录获取文件列表(包含相对路径和文件信息)
     * @param string $dir 目录路径
     * @return array 文件信息数组
     */
    private function scanDirectory(string $dir) {
        $files = [];
        $iterator = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
            RecursiveIteratorIterator::SELF_FIRST
        );
        
        foreach ($iterator as $file) {
            // 获取相对路径(相对于扫描的目录)
            $relativePath = $iterator->getSubPathname();
            $fullPath = $file->getPathname();
            
            // 记录文件基本信息
            $files[$relativePath] = [
                'path' => $fullPath,
                'size' => $file->getSize(),
                'mtime' => $file->getMTime(), // 最后修改时间
                'is_dir' => $file->isDir()
            ];
        }
        
        return $files;
    }

    /**
     * 单向同步:源目录 -> 目标目录
     * @param array $sourceFiles 源目录文件列表
     * @param array $targetFiles 目标目录文件列表
     */
    private function oneWaySync(array $sourceFiles, array $targetFiles) {
        // 处理源目录中的文件和目录
        foreach ($sourceFiles as $relativePath => $sourceInfo) {
            // 如果是目录,确保目标目录存在
            if ($sourceInfo['is_dir']) {
                $targetPath = $this->targetDir . $relativePath;
                if (!isset($targetFiles[$relativePath]) && !is_dir($targetPath)) {
                    mkdir($targetPath, 0755, true);
                    $this->log("创建目录: {$targetPath}");
                }
                continue;
            }
            
            // 如果目标文件不存在,直接复制
            if (!isset($targetFiles[$relativePath])) {
                $this->copyFile($sourceInfo['path'], $this->targetDir . $relativePath);
                continue;
            }
            
            // 目标文件存在,检查是否需要更新(通过修改时间和大小判断)
            $targetInfo = $targetFiles[$relativePath];
            if ($this->needsUpdate($sourceInfo, $targetInfo)) {
                $this->copyFile($sourceInfo['path'], $targetInfo['path']);
            }
        }
    }

    /**
     * 镜像同步:目标目录完全镜像源目录
     * @param array $sourceFiles 源目录文件列表
     * @param array $targetFiles 目标目录文件列表
     */
    private function mirrorSync(array $sourceFiles, array $targetFiles) {
        // 先执行单向同步
        $this->oneWaySync($sourceFiles, $targetFiles);
        
        // 删除目标目录中源目录不存在的文件和目录
        foreach ($targetFiles as $relativePath => $targetInfo) {
            if (!isset($sourceFiles[$relativePath])) {
                $this->deleteItem($targetInfo['path'], $targetInfo['is_dir']);
            }
        }
    }

    /**
     * 双向同步:双方的变更相互同步
     * @param array $sourceFiles 源目录文件列表
     * @param array $targetFiles 目标目录文件列表
     */
    private function twoWaySync(array $sourceFiles, array $targetFiles) {
        // 先处理源到目标的更新
        $this->oneWaySync($sourceFiles, $targetFiles);
        
        // 再处理目标到源的更新(交换源和目标执行单向同步)
        $tempSource = $this->sourceDir;
        $this->sourceDir = $this->targetDir;
        $this->targetDir = $tempSource;
        
        $this->oneWaySync($targetFiles, $sourceFiles);
        
        // 恢复源目录和目标目录
        $this->targetDir = $this->sourceDir;
        $this->sourceDir = $tempSource;
    }

    /**
     * 判断文件是否需要更新
     * @param array $sourceInfo 源文件信息
     * @param array $targetInfo 目标文件信息
     * @return bool 是否需要更新
     */
    private function needsUpdate(array $sourceInfo, array $targetInfo) {
        // 如果大小不同,需要更新
        if ($sourceInfo['size'] != $targetInfo['size']) {
            return true;
        }
        
        // 如果源文件修改时间更新,需要更新(允许1秒误差)
        if ($sourceInfo['mtime'] > $targetInfo['mtime'] + 1) {
            return true;
        }
        
        return false;
    }

    /**
     * 复制文件
     * @param string $sourcePath 源文件路径
     * @param string $targetPath 目标文件路径
     */
    private function copyFile(string $sourcePath, string $targetPath) {
        // 确保目标目录存在
        $targetDir = dirname($targetPath);
        if (!is_dir($targetDir)) {
            mkdir($targetDir, 0755, true);
            $this->log("创建目录: {$targetDir}");
        }
        
        // 复制文件
        if (copy($sourcePath, $targetPath)) {
            // 保持文件修改时间一致
            touch($targetPath, filemtime($sourcePath));
            $this->log("复制文件: {$sourcePath} -> {$targetPath}");
        } else {
            $this->log("复制文件失败: {$sourcePath} -> {$targetPath}", 'error');
        }
    }

    /**
     * 删除文件或目录
     * @param string $path 路径
     * @param bool $isDir 是否为目录
     */
    private function deleteItem(string $path, bool $isDir) {
        if ($isDir) {
            // 递归删除目录
            $this->deleteDirectory($path);
            $this->log("删除目录: {$path}");
        } else {
            // 删除文件
            if (unlink($path)) {
                $this->log("删除文件: {$path}");
            } else {
                $this->log("删除文件失败: {$path}", 'error');
            }
        }
    }

    /**
     * 递归删除目录
     * @param string $dir 目录路径
     * @return bool 是否删除成功
     */
    private function deleteDirectory(string $dir) {
        if (!is_dir($dir)) {
            return false;
        }
        
        $files = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
            RecursiveIteratorIterator::CHILD_FIRST
        );
        
        foreach ($files as $file) {
            if ($file->isDir()) {
                rmdir($file->getPathname());
            } else {
                unlink($file->getPathname());
            }
        }
        
        return rmdir($dir);
    }

    /**
     * 记录日志
     * @param string $message 日志消息
     * @param string $level 日志级别
     */
    private function log(string $message, string $level = 'info') {
        $timestamp = date('Y-m-d H:i:s');
        $logEntry = "[$timestamp] [$level] $message";
        $this->log[] = $logEntry;
        
        // 详细模式下实时输出日志
        if ($this->verbose) {
            echo $logEntry . PHP_EOL;
        }
    }
}

// 示例用法
try {
    // 初始化同步器(源目录、目标目录、同步模式)
    $syncer = new FileSyncer(
        '/path/to/source',  // 源目录
        '/path/to/target',  // 目标目录
        'mirror'            // 同步模式:one_way, mirror, two_way
    );
    
    // 执行同步
    $log = $syncer->sync();
    
    // 可以将日志保存到文件
    // file_put_contents('sync_log.txt', implode(PHP_EOL, $log), FILE_APPEND);
} catch (Exception $e) {
    echo "同步失败: " . $e->getMessage() . PHP_EOL;
    exit(1);
}
?>

三、代码功能解析

1. 类结构设计

FileSyncer类采用面向对象设计,主要包含以下核心组件:

  • 私有属性:存储源目录、目标目录、同步模式和日志信息
  • 构造函数:初始化参数并验证目录有效性
  • 核心方法:sync()作为入口,根据不同模式调用对应同步方法
  • 辅助方法:目录扫描、文件复制、删除、日志记录等

2. 目录扫描实现

使用RecursiveDirectoryIteratorRecursiveIteratorIterator实现递归扫描目录,获取所有文件的相对路径、大小、修改时间等信息。相对路径的使用是关键,它确保了在源目录和目标目录之间进行路径映射时的准确性。

3. 同步策略详解

  • 单向同步:只处理源目录到目标目录的更新,不删除目标目录的额外文件
  • 镜像同步:在单向同步基础上,删除目标目录中源目录不存在的文件,确保完全一致
  • 双向同步:通过交换源和目标目录执行两次单向同步,实现双向更新

4. 文件差异检测

采用 "修改时间 + 文件大小" 的双重判断机制:

  • 先比较文件大小,不同则需要更新
  • 大小相同则比较修改时间,源文件更新则需要同步
  • 这种方式比计算文件哈希更高效,适合大多数场景

四、扩展与优化方向

  1. 增量同步优化:对于大文件,可以采用分块比较的方式,只同步差异部分
  2. 断点续传:添加文件传输中断后的续传功能
  3. 过滤规则:支持按文件类型、大小、名称等设置过滤规则
  4. 定时任务:结合 Cron 或 Windows 任务计划实现定时同步
  5. 哈希校验:对重要文件添加 MD5/SHA1 哈希校验,确保文件完整性
  6. GUI 界面:结合 Web 框架实现可视化操作界面

五、注意事项

  1. 权限问题:确保 PHP 进程对源目录有读取权限,对目标目录有读写权限
  2. 符号链接:代码中未处理符号链接,生产环境需根据需求添加支持
  3. 大文件处理:PHP 默认有内存限制,处理超大文件时需调整memory_limit配置
  4. 性能考虑:扫描大量文件时可能耗时较长,可考虑添加缓存机制
  5. 错误处理:实际应用中应添加更完善的异常处理和重试机制

通过本文的实现,我们了解了文件同步工具的核心原理和 PHP 实现方式。虽然这个简易版本无法与 Syncovery Premium 这样的专业软件相比,但已经包含了文件同步的核心逻辑。开发者可以根据实际需求,在此基础上扩展更多高级功能,如多协议支持(FTP、SFTP、云存储)、增量同步算法优化等。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐