|
|
|
|
@ -1,6 +1,6 @@
|
|
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
|
|
|
|
VERSION = "0.2.0"
|
|
|
|
|
VERSION = "0.2.1"
|
|
|
|
|
|
|
|
|
|
import re
|
|
|
|
|
import sys
|
|
|
|
|
@ -121,26 +121,25 @@ def _index_by_name(nodes: List[Dict[str, Optional[str]]], name: str) -> int:
|
|
|
|
|
return -1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Accepts either "user@host" or "user@host:/absolute/dir"
|
|
|
|
|
_NODE_ARG_RE = re.compile(r"^(?P<name>[^:]+):(?P<dir>/.*)$")
|
|
|
|
|
|
|
|
|
|
def _is_pathlike(p: str) -> bool:
|
|
|
|
|
"""Return True if token looks like a remote path we should pass to rsync."""
|
|
|
|
|
return p.startswith(("/", "~", "./", "../"))
|
|
|
|
|
|
|
|
|
|
def _parse_node_token(token: str) -> Tuple[str, Optional[str]]:
|
|
|
|
|
"""Parse a node token into (name, remote_dir).
|
|
|
|
|
|
|
|
|
|
Supported forms:
|
|
|
|
|
"user@host" -> ("user@host", None)
|
|
|
|
|
"user@host:/path/to/dir" -> ("user@host", "/path/to/dir")
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
token: CLI argument representing a node.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Tuple of (node_name, remote_dir_or_None).
|
|
|
|
|
"""
|
|
|
|
|
m = _NODE_ARG_RE.match(token)
|
|
|
|
|
if m:
|
|
|
|
|
return m.group("name"), m.group("dir")
|
|
|
|
|
Parse a node token into (name, remote_dir).
|
|
|
|
|
|
|
|
|
|
Supports:
|
|
|
|
|
- "user@host" -> ("user@host", None)
|
|
|
|
|
- "user@host:/abs/path" -> ("user@host", "/abs/path")
|
|
|
|
|
- "user@host:~/home/path" -> ("user@host", "~/home/path")
|
|
|
|
|
- "user@host:./relative" -> ("user@host", "./relative")
|
|
|
|
|
- "user@host:../relative" -> ("user@host", "../relative")
|
|
|
|
|
"""
|
|
|
|
|
if ":" in token:
|
|
|
|
|
name, tail = token.split(":", 1)
|
|
|
|
|
if name and tail and _is_pathlike(tail):
|
|
|
|
|
return name, tail
|
|
|
|
|
return token, None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -479,16 +478,15 @@ node_modules/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def add_nodes(*args: str) -> None:
|
|
|
|
|
"""Add one or multiple nodes.
|
|
|
|
|
|
|
|
|
|
Supported forms:
|
|
|
|
|
dsync add-node user@host
|
|
|
|
|
dsync add-node user@host:/remote/dir
|
|
|
|
|
dsync add-node user@host /remote/dir # alt form for a single node
|
|
|
|
|
dsync add-node user@h1 user@h2:/dir user@h3 ...
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
*args: Node tokens, see forms above.
|
|
|
|
|
"""
|
|
|
|
|
Add one or multiple nodes.
|
|
|
|
|
|
|
|
|
|
Supported forms (can be mixed in one command):
|
|
|
|
|
dsync add-node user@h1
|
|
|
|
|
dsync add-node user@h1:/path
|
|
|
|
|
dsync add-node user@h1 ~/path # as two tokens (pair)
|
|
|
|
|
dsync add-node user@h1 ./rel # pair with relative
|
|
|
|
|
dsync add-node user@h1 /abs user@h2:~/p2 user@h3
|
|
|
|
|
"""
|
|
|
|
|
if not args:
|
|
|
|
|
print(colored("No nodes provided", Colors.YELLOW))
|
|
|
|
|
@ -498,35 +496,67 @@ def add_nodes(*args: str) -> None:
|
|
|
|
|
nodes = _normalize_nodes(config)
|
|
|
|
|
master_dir = config.get("master_dir", str(project_dir))
|
|
|
|
|
|
|
|
|
|
# Special alt-form: exactly two args "user@host /remote/dir"
|
|
|
|
|
if len(args) == 2 and args[1].startswith(("/", ".", "~")):
|
|
|
|
|
name, rdir = args[0], args[1]
|
|
|
|
|
added_or_updated = False
|
|
|
|
|
i = 0
|
|
|
|
|
n = len(args)
|
|
|
|
|
|
|
|
|
|
while i < n:
|
|
|
|
|
tok = args[i]
|
|
|
|
|
|
|
|
|
|
# 1) Full "node:path" form?
|
|
|
|
|
name, rdir = _parse_node_token(tok)
|
|
|
|
|
if rdir is not None:
|
|
|
|
|
idx = _index_by_name(nodes, name)
|
|
|
|
|
if idx >= 0:
|
|
|
|
|
print(colored(f"Node {name} already exists", Colors.YELLOW))
|
|
|
|
|
# update existing node dir
|
|
|
|
|
nodes[idx]["remote_dir"] = rdir
|
|
|
|
|
print(colored(f"Updated node: {name} ({rdir})", Colors.GREEN))
|
|
|
|
|
else:
|
|
|
|
|
nodes.append({"name": name, "remote_dir": rdir})
|
|
|
|
|
print(colored(f"Added node: {name} ({rdir})", Colors.GREEN))
|
|
|
|
|
config["nodes"] = nodes
|
|
|
|
|
save_config(config, project_dir)
|
|
|
|
|
return
|
|
|
|
|
added_or_updated = True
|
|
|
|
|
i += 1
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
added_any = False
|
|
|
|
|
for tok in args:
|
|
|
|
|
name, rdir = _parse_node_token(tok)
|
|
|
|
|
# 2) Pair form: "node" followed by a path-like token
|
|
|
|
|
next_is_path = (i + 1 < n) and _is_pathlike(args[i + 1])
|
|
|
|
|
if not _is_pathlike(tok) and next_is_path:
|
|
|
|
|
name = tok
|
|
|
|
|
rdir = args[i + 1]
|
|
|
|
|
idx = _index_by_name(nodes, name)
|
|
|
|
|
if idx >= 0:
|
|
|
|
|
print(colored(f"Node {name} already exists", Colors.YELLOW))
|
|
|
|
|
continue
|
|
|
|
|
nodes[idx]["remote_dir"] = rdir
|
|
|
|
|
print(colored(f"Updated node: {name} ({rdir})", Colors.GREEN))
|
|
|
|
|
else:
|
|
|
|
|
nodes.append({"name": name, "remote_dir": rdir})
|
|
|
|
|
print(colored(f"Added node: {name} ({rdir or master_dir})", Colors.GREEN))
|
|
|
|
|
added_any = True
|
|
|
|
|
print(colored(f"Added node: {name} ({rdir})", Colors.GREEN))
|
|
|
|
|
added_or_updated = True
|
|
|
|
|
i += 2
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
# 3) Lone path-like token (e.g., "/abs") — ignore with warning
|
|
|
|
|
if _is_pathlike(tok):
|
|
|
|
|
print(colored(f"Ignored stray path token: {tok}", Colors.YELLOW))
|
|
|
|
|
i += 1
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
# 4) Bare node (no path provided)
|
|
|
|
|
name = tok
|
|
|
|
|
idx = _index_by_name(nodes, name)
|
|
|
|
|
if idx >= 0:
|
|
|
|
|
# already exists, keep its dir; do not spam
|
|
|
|
|
print(colored(f"Node {name} already exists", Colors.YELLOW))
|
|
|
|
|
else:
|
|
|
|
|
nodes.append({"name": name, "remote_dir": None})
|
|
|
|
|
print(colored(f"Added node: {name} ({master_dir})", Colors.GREEN))
|
|
|
|
|
added_or_updated = True
|
|
|
|
|
i += 1
|
|
|
|
|
|
|
|
|
|
if added_any:
|
|
|
|
|
if added_or_updated:
|
|
|
|
|
config["nodes"] = nodes
|
|
|
|
|
save_config(config, project_dir)
|
|
|
|
|
else:
|
|
|
|
|
print(colored("No new nodes added", Colors.YELLOW))
|
|
|
|
|
print(colored("No new nodes added or updated", Colors.YELLOW))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def del_nodes(*nodes_to_remove: str) -> None:
|
|
|
|
|
|