UNPKG

13.7 kBJavaScriptView Raw
1config = require("./config")
2if config().upload_prefix && config().upload_prefix[0..4] == 'http:'
3 https = require('http')
4else
5 https = require('https')
6#http = require('http')
7UploadStream = require('./upload_stream')
8defaultsDeep = require('lodash/defaultsDeep')
9utils = require("./utils")
10{
11 extend
12 includes,
13 isArray,
14 isObject,
15} = utils
16fs = require('fs')
17path = require('path')
18Q = require('q')
19Writable = require("stream").Writable
20Cache = require('./cache');
21
22# Multipart support based on http://onteria.wordpress.com/2011/05/30/multipartform-data-uploads-using-node-js-and-http-request/
23build_upload_params = (options) ->
24 utils.build_upload_params(options)
25
26exports.unsigned_upload_stream = (upload_preset, callback, options = {}) ->
27 exports.upload_stream(callback, utils.merge(options, unsigned: true, upload_preset: upload_preset))
28
29exports.upload_stream = (callback, options = {}) ->
30 exports.upload(null, callback, extend({stream: true}, options))
31
32exports.unsigned_upload = (file, upload_preset, callback, options = {}) ->
33 exports.upload(file, callback, utils.merge(options, unsigned: true, upload_preset: upload_preset))
34
35exports.upload = (file, callback, options = {}) ->
36 call_api "upload", callback, options, ->
37 params = build_upload_params(options)
38 if file? && file.match(/^ftp:|^https?:|^s3:|^data:[^;]*;base64,([a-zA-Z0-9\/+\n=]+)$/)
39 [params, file: file]
40 else
41 [params, {}, file]
42
43exports.upload_large = (path, callback, options = {}) ->
44 if path? && path.match(/^https?:/)
45 exports.upload(path, callback, options)
46 else
47 exports.upload_chunked(path, callback, extend({resource_type: 'raw'}, options))
48
49exports.upload_chunked = (path, callback, options) ->
50 file_reader = fs.createReadStream(path)
51 out_stream = exports.upload_chunked_stream(callback, options)
52 return file_reader.pipe(out_stream)
53
54class Chunkable extends Writable
55 constructor: (options)->
56 super(options)
57 @chunk_size = options.chunk_size ? 20000000
58 @buffer = new Buffer(0)
59 @active = true
60 @on 'finish', () =>
61 @emit('ready', @buffer, true, ->) if @active
62
63 _write: (data, encoding, done) ->
64 return done() unless @active
65 if @buffer.length + data.length <= @chunk_size
66 @buffer = Buffer.concat([@buffer, data], @buffer.length + data.length);
67 done()
68 else
69 grab = @chunk_size - @buffer.length
70 @buffer = Buffer.concat([@buffer, data.slice(0, grab)], @buffer.length + grab)
71 @emit 'ready', @buffer, false, (@active) =>
72 if @active
73 @buffer = data.slice(grab)
74 done()
75
76exports.upload_large_stream = (_unused_, callback, options = {}) ->
77 exports.upload_chunked_stream(callback, extend({resource_type: 'raw'}, options))
78
79exports.upload_chunked_stream = (callback, options = {}) ->
80 options = extend({}, options, stream: true)
81 options.x_unique_upload_id = utils.random_public_id()
82 params = build_upload_params(options)
83
84 chunk_size = options.chunk_size ? options.part_size
85 chunker = new Chunkable(chunk_size: chunk_size)
86 sent = 0
87
88 chunker.on 'ready', (buffer, is_last, done) ->
89 chunk_start = sent
90 sent += buffer.length
91 options.content_range = "bytes #{chunk_start}-#{sent - 1}/#{if is_last then sent else -1}"
92 finished_part = (result) ->
93 if result.error? || is_last
94 callback?(result)
95 done(false)
96 else
97 done(true)
98 stream = call_api "upload", finished_part, options, ->
99 [params, {}, buffer]
100 stream.write(buffer, 'buffer', -> stream.end())
101
102 return chunker
103
104exports.explicit = (public_id, callback, options = {}) ->
105 call_api "explicit", callback, options, ->
106 utils.build_explicit_api_params(public_id, options)
107
108
109# Creates a new archive in the server and returns information in JSON format
110exports.create_archive = (callback, options = {}, target_format = null)->
111 call_api "generate_archive", callback, options, ->
112 opt = utils.archive_params(options)
113 opt.target_format = target_format if target_format
114 [opt]
115
116# Creates a new zip archive in the server and returns information in JSON format
117exports.create_zip = (callback, options = {})->
118 exports.create_archive(callback, options, "zip")
119
120exports.destroy = (public_id, callback, options = {}) ->
121 call_api "destroy", callback, options, ->
122 return [timestamp: utils.timestamp(), type: options.type, invalidate: options.invalidate, public_id: public_id]
123
124exports.rename = (from_public_id, to_public_id, callback, options = {}) ->
125 call_api "rename", callback, options, ->
126 return [
127 timestamp: utils.timestamp(),
128 type: options.type,
129 from_public_id: from_public_id,
130 to_public_id: to_public_id,
131 overwrite: options.overwrite,
132 invalidate: options.invalidate,
133 to_type: options.to_type
134 ]
135
136TEXT_PARAMS = ["public_id", "font_family", "font_size", "font_color", "text_align", "font_weight", "font_style",
137 "background", "opacity", "text_decoration"]
138exports.text = (text, callback, options = {}) ->
139 call_api "text", callback, options, ->
140 params = {timestamp: utils.timestamp(), text: text}
141 for k in TEXT_PARAMS when options[k]?
142 params[k] = options[k]
143 [params]
144
145exports.generate_sprite = (tag, callback, options = {}) ->
146 call_api "sprite", callback, options, ->
147 transformation = utils.generate_transformation_string(extend({}, options, fetch_format: options.format))
148 return [{
149 timestamp: utils.timestamp(),
150 tag: tag,
151 transformation: transformation,
152 async: options.async,
153 notification_url: options.notification_url
154 }]
155
156exports.multi = (tag, callback, options = {}) ->
157 call_api "multi", callback, options, ->
158 transformation = utils.generate_transformation_string(extend({}, options))
159 return [{
160 timestamp: utils.timestamp(),
161 tag: tag,
162 transformation: transformation,
163 format: options.format,
164 async: options.async,
165 notification_url: options.notification_url
166 }]
167
168exports.explode = (public_id, callback, options = {}) ->
169 call_api "explode", callback, options, ->
170 transformation = utils.generate_transformation_string(extend({}, options))
171 return [{
172 timestamp: utils.timestamp(),
173 public_id: public_id,
174 transformation: transformation,
175 format: options.format,
176 type: options.type,
177 notification_url: options.notification_url
178 }]
179
180# options may include 'exclusive' (boolean) which causes clearing this tag from all other resources
181exports.add_tag = (tag, public_ids = [], callback, options = {}) ->
182 exclusive = utils.option_consume("exclusive", options)
183 command = if exclusive then "set_exclusive" else "add"
184 call_tags_api(tag, command, public_ids, callback, options)
185
186exports.remove_tag = (tag, public_ids = [], callback, options = {}) ->
187 call_tags_api(tag, "remove", public_ids, callback, options)
188
189exports.remove_all_tags = (public_ids = [], callback, options = {}) ->
190 call_tags_api(null, "remove_all", public_ids, callback, options)
191
192exports.replace_tag = (tag, public_ids = [], callback, options = {}) ->
193 call_tags_api(tag, "replace", public_ids, callback, options)
194
195call_tags_api = (tag, command, public_ids = [], callback, options = {}) ->
196 call_api "tags", callback, options, ->
197 params = {
198 timestamp: utils.timestamp(),
199 public_ids: utils.build_array(public_ids),
200 command: command,
201 type: options.type
202 }
203 if tag?
204 params.tag = tag
205 return [params]
206
207exports.add_context = (context, public_ids = [], callback, options = {}) ->
208 call_context_api(context, 'add', public_ids, callback, options)
209
210exports.remove_all_context = (public_ids = [], callback, options = {}) ->
211 call_context_api(null, 'remove_all', public_ids, callback, options)
212
213call_context_api = (context, command, public_ids = [], callback, options = {}) ->
214 call_api 'context', callback, options, ->
215 params = {
216 timestamp: utils.timestamp(),
217 public_ids: utils.build_array(public_ids),
218 command: command,
219 type: options.type
220 }
221 if context?
222 params.context = utils.encode_context(context)
223 return [params]
224
225call_api = (action, callback, options, get_params) ->
226 deferred = Q.defer()
227 options ?= {}
228
229 [params, unsigned_params, file] = get_params.call()
230
231 params = utils.process_request_params(params, options)
232 params = extend(params, unsigned_params)
233
234 api_url = utils.api_url(action, options)
235
236 boundary = utils.random_public_id()
237
238 error = false
239 handle_response = (res) ->
240 if error
241# Already reported
242 else if res.error
243 error = true
244 deferred.reject(res)
245 callback?(res)
246 else if includes([200, 400, 401, 404, 420, 500], res.statusCode)
247 buffer = ""
248 res.on "data", (d) -> buffer += d
249 res.on "end", ->
250 return if error
251 try
252 result = JSON.parse(buffer)
253 catch e
254 result = {error: {message: "Server return invalid JSON response. Status Code #{res.statusCode}"}}
255 result["error"]["http_code"] = res.statusCode if result["error"]
256 if result.error
257 deferred.reject(result.error)
258 else
259 if result.responsive_breakpoints
260 result.responsive_breakpoints.forEach (bp,i)->
261 Cache.set(
262 result.public_id,
263 {type: options.type, resource_type: options.resource_type, transformation: bp.transformation},
264 bp.breakpoints.map((i)->i.width)
265 )
266 deferred.resolve(result)
267 callback?(result)
268 res.on "error", (e) ->
269 error = true
270 deferred.reject(e)
271 callback?(error: e)
272 else
273 error_obj =
274 error: {message: "Server returned unexpected status code - #{res.statusCode}", http_code: res.statusCode}
275 deferred.reject(error_obj.error)
276 callback?(error_obj)
277 post_data = []
278 for key, value of params
279 if isArray(value)
280 for v in value
281 post_data.push new Buffer(EncodeFieldPart(boundary, key + "[]", v), 'utf8')
282 else if utils.present(value)
283 post_data.push new Buffer(EncodeFieldPart(boundary, key, value), 'utf8')
284
285 result = post api_url, post_data, boundary, file, handle_response, options
286 if isObject(result)
287 return result
288 else
289 return deferred.promise
290
291post = (url, post_data, boundary, file, callback, options) ->
292 finish_buffer = new Buffer("--" + boundary + "--", 'ascii')
293 if file? || options.stream
294 filename = if options.stream then "file" else path.basename(file)
295 file_header = new Buffer(EncodeFilePart(boundary, 'application/octet-stream', 'file', filename), 'binary')
296
297 post_options = require('url').parse(url)
298 headers =
299 'Content-Type': 'multipart/form-data; boundary=' + boundary
300 'User-Agent': utils.getUserAgent()
301 headers['Content-Range'] = options.content_range if options.content_range?
302 headers['X-Unique-Upload-Id'] = options.x_unique_upload_id if options.x_unique_upload_id?
303 post_options = extend post_options,
304 method: 'POST',
305 headers: headers
306 post_options.agent = options.agent if options.agent?
307 post_request = https.request(post_options, callback)
308 upload_stream = new UploadStream({boundary: boundary})
309 upload_stream.pipe(post_request)
310 timeout = false
311 post_request.on "error", (e) ->
312 if timeout
313 callback(error: {message: "Request Timeout", http_code: 499})
314 else
315 callback(error: e)
316 post_request.setTimeout options.timeout ? 60000, ->
317 timeout = true
318 post_request.abort()
319
320 for i in [0..post_data.length - 1]
321 post_request.write(post_data[i])
322
323 if options.stream
324 post_request.write(file_header)
325 return upload_stream
326 else if file?
327 post_request.write(file_header)
328 fs.createReadStream(file)
329 .on('error', (error)->
330 callback(error: error)
331 post_request.abort()
332 ).pipe(upload_stream)
333 else
334 post_request.write(finish_buffer)
335 post_request.end()
336
337 true
338
339EncodeFieldPart = (boundary, name, value) ->
340 return_part = "--#{boundary}\r\n"
341 return_part += "Content-Disposition: form-data; name=\"#{name}\"\r\n\r\n"
342 return_part += value + "\r\n"
343 return_part
344
345EncodeFilePart = (boundary, type, name, filename) ->
346 return_part = "--#{boundary}\r\n"
347 return_part += "Content-Disposition: form-data; name=\"#{name}\"; filename=\"#{filename}\"\r\n"
348 return_part += "Content-Type: #{type}\r\n\r\n"
349 return_part
350
351exports.direct_upload = (callback_url, options = {}) ->
352 params = build_upload_params(extend({callback: callback_url}, options))
353 params = utils.process_request_params(params, options)
354 api_url = utils.api_url("upload", options)
355
356 return hidden_fields: params, form_attrs: {action: api_url, method: "POST", enctype: "multipart/form-data"}
357
358exports.upload_tag_params = (options = {}) ->
359 params = build_upload_params(options)
360 params = utils.process_request_params(params, options)
361 JSON.stringify(params)
362
363exports.upload_url = (options = {}) ->
364 options.resource_type ?= "auto"
365 utils.api_url("upload", options)
366
367exports.image_upload_tag = (field, options = {}) ->
368 html_options = options.html ? {}
369
370 tag_options = extend( {
371 type: "file",
372 name: "file",
373 "data-url": exports.upload_url(options),
374 "data-form-data": exports.upload_tag_params(options),
375 "data-cloudinary-field": field,
376 "data-max-chunk-size": options.chunk_size,
377 "class": [html_options["class"], "cloudinary-fileupload"].join(" ")
378 }, html_options)
379 return '<input ' + utils.html_attrs(tag_options) + '/>'
380
381exports.unsigned_image_upload_tag = (field, upload_preset, options = {}) ->
382 exports.image_upload_tag(field, utils.merge(options, unsigned: true, upload_preset: upload_preset))