Skip to content

feat(executor): stream binary responses to stdout when --output is omitted#750

Open
hoyt-harness wants to merge 1 commit intogoogleworkspace:mainfrom
hoyt-harness:fix/743-binary-stdout
Open

feat(executor): stream binary responses to stdout when --output is omitted#750
hoyt-harness wants to merge 1 commit intogoogleworkspace:mainfrom
hoyt-harness:fix/743-binary-stdout

Conversation

@hoyt-harness
Copy link
Copy Markdown

Summary

drive files export, drive files get --alt media, and any other binary endpoint previously always wrote content to a download.{ext} file in cwd and printed a status JSON object to stdout, with no way to pipe the content to another command without an intermediate file.

This PR changes the default to the Unix/curl convention:

Invocation Before After
gws drive files export ... writes download.txt to cwd; prints status JSON raw bytes on stdout
gws drive files export ... --output file.txt writes file.txt; prints status JSON writes file.txt; prints status JSON (unchanged)
gws drive files get ... --alt media writes download.pdf to cwd; prints status JSON raw bytes on stdout
gws drive files get ... --alt media --output file.pdf writes file.pdf; prints status JSON writes file.pdf; prints status JSON (unchanged)

The mime_to_extension() helper, which existed only to derive the download.{ext} fallback filename, is removed as dead code.

Note: This PR addresses the 4b sub-issue of #743. The 4d sub-issue (calendar events delete 204 mis-routing) is a separate, independent fix in PR #749.

Breaking change

Scripts that rely on a download.txt / download.pdf / etc. appearing in cwd after drive files export or drive files get --alt media must be updated to either:

gws drive files export ... > output.txt        # redirect stdout
gws drive files export ... --output output.txt # explicit --output flag

Test plan

  • gws drive files export --file-id <ID> --params '{"mimeType":"text/plain"}' | head streams content to stdout without creating any file in cwd
  • gws drive files export --file-id <ID> --params '{"mimeType":"text/plain"}' --output export.txt saves export.txt and prints status JSON (existing behaviour)
  • gws drive files get --file-id <ID> --params '{"alt":"media"}' > file.pdf produces a valid PDF
  • cargo clippy -- -D warnings passes
  • cargo test passes (excluding the two pre-existing auth::tests::test_get_quota_project_* failures that are environment-specific ADC file reads unrelated to this change)

Closes #743 (sub-issue 4b)

🤖 Generated with Claude Code

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 22, 2026

🦋 Changeset detected

Latest commit: b2c68f1

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@googleworkspace/cli Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@googleworkspace-bot googleworkspace-bot added area: http area: core Core CLI parsing, commands, error handling, utilities labels Apr 22, 2026
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request modifies the CLI's behavior for binary file exports and downloads. Previously, these commands automatically saved content to a local file in the current working directory. The change updates the default behavior to stream raw bytes to stdout, allowing users to pipe output directly to other processes, while maintaining the existing functionality when an explicit --output path is provided.

Highlights

  • Binary Streaming: Updated binary response handling to stream raw bytes directly to stdout when no --output flag is provided, aligning with standard Unix/curl behavior.
  • Code Cleanup: Removed the mime_to_extension helper function and its associated tests, as it is no longer required for generating default download filenames.
  • Refactoring: Simplified match arm logic in script processing for better readability.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Generative AI Prohibited Use Policy, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates binary response handling to stream directly to stdout when an output path is not provided, replacing the previous behavior of defaulting to a local file. It also removes the now-obsolete mime_to_extension helper and refactors some match logic in the script helper. A review comment points out a regression in the new stdout streaming implementation: it fails to respect the capture_output flag and does not track the total number of bytes transferred, which is inconsistent with the file-based output path.

Comment thread crates/google-workspace-cli/src/executor.rs
@hoyt-harness
Copy link
Copy Markdown
Author

