Skip to content

Commit 8eaee0b

Browse files
authored
fix(cli): preserve symlinks during sandbox upload (NVIDIA#1595)
* fix(cli): preserve symlinks during sandbox upload * docs(sandboxes): document upload symlink behavior
1 parent 6665365 commit 8eaee0b

4 files changed

Lines changed: 296 additions & 50 deletions

File tree

crates/openshell-cli/src/main.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2609,15 +2609,18 @@ async fn main() -> Result<()> {
26092609
apply_auth(&mut tls, &ctx.name);
26102610
let sandbox_dest = dest.as_deref();
26112611
let local = std::path::Path::new(&local_path);
2612-
if !local.exists() {
2612+
if !run::local_upload_path_exists(local) {
26132613
return Err(miette::miette!(
26142614
"local path does not exist: {}",
26152615
local.display()
26162616
));
26172617
}
26182618
let dest_display = sandbox_dest.unwrap_or("~");
26192619
eprintln!("Uploading {} -> sandbox:{}", local.display(), dest_display);
2620-
if !no_git_ignore && let Ok((base_dir, files)) = run::git_sync_files(local) {
2620+
if !no_git_ignore
2621+
&& !run::local_upload_path_is_symlink(local)
2622+
&& let Ok((base_dir, files)) = run::git_sync_files(local)
2623+
{
26212624
run::sandbox_sync_up_files(
26222625
&ctx.endpoint,
26232626
&name,

crates/openshell-cli/src/run.rs

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2020,7 +2020,17 @@ pub async fn sandbox_create(
20202020
"\u{2022}".dimmed(),
20212021
);
20222022
let local = Path::new(local_path);
2023-
if *git_ignore && let Ok((base_dir, files)) = git_sync_files(local) {
2023+
if !local_upload_path_exists(local) {
2024+
return Err(miette::miette!(
2025+
"local path does not exist: {}",
2026+
local.display()
2027+
));
2028+
}
2029+
2030+
if *git_ignore
2031+
&& !local_upload_path_is_symlink(local)
2032+
&& let Ok((base_dir, files)) = git_sync_files(local)
2033+
{
20242034
sandbox_sync_up_files(
20252035
&effective_server,
20262036
&sandbox_name,
@@ -2031,7 +2041,7 @@ pub async fn sandbox_create(
20312041
&effective_tls,
20322042
)
20332043
.await?;
2034-
} else if local.exists() {
2044+
} else {
20352045
sandbox_sync_up(
20362046
&effective_server,
20372047
&sandbox_name,
@@ -2376,7 +2386,7 @@ pub async fn sandbox_sync_command(
23762386
match (up, down) {
23772387
(Some(local_path), None) => {
23782388
let local = Path::new(local_path);
2379-
if !local.exists() {
2389+
if !local_upload_path_exists(local) {
23802390
return Err(miette::miette!(
23812391
"local path does not exist: {}",
23822392
local.display()
@@ -5412,6 +5422,14 @@ pub fn git_sync_files(local_path: &Path) -> Result<(PathBuf, Vec<String>)> {
54125422
Ok((base_dir, files))
54135423
}
54145424

5425+
pub fn local_upload_path_exists(path: &Path) -> bool {
5426+
std::fs::symlink_metadata(path).is_ok()
5427+
}
5428+
5429+
pub fn local_upload_path_is_symlink(path: &Path) -> bool {
5430+
std::fs::symlink_metadata(path).is_ok_and(|metadata| metadata.file_type().is_symlink())
5431+
}
5432+
54155433
// ---------------------------------------------------------------------------
54165434
// Sandbox policy commands
54175435
// ---------------------------------------------------------------------------
@@ -6967,12 +6985,13 @@ mod tests {
69676985
format_gateway_select_items, format_provider_attachment_table, gateway_add,
69686986
gateway_auth_label, gateway_env_override_warning, gateway_select_with, gateway_type_label,
69696987
git_sync_files, http_health_check, image_requests_gpu, import_local_package_mtls_bundle,
6970-
inferred_provider_type, package_managed_tls_dirs, parse_cli_setting_value,
6971-
parse_credential_expiry_cli_value, parse_credential_expiry_pairs, parse_credential_pairs,
6972-
plaintext_gateway_is_remote, progress_step_from_metadata,
6973-
provider_profile_allows_refresh_bootstrap, provisioning_timeout_message,
6974-
ready_false_condition_message, refresh_status_header, refresh_status_row, resolve_from,
6975-
sandbox_should_persist, service_expose_status_error, service_url_for_gateway,
6988+
inferred_provider_type, local_upload_path_exists, local_upload_path_is_symlink,
6989+
package_managed_tls_dirs, parse_cli_setting_value, parse_credential_expiry_cli_value,
6990+
parse_credential_expiry_pairs, parse_credential_pairs, plaintext_gateway_is_remote,
6991+
progress_step_from_metadata, provider_profile_allows_refresh_bootstrap,
6992+
provisioning_timeout_message, ready_false_condition_message, refresh_status_header,
6993+
refresh_status_row, resolve_from, sandbox_should_persist, service_expose_status_error,
6994+
service_url_for_gateway,
69766995
};
69776996
use crate::TEST_ENV_LOCK;
69786997
use hyper::StatusCode;
@@ -7758,6 +7777,31 @@ mod tests {
77587777
assert_eq!(files, vec!["file.txt", "inner/child.txt"]);
77597778
}
77607779

7780+
#[cfg(unix)]
7781+
#[test]
7782+
fn local_upload_path_helpers_accept_symlinks() {
7783+
let tmpdir = tempfile::tempdir().expect("create tmpdir");
7784+
let target = tmpdir.path().join("target.txt");
7785+
let link = tmpdir.path().join("link.txt");
7786+
fs::write(&target, "target").expect("write target");
7787+
std::os::unix::fs::symlink("target.txt", &link).expect("create symlink");
7788+
7789+
assert!(local_upload_path_exists(&link));
7790+
assert!(local_upload_path_is_symlink(&link));
7791+
}
7792+
7793+
#[cfg(unix)]
7794+
#[test]
7795+
fn local_upload_path_helpers_accept_dangling_symlinks() {
7796+
let tmpdir = tempfile::tempdir().expect("create tmpdir");
7797+
let link = tmpdir.path().join("dangling-link.txt");
7798+
std::os::unix::fs::symlink("missing.txt", &link).expect("create symlink");
7799+
7800+
assert!(local_upload_path_exists(&link));
7801+
assert!(local_upload_path_is_symlink(&link));
7802+
assert!(!link.exists(), "std::path::Path::exists follows symlinks");
7803+
}
7804+
77617805
#[test]
77627806
fn gateway_select_uses_explicit_name_without_prompting() {
77637807
let tmpdir = tempfile::tempdir().expect("create tmpdir");

0 commit comments

Comments
 (0)