Skip to content
Open
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
145 changes: 135 additions & 10 deletions lib/uploader.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -643,19 +743,44 @@ 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();
}
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`,
Expand Down
75 changes: 75 additions & 0 deletions test/integration/api/uploader/uploader_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
12 changes: 12 additions & 0 deletions types/cloudinary_ts_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -900,6 +900,18 @@ cloudinary.v2.uploader.upload("ftp://user1:mypass@ftp.example.com/sample.jpg",
console.log(result, error);
});

// $ExpectType Promise<UploadApiResponse>
cloudinary.v2.uploader.upload(Buffer.from("sample"));

// $ExpectType Promise<UploadApiResponse>
cloudinary.v2.uploader.upload(new Uint8Array([1, 2, 3]));

// $ExpectType Promise<UploadApiResponse>
cloudinary.v2.uploader.upload(new ArrayBuffer(8));

// $ExpectType Promise<UploadApiResponse>
cloudinary.v2.uploader.upload(new Blob(["sample"], {type: "text/plain"}));

// $ExpectType Promise<UploadApiResponse> | UploadStream
cloudinary.v2.uploader.upload_large("my_large_video.mp4",
{
Expand Down
10 changes: 6 additions & 4 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1394,17 +1396,17 @@ declare module 'cloudinary' {

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

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

function unsigned_upload(file: string, upload_preset: string, callback?: ResponseCallback): Promise<any>;
function unsigned_upload(file: UploadFile, upload_preset: string, callback?: ResponseCallback): Promise<any>;

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<UploadApiResponse>;
function upload(file: UploadFile, options?: UploadApiOptions, callback?: UploadResponseCallback): Promise<UploadApiResponse>;

function upload(file: string, callback?: UploadResponseCallback): Promise<UploadApiResponse>;
function upload(file: UploadFile, callback?: UploadResponseCallback): Promise<UploadApiResponse>;

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

Expand Down