Good catch — two issues fixed in the latest commit:

  1. capture_output regression — the stdout path now respects the flag. When capture_output is true the stream is drained (bytes counted) without writing to stdout, and the function returns {status, mimeType, bytes} metadata consistent with the file-output path. When false it streams to stdout as before.

  2. total_bytes tracking — added to the stdout path so both code paths return consistent metadata.

Note: I kept the helpers/script.rs clippy fix in this PR since upstream/main carries that pre-existing lint error — removing it would cause this PR's clippy CI check to fail.

@googleworkspace-bot
Copy link
Copy Markdown
Collaborator

/gemini review

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request modifies the handling of binary responses to stream directly to stdout when the --output flag is omitted, facilitating easier piping to other processes. It also removes the mime_to_extension helper and refactors some internal logic. Feedback highlights an inconsistency where file operations are still performed when capture_output is enabled, unlike the stdout implementation. Additionally, there is a recommendation to implement a TTY check to prevent streaming raw binary data directly to a terminal, which can cause corruption or security issues.

Comment thread crates/google-workspace-cli/src/executor.rs
Comment thread crates/google-workspace-cli/src/executor.rs
…itted

Previously handle_binary_response() always wrote content to a
download.{ext} file in cwd and printed a status JSON to stdout, making
it impossible to pipe binary output to another command without an
intermediate file.

Now:
- With --output <path>: content saved to file, status JSON printed
  (existing behaviour, unchanged)
- Without --output: raw bytes streamed directly to stdout

This makes drive files export and drive files get --alt media behave
like curl: the content goes to stdout by default, and --output is the
opt-in for file saving.

Also removes mime_to_extension() and its test — the helper existed only
to derive the download.{ext} fallback filename and is now dead code.

Also collapses a pre-existing clippy::collapsible_match in
helpers/script.rs to keep the build warning-clean.

Fixes googleworkspace#743 (sub-issue 4b)

BREAKING CHANGE: scripts that rely on a download.txt / download.pdf
appearing in cwd after drive files export must redirect stdout or add
--output explicitly.
@hoyt-harness hoyt-harness force-pushed the fix/743-binary-stdout branch from 36bda0c to b2c68f1 Compare April 22, 2026 22:07
@hoyt-harness
Copy link
Copy Markdown
Author

Line 397 (TTY guard) — fixed. This is a real gap introduced by the PR: pre-change, binary always went to a file; post-change it can reach a terminal. Added an IsTerminal check (stable since Rust 1.70; CI uses dtolnay/rust-toolchain@stable so no MSRV concern, no new dep):

if !capture_output && std::io::IsTerminal::is_terminal(&std::io::stdout()) {
    return Err(GwsError::Validation(
        "Refusing to stream binary content to a terminal. \
         Use --output <path> to save to a file, or redirect stdout (e.g. '> file')."
            .to_string(),
    ));
}

The !capture_output guard ensures the check only fires when we'd actually write bytes to stdout — the capture path drains the stream without writing, so no TTY risk there.


Line 369 (skip file write when capture_output=true) — respectfully declining. The two branches are asymmetric by design, not by accident:

  • The stdout path skips writing because there's no separate destination — writing bytes to stdout and returning a captured Value are mutually exclusive.
  • The file path has two independent destinations: the disk (honoring the user's explicit --output request) and the return value. Skipping the disk write when capture_output=true would silently ignore a user-specified argument, which is the worse behaviour. The existing pattern — write the file, then either print or return the metadata depending on capture_output — is intentional and consistent with how the JSON response path works.

@googleworkspace-bot
Copy link
Copy Markdown
Collaborator

/gemini review

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request modifies the CLI to stream binary responses directly to stdout when the --output flag is omitted, rather than defaulting to a generated filename. It includes a safety check to prevent binary data from being streamed to a terminal and removes the now-obsolete mime_to_extension helper and its tests. Additionally, the script processing logic was refactored for better readability using match guards. There were no review comments provided, and I have no further feedback to provide.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: core Core CLI parsing, commands, error handling, utilities area: http

Projects

None yet

2 participants