X-Git-Url: https://jasonwoof.com/gitweb/?a=blobdiff_plain;f=api.coffee;fp=api.coffee;h=cda3905abb0d3892dad6482f8b39263ce4b56623;hb=d20d81a40aaf14801c8be6b94ff2e97dd54f15a1;hp=0000000000000000000000000000000000000000;hpb=6589bcee44f3a39963e630d053989b23fda16e9b;p=af-coffee.git diff --git a/api.coffee b/api.coffee new file mode 100644 index 0000000..cda3905 --- /dev/null +++ b/api.coffee @@ -0,0 +1,226 @@ +# 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? + # no Accept header: -> unsupported media type + opts.headers['Accept'] = '*/*;q=0.8' # -> bad request (in json) + #opts.headers['Accept'] = 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' # -> bad request (in json) or blank 404 + #opts.headers['Accept'] = 'application/json' -> unsupported media type + 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 + +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() if response is ' ' + return callback() if response is '' + try + callback null, JSON.parse(response) + catch error + callback "Error: AF server returned invalid JSON for #{method} to #{path}: \"#{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}" + +# this is the friendly one that updates the files then restarts the app +exports.app_publish = (token, app_name, zip_file, callback) -> + exports.app_update_files token, app_name, zip_file, (err) -> + return callback err if err? + # FIXME add "restart" call that only calls app_info once + exports.app_stop token, app_name, (err) -> + if err? + return callback "Couldn't stop the app (to restart it). App is probably still running old version (though perhaps accessing new files.). Server responded: #{err}" + exports.app_start token, app_name, (err) -> + if err? + return callback "CRITICAL ERROR: Couldn't start the app back up! Server responded: #{err}" + callback() + +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_set_state = (token, app_name, state, callback) -> + exports.app_info token, app_name, (err, info) -> + return callback err if err? + info.state = state + exports.app_set_info token, app_name, info, callback + +exports.app_start = (token, app_name, callback) -> + app_set_state token, app_name, 'STARTED', callback + +exports.app_stop = (token, app_name, callback) -> + app_set_state token, app_name, 'STOPPED', 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'