# Copyright 2012 Jason Woofenden -- GPLv3 fs = require 'fs' https = require 'https' async = require 'async' # SETTINGS HOST = 'api.appfog.com' PORT = 443 # multipart/form-data isn't supposed to be chunk encoded, so we calculate the # length, and generate the textual parts as we go multipart_prepare = (boundary, data, callback) -> arr = [] # async doesn't seem to work on dictionaries so convert to an array for k, v of data arr.push [k, v] async.map arr, ([k,v], callback) -> out = "--#{boundary}\r\nContent-Disposition: form-data; name=\"#{k}\"" if v.file out += "; filename=\"#{v.file}\"" out += "\r\n" if v instanceof Object if v['Content-Type']? ct = v['Content-Type'] else if v.file? ct = 'application/octet-stream' else ct = 'text/plain;charset=UTF-8' out += "Content-Type: #{ct}\r\n\r\n" if v.file? fs.stat v.file, (err, stat) -> return callback err if err? callback null, text: out, file: v.file, file_length: stat.size else callback null, text: out else #out += "Content-Type: text/plain;charset=UTF-8\r\n\r\n" #out += "Content-Type: text/plain\r\n\r\n" out += "\r\n" out += v.toString() callback null, text: out (err, parts) -> return callback err if err parts.push text: "--#{boundary}--" size = 0 for part in parts size += part.text.length if part.file_length? size += part.file_length size += 2 callback null, size, parts # encode and print data (as created by multipart_prepare()) to writable stream # req (and end() it) multipart_write = (req, parts, callback) -> async.forEachSeries parts, (part, callback) -> req.write part.text if part.file? done = false file = fs.createReadStream part.file file.on 'error', (err) -> unless done done = true callback err file.on 'end', -> unless done done = true req.write '\r\n' callback() file.pipe req, end: false return else req.write '\r\n' callback() (err) -> req.end() return callback err if err callback() new_multipart_boundary = -> return "----bmawvch#{Math.floor(Math.random() * 1000000000)}m#{Math.floor(Math.random() * 1000000000)}9rch48dh" request = (method, path, content_type, data, token, callback) -> opts = host: HOST port: PORT path: path method: method headers: 'Content-Type': content_type opts.headers['Authorization'] = token if token? opts.headers['Accept'] = '*/*;q=0.8' async.waterfall [ (callback) -> if content_type is 'multipart/form-data' boundary = new_multipart_boundary() opts.headers['Content-Type'] = "#{content_type}; boundary=#{boundary}" multipart_prepare boundary, data, (err, size, parts) -> return callback err if err opts.headers['Content-Length'] = size callback null, parts else opts.headers['Content-Length'] = data?.length ? 0 callback null, data (data, callback) -> called = false req = https.request opts, (res) -> res.setEncoding 'utf8' response_text = '' res.on 'data', (data) -> response_text += data res.on 'end', -> unless called called = true if res.statusCode isnt 200 callback code: res.statusCode, path: path, response: response_text else callback null, response_text req.on 'error', (err) -> unless called called = true callback err if content_type is 'multipart/form-data' multipart_write req, data, (err) -> if err? unless called called = true callback err req.end() else req.write data if data? req.end() ], callback # send json # try parsing result as json, and fall back to returning string json_request = (method, path, data, token, callback) -> if data? data = JSON.stringify data request method, path, 'application/json', data, token, (err, response) -> return callback err if err return callback null, '' if response is ' ' return callback null, '' if response is '' try callback null, JSON.parse(response) catch error # some api calls return plain text callback null, response json_get = (path, token, callback) -> json_request 'GET', path, null, token, callback json_post = (path, data, token, callback) -> json_request 'POST', path, data, token, callback json_put = (path, data, token, callback) -> json_request 'PUT', path, data, token, callback exports.login = login = (username, password, callback) -> json_post "/users/#{username}/tokens", {password: password}, null, (err, response) -> return callback err if err if response.token?.length callback null, response.token else callback "login: couldn't find the token in the server response: #{JSON.stringify response}" exports.app_update_files = (token, app_name, zip_file, callback) -> request( 'POST', "/apps/#{app_name}/application", 'multipart/form-data', { _method: 'put', resources: '[]', application: file: zip_file, "Content-Type": 'application/octet-stream' }, token, callback ) exports.app_set_info = (token, app_name, info, callback) -> json_put "/apps/#{app_name}", info, token, callback app_get = (path) -> return (token, app_name, callback) -> json_get "/apps/#{app_name}#{path}", token, callback exports.app_instances = app_get '/instances' exports.app_crashes = app_get '/crashes' exports.app_info = app_get '' exports.app_logs = app_get '/instances/0/files/logs' exports.app_stdout = app_get '/instances/0/files/logs/stdout.log' exports.app_stderr = app_get '/instances/0/files/logs/stderr.log' exports.app_files = app_get '/instances/0/files/app/app.js' exports.app_stats = app_get '/stats'