JasonWoof Got questions, comments, patches, etc.? Contact Jason Woofenden
basic features for updating/checking one instance
[af-coffee.git] / api.coffee
1 # Copyright 2012 Jason Woofenden -- GPLv3
2
3 fs = require 'fs'
4 https = require 'https'
5 async = require 'async'
6
7 # SETTINGS
8 HOST = 'api.appfog.com'
9 PORT = 443
10
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) ->
14         arr = []
15         # async doesn't seem to work on dictionaries so convert to an array
16         for k, v of data
17                 arr.push [k, v]
18         async.map arr,
19                 ([k,v], callback) ->
20                         out = "--#{boundary}\r\nContent-Disposition: form-data; name=\"#{k}\""
21                         if v.file
22                                 out += "; filename=\"#{v.file}\""
23                         out += "\r\n"
24                         if v instanceof Object
25                                 if v['Content-Type']?
26                                         ct = v['Content-Type']
27                                 else
28                                         if v.file?
29                                                 ct = 'application/octet-stream'
30                                         else
31                                                 ct = 'text/plain;charset=UTF-8'
32                                 out += "Content-Type: #{ct}\r\n\r\n"
33                                 if v.file?
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
37                                 else
38                                         callback null, text: out
39                         else
40                                 #out += "Content-Type: text/plain;charset=UTF-8\r\n\r\n"
41                                 #out += "Content-Type: text/plain\r\n\r\n"
42                                 out += "\r\n"
43                                 out += v.toString()
44                                 callback null, text: out
45                 (err, parts) ->
46                         return callback err if err
47                         parts.push text: "--#{boundary}--"
48                         size = 0
49                         for part in parts
50                                 size += part.text.length
51                                 if part.file_length?
52                                         size += part.file_length
53                                 size += 2
54                         callback null, size, parts
55
56 # encode and print data (as created by multipart_prepare()) to writable stream
57 # req (and end() it)
58 multipart_write = (req, parts, callback) ->
59         async.forEachSeries parts,
60                 (part, callback) ->
61                         req.write part.text
62                         if part.file?
63                                 done = false
64                                 file = fs.createReadStream part.file
65                                 file.on 'error', (err) ->
66                                         unless done
67                                                 done = true
68                                                 callback err
69                                 file.on 'end', ->
70                                         unless done
71                                                 done = true
72                                                 req.write '\r\n'
73                                                 callback()
74                                 file.pipe req, end: false
75                                 return
76                         else
77                                 req.write '\r\n'
78                                 callback()
79                 (err) ->
80                         req.end()
81                         return callback err if err
82                         callback()
83
84 new_multipart_boundary = ->
85         return "----bmawvch#{Math.floor(Math.random() * 1000000000)}m#{Math.floor(Math.random() * 1000000000)}9rch48dh"
86
87 request = (method, path, content_type, data, token, callback) ->
88         opts =
89                 host: HOST
90                 port: PORT
91                 path: path
92                 method: method
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
99         async.waterfall [
100                 (callback) ->
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
107                                         callback null, parts
108                         else
109                                 opts.headers['Content-Length'] = data?.length ? 0
110                                 callback null, data
111                 (data, callback) ->
112                         called = false
113                         req = https.request opts, (res) ->
114                                 res.setEncoding 'utf8'
115                                 response_text = ''
116                                 res.on 'data', (data) ->
117                                         response_text += data
118                                 res.on 'end', ->
119                                         unless called
120                                                 called = true
121                                                 if res.statusCode isnt 200
122                                                         callback code: res.statusCode, path: path, response: response_text
123                                                 else
124                                                         callback null, response_text
125                         req.on 'error', (err) ->
126                                 unless called
127                                         called = true
128                                         callback err
129                         if content_type is 'multipart/form-data'
130                                 multipart_write req, data, (err) ->
131                                         if err?
132                                                 unless called
133                                                         called = true
134                                                         callback err
135                                                 req.end()
136                         else
137                                 req.write data if data?
138                                 req.end()
139                 ], callback
140
141 json_request = (method, path, data, token, callback) ->
142         if data?
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 ''
148                 try
149                         callback null, JSON.parse(response)
150                 catch error
151                         callback "Error: AF server returned invalid JSON for #{method} to #{path}: \"#{response}\""
152
153 json_get = (path, token, callback) ->
154         json_request 'GET', path, null, token, callback
155
156 json_post = (path, data, token, callback) ->
157         json_request 'POST', path, data, token, callback
158
159 json_put = (path, data, token, callback) ->
160         json_request 'PUT', path, data, token, callback
161
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
167                 else
168                         callback "login: couldn't find the token in the server response: #{JSON.stringify response}"
169
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) ->
176                         if 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) ->
179                                 if err?
180                                         return callback "CRITICAL ERROR: Couldn't start the app back up! Server responded: #{err}"
181                                 callback()
182
183 exports.app_update_files = (token, app_name, zip_file, callback) ->
184         request(
185                 'POST',
186                 "/apps/#{app_name}/application",
187                 'multipart/form-data',
188                 {
189                         _method: 'put',
190                         resources: '[]',
191                         application:
192                                 file: zip_file,
193                                 "Content-Type": 'application/octet-stream'
194                 },
195                 token,
196                 callback
197         )
198
199 exports.app_set_info = (token, app_name, info, callback) ->
200         json_put "/apps/#{app_name}", info, token, callback
201
202 app_set_state = (token, app_name, state, callback) ->
203         exports.app_info token, app_name, (err, info) ->
204                 return callback err if err?
205                 info.state = state
206                 exports.app_set_info token, app_name, info, callback
207
208 exports.app_start = (token, app_name, callback) ->
209         app_set_state token, app_name, 'STARTED', callback
210
211 exports.app_stop = (token, app_name, callback) ->
212         app_set_state token, app_name, 'STOPPED', callback
213
214
215 app_get = (path) ->
216         return (token, app_name, callback) ->
217                 json_get "/apps/#{app_name}#{path}", token, callback
218
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'