commit ba41a9f95ba2e29837442d461b9fa62d17f60c1f Author: sleptworld Date: Tue Mar 28 00:55:40 2023 +0800 create repo diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5e04ff2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 sleptworld + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/__pycache__/sync.cpython-310.pyc b/__pycache__/sync.cpython-310.pyc new file mode 100644 index 0000000..df341fe Binary files /dev/null and b/__pycache__/sync.cpython-310.pyc differ diff --git a/log.txt b/log.txt new file mode 100644 index 0000000..8b9bccc --- /dev/null +++ b/log.txt @@ -0,0 +1 @@ + lixiang@192.168.1.70's password: \ No newline at end of file diff --git a/rs.egg-info/PKG-INFO b/rs.egg-info/PKG-INFO new file mode 100644 index 0000000..30bacde --- /dev/null +++ b/rs.egg-info/PKG-INFO @@ -0,0 +1,4 @@ +Metadata-Version: 2.1 +Name: rs +Version: 0.1 +License-File: LICENSE diff --git a/rs.egg-info/SOURCES.txt b/rs.egg-info/SOURCES.txt new file mode 100644 index 0000000..45a57b1 --- /dev/null +++ b/rs.egg-info/SOURCES.txt @@ -0,0 +1,9 @@ +LICENSE +setup.py +sync.py +rs.egg-info/PKG-INFO +rs.egg-info/SOURCES.txt +rs.egg-info/dependency_links.txt +rs.egg-info/entry_points.txt +rs.egg-info/requires.txt +rs.egg-info/top_level.txt \ No newline at end of file diff --git a/rs.egg-info/dependency_links.txt b/rs.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/rs.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/rs.egg-info/entry_points.txt b/rs.egg-info/entry_points.txt new file mode 100644 index 0000000..5d152b8 --- /dev/null +++ b/rs.egg-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +rs = sync:app diff --git a/rs.egg-info/requires.txt b/rs.egg-info/requires.txt new file mode 100644 index 0000000..a69708b --- /dev/null +++ b/rs.egg-info/requires.txt @@ -0,0 +1,3 @@ +Click +pyyaml +pyinotify diff --git a/rs.egg-info/top_level.txt b/rs.egg-info/top_level.txt new file mode 100644 index 0000000..def1d9c --- /dev/null +++ b/rs.egg-info/top_level.txt @@ -0,0 +1 @@ +sync diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..8e20830 --- /dev/null +++ b/setup.py @@ -0,0 +1,16 @@ +from setuptools import setup, find_packages + +setup( + name='rs', + version='0.1', + py_modules=['sync'], + install_requires=[ + 'Click', + 'pyyaml', + 'pyinotify' + ], + entry_points=''' + [console_scripts] + rs=sync:app + ''' +) diff --git a/sync.egg-info/PKG-INFO b/sync.egg-info/PKG-INFO new file mode 100644 index 0000000..87bcc13 --- /dev/null +++ b/sync.egg-info/PKG-INFO @@ -0,0 +1,4 @@ +Metadata-Version: 2.1 +Name: sync +Version: 0.1 +License-File: LICENSE diff --git a/sync.egg-info/SOURCES.txt b/sync.egg-info/SOURCES.txt new file mode 100644 index 0000000..4b4d9c0 --- /dev/null +++ b/sync.egg-info/SOURCES.txt @@ -0,0 +1,9 @@ +LICENSE +setup.py +sync.py +sync.egg-info/PKG-INFO +sync.egg-info/SOURCES.txt +sync.egg-info/dependency_links.txt +sync.egg-info/entry_points.txt +sync.egg-info/requires.txt +sync.egg-info/top_level.txt \ No newline at end of file diff --git a/sync.egg-info/dependency_links.txt b/sync.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/sync.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/sync.egg-info/entry_points.txt b/sync.egg-info/entry_points.txt new file mode 100644 index 0000000..f3cf998 --- /dev/null +++ b/sync.egg-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +sync = sync:app diff --git a/sync.egg-info/requires.txt b/sync.egg-info/requires.txt new file mode 100644 index 0000000..a69708b --- /dev/null +++ b/sync.egg-info/requires.txt @@ -0,0 +1,3 @@ +Click +pyyaml +pyinotify diff --git a/sync.egg-info/top_level.txt b/sync.egg-info/top_level.txt new file mode 100644 index 0000000..def1d9c --- /dev/null +++ b/sync.egg-info/top_level.txt @@ -0,0 +1 @@ +sync diff --git a/sync.py b/sync.py new file mode 100644 index 0000000..6d3bba6 --- /dev/null +++ b/sync.py @@ -0,0 +1,264 @@ +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):