Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Visual search file upload #635

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 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
17 changes: 16 additions & 1 deletion lib-es5/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

var utils = require("./utils");
var call_api = require("./api_client/call_api");
var fs = require("fs");

var extend = utils.extend,
pickOnlyExistingValues = utils.pickOnlyExistingValues;
Expand Down Expand Up @@ -588,10 +589,24 @@ exports.search = function search(params, callback) {
return call_api("post", "resources/search", params, callback, options);
};

function handleImageFile(image_file) {
if (Buffer.isBuffer(image_file)) {
return image_file;
}
if (typeof image_file === 'string') {
return fs.createReadStream(image_file);
}
throw new Error('image_file has to be either a path to file or a buffer');
}

exports.visual_search = function visual_search(params, callback) {
var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};

var allowedParams = pickOnlyExistingValues(params, 'image_url', 'image_asset_id', 'text');
var allowedParams = pickOnlyExistingValues(params, 'image_url', 'image_asset_id', 'text', 'image_file');
if ('image_file' in allowedParams) {
var imageFileData = handleImageFile(allowedParams.image_file);
return call_api('post', ['resources', 'visual_search'], { image_file: imageFileData }, callback, options);
}
return call_api('get', ['resources', 'visual_search'], allowedParams, callback, options);
};

Expand Down
62 changes: 57 additions & 5 deletions lib-es5/api_client/execute_request.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ var Q = require('q');
var url = require('url');
var utils = require("../utils");
var ensureOption = require('../utils/ensureOption').defaults(config());
var request = require('request');

var extend = utils.extend,
includes = utils.includes,
Expand All @@ -18,6 +19,50 @@ var extend = utils.extend,

var agent = config.api_proxy ? new https.Agent(config.api_proxy) : null;

function requestWithUpload(uploadConfig, file, callback) {
var options = _extends({
method: 'POST',
url: uploadConfig.api_url,
formData: {
image_file: file
}
}, uploadConfig.auth.oauth_token && { headers: { 'Authorization': `Bearer ${uploadConfig.auth.oauth_token}` } }, uploadConfig.auth.key && uploadConfig.auth.secret && {
auth: {
user: uploadConfig.auth.key,
pass: uploadConfig.auth.secret
}
});

if (typeof callback === "function") {
return request(options, function (error, response, body) {
if (error) {
return callback(error);
}
return callback({
statusCode: response.statusCode,
body: JSON.parse(body)
});
});
}

return new Promise(function (resolve, reject) {
var req = request(options, function (error, response, body) {
if (error) {
reject(error);
} else {
resolve({
statusCode: response.statusCode,
body: JSON.parse(body)
});
}
});

req.on('error', function (error) {
reject(error);
});
});
}

