UNPKG

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