@@ -6,7 +6,7 @@ use clap::Args;
66use rc_core:: { AliasManager , ObjectStore as _, ParsedPath , RemotePath , parse_path} ;
77use rc_s3:: S3Client ;
88use serde:: Serialize ;
9- use std:: path:: Path ;
9+ use std:: path:: { Path , PathBuf } ;
1010
1111use crate :: exit_code:: ExitCode ;
1212use crate :: output:: { Formatter , OutputConfig } ;
@@ -63,17 +63,18 @@ struct CpOutput {
6363/// Execute the cp command
6464pub async fn execute ( args : CpArgs , output_config : OutputConfig ) -> ExitCode {
6565 let formatter = Formatter :: new ( output_config) ;
66+ let alias_manager = AliasManager :: new ( ) . ok ( ) ;
6667
6768 // Parse source and target paths
68- let source = match parse_path ( & args. source ) {
69+ let source = match parse_cp_path ( & args. source , alias_manager . as_ref ( ) ) {
6970 Ok ( p) => p,
7071 Err ( e) => {
7172 formatter. error ( & format ! ( "Invalid source path: {e}" ) ) ;
7273 return ExitCode :: UsageError ;
7374 }
7475 } ;
7576
76- let target = match parse_path ( & args. target ) {
77+ let target = match parse_cp_path ( & args. target , alias_manager . as_ref ( ) ) {
7778 Ok ( p) => p,
7879 Err ( e) => {
7980 formatter. error ( & format ! ( "Invalid target path: {e}" ) ) ;
@@ -102,6 +103,26 @@ pub async fn execute(args: CpArgs, output_config: OutputConfig) -> ExitCode {
102103 }
103104}
104105
106+ fn parse_cp_path ( path : & str , alias_manager : Option < & AliasManager > ) -> rc_core:: Result < ParsedPath > {
107+ let parsed = parse_path ( path) ?;
108+
109+ let ParsedPath :: Remote ( remote) = & parsed else {
110+ return Ok ( parsed) ;
111+ } ;
112+
113+ if let Some ( manager) = alias_manager
114+ && matches ! ( manager. exists( & remote. alias) , Ok ( true ) )
115+ {
116+ return Ok ( parsed) ;
117+ }
118+
119+ if Path :: new ( path) . exists ( ) {
120+ return Ok ( ParsedPath :: Local ( PathBuf :: from ( path) ) ) ;
121+ }
122+
123+ Ok ( parsed)
124+ }
125+
105126async fn copy_local_to_s3 (
106127 src : & Path ,
107128 dst : & RemotePath ,
@@ -583,6 +604,16 @@ async fn copy_s3_to_s3(
583604#[ cfg( test) ]
584605mod tests {
585606 use super :: * ;
607+ use rc_core:: { Alias , ConfigManager } ;
608+ use tempfile:: TempDir ;
609+
610+ fn temp_alias_manager ( ) -> ( AliasManager , TempDir ) {
611+ let temp_dir = TempDir :: new ( ) . expect ( "create temp dir" ) ;
612+ let config_path = temp_dir. path ( ) . join ( "config.toml" ) ;
613+ let config_manager = ConfigManager :: with_path ( config_path) ;
614+ let alias_manager = AliasManager :: with_config_manager ( config_manager) ;
615+ ( alias_manager, temp_dir)
616+ }
586617
587618 #[ test]
588619 fn test_parse_local_path ( ) {
@@ -639,6 +670,41 @@ mod tests {
639670 }
640671 }
641672
673+ #[ test]
674+ fn test_parse_cp_path_prefers_existing_local_path_when_alias_missing ( ) {
675+ let ( alias_manager, temp_dir) = temp_alias_manager ( ) ;
676+ let full = temp_dir. path ( ) . join ( "issue-2094-local" ) . join ( "file.txt" ) ;
677+ let full_str = full. to_string_lossy ( ) . to_string ( ) ;
678+
679+ if let Some ( parent) = full. parent ( ) {
680+ std:: fs:: create_dir_all ( parent) . expect ( "create parent dirs" ) ;
681+ }
682+ std:: fs:: write ( & full, b"test" ) . expect ( "write local file" ) ;
683+
684+ let parsed = parse_cp_path ( & full_str, Some ( & alias_manager) ) . expect ( "parse path" ) ;
685+ assert ! ( matches!( parsed, ParsedPath :: Local ( _) ) ) ;
686+ }
687+
688+ #[ test]
689+ fn test_parse_cp_path_keeps_remote_when_alias_exists ( ) {
690+ let ( alias_manager, _temp_dir) = temp_alias_manager ( ) ;
691+ alias_manager
692+ . set ( Alias :: new ( "target" , "http://localhost:9000" , "a" , "b" ) )
693+ . expect ( "set alias" ) ;
694+
695+ let parsed = parse_cp_path ( "target/bucket/file.txt" , Some ( & alias_manager) )
696+ . expect ( "parse remote path" ) ;
697+ assert ! ( matches!( parsed, ParsedPath :: Remote ( _) ) ) ;
698+ }
699+
700+ #[ test]
701+ fn test_parse_cp_path_keeps_remote_when_local_missing ( ) {
702+ let ( alias_manager, _temp_dir) = temp_alias_manager ( ) ;
703+ let parsed = parse_cp_path ( "missing/bucket/file.txt" , Some ( & alias_manager) )
704+ . expect ( "parse remote path" ) ;
705+ assert ! ( matches!( parsed, ParsedPath :: Remote ( _) ) ) ;
706+ }
707+
642708 #[ test]
643709 fn test_cp_args_defaults ( ) {
644710 let args = CpArgs {
0 commit comments