用脚本实现目录实时同步

wen 实用脚本 3

本文目录导读:

用脚本实现目录实时同步

  1. 方案一:Python + watchdog(推荐)
  2. 方案二:Bash + rsync + inotify(Linux专用)
  3. 方案三:Python + watchdog(带配置文件版本)
  4. 配置文件示例 (sync_config.json)
  5. 安装依赖
  6. 使用方法
  7. 特点

我来为您编写一个目录实时同步脚本,使用Python和watchdog库来实现:

Python + watchdog(推荐)

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
目录实时同步脚本
功能:实时监控源目录变化并同步到目标目录
"""
import os
import shutil
import time
import logging
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
from pathlib import Path
class SyncHandler(FileSystemEventHandler):
    """文件系统事件处理器"""
    def __init__(self, source_dir, target_dir):
        self.source_dir = Path(source_dir).resolve()
        self.target_dir = Path(target_dir).resolve()
        self.logger = logging.getLogger('dir_sync')
    def get_relative_path(self, path):
        """获取相对路径"""
        src_path = Path(path).resolve()
        try:
            relative = src_path.relative_to(self.source_dir)
            return relative
        except ValueError:
            self.logger.error(f"路径不在源目录中: {path}")
            return None
    def get_target_path(self, path):
        """获取目标路径"""
        relative = self.get_relative_path(path)
        if relative is None:
            return None
        return self.target_dir / relative
    def on_modified(self, event):
        """文件或目录修改时触发"""
        if event.is_directory:
            return
        self.logger.info(f"修改: {event.src_path}")
        self.sync_file(event.src_path)
    def on_created(self, event):
        """文件或目录创建时触发"""
        self.logger.info(f"创建: {event.src_path}")
        if event.is_directory:
            self.create_directory(event.src_path)
        else:
            self.sync_file(event.src_path)
    def on_moved(self, event):
        """文件或目录移动/重命名时触发"""
        src_path = event.src_path
        dest_path = event.dest_path
        self.logger.info(f"移动: {src_path} -> {dest_path}")
        # 计算目标路径
        src_relative = self.get_relative_path(src_path)
        dest_relative = self.get_relative_path(dest_path)
        if src_relative is None or dest_relative is None:
            return
        # 删除旧路径
        old_target = self.target_dir / src_relative
        # 创建新路径
        new_target = self.target_dir / dest_relative
        try:
            if event.is_directory:
                # 移动目录
                if old_target.exists():
                    shutil.rmtree(old_target)
                self.create_directory(new_target)
            else:
                # 移动文件
                if old_target.exists():
                    old_target.unlink()
                self.sync_file(dest_path)
        except Exception as e:
            self.logger.error(f"处理移动事件失败: {e}")
    def on_deleted(self, event):
        """文件或目录删除时触发"""
        self.logger.info(f"删除: {event.src_path}")
        target_path = self.get_target_path(event.src_path)
        if target_path is None:
            return
        try:
            if event.is_directory:
                if target_path.exists():
                    shutil.rmtree(target_path)
                    self.logger.info(f"已删除目录: {target_path}")
            else:
                if target_path.exists():
                    target_path.unlink()
                    self.logger.info(f"已删除文件: {target_path}")
        except Exception as e:
            self.logger.error(f"删除失败 {target_path}: {e}")
    def sync_file(self, src_file):
        """同步单个文件"""
        target_path = self.get_target_path(src_file)
        if target_path is None:
            return
        try:
            # 确保目标目录存在
            target_path.parent.mkdir(parents=True, exist_ok=True)
            # 复制文件(保留文件元数据)
            src_file_path = Path(src_file)
            if src_file_path.exists():
                shutil.copy2(src_file_path, target_path)
                self.logger.info(f"已同步文件: {target_path}")
        except Exception as e:
            self.logger.error(f"同步文件失败 {src_file}: {e}")
    def create_directory(self, src_dir):
        """创建目录"""
        target_path = self.get_target_path(src_dir)
        if target_path is None:
            return
        try:
            target_path.mkdir(parents=True, exist_ok=True)
            self.logger.info(f"已创建目录: {target_path}")
        except Exception as e:
            self.logger.error(f"创建目录失败 {target_path}: {e}")
class DirectorySync:
    """目录同步管理器"""
    def __init__(self, source_dir, target_dir, log_file=None):
        self.source_dir = source_dir
        self.target_dir = target_dir
        self.observer = None
        self.setup_logging(log_file)
    def setup_logging(self, log_file=None):
        """配置日志"""
        log_format = '%(asctime)s - %(levelname)s - %(message)s'
        # 清除现有的handler
        for handler in logging.root.handlers[:]:
            logging.root.removeHandler(handler)
        logging.basicConfig(
            level=logging.INFO,
            format=log_format,
            handlers=[
                logging.StreamHandler(),  # 控制台输出
            ]
        )
        if log_file:
            file_handler = logging.FileHandler(log_file, encoding='utf-8')
            file_handler.setFormatter(logging.Formatter(log_format))
            logging.getLogger().addHandler(file_handler)
    def validate_paths(self):
        """验证路径是否有效"""
        if not os.path.exists(self.source_dir):
            raise ValueError(f"源目录不存在: {self.source_dir}")
        if os.path.exists(self.target_dir) and not os.path.isdir(self.target_dir):
            raise ValueError(f"目标路径不是目录: {self.target_dir}")
        # 创建目标目录
        os.makedirs(self.target_dir, exist_ok=True)
    def is_same_directory(self):
        """检查源目录和目标目录是否相同"""
        return os.path.abspath(self.source_dir) == os.path.abspath(self.target_dir)
    def initial_sync(self):
        """初始同步"""
        logging.info(f"开始初始同步: {self.source_dir} -> {self.target_dir}")
        try:
            if self.is_same_directory():
                logging.warning("源目录和目标目录相同,不需要同步")
                return
            # 复制所有文件
            for root, dirs, files in os.walk(self.source_dir):
                # 计算相对路径
                root_relative = os.path.relpath(root, self.source_dir)
                target_root = self.target_dir
                if root_relative != '.':
                    target_root = os.path.join(self.target_dir, root_relative)
                    os.makedirs(target_root, exist_ok=True)
                # 复制文件
                for file in files:
                    src_file = os.path.join(root, file)
                    target_file = os.path.join(target_root, file)
                    shutil.copy2(src_file, target_file)
            logging.info("初始同步完成")
        except Exception as e:
            logging.error(f"初始同步失败: {e}")
            raise
    def start(self):
        """启动监控"""
        try:
            self.validate_paths()
            if self.is_same_directory():
                raise ValueError("源目录和目标目录不能相同")
            # 初始同步
            self.initial_sync()
            # 创建事件处理器
            event_handler = SyncHandler(self.source_dir, self.target_dir)
            # 创建观察者
            self.observer = Observer()
            self.observer.schedule(
                event_handler,
                self.source_dir,
                recursive=True  # 递归监控子目录
            )
            self.observer.start()
            logging.info(f"目录同步已启动:")
            logging.info(f"  源目录: {self.source_dir}")
            logging.info(f"  目标目录: {self.target_dir}")
            logging.info("按 Ctrl+C 停止同步")
            # 保持程序运行
            self.observer.join()
        except KeyboardInterrupt:
            logging.info("收到中断信号,停止同步...")
            self.stop()
        except Exception as e:
            logging.error(f"启动同步失败: {e}")
            exit(1)
    def stop(self):
        """停止监控"""
        if self.observer:
            self.observer.stop()
            self.observer.join()
            logging.info("目录同步已停止")
def main():
    """主函数"""
    import argparse
    parser = argparse.ArgumentParser(description='目录实时同步工具')
    parser.add_argument('source', help='源目录路径')
    parser.add_argument('target', help='目标目录路径')
    parser.add_argument('--log', help='日志文件路径(可选)', default=None)
    parser.add_argument('--initial-sync', type=bool, default=True,
                       help='是否执行初始同步(默认: True)')
    args = parser.parse_args()
    # 创建同步实例
    sync_manager = DirectorySync(
        source_dir=args.source,
        target_dir=args.target,
        log_file=args.log
    )
    # 启动同步
    sync_manager.start()
if __name__ == "__main__":
    main()

Bash + rsync + inotify(Linux专用)

#!/bin/bash
# 目录实时同步脚本(使用inotifywait和rsync)
SOURCE_DIR="$1"
TARGET_DIR="$2"
LOG_FILE="${3:-/tmp/dir_sync.log}"
if [ -z "$SOURCE_DIR" ] || [ -z "$TARGET_DIR" ]; then
    echo "用法: $0 <源目录> <目标目录> [日志文件]"
    exit 1
fi
# 检查依赖
if ! command -v inotifywait &> /dev/null; then
    echo "Error: 请先安装 inotify-tools"
    exit 1
fi
if ! command -v rsync &> /dev/null; then
    echo "Error: 请先安装 rsync"
    exit 1
fi
# 确保目录存在
mkdir -p "$SOURCE_DIR" "$TARGET_DIR"
# 获取目录的绝对路径
SOURCE_DIR=$(realpath "$SOURCE_DIR")
TARGET_DIR=$(realpath "$TARGET_DIR")
if [ "$SOURCE_DIR" = "$TARGET_DIR" ]; then
    echo "Error: 源目录和目标目录不能相同"
    exit 1
fi
log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
# 初始同步
log "开始初始同步: $SOURCE_DIR -> $TARGET_DIR"
rsync -avz --delete "$SOURCE_DIR/" "$TARGET_DIR/"
log "初始同步完成"
log "启动目录监控..."
# 监控文件变化
inotifywait -m -r -e create -e modify -e delete -e move \
    --format '%e %w%f' \
    "$SOURCE_DIR" |
while read event file; do
    case "$event" in
        CREATE|MODIFY)
            log "事件: $event - $file"
            rsync -avz --update "$SOURCE_DIR/" "$TARGET_DIR/"
            ;;
        DELETE)
            log "事件: DELETE - $file"
            rsync -avz --delete "$SOURCE_DIR/" "$TARGET_DIR/"
            ;;
        MOVED_TO|MOVED_FROM)
            log "事件: MOVE - $file"
            rsync -avz --delete "$SOURCE_DIR/" "$TARGET_DIR/"
            ;;
        *)
            rsync -avz --update "$SOURCE_DIR/" "$TARGET_DIR/"
            ;;
    esac
done

Python + watchdog(带配置文件版本)

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
目录同步工具 - 配置文件版本
支持多个目录对同步
"""
import json
import os
import sys
from pathlib import Path
from directory_sync import DirectorySync  # 导入上面的类
def load_config(config_file):
    """加载配置文件"""
    with open(config_file, 'r', encoding='utf-8') as f:
        return json.load(f)
