@@ -290,22 +290,23 @@ export default {
290290 ...args ,
291291 } ) ;
292292 } ,
293+ /**
294+ * Upload a single file (local path or http(s) URL) to Zendesk Uploads API.
295+ * @param {Object } params
296+ * @param {string } params.filePath - Local filesystem path or http(s) URL.
297+ * @param {string } [params.filename] - Optional filename override for the upload.
298+ * @param {string } [params.customSubdomain]
299+ * @param {* } [params.step]
300+ */
293301 async uploadFile ( {
294302 filePath, filename, customSubdomain, step,
295303 } = { } ) {
304+ if ( ! filePath || typeof filePath !== "string" ) {
305+ throw new Error ( "uploadFile: 'filePath' (string) is required" ) ;
306+ }
296307 const fs = await import ( "fs" ) ;
297308 const path = await import ( "path" ) ;
298-
299- // If filename not provided, extract from filePath
300- if ( ! filename && filePath ) {
301- filename = path . basename ( filePath ) ;
302- }
303-
304- // Read file content
305- const fileContent = fs . readFileSync ( filePath ) ;
306-
307- // Get file extension to determine Content-Type
308- const ext = path . extname ( filename ) . toLowerCase ( ) ;
309+
309310 const contentTypeMap = {
310311 ".pdf" : "application/pdf" ,
311312 ".png" : "image/png" ,
@@ -319,8 +320,49 @@ export default {
319320 ".xlsx" : "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ,
320321 ".zip" : "application/zip" ,
321322 } ;
322- const contentType = contentTypeMap [ ext ] || "application/octet-stream" ;
323-
323+
324+ let fileContent ;
325+ let contentType ;
326+
327+ const isHttp = / ^ h t t p s ? : \/ \/ / i. test ( filePath ) ;
328+ if ( isHttp ) {
329+ // Fetch remote file as arraybuffer to preserve bytes
330+ const res = await axios ( step , {
331+ method : "get" ,
332+ url : filePath ,
333+ responseType : "arraybuffer" ,
334+ returnFullResponse : true ,
335+ timeout : 60_000 ,
336+ } ) ;
337+ fileContent = res . data ;
338+
339+ const headerCT = res . headers ?. [ "content-type" ] ;
340+ const cd = res . headers ?. [ "content-disposition" ] ;
341+
342+ if ( ! filename ) {
343+ const cdMatch = cd ?. match ( / f i l e n a m e \* ? = (?: U T F - 8 ' ' | " ) ? ( [ ^ \" ; ] + ) / i) ;
344+ filename = cdMatch ?. [ 1 ]
345+ ? decodeURIComponent ( cdMatch [ 1 ] . replace ( / ( ^ " | " $ ) / g, "" ) )
346+ : ( ( ) => {
347+ try {
348+ return path . basename ( new URL ( filePath ) . pathname ) ;
349+ } catch {
350+ return "attachment" ;
351+ }
352+ } ) ( ) ;
353+ }
354+ const ext = path . extname ( filename || "" ) . toLowerCase ( ) ;
355+ contentType = headerCT || contentTypeMap [ ext ] || "application/octet-stream" ;
356+ } else {
357+ // Local file: non-blocking read
358+ if ( ! filename ) {
359+ filename = path . basename ( filePath ) ;
360+ }
361+ fileContent = await fs . promises . readFile ( filePath ) ;
362+ const ext = path . extname ( filename || "" ) . toLowerCase ( ) ;
363+ contentType = contentTypeMap [ ext ] || "application/octet-stream" ;
364+ }
365+
324366 return this . makeRequest ( {
325367 step,
326368 method : "post" ,
@@ -338,25 +380,37 @@ export default {
338380 if ( ! attachments || ! attachments . length ) {
339381 return [ ] ;
340382 }
341-
342- const uploadResults = [ ] ;
343- for ( const attachment of attachments ) {
344- try {
345- const result = await this . uploadFile ( {
346- filePath : attachment ,
347- customSubdomain,
348- step,
349- } ) ;
350- const token = result ?. upload ?. token ;
383+ const files = attachments
384+ . map ( ( a ) => ( typeof a === "string" ? a . trim ( ) : a ) )
385+ . filter ( Boolean ) ;
386+
387+ const settled = await Promise . allSettled (
388+ files . map ( ( attachment ) =>
389+ this . uploadFile ( { filePath : attachment , customSubdomain, step } ) ,
390+ ) ,
391+ ) ;
392+
393+ const tokens = [ ] ;
394+ const errors = [ ] ;
395+ settled . forEach ( ( res , i ) => {
396+ const attachment = files [ i ] ;
397+ if ( res . status === "fulfilled" ) {
398+ const token = res . value ?. upload ?. token ;
351399 if ( ! token ) {
352- throw new Error ( `Upload API returned no token for ${ attachment } ` ) ;
400+ errors . push ( `Upload API returned no token for ${ attachment } ` ) ;
401+ } else {
402+ tokens . push ( token ) ;
353403 }
354- uploadResults . push ( token ) ;
355- } catch ( error ) {
356- throw error ;
404+ } else {
405+ const reason = res . reason ?. message || String ( res . reason || "Unknown error" ) ;
406+ errors . push ( ` ${ attachment } : ${ reason } ` ) ;
357407 }
408+ } ) ;
409+
410+ if ( errors . length ) {
411+ throw new Error ( `Failed to upload ${ errors . length } /${ files . length } attachment(s): ${ errors . join ( "; " ) } ` ) ;
358412 }
359- return uploadResults ;
413+ return tokens ;
360414 } ,
361415 async * paginate ( {
362416 fn, args, resourceKey, max,
0 commit comments