JasonWoof Got questions, comments, patches, etc.? Contact Jason Woofenden
add .editorconfig
[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         opts.headers['Accept'] = '*/*;q=0.8'
96         async.waterfall [
97                 (callback) ->
98                         if content_type is 'multipart/form-data'
99                                 boundary = new_multipart_boundary()
100                                 opts.headers['Content-Type'] = "#{content_type}; boundary=#{boundary}"
101                                 multipart_prepare boundary, data, (err, size, parts) ->
102                                         return callback err if err
103                                         opts.headers['Content-Length'] = size
104                                         callback null, parts
105                         else
106                                 opts.headers['Content-Length'] = data?.length ? 0
107                                 callback null, data
108                 (data, callback) ->
109                         called = false
110                         req = https.request opts, (res) ->
111                                 res.setEncoding 'utf8'
112                                 response_text = ''
113                                 res.on 'data', (data) ->
114                                         response_text += data
115                                 res.on 'end', ->
116                                         unless called
117                                                 called = true
118                                                 if res.statusCode isnt 200
119                                                         callback code: res.statusCode, path: path, response: response_text
120                                                 else
121                                                         callback null, response_text
122                         req.on 'error', (err) ->
123                                 unless called
124                                         called = true
125                                         callback err
126                         if content_type is 'multipart/form-data'
127                                 multipart_write req, data, (err) ->
128                                         if err?
129                                                 unless called
130                                                         called = true
131                                                         callback err
132                                                 req.end()
133                         else
134                                 req.write data if data?
135                                 req.end()
136                 ], callback
137
138 # send json
139 # try parsing result as json, and fall back to returning string
140 json_request = (method, path, data, token, callback) ->
141         if data?
142                 data = JSON.stringify data
143         request method, path, 'application/json', data, token, (err, response) ->
144                 return callback err if err
145                 return callback null, '' if response is ' '
146                 return callback null, '' if response is ''
147                 try
148                         callback null, JSON.parse(response)
149                 catch error
150                         # some api calls return plain text
151                         callback null, 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 exports.app_update_files = (token, app_name, zip_file, callback) ->
171         request(
172                 'POST',
173                 "/apps/#{app_name}/application",
174                 'multipart/form-data',
175                 {
176                         _method: 'put',
177                         resources: '[]',
178                         application:
179                                 file: zip_file,
180                                 "Content-Type": 'application/octet-stream'
181                 },
182                 token,
183                 callback
184         )
185
186 exports.app_set_info = (token, app_name, info, callback) ->
187         json_put "/apps/#{app_name}", info, token, callback
188
189 app_get = (path) ->
190         return (token, app_name, callback) ->
191                 json_get "/apps/#{app_name}#{path}", token, callback
192
193 exports.app_instances = app_get '/instances'
194 exports.app_crashes   = app_get '/crashes'
195 exports.app_info      = app_get ''
196 exports.app_logs      = app_get '/instances/0/files/logs'
197 exports.app_stdout    = app_get '/instances/0/files/logs/stdout.log'
198 exports.app_stderr    = app_get '/instances/0/files/logs/stderr.log'
199 exports.app_files     = app_get '/instances/0/files/app/app.js'
200 exports.app_stats     = app_get '/stats'