Skip to content

feat(drive): add +download helper for downloading Drive files#704

Open
nuthalapativarun wants to merge 9 commits intogoogleworkspace:mainfrom
nuthalapativarun:feat/drive-download
Open

feat(drive): add +download helper for downloading Drive files#704
nuthalapativarun wants to merge 9 commits intogoogleworkspace:mainfrom
nuthalapativarun:feat/drive-download

Conversation

@nuthalapativarun
Copy link
Copy Markdown

Description

Adds a drive +download helper that downloads a Drive file to a local path. This is a multi-step orchestration helper that cannot be expressed as a single Discovery API call:

  1. Fetch metadatafiles.get?fields=name,mimeType to determine the file's name and type
  2. Route to the correct endpoint based on MIME type:
    • Google Workspace native files (Docs, Sheets, Slides — application/vnd.google-apps.*) → files.export?mimeType=<type>, requiring a caller-supplied --mime-type
    • All other files → files.get?alt=media
  3. Write bytes to a validated local path

This complements the existing +upload helper and satisfies the helper justification criteria in AGENTS.md (multi-step orchestration, format translation for native files).

Input validation:

  • --file (Drive file ID): validate_resource_name() — rejects traversal/injection
  • --output (local path): validate_safe_file_path() — rejects path traversal outside CWD
  • --mime-type: reject_dangerous_chars() — rejects control characters

Usage:

# Download a binary file (PDF, image, etc.)
gws drive +download --file FILE_ID

# Download with explicit output path
gws drive +download --file FILE_ID --output report.pdf

# Export a Google Doc as PDF
gws drive +download --file DOC_ID --mime-type application/pdf

# Export a Google Sheet as CSV
gws drive +download --file SHEET_ID --mime-type text/csv --output data.csv

Dry Run Output:

// +download is a read-only operation with no POST body.
// On success it prints:
{
  "file": "report.pdf",
  "bytes": 102400,
  "mimeType": "application/pdf"
}

Checklist:

  • My code follows the AGENTS.md guidelines (no generated google-* crates).
  • I have run cargo fmt --all to format the code perfectly.
  • I have run cargo clippy -- -D warnings and resolved all warnings.
  • I have added tests that prove my fix is effective or that my feature works.
  • I have provided a Changeset file (e.g. via pnpx changeset) to document my changes.

Adds a multi-step +download command that determines how to fetch a file
based on its MIME type:
- Google Workspace native files (Docs/Sheets/Slides) → files.export with
  a caller-supplied --mime-type (e.g. application/pdf, text/csv)
- Binary/other files → files.get?alt=media

The output path is validated with validate_safe_file_path() to reject
path traversal and control characters. The file ID is validated with
validate_resource_name() before being embedded in URLs.

Complements the existing +upload helper; justified as a multi-step
orchestration helper per the helper guidelines in AGENTS.md.
- Sanitize API error body strings with sanitize_for_terminal() before
  embedding them in GwsError::Api to prevent terminal escape injection
- Add --dry-run support: after metadata fetch and path resolution, print
  what would be downloaded and return without network/disk I/O
- Stream response bytes to file via tokio::fs::File + bytes_stream()
  instead of resp.bytes().await to avoid OOM on large Drive files
- Parse Google API error JSON properly (reason, message, enable_url) so that
  specialised error hints in error.rs (e.g. accessNotConfigured) still fire
- Sanitize Drive filename: replace '/' chars to prevent unintended subdirectories
- Use u64 for byte_count to avoid overflow on 32-bit platforms with >4GB files
- Stream to a .tmp file and rename on success; delete partial file on any error
- Report actual output MIME type in JSON result (export format for native files)
…tion, rename cleanup

- Sanitize backslash in addition to slash in Drive filename to prevent
  unintended subdirectory creation on Windows
- Validate --mime-type requirement for native Google Workspace files before
  the dry-run block so dry-run output is accurate and errors surface early
- Clean up temp file if tokio::fs::rename fails to avoid orphaned .tmp files
… rename, dedup error parsing

- Short-circuit dry-run before any auth or network I/O (consistent with +upload
  pattern) so unauthenticated users can verify syntax offline
- Remove local parse_google_api_error; add pub executor::api_error_from_response
  so error parsing logic lives in one place and accessNotConfigured hints fire
  consistently from all callers
- Remove existing destination file before rename for cross-platform consistency:
  tokio::fs::rename overwrites on Unix but fails if dest exists on Windows;
  TOCTOU limitation is documented as a known constraint
…response and +download

- Use "httpError" (not "unknown") as fallback reason in api_error_from_response
  to match the existing handle_error_response fallback in executor.rs
- Use snake_case keys in all JSON output (dry_run, file_id, export_mime_type,
  saved_file, mime_type) to match the project convention used in executor.rs