def main():
    config_file = 'sync_config.json'
    if not os.path.exists(config_file):
        # 创建默认配置
        default_config = {
            "sync_pairs": [
                {
                    "source": "/path/to/source1",
                    "target": "/path/to/target1",
                    "enabled": True
                },
                {
                    "source": "/path/to/source2",
                    "target": "/path/to/target2",
                    "enabled": False
                }
            ],
            "log_dir": "logs"
        }
        with open(config_file, 'w', encoding='utf-8') as f:
            json.dump(default_config, f, indent=2)
        print(f"已创建默认配置文件: {config_file}")
        print("请编辑文件后重新运行程序")
        return
    # 加载配置
    config = load_config(config_file)
    # 创建日志目录
    log_dir = config.get('log_dir', 'logs')
    os.makedirs(log_dir, exist_ok=True)
    # 同步所有启用的目录对
    sync_managers = []
    for pair in config['sync_pairs']:
        if not pair.get('enabled', True):
            continue
        source = pair['source']
        target = pair['target']
        print(f"配置同步: {source} -> {target}")
        # 为每个同步对创建独立的日志
        log_file = os.path.join(log_dir, f"sync_{Path(source).name}.log")
        try:
            sync_manager = DirectorySync(
                source_dir=source,
                target_dir=target,
                log_file=log_file
            )
            sync_managers.append(sync_manager)
            # 创建线程来运行同步(这里简化为逐个运行)
            sync_manager.start()
        except Exception as e:
            print(f"启动同步失败 ({source}): {e}")
