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.

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)

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.


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