commit 8fd8b08fe2a19fb57a64cb7cc21f8d099138b616 Author: vvzvlad Date: Mon Aug 5 16:24:37 2024 +0300 init diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..a8b53e8 --- /dev/null +++ b/Pipfile @@ -0,0 +1,13 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +sysrsync = "*" +watchdog = "*" + +[dev-packages] + +[requires] +python_version = "3.11" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..0a5a715 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,77 @@ +{ + "_meta": { + "hash": { + "sha256": "9e22bfb09a5c4ca9d55498299d1b5eebe2d649be0adc7e90821d3c4a9195c038" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.11" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "sysrsync": { + "hashes": [ + "sha256:435f9eb620e68ffb18ca5cbad32b113396a432361c7722038eab65c97dd83bd5", + "sha256:9c8877f162dce9c480804445fca56cd19bf41b998d2172651468ccebfdc60850" + ], + "index": "pypi", + "markers": "python_version >= '3.6' and python_version < '4.0'", + "version": "==1.1.1" + }, + "toml": { + "hashes": [ + "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", + "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", + "version": "==0.10.2" + }, + "watchdog": { + "hashes": [ + "sha256:0144c0ea9997b92615af1d94afc0c217e07ce2c14912c7b1a5731776329fcfc7", + "sha256:03e70d2df2258fb6cb0e95bbdbe06c16e608af94a3ffbd2b90c3f1e83eb10767", + "sha256:093b23e6906a8b97051191a4a0c73a77ecc958121d42346274c6af6520dec175", + "sha256:123587af84260c991dc5f62a6e7ef3d1c57dfddc99faacee508c71d287248459", + "sha256:17e32f147d8bf9657e0922c0940bcde863b894cd871dbb694beb6704cfbd2fb5", + "sha256:206afc3d964f9a233e6ad34618ec60b9837d0582b500b63687e34011e15bb429", + "sha256:4107ac5ab936a63952dea2a46a734a23230aa2f6f9db1291bf171dac3ebd53c6", + "sha256:4513ec234c68b14d4161440e07f995f231be21a09329051e67a2118a7a612d2d", + "sha256:611be3904f9843f0529c35a3ff3fd617449463cb4b73b1633950b3d97fa4bfb7", + "sha256:62c613ad689ddcb11707f030e722fa929f322ef7e4f18f5335d2b73c61a85c28", + "sha256:667f3c579e813fcbad1b784db7a1aaa96524bed53437e119f6a2f5de4db04235", + "sha256:6e8c70d2cd745daec2a08734d9f63092b793ad97612470a0ee4cbb8f5f705c57", + "sha256:7577b3c43e5909623149f76b099ac49a1a01ca4e167d1785c76eb52fa585745a", + "sha256:998d2be6976a0ee3a81fb8e2777900c28641fb5bfbd0c84717d89bca0addcdc5", + "sha256:a3c2c317a8fb53e5b3d25790553796105501a235343f5d2bf23bb8649c2c8709", + "sha256:ab998f567ebdf6b1da7dc1e5accfaa7c6992244629c0fdaef062f43249bd8dee", + "sha256:ac7041b385f04c047fcc2951dc001671dee1b7e0615cde772e84b01fbf68ee84", + "sha256:bca36be5707e81b9e6ce3208d92d95540d4ca244c006b61511753583c81c70dd", + "sha256:c9904904b6564d4ee8a1ed820db76185a3c96e05560c776c79a6ce5ab71888ba", + "sha256:cad0bbd66cd59fc474b4a4376bc5ac3fc698723510cbb64091c2a793b18654db", + "sha256:d10a681c9a1d5a77e75c48a3b8e1a9f2ae2928eda463e8d33660437705659682", + "sha256:d4925e4bf7b9bddd1c3de13c9b8a2cdb89a468f640e66fbfabaf735bd85b3e35", + "sha256:d7b9f5f3299e8dd230880b6c55504a1f69cf1e4316275d1b215ebdd8187ec88d", + "sha256:da2dfdaa8006eb6a71051795856bedd97e5b03e57da96f98e375682c48850645", + "sha256:dddba7ca1c807045323b6af4ff80f5ddc4d654c8bce8317dde1bd96b128ed253", + "sha256:e7921319fe4430b11278d924ef66d4daa469fafb1da679a2e48c935fa27af193", + "sha256:e93f451f2dfa433d97765ca2634628b789b49ba8b504fdde5837cdcf25fdb53b", + "sha256:eebaacf674fa25511e8867028d281e602ee6500045b57f43b08778082f7f8b44", + "sha256:ef0107bbb6a55f5be727cfc2ef945d5676b97bffb8425650dadbb184be9f9a2b", + "sha256:f0de0f284248ab40188f23380b03b59126d1479cd59940f2a34f8852db710625", + "sha256:f27279d060e2ab24c0aa98363ff906d2386aa6c4dc2f1a374655d4e02a6c5e5e", + "sha256:f8affdf3c0f0466e69f5b3917cdd042f89c8c63aebdb9f7c078996f607cdb0f5" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==4.0.1" + } + }, + "develop": {} +} diff --git a/syncer.py b/syncer.py new file mode 100644 index 0000000..6a6a39c --- /dev/null +++ b/syncer.py @@ -0,0 +1,112 @@ +import time +import logging +import argparse +import sysrsync +from watchdog.observers import Observer +from watchdog.events import FileSystemEventHandler + +# Setup logging +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +logger = logging.getLogger() + +class SyncHandler(FileSystemEventHandler): + def __init__(self, local_dir, remote_dir, remote_user, remote_host, exclude_dirs, sync_interval, jump_host=None): + self.local_dir = local_dir + self.remote_dir = remote_dir + self.remote_user = remote_user + self.remote_host = remote_host + self.exclude_dirs = exclude_dirs + self.jump_host = jump_host + self.sync_interval = sync_interval + self.observer = Observer() + + # Initial sync + logger.info("Initial sync in both directions") + self.sync_local_to_remote() + self.sync_remote_to_local() + + self.observer.schedule(self, path=self.local_dir, recursive=True) + self.observer.start() + + def on_any_event(self, event): + if any(excluded in event.src_path for excluded in self.exclude_dirs): + return + logger.info(f"Local change detected: {event.src_path} - {event.event_type}") + self.sync_local_to_remote() + + def sync_local_to_remote(self): + ssh_command = f"ssh -J {self.jump_host}" if self.jump_host else "ssh" + try: + sysrsync.run( + source=self.local_dir, + destination=f"{self.remote_user}@{self.remote_host}:{self.remote_dir}", + options=[ + "--archive", + "--compress", + f"-e {ssh_command}" + ], + exclusions=self.exclude_dirs + ) + logger.info("Local => Remote complete") + except Exception as e: + logger.error(f"Error during local to remote sync: {e}") + + def sync_remote_to_local(self): + ssh_command = f"ssh -J {self.jump_host}" if self.jump_host else "ssh" + try: + sysrsync.run( + source=f"{self.remote_user}@{self.remote_host}:{self.remote_dir}", + destination=self.local_dir, + options=[ + "--archive", + "--compress", + f"-e {ssh_command}" + ], + exclusions=self.exclude_dirs + ) + logger.info("Local <= Remote complete") + except Exception as e: + logger.error(f"Error during remote to local sync: {e}") + + def start(self): + count = 0 + try: + while True: + time.sleep(self.sync_interval) + self.sync_remote_to_local() + if count % 10 == 0: + self.sync_local_to_remote() + count += 1 + except KeyboardInterrupt: + self.observer.stop() + self.observer.join() + +def main(): + parser = argparse.ArgumentParser(description="Two-way auto sync script using rsync") + parser.add_argument("--local-dir", required=True, help="Local directory to sync") + parser.add_argument("--remote-dir", required=True, help="Remote directory to sync") + parser.add_argument("--remote-user", default="root", help="Remote user") + parser.add_argument("--remote-host", required=True, help="Remote host") + parser.add_argument("--sync-interval", type=int, default=5, help="Sync interval in seconds") + parser.add_argument("--jump-host", help="Jump host for SSH") + parser.add_argument("--exclude-dirs", nargs='*', default=[".git", ".venv", "__pycache__", "node_modules", ".DS_Store"], help="Directories to exclude from sync") + + args = parser.parse_args() + + sync_handler = SyncHandler( + local_dir=args.local_dir, + remote_dir=args.remote_dir, + remote_user=args.remote_user, + remote_host=args.remote_host, + exclude_dirs=args.exclude_dirs, + sync_interval=args.sync_interval, + jump_host=args.jump_host + ) + + # Start the sync handler + sync_handler.start() + +if __name__ == "__main__": + main() + +