Skip to content

Commit 60be094

Browse files
committed
fix(cli): preserve explicit sandbox upload file paths
1 parent 683724e commit 60be094

File tree

1 file changed

+55
-10
lines changed
  • crates/openshell-cli/src

1 file changed

+55
-10
lines changed

crates/openshell-cli/src/ssh.rs

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,30 @@ pub(crate) async fn sandbox_exec_without_exec(
451451
///
452452
/// This replaces the old rsync-based sync. Files are streamed as a tar archive
453453
/// to `ssh ... tar xf - -C <dest>` on the sandbox side.
454+
fn sandbox_extract_command(dest: &str, local_is_file: bool) -> String {
455+
let dest = dest.trim_end_matches('/');
456+
457+
if local_is_file {
458+
let parent = dest
459+
.rfind('/')
460+
.map_or(".", |pos| if pos == 0 { "/" } else { &dest[..pos] });
461+
let name = dest.rfind('/').map_or(dest, |pos| &dest[pos + 1..]);
462+
463+
format!(
464+
"mkdir -p {parent} && cat | tar xf - -C {parent} && if [ \"{name}\" != \"$(basename {name_escaped})\" ]; then exit 1; fi",
465+
parent = shell_escape(parent),
466+
name = name,
467+
name_escaped = shell_escape(name),
468+
)
469+
} else {
470+
format!(
471+
"mkdir -p {} && cat | tar xf - -C {}",
472+
shell_escape(dest),
473+
shell_escape(dest)
474+
)
475+
}
476+
}
477+
454478
pub async fn sandbox_sync_up_files(
455479
server: &str,
456480
name: &str,
@@ -464,17 +488,14 @@ pub async fn sandbox_sync_up_files(
464488
}
465489

466490
let session = ssh_session_config(server, name, tls).await?;
491+
let local_is_file = files.len() == 1 && base_dir.join(&files[0]).is_file();
467492

468493
let mut ssh = ssh_base_command(&session.proxy_command);
469494
ssh.arg("-T")
470495
.arg("-o")
471496
.arg("RequestTTY=no")
472497
.arg("sandbox")
473-
.arg(format!(
474-
"mkdir -p {} && cat | tar xf - -C {}",
475-
shell_escape(dest),
476-
shell_escape(dest)
477-
))
498+
.arg(sandbox_extract_command(dest, local_is_file))
478499
.stdin(Stdio::piped())
479500
.stdout(Stdio::inherit())
480501
.stderr(Stdio::inherit());
@@ -539,11 +560,7 @@ pub async fn sandbox_sync_up(
539560
.arg("-o")
540561
.arg("RequestTTY=no")
541562
.arg("sandbox")
542-
.arg(format!(
543-
"mkdir -p {} && cat | tar xf - -C {}",
544-
shell_escape(sandbox_path),
545-
shell_escape(sandbox_path)
546-
))
563+
.arg(sandbox_extract_command(sandbox_path, local_path.is_file()))
547564
.stdin(Stdio::piped())
548565
.stdout(Stdio::inherit())
549566
.stderr(Stdio::inherit());
@@ -1050,6 +1067,34 @@ mod tests {
10501067
use super::*;
10511068
use crate::TEST_ENV_LOCK;
10521069

1070+
#[test]
1071+
fn sandbox_extract_command_uses_parent_dir_for_explicit_file_destination() {
1072+
let command = sandbox_extract_command("/sandbox/.local/share/opencode/auth.json", true);
1073+
1074+
assert!(
1075+
command.contains("mkdir -p /sandbox/.local/share/opencode"),
1076+
"expected parent directory creation, got: {command}"
1077+
);
1078+
assert!(
1079+
command.contains("tar xf - -C /sandbox/.local/share/opencode"),
1080+
"expected extraction in parent directory, got: {command}"
1081+
);
1082+
assert!(
1083+
!command.contains("mkdir -p /sandbox/.local/share/opencode/auth.json"),
1084+
"must not treat explicit file path as directory: {command}"
1085+
);
1086+
}
1087+
1088+
#[test]
1089+
fn sandbox_extract_command_keeps_directory_destinations_as_directories() {
1090+
let command = sandbox_extract_command("/sandbox/src", false);
1091+
1092+
assert_eq!(
1093+
command,
1094+
"mkdir -p /sandbox/src && cat | tar xf - -C /sandbox/src"
1095+
);
1096+
}
1097+
10531098
#[test]
10541099
fn upsert_host_block_appends_when_missing() {
10551100
let input = "Host existing\n HostName example.com\n";

0 commit comments

Comments
 (0)