Skip to content

Commit 3d8807b

Browse files
authored
feat: Add Parse.File option maxUploadSize to override the Parse Server option maxUploadSize per file upload (#10093)
1 parent 792af37 commit 3d8807b

File tree

4 files changed

+269
-14
lines changed

4 files changed

+269
-14
lines changed

package-lock.json

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
"mongodb": "7.1.0",
4949
"mustache": "4.2.0",
5050
"otpauth": "9.4.0",
51-
"parse": "8.4.0",
51+
"parse": "8.5.0",
5252
"path-to-regexp": "8.3.0",
5353
"pg-monitor": "3.1.0",
5454
"pg-promise": "12.6.0",

spec/ParseFile.spec.js

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2021,6 +2021,210 @@ describe('Parse.File testing', () => {
20212021
}
20222022
});
20232023

2024+
describe('maxUploadSize override', () => {
2025+
it('allows streaming upload exceeding server limit with maxUploadSize override and master key', async () => {
2026+
await reconfigureServer({ maxUploadSize: '10b' });
2027+
const headers = {
2028+
'Content-Type': 'application/octet-stream',
2029+
'X-Parse-Application-Id': 'test',
2030+
'X-Parse-Master-Key': 'test',
2031+
'X-Parse-Upload-Mode': 'stream',
2032+
'X-Parse-File-Max-Upload-Size': '1mb',
2033+
};
2034+
const response = await request({
2035+
method: 'POST',
2036+
headers: headers,
2037+
url: 'http://localhost:8378/1/files/override-stream.txt',
2038+
body: 'this content is definitely longer than 10 bytes',
2039+
});
2040+
expect(response.data.name).toContain('override-stream');
2041+
expect(response.data.url).toBeDefined();
2042+
});
2043+
2044+
it('allows buffered upload exceeding server limit with maxUploadSize override and master key', async () => {
2045+
await reconfigureServer({ maxUploadSize: '10b' });
2046+
const headers = {
2047+
'Content-Type': 'application/octet-stream',
2048+
'X-Parse-Application-Id': 'test',
2049+
'X-Parse-Master-Key': 'test',
2050+
'X-Parse-File-Max-Upload-Size': '1mb',
2051+
};
2052+
const response = await request({
2053+
method: 'POST',
2054+
headers: headers,
2055+
url: 'http://localhost:8378/1/files/override-buffer.txt',
2056+
body: 'this content is definitely longer than 10 bytes',
2057+
});
2058+
expect(response.data.name).toContain('override-buffer');
2059+
expect(response.data.url).toBeDefined();
2060+
});
2061+
2062+
it('rejects maxUploadSize override without master key', async () => {
2063+
await reconfigureServer({ maxUploadSize: '10b' });
2064+
const headers = {
2065+
'Content-Type': 'application/octet-stream',
2066+
'X-Parse-Application-Id': 'test',
2067+
'X-Parse-REST-API-Key': 'rest',
2068+
'X-Parse-Upload-Mode': 'stream',
2069+
'X-Parse-File-Max-Upload-Size': '1mb',
2070+
};
2071+
try {
2072+
await request({
2073+
method: 'POST',
2074+
headers: headers,
2075+
url: 'http://localhost:8378/1/files/no-master.txt',
2076+
body: 'this content is longer than 10 bytes',
2077+
});
2078+
fail('should have thrown');
2079+
} catch (response) {
2080+
expect(response.status).toBe(403);
2081+
}
2082+
});
2083+
2084+
it('rejects invalid maxUploadSize override value', async () => {
2085+
const headers = {
2086+
'Content-Type': 'application/octet-stream',
2087+
'X-Parse-Application-Id': 'test',
2088+
'X-Parse-Master-Key': 'test',
2089+
'X-Parse-Upload-Mode': 'stream',
2090+
'X-Parse-File-Max-Upload-Size': 'notasize',
2091+
};
2092+
try {
2093+
await request({
2094+
method: 'POST',
2095+
headers: headers,
2096+
url: 'http://localhost:8378/1/files/bad-value.txt',
2097+
body: 'some data',
2098+
});
2099+
fail('should have thrown');
2100+
} catch (response) {
2101+
expect(response.data.code).toBe(Parse.Error.FILE_SAVE_ERROR);
2102+
expect(response.data.error).toContain('Invalid maxUploadSize override');
2103+
}
2104+
});
2105+
2106+
it('rejects streaming upload exceeding the overridden maxUploadSize', async () => {
2107+
await reconfigureServer({ maxUploadSize: '5b' });
2108+
const headers = {
2109+
'Content-Type': 'application/octet-stream',
2110+
'X-Parse-Application-Id': 'test',
2111+
'X-Parse-Master-Key': 'test',
2112+
'X-Parse-Upload-Mode': 'stream',
2113+
'X-Parse-File-Max-Upload-Size': '10b',
2114+
};
2115+
try {
2116+
await request({
2117+
method: 'POST',
2118+
headers: headers,
2119+
url: 'http://localhost:8378/1/files/still-too-big.txt',
2120+
body: 'this content is definitely longer than 10 bytes',
2121+
});
2122+
fail('should have thrown');
2123+
} catch (response) {
2124+
expect(response.data.code).toBe(Parse.Error.FILE_SAVE_ERROR);
2125+
expect(response.data.error).toContain('exceeds');
2126+
}
2127+
});
2128+
2129+
it('rejects maxUploadSize override with wrong master key', async () => {
2130+
const headers = {
2131+
'Content-Type': 'application/octet-stream',
2132+
'X-Parse-Application-Id': 'test',
2133+
'X-Parse-Master-Key': 'wrong-key',
2134+
'X-Parse-Upload-Mode': 'stream',
2135+
'X-Parse-File-Max-Upload-Size': '1mb',
2136+
};
2137+
try {
2138+
await request({
2139+
method: 'POST',
2140+
headers: headers,
2141+
url: 'http://localhost:8378/1/files/wrong-key.txt',
2142+
body: 'some data',
2143+
});
2144+
fail('should have thrown');
2145+
} catch (response) {
2146+
expect(response.status).toBe(403);
2147+
}
2148+
});
2149+
2150+
it('rejects maxUploadSize override with invalid application ID', async () => {
2151+
const headers = {
2152+
'Content-Type': 'application/octet-stream',
2153+
'X-Parse-Application-Id': 'invalid-app-id',
2154+
'X-Parse-Master-Key': 'test',
2155+
'X-Parse-Upload-Mode': 'stream',
2156+
'X-Parse-File-Max-Upload-Size': '1mb',
2157+
};
2158+
try {
2159+
await request({
2160+
method: 'POST',
2161+
headers: headers,
2162+
url: 'http://localhost:8378/1/files/bad-app.txt',
2163+
body: 'some data',
2164+
});
2165+
fail('should have thrown');
2166+
} catch (response) {
2167+
expect(response.status).toBe(403);
2168+
}
2169+
});
2170+
2171+
it('rejects maxUploadSize override when masterKeyIps blocks the IP', async () => {
2172+
await reconfigureServer({ masterKeyIps: ['10.0.0.1'] });
2173+
const headers = {
2174+
'Content-Type': 'application/octet-stream',
2175+
'X-Parse-Application-Id': 'test',
2176+
'X-Parse-Master-Key': 'test',
2177+
'X-Parse-Upload-Mode': 'stream',
2178+
'X-Parse-File-Max-Upload-Size': '1mb',
2179+
};
2180+
try {
2181+
await request({
2182+
method: 'POST',
2183+
headers: headers,
2184+
url: 'http://localhost:8378/1/files/blocked-ip.txt',
2185+
body: 'some data',
2186+
});
2187+
fail('should have thrown');
2188+
} catch (response) {
2189+
expect(response.status).toBe(403);
2190+
}
2191+
});
2192+
2193+
});
2194+
2195+
describe('maxUploadSize override via SDK', () => {
2196+
it('saves buffer file with maxUploadSize override and master key', async () => {
2197+
await reconfigureServer({ maxUploadSize: '10b' });
2198+
const data = Buffer.alloc(100, 'a');
2199+
const file = new Parse.File('sdk-buffer-override.txt', data, 'text/plain');
2200+
const result = await file.save({ useMasterKey: true, maxUploadSize: '1mb' });
2201+
expect(result.url()).toBeDefined();
2202+
expect(result.name()).toContain('sdk-buffer-override');
2203+
});
2204+
2205+
it('saves stream file with maxUploadSize override and master key', async () => {
2206+
await reconfigureServer({ maxUploadSize: '10b' });
2207+
const { Readable } = require('stream');
2208+
const stream = Readable.from(Buffer.alloc(100, 'b'));
2209+
const file = new Parse.File('sdk-stream-override.txt', stream, 'text/plain');
2210+
const result = await file.save({ useMasterKey: true, maxUploadSize: '1mb' });
2211+
expect(result.url()).toBeDefined();
2212+
expect(result.name()).toContain('sdk-stream-override');
2213+
});
2214+
2215+
it('rejects maxUploadSize override without master key', async () => {
2216+
await reconfigureServer({ maxUploadSize: '10b' });
2217+
const data = Buffer.alloc(100, 'c');
2218+
const file = new Parse.File('sdk-no-master.txt', data, 'text/plain');
2219+
try {
2220+
await file.save({ maxUploadSize: '1mb' });
2221+
fail('should have thrown');
2222+
} catch (error) {
2223+
expect(error.error).toBeDefined();
2224+
}
2225+
});
2226+
});
2227+
20242228
it('fires beforeSave trigger with request.stream = true on streaming upload', async () => {
20252229
let receivedStream;
20262230
let receivedData;

src/Routers/FilesRouter.js

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ export class FilesRouter {
9898

9999
router.post(
100100
'/files/:filename',
101+
this._earlyHeadersMiddleware(),
101102
this._bodyParsingMiddleware(maxUploadSize),
102103
Middlewares.handleParseHeaders,
103104
Middlewares.handleParseSession,
@@ -234,17 +235,67 @@ export class FilesRouter {
234235
}
235236
}
236237

238+
/**
239+
* Middleware that runs before body parsing to handle headers that must be
240+
* resolved before the request body is consumed. Currently supports:
241+
*
242+
* - `X-Parse-File-Max-Upload-Size`: Overrides the server-wide `maxUploadSize`
243+
* for this request. Requires the master key. The value uses the same format
244+
* as the server option (e.g. `'50mb'`, `'1gb'`). Sets `req._maxUploadSizeOverride`
245+
* (in bytes) for `_bodyParsingMiddleware` to use.
246+
*/
247+
_earlyHeadersMiddleware() {
248+
return async (req, res, next) => {
249+
const maxUploadSizeOverride = req.get('X-Parse-File-Max-Upload-Size');
250+
if (!maxUploadSizeOverride) {
251+
return next();
252+
}
253+
const appId = req.get('X-Parse-Application-Id');
254+
const config = Config.get(appId);
255+
if (!config) {
256+
const error = createSanitizedHttpError(403, 'Invalid application ID.', undefined);
257+
res.status(error.status);
258+
res.json({ error: error.message });
259+
return;
260+
}
261+
const masterKey = await config.loadMasterKey();
262+
if (req.get('X-Parse-Master-Key') !== masterKey) {
263+
const error = createSanitizedHttpError(403, 'unauthorized: master key is required', config);
264+
res.status(error.status);
265+
res.json({ error: error.message });
266+
return;
267+
}
268+
if (config.masterKeyIps?.length && !Middlewares.checkIp(req.ip, config.masterKeyIps, config.masterKeyIpsStore)) {
269+
const error = createSanitizedHttpError(403, 'unauthorized: master key is required', config);
270+
res.status(error.status);
271+
res.json({ error: error.message });
272+
return;
273+
}
274+
let parsedBytes;
275+
try {
276+
parsedBytes = Utils.parseSizeToBytes(maxUploadSizeOverride);
277+
} catch {
278+
return next(
279+
new Parse.Error(
280+
Parse.Error.FILE_SAVE_ERROR,
281+
`Invalid maxUploadSize override value: ${maxUploadSizeOverride}`
282+
)
283+
);
284+
}
285+
req._maxUploadSizeOverride = parsedBytes;
286+
next();
287+
};
288+
}
289+
237290
_bodyParsingMiddleware(maxUploadSize) {
238-
const rawParser = express.raw({
239-
type: () => true,
240-
limit: maxUploadSize,
241-
});
291+
const defaultMaxBytes = Utils.parseSizeToBytes(maxUploadSize);
242292
return (req, res, next) => {
243293
if (req.get('X-Parse-Upload-Mode') === 'stream') {
244-
req._maxUploadSizeBytes = Utils.parseSizeToBytes(maxUploadSize);
294+
req._maxUploadSizeBytes = req._maxUploadSizeOverride ?? defaultMaxBytes;
245295
return next();
246296
}
247-
return rawParser(req, res, next);
297+
const limit = req._maxUploadSizeOverride ?? maxUploadSize;
298+
return express.raw({ type: () => true, limit })(req, res, next);
248299
};
249300
}
250301

0 commit comments

Comments
 (0)