@@ -161,6 +161,14 @@ export async function createTarBuffer(
161161 return Buffer . from ( data ) ;
162162}
163163
164+ async function readStdinBuffer ( ) : Promise < Buffer > {
165+ const chunks : Buffer [ ] = [ ] ;
166+ for await ( const chunk of process . stdin ) {
167+ chunks . push ( Buffer . isBuffer ( chunk ) ? chunk : Buffer . from ( chunk ) ) ;
168+ }
169+ return Buffer . concat ( chunks ) ;
170+ }
171+
164172export async function uploadObject ( options : UploadObjectOptions ) {
165173 try {
166174 const client = getClient ( ) ;
@@ -171,69 +179,95 @@ export async function uploadObject(options: UploadObjectOptions) {
171179 return ;
172180 }
173181
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 ;
185- }
186- statsMap . set ( resolve ( p ) , s ) ;
187- } catch {
188- outputError ( `Path does not exist: ${ p } ` ) ;
189- return ;
190- }
191- }
182+ const hasStdin = paths . includes ( "-" ) ;
183+ const isStdin = paths . length === 1 && hasStdin ;
192184
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 ( ) ;
199-
200- // Multi-path requires tar/tgz content type
201- if ( paths . length > 1 && ! isTarType ) {
185+ // stdin cannot be mixed with other paths (e.g. `upload - file1.txt`)
186+ if ( hasStdin && ! isStdin ) {
202187 outputError (
203- "Multiple paths require --content-type tar or --content-type tgz " ,
188+ "Cannot mix stdin (-) with other paths. Use - alone or provide only file/directory paths. " ,
204189 ) ;
205- return ;
206190 }
207191
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 ;
192+ if ( isStdin ) {
193+ if ( ! name ) {
194+ outputError ( "--name is required when uploading from stdin" ) ;
195+ }
196+ if ( ! contentType ) {
197+ outputError ( "--content-type is required when uploading from stdin" ) ;
198+ }
214199 }
215200
216201 let fileBuffer : Buffer ;
217202 let detectedContentType : ContentType ;
218203 let fileSize : number ;
219204
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 ;
205+ if ( isStdin ) {
206+ fileBuffer = await readStdinBuffer ( ) ;
226207 fileSize = fileBuffer . length ;
208+ detectedContentType = contentType as ContentType ;
227209 } else {
228- // Single file upload (existing behavior)
229- const filePath = paths [ 0 ] ;
230- fileBuffer = await readFile ( filePath ) ;
231- fileSize = fileBuffer . length ;
210+ // Validate all paths exist (use lstat to match collectEntries and detect symlinks)
211+ // Key by resolved absolute path so collectEntries can reuse stats
212+ const statsMap = new Map < string , Awaited < ReturnType < typeof lstat > > > ( ) ;
213+ for ( const p of paths ) {
214+ try {
215+ const s = await lstat ( p ) ;
216+ if ( s . isSymbolicLink ( ) ) {
217+ outputError (
218+ `Path is a symlink: ${ p } . Resolve the symlink or pass the target path directly.` ,
219+ ) ;
220+ return ;
221+ }
222+ statsMap . set ( resolve ( p ) , s ) ;
223+ } catch {
224+ outputError ( `Path does not exist: ${ p } ` ) ;
225+ return ;
226+ }
227+ }
232228
233- detectedContentType = contentType as ContentType ;
234- if ( ! detectedContentType ) {
235- const ext = extname ( filePath ) . toLowerCase ( ) ;
236- detectedContentType = CONTENT_TYPE_MAP [ ext ] || "unspecified" ;
229+ const isTarType = contentType === "tar" || contentType === "tgz" ;
230+ const isSinglePath = paths . length === 1 ;
231+ const firstStats = isSinglePath
232+ ? statsMap . get ( resolve ( paths [ 0 ] ) ) !
233+ : undefined ;
234+ const singleIsDir = isSinglePath && firstStats ! . isDirectory ( ) ;
235+
236+ // Multi-path requires tar/tgz content type
237+ if ( paths . length > 1 && ! isTarType ) {
238+ outputError (
239+ "Multiple paths require --content-type tar or --content-type tgz" ,
240+ ) ;
241+ return ;
242+ }
243+
244+ // Directory without tar/tgz type
245+ if ( singleIsDir && ! isTarType ) {
246+ outputError (
247+ "Cannot upload a directory directly. Use --content-type tar or --content-type tgz to create an archive." ,
248+ ) ;
249+ return ;
250+ }
251+
252+ const shouldCreateArchive =
253+ isTarType && ( paths . length > 1 || singleIsDir ) ;
254+
255+ if ( shouldCreateArchive ) {
256+ const gzip = contentType === "tgz" ;
257+ fileBuffer = await createTarBuffer ( paths , gzip , statsMap ) ;
258+ detectedContentType = contentType as ContentType ;
259+ fileSize = fileBuffer . length ;
260+ } else {
261+ // Single file upload (existing behavior)
262+ const filePath = paths [ 0 ] ;
263+ fileBuffer = await readFile ( filePath ) ;
264+ fileSize = fileBuffer . length ;
265+
266+ detectedContentType = contentType as ContentType ;
267+ if ( ! detectedContentType ) {
268+ const ext = extname ( filePath ) . toLowerCase ( ) ;
269+ detectedContentType = CONTENT_TYPE_MAP [ ext ] || "unspecified" ;
270+ }
237271 }
238272 }
239273
0 commit comments