Skip to content

Commit 2a82dd5

Browse files
committed
feat: accept in-memory upload inputs
1 parent c86af98 commit 2a82dd5

4 files changed

Lines changed: 208 additions & 14 deletions

File tree

lib/uploader.js

Lines changed: 115 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,92 @@ exports.unsigned_upload = function unsigned_upload(file, upload_preset, callback
5252
};
5353

5454
exports.upload = function upload(file, callback, options = {}) {
55+
callback = typeof callback === "function" ? callback : function () {
56+
};
57+
58+
if (isBlob(file)) {
59+
let uploadSourcePromise = getBlobArrayBuffer(file).then((arrayBuffer) => toUploadSource(Buffer.from(arrayBuffer), options, {
60+
contentType: file.type,
61+
filename: file.name
62+
}));
63+
64+
if (options.disable_promises) {
65+
uploadSourcePromise.then((uploadSource) => call_upload_api(uploadSource, callback, options)).catch((error) => callback({
66+
error
67+
}));
68+
return;
69+
}
70+
71+
return uploadSourcePromise.then((uploadSource) => call_upload_api(uploadSource, callback, options)).catch((error) => {
72+
callback({
73+
error
74+
});
75+
throw error;
76+
});
77+
}
78+
79+
if (isUploadData(file)) {
80+
file = toUploadSource(file, options);
81+
}
82+
83+
return call_upload_api(file, callback, options);
84+
};
85+
86+
function call_upload_api(file, callback, options) {
5587
return call_api("upload", callback, options, function () {
5688
let params = build_upload_params(options);
5789
return isRemoteUrl(file) ? [params, { file: file }] : [params, {}, file];
5890
});
59-
};
91+
}
92+
93+
function isUint8Array(file) {
94+
return typeof Uint8Array !== "undefined" && file instanceof Uint8Array;
95+
}
96+
97+
function isArrayBuffer(file) {
98+
return typeof ArrayBuffer !== "undefined" && file instanceof ArrayBuffer;
99+
}
100+
101+
function isBlob(file) {
102+
return typeof Blob !== "undefined" && file instanceof Blob;
103+
}
104+
105+
function getBlobArrayBuffer(file) {
106+
if (typeof file.arrayBuffer === "function") {
107+
return file.arrayBuffer();
108+
}
109+
110+
if (typeof FileReader !== "undefined") {
111+
return new Promise((resolve, reject) => {
112+
let reader = new FileReader();
113+
reader.onload = function () {
114+
resolve(reader.result);
115+
};
116+
reader.onerror = function () {
117+
reject(reader.error || new Error("Failed to read the Blob"));
118+
};
119+
reader.readAsArrayBuffer(file);
120+
});
121+
}
122+
123+
return Promise.reject(new Error("Blob upload requires Blob.arrayBuffer() or FileReader support"));
124+
}
125+
126+
function isUploadData(file) {
127+
return Buffer.isBuffer(file) || isUint8Array(file) || isArrayBuffer(file);
128+
}
129+
130+
function isUploadSource(file) {
131+
return isObject(file) && Buffer.isBuffer(file.data);
132+
}
133+
134+
function toUploadSource(file, options = {}, extra = {}) {
135+
return {
136+
data: Buffer.isBuffer(file) ? file : Buffer.from(file),
137+
filename: options.filename || extra.filename || "file",
138+
contentType: extra.contentType || "application/octet-stream"
139+
};
140+
}
60141

