1 # Copyright 2012 Jason Woofenden -- GPLv3
4 https = require 'https'
5 async = require 'async'
8 HOST = 'api.appfog.com'
11 # multipart/form-data isn't supposed to be chunk encoded, so we calculate the
12 # length, and generate the textual parts as we go
13 multipart_prepare = (boundary, data, callback) ->
15 # async doesn't seem to work on dictionaries so convert to an array
20 out = "--#{boundary}\r\nContent-Disposition: form-data; name=\"#{k}\""
22 out += "; filename=\"#{v.file}\""
24 if v instanceof Object
26 ct = v['Content-Type']
29 ct = 'application/octet-stream'
31 ct = 'text/plain;charset=UTF-8'
32 out += "Content-Type: #{ct}\r\n\r\n"
34 fs.stat v.file, (err, stat) ->
35 return callback err if err?
36 callback null, text: out, file: v.file, file_length: stat.size
38 callback null, text: out
40 #out += "Content-Type: text/plain;charset=UTF-8\r\n\r\n"
41 #out += "Content-Type: text/plain\r\n\r\n"
44 callback null, text: out
46 return callback err if err
47 parts.push text: "--#{boundary}--"
50 size += part.text.length
52 size += part.file_length
54 callback null, size, parts
56 # encode and print data (as created by multipart_prepare()) to writable stream
58 multipart_write = (req, parts, callback) ->
59 async.forEachSeries parts,
64 file = fs.createReadStream part.file
65 file.on 'error', (err) ->
74 file.pipe req, end: false
81 return callback err if err
84 new_multipart_boundary = ->
85 return "----bmawvch#{Math.floor(Math.random() * 1000000000)}m#{Math.floor(Math.random() * 1000000000)}9rch48dh"
87 request = (method, path, content_type, data, token, callback) ->
93 headers: 'Content-Type': content_type
94 opts.headers['Authorization'] = token if token?
95 # no Accept header: -> unsupported media type
96 opts.headers['Accept'] = '*/*;q=0.8' # -> bad request (in json)
97 #opts.headers['Accept'] = 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' # -> bad request (in json) or blank 404
98 #opts.headers['Accept'] = 'application/json' -> unsupported media type
101 if content_type is 'multipart/form-data'
102 boundary = new_multipart_boundary()
103 opts.headers['Content-Type'] = "#{content_type}; boundary=#{boundary}"
104 multipart_prepare boundary, data, (err, size, parts) ->
105 return callback err if err
106 opts.headers['Content-Length'] = size
109 opts.headers['Content-Length'] = data?.length ? 0
113 req = https.request opts, (res) ->
114 res.setEncoding 'utf8'
116 res.on 'data', (data) ->
117 response_text += data
121 if res.statusCode isnt 200
122 callback code: res.statusCode, path: path, response: response_text
124 callback null, response_text
125 req.on 'error', (err) ->
129 if content_type is 'multipart/form-data'
130 multipart_write req, data, (err) ->
137 req.write data if data?
141 json_request = (method, path, data, token, callback) ->
143 data = JSON.stringify data
144 request method, path, 'application/json', data, token, (err, response) ->
145 return callback err if err
146 return callback() if response is ' '
147 return callback() if response is ''
149 callback null, JSON.parse(response)
151 callback "Error: AF server returned invalid JSON for #{method} to #{path}: \"#{response}\""
153 json_get = (path, token, callback) ->
154 json_request 'GET', path, null, token, callback
156 json_post = (path, data, token, callback) ->
157 json_request 'POST', path, data, token, callback
159 json_put = (path, data, token, callback) ->
160 json_request 'PUT', path, data, token, callback
162 exports.login = login = (username, password, callback) ->
163 json_post "/users/#{username}/tokens", {password: password}, null, (err, response) ->
164 return callback err if err
165 if response.token?.length
166 callback null, response.token
168 callback "login: couldn't find the token in the server response: #{JSON.stringify response}"
170 # this is the friendly one that updates the files then restarts the app
171 exports.app_publish = (token, app_name, zip_file, callback) ->
172 exports.app_update_files token, app_name, zip_file, (err) ->
173 return callback err if err?
174 # FIXME add "restart" call that only calls app_info once
175 exports.app_stop token, app_name, (err) ->
177 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}"
178 exports.app_start token, app_name, (err) ->
180 return callback "CRITICAL ERROR: Couldn't start the app back up! Server responded: #{err}"
183 exports.app_update_files = (token, app_name, zip_file, callback) ->
186 "/apps/#{app_name}/application",
187 'multipart/form-data',
193 "Content-Type": 'application/octet-stream'
199 exports.app_set_info = (token, app_name, info, callback) ->
200 json_put "/apps/#{app_name}", info, token, callback
202 app_set_state = (token, app_name, state, callback) ->
203 exports.app_info token, app_name, (err, info) ->
204 return callback err if err?
206 exports.app_set_info token, app_name, info, callback
208 exports.app_start = (token, app_name, callback) ->
209 app_set_state token, app_name, 'STARTED', callback
211 exports.app_stop = (token, app_name, callback) ->
212 app_set_state token, app_name, 'STOPPED', callback
216 return (token, app_name, callback) ->
217 json_get "/apps/#{app_name}#{path}", token, callback
219 exports.app_instances = app_get '/instances'
220 exports.app_crashes = app_get '/crashes'
221 exports.app_info = app_get ''
222 exports.app_logs = app_get '/instances/0/files/logs'
223 exports.app_stdout = app_get '/instances/0/files/logs/stdout.log'
224 exports.app_stderr = app_get '/instances/0/files/logs/stderr.log'
225 exports.app_files = app_get '/instances/0/files/app/app.js'
226 exports.app_stats = app_get '/stats'