@@ -1878,4 +1878,265 @@ describe('Parse.File testing', () => {
18781878 ) . toBeRejectedWith ( jasmine . objectContaining ( { status : 400 } ) ) ;
18791879 } ) ;
18801880 } ) ;
1881+
1882+ describe ( 'streaming binary uploads' , ( ) => {
1883+ describe ( 'createSizeLimitedStream' , ( ) => {
1884+ const { createSizeLimitedStream } = require ( '../lib/Routers/FilesRouter' ) ;
1885+ const { Readable } = require ( 'stream' ) ;
1886+
1887+ it ( 'passes data through when under limit' , async ( ) => {
1888+ const input = Readable . from ( Buffer . from ( 'hello' ) ) ;
1889+ const limited = createSizeLimitedStream ( input , 100 ) ;
1890+ const chunks = [ ] ;
1891+ for await ( const chunk of limited ) {
1892+ chunks . push ( chunk ) ;
1893+ }
1894+ expect ( Buffer . concat ( chunks ) . toString ( ) ) . toBe ( 'hello' ) ;
1895+ } ) ;
1896+
1897+ it ( 'destroys stream when data exceeds limit' , async ( ) => {
1898+ const input = Readable . from ( Buffer . from ( 'hello world, this is too long' ) ) ;
1899+ const limited = createSizeLimitedStream ( input , 5 ) ;
1900+ const chunks = [ ] ;
1901+ try {
1902+ for await ( const chunk of limited ) {
1903+ chunks . push ( chunk ) ;
1904+ }
1905+ fail ( 'should have thrown' ) ;
1906+ } catch ( e ) {
1907+ expect ( e . message ) . toContain ( 'exceeds' ) ;
1908+ }
1909+ } ) ;
1910+
1911+ it ( 'passes through when Content-Length is within limit' , async ( ) => {
1912+ const input = Readable . from ( Buffer . from ( 'hi' ) ) ;
1913+ input . headers = { 'content-length' : '2' } ;
1914+ const limited = createSizeLimitedStream ( input , 100 ) ;
1915+ const chunks = [ ] ;
1916+ for await ( const chunk of limited ) {
1917+ chunks . push ( chunk ) ;
1918+ }
1919+ expect ( Buffer . concat ( chunks ) . toString ( ) ) . toBe ( 'hi' ) ;
1920+ } ) ;
1921+ } ) ;
1922+
1923+ it ( 'streams binary upload with X-Parse-Upload-Mode header' , async ( ) => {
1924+ const headers = {
1925+ 'Content-Type' : 'application/octet-stream' ,
1926+ 'X-Parse-Application-Id' : 'test' ,
1927+ 'X-Parse-REST-API-Key' : 'rest' ,
1928+ 'X-Parse-Upload-Mode' : 'stream' ,
1929+ } ;
1930+ let response ;
1931+ try {
1932+ response = await request ( {
1933+ method : 'POST' ,
1934+ headers : headers ,
1935+ url : 'http://localhost:8378/1/files/stream-test.txt' ,
1936+ body : 'streaming file content' ,
1937+ } ) ;
1938+ } catch ( e ) {
1939+ fail ( 'Request failed: status=' + e . status + ' text=' + e . text + ' data=' + JSON . stringify ( e . data ) ) ;
1940+ return ;
1941+ }
1942+ const b = response . data ;
1943+ expect ( b . name ) . toMatch ( / _ s t r e a m - t e s t .t x t $ / ) ;
1944+ expect ( b . url ) . toMatch ( / s t r e a m - t e s t \. t x t $ / ) ;
1945+ const getResponse = await request ( { url : b . url } ) ;
1946+ expect ( getResponse . text ) . toEqual ( 'streaming file content' ) ;
1947+ } ) ;
1948+
1949+ it ( 'uses buffered path without X-Parse-Upload-Mode header' , async ( ) => {
1950+ const headers = {
1951+ 'Content-Type' : 'application/octet-stream' ,
1952+ 'X-Parse-Application-Id' : 'test' ,
1953+ 'X-Parse-REST-API-Key' : 'rest' ,
1954+ } ;
1955+ const response = await request ( {
1956+ method : 'POST' ,
1957+ headers : headers ,
1958+ url : 'http://localhost:8378/1/files/buffered-test.txt' ,
1959+ body : 'buffered file content' ,
1960+ } ) ;
1961+ const b = response . data ;
1962+ expect ( b . name ) . toMatch ( / _ b u f f e r e d - t e s t .t x t $ / ) ;
1963+ const getResponse = await request ( { url : b . url } ) ;
1964+ expect ( getResponse . text ) . toEqual ( 'buffered file content' ) ;
1965+ } ) ;
1966+
1967+ it ( 'rejects streaming upload exceeding size limit' , async ( ) => {
1968+ await reconfigureServer ( { maxUploadSize : '10b' } ) ;
1969+ const headers = {
1970+ 'Content-Type' : 'application/octet-stream' ,
1971+ 'X-Parse-Application-Id' : 'test' ,
1972+ 'X-Parse-REST-API-Key' : 'rest' ,
1973+ 'X-Parse-Upload-Mode' : 'stream' ,
1974+ } ;
1975+ try {
1976+ await request ( {
1977+ method : 'POST' ,
1978+ headers : headers ,
1979+ url : 'http://localhost:8378/1/files/big-file.txt' ,
1980+ body : 'this content is definitely longer than 10 bytes' ,
1981+ } ) ;
1982+ fail ( 'should have thrown' ) ;
1983+ } catch ( response ) {
1984+ expect ( response . data . code ) . toBe ( Parse . Error . FILE_SAVE_ERROR ) ;
1985+ expect ( response . data . error ) . toContain ( 'exceeds' ) ;
1986+ }
1987+ } ) ;
1988+
1989+ it ( 'rejects streaming upload with Content-Length exceeding limit' , async ( ) => {
1990+ await reconfigureServer ( { maxUploadSize : '10b' } ) ;
1991+ const headers = {
1992+ 'Content-Type' : 'application/octet-stream' ,
1993+ 'X-Parse-Application-Id' : 'test' ,
1994+ 'X-Parse-REST-API-Key' : 'rest' ,
1995+ 'X-Parse-Upload-Mode' : 'stream' ,
1996+ 'Content-Length' : '99999' ,
1997+ } ;
1998+ try {
1999+ await request ( {
2000+ method : 'POST' ,
2001+ headers : headers ,
2002+ url : 'http://localhost:8378/1/files/big-file.txt' ,
2003+ body : 'hi' ,
2004+ } ) ;
2005+ fail ( 'should have thrown' ) ;
2006+ } catch ( response ) {
2007+ expect ( response . data . code ) . toBe ( Parse . Error . FILE_SAVE_ERROR ) ;
2008+ expect ( response . data . error ) . toContain ( 'exceeds' ) ;
2009+ }
2010+ } ) ;
2011+
2012+ it ( 'fires beforeSave trigger with request.stream = true on streaming upload' , async ( ) => {
2013+ let receivedStream ;
2014+ let receivedData ;
2015+ Parse . Cloud . beforeSave ( Parse . File , ( request ) => {
2016+ receivedStream = request . stream ;
2017+ receivedData = request . file . _data ;
2018+ request . file . addMetadata ( 'source' , 'stream' ) ;
2019+ request . file . addTag ( 'env' , 'test' ) ;
2020+ } ) ;
2021+ const headers = {
2022+ 'Content-Type' : 'application/octet-stream' ,
2023+ 'X-Parse-Application-Id' : 'test' ,
2024+ 'X-Parse-REST-API-Key' : 'rest' ,
2025+ 'X-Parse-Upload-Mode' : 'stream' ,
2026+ } ;
2027+ const response = await request ( {
2028+ method : 'POST' ,
2029+ headers : headers ,
2030+ url : 'http://localhost:8378/1/files/trigger-test.txt' ,
2031+ body : 'trigger test content' ,
2032+ } ) ;
2033+ expect ( response . data . name ) . toMatch ( / _ t r i g g e r - t e s t .t x t $ / ) ;
2034+ expect ( receivedStream ) . toBe ( true ) ;
2035+ expect ( receivedData ) . toBeFalsy ( ) ;
2036+ const getResponse = await request ( { url : response . data . url } ) ;
2037+ expect ( getResponse . text ) . toEqual ( 'trigger test content' ) ;
2038+ } ) ;
2039+
2040+ it ( 'rejects streaming upload when beforeSave trigger throws' , async ( ) => {
2041+ Parse . Cloud . beforeSave ( Parse . File , ( ) => {
2042+ throw new Parse . Error ( Parse . Error . SCRIPT_FAILED , 'Upload rejected' ) ;
2043+ } ) ;
2044+ const headers = {
2045+ 'Content-Type' : 'application/octet-stream' ,
2046+ 'X-Parse-Application-Id' : 'test' ,
2047+ 'X-Parse-REST-API-Key' : 'rest' ,
2048+ 'X-Parse-Upload-Mode' : 'stream' ,
2049+ } ;
2050+ try {
2051+ await request ( {
2052+ method : 'POST' ,
2053+ headers : headers ,
2054+ url : 'http://localhost:8378/1/files/rejected.txt' ,
2055+ body : 'rejected content' ,
2056+ } ) ;
2057+ fail ( 'should have thrown' ) ;
2058+ } catch ( response ) {
2059+ expect ( response . data . code ) . toBe ( Parse . Error . SCRIPT_FAILED ) ;
2060+ expect ( response . data . error ) . toBe ( 'Upload rejected' ) ;
2061+ }
2062+ } ) ;
2063+
2064+ it ( 'skips save when beforeSave trigger returns Parse.File with URL on streaming upload' , async ( ) => {
2065+ Parse . Cloud . beforeSave ( Parse . File , ( ) => {
2066+ const file = new Parse . File ( 'existing.txt' ) ;
2067+ file . _url = 'http://example.com/existing.txt' ;
2068+ return file ;
2069+ } ) ;
2070+ const headers = {
2071+ 'Content-Type' : 'application/octet-stream' ,
2072+ 'X-Parse-Application-Id' : 'test' ,
2073+ 'X-Parse-REST-API-Key' : 'rest' ,
2074+ 'X-Parse-Upload-Mode' : 'stream' ,
2075+ } ;
2076+ const response = await request ( {
2077+ method : 'POST' ,
2078+ headers : headers ,
2079+ url : 'http://localhost:8378/1/files/skip-save.txt' ,
2080+ body : 'should not be saved' ,
2081+ } ) ;
2082+ expect ( response . data . url ) . toBe ( 'http://example.com/existing.txt' ) ;
2083+ expect ( response . data . name ) . toBe ( 'existing.txt' ) ;
2084+ } ) ;
2085+
2086+ it ( 'fires afterSave trigger with request.stream = true on streaming upload' , async ( ) => {
2087+ let afterSaveStream ;
2088+ let afterSaveData ;
2089+ let afterSaveUrl ;
2090+ Parse . Cloud . afterSave ( Parse . File , ( request ) => {
2091+ afterSaveStream = request . stream ;
2092+ afterSaveData = request . file . _data ;
2093+ afterSaveUrl = request . file . _url ;
2094+ } ) ;
2095+ const headers = {
2096+ 'Content-Type' : 'application/octet-stream' ,
2097+ 'X-Parse-Application-Id' : 'test' ,
2098+ 'X-Parse-REST-API-Key' : 'rest' ,
2099+ 'X-Parse-Upload-Mode' : 'stream' ,
2100+ } ;
2101+ const response = await request ( {
2102+ method : 'POST' ,
2103+ headers : headers ,
2104+ url : 'http://localhost:8378/1/files/after-save.txt' ,
2105+ body : 'after save content' ,
2106+ } ) ;
2107+ expect ( response . data . name ) . toMatch ( / _ a f t e r - s a v e .t x t $ / ) ;
2108+ expect ( afterSaveStream ) . toBe ( true ) ;
2109+ expect ( afterSaveData ) . toBeFalsy ( ) ;
2110+ expect ( afterSaveUrl ) . toBeTruthy ( ) ;
2111+ } ) ;
2112+
2113+ it ( 'verifies FilesAdapter default supportsStreaming is false' , ( ) => {
2114+ const { FilesAdapter } = require ( '../lib/Adapters/Files/FilesAdapter' ) ;
2115+ const adapter = new FilesAdapter ( ) ;
2116+ expect ( adapter . supportsStreaming ) . toBe ( false ) ;
2117+ } ) ;
2118+
2119+ it ( 'legacy JSON-wrapped upload still works' , async ( ) => {
2120+ await reconfigureServer ( {
2121+ fileUpload : {
2122+ enableForPublic : true ,
2123+ fileExtensions : [ '*' ] ,
2124+ } ,
2125+ } ) ;
2126+ const response = await request ( {
2127+ method : 'POST' ,
2128+ url : 'http://localhost:8378/1/files/legacy.txt' ,
2129+ body : JSON . stringify ( {
2130+ _ApplicationId : 'test' ,
2131+ _JavaScriptKey : 'test' ,
2132+ _ContentType : 'text/plain' ,
2133+ base64 : Buffer . from ( 'legacy content' ) . toString ( 'base64' ) ,
2134+ } ) ,
2135+ } ) ;
2136+ const b = response . data ;
2137+ expect ( b . name ) . toMatch ( / _ l e g a c y .t x t $ / ) ;
2138+ const getResponse = await request ( { url : b . url } ) ;
2139+ expect ( getResponse . text ) . toEqual ( 'legacy content' ) ;
2140+ } ) ;
2141+ } ) ;
18812142} ) ;
0 commit comments