UNPKG

17.9 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');
16
17const {
18 build_upload_params,
19 extend,
20 includes,
21 isObject,
22 isRemoteUrl,
23 merge,
24} = utils;
25
26exports.unsigned_upload_stream = function unsigned_upload_stream(upload_preset, callback, options = {}) {
27 return exports.upload_stream(callback, merge(options, {
28 unsigned: true,
29 upload_preset: upload_preset,
30 }));
31};
32
33exports.upload_stream = function upload_stream(callback, options = {}) {
34 return exports.upload(null, callback, extend({
35 stream: true,
36 }, options));
37};
38
39exports.unsigned_upload = function unsigned_upload(file, upload_preset, callback, options = {}) {
40 return exports.upload(file, callback, merge(options, {
41 unsigned: true,
42 upload_preset: upload_preset,
43 }));
44};
45
46exports.upload = function upload(file, callback, options = {}) {
47 return call_api("upload", callback, options, function () {
48 let params = build_upload_params(options);
49 return isRemoteUrl(file) ? [params, { file: file }] : [params, {}, file];
50 });
51};
52
53exports.upload_large = function upload_large(path, callback, options = {}) {
54 if ((path != null) && isRemoteUrl(path)) {
55 // upload a remote file
56 return exports.upload(path, callback, options);
57 }
58 if (path != null && !options.filename) {
59 options.filename = path.split(/(\\|\/)/g).pop().replace(/\.[^/.]+$/, "");
60 }
61 return exports.upload_chunked(path, callback, extend({
62 resource_type: 'raw',
63 }, options));
64};
65
66exports.upload_chunked = function upload_chunked(path, callback, options) {
67 let file_reader = fs.createReadStream(path);
68 let out_stream = exports.upload_chunked_stream(callback, options);
69 return file_reader.pipe(out_stream);
70};
71
72class Chunkable extends Writable {
73 constructor(options) {
74 super(options);
75 this.chunk_size = options.chunk_size != null ? options.chunk_size : 20000000;
76 this.buffer = Buffer.alloc(0);
77 this.active = true;
78 this.on('finish', () => {
79 if (this.active) {
80 this.emit('ready', this.buffer, true, function () {
81 });
82 }
83 });
84 }
85
86 _write(data, encoding, done) {
87 if (!this.active) {
88 done();
89 }
90 if (this.buffer.length + data.length <= this.chunk_size) {
91 this.buffer = Buffer.concat([this.buffer, data], this.buffer.length + data.length);
92 done();
93 } else {
94 const grab = this.chunk_size - this.buffer.length;
95 this.buffer = Buffer.concat([this.buffer, data.slice(0, grab)], this.buffer.length + grab);
96 this.emit('ready', this.buffer, false, (active) => {
97 this.active = active;
98 if (this.active) {
99 this.buffer = data.slice(grab);
100 done();
101 }
102 });
103 }
104 }
105}
106
107exports.upload_large_stream = function upload_large_stream(_unused_, callback, options = {}) {
108 return exports.upload_chunked_stream(callback, extend({
109 resource_type: 'raw',
110 }, options));
111};
112
113exports.upload_chunked_stream = function upload_chunked_stream(callback, options = {}) {
114 options = extend({}, options, {
115 stream: true,
116 });
117 options.x_unique_upload_id = utils.random_public_id();
118 let params = build_upload_params(options);
119 let chunk_size = options.chunk_size != null ? options.chunk_size : options.part_size;
120 let chunker = new Chunkable({
121 chunk_size: chunk_size,
122 });
123 let sent = 0;
124 chunker.on('ready', function (buffer, is_last, done) {
125 let chunk_start = sent;
126 sent += buffer.length;
127 options.content_range = `bytes ${chunk_start}-${sent - 1}/${(is_last ? sent : -1)}`;
128 params.timestamp = utils.timestamp();
129 let finished_part = function (result) {
130 const errorOrLast = (result.error != null) || is_last;
131 if (errorOrLast && typeof callback === "function") {
132 callback(result);
133 }
134 return done(!errorOrLast);
135 };
136 let stream = call_api("upload", finished_part, options, function () {
137 return [params, {}, buffer];
138 });
139 return stream.write(buffer, 'buffer', function () {
140 return stream.end();
141 });
142 });
143 return chunker;
144};
145
146exports.explicit = function explicit(public_id, callback, options = {}) {
147 return call_api("explicit", callback, options, function () {
148 return utils.build_explicit_api_params(public_id, options);
149 });
150};
151
152// Creates a new archive in the server and returns information in JSON format
153exports.create_archive = function create_archive(callback, options = {}, target_format = null) {
154 return call_api("generate_archive", callback, options, function () {
155 let opt = utils.archive_params(options);
156 if (target_format) {
157 opt.target_format = target_format;
158 }
159 return [opt];
160 });
161};
162
163// Creates a new zip archive in the server and returns information in JSON format
164exports.create_zip = function create_zip(callback, options = {}) {
165 return exports.create_archive(callback, options, "zip");
166};
167
168exports.destroy = function destroy(public_id, callback, options = {}) {
169 return call_api("destroy", callback, options, function () {
170 return [
171 {
172 timestamp: utils.timestamp(),
173 type: options.type,
174 invalidate: options.invalidate,
175 public_id: public_id,
176 },
177 ];
178 });
179};
180
181exports.rename = function rename(from_public_id, to_public_id, callback, options = {}) {
182 return call_api("rename", callback, options, function () {
183 return [
184 {
185 timestamp: utils.timestamp(),
186 type: options.type,
187 from_public_id: from_public_id,
188 to_public_id: to_public_id,
189 overwrite: options.overwrite,
190 invalidate: options.invalidate,
191 to_type: options.to_type,
192 },
193 ];
194 });
195};
196
197const TEXT_PARAMS = ["public_id", "font_family", "font_size", "font_color", "text_align", "font_weight", "font_style", "background", "opacity", "text_decoration", "font_hinting", "font_antialiasing"];
198
199exports.text = function text(content, callback, options = {}) {
200 return call_api("text", callback, options, function () {
201 let textParams = utils.only(options, ...TEXT_PARAMS);
202 let params = {
203 timestamp: utils.timestamp(),
204 text: content,
205 ...textParams,
206 };
207
208 return [params];
209 });
210};
211
212exports.generate_sprite = function generate_sprite(tag, callback, options = {}) {
213 return call_api("sprite", callback, options, function () {
214 const transformation = utils.generate_transformation_string(extend({}, options, {
215 fetch_format: options.format,
216 }));
217 return [
218 {
219 timestamp: utils.timestamp(),
220 tag: tag,
221 transformation: transformation,
222 async: options.async,
223 notification_url: options.notification_url,
224 },
225 ];
226 });
227};
228
229exports.multi = function multi(tag, callback, options = {}) {
230 return call_api("multi", callback, options, function () {
231 const transformation = utils.generate_transformation_string(extend({}, options));
232 return [
233 {
234 timestamp: utils.timestamp(),
235 tag: tag,
236 transformation: transformation,
237 format: options.format,
238 async: options.async,
239 notification_url: options.notification_url,
240 },
241 ];
242 });
243};
244
245exports.explode = function explode(public_id, callback, options = {}) {
246 return call_api("explode", callback, options, function () {
247 const transformation = utils.generate_transformation_string(extend({}, options));
248 return [
249 {
250 timestamp: utils.timestamp(),
251 public_id: public_id,
252 transformation: transformation,
253 format: options.format,
254 type: options.type,
255 notification_url: options.notification_url,
256 },
257 ];
258 });
259};
260
261// options may include 'exclusive' (boolean) which causes clearing this tag from all other resources
262exports.add_tag = function add_tag(tag, public_ids = [], callback, options = {}) {
263 const exclusive = utils.option_consume("exclusive", options);
264 const command = exclusive ? "set_exclusive" : "add";
265 return call_tags_api(tag, command, public_ids, callback, options);
266};
267
268exports.remove_tag = function remove_tag(tag, public_ids = [], callback, options = {}) {
269 return call_tags_api(tag, "remove", public_ids, callback, options);
270};
271
272exports.remove_all_tags = function remove_all_tags(public_ids = [], callback, options = {}) {
273 return call_tags_api(null, "remove_all", public_ids, callback, options);
274};
275
276exports.replace_tag = function replace_tag(tag, public_ids = [], callback, options = {}) {
277 return call_tags_api(tag, "replace", public_ids, callback, options);
278};
279
280function call_tags_api(tag, command, public_ids = [], callback, options = {}) {
281 return call_api("tags", callback, options, function () {
282 let params = {
283 timestamp: utils.timestamp(),
284 public_ids: utils.build_array(public_ids),
285 command: command,
286 type: options.type,
287 };
288 if (tag != null) {
289 params.tag = tag;
290 }
291 return [params];
292 });
293}
294
295exports.add_context = function add_context(context, public_ids = [], callback, options = {}) {
296 return call_context_api(context, 'add', public_ids, callback, options);
297};
298
299exports.remove_all_context = function remove_all_context(public_ids = [], callback, options = {}) {
300 return call_context_api(null, 'remove_all', public_ids, callback, options);
301};
302
303function call_context_api(context, command, public_ids = [], callback, options = {}) {
304 return call_api('context', callback, options, function () {
305 let params = {
306 timestamp: utils.timestamp(),
307 public_ids: utils.build_array(public_ids),
308 command: command,
309 type: options.type,
310 };
311 if (context != null) {
312 params.context = utils.encode_context(context);
313 }
314 return [params];
315 });
316}
317
318/**
319 * Cache (part of) the upload results.
320 * @param result
321 * @param {object} options
322 * @param {string} options.type
323 * @param {string} options.resource_type
324 */
325function cacheResults(result, { type, resource_type }) {
326 if (result.responsive_breakpoints) {
327 result.responsive_breakpoints.forEach(
328 ({ transformation,
329 url,
330 breakpoints }) => Cache.set(
331 result.public_id,
332 { type, resource_type, raw_transformation: transformation, format: extname(breakpoints[0].url).slice(1) },
333 breakpoints.map(i => i.width),
334 ),
335 );
336 }
337}
338
339
340function parseResult(buffer, res) {
341 let result = '';
342 try {
343 result = JSON.parse(buffer);
344 if (result.error && !result.error.name) {
345 result.error.name = "Error";
346 }
347 } catch (jsonError) {
348 result = {
349 error: {
350 message: `Server return invalid JSON response. Status Code ${res.statusCode}. ${jsonError}`,
351 name: "Error",
352 },
353 };
354 }
355 return result;
356}
357
358function call_api(action, callback, options, get_params) {
359 if (typeof callback !== "function") {
360 callback = function () {};
361 }
362
363 let deferred = Q.defer();
364 if (options == null) {
365 options = {};
366 }
367 let [params, unsigned_params, file] = get_params.call();
368 params = utils.process_request_params(params, options);
369 params = extend(params, unsigned_params);
370 let api_url = utils.api_url(action, options);
371 let boundary = utils.random_public_id();
372 let errorRaised = false;
373 let handle_response = function (res) {
374 // let buffer;
375 if (errorRaised) {
376
377 // Already reported
378 } else if (res.error) {
379 errorRaised = true;
380 deferred.reject(res);
381 callback(res);
382 } else if (includes([200, 400, 401, 404, 420, 500], res.statusCode)) {
383 let buffer = "";
384 res.on("data", (d) => {
385 buffer += d;
386 return buffer;
387 });
388 res.on("end", () => {
389 let result;
390 if (errorRaised) {
391 return;
392 }
393 result = parseResult(buffer, res);
394 if (result.error) {
395 result.error.http_code = res.statusCode;
396 deferred.reject(result.error);
397 } else {
398 cacheResults(result, options);
399 deferred.resolve(result);
400 }
401 callback(result);
402 });
403 res.on("error", (error) => {
404 errorRaised = true;
405 deferred.reject(error);
406 callback({ error });
407 });
408 } else {
409 let error = {
410 message: `Server returned unexpected status code - ${res.statusCode}`,
411 http_code: res.statusCode,
412 name: "UnexpectedResponse",
413 };
414 deferred.reject(error);
415 callback({ error });
416 }
417 };
418 let post_data = utils.hashToParameters(params)
419 .filter(([key, value]) => value != null)
420 .map(
421 ([key, value]) => Buffer.from(encodeFieldPart(boundary, key, value), 'utf8'),
422 );
423
424 let result = post(api_url, post_data, boundary, file, handle_response, options);
425 if (isObject(result)) {
426 return result;
427 }
428 return deferred.promise;
429}
430
431function post(url, post_data, boundary, file, callback, options) {
432 let file_header;
433 let finish_buffer = Buffer.from("--" + boundary + "--", 'ascii');
434 if ((file != null) || options.stream) {
435 // eslint-disable-next-line no-nested-ternary
436 let filename = options.stream ? options.filename ? options.filename : "file" : basename(file);
437 file_header = Buffer.from(encodeFilePart(boundary, 'application/octet-stream', 'file', filename), 'binary');
438 }
439 let post_options = urlLib.parse(url);
440 let headers = {
441 'Content-Type': `multipart/form-data; boundary=${boundary}`,
442 'User-Agent': utils.getUserAgent(),
443 };
444 if (options.content_range != null) {
445 headers['Content-Range'] = options.content_range;
446 }
447 if (options.x_unique_upload_id != null) {
448 headers['X-Unique-Upload-Id'] = options.x_unique_upload_id;
449 }
450 post_options = extend(post_options, {
451 method: 'POST',
452 headers: headers,
453 });
454 if (options.agent != null) {
455 post_options.agent = options.agent;
456 }
457 let post_request = https.request(post_options, callback);
458 let upload_stream = new UploadStream({ boundary });
459 upload_stream.pipe(post_request);
460 let timeout = false;
461 post_request.on("error", function (error) {
462 if (timeout) {
463 error = {
464 message: "Request Timeout",
465 http_code: 499,
466 name: "TimeoutError",
467 };
468 }
469 return callback({ error });
470 });
471 post_request.setTimeout(options.timeout != null ? options.timeout : 60000, function () {
472 timeout = true;
473 return post_request.abort();
474 });
475 post_data.forEach(postDatum => post_request.write(postDatum));
476 if (options.stream) {
477 post_request.write(file_header);
478 return upload_stream;
479 }
480 if (file != null) {
481 post_request.write(file_header);
482 fs.createReadStream(file).on('error', function (error) {
483 callback({
484 error: error,
485 });
486 return post_request.abort();
487 }).pipe(upload_stream);
488 } else {
489 post_request.write(finish_buffer);
490 post_request.end();
491 }
492 return true;
493}
494
495function encodeFieldPart(boundary, name, value) {
496 return [
497 `--${boundary}`,
498 `Content-Disposition: form-data; name="${name}"`,
499 '',
500 value,
501 '',
502 ].join("\r\n");
503}
504
505function encodeFilePart(boundary, type, name, filename) {
506 return [
507 `--${boundary}`,
508 `Content-Disposition: form-data; name="${name}"; filename="${filename}"`,
509 `Content-Type: ${type}`,
510 '',
511 '',
512 ].join("\r\n");
513}
514
515exports.direct_upload = function direct_upload(callback_url, options = {}) {
516 let params = build_upload_params(extend({
517 callback: callback_url,
518 }, options));
519 params = utils.process_request_params(params, options);
520 let api_url = utils.api_url("upload", options);
521 return {
522 hidden_fields: params,
523 form_attrs: {
524 action: api_url,
525 method: "POST",
526 enctype: "multipart/form-data",
527 },
528 };
529};
530
531exports.upload_tag_params = function upload_tag_params(options = {}) {
532 let params = build_upload_params(options);
533 params = utils.process_request_params(params, options);
534 return JSON.stringify(params);
535};
536
537exports.upload_url = function upload_url(options = {}) {
538 if (options.resource_type == null) {
539 options.resource_type = "auto";
540 }
541 return utils.api_url("upload", options);
542};
543
544exports.image_upload_tag = function image_upload_tag(field, options = {}) {
545 let html_options = options.html || {};
546 let tag_options = extend({
547 type: "file",
548 name: "file",
549 "data-url": exports.upload_url(options),
550 "data-form-data": exports.upload_tag_params(options),
551 "data-cloudinary-field": field,
552 "data-max-chunk-size": options.chunk_size,
553 "class": [html_options.class, "cloudinary-fileupload"].join(" "),
554 }, html_options);
555 return `<input ${utils.html_attrs(tag_options)}/>`;
556};
557
558exports.unsigned_image_upload_tag = function unsigned_image_upload_tag(field, upload_preset, options = {}) {
559 return exports.image_upload_tag(field, merge(options, {
560 unsigned: true,
561 upload_preset: upload_preset,
562 }));
563};
564
565
566/**
567 * Populates metadata fields with the given values. Existing values will be overwritten.
568 *
569 * @param {Object} metadata A list of custom metadata fields (by external_id) and the values to assign to each
570 * @param {Array} public_ids The public IDs of the resources to update
571 * @param {Function} callback Callback function
572 * @param {Object} options Configuration options
573 *
574 * @return {Object}
575 */
576exports.update_metadata = function update_metadata(metadata, public_ids, callback, options = {}) {
577 return call_api("metadata", callback, options, function () {
578 let params = {
579 metadata: utils.encode_context(metadata),
580 public_ids: utils.build_array(public_ids),
581 timestamp: utils.timestamp(),
582 type: options.type,
583 };
584 return [params];
585 });
586};