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
- Verify the exact path exists and is readable before invoking the command: ls -l on the literal string you pass
- Use an absolute path (and expand ~ yourself: ${HOME}/upload.pdf) so the CLI's working directory cannot change resolution
- 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
- 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
- Pass absolute paths and expand ~ in your own shell/script ($HOME/file) — the CLI does not expand tildes in quoted arguments
- Validate existence + is_file + read permission immediately before invoking any upload command, especially in scripts that build paths from variables
- For files being written concurrently, copy to a stable temporary location first so the file cannot vanish between the size stat and the streamed open
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.