Skip to content

File-path handling is inconsistent across flags and endpoints: cwd restrictions on --output/--upload, files export writes to download.txt instead of stdout, events delete creates spurious download.html on 204 #743

@hoyt-harness

Description

@hoyt-harness

Body

Version: gws v0.22.5 (Windows x86_64 binary from GitHub Releases)

Environment:

  • OS: Windows 10/11
  • Invocation: cmd /c gws ...

Related: See also #714, which documents a separate --output failure mode (silent non-write when the API returns JSON-wrapped base64). The sub-issues below are distinct from #714 but share the same flag surface.


Summary

Four distinct file-path handling behaviors observed across gws subcommands, all undocumented in --help output, and all inconsistent with typical CLI conventions. They are grouped here because they share a common root — gws treats file paths and output handling as implementation details with inconsistent contracts across subcommands.

4a. gws drive files get --output <path> rejects any path that is not relative to gws's current working directory. Absolute paths — even to normal writable locations like the user's temp directory — return exit code 3 (validation error).

4b. gws drive files export does not stream content to stdout and does not honor --output. It always writes to a file named download.txt in the current working directory, and prints a status JSON object to stdout instead of the content.

4c. gws drive files create --upload <path> has the same cwd restriction as --output. Uploading a file from outside gws's cwd returns exit 3 with a "resolves to ... which is outside the current directory" validation error.

4d. gws calendar events delete mis-routes the 204 No Content response into the same to-file save path as drive files export. It writes an empty download.html to cwd and emits a synthesized status JSON to stdout. This behavior is endpoint-specific: other 204 endpoints (gmail users labels delete, gmail users drafts delete, gmail users messages trash, tasks tasklists delete, people people deleteContact) all return clean empty or {} stdout without any file artifact.


Reproduction

4a: --output absolute path rejection

gws drive files get --file-id <ID> --params "{\"alt\":\"media\"}" --output C:\Users\<user>\AppData\Local\Temp\file.pdf

Expected: File saved to the specified absolute path.

Actual: Exit code 3. Validation error. No file written.

Workaround: Use a relative filename — file is written to gws's cwd; consumer must move it.


4b: files export saves to download.txt regardless of flag or intent

gws drive files export --file-id <ID> --params "{\"mimeType\":\"text/plain\"}"

Expected (option 1): Exported text content on stdout.

Expected (option 2): File written to a consumer-specified path via --output, with status on stderr.

Actual: Stdout contains:

{"bytes": 5234, "mimeType": "text/plain", "saved_file": "download.txt", "status": "success"}

The exported content is in download.txt in gws's cwd — created without any user-supplied path.


4c: --upload cwd restriction

gws drive files create --params "{\"supportsAllDrives\":true}" --json "{\"name\":\"test.txt\",\"parents\":[\"<folder-id>\"]}" --upload D:/some/path/file.txt

Expected: File uploaded from specified absolute path.

Actual: Exit code 3:

{"error": {"code": 400, "message": "--upload 'D:/some/path/file.txt' resolves to '\\\\?\\D:\\some\\path\\file.txt' which is outside the current directory", "reason": "validationError"}}

Workaround: Copy the file into gws's cwd first, then use a relative filename.


4d: events delete creates spurious download.html on 204

gws calendar events delete --params "{\"calendarId\":\"primary\",\"eventId\":\"<id>\",\"sendUpdates\":\"none\"}"

Expected: Clean 204 response — empty stdout or a minimal {} success marker, no file artifacts.

Actual: Exit 0, but stdout contains:

{"bytes": 0, "mimeType": "text/html", "saved_file": "download.html", "status": "success"}

And an empty download.html file is created in gws's cwd.

Contrast with other 204 endpoints in the same session:

Endpoint stdout on 204 Side-effect file
gmail users labels delete empty none
gmail users drafts delete empty none
gmail users settings filters delete empty none
tasks tasklists delete empty none
people people deleteContact {} none
calendar events delete status JSON with saved_file download.html

Hypothesis

The output pipeline appears to branch on response characteristics — if the content-type header suggests binary, or if a 204 is received on certain endpoints, the response is routed through a "save to file" path that writes download.{ext} to cwd and returns a status JSON to stdout. This branch is triggered inappropriately for:

  1. files export responses (always — arguably intended for binary exports, but the default should be stdout streaming).
  2. calendar events delete 204 responses — likely because the empty 204 carries a default text/html content-type header, which trips the binary-save branch. Other delete endpoints appear to reach a different code path.

The cwd restrictions on --output and --upload (4a, 4c) appear to be a path-validation function applied to any flag that accepts a file path. There may be a safety intent here, but the implementation is over-broad — rejecting absolute paths within the user's own home or temp directory is not meaningful protection.


Impact

  1. cwd management becomes the consumer's job. Scripts calling drive files get, drive files export, drive files create --upload, or calendar events delete must track gws's cwd, watch for output files appearing there, and move or clean them up.
  2. Collision risk. Shared download.txt / download.html names mean two simultaneous operations overwrite each other's output.
  3. Inconsistency across 204 endpoints. Consumers cannot rely on a uniform "delete = empty stdout" contract.
  4. Not pipe-friendly. files export content cannot be piped directly to another command.
  5. Upload friction for automation. --upload cwd restriction forces a copy-into-cwd step for pipelines where the source file is in a generated or fixed location.

Requested behavior

4a / 4c: Accept absolute paths in --output and --upload. There is no safety justification for restricting these to cwd — the user ran the command and chose the path. If preventing accidental overwrite is the intent, a confirmation prompt on existing-file would be more appropriate than a blanket rejection.

4b: Default files export to streaming content to stdout. Respect --output if provided. Document the behavior in --help.

4d: Route all 204 No Content responses through the same handler regardless of endpoint. calendar events delete should produce the same clean empty stdout that gmail users labels delete and tasks tasklists delete produce — no download.html, no status JSON.


Proposed fix direction

  • Locate the --output / --upload path validator and remove the cwd-relativity check. Resolve to canonical form and accept any writable path.
  • Audit 204 response handling: confirm calendar events delete is hitting the "save binary to file" branch and trace why. The branch likely keys on content-type; events delete may be tripping it because the empty 204 carries a text/html content-type header from the server. Fix the branch condition to also require a non-empty body before saving to file.
  • For files export: change default to stdout streaming; keep --output as the opt-in for file saving.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions