@@ -8,6 +8,7 @@ import { createTar, createTarGzip } from "nanotar";
88import type { TarFileInput } from "nanotar" ;
99import { getClient } from "../../utils/client.js" ;
1010import { output , outputError } from "../../utils/output.js" ;
11+ import { processUtils } from "../../utils/processUtils.js" ;
1112
1213interface UploadObjectOptions {
1314 paths : string [ ] ;
@@ -161,79 +162,145 @@ export async function createTarBuffer(
161162 return Buffer . from ( data ) ;
162163}
163164
165+ async function readStdinBuffer ( ) : Promise < Buffer > {
166+ const chunks : Buffer [ ] = [ ] ;
167+ for await ( const chunk of processUtils . stdin ) {
168+ chunks . push ( Buffer . isBuffer ( chunk ) ? chunk : Buffer . from ( chunk ) ) ;
169+ }
170+ return Buffer . concat ( chunks ) ;
171+ }
172+
164173export async function uploadObject ( options : UploadObjectOptions ) {
165174 try {
166175 const client = getClient ( ) ;
167176 const { paths, name, contentType, output : outputFormat } = options ;
168177
169178 if ( paths . length === 0 ) {
170- outputError ( "At least one path is required" ) ;
171- return ;
172- }
179+ if ( ! processUtils . stdin . isTTY ) {
180+ // Piped stdin detected — normalize to explicit stdin path below
181+ paths . push ( "-" ) ;
182+ } else {
183+ // Interactive terminal: print pre-signed upload URL
184+ if ( ! name ) {
185+ outputError ( "--name is required when no paths are provided" ) ;
186+ }
187+ const resolvedContentType : ContentType =
188+ ( contentType as ContentType ) || "unspecified" ;
173189
174- // Validate all paths exist (use lstat to match collectEntries and detect symlinks)
175- // Key by resolved absolute path so collectEntries can reuse stats
176- const statsMap = new Map < string , Awaited < ReturnType < typeof lstat > > > ( ) ;
177- for ( const p of paths ) {
178- try {
179- const s = await lstat ( p ) ;
180- if ( s . isSymbolicLink ( ) ) {
181- outputError (
182- `Path is a symlink: ${ p } . Resolve the symlink or pass the target path directly.` ,
183- ) ;
184- return ;
190+ const createResponse = await client . objects . create ( {
191+ name,
192+ content_type : resolvedContentType ,
193+ } ) ;
194+
195+ if ( ! createResponse . upload_url ) {
196+ outputError ( "API did not return an upload URL" ) ;
197+ }
198+
199+ const result = {
200+ id : createResponse . id ,
201+ name,
202+ contentType : resolvedContentType ,
203+ uploadUrl : createResponse . upload_url ,
204+ } ;
205+
206+ if ( ! outputFormat || outputFormat === "text" ) {
207+ console . log ( createResponse . upload_url ) ;
208+ } else {
209+ output ( result , { format : outputFormat , defaultFormat : "json" } ) ;
185210 }
186- statsMap . set ( resolve ( p ) , s ) ;
187- } catch {
188- outputError ( `Path does not exist: ${ p } ` ) ;
189211 return ;
190212 }
191213 }
192214
193- const isTarType = contentType === "tar" || contentType === "tgz" ;
194- const isSinglePath = paths . length === 1 ;
195- const firstStats = isSinglePath
196- ? statsMap . get ( resolve ( paths [ 0 ] ) ) !
197- : undefined ;
198- const singleIsDir = isSinglePath && firstStats ! . isDirectory ( ) ;
215+ const hasStdin = paths . includes ( "-" ) ;
216+ const isStdin = paths . length === 1 && hasStdin ;
199217
200- // Multi-path requires tar/tgz content type
201- if ( paths . length > 1 && ! isTarType ) {
218+ // stdin cannot be mixed with other paths (e.g. `upload - file1.txt`)
219+ if ( hasStdin && ! isStdin ) {
202220 outputError (
203- "Multiple paths require --content-type tar or --content-type tgz " ,
221+ "Cannot mix stdin (-) with other paths. Use - alone or provide only file/directory paths. " ,
204222 ) ;
205- return ;
206223 }
207224
208- // Directory without tar/tgz type
209- if ( singleIsDir && ! isTarType ) {
210- outputError (
211- "Cannot upload a directory directly. Use --content-type tar or --content-type tgz to create an archive." ,
212- ) ;
213- return ;
225+ if ( isStdin ) {
226+ if ( ! name ) {
227+ outputError ( "--name is required when uploading from stdin" ) ;
228+ }
229+ if ( ! contentType ) {
230+ outputError ( "--content-type is required when uploading from stdin" ) ;
231+ }
214232 }
215233
216234 let fileBuffer : Buffer ;
217235 let detectedContentType : ContentType ;
218236 let fileSize : number ;
219237
220- const shouldCreateArchive = isTarType && ( paths . length > 1 || singleIsDir ) ;
221-
222- if ( shouldCreateArchive ) {
223- const gzip = contentType === "tgz" ;
224- fileBuffer = await createTarBuffer ( paths , gzip , statsMap ) ;
225- detectedContentType = contentType as ContentType ;
238+ if ( isStdin ) {
239+ fileBuffer = await readStdinBuffer ( ) ;
226240 fileSize = fileBuffer . length ;
241+ detectedContentType = contentType as ContentType ;
227242 } else {
228- // Single file upload (existing behavior)
229- const filePath = paths [ 0 ] ;
230- fileBuffer = await readFile ( filePath ) ;
231- fileSize = fileBuffer . length ;
243+ // Validate all paths exist (use lstat to match collectEntries and detect symlinks)
244+ // Key by resolved absolute path so collectEntries can reuse stats
245+ const statsMap = new Map < string , Awaited < ReturnType < typeof lstat > > > ( ) ;
246+ for ( const p of paths ) {
247+ try {
248+ const s = await lstat ( p ) ;
249+ if ( s . isSymbolicLink ( ) ) {
250+ outputError (
251+ `Path is a symlink: ${ p } . Resolve the symlink or pass the target path directly.` ,
252+ ) ;
253+ return ;
254+ }
255+ statsMap . set ( resolve ( p ) , s ) ;
256+ } catch {
257+ outputError ( `Path does not exist: ${ p } ` ) ;
258+ return ;
259+ }
260+ }
232261
233- detectedContentType = contentType as ContentType ;
234- if ( ! detectedContentType ) {
235- const ext = extname ( filePath ) . toLowerCase ( ) ;
236- detectedContentType = CONTENT_TYPE_MAP [ ext ] || "unspecified" ;
262+ const isTarType = contentType === "tar" || contentType === "tgz" ;
263+ const isSinglePath = paths . length === 1 ;
264+ const firstStats = isSinglePath
265+ ? statsMap . get ( resolve ( paths [ 0 ] ) ) !
266+ : undefined ;
267+ const singleIsDir = isSinglePath && firstStats ! . isDirectory ( ) ;
268+
269+ // Multi-path requires tar/tgz content type
270+ if ( paths . length > 1 && ! isTarType ) {
271+ outputError (
272+ "Multiple paths require --content-type tar or --content-type tgz" ,
273+ ) ;
274+ return ;
275+ }
276+
277+ // Directory without tar/tgz type
278+ if ( singleIsDir && ! isTarType ) {
279+ outputError (
280+ "Cannot upload a directory directly. Use --content-type tar or --content-type tgz to create an archive." ,
281+ ) ;
282+ return ;
283+ }
284+
285+ const shouldCreateArchive =
286+ isTarType && ( paths . length > 1 || singleIsDir ) ;
287+
288+ if ( shouldCreateArchive ) {
289+ const gzip = contentType === "tgz" ;
290+ fileBuffer = await createTarBuffer ( paths , gzip , statsMap ) ;
291+ detectedContentType = contentType as ContentType ;
292+ fileSize = fileBuffer . length ;
293+ } else {
294+ // Single file upload (existing behavior)
295+ const filePath = paths [ 0 ] ;
296+ fileBuffer = await readFile ( filePath ) ;
297+ fileSize = fileBuffer . length ;
298+
299+ detectedContentType = contentType as ContentType ;
300+ if ( ! detectedContentType ) {
301+ const ext = extname ( filePath ) . toLowerCase ( ) ;
302+ detectedContentType = CONTENT_TYPE_MAP [ ext ] || "unspecified" ;
303+ }
237304 }
238305 }
239306
0 commit comments