feat: add upload/download progress reporting in ssh.py
Files <1MB show size+time in OK line. Files >=1MB show 25/50/75% milestones with transferred/total, plus speed in final OK line. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
56
tools/ssh.py
56
tools/ssh.py
@@ -188,29 +188,73 @@ def _normalize_remote_path(remote_path: str) -> str:
|
||||
return remote_path
|
||||
|
||||
|
||||
def _fmt_size(nbytes: int) -> str:
|
||||
"""Format byte count for human display."""
|
||||
if nbytes < 1024:
|
||||
return f"{nbytes} B"
|
||||
elif nbytes < 1024 * 1024:
|
||||
return f"{nbytes / 1024:.1f} KB"
|
||||
elif nbytes < 1024 * 1024 * 1024:
|
||||
return f"{nbytes / (1024 * 1024):.1f} MB"
|
||||
else:
|
||||
return f"{nbytes / (1024 * 1024 * 1024):.2f} GB"
|
||||
|
||||
|
||||
def _progress_cb(total_bytes: int):
|
||||
"""Return a Paramiko-compatible progress callback.
|
||||
For files >= 1 MB, prints at 25%, 50%, 75% milestones.
|
||||
For files < 1 MB, stays silent."""
|
||||
threshold = 1024 * 1024 # 1 MB
|
||||
reported = set()
|
||||
|
||||
def callback(transferred: int, total: int):
|
||||
if total < threshold:
|
||||
return
|
||||
pct = int(transferred * 100 / total)
|
||||
for milestone in (25, 50, 75):
|
||||
if pct >= milestone and milestone not in reported:
|
||||
reported.add(milestone)
|
||||
print(f"{milestone}% ({_fmt_size(transferred)}/{_fmt_size(total)})")
|
||||
|
||||
return callback
|
||||
|
||||
|
||||
def upload_file(server: dict, local_path: str, remote_path: str):
|
||||
# Normalize the remote path to handle MSYS conversion issues
|
||||
normalized_remote_path = _normalize_remote_path(remote_path)
|
||||
file_size = os.path.getsize(local_path)
|
||||
client = get_client(server)
|
||||
try:
|
||||
sftp = client.open_sftp()
|
||||
sftp.put(local_path, normalized_remote_path)
|
||||
t0 = time.time()
|
||||
sftp.put(local_path, normalized_remote_path, callback=_progress_cb(file_size))
|
||||
elapsed = time.time() - t0
|
||||
sftp.chmod(normalized_remote_path, 0o664)
|
||||
sftp.close()
|
||||
print(f"OK: {local_path} -> {server['alias']}:{normalized_remote_path}")
|
||||
|
||||
info = f"{_fmt_size(file_size)}, {elapsed:.1f}s"
|
||||
if file_size >= 1024 * 1024 and elapsed > 0:
|
||||
speed = file_size / elapsed
|
||||
info += f", {_fmt_size(int(speed))}/s"
|
||||
print(f"OK: {local_path} -> {server['alias']}:{normalized_remote_path} ({info})")
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
def download_file(server: dict, remote_path: str, local_path: str):
|
||||
# Normalize the remote path to handle MSYS conversion issues
|
||||
normalized_remote_path = _normalize_remote_path(remote_path)
|
||||
client = get_client(server)
|
||||
try:
|
||||
sftp = client.open_sftp()
|
||||
sftp.get(normalized_remote_path, local_path)
|
||||
file_size = sftp.stat(normalized_remote_path).st_size
|
||||
t0 = time.time()
|
||||
sftp.get(normalized_remote_path, local_path, callback=_progress_cb(file_size))
|
||||
elapsed = time.time() - t0
|
||||
sftp.close()
|
||||
print(f"OK: {server['alias']}:{normalized_remote_path} -> {local_path}")
|
||||
|
||||
info = f"{_fmt_size(file_size)}, {elapsed:.1f}s"
|
||||
if file_size >= 1024 * 1024 and elapsed > 0:
|
||||
speed = file_size / elapsed
|
||||
info += f", {_fmt_size(int(speed))}/s"
|
||||
print(f"OK: {server['alias']}:{normalized_remote_path} -> {local_path} ({info})")
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user