googleworkspace/cli · error · std::io::Error

failed to open upload file

Error message

failed to open upload file '{}': {}

What it means

This io::Error is produced inside build_multipart_stream() when tokio::fs::File::open fails while starting the streaming multipart/related upload body. Because the open happens lazily inside futures_util::stream::once, the error surfaces at request-stream time (during reqwest .send()), not when the command arguments are parsed. The original io::ErrorKind (NotFound, PermissionDenied, etc.) is preserved and the path plus OS cause are embedded in the message.

Solutions

  1. Verify the exact path exists and is readable before invoking the command: ls -l on the literal string you pass
  2. Use an absolute path (and expand ~ yourself: ${HOME}/upload.pdf) so the CLI's working directory cannot change resolution
  3. Check file permissions (read bit for the running user) and that the path is a regular file, not a directory or symlink to a missing target
  4. If the file may be written concurrently, upload from a snapshot/copy so it cannot disappear between size computation and streaming

Example fix

# before
gws drive files create --media "~/report.pdf"   # ~ not expanded inside quotes

# after
gws drive files create --media "$HOME/report.pdf"
Defensive patterns

Strategy: validation

Validate before calling

use tokio::fs;

let path = std::path::Path::new(media_path);
let meta = fs::metadata(path).await?;
if !meta.is_file() {
    anyhow::bail!("upload path is not a regular file: {media_path}");
}
if meta.permissions().readonly() {
    // readable check is what matters; on unix also verify read bit:
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        assert!(meta.permissions().mode() & 0o400 != 0, "no read permission");
    }
}
// Now safe to build the multipart upload

Try / catch

// The open error surfaces as io::Error while the reqwest body streams.
match execute(cmd).await {
    Err(e) if e.to_string().contains("failed to open upload file") => {
        eprintln!("check that the --media path exists and is readable");
    }
    other => other?,
}

Prevention

When it happens

Trigger: Passing a non-existent path to a media upload argument (kind NotFound); a path with no read permission (PermissionDenied); a directory instead of a file (IsADirectory/NotFound on some platforms); a file deleted between the earlier size stat and the body stream starting; a relative path resolved from a different working directory; a leading ~ that the shell did not expand because it was quoted.

Common situations: Uploading via gws drive files create --media from a script where the path was built by joining variables that are empty or wrong; quoting "~/upload.pdf" so tilde expansion never happens; running in a container where the file was not bind-mounted; case-sensitive filesystem mismatch after developing on macOS and deploying on Linux.

Understand the failure class

Background: "open() failed", "failed to open file", "cannot create file" — what a file open error means and how to fix it — this error's family across 42 libraries.


AI-assisted analysis of googleworkspace/cli@a3768d0e82 (2026-08-16). Data as JSON: /api/errors/dfb95d64acfde3c0. Report an issue: GitHub.

Appendix: source

Thrown at crates/google-workspace-cli/src/executor.rs:899

        None => "{}".to_string(),
    };

    let preamble = format!(
        "--{boundary}\r\nContent-Type: application/json; charset=UTF-8\r\n\r\n{metadata_json}\r\n\
         --{boundary}\r\nContent-Type: {media_mime}\r\n\r\n"
    );
    let postamble = format!("\r\n--{boundary}--\r\n");

    let content_length = preamble.len() as u64 + file_size + postamble.len() as u64;
    let content_type = format!("multipart/related; boundary={boundary}");

    let preamble_bytes: bytes::Bytes = preamble.into_bytes().into();
    let postamble_bytes: bytes::Bytes = postamble.into_bytes().into();

    let file_path_owned = file_path.to_owned();
    let file_stream = futures_util::stream::once(async move {
        tokio::fs::File::open(&file_path_owned).await.map_err(|e| {
            std::io::Error::new(
                e.kind(),
                format!("failed to open upload file '{}': {}", file_path_owned, e),
            )
        })
    })
    .map_ok(tokio_util::io::ReaderStream::new)
    .try_flatten();

    let stream = futures_util::stream::once(async { Ok::<_, std::io::Error>(preamble_bytes) })
        .chain(file_stream)
        .chain(futures_util::stream::once(async {
            Ok::<_, std::io::Error>(postamble_bytes)
        }));

    Ok((
        reqwest::Body::wrap_stream(stream),
        content_type,
        content_length,

View on GitHub (pinned to a3768d0e82)