Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
734d85e
feat: adjust the router for large buffers
dalyaidan1 Aug 17, 2024
088c669
feat: adjust adapter for large blobs
dalyaidan1 Aug 17, 2024
3385d2f
test: test file uploads
dalyaidan1 Aug 17, 2024
0a2c904
fix: don't depend on axios
dalyaidan1 Aug 17, 2024
d2c89af
feat: clean up files
dalyaidan1 Aug 17, 2024
216c078
fix: file default data and adapter comment
dalyaidan1 Aug 17, 2024
a559453
test: split test and move to better file
dalyaidan1 Aug 17, 2024
39face2
fix: remove unwanted line
dalyaidan1 Aug 18, 2024
aedd984
fix typo
mtrezza Aug 18, 2024
c814b30
Merge branch 'alpha' into parse-file-large-upload
mtrezza Aug 18, 2024
0cf1391
fix: remove unneeded server reconfigure
dalyaidan1 Aug 18, 2024
92b5ec5
fix: actually upload the full files
dalyaidan1 Aug 18, 2024
5bd46b6
fix: encryption chunking
dalyaidan1 Aug 19, 2024
01cce43
test: adjust to test blobs too
dalyaidan1 Aug 19, 2024
f296828
use pipes
dalyaidan1 Aug 19, 2024
73bbdb0
fix: remove web readable
dalyaidan1 Aug 19, 2024
d43a8ad
fix: test errors and clean up pipes
dalyaidan1 Aug 20, 2024
f817c20
fix: remove unused dep
dalyaidan1 Aug 20, 2024
aabae8b
fix: comment typo
dalyaidan1 Aug 20, 2024
07098f9
feat: ternary iv
dalyaidan1 Aug 20, 2024
f09037f
Merge branch 'alpha' into parse-file-large-upload
mtrezza Sep 4, 2024
5c298b3
remove Blob avail check
mtrezza Oct 8, 2024
e276df3
remove blob check
mtrezza Oct 8, 2024
fad46b3
Merge branch 'alpha' into parse-file-large-upload
mtrezza Oct 8, 2024
18157be
change file router to only mongo
dalyaidan1 Nov 2, 2024
a87a452
Merge branch 'alpha' into parse-file-large-upload
mtrezza Nov 3, 2024
7e23668
Merge branch 'alpha' into parse-file-large-upload
mtrezza Nov 7, 2024
65b06b5
Merge branch 'alpha' into parse-file-large-upload
mtrezza Feb 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions spec/FilesRouter.spec.js
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) {
Copy link
Copy Markdown
Member

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?

Copy link
Copy Markdown
Author

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.

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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Content-Type: multipart/form-data is semantically incorrect for a raw stream body.

The request sends a raw file stream, not a multipart-encoded body. multipart/form-data requires boundary delimiters. Since express.raw() accepts any content type, this works by accident. Use application/octet-stream (or the actual MIME type) to be accurate.

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const headers = {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Master-Key': Parse.masterKey,
'Content-Type': 'multipart/form-data',
};
const headers = {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Master-Key': Parse.masterKey,
'Content-Type': 'application/octet-stream',
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@spec/FilesRouter.spec.js` around lines 27 - 31, The test's headers object
sets 'Content-Type' to 'multipart/form-data' even though the request body is a
raw file stream; update the headers in FilesRouter.spec.js (the headers constant
used in the test) to use 'application/octet-stream' or the file's real MIME type
so the Content-Type accurately reflects a raw stream rather than
multipart/form-data.


// 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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Replace done callback with pure async/await.

All three tests use the async done => pattern. Since the test body is fully await-based, the done callback is unnecessary and can mask unhandled rejections. This applies equally to the tests on lines 57 and 68.

Based on learnings: "New tests in the parse-server repository should use async/await with promise-based patterns rather than callback patterns with done()."

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('should allow Parse.File uploads under 512MB', async done => {
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();
});
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);
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@spec/FilesRouter.spec.js` around lines 46 - 55, The tests use the legacy
async done callback pattern which can mask promise rejections; update each test
(e.g., the it blocks with descriptions "should allow Parse.File uploads under
512MB" and the two following its) by removing the done parameter (change async
done => to async () =>) and deleting the terminal done() call(s), leaving the
existing await calls so Jest handles promise resolution/rejection natively;
apply the same change to the other two it blocks referenced in the diff so all
three tests use pure async/await.


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();
});
});
});
2 changes: 1 addition & 1 deletion src/Adapters/Files/FilesAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export class FilesAdapter {
/** Responsible for storing the file in order to be retrieved later by its filename
*
* @param {string} filename - the filename to save
* @param {*} data - the buffer of data from the file
* @param {*} data - the repersentation of data from the file as buffer or a blob
Comment thread
mtrezza marked this conversation as resolved.
Outdated
* @param {string} contentType - the supposed contentType
* @discussion the contentType can be undefined if the controller was not able to determine it
* @param {object} options - (Optional) options to be passed to file adapter (S3 File Adapter Only)
Expand Down
55 changes: 44 additions & 11 deletions src/Adapters/Files/GridFSBucketAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,24 +68,57 @@ export class GridFSBucketAdapter extends FilesAdapter {
const stream = await bucket.openUploadStream(filename, {
metadata: options.metadata,
});
if (this._encryptionKey !== null) {

// when working with a Blob, it could be over the max size of a buffer, so we need to stream it
if (typeof Blob !== 'undefined' && data instanceof Blob) {
const reader = data.stream().getReader();
const iv = crypto.randomBytes(16);
const cipher = this._encryptionKey !== null ? crypto.createCipheriv(this._algorithm, this._encryptionKey, iv) : null;

const processChunk = async ({ done, value }) => {
if (done) {
if (cipher) {
const finalChunk = Buffer.concat([cipher.final(), iv, cipher.getAuthTag()]);
await stream.write(finalChunk);
}
stream.end();
return;
}

if (cipher) {
value = cipher.update(value);
}

await stream.write(value);
reader.read().then(processChunk);
};
try {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(this._algorithm, this._encryptionKey, iv);
const encryptedResult = Buffer.concat([
cipher.update(data),
cipher.final(),
iv,
cipher.getAuthTag(),
]);
await stream.write(encryptedResult);
reader.read().then(processChunk);
} catch (err) {
return new Promise((resolve, reject) => {
return reject(err);
});
}
} else {
await stream.write(data);
if (this._encryptionKey !== null) {
try {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(this._algorithm, this._encryptionKey, iv);
const encryptedResult = Buffer.concat([
cipher.update(data),
cipher.final(),
iv,
cipher.getAuthTag(),
]);
await stream.write(encryptedResult);
} catch (err) {
return new Promise((resolve, reject) => {
return reject(err);
});
}
} else {
await stream.write(data);
}
}
stream.end();
return new Promise((resolve, reject) => {
Expand Down
34 changes: 29 additions & 5 deletions src/Routers/FilesRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
) {
Comment thread
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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Large uploads temporarily double memory usage.

new Blob([req.body]) copies the buffer contents, so for a 512 MB+ upload both req.body (Buffer) and the Blob coexist in memory until the handler completes. This means a single ~600 MB upload consumes ~1.2 GB of process memory.

Consider whether the original req.body reference can be released early (e.g., req.body = null after creating the Blob), or whether the Blob wrapping can be deferred closer to the storage adapter call to shorten the overlap window.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Routers/FilesRouter.js` around lines 185 - 195, The code creates a Blob
from req.body which doubles memory during large uploads; update the FilesRouter
handling around MAX_V8_STRING_SIZE_BYTES and Parse.File so you either (a) defer
Blob creation until right before the storage adapter upload call (move new
Blob([req.body]) close to where Parse.File is handed to the adapter) or (b)
immediately drop the original Buffer reference by setting req.body = null right
after constructing the Blob (ensure filename and contentType are preserved), so
the temporary overlap of req.body and the Blob is minimized.


const { metadata = {}, tags = {} } = req.fileData || {};
try {
// Scan request data for denied keywords
Expand Down Expand Up @@ -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) {
Comment thread
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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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 -30

Repository: 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 -20

Repository: 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 -10

Repository: parse-community/parse-server

Length of output: 54


🌐 Web query:

Parse.File _source _data internal properties JavaScript SDK documentation

💡 Result:

In the Parse JavaScript SDK, Parse.File’s underscored fields like _source and _data are internal implementation details (not part of the public API), so they’re not documented as supported/stable. The supported/public surface is methods like name(), url(), save(), getData() and toJSON(). [1]

What _source is (internal)

From the SDK implementation, Parse.File stores an internal _source object that describes where the file’s content came from and how it should be uploaded. It’s set by the constructor based on what you pass as data (bytes/base64/blob/uri), e.g. formats like:

  • { format: 'base64', base64: '...', type: '...' }
  • { format: 'file', file: Blob, type: '...' }
  • { format: 'uri', uri: '...', type: '...' } [2]

This mirrors what the constructor docs say you can pass as data (byte array, {base64: ...}, {uri: ...}, browser File/Blob). [1]

What _data is (internal)

In current SDK builds, _data is an internal base64 cache of the file’s contents:

  • It’s populated when you construct the file from bytes/base64 (and after downloading via getData()).
  • getData() returns _data immediately if it’s already present; otherwise it downloads from _url and stores result.base64 into _data. [2]
  • When uploading from a browser Blob/file, _data is cleared (null) after a successful save() (since the SDK didn’t necessarily keep a base64 copy around). [2]

What to use instead of _source / _data

Use the public methods:

  • await file.getData() to obtain base64 (rather than reading _data) [1]
  • file.url() / file.name() [1]
  • file.toJSON() to get the { __type: 'File', name, url } representation stored in objects [2]

Sources:
[1] Parse JS SDK API Reference – Parse.File (v2.6.0) https://parseplatform.org/Parse-SDK-JS/api/2.6.0/Parse.File.html
[2] Parse JS SDK source (ParseFile.js, parse@8.0.3 build on UNPKG) https://app.unpkg.com/parse@8.0.3/files/lib/browser/ParseFile.js


🌐 Web query:

Parse JavaScript SDK Parse.File public API extract file data

💡 Result:

In the Parse JavaScript SDK, Parse.File’s public instance API does not expose the file’s raw bytes/base64 after it’s been created—its documented public methods are essentially name(), save(), and url() (plus options). So the supported way to “extract file data” is to download it from the file URL. [1]

Browser (download as Blob)

const file = obj.get('myFile');          // Parse.File
const res = await fetch(file.url());
const blob = await res.blob();           // <-- file data

Node / Cloud Code (download as Buffer)

const file = request.object.get('myFile');  // Parse.File
const res = await Parse.Cloud.httpRequest({ url: file.url() });
const buf = res.buffer;                     // <-- file data (Buffer)

This pattern (get file.url() then httpRequest) is commonly used in Cloud Code. [2]

Note: file.url() is only available after the file is saved or loaded from a Parse.Object. [1]

Sources: Parse JS SDK Parse.File API docs [1]; example approach via Parse.Cloud.httpRequest [2].

[1] https://parseplatform.org/Parse-SDK-JS/api/2.1.0/Parse.File.html
[2] https://stackoverflow.com/questions/44604937/get-data-from-parse-file-via-beforesave


🏁 Script executed:

# Check if there's any Parse SDK type definitions or documentation in node_modules
find node_modules/parse -name "*.d.ts" -o -name "*.md" 2>/dev/null | head -20

Repository: parse-community/parse-server

Length of output: 54


🏁 Script executed:

# Look at the Parse.File usage pattern more carefully - what fields are publicly documented?
cat node_modules/parse/package.json 2>/dev/null | jq '.version, .main, .types'

Repository: parse-community/parse-server

Length of output: 54


Reliance on undocumented Parse.File internals (_source, _data) creates maintenance risk.

This code directly accesses fileObject.file._source.file, fileObject.file._source.type, and fileObject.file._data—all internal implementation details of Parse.File not exposed in the public API. The Parse SDK web search confirms these are undocumented and not guaranteed stable across versions. Even with Parse SDK pinned at 8.0.3, a future minor/patch update could restructure these internals without breaking semver contracts, silently breaking file uploads.

Consider extracting file data through documented APIs where available, or document the tight coupling to Parse SDK internals with a comment explaining why these private fields are required (e.g., Parse.File.getData() requires saved files, which is not applicable in this upload context).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Routers/FilesRouter.js` around lines 234 - 245, The code is directly
accessing Parse.File internals (fileObject.file._source and _data) which are
undocumented and brittle; update the upload flow to avoid these private fields
by either (1) passing the original raw file (Blob or Buffer) into the request
payload or into fileObject (e.g., fileObject.rawFile) so this router can read
size and data from that documented source, or (2) if the Parse.File is already
saved, use documented APIs (e.g., fetch the file via fileObject.file.url() over
HTTP) to obtain bytes instead of reading _source/_data; if neither is possible
add a concise comment on why a private field is used and implement a guarded
fallback that throws a clear error when those internals are missing. Ensure
changes touch the code paths that set fileData/fileObject.fileSize and reference
symbols fileObject.file, fileObject.fileSize, and fileData so the coupling to
Parse.File internals is removed or clearly documented and fail-safe.

// prepare file options
const fileOptions = {
metadata: fileObject.file._metadata,
Expand All @@ -228,7 +252,7 @@ export class FilesRouter {
const createFileResult = await filesController.createFile(
config,
fileObject.file._name,
bufferData,
fileData,
fileObject.file._source.type,
fileOptions
);
Expand Down