sync/sync.py
2023-03-28 00:55:40 +08:00

265 lines
6.9 KiB
Python

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):