やみとものプログラミング日記 やみとものプログラミング日記
TOP ローカルとサーバーの自動同期スクリプトをPythonで書きました
ローカルとサーバーの自動同期スクリプトをPythonで書きました

ローカルとサーバーの自動同期スクリプトをPythonで書きました

Python プログラミング Linux
作成日時: 2020年1月7日
更新日時: 2021年3月7日
Webサイトのプログラムを修正するとき、ローカルの変更を即座にサーバーに反映するのに以前はVisual Studio Codeの拡張機能ftp-simpleを使っていました。

しかし、ローカルでgit操作ができなかったり、ディレクトリの一括検索ができなかったので、
ローカルの変更をサーバーに反映するスクリプトをPythonで作ってしまいました。
(スクリプト & 設定ファイル例は記事の最後)

githubリポジトリ

注意

サーバー上のファイルを自動で作成、編集、削除、移動するので、
スクリプトをよく読んで注意して使ってください。

使い方


python スクリプトファイル名 設定ファイルパス


スクリプト

from watchdog.events import FileSystemEventHandler
from watchdog.observers import Observer

import os
import time

import paramiko
import json
import sys
import scp

import re

default_ignore = [
    "^\.git.*$",
    "^vendor.*$",
    "^.*\.swx$",
    "^.*\.swp$",
    "^.*\.swo$",
]

class ChangeHandler(FileSystemEventHandler):
    def on_created(self, event):
        filepath = event.src_path

        checkpath = filepath[len(config["local_dir"]) + 1 : ]
        for pat in default_ignore:
            if re.match(pat, checkpath):
                return

        remote_path = config["remote_dir"] + filepath[len(config["local_dir"]) : ]

        if os.path.isdir(filepath):
            shell.send(f"mkdir {remote_path} \n")
            print(f"リモートディレクトリ[{remote_path}]を作成")
            return

        with scp.SCPClient(client.get_transport()) as scp_client:
            scp_client.put(filepath, remote_path)
            print(f"リモートファイル[{remote_path}]を作成")

    def on_modified(self, event):
        filepath = event.src_path

        checkpath = filepath[len(config["local_dir"]) + 1 : ]
        for pat in default_ignore:
            if re.match(pat, checkpath):
                return

        if os.path.isdir(filepath):
            return

        remote_path = config["remote_dir"] + filepath[len(config["local_dir"]) : ]

        with scp.SCPClient(client.get_transport()) as scp_client:
            try:
                scp_client.put(filepath, remote_path)
                print(f"リモートファイル[{remote_path}]を変更")
            except:
                print("[[[ERROR]]] 変更後のファイルのアップロードに失敗しました")
                print(f"[Error Info]filepath = {filepath}")
                print(f"[Error Info]remote_path = {remote_path}")

    def on_deleted(self, event):
        filepath = event.src_path

        checkpath = filepath[len(config["local_dir"]) + 1 : ]
        for pat in default_ignore:
            if re.match(pat, checkpath):
                return

        remote_path = config["remote_dir"] + filepath[len(config["local_dir"]) : ]
        shell.send(f"rm -r {remote_path} \n")
        print(f"リモートファイル[{remote_path}]を削除")

    def on_moved(self, event):
        filepath = event.dest_path

        checkpath = filepath[len(config["local_dir"]) + 1 : ]
        for pat in default_ignore:
            if re.match(pat, checkpath):
                return

        remote_path = config["remote_dir"] + filepath[len(config["local_dir"]) : ]

        if os.path.isdir(filepath):
            shell.send(f"mkdir {remote_path} \n")
            print(f"リモートディレクトリ[{remote_path}]を作成")
            return

        with scp.SCPClient(client.get_transport()) as scp_client:
            scp_client.put(filepath, remote_path)

        remote_src_path = config["remote_dir"] + event.src_path[len(config["local_dir"]) : ]
        shell.send(f"rm -r {remote_src_path} \n")
        print(f"リモートファイル[{remote_src_path}]を[{remote_path}]に移動")


if __name__ in '__main__':
    if len(sys.argv) != 2:
        print("Usage: python hiyo_sync.py config_path")
        exit()

    config_path = sys.argv[1]

    if not os.path.exists(config_path):
        print("Error: config file doesn't exist.")
        exit()

    with open(config_path) as f:
        config = json.load(f)

    # print(config)
    
    if re.match("^.*/$", config["local_dir"]) or re.match("^.*/$", config["remote_dir"]):
        print("Error: local_dirまたはremote_dirの末尾に/を付けないでください")
        exit()

    client = paramiko.SSHClient()
    client.load_system_host_keys()
    client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    client.connect(
        config["remote_host"], 
        username=config["username"],
        password=config["password"],
        port=22,
        timeout=15.0,
        look_for_keys=False
    )

    shell = client.invoke_shell()
    shell.send(f"cd {config['remote_dir']} \n")

    while 1:
        event_handler = ChangeHandler()
        observer = Observer()
        observer.schedule(event_handler, config["local_dir"], recursive=True)
        observer.start()
        try:
            while True:
                time.sleep(0.1)
        except KeyboardInterrupt:
            observer.stop()
        observer.join()

設定ファイル例

{
    "local_dir": "/local/directory/with/no/last/slash",
    "remote_dir": "/remote/directory/with/no/last/slash",
    "password": "yourpassword",
    "remote_host": "your.host.name",
    "username": "yourname"
}