machineconfig 5.66__py3-none-any.whl → 5.68__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of machineconfig might be problematic. Click here for more details.
- machineconfig/jobs/installer/installer_data.json +3 -3
- machineconfig/scripts/python/agents.py +41 -28
- machineconfig/scripts/python/ai/generate_files.py +127 -17
- machineconfig/scripts/python/ai/solutions/generic.py +1 -0
- machineconfig/scripts/python/croshell.py +1 -1
- machineconfig/scripts/python/devops_helpers/cli_config.py +1 -1
- machineconfig/scripts/python/devops_helpers/cli_self.py +3 -3
- machineconfig/scripts/python/ftpx.py +6 -6
- machineconfig/scripts/python/interactive.py +2 -2
- machineconfig/scripts/python/nw/mount_nfs +1 -1
- machineconfig/scripts/python/nw/mount_nfs.py +2 -2
- machineconfig/scripts/python/nw/mount_ssh.py +1 -1
- machineconfig/scripts/python/repos_helpers/count_lines_frontend.py +1 -1
- machineconfig/scripts/python/sessions.py +1 -1
- machineconfig/scripts/windows/mounts/mount_ssh.ps1 +1 -1
- machineconfig/setup_linux/apps.sh +0 -1
- machineconfig/setup_linux/web_shortcuts/interactive.sh +7 -7
- machineconfig/setup_windows/web_shortcuts/interactive.ps1 +7 -7
- machineconfig/utils/ssh.py +466 -247
- machineconfig/utils/ssh_utils/utils.py +0 -0
- {machineconfig-5.66.dist-info → machineconfig-5.68.dist-info}/METADATA +1 -1
- {machineconfig-5.66.dist-info → machineconfig-5.68.dist-info}/RECORD +25 -24
- {machineconfig-5.66.dist-info → machineconfig-5.68.dist-info}/WHEEL +0 -0
- {machineconfig-5.66.dist-info → machineconfig-5.68.dist-info}/entry_points.txt +0 -0
- {machineconfig-5.66.dist-info → machineconfig-5.68.dist-info}/top_level.txt +0 -0
machineconfig/utils/ssh.py
CHANGED
|
@@ -1,50 +1,20 @@
|
|
|
1
|
-
from typing import Optional, Any, Union
|
|
1
|
+
from typing import Optional, Any, Union
|
|
2
2
|
import os
|
|
3
|
-
from
|
|
3
|
+
from pathlib import Path
|
|
4
4
|
import rich.console
|
|
5
5
|
from machineconfig.utils.terminal import Response, MACHINE
|
|
6
|
-
from machineconfig.utils.path_extended import PathExtended, PLike, OPLike
|
|
7
6
|
from machineconfig.utils.accessories import pprint
|
|
8
|
-
# from machineconfig.utils.ve import get_ve_activate_line
|
|
9
7
|
|
|
8
|
+
UV_RUN_CMD = "$HOME/.local/bin/uv run"
|
|
9
|
+
MACHINECONFIG_VERSION = "machineconfig>=5.67"
|
|
10
|
+
DEFAULT_PICKLE_SUBDIR = "tmp_results/tmp_scripts/ssh"
|
|
10
11
|
|
|
11
|
-
def get_header(wdir: str): return f"""import sys; sys.path.insert(0, r'{wdir}')"""
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
@dataclass
|
|
15
|
-
class Scout:
|
|
16
|
-
source_full: PathExtended
|
|
17
|
-
source_rel2home: PathExtended
|
|
18
|
-
exists: bool
|
|
19
|
-
is_dir: bool
|
|
20
|
-
files: Optional[List[PathExtended]]
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def scout(source: PLike, z: bool = False, r: bool = False) -> Scout:
|
|
24
|
-
source_full = PathExtended(source).expanduser().absolute()
|
|
25
|
-
source_rel2home = source_full.collapseuser()
|
|
26
|
-
exists = source_full.exists()
|
|
27
|
-
is_dir = source_full.is_dir() if exists else False
|
|
28
|
-
if z and exists:
|
|
29
|
-
try:
|
|
30
|
-
source_full = source_full.zip()
|
|
31
|
-
except Exception as ex:
|
|
32
|
-
raise Exception(f"Could not zip {source_full} due to {ex}") from ex # type: ignore # pylint: disable=W0719
|
|
33
|
-
source_rel2home = source_full.zip()
|
|
34
|
-
if r and exists and is_dir:
|
|
35
|
-
files = [item.collapseuser() for item in source_full.search(folders=False, r=True)]
|
|
36
|
-
else:
|
|
37
|
-
files = None
|
|
38
|
-
return Scout(source_full=source_full, source_rel2home=source_rel2home, exists=exists, is_dir=is_dir, files=files)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
class SSH: # inferior alternative: https://github.com/fabric/fabric
|
|
13
|
+
class SSH:
|
|
42
14
|
def __init__(
|
|
43
|
-
self, host: Optional[str]
|
|
44
|
-
|
|
45
|
-
self.
|
|
46
|
-
self.ve = ve
|
|
47
|
-
self.compress = compress # Defaults: (1) use localhost if nothing provided.
|
|
15
|
+
self, host: Optional[str], username: Optional[str], hostname: Optional[str], ssh_key_path: Optional[str], password: Optional[str], port: int, enable_compression: bool):
|
|
16
|
+
self.password = password
|
|
17
|
+
self.enable_compression = enable_compression
|
|
48
18
|
|
|
49
19
|
self.host: Optional[str] = None
|
|
50
20
|
self.hostname: str
|
|
@@ -59,24 +29,24 @@ class SSH: # inferior alternative: https://github.com/fabric/fabric
|
|
|
59
29
|
try:
|
|
60
30
|
import paramiko.config as pconfig
|
|
61
31
|
|
|
62
|
-
config = pconfig.SSHConfig.from_path(str(
|
|
32
|
+
config = pconfig.SSHConfig.from_path(str(Path.home().joinpath(".ssh/config")))
|
|
63
33
|
config_dict = config.lookup(host)
|
|
64
34
|
self.hostname = config_dict["hostname"]
|
|
65
35
|
self.username = config_dict["user"]
|
|
66
36
|
self.host = host
|
|
67
37
|
self.port = int(config_dict.get("port", port))
|
|
68
|
-
|
|
69
|
-
if isinstance(
|
|
70
|
-
|
|
38
|
+
identity_file_value = config_dict.get("identityfile", ssh_key_path)
|
|
39
|
+
if isinstance(identity_file_value, list):
|
|
40
|
+
ssh_key_path = identity_file_value[0]
|
|
71
41
|
else:
|
|
72
|
-
|
|
42
|
+
ssh_key_path = identity_file_value
|
|
73
43
|
self.proxycommand = config_dict.get("proxycommand", None)
|
|
74
|
-
if
|
|
75
|
-
|
|
76
|
-
if isinstance(
|
|
77
|
-
|
|
44
|
+
if ssh_key_path is not None:
|
|
45
|
+
wildcard_identity_file = config.lookup("*").get("identityfile", ssh_key_path)
|
|
46
|
+
if isinstance(wildcard_identity_file, list):
|
|
47
|
+
ssh_key_path = wildcard_identity_file[0]
|
|
78
48
|
else:
|
|
79
|
-
|
|
49
|
+
ssh_key_path = wildcard_identity_file
|
|
80
50
|
except (FileNotFoundError, KeyError):
|
|
81
51
|
assert "@" in host or ":" in host, f"Host must be in the form of `username@hostname:port` or `username@hostname` or `hostname:port`, but it is: {host}"
|
|
82
52
|
if "@" in host:
|
|
@@ -94,31 +64,29 @@ class SSH: # inferior alternative: https://github.com/fabric/fabric
|
|
|
94
64
|
print(f"Provided values: host={host}, username={username}, hostname={hostname}")
|
|
95
65
|
raise ValueError("Either host or username and hostname must be provided.")
|
|
96
66
|
|
|
97
|
-
self.
|
|
67
|
+
self.ssh_key_path = str(Path(ssh_key_path).expanduser().absolute()) if ssh_key_path is not None else None
|
|
98
68
|
self.ssh = paramiko.SSHClient()
|
|
99
69
|
self.ssh.load_system_host_keys()
|
|
100
70
|
self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
101
|
-
pprint(dict(host=self.host, hostname=self.hostname, username=self.username, password="***", port=self.port, key_filename=self.
|
|
71
|
+
pprint(dict(host=self.host, hostname=self.hostname, username=self.username, password="***", port=self.port, key_filename=self.ssh_key_path), title="SSHing To")
|
|
102
72
|
sock = paramiko.ProxyCommand(self.proxycommand) if self.proxycommand is not None else None
|
|
103
73
|
try:
|
|
104
|
-
if
|
|
74
|
+
if password is None:
|
|
105
75
|
allow_agent = True
|
|
106
76
|
look_for_keys = True
|
|
107
77
|
else:
|
|
108
78
|
allow_agent = False
|
|
109
79
|
look_for_keys = False
|
|
110
|
-
self.ssh.connect(hostname=self.hostname, username=self.username, password=self.
|
|
80
|
+
self.ssh.connect(hostname=self.hostname, username=self.username, password=self.password, port=self.port, key_filename=self.ssh_key_path, compress=self.enable_compression, sock=sock, allow_agent=allow_agent, look_for_keys=look_for_keys) # type: ignore
|
|
111
81
|
except Exception as _err:
|
|
112
82
|
rich.console.Console().print_exception()
|
|
113
|
-
self.
|
|
114
|
-
self.ssh.connect(hostname=self.hostname, username=self.username, password=self.
|
|
83
|
+
self.password = getpass.getpass(f"Enter password for {self.username}@{self.hostname}: ")
|
|
84
|
+
self.ssh.connect(hostname=self.hostname, username=self.username, password=self.password, port=self.port, key_filename=self.ssh_key_path, compress=self.enable_compression, sock=sock, allow_agent=False, look_for_keys=False) # type: ignore
|
|
115
85
|
try:
|
|
116
86
|
self.sftp: Optional[paramiko.SFTPClient] = self.ssh.open_sftp()
|
|
117
87
|
except Exception as err:
|
|
118
88
|
self.sftp = None
|
|
119
|
-
print(f"""⚠️ WARNING: Failed to open SFTP connection to {hostname}.
|
|
120
|
-
Error Details: {err}\nData transfer may be affected!""")
|
|
121
|
-
|
|
89
|
+
print(f"""⚠️ WARNING: Failed to open SFTP connection to {self.hostname}. Error Details: {err}\nData transfer may be affected!""")
|
|
122
90
|
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, FileSizeColumn, TransferSpeedColumn
|
|
123
91
|
|
|
124
92
|
class RichProgressWrapper:
|
|
@@ -148,238 +116,489 @@ class SSH: # inferior alternative: https://github.com/fabric/fabric
|
|
|
148
116
|
self.terminal_responses: list[Response] = []
|
|
149
117
|
self.platform = platform
|
|
150
118
|
|
|
119
|
+
def __enter__(self) -> "SSH":
|
|
120
|
+
return self
|
|
121
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
122
|
+
self.close()
|
|
123
|
+
def close(self) -> None:
|
|
124
|
+
if self.sftp is not None:
|
|
125
|
+
self.sftp.close()
|
|
126
|
+
self.sftp = None
|
|
127
|
+
self.ssh.close()
|
|
151
128
|
def get_remote_machine(self) -> MACHINE:
|
|
152
129
|
if self._remote_machine is None:
|
|
153
|
-
|
|
130
|
+
windows_test1 = self.run_shell(command="$env:OS", verbose_output=False, description="Testing Remote OS Type", strict_stderr=False, strict_return_code=False).op
|
|
131
|
+
windows_test2 = self.run_shell(command="echo %OS%", verbose_output=False, description="Testing Remote OS Type Again", strict_stderr=False, strict_return_code=False).op
|
|
132
|
+
if windows_test1 == "Windows_NT" or windows_test2 == "Windows_NT":
|
|
154
133
|
self._remote_machine = "Windows"
|
|
155
134
|
else:
|
|
156
135
|
self._remote_machine = "Linux"
|
|
157
|
-
return self._remote_machine
|
|
158
|
-
|
|
136
|
+
return self._remote_machine
|
|
159
137
|
def get_local_distro(self) -> str:
|
|
160
138
|
if self._local_distro is None:
|
|
161
|
-
command = """
|
|
139
|
+
command = f"""{UV_RUN_CMD} --with distro python -c "import distro; print(distro.name(pretty=True))" """
|
|
162
140
|
import subprocess
|
|
163
141
|
res = subprocess.run(command, shell=True, capture_output=True, text=True).stdout.strip()
|
|
164
142
|
self._local_distro = res
|
|
165
143
|
return res
|
|
166
144
|
return self._local_distro
|
|
167
|
-
|
|
168
|
-
def get_remote_distro(self):
|
|
145
|
+
def get_remote_distro(self) -> str:
|
|
169
146
|
if self._remote_distro is None:
|
|
170
|
-
|
|
147
|
+
command_str = f"""{UV_RUN_CMD} --with distro python -c "import distro; print(distro.name(pretty=True))" """
|
|
148
|
+
res = self.run_shell(command=command_str, verbose_output=True, description="", strict_stderr=False, strict_return_code=False)
|
|
171
149
|
self._remote_distro = res.op_if_successfull_or_default() or ""
|
|
172
150
|
return self._remote_distro
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
assert self.get_remote_machine() == "Windows"
|
|
151
|
+
def restart_computer(self) -> Response:
|
|
152
|
+
return self.run_shell(command="Restart-Computer -Force" if self.get_remote_machine() == "Windows" else "sudo reboot", verbose_output=True, description="", strict_stderr=False, strict_return_code=False)
|
|
153
|
+
def send_ssh_key(self) -> Response:
|
|
154
|
+
self.copy_from_here(source_path=Path("~/.ssh/id_rsa.pub"), target_path=None, compress_with_zip=False, recursive=False, overwrite_existing=False)
|
|
155
|
+
if self.get_remote_machine() != "Windows":
|
|
156
|
+
raise RuntimeError("send_ssh_key is only supported for Windows remote machines")
|
|
180
157
|
code_url = "https://raw.githubusercontent.com/thisismygitrepo/machineconfig/refs/heads/main/src/machineconfig/setup_windows/openssh-server_add-sshkey.ps1"
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
assert self.get_remote_machine() == "Linux"
|
|
186
|
-
return self.run(f"{name} = {os.environ[name]}; export {name}")
|
|
158
|
+
import urllib.request
|
|
159
|
+
with urllib.request.urlopen(code_url) as response:
|
|
160
|
+
code = response.read().decode("utf-8")
|
|
161
|
+
return self.run_shell(command=code, verbose_output=True, description="", strict_stderr=False, strict_return_code=False)
|
|
187
162
|
|
|
188
163
|
def get_remote_repr(self, add_machine: bool = False) -> str:
|
|
189
164
|
return f"{self.username}@{self.hostname}:{self.port}" + (f" [{self.get_remote_machine()}][{self.get_remote_distro()}]" if add_machine else "")
|
|
190
|
-
|
|
191
165
|
def get_local_repr(self, add_machine: bool = False) -> str:
|
|
192
166
|
import getpass
|
|
193
|
-
|
|
194
167
|
return f"{getpass.getuser()}@{self.platform.node()}" + (f" [{self.platform.system()}][{self.get_local_distro()}]" if add_machine else "")
|
|
195
|
-
|
|
196
|
-
|
|
168
|
+
def get_ssh_conn_str(self, command: str) -> str:
|
|
169
|
+
return "ssh " + (f" -i {self.ssh_key_path}" if self.ssh_key_path else "") + self.get_remote_repr(add_machine=False).replace(":", " -p ") + (f" -t {command} " if command != "" else " ")
|
|
170
|
+
def __repr__(self) -> str:
|
|
197
171
|
return f"local {self.get_local_repr(add_machine=True)} >>> SSH TO >>> remote {self.get_remote_repr(add_machine=True)}"
|
|
198
172
|
|
|
199
|
-
def run_locally(self, command: str):
|
|
173
|
+
def run_locally(self, command: str) -> Response:
|
|
200
174
|
print(f"""💻 [LOCAL EXECUTION] Running command on node: {self.platform.node()} Command: {command}""")
|
|
201
175
|
res = Response(cmd=command)
|
|
202
176
|
res.output.returncode = os.system(command)
|
|
203
177
|
return res
|
|
204
178
|
|
|
205
|
-
def get_ssh_conn_str(self, cmd: str = ""):
|
|
206
|
-
return "ssh " + (f" -i {self.sshkey}" if self.sshkey else "") + self.get_remote_repr().replace(":", " -p ") + (f" -t {cmd} " if cmd != "" else " ")
|
|
207
179
|
|
|
208
|
-
def
|
|
209
|
-
raw = self.ssh.exec_command(
|
|
210
|
-
res = Response(stdin=raw[0], stdout=raw[1], stderr=raw[2], cmd=
|
|
211
|
-
if
|
|
212
|
-
res.capture().print_if_unsuccessful(desc=desc, strict_err=strict_err, strict_returncode=strict_returncode, assert_success=False)
|
|
213
|
-
else:
|
|
180
|
+
def run_shell(self, command: str, verbose_output: bool, description: str, strict_stderr: bool, strict_return_code: bool) -> Response:
|
|
181
|
+
raw = self.ssh.exec_command(command)
|
|
182
|
+
res = Response(stdin=raw[0], stdout=raw[1], stderr=raw[2], cmd=command, desc=description) # type: ignore
|
|
183
|
+
if verbose_output:
|
|
214
184
|
res.print()
|
|
185
|
+
else:
|
|
186
|
+
res.capture().print_if_unsuccessful(desc=description, strict_err=strict_stderr, strict_returncode=strict_return_code, assert_success=False)
|
|
215
187
|
self.terminal_responses.append(res)
|
|
216
188
|
return res
|
|
217
|
-
def run_py(self,
|
|
189
|
+
def run_py(self, python_code: str, description: str, verbose_output: bool, strict_stderr: bool, strict_return_code: bool) -> Response:
|
|
218
190
|
from machineconfig.utils.accessories import randstr
|
|
219
|
-
|
|
220
|
-
cmd_path = Path.home().joinpath(f"tmp_results/tmp_scripts/ssh/runpy_{randstr()}.py")
|
|
191
|
+
cmd_path = Path.home().joinpath(f"{DEFAULT_PICKLE_SUBDIR}/runpy_{randstr()}.py")
|
|
221
192
|
cmd_path.parent.mkdir(parents=True, exist_ok=True)
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
193
|
+
cmd_path.write_text(python_code, encoding="utf-8")
|
|
194
|
+
self.copy_from_here(source_path=cmd_path, target_path=None, compress_with_zip=False, recursive=False, overwrite_existing=False)
|
|
195
|
+
uv_cmd = f"""{UV_RUN_CMD} --with {MACHINECONFIG_VERSION} python {cmd_path.relative_to(Path.home())}"""
|
|
196
|
+
return self.run_shell(command=uv_cmd, verbose_output=verbose_output, description=description or f"run_py on {self.get_remote_repr(add_machine=False)}", strict_stderr=strict_stderr, strict_return_code=strict_return_code)
|
|
197
|
+
|
|
198
|
+
def _simple_sftp_get(self, remote_path: str, local_path: Path) -> None:
|
|
199
|
+
"""Simple SFTP get without any recursion or path expansion - for internal use only."""
|
|
200
|
+
if self.sftp is None:
|
|
201
|
+
raise RuntimeError(f"SFTP connection not available for {self.hostname}")
|
|
202
|
+
local_path.parent.mkdir(parents=True, exist_ok=True)
|
|
203
|
+
self.sftp.get(remotepath=remote_path, localpath=str(local_path))
|
|
204
|
+
|
|
205
|
+
def _create_remote_target_dir(self, target_path: Union[str, Path], overwrite_existing: bool) -> str:
|
|
206
|
+
"""Helper to create a directory on remote machine and return its path."""
|
|
207
|
+
def create_target_dir(target_dir_path: str, overwrite: bool, json_output_path: str) -> str:
|
|
208
|
+
from pathlib import Path
|
|
209
|
+
import shutil
|
|
210
|
+
import json
|
|
211
|
+
directory_path = Path(target_dir_path).expanduser()
|
|
212
|
+
if overwrite and directory_path.exists():
|
|
213
|
+
if directory_path.is_dir():
|
|
214
|
+
shutil.rmtree(directory_path)
|
|
215
|
+
else:
|
|
216
|
+
directory_path.unlink()
|
|
217
|
+
directory_path.parent.mkdir(parents=True, exist_ok=True)
|
|
218
|
+
result_path_posix = directory_path.as_posix()
|
|
219
|
+
json_result_path = Path(json_output_path)
|
|
220
|
+
json_result_path.parent.mkdir(parents=True, exist_ok=True)
|
|
221
|
+
json_result_path.write_text(json.dumps(result_path_posix, indent=2), encoding="utf-8")
|
|
222
|
+
print(json_result_path.as_posix())
|
|
223
|
+
return result_path_posix
|
|
239
224
|
from machineconfig.utils.meta import function_to_script
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
{
|
|
243
|
-
""
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
225
|
+
from machineconfig.utils.accessories import randstr
|
|
226
|
+
remote_json_output = Path.home().joinpath(f"{DEFAULT_PICKLE_SUBDIR}/return_{randstr()}.json").as_posix()
|
|
227
|
+
command = function_to_script(func=create_target_dir, call_with_kwargs={"target_dir_path": Path(target_path).as_posix(), "overwrite": overwrite_existing, "json_output_path": remote_json_output})
|
|
228
|
+
response = self.run_py(python_code=command, description=f"Creating target directory `{Path(target_path).parent.as_posix()}` @ {self.get_remote_repr(add_machine=False)}", verbose_output=False, strict_stderr=False, strict_return_code=False)
|
|
229
|
+
remote_json_path = response.op.strip()
|
|
230
|
+
if not remote_json_path:
|
|
231
|
+
raise RuntimeError(f"Failed to create target directory {target_path} - no response from remote")
|
|
232
|
+
from machineconfig.utils.accessories import randstr
|
|
233
|
+
local_json = Path.home().joinpath(f"{DEFAULT_PICKLE_SUBDIR}/local_{randstr()}.json")
|
|
234
|
+
self._simple_sftp_get(remote_path=remote_json_path, local_path=local_json)
|
|
235
|
+
import json
|
|
236
|
+
try:
|
|
237
|
+
result = json.loads(local_json.read_text(encoding="utf-8"))
|
|
238
|
+
except (json.JSONDecodeError, FileNotFoundError) as err:
|
|
239
|
+
raise RuntimeError(f"Failed to create target directory {target_path} - invalid JSON response: {err}") from err
|
|
240
|
+
finally:
|
|
241
|
+
if local_json.exists():
|
|
242
|
+
local_json.unlink()
|
|
243
|
+
assert isinstance(result, str), f"Failed to create target directory {target_path} on remote"
|
|
244
|
+
return result
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def copy_from_here(self, source_path: Union[str, Path], target_path: Optional[Union[str, Path]], compress_with_zip: bool, recursive: bool, overwrite_existing: bool) -> Path:
|
|
248
|
+
if self.sftp is None:
|
|
249
|
+
raise RuntimeError(f"SFTP connection not available for {self.hostname}. Cannot transfer files.")
|
|
250
|
+
|
|
251
|
+
source_obj = Path(source_path).expanduser().absolute()
|
|
255
252
|
if not source_obj.exists():
|
|
256
|
-
raise RuntimeError(f"
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
print(f" {idx + 1:03d}. {item}")
|
|
278
|
-
for item in source_list:
|
|
279
|
-
a__target = PathExtended(remote_root).joinpath(item.relative_to(source_obj))
|
|
280
|
-
self.copy_from_here(source=item, target=a__target)
|
|
281
|
-
return list(source_list)
|
|
282
|
-
if z:
|
|
253
|
+
raise RuntimeError(f"SSH Error: source `{source_obj}` does not exist!")
|
|
254
|
+
|
|
255
|
+
if target_path is None:
|
|
256
|
+
try:
|
|
257
|
+
target_path_relative = source_obj.relative_to(Path.home())
|
|
258
|
+
target_path = Path("~") / target_path_relative
|
|
259
|
+
except ValueError:
|
|
260
|
+
raise RuntimeError(f"If target is not specified, source must be relative to home directory, but got: {source_obj}")
|
|
261
|
+
|
|
262
|
+
if not compress_with_zip and source_obj.is_dir():
|
|
263
|
+
if not recursive:
|
|
264
|
+
raise RuntimeError(f"SSH Error: source `{source_obj}` is a directory! Set `recursive=True` for recursive sending or `compress_with_zip=True` to zip it first.")
|
|
265
|
+
file_paths_to_upload: list[Path] = [file_path for file_path in source_obj.rglob("*") if file_path.is_file()]
|
|
266
|
+
remote_root = self._create_remote_target_dir(target_path=target_path, overwrite_existing=overwrite_existing)
|
|
267
|
+
for idx, file_path in enumerate(file_paths_to_upload):
|
|
268
|
+
print(f" {idx + 1:03d}. {file_path}")
|
|
269
|
+
for file_path in file_paths_to_upload:
|
|
270
|
+
remote_file_target = Path(remote_root).joinpath(file_path.relative_to(source_obj))
|
|
271
|
+
self.copy_from_here(source_path=file_path, target_path=remote_file_target, compress_with_zip=False, recursive=False, overwrite_existing=overwrite_existing)
|
|
272
|
+
return Path(remote_root)
|
|
273
|
+
if compress_with_zip:
|
|
283
274
|
print("🗜️ ZIPPING ...")
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
)
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
275
|
+
import shutil
|
|
276
|
+
zip_path = Path(str(source_obj) + "_archive")
|
|
277
|
+
if source_obj.is_dir():
|
|
278
|
+
shutil.make_archive(str(zip_path), "zip", source_obj)
|
|
279
|
+
else:
|
|
280
|
+
shutil.make_archive(str(zip_path), "zip", source_obj.parent, source_obj.name)
|
|
281
|
+
source_obj = Path(str(zip_path) + ".zip")
|
|
282
|
+
if not str(target_path).endswith(".zip"):
|
|
283
|
+
target_path = Path(str(target_path) + ".zip")
|
|
284
|
+
remotepath_str = self._create_remote_target_dir(target_path=target_path, overwrite_existing=overwrite_existing)
|
|
285
|
+
remotepath = Path(remotepath_str)
|
|
286
|
+
print(f"""📤 [SFTP UPLOAD] Sending file: {repr(source_obj)} ==> Remote Path: {remotepath.as_posix()}""")
|
|
287
|
+
try:
|
|
288
|
+
with self.tqdm_wrap(ascii=True, unit="b", unit_scale=True) as pbar:
|
|
289
|
+
if self.sftp is None: # type: ignore[unreachable]
|
|
290
|
+
raise RuntimeError(f"SFTP connection lost for {self.hostname}")
|
|
291
|
+
self.sftp.put(localpath=str(source_obj), remotepath=remotepath.as_posix(), callback=pbar.view_bar) # type: ignore
|
|
292
|
+
except Exception:
|
|
293
|
+
if compress_with_zip and source_obj.exists() and str(source_obj).endswith("_archive.zip"):
|
|
294
|
+
source_obj.unlink()
|
|
295
|
+
raise
|
|
296
|
+
|
|
297
|
+
if compress_with_zip:
|
|
298
|
+
def unzip_archive(zip_file_path: str, overwrite_flag: bool) -> None:
|
|
299
|
+
from pathlib import Path
|
|
300
|
+
import shutil
|
|
301
|
+
import zipfile
|
|
302
|
+
archive_path = Path(zip_file_path).expanduser()
|
|
303
|
+
extraction_directory = archive_path.parent / archive_path.stem
|
|
304
|
+
if overwrite_flag and extraction_directory.exists():
|
|
305
|
+
shutil.rmtree(extraction_directory)
|
|
306
|
+
with zipfile.ZipFile(archive_path, "r") as archive_handle:
|
|
307
|
+
archive_handle.extractall(extraction_directory)
|
|
308
|
+
archive_path.unlink()
|
|
309
|
+
from machineconfig.utils.meta import function_to_script
|
|
310
|
+
command = function_to_script(func=unzip_archive, call_with_kwargs={"zip_file_path": remotepath.as_posix(), "overwrite_flag": overwrite_existing})
|
|
311
|
+
_resp = self.run_py(python_code=command, description=f"UNZIPPING {remotepath.as_posix()}", verbose_output=False, strict_stderr=True, strict_return_code=True)
|
|
312
|
+
source_obj.unlink()
|
|
313
|
+
print("\n")
|
|
306
314
|
return source_obj
|
|
307
315
|
|
|
308
|
-
def
|
|
309
|
-
if
|
|
316
|
+
def _check_remote_is_dir(self, source_path: Union[str, Path]) -> bool:
|
|
317
|
+
"""Helper to check if a remote path is a directory."""
|
|
318
|
+
def check_is_dir(path_to_check: str, json_output_path: str) -> bool:
|
|
319
|
+
from pathlib import Path
|
|
320
|
+
import json
|
|
321
|
+
is_directory = Path(path_to_check).expanduser().absolute().is_dir()
|
|
322
|
+
json_result_path = Path(json_output_path)
|
|
323
|
+
json_result_path.parent.mkdir(parents=True, exist_ok=True)
|
|
324
|
+
json_result_path.write_text(json.dumps(is_directory, indent=2), encoding="utf-8")
|
|
325
|
+
print(json_result_path.as_posix())
|
|
326
|
+
return is_directory
|
|
327
|
+
|
|
328
|
+
from machineconfig.utils.meta import function_to_script
|
|
329
|
+
from machineconfig.utils.accessories import randstr
|
|
330
|
+
remote_json_output = Path.home().joinpath(f"{DEFAULT_PICKLE_SUBDIR}/return_{randstr()}.json").as_posix()
|
|
331
|
+
command = function_to_script(func=check_is_dir, call_with_kwargs={"path_to_check": str(source_path), "json_output_path": remote_json_output})
|
|
332
|
+
response = self.run_py(python_code=command, description=f"Check if source `{source_path}` is a dir", verbose_output=False, strict_stderr=False, strict_return_code=False)
|
|
333
|
+
remote_json_path = response.op.strip()
|
|
334
|
+
if not remote_json_path:
|
|
335
|
+
raise RuntimeError(f"Failed to check if {source_path} is directory - no response from remote")
|
|
336
|
+
from machineconfig.utils.accessories import randstr
|
|
337
|
+
local_json = Path.home().joinpath(f"{DEFAULT_PICKLE_SUBDIR}/local_{randstr()}.json")
|
|
338
|
+
self._simple_sftp_get(remote_path=remote_json_path, local_path=local_json)
|
|
339
|
+
import json
|
|
340
|
+
try:
|
|
341
|
+
result = json.loads(local_json.read_text(encoding="utf-8"))
|
|
342
|
+
except (json.JSONDecodeError, FileNotFoundError) as err:
|
|
343
|
+
raise RuntimeError(f"Failed to check if {source_path} is directory - invalid JSON response: {err}") from err
|
|
344
|
+
finally:
|
|
345
|
+
if local_json.exists():
|
|
346
|
+
local_json.unlink()
|
|
347
|
+
assert isinstance(result, bool), f"Failed to check if {source_path} is directory"
|
|
348
|
+
return result
|
|
349
|
+
|
|
350
|
+
def _expand_remote_path(self, source_path: Union[str, Path]) -> str:
|
|
351
|
+
"""Helper to expand a path on the remote machine."""
|
|
352
|
+
def expand_source(path_to_expand: str, json_output_path: str) -> str:
|
|
353
|
+
from pathlib import Path
|
|
354
|
+
import json
|
|
355
|
+
expanded_path_posix = Path(path_to_expand).expanduser().absolute().as_posix()
|
|
356
|
+
json_result_path = Path(json_output_path)
|
|
357
|
+
json_result_path.parent.mkdir(parents=True, exist_ok=True)
|
|
358
|
+
json_result_path.write_text(json.dumps(expanded_path_posix, indent=2), encoding="utf-8")
|
|
359
|
+
print(json_result_path.as_posix())
|
|
360
|
+
return expanded_path_posix
|
|
361
|
+
|
|
362
|
+
from machineconfig.utils.meta import function_to_script
|
|
363
|
+
from machineconfig.utils.accessories import randstr
|
|
364
|
+
remote_json_output = Path.home().joinpath(f"{DEFAULT_PICKLE_SUBDIR}/return_{randstr()}.json").as_posix()
|
|
365
|
+
command = function_to_script(func=expand_source, call_with_kwargs={"path_to_expand": str(source_path), "json_output_path": remote_json_output})
|
|
366
|
+
response = self.run_py(python_code=command, description="Resolving source path by expanding user", verbose_output=False, strict_stderr=False, strict_return_code=False)
|
|
367
|
+
remote_json_path = response.op.strip()
|
|
368
|
+
if not remote_json_path:
|
|
369
|
+
raise RuntimeError(f"Could not resolve source path {source_path} - no response from remote")
|
|
370
|
+
from machineconfig.utils.accessories import randstr
|
|
371
|
+
local_json = Path.home().joinpath(f"{DEFAULT_PICKLE_SUBDIR}/local_{randstr()}.json")
|
|
372
|
+
self._simple_sftp_get(remote_path=remote_json_path, local_path=local_json)
|
|
373
|
+
import json
|
|
374
|
+
try:
|
|
375
|
+
result = json.loads(local_json.read_text(encoding="utf-8"))
|
|
376
|
+
except (json.JSONDecodeError, FileNotFoundError) as err:
|
|
377
|
+
raise RuntimeError(f"Could not resolve source path {source_path} - invalid JSON response: {err}") from err
|
|
378
|
+
finally:
|
|
379
|
+
if local_json.exists():
|
|
380
|
+
local_json.unlink()
|
|
381
|
+
assert isinstance(result, str), f"Could not resolve source path {source_path}"
|
|
382
|
+
return result
|
|
383
|
+
|
|
384
|
+
def copy_to_here(self, source: Union[str, Path], target: Optional[Union[str, Path]], compress_with_zip: bool = False, recursive: bool = False, internal_call: bool = False) -> Path:
|
|
385
|
+
if self.sftp is None:
|
|
386
|
+
raise RuntimeError(f"SFTP connection not available for {self.hostname}. Cannot transfer files.")
|
|
387
|
+
|
|
388
|
+
if not internal_call:
|
|
310
389
|
print(f"{'⬇️' * 5} SFTP DOWNLOADING FROM `{source}` TO `{target}`")
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
390
|
+
|
|
391
|
+
source_obj = Path(source)
|
|
392
|
+
expanded_source = self._expand_remote_path(source_path=source_obj)
|
|
393
|
+
|
|
394
|
+
if not compress_with_zip:
|
|
395
|
+
is_dir = self._check_remote_is_dir(source_path=expanded_source)
|
|
396
|
+
|
|
397
|
+
if is_dir:
|
|
398
|
+
if not recursive:
|
|
399
|
+
raise RuntimeError(f"SSH Error: source `{source_obj}` is a directory! Set recursive=True for recursive transfer or compress_with_zip=True to zip it.")
|
|
400
|
+
|
|
401
|
+
def search_files(directory_path: str, json_output_path: str) -> list[str]:
|
|
402
|
+
from pathlib import Path
|
|
403
|
+
import json
|
|
404
|
+
file_paths_list = [file_path.as_posix() for file_path in Path(directory_path).expanduser().absolute().rglob("*") if file_path.is_file()]
|
|
405
|
+
json_result_path = Path(json_output_path)
|
|
406
|
+
json_result_path.parent.mkdir(parents=True, exist_ok=True)
|
|
407
|
+
json_result_path.write_text(json.dumps(file_paths_list, indent=2), encoding="utf-8")
|
|
408
|
+
print(json_result_path.as_posix())
|
|
409
|
+
return file_paths_list
|
|
410
|
+
|
|
411
|
+
from machineconfig.utils.meta import function_to_script
|
|
412
|
+
from machineconfig.utils.accessories import randstr
|
|
413
|
+
remote_json_output = Path.home().joinpath(f"{DEFAULT_PICKLE_SUBDIR}/return_{randstr()}.json").as_posix()
|
|
414
|
+
command = function_to_script(func=search_files, call_with_kwargs={"directory_path": expanded_source, "json_output_path": remote_json_output})
|
|
415
|
+
response = self.run_py(python_code=command, description="Searching for files in source", verbose_output=False, strict_stderr=False, strict_return_code=False)
|
|
416
|
+
remote_json_path = response.op.strip()
|
|
417
|
+
if not remote_json_path:
|
|
418
|
+
raise RuntimeError(f"Could not resolve source path {source} - no response from remote")
|
|
419
|
+
from machineconfig.utils.accessories import randstr
|
|
420
|
+
local_json = Path.home().joinpath(f"{DEFAULT_PICKLE_SUBDIR}/local_{randstr()}.json")
|
|
421
|
+
self._simple_sftp_get(remote_path=remote_json_path, local_path=local_json)
|
|
422
|
+
import json
|
|
423
|
+
try:
|
|
424
|
+
source_list_str = json.loads(local_json.read_text(encoding="utf-8"))
|
|
425
|
+
except (json.JSONDecodeError, FileNotFoundError) as err:
|
|
426
|
+
raise RuntimeError(f"Could not resolve source path {source} - invalid JSON response: {err}") from err
|
|
427
|
+
finally:
|
|
428
|
+
if local_json.exists():
|
|
429
|
+
local_json.unlink()
|
|
430
|
+
assert isinstance(source_list_str, list), f"Could not resolve source path {source}"
|
|
431
|
+
file_paths_to_download = [Path(file_path_str) for file_path_str in source_list_str]
|
|
432
|
+
|
|
433
|
+
if target is None:
|
|
434
|
+
def collapse_to_home_dir(absolute_path: str, json_output_path: str) -> str:
|
|
435
|
+
from pathlib import Path
|
|
436
|
+
import json
|
|
437
|
+
source_absolute_path = Path(absolute_path).expanduser().absolute()
|
|
438
|
+
try:
|
|
439
|
+
relative_to_home = source_absolute_path.relative_to(Path.home())
|
|
440
|
+
collapsed_path_posix = (Path("~") / relative_to_home).as_posix()
|
|
441
|
+
json_result_path = Path(json_output_path)
|
|
442
|
+
json_result_path.parent.mkdir(parents=True, exist_ok=True)
|
|
443
|
+
json_result_path.write_text(json.dumps(collapsed_path_posix, indent=2), encoding="utf-8")
|
|
444
|
+
print(json_result_path.as_posix())
|
|
445
|
+
return collapsed_path_posix
|
|
446
|
+
except ValueError:
|
|
447
|
+
raise RuntimeError(f"Source path must be relative to home directory: {source_absolute_path}")
|
|
448
|
+
|
|
449
|
+
from machineconfig.utils.meta import function_to_script
|
|
450
|
+
from machineconfig.utils.accessories import randstr
|
|
451
|
+
remote_json_output = Path.home().joinpath(f"{DEFAULT_PICKLE_SUBDIR}/return_{randstr()}.json").as_posix()
|
|
452
|
+
command = function_to_script(func=collapse_to_home_dir, call_with_kwargs={"absolute_path": expanded_source, "json_output_path": remote_json_output})
|
|
453
|
+
response = self.run_py(python_code=command, description="Finding default target via relative source path", verbose_output=False, strict_stderr=False, strict_return_code=False)
|
|
454
|
+
remote_json_path_dir = response.op.strip()
|
|
455
|
+
if not remote_json_path_dir:
|
|
456
|
+
raise RuntimeError("Could not resolve target path - no response from remote")
|
|
457
|
+
from machineconfig.utils.accessories import randstr
|
|
458
|
+
local_json_dir = Path.home().joinpath(f"{DEFAULT_PICKLE_SUBDIR}/local_{randstr()}.json")
|
|
459
|
+
self._simple_sftp_get(remote_path=remote_json_path_dir, local_path=local_json_dir)
|
|
460
|
+
import json
|
|
461
|
+
try:
|
|
462
|
+
target_dir_str = json.loads(local_json_dir.read_text(encoding="utf-8"))
|
|
463
|
+
except (json.JSONDecodeError, FileNotFoundError) as err:
|
|
464
|
+
raise RuntimeError(f"Could not resolve target path - invalid JSON response: {err}") from err
|
|
465
|
+
finally:
|
|
466
|
+
if local_json_dir.exists():
|
|
467
|
+
local_json_dir.unlink()
|
|
468
|
+
assert isinstance(target_dir_str, str), "Could not resolve target path"
|
|
469
|
+
target = Path(target_dir_str)
|
|
470
|
+
|
|
471
|
+
target_dir = Path(target).expanduser().absolute()
|
|
472
|
+
|
|
473
|
+
for idx, file_path in enumerate(file_paths_to_download):
|
|
474
|
+
print(f" {idx + 1:03d}. {file_path}")
|
|
475
|
+
|
|
476
|
+
for file_path in file_paths_to_download:
|
|
477
|
+
local_file_target = target_dir.joinpath(Path(file_path).relative_to(expanded_source))
|
|
478
|
+
self.copy_to_here(source=file_path, target=local_file_target, compress_with_zip=False, recursive=False, internal_call=True)
|
|
479
|
+
|
|
480
|
+
return target_dir
|
|
481
|
+
|
|
482
|
+
if compress_with_zip:
|
|
483
|
+
print("🗜️ ZIPPING ...")
|
|
484
|
+
def zip_source(path_to_zip: str, json_output_path: str) -> str:
|
|
485
|
+
from pathlib import Path
|
|
486
|
+
import shutil
|
|
487
|
+
import json
|
|
488
|
+
source_to_compress = Path(path_to_zip).expanduser().absolute()
|
|
489
|
+
archive_base_path = source_to_compress.parent / (source_to_compress.name + "_archive")
|
|
490
|
+
if source_to_compress.is_dir():
|
|
491
|
+
shutil.make_archive(str(archive_base_path), "zip", source_to_compress)
|
|
492
|
+
else:
|
|
493
|
+
shutil.make_archive(str(archive_base_path), "zip", source_to_compress.parent, source_to_compress.name)
|
|
494
|
+
zip_file_path = str(archive_base_path) + ".zip"
|
|
495
|
+
json_result_path = Path(json_output_path)
|
|
496
|
+
json_result_path.parent.mkdir(parents=True, exist_ok=True)
|
|
497
|
+
json_result_path.write_text(json.dumps(zip_file_path, indent=2), encoding="utf-8")
|
|
498
|
+
print(json_result_path.as_posix())
|
|
499
|
+
return zip_file_path
|
|
500
|
+
|
|
501
|
+
from machineconfig.utils.meta import function_to_script
|
|
502
|
+
from machineconfig.utils.accessories import randstr
|
|
503
|
+
remote_json_output = Path.home().joinpath(f"{DEFAULT_PICKLE_SUBDIR}/return_{randstr()}.json").as_posix()
|
|
504
|
+
command = function_to_script(func=zip_source, call_with_kwargs={"path_to_zip": expanded_source, "json_output_path": remote_json_output})
|
|
505
|
+
response = self.run_py(python_code=command, description=f"Zipping source file {source}", verbose_output=False, strict_stderr=False, strict_return_code=False)
|
|
506
|
+
remote_json_path = response.op.strip()
|
|
507
|
+
if not remote_json_path:
|
|
508
|
+
raise RuntimeError(f"Could not zip {source} - no response from remote")
|
|
509
|
+
from machineconfig.utils.accessories import randstr
|
|
510
|
+
local_json = Path.home().joinpath(f"{DEFAULT_PICKLE_SUBDIR}/local_{randstr()}.json")
|
|
511
|
+
self._simple_sftp_get(remote_path=remote_json_path, local_path=local_json)
|
|
512
|
+
import json
|
|
513
|
+
try:
|
|
514
|
+
zipped_path = json.loads(local_json.read_text(encoding="utf-8"))
|
|
515
|
+
except (json.JSONDecodeError, FileNotFoundError) as err:
|
|
516
|
+
raise RuntimeError(f"Could not zip {source} - invalid JSON response: {err}") from err
|
|
517
|
+
finally:
|
|
518
|
+
if local_json.exists():
|
|
519
|
+
local_json.unlink()
|
|
520
|
+
assert isinstance(zipped_path, str), f"Could not zip {source}"
|
|
521
|
+
source_obj = Path(zipped_path)
|
|
522
|
+
expanded_source = zipped_path
|
|
523
|
+
|
|
330
524
|
if target is None:
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
525
|
+
def collapse_to_home(absolute_path: str, json_output_path: str) -> str:
|
|
526
|
+
from pathlib import Path
|
|
527
|
+
import json
|
|
528
|
+
source_absolute_path = Path(absolute_path).expanduser().absolute()
|
|
529
|
+
try:
|
|
530
|
+
relative_to_home = source_absolute_path.relative_to(Path.home())
|
|
531
|
+
collapsed_path_posix = (Path("~") / relative_to_home).as_posix()
|
|
532
|
+
json_result_path = Path(json_output_path)
|
|
533
|
+
json_result_path.parent.mkdir(parents=True, exist_ok=True)
|
|
534
|
+
json_result_path.write_text(json.dumps(collapsed_path_posix, indent=2), encoding="utf-8")
|
|
535
|
+
print(json_result_path.as_posix())
|
|
536
|
+
return collapsed_path_posix
|
|
537
|
+
except ValueError:
|
|
538
|
+
raise RuntimeError(f"Source path must be relative to home directory: {source_absolute_path}")
|
|
539
|
+
|
|
540
|
+
from machineconfig.utils.meta import function_to_script
|
|
541
|
+
from machineconfig.utils.accessories import randstr
|
|
542
|
+
remote_json_output = Path.home().joinpath(f"{DEFAULT_PICKLE_SUBDIR}/return_{randstr()}.json").as_posix()
|
|
543
|
+
command = function_to_script(func=collapse_to_home, call_with_kwargs={"absolute_path": expanded_source, "json_output_path": remote_json_output})
|
|
544
|
+
response = self.run_py(python_code=command, description="Finding default target via relative source path", verbose_output=False, strict_stderr=False, strict_return_code=False)
|
|
545
|
+
remote_json_path = response.op.strip()
|
|
546
|
+
if not remote_json_path:
|
|
547
|
+
raise RuntimeError("Could not resolve target path - no response from remote")
|
|
548
|
+
from machineconfig.utils.accessories import randstr
|
|
549
|
+
local_json = Path.home().joinpath(f"{DEFAULT_PICKLE_SUBDIR}/local_{randstr()}.json")
|
|
550
|
+
self._simple_sftp_get(remote_path=remote_json_path, local_path=local_json)
|
|
551
|
+
import json
|
|
552
|
+
try:
|
|
553
|
+
target_str = json.loads(local_json.read_text(encoding="utf-8"))
|
|
554
|
+
except (json.JSONDecodeError, FileNotFoundError) as err:
|
|
555
|
+
raise RuntimeError(f"Could not resolve target path - invalid JSON response: {err}") from err
|
|
556
|
+
finally:
|
|
557
|
+
if local_json.exists():
|
|
558
|
+
local_json.unlink()
|
|
559
|
+
assert isinstance(target_str, str), "Could not resolve target path"
|
|
560
|
+
target = Path(target_str)
|
|
561
|
+
assert str(target).startswith("~"), f"If target is not specified, source must be relative to home.\n{target=}"
|
|
562
|
+
|
|
563
|
+
target_obj = Path(target).expanduser().absolute()
|
|
338
564
|
target_obj.parent.mkdir(parents=True, exist_ok=True)
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
565
|
+
|
|
566
|
+
if compress_with_zip and target_obj.suffix != ".zip":
|
|
567
|
+
target_obj = target_obj.with_suffix(target_obj.suffix + ".zip")
|
|
568
|
+
|
|
569
|
+
print(f"""📥 [DOWNLOAD] Receiving: {expanded_source} ==> Local Path: {target_obj}""")
|
|
570
|
+
try:
|
|
571
|
+
with self.tqdm_wrap(ascii=True, unit="b", unit_scale=True) as pbar:
|
|
572
|
+
if self.sftp is None: # type: ignore[unreachable]
|
|
573
|
+
raise RuntimeError(f"SFTP connection lost for {self.hostname}")
|
|
574
|
+
self.sftp.get(remotepath=expanded_source, localpath=str(target_obj), callback=pbar.view_bar) # type: ignore
|
|
575
|
+
except Exception:
|
|
576
|
+
if target_obj.exists():
|
|
577
|
+
target_obj.unlink()
|
|
578
|
+
raise
|
|
579
|
+
|
|
580
|
+
if compress_with_zip:
|
|
581
|
+
import zipfile
|
|
582
|
+
extract_to = target_obj.parent / target_obj.stem
|
|
583
|
+
with zipfile.ZipFile(target_obj, "r") as zip_ref:
|
|
584
|
+
zip_ref.extractall(extract_to)
|
|
585
|
+
target_obj.unlink()
|
|
586
|
+
target_obj = extract_to
|
|
587
|
+
|
|
588
|
+
def delete_temp_zip(path_to_delete: str) -> None:
|
|
589
|
+
from pathlib import Path
|
|
590
|
+
import shutil
|
|
591
|
+
file_or_dir_path = Path(path_to_delete)
|
|
592
|
+
if file_or_dir_path.exists():
|
|
593
|
+
if file_or_dir_path.is_dir():
|
|
594
|
+
shutil.rmtree(file_or_dir_path)
|
|
595
|
+
else:
|
|
596
|
+
file_or_dir_path.unlink()
|
|
597
|
+
|
|
598
|
+
from machineconfig.utils.meta import function_to_script
|
|
599
|
+
command = function_to_script(func=delete_temp_zip, call_with_kwargs={"path_to_delete": expanded_source})
|
|
600
|
+
self.run_py(python_code=command, description="Cleaning temp zip files @ remote.", verbose_output=False, strict_stderr=True, strict_return_code=True)
|
|
601
|
+
|
|
356
602
|
print("\n")
|
|
357
603
|
return target_obj
|
|
358
604
|
|
|
359
|
-
# def receieve(self, source: PLike, target: OPLike = None, z: bool = False, r: bool = False) -> PathExtended:
|
|
360
|
-
# scout = self.run_py(cmd=f"obj=scout(r'{source}', z={z}, r={r})", desc=f"Scouting source `{source}` path on remote", return_obj=True, verbose=False)
|
|
361
|
-
# assert isinstance(scout, Scout)
|
|
362
|
-
# if not z and scout.is_dir and scout.files is not None:
|
|
363
|
-
# if r:
|
|
364
|
-
# tmp: list[PathExtended] = [self.receieve(source=file.as_posix(), target=PathExtended(target).joinpath(PathExtended(file).relative_to(source)) if target else None, r=False) for file in scout.files]
|
|
365
|
-
# return tmp[0]
|
|
366
|
-
# else:
|
|
367
|
-
# print("Source is a directory! either set `r=True` for recursive sending or raise `zip_first=True` flag.")
|
|
368
|
-
# if target:
|
|
369
|
-
# target = PathExtended(target).expanduser().absolute()
|
|
370
|
-
# else:
|
|
371
|
-
# target = scout.source_rel2home.expanduser().absolute()
|
|
372
|
-
# target.parent.mkdir(parents=True, exist_ok=True)
|
|
373
|
-
# if z and ".zip" not in target.suffix:
|
|
374
|
-
# target += ".zip"
|
|
375
|
-
# source = scout.source_full
|
|
376
|
-
# with self.tqdm_wrap(ascii=True, unit="b", unit_scale=True) as pbar:
|
|
377
|
-
# self.sftp.get(remotepath=source.as_posix(), localpath=target.as_posix(), callback=pbar.view_bar) # type: ignore # pylint: disable=E1129
|
|
378
|
-
# if z:
|
|
379
|
-
# target = target.unzip(inplace=True, content=True)
|
|
380
|
-
# self.run_py(f"""
|
|
381
|
-
# from machineconfig.utils.path_extended import PathExtended as P;
|
|
382
|
-
# P(r'{source.as_posix()}').delete(sure=True)
|
|
383
|
-
# """, desc="Cleaning temp zip files @ remote.", strict_returncode=True, strict_err=True)
|
|
384
|
-
# print("\n")
|
|
385
|
-
# return target
|