-
-
Notifications
You must be signed in to change notification settings - Fork 4.8k
feat: Allow large Parse File uploads #9286
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: alpha
Are you sure you want to change the base?
Changes from 8 commits
734d85e
088c669
3385d2f
0a2c904
d2c89af
216c078
a559453
39face2
aedd984
c814b30
0cf1391
92b5ec5
5bd46b6
01cce43
f296828
73bbdb0
d43a8ad
f817c20
aabae8b
07098f9
f09037f
5c298b3
e276df3
fad46b3
18157be
a87a452
7e23668
65b06b5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,94 @@ | ||||||||||||||||||||||||||||||||||||||||
| const fs = require('fs'); | ||||||||||||||||||||||||||||||||||||||||
| const path = require('path'); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| describe('FilesRouter', () => { | ||||||||||||||||||||||||||||||||||||||||
| describe('File Uploads', () => { | ||||||||||||||||||||||||||||||||||||||||
| const V8_STRING_LIMIT_BYTES = 536_870_912; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| let server; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| beforeAll(async () => { | ||||||||||||||||||||||||||||||||||||||||
| server = await reconfigureServer({ | ||||||||||||||||||||||||||||||||||||||||
| maxUploadSize: '1GB', | ||||||||||||||||||||||||||||||||||||||||
| port: 8384, | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| afterAll(async () => { | ||||||||||||||||||||||||||||||||||||||||
| // clean up the server for resuse | ||||||||||||||||||||||||||||||||||||||||
| if (server && server.close) { | ||||||||||||||||||||||||||||||||||||||||
| await new Promise((resolve, reject) => { | ||||||||||||||||||||||||||||||||||||||||
| server.close(err => { | ||||||||||||||||||||||||||||||||||||||||
| if (err) return reject(err); | ||||||||||||||||||||||||||||||||||||||||
| resolve(); | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||
| * Quick helper function to upload the file to the server via the REST API | ||||||||||||||||||||||||||||||||||||||||
| * We do this because creating a Parse.File object with a file over 512MB | ||||||||||||||||||||||||||||||||||||||||
| * will try to use the Web API FileReader API, which will fail the test | ||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||
| * @param {string} fileName the name of the file | ||||||||||||||||||||||||||||||||||||||||
| * @param {string} filePath the path to the file locally | ||||||||||||||||||||||||||||||||||||||||
| * @returns | ||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||
| const postFile = async (fileName, filePath) => { | ||||||||||||||||||||||||||||||||||||||||
| const url = `${Parse.serverURL}/files/${fileName}`; | ||||||||||||||||||||||||||||||||||||||||
| const headers = { | ||||||||||||||||||||||||||||||||||||||||
| 'X-Parse-Application-Id': Parse.applicationId, | ||||||||||||||||||||||||||||||||||||||||
| 'X-Parse-Master-Key': Parse.masterKey, | ||||||||||||||||||||||||||||||||||||||||
| 'Content-Type': 'multipart/form-data', | ||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+27
to
+31
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The request sends a raw file stream, not a multipart-encoded body. Proposed fix const headers = {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Master-Key': Parse.masterKey,
- 'Content-Type': 'multipart/form-data',
+ 'Content-Type': 'application/octet-stream',
};📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // Create a FormData object to send the file | ||||||||||||||||||||||||||||||||||||||||
| const formData = new FormData(); | ||||||||||||||||||||||||||||||||||||||||
| formData.append('file', fs.createReadStream(filePath)); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // Send the request | ||||||||||||||||||||||||||||||||||||||||
| const response = await fetch(url, { | ||||||||||||||||||||||||||||||||||||||||
| method: 'POST', | ||||||||||||||||||||||||||||||||||||||||
| headers, | ||||||||||||||||||||||||||||||||||||||||
| body: formData, | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| return response; | ||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| it('should allow Parse.File uploads under 512MB', async done => { | ||||||||||||||||||||||||||||||||||||||||
| const filePath = path.join(__dirname, 'file.txt'); | ||||||||||||||||||||||||||||||||||||||||
| fs.writeFileSync(filePath, Buffer.alloc(1024 * 1024)); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const response = await postFile('file.txt', filePath); | ||||||||||||||||||||||||||||||||||||||||
| expect(response.ok).toBe(true); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| fs.unlinkSync(filePath); | ||||||||||||||||||||||||||||||||||||||||
| done(); | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+46
to
+55
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Replace All three tests use the Based on learnings: "New tests in the parse-server repository should use async/await with promise-based patterns rather than callback patterns with Proposed fix (apply same pattern to all three tests)- it('should allow Parse.File uploads under 512MB', async done => {
+ it('should allow Parse.File uploads under 512MB', async () => {
const filePath = path.join(__dirname, 'file.txt');
await fs.promises.writeFile(filePath, Buffer.alloc(1024 * 1024));
const response = await postFile('file.txt', filePath);
expect(response.ok).toBe(true);
fs.unlinkSync(filePath);
- done();
});📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| it('should allow Parse.File uploads exactly 512MB', async done => { | ||||||||||||||||||||||||||||||||||||||||
| const filePath = path.join(__dirname, 'file.txt'); | ||||||||||||||||||||||||||||||||||||||||
| fs.writeFileSync(filePath, Buffer.alloc(V8_STRING_LIMIT_BYTES)); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const response = await postFile('file.txt', filePath); | ||||||||||||||||||||||||||||||||||||||||
| expect(response.ok).toBe(true); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| fs.unlinkSync(filePath); | ||||||||||||||||||||||||||||||||||||||||
| done(); | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| it('should allow Parse.File uploads over 512MB', async done => { | ||||||||||||||||||||||||||||||||||||||||
| const filePath = path.join(__dirname, 'file.txt'); | ||||||||||||||||||||||||||||||||||||||||
| fs.writeFileSync(filePath, Buffer.alloc(V8_STRING_LIMIT_BYTES + 50 * 1024 * 1024)); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const response = await postFile('file.txt', filePath); | ||||||||||||||||||||||||||||||||||||||||
| expect(response.ok).toBe(true); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| fs.unlinkSync(filePath); | ||||||||||||||||||||||||||||||||||||||||
| done(); | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -172,8 +172,22 @@ export class FilesRouter { | |
| } | ||
| } | ||
|
|
||
| const base64 = req.body.toString('base64'); | ||
| const file = new Parse.File(filename, { base64 }, contentType); | ||
| // If the request body is a buffer and it's size is greater than the V8 string size limit | ||
| // we need to use a Blob to avoid the V8 string size limit | ||
| const MAX_V8_STRING_SIZE_BYTES = 536_870_912; | ||
|
|
||
| let file; | ||
|
|
||
| if ( | ||
| typeof Blob !== 'undefined' && | ||
| Buffer.isBuffer(req.body) && | ||
| req.body?.length >= MAX_V8_STRING_SIZE_BYTES | ||
| ) { | ||
|
mtrezza marked this conversation as resolved.
Outdated
|
||
| file = new Parse.File(filename, new Blob([req.body]), contentType); | ||
| } else { | ||
| file = new Parse.File(filename, { base64: req.body.toString('base64') }, contentType); | ||
| } | ||
|
Comment on lines
+185
to
+195
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Large uploads temporarily double memory usage.
Consider whether the original 🤖 Prompt for AI Agents |
||
|
|
||
| const { metadata = {}, tags = {} } = req.fileData || {}; | ||
| try { | ||
| // Scan request data for denied keywords | ||
|
|
@@ -213,8 +227,18 @@ export class FilesRouter { | |
| // if the ParseFile returned is type uri, download the file before saving it | ||
| await addFileDataIfNeeded(fileObject.file); | ||
| // update fileSize | ||
| const bufferData = Buffer.from(fileObject.file._data, 'base64'); | ||
| fileObject.fileSize = Buffer.byteLength(bufferData); | ||
| let fileData; | ||
| // if the file is a blob, get the size from the blob | ||
| if (typeof Blob !== 'undefined' && fileObject.file._source?.file instanceof Blob) { | ||
|
mtrezza marked this conversation as resolved.
Outdated
|
||
| // get the size of the blob | ||
| fileObject.fileSize = fileObject.file._source.file.size; | ||
| // set the file data | ||
| fileData = fileObject.file._source?.file; | ||
| } else { | ||
| const bufferData = Buffer.from(fileObject.file._data, 'base64'); | ||
| fileObject.fileSize = Buffer.byteLength(bufferData); | ||
| fileData = bufferData; | ||
| } | ||
|
Comment on lines
+234
to
+245
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: cat package.json | jq '.dependencies["parse"], .devDependencies["parse"]'Repository: parse-community/parse-server Length of output: 84 🏁 Script executed: rg -n '_source\b|_data\b' --type=js src/ | head -30Repository: parse-community/parse-server Length of output: 2505 🏁 Script executed: # Check if Parse.File is instantiated elsewhere and how
rg -n 'new Parse\.File|Parse\.File\(' --type=js src/ | head -20Repository: parse-community/parse-server Length of output: 597 🏁 Script executed: # Look for type definitions or documentation about Parse.File
fd -e d.ts -e ts -e js 'parse' --search-path=node_modules 2>/dev/null | head -10Repository: parse-community/parse-server Length of output: 54 🌐 Web query:
💡 Result: In the Parse JavaScript SDK, What
|
||
| // prepare file options | ||
| const fileOptions = { | ||
| metadata: fileObject.file._metadata, | ||
|
|
@@ -228,7 +252,7 @@ export class FilesRouter { | |
| const createFileResult = await filesController.createFile( | ||
| config, | ||
| fileObject.file._name, | ||
| bufferData, | ||
| fileData, | ||
| fileObject.file._source.type, | ||
| fileOptions | ||
| ); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't it be enough to just call
await reconfigureServer()to init the server with the default config?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Your right. Missed that, new to testing here.