function execute_request(method, params, auth, api_url, callback) {
var options = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : {};

Expand All @@ -31,6 +76,13 @@ function execute_request(method, params, auth, api_url, callback) {
var oauth_token = auth.oauth_token;
var content_type = 'application/x-www-form-urlencoded';

if (params.image_file && method.toLowerCase() === 'post') {
return requestWithUpload({
api_url,
auth
}, params.image_file, callback);
}

if (options.content_type === 'json') {
query_params = JSON.stringify(params);
content_type = 'application/json';
Expand Down Expand Up @@ -164,16 +216,16 @@ function execute_request(method, params, auth, api_url, callback) {
}
};

var request = https.request(request_options, handle_response);
request.on("error", function (e) {
var req = https.request(request_options, handle_response);
req.on("error", function (e) {
deferred.reject(e);
return typeof callback === "function" ? callback({ error: e }) : void 0;
});
request.setTimeout(ensureOption(options, "timeout", 60000));
req.setTimeout(ensureOption(options, "timeout", 60000));
if (method !== "GET") {
request.write(query_params);
req.write(query_params);
}
request.end();
req.end();
return deferred.promise;
}

Expand Down
17 changes: 16 additions & 1 deletion lib/api.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const utils = require("./utils");
const call_api = require("./api_client/call_api");
const fs = require("fs");

const {
extend,
Expand Down Expand Up @@ -439,8 +440,22 @@ exports.search = function search(params, callback, options = {}) {
return call_api("post", "resources/search", params, callback, options);
};

function handleImageFile(image_file) {
if (Buffer.isBuffer(image_file)) {
return image_file;
}
if (typeof image_file === 'string') {
return fs.createReadStream(image_file);
}
throw new Error('image_file has to be either a path to file or a buffer');
}

exports.visual_search = function visual_search(params, callback, options = {}) {
const allowedParams = pickOnlyExistingValues(params, 'image_url', 'image_asset_id', 'text');
const allowedParams = pickOnlyExistingValues(params, 'image_url', 'image_asset_id', 'text', 'image_file');
if ('image_file' in allowedParams) {
const imageFileData = handleImageFile(allowedParams.image_file);
return call_api('post', ['resources', 'visual_search'], {image_file: imageFileData}, callback, options);
}
return call_api('get', ['resources', 'visual_search'], allowedParams, callback, options);
};

Expand Down
82 changes: 72 additions & 10 deletions lib/api_client/execute_request.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,62 @@ const Q = require('q');
const url = require('url');
const utils = require("../utils");
const ensureOption = require('../utils/ensureOption').defaults(config());
const request = require('request');
cloudinary-pkoniu marked this conversation as resolved.
Show resolved Hide resolved

const { extend, includes, isEmpty } = utils;
const {
extend,
includes,
isEmpty
} = utils;

const agent = config.api_proxy ? new https.Agent(config.api_proxy) : null;

function requestWithUpload(uploadConfig, file, callback) {
const options = {
method: 'POST',
url: uploadConfig.api_url,
formData: {
image_file: file
},
...(uploadConfig.auth.oauth_token && {headers: {'Authorization': `Bearer ${uploadConfig.auth.oauth_token}`}}),
...(uploadConfig.auth.key && uploadConfig.auth.secret && {
auth: {
user: uploadConfig.auth.key,
pass: uploadConfig.auth.secret
}
})
};

if (typeof callback === "function") {
return request(options, (error, response, body) => {
if (error) {
return callback(error);
}
return callback({
statusCode: response.statusCode,
body: JSON.parse(body)
});
})
}

return new Promise((resolve, reject) => {
const req = request(options, (error, response, body) => {
if (error) {
reject(error);
} else {
resolve({
statusCode: response.statusCode,
body: JSON.parse(body)
});
}
});

req.on('error', (error) => {
reject(error);
});
});
}

function execute_request(method, params, auth, api_url, callback, options = {}) {
method = method.toUpperCase();
const deferred = Q.defer();
Expand All @@ -21,6 +72,13 @@ function execute_request(method, params, auth, api_url, callback, options = {})
let oauth_token = auth.oauth_token;
let content_type = 'application/x-www-form-urlencoded';

if (params.image_file && method.toLowerCase() === 'post') {
cloudinary-pkoniu marked this conversation as resolved.
Show resolved Hide resolved
return requestWithUpload({
api_url,
auth
}, params.image_file, callback);
}

if (options.content_type === 'json') {
query_params = JSON.stringify(params);
content_type = 'application/json';
Expand Down Expand Up @@ -69,9 +127,13 @@ function execute_request(method, params, auth, api_url, callback, options = {})
const {hide_sensitive = false} = config();
const sanitizedOptions = {...request_options};

if (hide_sensitive === true){
if ("auth" in sanitizedOptions) { delete sanitizedOptions.auth; }
if ("Authorization" in sanitizedOptions.headers) { delete sanitizedOptions.headers.Authorization; }
if (hide_sensitive === true) {
if ("auth" in sanitizedOptions) {
delete sanitizedOptions.auth;
}
if ("Authorization" in sanitizedOptions.headers) {
delete sanitizedOptions.headers.Authorization;
}
}

if (includes([200, 400, 401, 403, 404, 409, 420, 500], res.statusCode)) {
Expand Down Expand Up @@ -147,16 +209,16 @@ function execute_request(method, params, auth, api_url, callback, options = {})
}
};

const request = https.request(request_options, handle_response);
request.on("error", function (e) {
const req = https.request(request_options, handle_response);
req.on("error", function (e) {
deferred.reject(e);
return typeof callback === "function" ? callback({ error: e }) : void 0;
return typeof callback === "function" ? callback({error: e}) : void 0;
});
request.setTimeout(ensureOption(options, "timeout", 60000));
req.setTimeout(ensureOption(options, "timeout", 60000));
if (method !== "GET") {
request.write(query_params);
req.write(query_params);
}
request.end();
req.end();
return deferred.promise;
}

Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@
"dependencies": {
"cloudinary-core": "^2.13.0",
"core-js": "^3.30.1",
"form-data": "^4.0.0",
"lodash": "^4.17.21",
"q": "^1.5.1"
"q": "^1.5.1",
"request": "^2.88.2"
},
"devDependencies": {
"@types/expect.js": "^0.3.29",
"@types/mocha": "^7.0.2",
"@types/node": "^13.5.0",
"@types/expect.js": "^0.3.29",
"babel-cli": "^6.26.0",
"babel-core": "^6.26.3",
"babel-plugin-transform-runtime": "^6.23.0",
Expand Down
23 changes: 20 additions & 3 deletions test/integration/api/search/visual_search_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ const helper = require('../../../spechelper');
const cloudinary = require('../../../../cloudinary');
const {
strictEqual,
deepStrictEqual
deepStrictEqual,
throws
} = require('assert');
const {TEST_CLOUD_NAME} = require('../../../testUtils/testConstants');

Expand All @@ -17,7 +18,7 @@ describe('Visual search', () => {
});
});

it('should pass the image_url parameter to the api call', () => {
it('should pass the image_asset_id parameter to the api call', () => {
return helper.provideMockObjects((mockXHR, writeSpy, requestSpy) => {
cloudinary.v2.api.visual_search({image_asset_id: 'image-asset-id'});

Expand All @@ -27,7 +28,7 @@ describe('Visual search', () => {
});
});

it('should pass the image_url parameter to the api call', () => {
it('should pass the text parameter to the api call', () => {
return helper.provideMockObjects((mockXHR, writeSpy, requestSpy) => {
cloudinary.v2.api.visual_search({text: 'visual-search-input'});

Expand All @@ -36,4 +37,20 @@ describe('Visual search', () => {
strictEqual(calledWithUrl.path, `/v1_1/${TEST_CLOUD_NAME}/resources/visual_search?text=visual-search-input`);
});
});

describe('with image_file', () => {
it('should allow uploading the file to get search results', async () => {
// todo: once migrated to DI, assert that image_file is passed, don't do actual http request
const searchResult = await cloudinary.v2.api.visual_search({image_file: `${__dirname}/../../../.resources/sample.jpg`});
deepStrictEqual(searchResult.statusCode, 200);
});

it('should throw an error if parameter is not a path or buffer', () => {
try {
cloudinary.v2.api.visual_search({image_file: 420})
} catch (error) {
strictEqual(error.message, 'image_file has to be either a path to file or a buffer');
}
});
});
});
10 changes: 10 additions & 0 deletions types/cloudinary_ts_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,16 @@ cloudinary.v2.api.visual_search({text: 'visual-search-input'}, function (error,
console.log(result);
});

// $ExpectType Promise<any>
cloudinary.v2.api.visual_search({image_file: 'path/to/file'}, function (error, result) {
console.log(result);
});

// $ExpectType Promise<any>
cloudinary.v2.api.visual_search({image_file: Buffer.from('abc')}, function (error, result) {
console.log(result);
});

// $ExpectType Promise<any>
cloudinary.v2.api.transformation({width: 150, height: 100, crop: 'fill'},
function (error, result) {
Expand Down
2 changes: 1 addition & 1 deletion types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,7 @@ declare module 'cloudinary' {
[futureKey: string]: any;
}

export type VisualSearchParams = { image_url: string } | { image_asset_id: string } | { text: string };
export type VisualSearchParams = { image_url: string } | { image_asset_id: string } | { text: string } | { image_file: string | Buffer };

export interface ArchiveApiOptions {
allow_missing?: boolean;
Expand Down