55import { writeFile } from "fs/promises" ;
66import { getClient } from "../../utils/client.js" ;
77import { output , outputError } from "../../utils/output.js" ;
8+ import { getDefaultDownloadPath } from "../../utils/downloadPath.js" ;
9+ import { processUtils } from "../../utils/processUtils.js" ;
810
911interface DownloadObjectOptions {
1012 id : string ;
11- path : string ;
13+ path ? : string ;
1214 extract ?: boolean ;
1315 durationSeconds ?: number ;
1416 output ?: string ;
1517}
1618
19+ /** Content types that produce non-text (binary) output */
20+ const BINARY_CONTENT_TYPES = new Set ( [
21+ "binary" ,
22+ "gzip" ,
23+ "tar" ,
24+ "tgz" ,
25+ "unspecified" ,
26+ ] ) ;
27+
1728export async function downloadObject ( options : DownloadObjectOptions ) {
1829 try {
1930 const client = getClient ( ) ;
31+ const isStdout = options . path === "-" ;
32+
33+ // Resolve the download path when not provided or when writing to stdout
34+ // (stdout mode still needs content_type for the TTY binary warning)
35+ let resolvedPath = options . path ;
36+ let contentType : string | undefined ;
37+ if ( ! resolvedPath || isStdout ) {
38+ const obj = await client . objects . retrieve ( options . id ) ;
39+ contentType = obj . content_type ;
40+ if ( ! resolvedPath ) {
41+ resolvedPath = getDefaultDownloadPath (
42+ obj . name ,
43+ obj . id ,
44+ obj . content_type ,
45+ ) ;
46+ }
47+ }
2048
2149 // Get the download URL
2250 const downloadUrlResponse = await client . objects . download ( options . id , {
@@ -32,19 +60,40 @@ export async function downloadObject(options: DownloadObjectOptions) {
3260 // Save the file
3361 const arrayBuffer = await response . arrayBuffer ( ) ;
3462 const buffer = Buffer . from ( arrayBuffer ) ;
35- await writeFile ( options . path , buffer ) ;
63+
64+ if ( isStdout ) {
65+ // Warn if writing binary data to a terminal
66+ if (
67+ processUtils . stdout . isTTY &&
68+ contentType &&
69+ BINARY_CONTENT_TYPES . has ( contentType )
70+ ) {
71+ processUtils . stderr . write (
72+ "Warning: writing binary data to terminal; pipe to a file or command instead\n" ,
73+ ) ;
74+ }
75+ // Raw process.stdout for binary data (processUtils.stdout.write only accepts string)
76+ process . stdout . write ( buffer ) ;
77+ } else {
78+ await writeFile ( resolvedPath , buffer ) ;
79+ }
3680
3781 // TODO: Handle extraction if requested (options.extract)
3882
3983 const result = {
4084 id : options . id ,
41- path : options . path ,
85+ path : isStdout ? "-" : resolvedPath ,
4286 extracted : options . extract || false ,
4387 } ;
4488
45- // Default: just output the local path for easy scripting
46- if ( ! options . output || options . output === "text" ) {
47- console . log ( options . path ) ;
89+ if ( isStdout ) {
90+ // Structured output goes to stderr to avoid mixing with data (always JSON)
91+ if ( options . output && options . output !== "text" ) {
92+ processUtils . stderr . write ( JSON . stringify ( result , null , 2 ) + "\n" ) ;
93+ }
94+ } else if ( ! options . output || options . output === "text" ) {
95+ // Default: just output the local path for easy scripting
96+ console . log ( resolvedPath ) ;
4897 } else {
4998 output ( result , { format : options . output , defaultFormat : "json" } ) ;
5099 }
0 commit comments