astrid-runtime/astrid · error · io::Error

layout migration source is redirected or not a directory: {}

Error message

layout migration source is redirected or not a directory: {}

What it means

inventory_tree inventories a migration source directory to fingerprint it into a LayoutTreeIdentityV1. If symlink_metadata on the source succeeds but the entry is a symlink or not a directory, the function refuses with this InvalidData error rather than hashing a redirected or wrong-kind target. (A NotFound source is tolerated and hashed as "absent"; other IO errors propagate as-is.)

Source

Thrown at crates/astrid-core/src/dirs_layout_records.rs:267

        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            "layout destination path cannot be represented losslessly on this platform",
        ));
    }
    Ok(path)
}

pub(super) fn inventory_tree(path: &Path) -> io::Result<LayoutTreeIdentityV1> {
    let mut hasher = blake3::Hasher::new_derive_key("astrid layout source inventory v1");
    let mut entries = 0_u64;
    let mut bytes = 0_u64;
    match std::fs::symlink_metadata(path) {
        Err(error) if error.kind() == io::ErrorKind::NotFound => {
            hasher.update(b"absent");
        },
        Err(error) => return Err(error),
        Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_dir() => {
            return Err(io::Error::new(
                io::ErrorKind::InvalidData,
                format!(
                    "layout migration source is redirected or not a directory: {}",
                    path.display()
                ),
            ));
        },
        Ok(_) => {
            crate::platform_fs::verify_no_redirects(path)?;
            inventory_directory(path, path, &mut hasher, &mut entries, &mut bytes)?;
        },
    }
    Ok(LayoutTreeIdentityV1 {
        path_encoding: "os-str-encoded-bytes-v1".to_owned(),
        physical_path_hex: physical_path_hex(path)?,
        inventory_algorithm: "blake3-derive-key-v1".to_owned(),
        inventory_digest: hasher.finalize().to_hex().to_string(),
        entries,

View on GitHub (pinned to affd8760f4)

Solutions

  1. Replace the symlink at the source path with the real directory (or repoint the migration at the real directory).
  2. Verify the source path with `ls -la` and ensure it is an actual directory, not a file.
  3. If the legacy source is legitimately gone, ensure it is absent (NotFound is accepted) rather than replaced by a wrong-kind entry.

Example fix

// before: source replaced by symlink
ln -s /data/new-layout /data/legacy-layout
// after: real directory
rm /data/legacy-layout
mkdir /data/legacy-layout  # or move the real directory back
Defensive patterns

Strategy: validation

Validate before calling

let md = std::fs::symlink_metadata(&source_dir)?;
match md {
    md if md.is_dir() => {},                       // ok
    _ if md.file_type().is_symlink() => return Err("legacy source is a symlink"),
    _ => return Err("legacy source is not a directory"),
}

Try / catch

match retire_verified_legacy_source(...) {
    Err(e) if e.to_string().contains("source is redirected or not a directory") => restore_source_directory(),
    other => other,
}

Prevention

When it happens

Trigger: Called from LayoutRetirementV1, retire_verified_legacy_source, or LayoutMigrationMaterialV1 when the source path is a symlink to a directory, a regular file, or another non-directory entry.

Common situations: Legacy source directory replaced by a symlink to consolidated storage; migration re-run after the source directory was deleted and a file created in its place; users pointing migration at a file instead of the directory.

Related errors


AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09). Data as JSON: /api/errors/d3048f2f55d9007c. Report an issue: GitHub.