1 | config = require("./config")
|
2 | if config().upload_prefix && config().upload_prefix[0..4] == 'http:'
|
3 | https = require('http')
|
4 | else
|
5 | https = require('https')
|
6 | #http = require('http')
|
7 | UploadStream = require('./upload_stream')
|
8 | defaultsDeep = require('lodash/defaultsDeep')
|
9 | utils = require("./utils")
|
10 | {
|
11 | extend
|
12 | includes,
|
13 | isArray,
|
14 | isObject,
|
15 | } = utils
|
16 | fs = require('fs')
|
17 | path = require('path')
|
18 | Q = require('q')
|
19 | Writable = require("stream").Writable
|
20 | Cache = require('./cache');
|
21 |
|
22 | # Multipart support based on http://onteria.wordpress.com/2011/05/30/multipartform-data-uploads-using-node-js-and-http-request/
|
23 | build_upload_params = (options) ->
|
24 | utils.build_upload_params(options)
|
25 |
|
26 | exports.unsigned_upload_stream = (upload_preset, callback, options = {}) ->
|
27 | exports.upload_stream(callback, utils.merge(options, unsigned: true, upload_preset: upload_preset))
|
28 |
|
29 | exports.upload_stream = (callback, options = {}) ->
|
30 | exports.upload(null, callback, extend({stream: true}, options))
|
31 |
|
32 | exports.unsigned_upload = (file, upload_preset, callback, options = {}) ->
|
33 | exports.upload(file, callback, utils.merge(options, unsigned: true, upload_preset: upload_preset))
|
34 |
|
35 | exports.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 |
|
43 | exports.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 |
|
49 | exports.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 |
|
54 | class 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 |
|
76 | exports.upload_large_stream = (_unused_, callback, options = {}) ->
|
77 | exports.upload_chunked_stream(callback, extend({resource_type: 'raw'}, options))
|
78 |
|
79 | exports.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 |
|
104 | exports.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
|
110 | exports.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
|
117 | exports.create_zip = (callback, options = {})->
|
118 | exports.create_archive(callback, options, "zip")
|
119 |
|
120 | exports.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 |
|
124 | exports.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 |
|
136 | TEXT_PARAMS = ["public_id", "font_family", "font_size", "font_color", "text_align", "font_weight", "font_style",
|
137 | "background", "opacity", "text_decoration"]
|
138 | exports.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 |
|
145 | exports.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 |
|
156 | exports.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 |
|
168 | exports.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
|
181 | exports.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 |
|
186 | exports.remove_tag = (tag, public_ids = [], callback, options = {}) ->
|
187 | call_tags_api(tag, "remove", public_ids, callback, options)
|
188 |
|
189 | exports.remove_all_tags = (public_ids = [], callback, options = {}) ->
|
190 | call_tags_api(null, "remove_all", public_ids, callback, options)
|
191 |
|
192 | exports.replace_tag = (tag, public_ids = [], callback, options = {}) ->
|
193 | call_tags_api(tag, "replace", public_ids, callback, options)
|
194 |
|
195 | call_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 |
|
207 | exports.add_context = (context, public_ids = [], callback, options = {}) ->
|
208 | call_context_api(context, 'add', public_ids, callback, options)
|
209 |
|
210 | exports.remove_all_context = (public_ids = [], callback, options = {}) ->
|
211 | call_context_api(null, 'remove_all', public_ids, callback, options)
|
212 |
|
213 | call_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 |
|
225 | call_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 |
|
291 | post = (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 |
|
339 | EncodeFieldPart = (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 |
|
345 | EncodeFilePart = (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 |
|
351 | exports.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 |
|
358 | exports.upload_tag_params = (options = {}) ->
|
359 | params = build_upload_params(options)
|
360 | params = utils.process_request_params(params, options)
|
361 | JSON.stringify(params)
|
362 |
|
363 | exports.upload_url = (options = {}) ->
|
364 | options.resource_type ?= "auto"
|
365 | utils.api_url("upload", options)
|
366 |
|
367 | exports.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 |
|
381 | exports.unsigned_image_upload_tag = (field, upload_preset, options = {}) ->
|
382 | exports.image_upload_tag(field, utils.merge(options, unsigned: true, upload_preset: upload_preset))
|