astrid-runtime/astrid · error

legacy source contains redirect

Error message

legacy source contains redirect: {}

What it means

During recursive directory snapshotting, snapshot_dir examines every child with symlink_metadata (no-follow). Any child that is a symlink is rejected as a "redirect" with InvalidData, because a symlink could point anywhere and would make the snapshot digest unrepresentative and the migration unsafe.

Solutions

  1. Replace the symlink with a real copy of its target (cp -L --remove-destination), then re-run the snapshot.
  2. Remove the symlink if it is not needed.
  3. Exclude the symlinked entry from the source directory and snapshot a symlink-free tree.
  4. If the symlink is intentional, copy the whole tree to a staging directory dereferencing symlinks and snapshot that.

Example fix

// before
legacy_dir/
  current -> v2/        # symlink: snapshot_dir rejects it

// after
cp -rL legacy_dir legacy_dir_flat   # dereference symlinks into real files
rm legacy_dir/current && mv legacy_dir_flat/v2 legacy_dir/current  # or just snapshot legacy_dir_flat
Defensive patterns

Strategy: validation

Validate before calling

fn tree_has_symlinks(root: &std::path::Path) -> std::io::Result<bool> {
    for entry in walkdir::WalkDir::new(root).follow_links(false) {
        let entry = entry?;
        if std::fs::symlink_metadata(entry.path())?.file_type().is_symlink() {
            return Ok(true);
        }
    }
    Ok(false)
}
assert!(!tree_has_symlinks(&src_dir)?, "tree contains symlinks");

Type guard

fn symlink_free_tree(root: &std::path::Path) -> bool {
    !tree_has_symlinks(root).unwrap_or(true)
}

Try / catch

match snapshot_owner_controlled_path(&dir) {
    Err(e) if e.to_string().contains("contains redirect") => {
        let flat = copy_dereferencing(&dir, "/tmp/astrid-flat")?; // cp -rL equivalent
        snapshot_owner_controlled_path(&flat)
    }
    other => other,
}

Prevention

When it happens

Trigger: Calling snapshot_path_with_access / snapshot_owner_controlled_path on a directory whose tree contains a symlink at any depth (snapshot_dir iterates children recursively).

Common situations: Legacy config directories containing convenience symlinks (e.g. current -> v2, rc -> rc.d); shared library or plugin dirs with symlinked versions; user-created aliases inside a state directory.

Understand the failure class

Background: "is not a compatible type" / "cannot merge" errors: when a value's type doesn't match what the library requires — this error's family across 65 libraries.

Related errors


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

Appendix: source

Thrown at crates/astrid-kernel/src/legacy_migration_barrier/host_fs.rs:708

fn snapshot_dir(
    root: &Path,
    dir: &Path,
    device: u64,
    access: SourceAccess,
    hasher: &mut blake3::Hasher,
    identity: &mut SourceInventory,
) -> io::Result<()> {
    let mut children = fs::read_dir(dir)
        .map_err(io::Error::other)?
        .collect::<Result<Vec<_>, _>>()
        .map_err(io::Error::other)?;
    children.sort_by_key(std::fs::DirEntry::file_name);
    for entry in children {
        let path = entry.path();
        let relative = path.strip_prefix(root).map_err(io::Error::other)?;
        let metadata = fs::symlink_metadata(&path).map_err(io::Error::other)?;
        if metadata.file_type().is_symlink() {
            return Err(io::Error::new(
                io::ErrorKind::InvalidData,
                format!("legacy source contains redirect: {}", path.display()),
            ));
        }
        validate_source_entry(&path, &metadata, access)?;
        if device_id(&metadata) != device {
            return Err(io::Error::new(
                io::ErrorKind::InvalidData,
                format!(
                    "legacy source crosses a device boundary: {}",
                    path.display()
                ),
            ));
        }
        if active_mountpoint(&path)? {
            return Err(io::Error::new(
                io::ErrorKind::InvalidData,
                format!("legacy source is an active mount: {}", path.display()),

View on GitHub (pinned to affd8760f4)