@@ -3,6 +3,11 @@ import CoreManager from './CoreManager';
33import type { FullOptions } from './RESTController' ;
44import ParseError from './ParseError' ;
55
6+ let NodeReadable : any ;
7+ if ( process . env . PARSE_BUILD === 'node' ) {
8+ NodeReadable = require ( 'stream' ) . Readable ;
9+ }
10+
611interface Base64 {
712 base64 : string ;
813}
@@ -29,6 +34,16 @@ export type FileSource =
2934 format : 'uri' ;
3035 uri : string ;
3136 type : string | undefined ;
37+ }
38+ | {
39+ format : 'buffer' ;
40+ buffer : any ;
41+ type : string | undefined ;
42+ }
43+ | {
44+ format : 'stream' ;
45+ stream : any ;
46+ type : string | undefined ;
3247 } ;
3348
3449export function b64Digit ( number : number ) : string {
@@ -75,8 +90,13 @@ class ParseFile {
7590 * 1. an Array of byte value Numbers or Uint8Array.
7691 * 2. an Object like { base64: "..." } with a base64-encoded String.
7792 * 3. an Object like { uri: "..." } with a uri String.
78- * 4. a File object selected with a file upload control. (3) only works
79- * in Firefox 3.6+, Safari 6.0.2+, Chrome 7+, and IE 10+.
93+ * 4. a File object selected with a file upload control.
94+ * 5. (Node.js only) a Buffer. Uploaded as raw binary data instead of
95+ * base64-encoding, reducing memory usage. Falls back to base64
96+ * JSON encoding if metadata or tags are set.
97+ * 6. (Node.js only) a Readable stream, or a Web ReadableStream.
98+ * Streamed as raw binary data directly into the upload request.
99+ * Throws if metadata or tags are set.
80100 * For example:
81101 * <pre>
82102 * var fileUploadControl = $("#profilePhotoFileUpload")[0];
@@ -104,7 +124,30 @@ class ParseFile {
104124 this . _tags = tags || { } ;
105125
106126 if ( data !== undefined ) {
107- if ( Array . isArray ( data ) || data instanceof Uint8Array ) {
127+ if ( typeof Buffer !== 'undefined' && Buffer . isBuffer ( data ) ) {
128+ this . _source = {
129+ format : 'buffer' ,
130+ buffer : data ,
131+ type : specifiedType ,
132+ } ;
133+ } else if (
134+ data !== null &&
135+ typeof data === 'object' &&
136+ typeof ( data as any ) . pipe === 'function' &&
137+ typeof ( data as any ) . read === 'function'
138+ ) {
139+ this . _source = {
140+ format : 'stream' ,
141+ stream : data ,
142+ type : specifiedType ,
143+ } ;
144+ } else if ( typeof ReadableStream !== 'undefined' && data instanceof ReadableStream ) {
145+ this . _source = {
146+ format : 'stream' ,
147+ stream : data ,
148+ type : specifiedType ,
149+ } ;
150+ } else if ( Array . isArray ( data ) || data instanceof Uint8Array ) {
108151 this . _data = ParseFile . encodeBase64 ( data ) ;
109152 this . _source = {
110153 format : 'base64' ,
@@ -165,6 +208,10 @@ class ParseFile {
165208 if ( this . _data ) {
166209 return this . _data ;
167210 }
211+ if ( this . _source ?. format === 'buffer' ) {
212+ this . _data = this . _source . buffer . toString ( 'base64' ) ;
213+ return this . _data ;
214+ }
168215 if ( ! this . _url ) {
169216 throw new Error ( 'Cannot retrieve data for unsaved ParseFile.' ) ;
170217 }
@@ -227,6 +274,12 @@ class ParseFile {
227274 /**
228275 * Saves the file to the Parse cloud.
229276 *
277+ * In Node.js, files created with Buffer or ReadableStream are uploaded as
278+ * raw binary data, avoiding base64 encoding overhead. If metadata
279+ * or tags are set on a Buffer-backed file, the upload falls back to base64
280+ * JSON encoding (since the binary endpoint does not support metadata).
281+ * Stream-backed files with metadata or tags will throw an error.
282+ *
230283 * @param {object } options
231284 * Valid options are:<ul>
232285 * <li>useMasterKey: In Cloud Code and Node only, causes the Master Key to
@@ -255,7 +308,50 @@ class ParseFile {
255308
256309 const controller = CoreManager . getFileController ( ) ;
257310 if ( ! this . _previousSave ) {
258- if ( this . _source . format === 'file' ) {
311+ if ( this . _source . format === 'buffer' || this . _source . format === 'stream' ) {
312+ const hasMetadataOrTags =
313+ ( this . _metadata && Object . keys ( this . _metadata ) . length > 0 ) ||
314+ ( this . _tags && Object . keys ( this . _tags ) . length > 0 ) ;
315+
316+ if ( this . _source . format === 'stream' && hasMetadataOrTags ) {
317+ throw new Error (
318+ 'Cannot save a stream-based file with metadata or tags. Use a Buffer instead.'
319+ ) ;
320+ }
321+ if ( this . _source . format === 'stream' && ! controller . saveBinary ) {
322+ throw new Error (
323+ 'Cannot save a stream-based file without saveBinary support on the FileController.'
324+ ) ;
325+ }
326+
327+ if ( ! hasMetadataOrTags && controller . saveBinary ) {
328+ // Binary upload via ajax
329+ this . _previousSave = controller
330+ . saveBinary ( this . _name , this . _source , options )
331+ . then ( res => {
332+ this . _name = res . name ;
333+ this . _url = res . url ;
334+ this . _data = null ;
335+ this . _requestTask = null ;
336+ return this ;
337+ } ) ;
338+ } else if ( this . _source . format === 'buffer' ) {
339+ // Buffer: fall back to base64 JSON encoding (metadata/tags or no saveBinary)
340+ const base64Source = {
341+ format : 'base64' as const ,
342+ base64 : this . _source . buffer . toString ( 'base64' ) ,
343+ type : this . _source . type ,
344+ } ;
345+ this . _previousSave = controller
346+ . saveBase64 ( this . _name , base64Source , options )
347+ . then ( res => {
348+ this . _name = res . name ;
349+ this . _url = res . url ;
350+ this . _requestTask = null ;
351+ return this ;
352+ } ) ;
353+ }
354+ } else if ( this . _source . format === 'file' ) {
259355 this . _previousSave = controller . saveFile ( this . _name , this . _source , options ) . then ( res => {
260356 this . _name = res . name ;
261357 this . _url = res . url ;
@@ -485,6 +581,74 @@ const DefaultController = {
485581 return CoreManager . getRESTController ( ) . request ( 'POST' , path , data , options ) ;
486582 } ,
487583
584+ saveBinary : async function (
585+ name : string ,
586+ source : FileSource ,
587+ options : FileSaveOptions = { }
588+ ) {
589+ if ( source . format !== 'buffer' && source . format !== 'stream' ) {
590+ throw new Error ( 'saveBinary can only be used with Buffer or Stream sources.' ) ;
591+ }
592+
593+ const headers : Record < string , string > = {
594+ 'X-Parse-Application-ID' : CoreManager . get ( 'APPLICATION_ID' ) ,
595+ } ;
596+ headers [ 'Content-Type' ] = ( source . type || 'application/octet-stream' ) . replace ( / [ \r \n ] / g, '' ) ;
597+ const jsKey = CoreManager . get ( 'JAVASCRIPT_KEY' ) ;
598+ if ( jsKey ) {
599+ headers [ 'X-Parse-JavaScript-Key' ] = jsKey ;
600+ }
601+ let useMasterKey = options . useMasterKey ;
602+ if ( typeof useMasterKey === 'undefined' ) {
603+ useMasterKey = CoreManager . get ( 'USE_MASTER_KEY' ) ;
604+ }
605+ if ( useMasterKey ) {
606+ if ( CoreManager . get ( 'MASTER_KEY' ) ) {
607+ delete headers [ 'X-Parse-JavaScript-Key' ] ;
608+ headers [ 'X-Parse-Master-Key' ] = CoreManager . get ( 'MASTER_KEY' ) ;
609+ } else {
610+ throw new Error ( 'Cannot use the Master Key, it has not been provided.' ) ;
611+ }
612+ }
613+
614+ if ( options . sessionToken ) {
615+ headers [ 'X-Parse-Session-Token' ] = options . sessionToken ;
616+ } else {
617+ const userController = CoreManager . getUserController ( ) ;
618+ if ( userController ) {
619+ const user = await userController . currentUserAsync ( ) ;
620+ if ( user ) {
621+ const token = user . getSessionToken ( ) ;
622+ if ( token ) {
623+ headers [ 'X-Parse-Session-Token' ] = token ;
624+ }
625+ }
626+ }
627+ }
628+
629+ let body : any ;
630+ if ( source . format === 'buffer' ) {
631+ body = source . buffer ;
632+ } else if ( source . format === 'stream' ) {
633+ const stream = source . stream ;
634+ if ( typeof stream . pipe === 'function' && typeof stream . read === 'function' ) {
635+ body = NodeReadable . toWeb ( stream ) ;
636+ } else {
637+ body = stream ;
638+ }
639+ }
640+
641+ let url = CoreManager . get ( 'SERVER_URL' ) ;
642+ if ( url [ url . length - 1 ] !== '/' ) {
643+ url += '/' ;
644+ }
645+ url += 'files/' + encodeURIComponent ( name ) ;
646+
647+ return CoreManager . getRESTController ( )
648+ . ajax ( 'POST' , url , body , headers , options )
649+ . then ( ( { response } ) => response ) ;
650+ } ,
651+
488652 download : async function ( uri , options ) {
489653 const controller = new AbortController ( ) ;
490654 options . requestTask ( controller ) ;
0 commit comments