windmill-labs/windmill · error

Path '{}' exists and is not a directory

Error message

Path '{}' exists and is not a directory

What it means

create_empty_dir prepares a fresh target directory used by archive fetch and git clone paths. If the path already exists but is a regular file, symlink-to-file, or other non-directory entry, it cannot create the directory there and returns io::ErrorKind::AlreadyExists with this message. It exists so callers fail fast instead of clobbering an unexpected file.

Source

Thrown at backend/windmill-worker/src/ansible_executor.rs:401

    Ok(commit_hash)
}

pub fn create_empty_dir(path: &PathBuf) -> std::io::Result<()> {
    if path.exists() {
        if path.is_dir() {
            let mut entries = std::fs::read_dir(&path)?;
            if entries.next().is_some() {
                return Err(std::io::Error::new(
                    std::io::ErrorKind::AlreadyExists,
                    format!(
                        "Directory '{}' already exists and is not empty",
                        path.display()
                    ),
                ));
            }
            Ok(())
        } else {
            Err(std::io::Error::new(
                std::io::ErrorKind::AlreadyExists,
                format!("Path '{}' exists and is not a directory", path.display()),
            ))
        }
    } else {
        std::fs::create_dir_all(path)
    }
}

/// Signals a detached `spawn_blocking` task that the future awaiting it is
/// gone, so it can stop instead of running to completion in the background.
struct AbortOnDrop(std::sync::Arc<std::sync::atomic::AtomicBool>);

impl Drop for AbortOnDrop {
    fn drop(&mut self) {
        self.0.store(true, std::sync::atomic::Ordering::Relaxed);
    }
}

View on GitHub (pinned to e474e8803c)

Solutions

  1. Inspect the path in the message and delete or rename the offending file (rm '<path>' or mv it aside), then re-run the job.
  2. Ensure the target directory passed to fetch_repo_archive/clone_repo_without_history is dedicated and empty; pick a different target path if the file must be kept.
  3. In calling code, check path.is_dir() first and handle the AlreadyExists kind explicitly (e.g. remove_file then retry create_empty_dir).

Example fix

// before
create_empty_dir(&target)?;
// after
if target.exists() && !target.is_dir() {
    std::fs::remove_file(&target)?;
}
create_empty_dir(&target)?;
Defensive patterns

Strategy: validation

Validate before calling

let path = std::path::Path::new("/target/dir");
if path.exists() && !path.is_dir() {
    return Err(format!("{} exists and is not a directory; remove it first", path.display()));
}

Type guard

fn is_usable_empty_dir(p: &std::path::Path) -> bool {
    p.is_dir() && std::fs::read_dir(p).map(|mut d| d.next().is_none()).unwrap_or(false)
}

Try / catch

match create_empty_dir(&path) {
    Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
        std::fs::remove_file(&path)?;
        create_empty_dir(&path)?;
    }
    other => other?,
}

Prevention

When it happens

Trigger: Calling create_empty_dir(path) when path exists as a non-directory (regular file or symlink to one), e.g. via fetch_repo_archive or clone_repo_without_history when the workspace/clone target path is occupied by a file.

Common situations: A leftover file at the repo checkout path (e.g. a tarball, lock file, or previous failed run wrote a file where a directory was expected); bind-mounted or pre-created files in container paths; case-insensitive filesystems colliding with an existing file.

Related errors


AI-assisted analysis of windmill-labs/windmill@e474e8803c (2026-09-03). Data as JSON: /api/errors/7fafd1f5afd09da1. Report an issue: GitHub.