@@ -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,87 @@ 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- }
192-
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 ) {
202- outputError (
203- "Multiple paths require --content-type tar or --content-type tgz" ,
204- ) ;
205- return ;
206- }
182+ const isStdin = paths . length === 1 && paths [ 0 ] === "-" ;
207183
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 ;
184+ if ( isStdin ) {
185+ if ( ! name ) {
186+ outputError ( "--name is required when uploading from stdin" ) ;
187+ }
188+ if ( ! contentType ) {
189+ outputError ( "--content-type is required when uploading from stdin" ) ;
190+ }
214191 }
215192
216193 let fileBuffer : Buffer ;
217194 let detectedContentType : ContentType ;
218195 let fileSize : number ;
219196
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 ;
197+ if ( isStdin ) {
198+ fileBuffer = await readStdinBuffer ( ) ;
226199 fileSize = fileBuffer . length ;
200+ detectedContentType = contentType as ContentType ;
227201 } else {
228- // Single file upload (existing behavior)
229- const filePath = paths [ 0 ] ;
230- fileBuffer = await readFile ( filePath ) ;
231- fileSize = fileBuffer . length ;
202+ // Validate all paths exist (use lstat to match collectEntries and detect symlinks)
203+ // Key by resolved absolute path so collectEntries can reuse stats
204+ const statsMap = new Map < string , Awaited < ReturnType < typeof lstat > > > ( ) ;
205+ for ( const p of paths ) {
206+ try {
207+ const s = await lstat ( p ) ;
208+ if ( s . isSymbolicLink ( ) ) {
209+ outputError (
210+ `Path is a symlink: ${ p } . Resolve the symlink or pass the target path directly.` ,
211+ ) ;
212+ return ;
213+ }
214+ statsMap . set ( resolve ( p ) , s ) ;
215+ } catch {
216+ outputError ( `Path does not exist: ${ p } ` ) ;
217+ return ;
218+ }
219+ }
232220
233- detectedContentType = contentType as ContentType ;
234- if ( ! detectedContentType ) {
235- const ext = extname ( filePath ) . toLowerCase ( ) ;
236- detectedContentType = CONTENT_TYPE_MAP [ ext ] || "unspecified" ;
221+ const isTarType = contentType === "tar" || contentType === "tgz" ;
222+ const isSinglePath = paths . length === 1 ;
223+ const firstStats = isSinglePath
224+ ? statsMap . get ( resolve ( paths [ 0 ] ) ) !
225+ : undefined ;
226+ const singleIsDir = isSinglePath && firstStats ! . isDirectory ( ) ;
227+
228+ // Multi-path requires tar/tgz content type
229+ if ( paths . length > 1 && ! isTarType ) {
230+ outputError (
231+ "Multiple paths require --content-type tar or --content-type tgz" ,
232+ ) ;
233+ return ;
234+ }
235+
236+ // Directory without tar/tgz type
237+ if ( singleIsDir && ! isTarType ) {
238+ outputError (
239+ "Cannot upload a directory directly. Use --content-type tar or --content-type tgz to create an archive." ,
240+ ) ;
241+ return ;
242+ }
243+
244+ const shouldCreateArchive =
245+ isTarType && ( paths . length > 1 || singleIsDir ) ;
246+
247+ if ( shouldCreateArchive ) {
248+ const gzip = contentType === "tgz" ;
249+ fileBuffer = await createTarBuffer ( paths , gzip , statsMap ) ;
250+ detectedContentType = contentType as ContentType ;
251+ fileSize = fileBuffer . length ;
252+ } else {
253+ // Single file upload (existing behavior)
254+ const filePath = paths [ 0 ] ;
255+ fileBuffer = await readFile ( filePath ) ;
256+ fileSize = fileBuffer . length ;
257+
258+ detectedContentType = contentType as ContentType ;
259+ if ( ! detectedContentType ) {
260+ const ext = extname ( filePath ) . toLowerCase ( ) ;
261+ detectedContentType = CONTENT_TYPE_MAP [ ext ] || "unspecified" ;
262+ }
237263 }
238264 }
239265
0 commit comments