From c70fb1991cf77bd0282430130a33ffd4fb038be4 Mon Sep 17 00:00:00 2001 From: shuki Date: Fri, 6 Mar 2026 06:38:15 +0200 Subject: [PATCH] Add Disk Info and Speed Test buttons to Remotes screen - Disk Info: runs df -h and df -i on remote via SSH (or locally) - Speed Test: uploads/downloads 10MB test file via rsync, measures Mbps - Both available as CLI commands: gniza remotes disk-info/speed-test --name=NAME Co-Authored-By: Claude Opus 4.6 --- bin/gniza | 12 +++++- lib/remotes.sh | 98 ++++++++++++++++++++++++++++++++++++++++++ tui/screens/remotes.py | 40 +++++++++++++++++ 3 files changed, 149 insertions(+), 1 deletion(-) diff --git a/bin/gniza b/bin/gniza index 71130f8..df11a4a 100755 --- a/bin/gniza +++ b/bin/gniza @@ -274,8 +274,18 @@ run_cli() { validate_remote "$name" echo "Remote '$name' is valid." ;; + disk-info) + [[ -z "$name" ]] && die "remotes disk-info requires --name=NAME" + load_remote "$name" + remote_disk_info + ;; + speed-test) + [[ -z "$name" ]] && die "remotes speed-test requires --name=NAME" + load_remote "$name" + remote_speed_test + ;; *) - die "Unknown remotes action: $action (expected list|add|delete|show|test)" + die "Unknown remotes action: $action (expected list|add|delete|show|test|disk-info|speed-test)" ;; esac ;; diff --git a/lib/remotes.sh b/lib/remotes.sh index ff83772..2b070d9 100644 --- a/lib/remotes.sh +++ b/lib/remotes.sh @@ -287,3 +287,101 @@ get_target_remotes() { log_error "No remotes configured. Create one in $CONFIG_DIR/remotes.d/" return 1 } + +# ── Disk info ──────────────────────────────────────────────── + +remote_disk_info() { + local base="${REMOTE_BASE:-/}" + case "${REMOTE_TYPE:-ssh}" in + ssh) + echo "Disk usage on ${REMOTE_USER}@${REMOTE_HOST}:${base}" + echo "──────────────────────────────────────────" + remote_exec "df -h '$base' 2>/dev/null && echo '' && df -i '$base' 2>/dev/null" + ;; + local) + echo "Disk usage on ${base}" + echo "──────────────────────────────────────────" + df -h "$base" 2>/dev/null + echo "" + df -i "$base" 2>/dev/null + ;; + *) + echo "Disk info not supported for remote type: ${REMOTE_TYPE}" + return 1 + ;; + esac +} + +# ── Speed test ─────────────────────────────────────────────── + +remote_speed_test() { + local test_size="10M" + local test_file="/tmp/.gniza_speedtest_$$" + local remote_file="${REMOTE_BASE:-.}/.gniza_speedtest_$$" + + case "${REMOTE_TYPE:-ssh}" in + ssh) + echo "Speed test to ${REMOTE_USER}@${REMOTE_HOST}" + echo "Test file size: ${test_size}" + echo "──────────────────────────────────────────" + + # Create local test file + dd if=/dev/urandom of="$test_file" bs=1M count=10 2>/dev/null + + # Upload test + echo "" + echo "Upload test..." + local ssh_cmd + ssh_cmd=$(build_rsync_ssh_cmd) + local start_up end_up duration_up speed_up + start_up=$(date +%s%N) + # shellcheck disable=SC2086 + if rsync -e "$ssh_cmd" --progress "$test_file" "${REMOTE_USER}@${REMOTE_HOST}:${remote_file}" 2>/dev/null; then + end_up=$(date +%s%N) + duration_up=$(( (end_up - start_up) / 1000000 )) + if [[ "$duration_up" -gt 0 ]]; then + speed_up=$(( 10 * 1000 * 8 / duration_up )) + echo " Upload: ${speed_up} Mbps (${duration_up} ms)" + else + echo " Upload: too fast to measure" + fi + else + echo " Upload: FAILED" + fi + + # Download test + echo "" + echo "Download test..." + local dl_file="${test_file}_dl" + local start_dn end_dn duration_dn speed_dn + start_dn=$(date +%s%N) + # shellcheck disable=SC2086 + if rsync -e "$ssh_cmd" --progress "${REMOTE_USER}@${REMOTE_HOST}:${remote_file}" "$dl_file" 2>/dev/null; then + end_dn=$(date +%s%N) + duration_dn=$(( (end_dn - start_dn) / 1000000 )) + if [[ "$duration_dn" -gt 0 ]]; then + speed_dn=$(( 10 * 1000 * 8 / duration_dn )) + echo " Download: ${speed_dn} Mbps (${duration_dn} ms)" + else + echo " Download: too fast to measure" + fi + else + echo " Download: FAILED" + fi + + # Cleanup + rm -f "$test_file" "$dl_file" 2>/dev/null + remote_exec "rm -f '$remote_file'" 2>/dev/null || true + + echo "" + echo "Done." + ;; + local) + echo "Speed test not applicable for local remotes." + ;; + *) + echo "Speed test not supported for remote type: ${REMOTE_TYPE}" + return 1 + ;; + esac +} diff --git a/tui/screens/remotes.py b/tui/screens/remotes.py index 04554e9..aaa4145 100644 --- a/tui/screens/remotes.py +++ b/tui/screens/remotes.py @@ -22,6 +22,8 @@ class RemotesScreen(Screen): yield Button("Add", variant="primary", id="btn-add") yield Button("Edit", id="btn-edit") yield Button("Test", variant="warning", id="btn-test") + yield Button("Disk Info", id="btn-disk") + yield Button("Speed Test", id="btn-speed") yield Button("Delete", variant="error", id="btn-delete") yield Button("Back", id="btn-back") yield Footer() @@ -71,6 +73,18 @@ class RemotesScreen(Screen): self._test_remote(name) else: self.notify("Select a remote first", severity="warning") + elif event.button.id == "btn-disk": + name = self._selected_remote() + if name: + self._disk_info(name) + else: + self.notify("Select a remote first", severity="warning") + elif event.button.id == "btn-speed": + name = self._selected_remote() + if name: + self._speed_test(name) + else: + self.notify("Select a remote first", severity="warning") elif event.button.id == "btn-delete": name = self._selected_remote() if name: @@ -96,6 +110,32 @@ class RemotesScreen(Screen): log_screen.write(f"\n[red]Connection test failed (exit code {rc}).[/red]") log_screen.finish() + @work + async def _disk_info(self, name: str) -> None: + log_screen = OperationLog(f"Disk Info: {name}", show_spinner=False) + self.app.push_screen(log_screen) + rc, stdout, stderr = await run_cli("remotes", "disk-info", f"--name={name}") + if stdout: + log_screen.write(stdout) + if stderr: + log_screen.write(stderr) + if rc != 0: + log_screen.write(f"\n[red]Failed to get disk info (exit code {rc}).[/red]") + log_screen.finish() + + @work + async def _speed_test(self, name: str) -> None: + log_screen = OperationLog(f"Speed Test: {name}") + self.app.push_screen(log_screen) + rc, stdout, stderr = await run_cli("remotes", "speed-test", f"--name={name}") + if stdout: + log_screen.write(stdout) + if stderr: + log_screen.write(stderr) + if rc != 0: + log_screen.write(f"\n[red]Speed test failed (exit code {rc}).[/red]") + log_screen.finish() + def _delete_remote(self, name: str) -> None: conf = CONFIG_DIR / "remotes.d" / f"{name}.conf" if conf.is_file():