UNPKG

23.7 kBJavaScriptView Raw
1const fs = require('fs');
2const { extname, basename } = require('path');
3const Q = require('q');
4const Writable = require("stream").Writable;
5const urlLib = require('url');
6
7// eslint-disable-next-line import/order
8const { upload_prefix } = require("./config")();
9
10const isSecure = !(upload_prefix && upload_prefix.slice(0, 5) === 'http:');
11const https = isSecure ? require('https') : require('http');
12
13const Cache = require('./cache');
14const utils = require("./utils");
15const UploadStream = require('./upload_stream');
16const config = require("./config");
17const ProxyAgent = utils.optionalRequire('proxy-agent');
18const ensureOption = require('./utils/ensureOption').defaults(config());
19
20const {
21 build_upload_params,
22 extend,
23 includes,
24 isEmpty,
25 isObject,
26 isRemoteUrl,
27 merge,
28 pickOnlyExistingValues
29} = utils;
30
31exports.unsigned_upload_stream = function unsigned_upload_stream(upload_preset, callback, options = {}) {
32 return exports.upload_stream(callback, merge(options, {
33 unsigned: true,
34 upload_preset: upload_preset
35 }));
36};
37
38exports.upload_stream = function upload_stream(callback, options = {}) {
39 return exports.upload(null, callback, extend({
40 stream: true
41 }, options));
42};
43
44exports.unsigned_upload = function unsigned_upload(file, upload_preset, callback, options = {}) {
45 return exports.upload(file, callback, merge(options, {
46 unsigned: true,
47 upload_preset: upload_preset
48 }));
49};
50
51exports.upload = function upload(file, callback, options = {}) {
52 return call_api("upload", callback, options, function () {
53 let params = build_upload_params(options);
54 return isRemoteUrl(file) ? [params, { file: file }] : [params, {}, file];
55 });
56};
57
58exports.upload_large = function upload_large(path, callback, options = {}) {
59 if ((path != null) && isRemoteUrl(path)) {
60 // upload a remote file
61 return exports.upload(path, callback, options);
62 }
63 if (path != null && !options.filename) {
64 options.filename = path.split(/(\\|\/)/g).pop().replace(/\.[^/.]+$/, "");
65 }
66 return exports.upload_chunked(path, callback, extend({
67 resource_type: 'raw'
68 }, options));
69};
70
71exports.upload_chunked = function upload_chunked(path, callback, options) {
72 let file_reader = fs.createReadStream(path);
73 let out_stream = exports.upload_chunked_stream(callback, options);
74 return file_reader.pipe(out_stream);
75};
76
77class Chunkable extends Writable {
78 constructor(options) {
79 super(options);
80 this.chunk_size = options.chunk_size != null ? options.chunk_size : 20000000;
81 this.buffer = Buffer.alloc(0);
82 this.active = true;
83 this.on('finish', () => {
84 if (this.active) {
85 this.emit('ready', this.buffer, true, function () {
86 });
87 }
88 });
89 }
90
91 _write(data, encoding, done) {
92 if (!this.active) {
93 done();
94 }
95 if (this.buffer.length + data.length <= this.chunk_size) {
96 this.buffer = Buffer.concat([this.buffer, data], this.buffer.length + data.length);
97 done();
98 } else {
99 const grab = this.chunk_size - this.buffer.length;
100 this.buffer = Buffer.concat([this.buffer, data.slice(0, grab)], this.buffer.length + grab);
101 this.emit('ready', this.buffer, false, (active) => {
102 this.active = active;
103 if (this.active) {
104 this.buffer = data.slice(grab);
105 done();
106 }
107 });
108 }
109 }
110}
111
112exports.upload_large_stream = function upload_large_stream(_unused_, callback, options = {}) {
113 return exports.upload_chunked_stream(callback, extend({
114 resource_type: 'raw'
115 }, options));
116};
117
118exports.upload_chunked_stream = function upload_chunked_stream(callback, options = {}) {
119 options = extend({}, options, {
120 stream: true
121 });
122 options.x_unique_upload_id = utils.random_public_id();
123 let params = build_upload_params(options);
124 let chunk_size = options.chunk_size != null ? options.chunk_size : options.part_size;
125 let chunker = new Chunkable({
126 chunk_size: chunk_size
127 });
128 let sent = 0;
129 chunker.on('ready', function (buffer, is_last, done) {
130 let chunk_start = sent;
131 sent += buffer.length;
132 options.content_range = `bytes ${chunk_start}-${sent - 1}/${(is_last ? sent : -1)}`;
133 params.timestamp = utils.timestamp();
134 let finished_part = function (result) {
135 const errorOrLast = (result.error != null) || is_last;
136 if (errorOrLast && typeof callback === "function") {
137 callback(result);
138 }
139 return done(!errorOrLast);
140 };
141 let stream = call_api("upload", finished_part, options, function () {
142 return [params, {}, buffer];
143 });
144 return stream.write(buffer, 'buffer', function () {
145 return stream.end();
146 });
147 });
148 return chunker;
149};
150
151exports.explicit = function explicit(public_id, callback, options = {}) {
152 return call_api("explicit", callback, options, function () {
153 return utils.build_explicit_api_params(public_id, options);
154 });
155};
156
157// Creates a new archive in the server and returns information in JSON format
158exports.create_archive = function create_archive(callback, options = {}, target_format = null) {
159 return call_api("generate_archive", callback, options, function () {
160 let opt = utils.archive_params(options);
161 if (target_format) {
162 opt.target_format = target_format;
163 }
164 return [opt];
165 });
166};
167
168// Creates a new zip archive in the server and returns information in JSON format
169exports.create_zip = function create_zip(callback, options = {}) {
170 return exports.create_archive(callback, options, "zip");
171};
172
173
174exports.create_slideshow = function create_slideshow(options, callback) {
175 options.resource_type = ensureOption(options, "resource_type", "video");
176 return call_api("create_slideshow", callback, options, function () {
177 // Generate a transformation from the manifest_transformation key, which should be a valid transformation
178 const manifest_transformation = utils.generate_transformation_string(extend({}, options.manifest_transformation));
179
180 // Try to use {options.transformation} to generate a transformation (Example: options.transformation.width, options.transformation.height)
181 const transformation = utils.generate_transformation_string(extend({}, ensureOption(options, 'transformation', {})));
182
183 return [
184 {
185 timestamp: utils.timestamp(),
186 manifest_transformation: manifest_transformation,
187 upload_preset: options.upload_preset,
188 overwrite: options.overwrite,
189 public_id: options.public_id,
190 notification_url: options.notification_url,
191 manifest_json: options.manifest_json,
192 tags: options.tags,
193 transformation: transformation
194 }
195 ];
196 });
197};
198
199
200exports.destroy = function destroy(public_id, callback, options = {}) {
201 return call_api("destroy", callback, options, function () {
202 return [
203 {
204 timestamp: utils.timestamp(),
205 type: options.type,
206 invalidate: options.invalidate,
207 public_id: public_id
208 }
209 ];
210 });
211};
212
213exports.rename = function rename(from_public_id, to_public_id, callback, options = {}) {
214 return call_api("rename", callback, options, function () {
215 return [
216 {
217 timestamp: utils.timestamp(),
218 type: options.type,
219 from_public_id: from_public_id,
220 to_public_id: to_public_id,
221 overwrite: options.overwrite,
222 invalidate: options.invalidate,
223 to_type: options.to_type
224 }
225 ];
226 });
227};
228
229const TEXT_PARAMS = ["public_id", "font_family", "font_size", "font_color", "text_align", "font_weight", "font_style", "background", "opacity", "text_decoration", "font_hinting", "font_antialiasing"];
230
231exports.text = function text(content, callback, options = {}) {
232 return call_api("text", callback, options, function () {
233 let textParams = pickOnlyExistingValues(options, ...TEXT_PARAMS);
234 let params = {
235 timestamp: utils.timestamp(),
236 text: content,
237 ...textParams
238 };
239
240 return [params];
241 });
242};
243
244/**
245 * Generate a sprite by merging multiple images into a single large image for reducing network overhead and bypassing
246 * download limitations.
247 *
248 * The process produces 2 files as follows:
249 * - A single image file containing all the images with the specified tag (PNG by default).
250 * - A CSS file that includes the style class names and the location of the individual images in the sprite.
251 *
252 * @param {String|Object} tag A string specifying a tag that indicates which images to include or an object
253 * which includes options and image URLs.
254 * @param {Function} callback Callback function
255 * @param {Object} options Configuration options. If options are passed as the first parameter, this parameter
256 * should be empty
257 *
258 * @return {Object}
259 */
260exports.generate_sprite = function generate_sprite(tag, callback, options = {}) {
261 return call_api("sprite", callback, options, function () {
262 return [utils.build_multi_and_sprite_params(tag, options)];
263 });
264};
265
266
267/**
268 * Returns a signed url to download a sprite
269 *
270 * @param {String|Object} tag A string specifying a tag that indicates which images to include or an object
271 * which includes options and image URLs.
272 * @param {Object} options Configuration options. If options are passed as the first parameter, this parameter
273 * should be empty
274 *
275 * @returns {string}
276 */
277exports.download_generated_sprite = function download_generated_sprite(tag, options = {}) {
278 return utils.api_download_url("sprite", utils.build_multi_and_sprite_params(tag, options), options);
279}
280
281/**
282 * Returns a signed url to download a single animated image (GIF, PNG or WebP), video (MP4 or WebM) or a single PDF from
283 * multiple image assets.
284 *
285 * @param {String|Object} tag A string specifying a tag that indicates which images to include or an object
286 * which includes options and image URLs.
287 * @param {Object} options Configuration options. If options are passed as the first parameter, this parameter
288 * should be empty
289 *
290 * @returns {string}
291 */
292exports.download_multi = function download_multi(tag, options = {}) {
293 return utils.api_download_url("multi", utils.build_multi_and_sprite_params(tag, options), options);
294}
295
296/**
297 * Creates either a single animated image (GIF, PNG or WebP), video (MP4 or WebM) or a single PDF from multiple image
298 * assets.
299 *
300 * Each asset is included as a single frame of the resulting animated image/video, or a page of the PDF (sorted
301 * alphabetically by their Public ID).
302 *
303 * @param {String|Object} tag A string specifying a tag that indicates which images to include or an object
304 * which includes options and image URLs.
305 * @param {Function} callback Callback function
306 * @param {Object} options Configuration options. If options are passed as the first parameter, this parameter
307 * should be empty
308 *
309 * @return {Object}
310 */
311exports.multi = function multi(tag, callback, options = {}) {
312 return call_api("multi", callback, options, function () {
313 return [utils.build_multi_and_sprite_params(tag, options)];
314 });
315};
316
317exports.explode = function explode(public_id, callback, options = {}) {
318 return call_api("explode", callback, options, function () {
319 const transformation = utils.generate_transformation_string(extend({}, options));
320 return [
321 {
322 timestamp: utils.timestamp(),
323 public_id: public_id,
324 transformation: transformation,
325 format: options.format,
326 type: options.type,
327 notification_url: options.notification_url
328 }
329 ];
330 });
331};
332
333/**
334 *
335 * @param {String} tag The tag or tags to assign. Can specify multiple
336 * tags in a single string, separated by commas - "t1,t2,t3,t4,t5,t6,t7,t8,t9,t10,t11".
337 *
338 * @param {Array} public_ids A list of public IDs (up to 1000) of assets uploaded to Cloudinary.
339 *
340 * @param {Function} callback Callback function
341 *
342 * @param {Object} options Configuration options may include 'exclusive' (boolean) which causes
343 * clearing this tag from all other resources
344 * @return {Object}
345 */
346exports.add_tag = function add_tag(tag, public_ids = [], callback, options = {}) {
347 const exclusive = utils.option_consume("exclusive", options);
348 const command = exclusive ? "set_exclusive" : "add";
349 return call_tags_api(tag, command, public_ids, callback, options);
350};
351
352
353/**
354 * @param {String} tag The tag or tags to remove. Can specify multiple
355 * tags in a single string, separated by commas - "t1,t2,t3,t4,t5,t6,t7,t8,t9,t10,t11".
356 *
357 * @param {Array} public_ids A list of public IDs (up to 1000) of assets uploaded to Cloudinary.
358 *
359 * @param {Function} callback Callback function
360 *
361 * @param {Object} options Configuration options may include 'exclusive' (boolean) which causes
362 * clearing this tag from all other resources
363 * @return {Object}
364 */
365exports.remove_tag = function remove_tag(tag, public_ids = [], callback, options = {}) {
366 return call_tags_api(tag, "remove", public_ids, callback, options);
367};
368
369exports.remove_all_tags = function remove_all_tags(public_ids = [], callback, options = {}) {
370 return call_tags_api(null, "remove_all", public_ids, callback, options);
371};
372
373exports.replace_tag = function replace_tag(tag, public_ids = [], callback, options = {}) {
374 return call_tags_api(tag, "replace", public_ids, callback, options);
375};
376
377function call_tags_api(tag, command, public_ids = [], callback, options = {}) {
378 return call_api("tags", callback, options, function () {
379 let params = {
380 timestamp: utils.timestamp(),
381 public_ids: utils.build_array(public_ids),
382 command: command,
383 type: options.type
384 };
385 if (tag != null) {
386 params.tag = tag;
387 }
388 return [params];
389 });
390}
391
392exports.add_context = function add_context(context, public_ids = [], callback, options = {}) {
393 return call_context_api(context, 'add', public_ids, callback, options);
394};
395
396exports.remove_all_context = function remove_all_context(public_ids = [], callback, options = {}) {
397 return call_context_api(null, 'remove_all', public_ids, callback, options);
398};
399
400function call_context_api(context, command, public_ids = [], callback, options = {}) {
401 return call_api('context', callback, options, function () {
402 let params = {
403 timestamp: utils.timestamp(),
404 public_ids: utils.build_array(public_ids),
405 command: command,
406 type: options.type
407 };
408 if (context != null) {
409 params.context = utils.encode_context(context);
410 }
411 return [params];
412 });
413}
414
415/**
416 * Cache (part of) the upload results.
417 * @param result
418 * @param {object} options
419 * @param {string} options.type
420 * @param {string} options.resource_type
421 */
422function cacheResults(result, { type, resource_type }) {
423 if (result.responsive_breakpoints) {
424 result.responsive_breakpoints.forEach(
425 ({ transformation,
426 url,
427 breakpoints }) => Cache.set(
428 result.public_id,
429 { type, resource_type, raw_transformation: transformation, format: extname(breakpoints[0].url).slice(1) },
430 breakpoints.map(i => i.width)
431 )
432 );
433 }
434}
435
436
437function parseResult(buffer, res) {
438 let result = '';
439 try {
440 result = JSON.parse(buffer);
441 if (result.error && !result.error.name) {
442 result.error.name = "Error";
443 }
444 } catch (jsonError) {
445 result = {
446 error: {
447 message: `Server return invalid JSON response. Status Code ${res.statusCode}. ${jsonError}`,
448 name: "Error"
449 }
450 };
451 }
452 return result;
453}
454
455function call_api(action, callback, options, get_params) {
456 if (typeof callback !== "function") {
457 callback = function () {};
458 }
459
460 const USE_PROMISES = !options.disable_promises;
461
462 let deferred = Q.defer();
463 if (options == null) {
464 options = {};
465 }
466 let [params, unsigned_params, file] = get_params.call();
467 params = utils.process_request_params(params, options);
468 params = extend(params, unsigned_params);
469 let api_url = utils.api_url(action, options);
470 let boundary = utils.random_public_id();
471 let errorRaised = false;
472 let handle_response = function (res) {
473 // let buffer;
474 if (errorRaised) {
475
476 // Already reported
477 } else if (res.error) {
478 errorRaised = true;
479
480 if (USE_PROMISES) {
481 deferred.reject(res);
482 }
483 callback(res);
484 } else if (includes([200, 400, 401, 404, 420, 500], res.statusCode)) {
485 let buffer = "";
486 res.on("data", (d) => {
487 buffer += d;
488 return buffer;
489 });
490 res.on("end", () => {
491 let result;
492 if (errorRaised) {
493 return;
494 }
495 result = parseResult(buffer, res);
496 if (result.error) {
497 result.error.http_code = res.statusCode;
498 if (USE_PROMISES) {
499 deferred.reject(result.error);
500 }
501 } else {
502 cacheResults(result, options);
503 if (USE_PROMISES) {
504 deferred.resolve(result);
505 }
506 }
507 callback(result);
508 });
509 res.on("error", (error) => {
510 errorRaised = true;
511 if (USE_PROMISES) {
512 deferred.reject(error);
513 }
514 callback({ error });
515 });
516 } else {
517 let error = {
518 message: `Server returned unexpected status code - ${res.statusCode}`,
519 http_code: res.statusCode,
520 name: "UnexpectedResponse"
521 };
522 if (USE_PROMISES) {
523 deferred.reject(error);
524 }
525 callback({ error });
526 }
527 };
528 let post_data = utils.hashToParameters(params)
529 .filter(([key, value]) => value != null)
530 .map(
531 ([key, value]) => Buffer.from(encodeFieldPart(boundary, key, value), 'utf8')
532 );
533 let result = post(api_url, post_data, boundary, file, handle_response, options);
534 if (isObject(result)) {
535 return result;
536 }
537
538 if (USE_PROMISES) {
539 return deferred.promise;
540 }
541}
542
543function post(url, post_data, boundary, file, callback, options) {
544 let file_header;
545 let finish_buffer = Buffer.from("--" + boundary + "--", 'ascii');
546 let oauth_token = options.oauth_token || config().oauth_token;
547 if ((file != null) || options.stream) {
548 // eslint-disable-next-line no-nested-ternary
549 let filename = options.stream ? options.filename ? options.filename : "file" : basename(file);
550 file_header = Buffer.from(encodeFilePart(boundary, 'application/octet-stream', 'file', filename), 'binary');
551 }
552 let post_options = urlLib.parse(url);
553 let headers = {
554 'Content-Type': `multipart/form-data; boundary=${boundary}`,
555 'User-Agent': utils.getUserAgent()
556 };
557 if (options.content_range != null) {
558 headers['Content-Range'] = options.content_range;
559 }
560 if (options.x_unique_upload_id != null) {
561 headers['X-Unique-Upload-Id'] = options.x_unique_upload_id;
562 }
563 if (oauth_token != null) {
564 headers.Authorization = `Bearer ${oauth_token}`;
565 }
566
567 post_options = extend(post_options, {
568 method: 'POST',
569 headers: headers
570 });
571 if (options.agent != null) {
572 post_options.agent = options.agent;
573 }
574 let proxy = options.api_proxy || config().api_proxy;
575 if (!isEmpty(proxy)) {
576 if (!post_options.agent) {
577 if (ProxyAgent === null) {
578 throw new Error("Proxy value is set, but `proxy-agent` is not installed, please install `proxy-agent` module.")
579 }
580 post_options.agent = new ProxyAgent(proxy);
581 } else {
582 console.warn("Proxy is set, but request uses a custom agent, proxy is ignored.");
583 }
584 }
585
586 let post_request = https.request(post_options, callback);
587 let upload_stream = new UploadStream({ boundary });
588 upload_stream.pipe(post_request);
589 let timeout = false;
590 post_request.on("error", function (error) {
591 if (timeout) {
592 error = {
593 message: "Request Timeout",
594 http_code: 499,
595 name: "TimeoutError"
596 };
597 }
598 return callback({ error });
599 });
600 post_request.setTimeout(options.timeout != null ? options.timeout : 60000, function () {
601 timeout = true;
602 return post_request.abort();
603 });
604 post_data.forEach(postDatum => post_request.write(postDatum));
605 if (options.stream) {
606 post_request.write(file_header);
607 return upload_stream;
608 }
609 if (file != null) {
610 post_request.write(file_header);
611 fs.createReadStream(file).on('error', function (error) {
612 callback({
613 error: error
614 });
615 return post_request.abort();
616 }).pipe(upload_stream);
617 } else {
618 post_request.write(finish_buffer);
619 post_request.end();
620 }
621 return true;
622}
623
624function encodeFieldPart(boundary, name, value) {
625 return [
626 `--${boundary}`,
627 `Content-Disposition: form-data; name="${name}"`,
628 '',
629 value,
630 ''
631 ].join("\r\n");
632}
633
634function encodeFilePart(boundary, type, name, filename) {
635 return [
636 `--${boundary}`,
637 `Content-Disposition: form-data; name="${name}"; filename="${filename}"`,
638 `Content-Type: ${type}`,
639 '',
640 ''
641 ].join("\r\n");
642}
643
644exports.direct_upload = function direct_upload(callback_url, options = {}) {
645 let params = build_upload_params(extend({
646 callback: callback_url
647 }, options));
648 params = utils.process_request_params(params, options);
649 let api_url = utils.api_url("upload", options);
650 return {
651 hidden_fields: params,
652 form_attrs: {
653 action: api_url,
654 method: "POST",
655 enctype: "multipart/form-data"
656 }
657 };
658};
659
660exports.upload_tag_params = function upload_tag_params(options = {}) {
661 let params = build_upload_params(options);
662 params = utils.process_request_params(params, options);
663 return JSON.stringify(params);
664};
665
666exports.upload_url = function upload_url(options = {}) {
667 if (options.resource_type == null) {
668 options.resource_type = "auto";
669 }
670 return utils.api_url("upload", options);
671};
672
673exports.image_upload_tag = function image_upload_tag(field, options = {}) {
674 let html_options = options.html || {};
675 let tag_options = extend({
676 type: "file",
677 name: "file",
678 "data-url": exports.upload_url(options),
679 "data-form-data": exports.upload_tag_params(options),
680 "data-cloudinary-field": field,
681 "data-max-chunk-size": options.chunk_size,
682 "class": [html_options.class, "cloudinary-fileupload"].join(" ")
683 }, html_options);
684 return `<input ${utils.html_attrs(tag_options)}/>`;
685};
686
687exports.unsigned_image_upload_tag = function unsigned_image_upload_tag(field, upload_preset, options = {}) {
688 return exports.image_upload_tag(field, merge(options, {
689 unsigned: true,
690 upload_preset: upload_preset
691 }));
692};
693
694
695/**
696 * Populates metadata fields with the given values. Existing values will be overwritten.
697 *
698 * @param {Object} metadata A list of custom metadata fields (by external_id) and the values to assign to each
699 * @param {Array} public_ids The public IDs of the resources to update
700 * @param {Function} callback Callback function
701 * @param {Object} options Configuration options
702 *
703 * @return {Object}
704 */
705exports.update_metadata = function update_metadata(metadata, public_ids, callback, options = {}) {
706 return call_api("metadata", callback, options, function () {
707 let params = {
708 metadata: utils.encode_context(metadata),
709 public_ids: utils.build_array(public_ids),
710 timestamp: utils.timestamp(),
711 type: options.type
712 };
713 return [params];
714 });
715};