{"record":{"id":"f57ea16c840b1cc5","repo":"astrid-runtime/astrid","slug":"capsule-materialization-destination-is-not-a-direc","errorCode":null,"errorMessage":"capsule materialization destination is not a directory","messagePattern":"capsule materialization destination is not a directory","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-capsule-install/src/storage.rs","lineNumber":542,"sourceCode":"        .with_context(|| format!(\"resolve durable uid for principal {principal}\"))?;\n    Ok(read_verified_durable_package(store, uid, id)?.map(|package| package.metadata().clone()))\n}\n\n/// Materialize a verified durable package into a fresh disposable directory.\n///\n/// This helper is for loaders whose current host ABI still accepts a path. It\n/// never establishes authority: callers must retain the package snapshot and\n/// digest, and the destination must be a new cache generation. Archive paths,\n/// links, special files, duplicate entries, and symlinked parents are rejected\n/// before any bytes are written. Exact metadata and authority sidecars are\n/// restored from the package bytes, not trusted from the archive.\npub fn materialize_capsule_package(\n    package: &CapsulePackage,\n    destination: &Path,\n) -> anyhow::Result<()> {\n    match fs::symlink_metadata(destination) {\n        Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_dir() => {\n            bail!(\"capsule materialization destination is not a directory\")\n        },\n        Ok(_) => bail!(\"capsule materialization destination already exists\"),\n        Err(error) if error.kind() == io::ErrorKind::NotFound => {},\n        Err(error) => {\n            return Err(error).with_context(|| format!(\"inspect {}\", destination.display()));\n        },\n    }\n    if let Some(parent) = destination.parent() {\n        fs::create_dir_all(parent)\n            .with_context(|| format!(\"create materialization parent {}\", parent.display()))?;\n    }\n    fs::create_dir(destination)\n        .with_context(|| format!(\"create materialization {}\", destination.display()))?;\n    let decoder = flate2::read::GzDecoder::new(Cursor::new(&package.archive));\n    let mut archive = tar::Archive::new(decoder);\n    let mut names = std::collections::BTreeSet::new();\n    for entry in archive.entries().context(\"read durable capsule archive\")? {\n        let mut entry = entry.context(\"read durable capsule archive entry\")?;","sourceCodeStart":524,"sourceCodeEnd":560,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-capsule-install/src/storage.rs#L524-L560","documentation":"materialize_capsule_package checks the destination with fs::symlink_metadata before extracting. If the path exists but is a symlink or a non-directory (e.g. a regular file), extraction cannot proceed and this error is thrown. It exists so the function never writes through a symlink or clobbers an existing file.","triggerScenarios":"Calling materialize_capsule_package(&package, dest) where dest exists as a symlink (even dangling) or as a regular file/socket/device rather than a directory.","commonSituations":"A leftover file from a previous run at the destination path; a symlinked 'current release' directory passed as the destination; a temp path pointing at a file instead of a directory; passing $HOME or a path that is actually a dotfile.","solutions":["Point the call at a fresh, non-existent directory path.","Remove or rename the file/symlink currently occupying the destination path.","If a symlink was intended, resolve it (fs::canonicalize) and materialize into the resolved parent directory instead."],"exampleFix":"// before\nmaterialize_capsule_package(&package, &Path::new(\"/opt/capsule/current\"))?; // symlink\n// after\nlet dest = Path::new(\"/opt/capsule/releases/v42\");\nmaterialize_capsule_package(&package, dest)?;\nfs::remove_file(\"/opt/capsule/current\");\nfs::symlink(dest, \"/opt/capsule/current\")?;","handlingStrategy":"validation","validationCode":"let dest = Path::new(\"/opt/capsule/materialized\");\nmatch fs::symlink_metadata(dest) {\n    Ok(m) if m.file_type().is_symlink() || !m.is_dir() => {\n        fs::remove_file(dest)?;\n    }\n    Ok(_) => anyhow::bail!(\"destination already exists\"),\n    Err(_) => {}\n}\nmaterialize_capsule_package(&package, dest)?;","typeGuard":null,"tryCatchPattern":"match materialize_capsule_package(&pkg, &dest) {\n    Ok(()) => {}\n    Err(e) if e.to_string().contains(\"not a directory\") => {\n        fs::remove_file(&dest).ok();\n        materialize_capsule_package(&pkg, &dest)?;\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Pass a path that does not yet exist (unique per run).","Never pass symlinks (e.g. 'current' release symlinks) as the destination.","Check with symlink_metadata (not exists()) so dangling symlinks are caught too."],"tags":["filesystem","destination","symlink","capsule"],"backgroundTag":"path-is-not-a-directory","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}