astrid-runtime/astrid · error

legacy state source is not a directory: {}

Error message

legacy state source is not a directory: {}

What it means

Thrown by validate_legacy_tree when the legacy state source path exists but is a regular file, FIFO, or other non-directory entry instead of the expected directory. The retirement machinery only knows how to validate and delete directory trees, so it refuses with InvalidData rather than proceeding.

Source

Thrown at crates/astrid-core/src/dirs_layout_retirement.rs:49

    metadata.dev()
}

#[cfg(not(unix))]
pub(super) fn legacy_tree_device(_metadata: &std::fs::Metadata) -> u64 {
    0
}

pub(super) fn validate_legacy_tree(path: &Path, root_device: u64) -> io::Result<()> {
    let metadata = std::fs::symlink_metadata(path)?;
    if metadata.file_type().is_symlink() {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            format!("legacy state source is redirected: {}", path.display()),
        ));
    }
    if !metadata.is_dir() {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            format!("legacy state source is not a directory: {}", path.display()),
        ));
    }
    crate::platform_fs::verify_no_redirects(path)?;
    ensure_legacy_tree_boundary(path, root_device, &metadata)?;

    for entry in std::fs::read_dir(path)? {
        let entry = entry?;
        let child = entry.path();
        let child_metadata = std::fs::symlink_metadata(&child)?;
        if child_metadata.file_type().is_symlink() {
            return Err(io::Error::new(
                io::ErrorKind::InvalidData,
                format!(
                    "legacy state source contains a redirect: {}",
                    child.display()
                ),

View on GitHub (pinned to affd8760f4)

Solutions

  1. Inspect the path (ls -la / stat) and determine what the non-directory entry is
  2. Remove or rename the stray file and restore the real state directory contents there
  3. Re-run retirement after the path is a real directory again
  4. Check for concurrent writers (another astrid instance, migration job) that may be swapping the entry

Example fix

// before: state path holds a stray file
/srv/astrid/var/state  (regular file, 42 bytes)
// after: remove the stray file and restore the directory
mv /srv/astrid/var/state /srv/astrid/var/state.stray
mkdir -p /srv/astrid/var/state   # restore contents from backup
Defensive patterns

Strategy: validation

Validate before calling

fn ensure_state_dir(path: &Path) -> std::io::Result<()> {
    let meta = std::fs::symlink_metadata(path)?;
    if !meta.is_dir() {
        return Err(std::io::Error::new(
            std::io::ErrorKind::InvalidData,
            format!("{} must be a real directory", path.display()),
        ));
    }
    Ok(())
}

Type guard

fn is_plain_directory(path: &Path) -> bool {
    std::fs::symlink_metadata(path)
        .map(|m| m.is_dir())
        .unwrap_or(false)
}

Try / catch

match retire_legacy_source_tree(&path) {
    Err(e) if e.kind() == std::io::ErrorKind::InvalidData && e.to_string().contains("not a directory") => {
        // inspect entry, restore directory contents, retry
    }
    other => other?,
}

Prevention

When it happens

Trigger: validate_legacy_tree (via validate_legacy_retirement_candidate or retire_legacy_source_tree) sees symlink_metadata report is_dir() == false for the source path. Occurs if the state path was replaced by a plain file, a truncated restore left a file where the directory was, or a race changed the entry type after the caller's initial check.

Common situations: Backup restores that recreated state as a tar file left in place; scripts that wrote a marker file at the state path; migration tooling that deleted the directory and wrote a placeholder; concurrent processes swapping the path mid-retirement.

Related errors


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