- Expand filename sanitization to cover all Windows-reserved chars (:*?"<>|),
  control characters (Cc), and dangerous Unicode (Cf/bidi/zero-width) to
  prevent file creation failures and terminal injection on all platforms
…on all platforms

tokio::fs::rename uses MOVEFILE_REPLACE_EXISTING on Windows (since Rust 1.26),
so the destination file is always overwritten atomically. The remove_file + rename
sequence added previously was wrong: it widens the failure window so that if rename
fails the user loses the original file at the destination as well.
…name

- Sanitize `message` and `reason` fields extracted from Google API JSON
  error responses to prevent terminal escape sequence injection from
  malicious or compromised API responses.
- Add a random 64-bit hex suffix to the download temp filename
  (`.{name}.{rand}.tmp`) to prevent symlink attacks in world-writable
  output directories.
…ints

- Pass `RestDescription` to `handle_download` so it resolves method paths,
  OAuth scopes (union of files.get + files.export), and the API base URL
  from the Discovery Document instead of hardcoded strings. This correctly
  respects custom root_url values (proxies, VPC-SC, alternative environments).
- Add `x-goog-user-project` header to all manual requests in `handle_download`
  for correct billing/quota attribution with service accounts, mirroring the
  existing behaviour in `executor::execute_method`.
- Add `auth_method` parameter to `api_error_from_response` and include the
  401/403 login hint (matching `handle_error_response`) so the public function
  is complete and correct for all callers.
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 12, 2026

🦋 Changeset detected

Latest commit: 06f53d3

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

@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 introduces a new drive +download helper to the Google Workspace CLI, allowing users to download files from Google Drive. The implementation handles the complexities of Google Drive's API by orchestrating metadata retrieval and routing between direct media downloads and format-specific exports for Google Workspace native files. It includes comprehensive security measures for file system operations and improved error handling for API interactions.

Highlights

  • New +download helper: Added a new drive +download command to the CLI, enabling users to download files from Google Drive to a local path with support for both binary files and Google Workspace native file exports.
  • Multi-step orchestration: Implemented a multi-step download process that fetches file metadata first to determine the correct download strategy (direct media download vs. export) and handles MIME type requirements for Google Docs, Sheets, and Slides.
  • Security and Validation: Added robust input validation for file IDs, output paths, and MIME types to prevent path traversal and injection attacks, including streaming downloads to temporary files before final renaming.
  • Error Handling: Introduced api_error_from_response to provide better error parsing and user-friendly feedback for API failures, including helpful login hints for authentication issues.
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.

@googleworkspace-bot googleworkspace-bot added area: http area: core Core CLI parsing, commands, error handling, utilities labels Apr 12, 2026
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 introduces the +download helper command for the Google Drive service, enabling users to download both binary and native Google Workspace files. The implementation includes metadata fetching, path traversal validation, and atomic file writing via temporary files. Additionally, a new utility for parsing Google API error responses was added to the executor. Feedback focuses on ensuring JSON output keys follow camelCase conventions for consistency with the Discovery API and improving the robustness of URL construction by handling potential missing trailing slashes in the base URL.

"dry_run": true,
"file_id": file_id,
"output": out_display,
"export_mime_type": export_mime,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

For consistency with the rest of the CLI and the Google Discovery API, JSON output keys should use camelCase. Please use exportMimeType instead of export_mime_type.

Suggested change
"export_mime_type": export_mime,
"exportMimeType": export_mime,


// 2. Fetch file metadata to get name and MIME type
let encoded_id = crate::validate::encode_path_segment(file_id);
let metadata_url = format!("{}files/{}", base_url, encoded_id);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Manual URL concatenation is risky if base_url does not end with a trailing slash. While Discovery Documents usually provide a trailing slash in servicePath, base_url can be overridden by users or proxies. It is safer to ensure the slash is present or use a robust join method.

Suggested change
let metadata_url = format!("{}files/{}", base_url, encoded_id);
let metadata_url = format!("{}/files/{}", base_url.trim_end_matches('/'), encoded_id);

// Safety: export_mime is validated as Some above for native files.
let mime = export_mime.as_deref().unwrap();
// Build export URL from Discovery Document base URL (respects custom root_url).
let export_url = format!("{}files/{}/export", base_url, encoded_id);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Manual URL concatenation is risky if base_url does not end with a trailing slash. Please ensure a safe join here as well.

Suggested change
let export_url = format!("{}files/{}/export", base_url, encoded_id);
let export_url = format!("{}/files/{}/export", base_url.trim_end_matches('/'), encoded_id);

serde_json::to_string_pretty(&json!({
"saved_file": out_path.display().to_string(),
"bytes": byte_count,
"mime_type": output_mime,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

For consistency with the rest of the CLI (e.g., executor::handle_binary_response) and the Google Discovery API, JSON output keys should use camelCase. Please use mimeType instead of mime_type.

Suggested change
"mime_type": output_mime,
"mimeType": output_mime,

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

Development

Successfully merging this pull request may close these issues.

2 participants