Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
44 changes: 26 additions & 18 deletions src/ParseFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ class ParseFile {
* JSON encoding if metadata or tags are set.
* 6. (Node.js only) a Readable stream, or a Web ReadableStream.
* Streamed as raw binary data directly into the upload request.
* Throws if metadata or tags are set.
* Supports metadata, tags, and directory when Parse Server >= 9.5.0.
* For example:
* <pre>
* var fileUploadControl = $("#profilePhotoFileUpload")[0];
Expand Down Expand Up @@ -289,8 +289,8 @@ class ParseFile {
* In Node.js, files created with Buffer or ReadableStream are uploaded as
* raw binary data, avoiding base64 encoding overhead. If metadata
* or tags are set on a Buffer-backed file, the upload falls back to base64
* JSON encoding (since the binary endpoint does not support metadata).
* Stream-backed files with metadata or tags will throw an error.
* JSON encoding. Stream-backed files support metadata, tags, and directory
* when Parse Server >= 9.5.0.
*
* @param {object} options
* Valid options are:<ul>
Expand Down Expand Up @@ -322,24 +322,19 @@ class ParseFile {
const controller = CoreManager.getFileController();
if (!this._previousSave) {
if (this._source.format === 'buffer' || this._source.format === 'stream') {
const hasFileData =
(this._metadata && Object.keys(this._metadata).length > 0) ||
(this._tags && Object.keys(this._tags).length > 0) ||
!!this._directory;

if (this._source.format === 'stream' && hasFileData) {
throw new Error(
'Cannot save a stream-based file with metadata, tags, or directory. Use a Buffer instead.'
);
}
if (this._source.format === 'stream' && !controller.saveBinary) {
throw new Error(
'Cannot save a stream-based file without saveBinary support on the FileController.'
);
}

if (!hasFileData && controller.saveBinary) {
// Binary upload via ajax
const hasFileData =
(this._metadata && Object.keys(this._metadata).length > 0) ||
(this._tags && Object.keys(this._tags).length > 0) ||
!!this._directory;

if (controller.saveBinary && (this._source.format === 'stream' || !hasFileData)) {
// Binary upload via ajax (file data sent via headers for streams)
this._previousSave = controller
.saveBinary(this._name, this._source, options)
.then(res => {
Expand Down Expand Up @@ -469,7 +464,8 @@ class ParseFile {
}

/**
* Sets metadata to be saved with file object. Overwrites existing metadata
* Sets metadata to be saved with file object. Overwrites existing metadata.
* When used with a stream-based file, requires Parse Server >= 9.5.0.
*
* @param {object} metadata Key value pairs to be stored with file object
*/
Expand All @@ -483,6 +479,7 @@ class ParseFile {

/**
* Sets metadata to be saved with file object. Adds to existing metadata.
* When used with a stream-based file, requires Parse Server >= 9.5.0.
*
* @param {string} key key to store the metadata
* @param {*} value metadata
Expand All @@ -494,7 +491,8 @@ class ParseFile {
}

/**
* Sets tags to be saved with file object. Overwrites existing tags
* Sets tags to be saved with file object. Overwrites existing tags.
* When used with a stream-based file, requires Parse Server >= 9.5.0.
*
* @param {object} tags Key value pairs to be stored with file object
*/
Expand All @@ -508,6 +506,7 @@ class ParseFile {

/**
* Sets tags to be saved with file object. Adds to existing tags.
* When used with a stream-based file, requires Parse Server >= 9.5.0.
*
* @param {string} key key to store tags
* @param {*} value tag
Expand All @@ -521,7 +520,7 @@ class ParseFile {
/**
* Sets the directory where the file will be stored.
* Requires the Master Key when saving.
* Requires Parse Server >= 9.4.0.
* Requires Parse Server >= 9.4.0; when used with a stream-based file, requires Parse Server >= 9.5.0.
*
* @param {string} directory the directory path
*/
Expand Down Expand Up @@ -624,6 +623,15 @@ const DefaultController = {
'X-Parse-Upload-Mode': 'stream',
};
headers['Content-Type'] = (source.type || 'application/octet-stream').replace(/[\r\n]/g, '');
if (options.directory) {
headers['X-Parse-File-Directory'] = options.directory.replace(/[\r\n]/g, '');
}
if (options.metadata && Object.keys(options.metadata).length > 0) {
headers['X-Parse-File-Metadata'] = JSON.stringify(options.metadata);
}
if (options.tags && Object.keys(options.tags).length > 0) {
headers['X-Parse-File-Tags'] = JSON.stringify(options.tags);
}
const jsKey = CoreManager.get('JAVASCRIPT_KEY');
if (jsKey) {
headers['X-Parse-JavaScript-Key'] = jsKey;
Expand Down
33 changes: 22 additions & 11 deletions src/__tests__/ParseFile-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1241,19 +1241,30 @@ describe('FileController', () => {
expect(ajax).not.toHaveBeenCalled();
});

it('stream with metadata throws error', async () => {
it('stream with file data uses saveBinary with headers', async () => {
const ajax = jest.fn().mockResolvedValue({
response: { name: 'mydir/parse.txt', url: 'https://files.example.com/a/mydir/parse.txt' },
});
const request = jest.fn();
CoreManager.setRESTController({ request, ajax });
CoreManager.set('APPLICATION_ID', 'testAppId');
CoreManager.set('MASTER_KEY', 'testMasterKey');

const { Readable } = require('stream');
const stream = new Readable({ read() { this.push(null); } });
const stream = new Readable({ read() { this.push('hello'); this.push(null); } });
const file = new ParseFile('parse.txt', stream, 'text/plain');
file.addMetadata('foo', 'bar');
try {
await file.save();
expect(true).toBe(false);
} catch (e) {
expect(e.message).toBe(
'Cannot save a stream-based file with metadata, tags, or directory. Use a Buffer instead.'
);
}
file.setDirectory('mydir');
file.addMetadata('key1', 'value1');
file.addTag('tag1', 'tagValue1');
const f = await file.save({ useMasterKey: true });

expect(f).toBe(file);
expect(ajax).toHaveBeenCalled();
const [, , , headers] = ajax.mock.calls[0];
expect(headers['X-Parse-File-Directory']).toBe('mydir');
expect(headers['X-Parse-File-Metadata']).toBe('{"key1":"value1"}');
expect(headers['X-Parse-File-Tags']).toBe('{"tag1":"tagValue1"}');
expect(request).not.toHaveBeenCalled();
});

it('buffer without metadata uses saveBinary', async () => {
Expand Down
16 changes: 10 additions & 6 deletions types/ParseFile.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ declare class ParseFile {
* JSON encoding if metadata or tags are set.
* 6. (Node.js only) a Readable stream, or a Web ReadableStream.
* Streamed as raw binary data directly into the upload request.
* Throws if metadata or tags are set.
* Supports metadata, tags, and directory when Parse Server >= 9.5.0.
* For example:
* <pre>
* var fileUploadControl = $("#profilePhotoFileUpload")[0];
Expand Down Expand Up @@ -151,8 +151,8 @@ declare class ParseFile {
* In Node.js, files created with Buffer or ReadableStream are uploaded as
* raw binary data, avoiding base64 encoding overhead. If metadata
* or tags are set on a Buffer-backed file, the upload falls back to base64
* JSON encoding (since the binary endpoint does not support metadata).
* Stream-backed files with metadata or tags will throw an error.
* JSON encoding. Stream-backed files support metadata, tags, and directory
* when Parse Server >= 9.5.0.
*
* @param {object} options
* Valid options are:<ul>
Expand Down Expand Up @@ -200,26 +200,30 @@ declare class ParseFile {
};
equals(other: any): boolean;
/**
* Sets metadata to be saved with file object. Overwrites existing metadata
* Sets metadata to be saved with file object. Overwrites existing metadata.
* When used with a stream-based file, requires Parse Server >= 9.5.0.
*
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
* @param {object} metadata Key value pairs to be stored with file object
*/
setMetadata(metadata: Record<string, any>): void;
/**
* Sets metadata to be saved with file object. Adds to existing metadata.
* When used with a stream-based file, requires Parse Server >= 9.5.0.
*
* @param {string} key key to store the metadata
* @param {*} value metadata
*/
addMetadata(key: string, value: any): void;
/**
* Sets tags to be saved with file object. Overwrites existing tags
* Sets tags to be saved with file object. Overwrites existing tags.
* When used with a stream-based file, requires Parse Server >= 9.5.0.
*
* @param {object} tags Key value pairs to be stored with file object
*/
setTags(tags: Record<string, any>): void;
/**
* Sets tags to be saved with file object. Adds to existing tags.
* When used with a stream-based file, requires Parse Server >= 9.5.0.
*
* @param {string} key key to store tags
* @param {*} value tag
Expand All @@ -228,7 +232,7 @@ declare class ParseFile {
/**
* Sets the directory where the file will be stored.
* Requires the Master Key when saving.
* Requires Parse Server >= 9.4.0.
* Requires Parse Server >= 9.4.0; when used with a stream-based file, requires Parse Server >= 9.5.0.
*
* @param {string} directory the directory path
*/
Expand Down