create repo
This commit is contained in:
commit
ba41a9f95b
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -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.
|
||||||
BIN
__pycache__/sync.cpython-310.pyc
Normal file
BIN
__pycache__/sync.cpython-310.pyc
Normal file
Binary file not shown.
4
rs.egg-info/PKG-INFO
Normal file
4
rs.egg-info/PKG-INFO
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
Metadata-Version: 2.1
|
||||||
|
Name: rs
|
||||||
|
Version: 0.1
|
||||||
|
License-File: LICENSE
|
||||||
9
rs.egg-info/SOURCES.txt
Normal file
9
rs.egg-info/SOURCES.txt
Normal file
@ -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
|
||||||
1
rs.egg-info/dependency_links.txt
Normal file
1
rs.egg-info/dependency_links.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
2
rs.egg-info/entry_points.txt
Normal file
2
rs.egg-info/entry_points.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[console_scripts]
|
||||||
|
rs = sync:app
|
||||||
3
rs.egg-info/requires.txt
Normal file
3
rs.egg-info/requires.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
Click
|
||||||
|
pyyaml
|
||||||
|
pyinotify
|
||||||
1
rs.egg-info/top_level.txt
Normal file
1
rs.egg-info/top_level.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
sync
|
||||||
16
setup.py
Normal file
16
setup.py
Normal file
@ -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
|
||||||
|
'''
|
||||||
|
)
|
||||||
4
sync.egg-info/PKG-INFO
Normal file
4
sync.egg-info/PKG-INFO
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
Metadata-Version: 2.1
|
||||||
|
Name: sync
|
||||||
|
Version: 0.1
|
||||||
|
License-File: LICENSE
|
||||||
9
sync.egg-info/SOURCES.txt
Normal file
9
sync.egg-info/SOURCES.txt
Normal file
@ -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
|
||||||
1
sync.egg-info/dependency_links.txt
Normal file
1
sync.egg-info/dependency_links.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
2
sync.egg-info/entry_points.txt
Normal file
2
sync.egg-info/entry_points.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[console_scripts]
|
||||||
|
sync = sync:app
|
||||||
3
sync.egg-info/requires.txt
Normal file
3
sync.egg-info/requires.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
Click
|
||||||
|
pyyaml
|
||||||
|
pyinotify
|
||||||
1
sync.egg-info/top_level.txt
Normal file
1
sync.egg-info/top_level.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
sync
|
||||||
264
sync.py
Normal file
264
sync.py
Normal file
@ -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):
|
||||||
Loading…
Reference in New Issue
Block a user