mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
feat: Add Parse.File option maxUploadSize to override the Parse Server option maxUploadSize per file upload (#10093)
This commit is contained in:
Generated
+7
-7
@@ -38,7 +38,7 @@
|
||||
"mongodb": "7.1.0",
|
||||
"mustache": "4.2.0",
|
||||
"otpauth": "9.4.0",
|
||||
"parse": "8.4.0",
|
||||
"parse": "8.5.0",
|
||||
"path-to-regexp": "8.3.0",
|
||||
"pg-monitor": "3.1.0",
|
||||
"pg-promise": "12.6.0",
|
||||
@@ -18402,9 +18402,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/parse": {
|
||||
"version": "8.4.0",
|
||||
"resolved": "https://registry.npmjs.org/parse/-/parse-8.4.0.tgz",
|
||||
"integrity": "sha512-Pfb0Oedh9PHU0ZQ54EupCgh9zZR0OqitiXSHJ6oyGIqCsaOZ+ENAuw6Nr06T0FqzYb/fYkq4cC4hdte2QmjjNA==",
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/parse/-/parse-8.5.0.tgz",
|
||||
"integrity": "sha512-X9gI4Yjbi9LPMPnCtKL4h0Nxe1aSCFMPWcB1zbu11qU/Be3eVSB5I5IMBunTuWlVz6Wchu3dtM5jl/1aBZ9wiQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.28.6",
|
||||
@@ -35512,9 +35512,9 @@
|
||||
}
|
||||
},
|
||||
"parse": {
|
||||
"version": "8.4.0",
|
||||
"resolved": "https://registry.npmjs.org/parse/-/parse-8.4.0.tgz",
|
||||
"integrity": "sha512-Pfb0Oedh9PHU0ZQ54EupCgh9zZR0OqitiXSHJ6oyGIqCsaOZ+ENAuw6Nr06T0FqzYb/fYkq4cC4hdte2QmjjNA==",
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/parse/-/parse-8.5.0.tgz",
|
||||
"integrity": "sha512-X9gI4Yjbi9LPMPnCtKL4h0Nxe1aSCFMPWcB1zbu11qU/Be3eVSB5I5IMBunTuWlVz6Wchu3dtM5jl/1aBZ9wiQ==",
|
||||
"requires": {
|
||||
"@babel/runtime": "7.28.6",
|
||||
"@babel/runtime-corejs3": "7.29.0",
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@
|
||||
"mongodb": "7.1.0",
|
||||
"mustache": "4.2.0",
|
||||
"otpauth": "9.4.0",
|
||||
"parse": "8.4.0",
|
||||
"parse": "8.5.0",
|
||||
"path-to-regexp": "8.3.0",
|
||||
"pg-monitor": "3.1.0",
|
||||
"pg-promise": "12.6.0",
|
||||
|
||||
@@ -2021,6 +2021,210 @@ describe('Parse.File testing', () => {
|
||||
}
|
||||
});
|
||||
|
||||
describe('maxUploadSize override', () => {
|
||||
it('allows streaming upload exceeding server limit with maxUploadSize override and master key', async () => {
|
||||
await reconfigureServer({ maxUploadSize: '10b' });
|
||||
const headers = {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Master-Key': 'test',
|
||||
'X-Parse-Upload-Mode': 'stream',
|
||||
'X-Parse-File-Max-Upload-Size': '1mb',
|
||||
};
|
||||
const response = await request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/files/override-stream.txt',
|
||||
body: 'this content is definitely longer than 10 bytes',
|
||||
});
|
||||
expect(response.data.name).toContain('override-stream');
|
||||
expect(response.data.url).toBeDefined();
|
||||
});
|
||||
|
||||
it('allows buffered upload exceeding server limit with maxUploadSize override and master key', async () => {
|
||||
await reconfigureServer({ maxUploadSize: '10b' });
|
||||
const headers = {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Master-Key': 'test',
|
||||
'X-Parse-File-Max-Upload-Size': '1mb',
|
||||
};
|
||||
const response = await request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/files/override-buffer.txt',
|
||||
body: 'this content is definitely longer than 10 bytes',
|
||||
});
|
||||
expect(response.data.name).toContain('override-buffer');
|
||||
expect(response.data.url).toBeDefined();
|
||||
});
|
||||
|
||||
it('rejects maxUploadSize override without master key', async () => {
|
||||
await reconfigureServer({ maxUploadSize: '10b' });
|
||||
const headers = {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'X-Parse-Upload-Mode': 'stream',
|
||||
'X-Parse-File-Max-Upload-Size': '1mb',
|
||||
};
|
||||
try {
|
||||
await request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/files/no-master.txt',
|
||||
body: 'this content is longer than 10 bytes',
|
||||
});
|
||||
fail('should have thrown');
|
||||
} catch (response) {
|
||||
expect(response.status).toBe(403);
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects invalid maxUploadSize override value', async () => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Master-Key': 'test',
|
||||
'X-Parse-Upload-Mode': 'stream',
|
||||
'X-Parse-File-Max-Upload-Size': 'notasize',
|
||||
};
|
||||
try {
|
||||
await request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/files/bad-value.txt',
|
||||
body: 'some data',
|
||||
});
|
||||
fail('should have thrown');
|
||||
} catch (response) {
|
||||
expect(response.data.code).toBe(Parse.Error.FILE_SAVE_ERROR);
|
||||
expect(response.data.error).toContain('Invalid maxUploadSize override');
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects streaming upload exceeding the overridden maxUploadSize', async () => {
|
||||
await reconfigureServer({ maxUploadSize: '5b' });
|
||||
const headers = {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Master-Key': 'test',
|
||||
'X-Parse-Upload-Mode': 'stream',
|
||||
'X-Parse-File-Max-Upload-Size': '10b',
|
||||
};
|
||||
try {
|
||||
await request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/files/still-too-big.txt',
|
||||
body: 'this content is definitely longer than 10 bytes',
|
||||
});
|
||||
fail('should have thrown');
|
||||
} catch (response) {
|
||||
expect(response.data.code).toBe(Parse.Error.FILE_SAVE_ERROR);
|
||||
expect(response.data.error).toContain('exceeds');
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects maxUploadSize override with wrong master key', async () => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Master-Key': 'wrong-key',
|
||||
'X-Parse-Upload-Mode': 'stream',
|
||||
'X-Parse-File-Max-Upload-Size': '1mb',
|
||||
};
|
||||
try {
|
||||
await request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/files/wrong-key.txt',
|
||||
body: 'some data',
|
||||
});
|
||||
fail('should have thrown');
|
||||
} catch (response) {
|
||||
expect(response.status).toBe(403);
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects maxUploadSize override with invalid application ID', async () => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'X-Parse-Application-Id': 'invalid-app-id',
|
||||
'X-Parse-Master-Key': 'test',
|
||||
'X-Parse-Upload-Mode': 'stream',
|
||||
'X-Parse-File-Max-Upload-Size': '1mb',
|
||||
};
|
||||
try {
|
||||
await request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/files/bad-app.txt',
|
||||
body: 'some data',
|
||||
});
|
||||
fail('should have thrown');
|
||||
} catch (response) {
|
||||
expect(response.status).toBe(403);
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects maxUploadSize override when masterKeyIps blocks the IP', async () => {
|
||||
await reconfigureServer({ masterKeyIps: ['10.0.0.1'] });
|
||||
const headers = {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Master-Key': 'test',
|
||||
'X-Parse-Upload-Mode': 'stream',
|
||||
'X-Parse-File-Max-Upload-Size': '1mb',
|
||||
};
|
||||
try {
|
||||
await request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/files/blocked-ip.txt',
|
||||
body: 'some data',
|
||||
});
|
||||
fail('should have thrown');
|
||||
} catch (response) {
|
||||
expect(response.status).toBe(403);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('maxUploadSize override via SDK', () => {
|
||||
it('saves buffer file with maxUploadSize override and master key', async () => {
|
||||
await reconfigureServer({ maxUploadSize: '10b' });
|
||||
const data = Buffer.alloc(100, 'a');
|
||||
const file = new Parse.File('sdk-buffer-override.txt', data, 'text/plain');
|
||||
const result = await file.save({ useMasterKey: true, maxUploadSize: '1mb' });
|
||||
expect(result.url()).toBeDefined();
|
||||
expect(result.name()).toContain('sdk-buffer-override');
|
||||
});
|
||||
|
||||
it('saves stream file with maxUploadSize override and master key', async () => {
|
||||
await reconfigureServer({ maxUploadSize: '10b' });
|
||||
const { Readable } = require('stream');
|
||||
const stream = Readable.from(Buffer.alloc(100, 'b'));
|
||||
const file = new Parse.File('sdk-stream-override.txt', stream, 'text/plain');
|
||||
const result = await file.save({ useMasterKey: true, maxUploadSize: '1mb' });
|
||||
expect(result.url()).toBeDefined();
|
||||
expect(result.name()).toContain('sdk-stream-override');
|
||||
});
|
||||
|
||||
it('rejects maxUploadSize override without master key', async () => {
|
||||
await reconfigureServer({ maxUploadSize: '10b' });
|
||||
const data = Buffer.alloc(100, 'c');
|
||||
const file = new Parse.File('sdk-no-master.txt', data, 'text/plain');
|
||||
try {
|
||||
await file.save({ maxUploadSize: '1mb' });
|
||||
fail('should have thrown');
|
||||
} catch (error) {
|
||||
expect(error.error).toBeDefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('fires beforeSave trigger with request.stream = true on streaming upload', async () => {
|
||||
let receivedStream;
|
||||
let receivedData;
|
||||
|
||||
@@ -98,6 +98,7 @@ export class FilesRouter {
|
||||
|
||||
router.post(
|
||||
'/files/:filename',
|
||||
this._earlyHeadersMiddleware(),
|
||||
this._bodyParsingMiddleware(maxUploadSize),
|
||||
Middlewares.handleParseHeaders,
|
||||
Middlewares.handleParseSession,
|
||||
@@ -234,17 +235,67 @@ export class FilesRouter {
|
||||
}
|
||||
}
|
||||
|
||||
_bodyParsingMiddleware(maxUploadSize) {
|
||||
const rawParser = express.raw({
|
||||
type: () => true,
|
||||
limit: maxUploadSize,
|
||||
});
|
||||
return (req, res, next) => {
|
||||
if (req.get('X-Parse-Upload-Mode') === 'stream') {
|
||||
req._maxUploadSizeBytes = Utils.parseSizeToBytes(maxUploadSize);
|
||||
/**
|
||||
* Middleware that runs before body parsing to handle headers that must be
|
||||
* resolved before the request body is consumed. Currently supports:
|
||||
*
|
||||
* - `X-Parse-File-Max-Upload-Size`: Overrides the server-wide `maxUploadSize`
|
||||
* for this request. Requires the master key. The value uses the same format
|
||||
* as the server option (e.g. `'50mb'`, `'1gb'`). Sets `req._maxUploadSizeOverride`
|
||||
* (in bytes) for `_bodyParsingMiddleware` to use.
|
||||
*/
|
||||
_earlyHeadersMiddleware() {
|
||||
return async (req, res, next) => {
|
||||
const maxUploadSizeOverride = req.get('X-Parse-File-Max-Upload-Size');
|
||||
if (!maxUploadSizeOverride) {
|
||||
return next();
|
||||
}
|
||||
return rawParser(req, res, next);
|
||||
const appId = req.get('X-Parse-Application-Id');
|
||||
const config = Config.get(appId);
|
||||
if (!config) {
|
||||
const error = createSanitizedHttpError(403, 'Invalid application ID.', undefined);
|
||||
res.status(error.status);
|
||||
res.json({ error: error.message });
|
||||
return;
|
||||
}
|
||||
const masterKey = await config.loadMasterKey();
|
||||
if (req.get('X-Parse-Master-Key') !== masterKey) {
|
||||
const error = createSanitizedHttpError(403, 'unauthorized: master key is required', config);
|
||||
res.status(error.status);
|
||||
res.json({ error: error.message });
|
||||
return;
|
||||
}
|
||||
if (config.masterKeyIps?.length && !Middlewares.checkIp(req.ip, config.masterKeyIps, config.masterKeyIpsStore)) {
|
||||
const error = createSanitizedHttpError(403, 'unauthorized: master key is required', config);
|
||||
res.status(error.status);
|
||||
res.json({ error: error.message });
|
||||
return;
|
||||
}
|
||||
let parsedBytes;
|
||||
try {
|
||||
parsedBytes = Utils.parseSizeToBytes(maxUploadSizeOverride);
|
||||
} catch {
|
||||
return next(
|
||||
new Parse.Error(
|
||||
Parse.Error.FILE_SAVE_ERROR,
|
||||
`Invalid maxUploadSize override value: ${maxUploadSizeOverride}`
|
||||
)
|
||||
);
|
||||
}
|
||||
req._maxUploadSizeOverride = parsedBytes;
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
_bodyParsingMiddleware(maxUploadSize) {
|
||||
const defaultMaxBytes = Utils.parseSizeToBytes(maxUploadSize);
|
||||
return (req, res, next) => {
|
||||
if (req.get('X-Parse-Upload-Mode') === 'stream') {
|
||||
req._maxUploadSizeBytes = req._maxUploadSizeOverride ?? defaultMaxBytes;
|
||||
return next();
|
||||
}
|
||||
const limit = req._maxUploadSizeOverride ?? maxUploadSize;
|
||||
return express.raw({ type: () => true, limit })(req, res, next);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user