Skip to content

Commit a92f38f

Browse files
authored
sindri: Reintroduce the progress bar while uploading (#440)
1 parent 6eb8937 commit a92f38f

5 files changed

Lines changed: 153 additions & 17 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tools/sindri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
description = "tool that simplifies robot interaction by automating the process of building and deploying to the robots"
33
name = "sindri"
4-
version = "0.15.0"
4+
version = "0.16.0"
55

66
authors.workspace = true
77
categories.workspace = true

tools/sindri/src/cli/robot_ops.rs

Lines changed: 148 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@ use colored::Colorize;
33
use indicatif::{HumanDuration, ProgressBar, ProgressDrawTarget, ProgressStyle};
44
use miette::{miette, Context, IntoDiagnostic};
55
use std::{
6-
borrow::Cow, collections::HashMap, fmt, fs, net::Ipv4Addr, process::Stdio, str::FromStr,
7-
time::Duration,
6+
borrow::Cow, collections::HashMap, fmt, fs, net::Ipv4Addr, path::Path, process::Stdio,
7+
str::FromStr, time::Duration,
8+
};
9+
use tokio::{
10+
self,
11+
io::{AsyncBufReadExt, BufReader},
12+
process::Command,
813
};
9-
use tokio::{self, process::Command};
1014
use yggdrasil::core::config::showtime::ShowtimeConfig;
1115
use yggdrasil::prelude::*;
1216

@@ -249,18 +253,21 @@ impl Output {
249253
match self {
250254
Output::Silent => {}
251255
Output::Single(pb) => {
252-
pb.set_message(format!("{}", "Ensuring host directories exist".dimmed()));
256+
pb.set_message(format!("{}", "Connecting...".dimmed()));
253257
pb.set_prefix(format!("{}", "Uploading".blue().bold()));
258+
pb.set_length(num_files);
259+
254260
pb.set_style(
255261
ProgressStyle::with_template(
256-
" {prefix:.blue.bold} {msg} [{bar:.blue/cyan}] {spinner:.blue.bold}",
262+
" {prefix:.blue.bold} {msg} [{bar:.blue/cyan}] [{pos}/{len}] {spinner:.blue.bold}",
257263
)
258264
.unwrap()
259265
.tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ ")
260266
.progress_chars("=>-"),
261267
);
262268
}
263269
Output::Multi(pb) => {
270+
pb.set_message(format!("{}", "Connecting...".dimmed()));
264271
pb.set_length(num_files);
265272
pb.set_style(
266273
ProgressStyle::with_template(
@@ -587,13 +594,127 @@ pub(crate) async fn stop_single_yggdrasil_service(robot: &Robot, output: Output)
587594
}
588595

589596
/// Copy the contents of the 'deploy' folder to the robot.
590-
pub(crate) async fn upload_to_robot(addr: &Ipv4Addr) -> Result<()> {
591-
let output = Command::new("rsync")
597+
pub(crate) async fn upload_to_robot(addr: &Ipv4Addr, output: Output) -> Result<()> {
598+
let local_directory = "deploy/";
599+
let transfer_list = get_rsync_transfer_list(local_directory, addr).await?;
600+
output.upload_phase(transfer_list.len() as u64);
601+
602+
transfer_files(local_directory, addr, &transfer_list, output.clone()).await
603+
}
604+
605+
fn make_remote_directory(addr: Ipv4Addr) -> String {
606+
format!("nao@{addr}:/home/nao")
607+
}
608+
609+
/// Transfers files using rsync and displays progress.
610+
///
611+
/// This function runs rsync to transfer the specified files while providing real-time progress updates.
612+
/// It reads `stdout` to track file transfers and `stderr` to capture any errors.
613+
///
614+
/// # Note
615+
///
616+
/// This function excludes hidden files (dotfiles) from the transfer using `--exclude=.*`.
617+
/// # Errors
618+
///
619+
/// Returns an [`Error::Rsync`] if rsync fails, including the exit code and error message.
620+
async fn transfer_files(
621+
local_directory: impl AsRef<str>,
622+
addr: &Ipv4Addr,
623+
files_to_transfer: &[String],
624+
output: Output,
625+
) -> Result<()> {
626+
let total_files = files_to_transfer.len();
627+
if total_files == 0 {
628+
return Ok(());
629+
}
630+
631+
let mut child = Command::new("rsync")
592632
.args([
593633
"-az",
594-
"deploy/",
595-
&format!("nao@{addr}:/home/nao"),
596-
"--out-format=\"%f\"",
634+
"--out-format=%f",
635+
"--exclude=.*",
636+
local_directory.as_ref(),
637+
&make_remote_directory(*addr),
638+
])
639+
.stdout(Stdio::piped())
640+
.stderr(Stdio::piped())
641+
.spawn()?;
642+
643+
let stdout = child.stdout.take().expect("failed to take stdiout!");
644+
let reader = BufReader::new(stdout);
645+
let mut lines = reader.lines();
646+
647+
let mut previous_line: Option<String> = None;
648+
649+
while let Some(line) = lines.next_line().await? {
650+
if !is_valid_file_path(&line) {
651+
continue;
652+
}
653+
654+
match output.clone() {
655+
Output::Silent => {}
656+
Output::Multi(pb) => {
657+
pb.set_message(format!("{}", line.dimmed()));
658+
pb.inc(1);
659+
}
660+
Output::Single(pb) => {
661+
if let Some(prev) = previous_line {
662+
pb.println(format!(
663+
"{} {}",
664+
" Uploaded".bright_blue().bold(),
665+
prev.dimmed()
666+
));
667+
}
668+
669+
previous_line = Some(line.clone());
670+
pb.set_message(format!("{}", line.dimmed()));
671+
pb.inc(1);
672+
}
673+
}
674+
}
675+
676+
output.spinner();
677+
678+
let status = child.wait().await?;
679+
if !status.success() {
680+
if let Some(code) = status.code() {
681+
return Err(Error::Rsync {
682+
exit_code: code,
683+
reason: "No idea".to_string(),
684+
});
685+
}
686+
}
687+
688+
if let Output::Multi(pb) = &output {
689+
pb.set_message(format!(
690+
" {} {}",
691+
"Uploaded".green().bold(),
692+
addr.to_string().red()
693+
));
694+
}
695+
696+
Ok(())
697+
}
698+
699+
/// Runs rsync in dry-run mode to get a list of files that need to be transferred.
700+
///
701+
/// # Note
702+
///
703+
/// This transfer list is used only for display purposes and not for the actual transfer.
704+
/// As a result, it also filters out any "invalid" paths. See [`is_valid_file_path`] for details.
705+
async fn get_rsync_transfer_list(
706+
local_directory: impl AsRef<str>,
707+
addr: &Ipv4Addr,
708+
) -> Result<Vec<String>> {
709+
let output = Command::new("rsync")
710+
.args([
711+
// run rsync in dry mode, to obtain the transfer list
712+
"-azn",
713+
local_directory.as_ref(),
714+
&make_remote_directory(*addr),
715+
"--out-format=%f",
716+
// exclude hidden files
717+
"--exclude=.*",
597718
])
598719
.stdout(Stdio::piped())
599720
.stderr(Stdio::piped())
@@ -609,6 +730,21 @@ pub(crate) async fn upload_to_robot(addr: &Ipv4Addr) -> Result<()> {
609730
}
610731
}
611732

612-
println!("{}", String::from_utf8_lossy(&output.stdout));
613-
Ok(())
733+
Ok(String::from_utf8_lossy(&output.stdout)
734+
.lines()
735+
.map(String::from)
736+
.filter(|filepath| is_valid_file_path(filepath))
737+
.collect())
738+
}
739+
740+
/// Check whether the provided path is considered valid.
741+
///
742+
/// We consider all files as valid, except directories and hidden files.
743+
fn is_valid_file_path(filepath: impl AsRef<Path>) -> bool {
744+
let path = filepath.as_ref();
745+
746+
!path.is_dir()
747+
&& path
748+
.file_name()
749+
.is_some_and(|name| !name.to_string_lossy().starts_with('.'))
614750
}

tools/sindri/src/cli/run.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ impl Run {
7575
if !self.robot_ops.local {
7676
output.spinner();
7777
robot_ops::stop_single_yggdrasil_service(&robot, output.clone()).await?;
78-
robot_ops::upload_to_robot(&robot.ip()).await?;
78+
robot_ops::upload_to_robot(&robot.ip(), output.clone()).await?;
7979

8080
if let Some(network) = self.robot_ops.network {
8181
output.spinner();

tools/sindri/src/cli/showtime.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ impl Showtime {
4343

4444
output.spinner();
4545
robot_ops::stop_single_yggdrasil_service(&robot, output.clone()).await?;
46-
robot_ops::upload_to_robot(&robot.ip()).await?;
46+
robot_ops::upload_to_robot(&robot.ip(), output.clone()).await?;
4747
output.spinner();
4848
robot_ops::start_single_yggdrasil_service(&robot, output.clone()).await?;
4949

@@ -101,7 +101,7 @@ impl Showtime {
101101
.block_on(async move {
102102
output.spinner();
103103
robot_ops::stop_single_yggdrasil_service(&robot, output.clone()).await?;
104-
robot_ops::upload_to_robot(&robot.ip()).await?;
104+
robot_ops::upload_to_robot(&robot.ip(), output.clone()).await?;
105105
output.spinner();
106106
robot_ops::start_single_yggdrasil_service(&robot, output.clone()).await?;
107107

0 commit comments

Comments
 (0)