61142
exports.upload_large = function upload_large(path, callback, options = {}) {
62143
if ((path != null) && isRemoteUrl(path)) {
@@ -566,9 +647,8 @@ function post(url, post_data, boundary, file, callback, options) {
566647
let finish_buffer = Buffer.from("--" + boundary + "--", 'ascii');
567648
let oauth_token = options.oauth_token || config().oauth_token;
568649
if ((file != null) || options.stream) {
569-
// eslint-disable-next-line no-nested-ternary
570-
let filename = options.stream ? options.filename ? options.filename : "file" : basename(file);
571-
file_header = Buffer.from(encodeFilePart(boundary, 'application/octet-stream', 'file', filename), 'binary');
650+
let { filename, contentType } = getFileUploadOptions(file, options);
651+
file_header = Buffer.from(encodeFilePart(boundary, contentType, 'file', filename), 'binary');
572652
}
573653
const parsedUrl = new URL(url);
574654
let post_options = {
@@ -643,19 +723,44 @@ function post(url, post_data, boundary, file, callback, options) {
643723
}
644724
if (file != null) {
645725
post_request.write(file_header);
646-
fs.createReadStream(file).on('error', function (error) {
647-
callback({
648-
error: error
649-
});
650-
return post_request.abort();
651-
}).pipe(upload_stream);
726+
if (isUploadSource(file)) {
727+
upload_stream.end(file.data);
728+
} else {
729+
fs.createReadStream(file).on('error', function (error) {
730+
callback({
731+
error: error
732+
});
733+
return post_request.abort();
734+
}).pipe(upload_stream);
735+
}
652736
} else {
653737
post_request.write(finish_buffer);
654738
post_request.end();
655739
}
656740
return true;
657741
}
658742

743+
function getFileUploadOptions(file, options = {}) {
744+
if (options.stream) {
745+
return {
746+
filename: options.filename || "file",
747+
contentType: "application/octet-stream"
748+
};
749+
}
750+
751+
if (isUploadSource(file)) {
752+
return {
753+
filename: file.filename || "file",
754+
contentType: file.contentType || "application/octet-stream"
755+
};
756+
}
757+
758+
return {
759+
filename: basename(file),
760+
contentType: "application/octet-stream"
761+
};
762+
}
763+
659764
function encodeFieldPart(boundary, name, value) {
660765
return [
661766
`--${boundary}\r\n`,

test/integration/api/uploader/uploader_spec.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,81 @@ describe("uploader", function () {
9595
expect(result.signature).to.eql(expected_signature);
9696
});
9797
});
98+
describe("in-memory uploads", function () {
99+
it("should successfully upload a Buffer", function () {
100+
const buffer = fs.readFileSync(IMAGE_FILE);
101+
return cloudinary.v2.uploader.upload(buffer, {
102+
tags: UPLOAD_TAGS
103+
}).then(function (result) {
104+
expect(result.width).to.eql(241);
105+
expect(result.height).to.eql(51);
106+
expect(result.format).to.eql("png");
107+
});
108+
});
109+
110+
it("should successfully upload a Uint8Array", function () {
111+
const uint8Array = new Uint8Array(fs.readFileSync(IMAGE_FILE));
112+
return cloudinary.v2.uploader.upload(uint8Array, {
113+
tags: UPLOAD_TAGS
114+
}).then(function (result) {
115+
expect(result.width).to.eql(241);
116+
expect(result.height).to.eql(51);
117+
expect(result.format).to.eql("png");
118+
});
119+
});
120+
121+
it("should successfully upload an ArrayBuffer", function () {
122+
const buffer = fs.readFileSync(IMAGE_FILE);
123+
const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
124+
return cloudinary.v2.uploader.upload(arrayBuffer, {
125+
tags: UPLOAD_TAGS
126+
}).then(function (result) {
127+
expect(result.width).to.eql(241);
128+
expect(result.height).to.eql(51);
129+
expect(result.format).to.eql("png");
130+
});
131+
});
132+
133+
it("should successfully upload a Blob", function () {
134+
const buffer = fs.readFileSync(IMAGE_FILE);
135+
const blob = new Blob([buffer], { type: "image/png" });
136+
return cloudinary.v2.uploader.upload(blob, {
137+
tags: UPLOAD_TAGS
138+
}).then(function (result) {
139+
expect(result.width).to.eql(241);
140+
expect(result.height).to.eql(51);
141+
expect(result.format).to.eql("png");
142+
});
143+
});
144+
145+
it("should upload a raw buffer when resource_type is raw", function () {
146+
const buffer = fs.readFileSync(RAW_FILE);
147+
return cloudinary.v2.uploader.upload(buffer, {
148+
resource_type: "raw",
149+
tags: UPLOAD_TAGS
150+
}).then(function (result) {
151+
expect(result.resource_type).to.eql("raw");
152+
});
153+
});
154+
155+
it("should send buffer uploads without reading from the filesystem", function () {
156+
const buffer = fs.readFileSync(IMAGE_FILE);
157+
return helper.provideMockObjects(async function (mockXHR, writeSpy) {
158+
const createReadStreamSpy = sinon.spy(fs, "createReadStream");
159+
try {
160+
await cloudinary.v2.uploader.upload(buffer, {
161+
filename: "buffer-upload.png",
162+
tags: UPLOAD_TAGS
163+
}).catch(helper.ignoreApiFailure);
164+
sinon.assert.notCalled(createReadStreamSpy);
165+
sinon.assert.calledWith(writeSpy, sinon.match((arg) => Buffer.isBuffer(arg) && arg.equals(buffer)));
166+
sinon.assert.calledWith(writeSpy, sinon.match((arg) => Buffer.isBuffer(arg) && arg.toString("utf8").includes('filename="buffer-upload.png"')));
167+
} finally {
168+
createReadStreamSpy.restore();
169+
}
170+
});
171+
});
172+
});
98173
it("should successfully upload with metadata", function () {
99174
return helper.provideMockObjects(async function (mockXHR, writeSpy, requestSpy) {
100175
await uploadImage({ metadata: METADATA_SAMPLE_DATA }).catch(helper.ignoreApiFailure);

types/cloudinary_ts_spec.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -900,6 +900,18 @@ cloudinary.v2.uploader.upload("ftp://user1:mypass@ftp.example.com/sample.jpg",
900900
console.log(result, error);
901901
});
902902

903+
// $ExpectType Promise<UploadApiResponse>
904+
cloudinary.v2.uploader.upload(Buffer.from("sample"));
905+
906+
// $ExpectType Promise<UploadApiResponse>
907+
cloudinary.v2.uploader.upload(new Uint8Array([1, 2, 3]));
908+
909+
// $ExpectType Promise<UploadApiResponse>
910+
cloudinary.v2.uploader.upload(new ArrayBuffer(8));
911+
912+
// $ExpectType Promise<UploadApiResponse>
913+
cloudinary.v2.uploader.upload(new Blob(["sample"], {type: "text/plain"}));
914+
903915
// $ExpectType Promise<UploadApiResponse> | UploadStream
904916
cloudinary.v2.uploader.upload_large("my_large_video.mp4",
905917
{

types/index.d.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1285,6 +1285,8 @@ declare module 'cloudinary' {
12851285
/****************************** Upload API V2 Methods *************************************/
12861286

12871287
namespace uploader {
1288+
type UploadFile = string | Buffer | Uint8Array | ArrayBuffer | Blob;
1289+
12881290
function add_context(context: string, public_ids: string[], options?: {
12891291
type?: DeliveryType,
12901292
resource_type?: ResourceType
@@ -1394,17 +1396,17 @@ declare module 'cloudinary' {
13941396

13951397
function unsigned_image_upload_tag(field: string, upload_preset: string, options?: UploadApiOptions): Promise<any>;
13961398

1397-
function unsigned_upload(file: string, upload_preset: string, options?: UploadApiOptions, callback?: ResponseCallback): Promise<any>;
1399+
function unsigned_upload(file: UploadFile, upload_preset: string, options?: UploadApiOptions, callback?: ResponseCallback): Promise<any>;
13981400

1399-
function unsigned_upload(file: string, upload_preset: string, callback?: ResponseCallback): Promise<any>;
1401+
function unsigned_upload(file: UploadFile, upload_preset: string, callback?: ResponseCallback): Promise<any>;
14001402

14011403
function unsigned_upload_stream(upload_preset: string, options?: UploadApiOptions, callback?: ResponseCallback): UploadStream;
14021404

14031405
function unsigned_upload_stream(upload_preset: string, callback?: ResponseCallback): UploadStream;
14041406

1405-
function upload(file: string, options?: UploadApiOptions, callback?: UploadResponseCallback): Promise<UploadApiResponse>;
1407+
function upload(file: UploadFile, options?: UploadApiOptions, callback?: UploadResponseCallback): Promise<UploadApiResponse>;
14061408

1407-
function upload(file: string, callback?: UploadResponseCallback): Promise<UploadApiResponse>;
1409+
function upload(file: UploadFile, callback?: UploadResponseCallback): Promise<UploadApiResponse>;
14081410

14091411
function upload_chunked(path: string, options?: UploadApiOptions, callback?: UploadResponseCallback): UploadStream;
14101412

0 commit comments

Comments
 (0)