11import { io , type Socket } from 'socket.io-client' ;
2+ import { Readable } from 'node:stream' ;
23import { version } from '../package.json' ;
34import JobsResource , { type JobEventData } from './JobsResource' ;
45import SignedUrlResource from './SignedUrlResource' ;
@@ -9,6 +10,85 @@ import TasksResource, {
910import UsersResource from './UsersResource' ;
1011import WebhooksResource from './WebhooksResource' ;
1112
13+ export type UploadFileSource =
14+ | Blob
15+ | Uint8Array
16+ | Iterable < Uint8Array >
17+ | AsyncIterable < Uint8Array >
18+ | NodeJS . ReadableStream ;
19+
20+ export class UploadFile {
21+ private readonly attributes : Array < [ key : string , value : unknown ] > = [ ] ;
22+ private readonly data : AsyncIterable < Uint8Array > ;
23+ constructor (
24+ data : UploadFileSource ,
25+ private readonly filename ?: string
26+ ) {
27+ this . data = UploadFile . unifySources ( data ) ;
28+ }
29+ add ( key : string , value : unknown ) {
30+ this . attributes . push ( [ key , value ] ) ;
31+ }
32+ async * stream ( ) {
33+ const enc = new TextEncoder ( ) ;
34+ const boundary = `----------${ Array . from ( Array ( 32 ) )
35+ . map ( ( ) => Math . random ( ) . toString ( 36 ) [ 2 ] || 0 )
36+ . join ( '' ) } `;
37+ // Start multipart/form-data protocol
38+ yield enc . encode ( `--${ boundary } \r\n` ) ;
39+ // Send all attributes
40+ const separator = enc . encode ( `\r\n--${ boundary } \r\n` ) ;
41+ let first = true ;
42+ for ( const [ key , value ] of this . attributes ) {
43+ if ( value == null ) continue ;
44+ if ( ! first ) yield separator ;
45+ yield enc . encode (
46+ `content-disposition:form-data;name="${ key } "\r\n\r\n${ value } `
47+ ) ;
48+ first = false ;
49+ }
50+ // Send file
51+ if ( ! first ) yield separator ;
52+ yield enc . encode (
53+ `content-disposition:form-data;name="file";filename=${ this . filename } \r\ncontent-type:application/octet-stream\r\n\r\n`
54+ ) ;
55+ yield * this . data ;
56+ // End multipart/form-data protocol
57+ yield enc . encode ( `\r\n--${ boundary } --\r\n` ) ;
58+ }
59+
60+ static async * unifySources (
61+ data : UploadFileSource
62+ ) : AsyncIterable < Uint8Array > {
63+ if ( data instanceof Uint8Array ) {
64+ yield data ;
65+ return ;
66+ }
67+
68+ if ( data instanceof Blob ) {
69+ yield data . bytes ( ) ;
70+ return ;
71+ }
72+
73+ if ( Symbol . iterator in data ) {
74+ yield * data ;
75+ return ;
76+ }
77+
78+ if ( Symbol . asyncIterator in data ) {
79+ const it = data [ Symbol . asyncIterator ] ( ) ;
80+ for await ( const chunk of data ) {
81+ if ( typeof chunk === 'string' )
82+ throw new Error (
83+ 'bad file data, received string but expected Uint8Array'
84+ ) ;
85+ yield chunk ;
86+ }
87+ return ;
88+ }
89+ }
90+ }
91+
1292export default class CloudConvert {
1393 private socket : Socket | undefined ;
1494 private subscribedChannels : Map < string , boolean > | undefined ;
@@ -38,7 +118,7 @@ export default class CloudConvert {
38118 async call (
39119 method : 'GET' | 'POST' | 'DELETE' ,
40120 route : string ,
41- parameters ?: FormData | object
121+ parameters ?: UploadFile | object
42122 ) {
43123 const baseURL = this . useSandbox
44124 ? 'https://api.sandbox.cloudconvert.com/v2/'
@@ -52,28 +132,27 @@ export default class CloudConvert {
52132 baseURL : string ,
53133 method : 'GET' | 'POST' | 'DELETE' ,
54134 route : string ,
55- parameters ?: FormData | object
135+ parameters ?: UploadFile | object
56136 ) {
57137 const url = new URL ( route , baseURL ) ;
58- let body : RequestInit [ 'body' ] | undefined ;
59- if ( parameters instanceof FormData ) {
60- body = parameters ;
61- } else {
62- if ( method === 'GET' ) {
63- url . search = new URLSearchParams (
64- Object . entries ( parameters ?? { } )
65- ) . toString ( ) ;
66- } else {
67- body = JSON . stringify ( parameters ) ;
68- }
138+ const { contentType, search, body } = prepareParameters (
139+ method ,
140+ parameters
141+ ) ;
142+ if ( search !== undefined ) {
143+ url . search = search ;
69144 }
145+ const headers = {
146+ Authorization : `Bearer ${ this . apiKey } ` ,
147+ 'User-Agent' : `cloudconvert-node/v${ version } (https://github.com/cloudconvert/cloudconvert-node)` ,
148+ ...( contentType ? { 'Content-Type' : contentType } : { } )
149+ } ;
70150 const res = await fetch ( url , {
71151 method,
72- headers : {
73- Authorization : `Bearer ${ this . apiKey } ` ,
74- 'User-Agent' : `cloudconvert-node/v${ version } (https://github.com/cloudconvert/cloudconvert-node)`
75- } ,
76- body
152+ headers,
153+ body,
154+ // @ts -expect-error incorrect types in @types/node@20
155+ duplex : 'half'
77156 } ) ;
78157 if (
79158 ! res . ok ||
@@ -127,3 +206,38 @@ export default class CloudConvert {
127206 this . socket ?. close ( ) ;
128207 }
129208}
209+
210+ function prepareParameters (
211+ method : 'GET' | 'POST' | 'DELETE' ,
212+ data ?: UploadFile | object
213+ ) : {
214+ contentType ?: string ;
215+ body ?: string | ReadableStream < Uint8Array > ;
216+ search ?: string ;
217+ } {
218+ if ( data === undefined ) {
219+ return { } ;
220+ }
221+
222+ if ( method === 'GET' ) {
223+ // abort early if all data needs to go into the search params
224+ const entries = Object . entries ( data ?? { } ) ;
225+ return { search : new URLSearchParams ( entries ) . toString ( ) } ;
226+ }
227+
228+ if ( data instanceof UploadFile ) {
229+ return {
230+ contentType : 'multipart/form-data' ,
231+ body : asyncIterableToReadableStream ( data . stream ( ) )
232+ } ;
233+ }
234+
235+ return { contentType : 'application/json' , body : JSON . stringify ( data ) } ;
236+ }
237+
238+ function asyncIterableToReadableStream (
239+ it : AsyncIterable < Uint8Array >
240+ ) : ReadableStream < Uint8Array > {
241+ const r = Readable . from ( it ) ;
242+ return Readable . toWeb ( r ) as ReadableStream < Uint8Array > ;
243+ }
0 commit comments