@@ -3,10 +3,14 @@ use colored::Colorize;
33use indicatif:: { HumanDuration , ProgressBar , ProgressDrawTarget , ProgressStyle } ;
44use miette:: { miette, Context , IntoDiagnostic } ;
55use 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 } ;
1014use yggdrasil:: core:: config:: showtime:: ShowtimeConfig ;
1115use 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}
0 commit comments