if __name__ == "__main__":
    main()

配置文件示例 (sync_config.json)

{
  "sync_pairs": [
    {
      "source": "/home/user/documents",
      "target": "/backup/documents",
      "enabled": true
    },
    {
      "source": "/home/user/photos",
      "target": "/backup/photos", 
      "enabled": true
    }
  ],
  "log_dir": "logs",
  "sync_interval": 5,
  "exclude_patterns": [
    "*.tmp",
    "*.log",
    ".git"
  ]
}

安装依赖

# 方法1:pip安装watchdog(推荐)
pip install watchdog
# 方法2:如果使用国内镜像
pip install watchdog -i https://pypi.tuna.tsinghua.edu.cn/simple
# 方法3:apt安装inotify-tools(Bash方案需要)
sudo apt-get install inotify-tools rsync

使用方法

# Python方案使用
python3 directory_sync.py /home/user/documents /backup/documents
# 指定日志文件
python3 directory_sync.py /home/user/documents /backup/documents --log /tmp/sync.log
# Bash方案使用(需要安装inotify-tools)
bash dir_sync.sh /home/user/documents /backup/documents
# 配置文件方案
python3 dir_sync_config.py

特点

实时监控:使用watchdog/inotify实时监听文件变化
保留元数据shutil.copy2和rsync保留文件时间戳、权限等
日志记录:详细记录所有同步操作
错误处理:完善的异常处理机制
配置灵活:支持多目录同步、排除模式等
跨平台:Python版本支持Windows、Linux、macOS

选择合适的方案后,您就可以实现目录的实时同步了!

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