diff --git a/lib/uploader.js b/lib/uploader.js index 7f224ff9..7c1264cd 100644 --- a/lib/uploader.js +++ b/lib/uploader.js @@ -52,11 +52,112 @@ exports.unsigned_upload = function unsigned_upload(file, upload_preset, callback }; exports.upload = function upload(file, callback, options = {}) { + callback = typeof callback === "function" ? callback : function () { + }; + + if (isBlob(file)) { + let uploadSourcePromise = getBlobArrayBuffer(file).then((arrayBuffer) => toUploadSource(Buffer.from(arrayBuffer), options, { + contentType: file.type, + filename: file.name + })); + + if (options.disable_promises) { + uploadSourcePromise.then((uploadSource) => call_upload_api(uploadSource, callback, options)).catch((error) => callback({ + error + })); + return; + } + + return uploadSourcePromise.then((uploadSource) => call_upload_api(uploadSource, callback, options)).catch((error) => { + callback({ + error + }); + throw error; + }); + } + + if (isUploadData(file)) { + file = toUploadSource(file, options); + } + + return call_upload_api(file, callback, options); +}; + +function call_upload_api(file, callback, options) { return call_api("upload", callback, options, function () { let params = build_upload_params(options); return isRemoteUrl(file) ? [params, { file: file }] : [params, {}, file]; }); -}; +} + +function isUint8Array(file) { + return typeof Uint8Array !== "undefined" && file instanceof Uint8Array; +} + +function isArrayBuffer(file) { + return typeof ArrayBuffer !== "undefined" && file instanceof ArrayBuffer; +} + +function isBlob(file) { + return typeof Blob !== "undefined" && file instanceof Blob; +} + +function getBlobArrayBuffer(file) { + if (typeof file.arrayBuffer === "function") { + return file.arrayBuffer(); + } + + let blobBuffer = getBlobBuffer(file); + if (blobBuffer) { + return Promise.resolve(blobBuffer.buffer.slice(blobBuffer.byteOffset, blobBuffer.byteOffset + blobBuffer.byteLength)); + } + + if (typeof FileReader !== "undefined") { + return new Promise((resolve, reject) => { + let reader = new FileReader(); + reader.onload = function () { + resolve(reader.result); + }; + reader.onerror = function () { + reject(reader.error || new Error("Failed to read the Blob")); + }; + reader.readAsArrayBuffer(file); + }); + } + + return Promise.reject(new Error("Blob upload requires Blob.arrayBuffer() or FileReader support")); +} + +function getBlobBuffer(file) { + if (typeof Object.getOwnPropertySymbols !== "function") { + return null; + } + + for (let symbol of Object.getOwnPropertySymbols(file)) { + let implementation = file[symbol]; + if (implementation && Buffer.isBuffer(implementation._buffer)) { + return implementation._buffer; + } + } + + return null; +} + +function isUploadData(file) { + return Buffer.isBuffer(file) || isUint8Array(file) || isArrayBuffer(file); +} + +function isUploadSource(file) { + return isObject(file) && Buffer.isBuffer(file.data); +} + +function toUploadSource(file, options = {}, extra = {}) { + return { + data: Buffer.isBuffer(file) ? file : Buffer.from(file), + filename: options.filename || extra.filename || "file", + contentType: extra.contentType || "application/octet-stream" + }; +} exports.upload_large = function upload_large(path, callback, options = {}) { if ((path != null) && isRemoteUrl(path)) { @@ -566,9 +667,8 @@ function post(url, post_data, boundary, file, callback, options) { let finish_buffer = Buffer.from("--" + boundary + "--", 'ascii'); let oauth_token = options.oauth_token || config().oauth_token; if ((file != null) || options.stream) { - // eslint-disable-next-line no-nested-ternary - let filename = options.stream ? options.filename ? options.filename : "file" : basename(file); - file_header = Buffer.from(encodeFilePart(boundary, 'application/octet-stream', 'file', filename), 'binary'); + let { filename, contentType } = getFileUploadOptions(file, options); + file_header = Buffer.from(encodeFilePart(boundary, contentType, 'file', filename), 'binary'); } const parsedUrl = new URL(url); let post_options = { @@ -643,12 +743,16 @@ function post(url, post_data, boundary, file, callback, options) { } if (file != null) { post_request.write(file_header); - fs.createReadStream(file).on('error', function (error) { - callback({ - error: error - }); - return post_request.abort(); - }).pipe(upload_stream); + if (isUploadSource(file)) { + upload_stream.end(file.data); + } else { + fs.createReadStream(file).on('error', function (error) { + callback({ + error: error + }); + return post_request.abort(); + }).pipe(upload_stream); + } } else { post_request.write(finish_buffer); post_request.end(); @@ -656,6 +760,27 @@ function post(url, post_data, boundary, file, callback, options) { return true; } +function getFileUploadOptions(file, options = {}) { + if (options.stream) { + return { + filename: options.filename || "file", + contentType: "application/octet-stream" + }; + } + + if (isUploadSource(file)) { + return { + filename: file.filename || "file", + contentType: file.contentType || "application/octet-stream" + }; + } + + return { + filename: basename(file), + contentType: "application/octet-stream" + }; +} + function encodeFieldPart(boundary, name, value) { return [ `--${boundary}\r\n`, diff --git a/test/integration/api/uploader/uploader_spec.js b/test/integration/api/uploader/uploader_spec.js index 33268532..76927aab 100644 --- a/test/integration/api/uploader/uploader_spec.js +++ b/test/integration/api/uploader/uploader_spec.js @@ -95,6 +95,81 @@ describe("uploader", function () { expect(result.signature).to.eql(expected_signature); }); }); + describe("in-memory uploads", function () { + it("should successfully upload a Buffer", function () { + const buffer = fs.readFileSync(IMAGE_FILE); + return cloudinary.v2.uploader.upload(buffer, { + tags: UPLOAD_TAGS + }).then(function (result) { + expect(result.width).to.eql(241); + expect(result.height).to.eql(51); + expect(result.format).to.eql("png"); + }); + }); + + it("should successfully upload a Uint8Array", function () { + const uint8Array = new Uint8Array(fs.readFileSync(IMAGE_FILE)); + return cloudinary.v2.uploader.upload(uint8Array, { + tags: UPLOAD_TAGS + }).then(function (result) { + expect(result.width).to.eql(241); + expect(result.height).to.eql(51); + expect(result.format).to.eql("png"); + }); + }); + + it("should successfully upload an ArrayBuffer", function () { + const buffer = fs.readFileSync(IMAGE_FILE); + const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength); + return cloudinary.v2.uploader.upload(arrayBuffer, { + tags: UPLOAD_TAGS + }).then(function (result) { + expect(result.width).to.eql(241); + expect(result.height).to.eql(51); + expect(result.format).to.eql("png"); + }); + }); + + it("should successfully upload a Blob", function () { + const buffer = fs.readFileSync(IMAGE_FILE); + const blob = new Blob([buffer], { type: "image/png" }); + return cloudinary.v2.uploader.upload(blob, { + tags: UPLOAD_TAGS + }).then(function (result) { + expect(result.width).to.eql(241); + expect(result.height).to.eql(51); + expect(result.format).to.eql("png"); + }); + }); + + it("should upload a raw buffer when resource_type is raw", function () { + const buffer = fs.readFileSync(RAW_FILE); + return cloudinary.v2.uploader.upload(buffer, { + resource_type: "raw", + tags: UPLOAD_TAGS + }).then(function (result) { + expect(result.resource_type).to.eql("raw"); + }); + }); + + it("should send buffer uploads without reading from the filesystem", function () { + const buffer = fs.readFileSync(IMAGE_FILE); + return helper.provideMockObjects(async function (mockXHR, writeSpy) { + const createReadStreamSpy = sinon.spy(fs, "createReadStream"); + try { + await cloudinary.v2.uploader.upload(buffer, { + filename: "buffer-upload.png", + tags: UPLOAD_TAGS + }).catch(helper.ignoreApiFailure); + sinon.assert.notCalled(createReadStreamSpy); + sinon.assert.calledWith(writeSpy, sinon.match((arg) => Buffer.isBuffer(arg) && arg.equals(buffer))); + sinon.assert.calledWith(writeSpy, sinon.match((arg) => Buffer.isBuffer(arg) && arg.toString("utf8").includes('filename="buffer-upload.png"'))); + } finally { + createReadStreamSpy.restore(); + } + }); + }); + }); it("should successfully upload with metadata", function () { return helper.provideMockObjects(async function (mockXHR, writeSpy, requestSpy) { await uploadImage({ metadata: METADATA_SAMPLE_DATA }).catch(helper.ignoreApiFailure); diff --git a/types/cloudinary_ts_spec.ts b/types/cloudinary_ts_spec.ts index 6fead38a..09132700 100644 --- a/types/cloudinary_ts_spec.ts +++ b/types/cloudinary_ts_spec.ts @@ -900,6 +900,18 @@ cloudinary.v2.uploader.upload("ftp://user1:mypass@ftp.example.com/sample.jpg", console.log(result, error); }); +// $ExpectType Promise +cloudinary.v2.uploader.upload(Buffer.from("sample")); + +// $ExpectType Promise +cloudinary.v2.uploader.upload(new Uint8Array([1, 2, 3])); + +// $ExpectType Promise +cloudinary.v2.uploader.upload(new ArrayBuffer(8)); + +// $ExpectType Promise +cloudinary.v2.uploader.upload(new Blob(["sample"], {type: "text/plain"})); + // $ExpectType Promise | UploadStream cloudinary.v2.uploader.upload_large("my_large_video.mp4", { diff --git a/types/index.d.ts b/types/index.d.ts index e212d98d..9f7b975d 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1285,6 +1285,8 @@ declare module 'cloudinary' { /****************************** Upload API V2 Methods *************************************/ namespace uploader { + type UploadFile = string | Buffer | Uint8Array | ArrayBuffer | Blob; + function add_context(context: string, public_ids: string[], options?: { type?: DeliveryType, resource_type?: ResourceType @@ -1394,17 +1396,17 @@ declare module 'cloudinary' { function unsigned_image_upload_tag(field: string, upload_preset: string, options?: UploadApiOptions): Promise; - function unsigned_upload(file: string, upload_preset: string, options?: UploadApiOptions, callback?: ResponseCallback): Promise; + function unsigned_upload(file: UploadFile, upload_preset: string, options?: UploadApiOptions, callback?: ResponseCallback): Promise; - function unsigned_upload(file: string, upload_preset: string, callback?: ResponseCallback): Promise; + function unsigned_upload(file: UploadFile, upload_preset: string, callback?: ResponseCallback): Promise; function unsigned_upload_stream(upload_preset: string, options?: UploadApiOptions, callback?: ResponseCallback): UploadStream; function unsigned_upload_stream(upload_preset: string, callback?: ResponseCallback): UploadStream; - function upload(file: string, options?: UploadApiOptions, callback?: UploadResponseCallback): Promise; + function upload(file: UploadFile, options?: UploadApiOptions, callback?: UploadResponseCallback): Promise; - function upload(file: string, callback?: UploadResponseCallback): Promise; + function upload(file: UploadFile, callback?: UploadResponseCallback): Promise; function upload_chunked(path: string, options?: UploadApiOptions, callback?: UploadResponseCallback): UploadStream;