UNPKG

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