import os import time import asyncio import platform import re from typing import Dict, List, Optional import sys import click import pexpect as pe import pyinotify import yaml from rich import print from rich.console import Console cwd = os.getcwd() wm = pyinotify.WatchManager() console = Console() DEFAULT_CONFIG_FILE_NAME = "sync.yaml" DEFAULT_CONFIG_PATH = os.path.join(cwd, DEFAULT_CONFIG_FILE_NAME) DEFAULT_CONFIG_PATHS = [cwd,os.path.expanduser("~"),r"/etc/conf.d/"] EVENTS = None CONFIG_ERROR = 1 class Config: DEFAULT_CONFIG = { "ssh": { "host": "", "port": 22, "user": "", "password": "", }, "workspace": [], "all":{ "events":[], "ignore":[] }, "log":{ "path":"", "level":"INFO" } } def __init__(self, level: int): self.__config = {} self.__changed_part = {} self.level = level @property def config(self): return self.__config @classmethod def default(cls): tmp = cls() tmp.change(cls.DEFAULT_CONFIG) return tmp @staticmethod def get_from_ctx(ctx): assert ctx.obj is None error = ctx.obj.get('error') config = ctx.obj.get('config') return (config, error) @classmethod def from_yaml(cls,path,level): with open(path, "r", encoding="utf-8") as f: try: __config = yaml.safe_load(f) tmp = cls(level) tmp.change(__config) return tmp except yaml.YAMLError as e: raise ConfigError(1, "config file error") def save(self,save_path:str): with open(save_path, 'w', encoding='utf-8') as f: yaml.safe_dump(self.config, f) def change(self, new_value): self.__config.update(new_value) self.__changed_part.update(new_value) def add_workspace(self, new_value:Dict): if self.__config.get("workspace") is None: self.__config["workspace"] = [] self.__config['workspace'].append(new_value) def __lt__(self, other): self.change(new_value = other.config) return self def __gt__(self, other): other.change(new_value = self.config) return other @property def ssh_info(self): return self.config['ssh'] @property def log_info(self): return self.config['log'] @property def workspace_info(self): return self.config['workspace'] class ConfigError(Exception): def __init__(self, code, message): super().__init__(code, message) self.code = code self.message = message def __str__(self): return (self.message) def main(events:Optional[List[str]]): if Optional is None: events = pyinotify.ALL_EVENTS else: events = [pyinotify.EventsCodes.ALL_EVENTS.get(event) for event in events] wm.add_watch(cwd, events) notifer = pyinotify.Notifier(wm, MyEventHandler()) notifer.loop() def get_cfg() -> str: for index,p in enumerate( [os.path.join(i,DEFAULT_CONFIG_FILE_NAME) for i in DEFAULT_CONFIG_PATHS ]): if os.path.exists(p): return (index, p) raise ConfigError(0,"config file not exists") # @click.argument('src') @click.command() @click.argument('dst') @click.option('--src', '-s', default='') @click.option('--events', '-e', multiple=True) @click.option('--port', default=22) @click.option('--ignore', '-i', default=None) @click.option('--config_path', '-c', default=DEFAULT_CONFIG_PATH) @click.pass_context def init(ctx,dst,src,events, port, ignore, config_path) -> Config: # init Config Class config,error = Config.get_from_ctx(ctx) try: if index == 0: click.echo("have init") return None except ConfigError as e: if e.code != 0: click.echo(e.message) exit(CONFIG_ERROR) finally: termux_config = Config() _ssh = dst.replace(" ", "") ssh_info = re.compile(r"(\w+)@([\w,.]+):/([\w,/]+)").match(_ssh) abs_path = (os.path.abspath(src)) dst = "/"+ssh_info.group(3) if not os.path.exists(abs_path): click.echo("ssh key not exists") exit(CONFIG_ERROR) if ssh_info is None: click.echo("ssh info error") exit(CONFIG_ERROR) pwd = click.prompt("password", hide_input=True,type=str) termux_config.change({ "ssh": { "host": ssh_info.group(2), "port":port, "user": ssh_info.group(1), "password": pwd }, "log": { "path": abs_path, "level": 0, }, }) @click.command() @click.option('--config_path', '-c', default=DEFAULT_CONFIG_PATH) @click.pass_context def sync(ctx,config_path): error = ctx.obj.get('error') config = ctx.obj.get('config') if error is not None or config is None: click.echo(error if not error else "" + "config") exit(CONFIG_ERROR) loop = asyncio.get_event_loop() password_re = re.compile(r"[pP]assword:") children = config.workspace_info.map(lambda workspace: pe.spawn("/bin/bash",[ "-c", "rsync -av -e ssh {} {}".format( workspace['path'], config.ssh_info['user'] + "@" + config.ssh_info['host'] + ":" + config.ssh_info['dir'] ), ],encoding='utf-8')) for child in children: child.logfile = sys.stdout.buffer async def answer_password(child): v = await child.expect_list([password_re],async_=True) if v == 0: v.sendline(config.ssh_info['password']) loop.run_until_complete(asyncio.gather( [answer_password(child) for child in children] )) @click.group(chain=True) @click.pass_context def app(ctx): ctx.ensure_object(dict) if platform.system() != "Linux": click.echo("only support linux") exit(1) try: index, file_config_path = get_cfg() config_from_file = Config.from_yaml(file_config_path) ctx.obj['config'] = config_from_file except ConfigError as e: ctx.obj['error'] = e app.add_command(init) app.add_command(sync) class MyEventHandler(pyinotify.ProcessEvent): def process_default(self, event): print(event.pathname) # def process_IN_ATTRIB(self, event): # pass # def process_IN_DELETE(self, event): # pass # def process_IN_OPEN(self, event): # pass # def process_IN_CLOSE_NOWRITE(self, event): # pass # def process_IN_CLOSE_WRITE(self, event): # pass # def process_IN_ACCESS(self, event): # pass # def process_IN_CREATE(self, event): # pass # def process_IN_MODIFY(self,event):