stupeflix-client 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,355 @@
1
+ require 'stupeflix/version'
2
+ require 'stupeflix/connection'
3
+ require 'stupeflix/base'
4
+ require 'cgi'
5
+ require 'json'
6
+
7
+ module Stupeflix
8
+ class StupeflixClient < Base
9
+ def initialize(accessKey, privateKey, host = "http://services.stupeflix.com", service = 'stupeflix-1.0', debug = false)
10
+ super(accessKey, privateKey, host, service, debug)
11
+ @batch = false
12
+ @batchData = ""
13
+ end
14
+
15
+ # Start a batch, used for speeduping video definition upload
16
+ # Operation that can be batched : sendDefinition and createProfiles
17
+ # Operation
18
+ # Only works for xml definition, not zip, and xml must be in UTF8
19
+ def batchStart( maxSize = 1000000)
20
+ @batch = true
21
+ @batchData = "<batch>"
22
+ @batchMaxSize = maxSize
23
+ end
24
+
25
+ # End a batch: actually send data
26
+ def batchEnd
27
+ @batchData += "</batch>"
28
+ sendDefinitionBatch(body = @batchData)
29
+ @batchData = ""
30
+ @batch = false
31
+ end
32
+
33
+ # Send a definition file to the API
34
+ def sendDefinition( user, resource, filename = nil, body = nil)
35
+ url = definitionUrl(user, resource)
36
+ if body
37
+ contentType = @TEXT_XML_CONTENT_TYPE;
38
+ elsif isZip(filename)
39
+ contentType = @APPLICATION_ZIP_CONTENT_TYPE
40
+ else
41
+ contentType = @TEXT_XML_CONTENT_TYPE
42
+ end
43
+ if @batch and contentType == @TEXT_XML_CONTENT_TYPE
44
+ @batchData += sprintf("<task user=\"%s\" resource=\"%s\">", user, resource)
45
+ if body
46
+ @batchData += body
47
+ else
48
+ @batchData += File.open(filename).read
49
+ end
50
+ else
51
+
52
+ return sendContent("PUT", url, contentType, filename, body)
53
+ end
54
+ end
55
+
56
+ # Send a definition file to the API
57
+ def sendDefinitionBatch( filename = nil, body = nil)
58
+ url = @definitionBatchUrl
59
+ contentType = @TEXT_XML_CONTENT_TYPE;
60
+ return sendContent("PUT", url, contentType, filename, body)
61
+ end
62
+
63
+ def getDefinition( user, resource, filename)
64
+ url = definitionUrl(user, resource)
65
+ return getContent(url, filename)['size']
66
+ end
67
+
68
+ def _getAbsoluteUrl( url, followRedirect = false)
69
+ urlPart = getContentUrl(url, 'GET', nil)
70
+ if followRedirect
71
+ conn = connection.Connection(@base_url, followRedirect = false)
72
+ response = conn.request_get(urlPart)
73
+ return response["headers"]["location"]
74
+ else
75
+ return @base_url + urlPart
76
+ end
77
+ end
78
+
79
+ def getProfileUrl( user, resource, profile, followRedirect = false)
80
+ url = profileUrl(user, resource, profile)
81
+ return _getAbsoluteUrl(url, followRedirect)
82
+ end
83
+
84
+ def getProfile( user, resource, profile, filename)
85
+ url = profileUrl(user, resource, profile)
86
+ getContent(url, filename)
87
+ end
88
+
89
+ def getProfileThumbUrl( user, resource, profile, followRedirect = false)
90
+ url = profileThumbUrl(user, resource, profile, "thumb.jpg")
91
+ return _getAbsoluteUrl(url, followRedirect)
92
+ end
93
+
94
+ def getProfileThumb( user, resource, profile, filename)
95
+ url = profileThumbUrl(user, resource, profile, "thumb.jpg")
96
+ getContent(url, filename)
97
+ end
98
+
99
+ def getProfileReportUrl( user, resource, profile, followRedirect = false)
100
+ url = profileReportUrl(user, resource, profile)
101
+ return _getAbsoluteUrl(url, followRedirect)
102
+ end
103
+
104
+ def getProfileReport( user, resource, profile, filename)
105
+ url = profileReportUrl(user, resource, profile)
106
+ getContent(url, filename)
107
+ end
108
+
109
+ def createProfiles( user, resource, profiles)
110
+ profileData = profiles.xmlGet
111
+ if @batch
112
+ @batchData += profileData
113
+ @batchData += "</task>"
114
+ if @batchData.length >= @batchMaxSize
115
+ begin
116
+ @batchEnd
117
+ finally
118
+ batchStart(@batchMaxSize)
119
+ end
120
+ end
121
+ else
122
+ url, parameters = profileCreateUrl(user, resource, profileData)
123
+ contentType = @APPLICATION_URLENCODED_CONTENT_TYPE
124
+ body = ""
125
+ parameters.each_pair do |k,v|
126
+ body += CGI::escape(k) + "=" + CGI::escape(v)
127
+ end
128
+ return sendContent("POST", url, contentType, filename = nil, body = body)
129
+ end
130
+ end
131
+
132
+ def getStatus( user = nil, resource = nil, profile = nil, marker = nil, maxKeys = nil)
133
+ url, parameters = statusUrl(user, resource, profile, marker, maxKeys)
134
+ ret = getContent(url, filename = nil, parameters = parameters)
135
+ status = JSON.parse(ret['body'])
136
+ return status
137
+ end
138
+
139
+ def getMarker( status)
140
+ if status.length == 0
141
+ return nil
142
+ end
143
+ lastStatus = status[-1]
144
+ #return map(lambda x: lastStatus[x], ["user", "resource", "profile"])
145
+ return []
146
+ end
147
+
148
+ # helper functions : build non signed urls for each kind of action
149
+ def definitionUrl( user, resource)
150
+ return sprintf( "/%s/%s/definition/", user, resource)
151
+ end
152
+
153
+ # helper functions : build non signed urls for each kind of action
154
+ def definitionBatchUrl
155
+ return "/batch/"
156
+ end
157
+
158
+ def profileUrl( user, resource, profile)
159
+ return sprintf( "/%s/%s/%s/", user, resource, profile)
160
+ end
161
+
162
+ def profileThumbUrl( user, resource, profile, thumbname)
163
+ return sprintf( "/%s/%s/%s/%s/", user, resource, profile, thumbname)
164
+ end
165
+
166
+ def profileReportUrl( user, resource, profile)
167
+ return sprintf( "/%s/%s/%s/%s/", user, resource, profile, "report.xml")
168
+ end
169
+
170
+ def profileCreateUrl( user, resource, profiles)
171
+ s = sprintf( "/%s/%s/", user, resource)
172
+ parameters = {@XML_PARAMETER => profiles}
173
+ return s, parameters
174
+ end
175
+
176
+ def actionUrl( user, resource, profile, action)
177
+ path = [user, resource, profile]
178
+ s = ""
179
+ path.each do |p|
180
+ if p == nil
181
+ break
182
+ end
183
+ s += sprintf( "/%s", p )
184
+ end
185
+ s += sprintf( "/%s/", action )
186
+ return s
187
+ end
188
+
189
+ def statusUrl( user, resource, profile, marker = nil, maxKeys = nil)
190
+ params = {}
191
+ if marker != nil
192
+ params[@MARKER_PARAMETER] = marker.join('/')
193
+ end
194
+ if maxKeys != nil
195
+ params[@MAXKEYS_PARAMETER] = maxKeys
196
+ end
197
+
198
+ return [actionUrl(user, resource, profile, "status"), params]
199
+ end
200
+ end
201
+
202
+ class StupeflixXMLNode
203
+ def initialize( nodeName, attributes = nil, children = nil, text = nil)
204
+ @children = children
205
+ @attributes = attributes
206
+ @nodeName = nodeName
207
+ @text = text
208
+ end
209
+
210
+ def xmlGet
211
+ docXML = '<' + @nodeName
212
+ if @attributes and @attributes.length != 0
213
+
214
+ @attributes.each_pair do |k, v|
215
+ docXML += " "
216
+ if v == nil
217
+ v = ""
218
+ end
219
+ k = k.to_s
220
+ v = v.to_s
221
+ docXML += k + '="' + CGI.escapeHTML(v) + '"'
222
+ end
223
+ end
224
+ docXML += '>'
225
+ if @children
226
+ for c in @children
227
+ docXML += c.xmlGet
228
+ end
229
+ end
230
+ if @text
231
+ docXML += @text
232
+ end
233
+ docXML += '</' + @nodeName + '>'
234
+
235
+ return docXML
236
+ end
237
+
238
+ def metaChildrenAppend( meta = nil, notify = nil, children = nil)
239
+ childrenArray = []
240
+ if meta
241
+ childrenArray += [meta]
242
+ end
243
+ if notify
244
+ childrenArray += [notify]
245
+ end
246
+ if children
247
+ childrenArray += children
248
+ end
249
+ return childrenArray
250
+ end
251
+ end
252
+
253
+ class StupeflixMeta < StupeflixXMLNode
254
+ def initialize(dict)
255
+ children = []
256
+
257
+ dict.all? {|k, v|
258
+ children += [StupeflixXMLNode.new(k, nil, nil, v)]
259
+ }
260
+ super("meta", {}, children)
261
+ end
262
+ end
263
+
264
+ class StupeflixProfileSet < StupeflixXMLNode
265
+ def initialize( profiles, meta = nil, notify = nil)
266
+ children = metaChildrenAppend(meta, notify, profiles)
267
+ super("profiles", {}, children)
268
+ end
269
+
270
+ def deflt(profiles)
271
+ profSet = []
272
+ for p in profiles
273
+ upload = StupeflixDefaultUpload
274
+ profSet += [StupeflixProfile(p, [upload])]
275
+ end
276
+
277
+ return StupeflixProfileSet.new(profSet)
278
+ end
279
+ end
280
+
281
+ class StupeflixProfile < StupeflixXMLNode
282
+ def initialize( profileName, uploads = nil, meta = nil, notify = nil)
283
+ children = metaChildrenAppend(meta, notify, uploads)
284
+ super("profile", {"name" => profileName}, children)
285
+ end
286
+ end
287
+
288
+ class StupeflixNotify < StupeflixXMLNode
289
+ def initialize( url, statusRegexp)
290
+ super("notify", {"url" => url, "statusRegexp" => statusRegexp})
291
+ end
292
+ end
293
+
294
+ class StupeflixHttpHeader < StupeflixXMLNode
295
+ def initialize(key, value)
296
+ super("header", {"key" => key, "value" => value})
297
+ end
298
+ end
299
+
300
+ class StupeflixUpload < StupeflixXMLNode
301
+ def initialize( name, parameters, meta = nil, notify = nil, children = nil)
302
+ children = metaChildrenAppend(meta, notify, children)
303
+ super(name, parameters, children)
304
+ end
305
+ end
306
+
307
+ class StupeflixHttpPOSTUpload < StupeflixUpload
308
+ def initialize( url, meta = nil, notify = nil)
309
+ super("httpPOST", {"url" => url}, meta, notify)
310
+ end
311
+ end
312
+
313
+ class StupeflixHttpPUTUpload < StupeflixUpload
314
+ def initialize( url, meta = nil, notify = nil, headers = nil)
315
+ super("httpPUT", {"url" => url}, meta, notify, headers)
316
+ end
317
+ end
318
+
319
+ class StupeflixYoutubeUpload < StupeflixUpload
320
+ def initialize(login, password, meta = nil, notify = nil)
321
+ super("youtube", {"login" => login, "password" => password}, meta, notify)
322
+ end
323
+ end
324
+
325
+ class StupeflixBrightcoveUpload < StupeflixUpload
326
+ def initialize(token, reference_id = nil, meta = nil, notify = nil)
327
+ parameters = {"sid" => token}
328
+ if reference_id != nil
329
+ parameters["reference_id"] = reference_id
330
+ end
331
+ super("brightcove", parameters, meta, notify)
332
+ end
333
+ end
334
+
335
+ class StupeflixDefaultUpload < StupeflixUpload
336
+ def initialize( meta = nil, notify = nil)
337
+ children = metaChildrenAppend(meta)
338
+ super("stupeflixStore", {}, meta, notify)
339
+ end
340
+ end
341
+
342
+ class StupeflixS3Upload < StupeflixUpload
343
+ def initialize(bucket, resourcePrefix, accesskey = nil, secretkey = nil, meta = nil, notify = nil)
344
+ children = metaChildrenAppend(meta)
345
+ parameters = {"bucket" => bucket, "resourcePrefix" => resourcePrefix}
346
+ if accesskey != nil
347
+ parameters["accesskey"] = accesskey
348
+ end
349
+ if secretkey != nil
350
+ parameters["secretkey"] = secretkey
351
+ end
352
+ super("s3", parameters, meta, notify)
353
+ end
354
+ end
355
+ end
@@ -0,0 +1,274 @@
1
+ require 'openssl'
2
+ require 'digest'
3
+ require 'base64'
4
+ require 'stupeflix/connection'
5
+ require 'digest/md5'
6
+
7
+ module Stupeflix
8
+ class Base
9
+ def initialize( accessKey, privateKey, host = "http://services.stupeflix.com", service = 'stupeflix-1.0', debug = false)
10
+ @accessKey = accessKey
11
+ @privateKey = privateKey
12
+ len = host.length - 7
13
+ if host[host.length - 1, 1] == "/"
14
+ host = host[0, host.length - 1]
15
+ end
16
+
17
+ @host = host[7,len]
18
+ @base_url = service
19
+ @debug = debug
20
+ @service = service
21
+ @TEXT_XML_CONTENT_TYPE = "text/xml"
22
+ @APPLICATION_ZIP_CONTENT_TYPE = "application/zip"
23
+ @APPLICATION_JSON_CONTENT_TYPE = "application/json"
24
+ @APPLICATION_URLENCODED_CONTENT_TYPE = "application/x-www-form-urlencoded"
25
+ @PROFILES_PARAMETER = "Profiles"
26
+ @XML_PARAMETER = "ProfilesXML"
27
+ @MARKER_PARAMETER = "Marker"
28
+ @MAXKEYS_PARAMETER = "MaxKeys"
29
+ # Currently there is only the Marker parameter (used for partial enumeration)
30
+ @parametersToAdd = [@MARKER_PARAMETER, @MAXKEYS_PARAMETER]
31
+ @sleepTime = 1.0
32
+ @maxRetry = 4
33
+ @base = true
34
+ end
35
+
36
+ def connectionGet
37
+ return Connection.new(@host, "/" + @base_url)
38
+ end
39
+
40
+ def paramString( parameters)
41
+ paramStr = ""
42
+ if parameters != nil
43
+ @parametersToAdd.each do |p|
44
+ if parameters.include?(p)
45
+ paramStr += sprintf( "%s\n%s\n", p, parameters[p])
46
+ end
47
+ end
48
+ end
49
+ return paramStr
50
+ end
51
+
52
+ def strToSign( method, resource, md5, mime, datestr, parameters)
53
+ paramStr = paramString(parameters)
54
+ stringToSign = sprintf( "%s\n%s\n%s\n%s\n%s\n%s", method, md5, mime, datestr, '/' + @service + resource, paramStr)
55
+ return stringToSign
56
+ end
57
+
58
+ def sign( strToSign, secretKey)
59
+ digest = OpenSSL::Digest::Digest.new('sha1')
60
+ return OpenSSL::HMAC.hexdigest(digest, secretKey, strToSign)
61
+ end
62
+
63
+ def signUrl( url, method, md5, mime, parameters = {})
64
+ now =Time.now.to_i
65
+ strToSign = strToSign(method, url, md5, mime, now, parameters)
66
+ signature = sign(strToSign, @privateKey)
67
+ url += sprintf( "?Date=%s&AccessKey=%s&Signature=%s", now, @accessKey,signature)
68
+ if parameters
69
+ parameters.each_pair do |k,v|
70
+ url += sprintf("&%s=%s", k,v)
71
+ end
72
+ end
73
+ return url
74
+ end
75
+
76
+ def md5FileOrBody( filename, body = nil)
77
+ md5 = Digest::MD5.new()
78
+
79
+ if body != nil
80
+ md5.update(body)
81
+ else
82
+ chunksize=1024
83
+ f = File.open(filename, 'r')
84
+
85
+ while true
86
+ chunk = f.read(chunksize)
87
+ if not chunk
88
+ break
89
+ end
90
+ md5.update(chunk)
91
+ end
92
+ f.close
93
+ end
94
+
95
+ digest = md5.digest
96
+
97
+ return [digest, md5.hexdigest, Base64.encode64(digest).strip]
98
+ end
99
+
100
+ def isZip( filename)
101
+ f = File.open(filename, 'r')
102
+ header = f.read(4)
103
+ return header == 'PK'+3.chr+4.chr
104
+ end
105
+
106
+ def logdebug( s)
107
+ if @debug
108
+ print s.to_s
109
+ end
110
+ end
111
+
112
+ def error( message)
113
+ logdebug(message)
114
+ raise StandardError, message
115
+ end
116
+
117
+ def answer_error( answer, message)
118
+ raise StandardError, sprintf( "%s\n%s", message, answer['body'])
119
+ end
120
+
121
+ # sendcallback is an object with
122
+ # - a 'sendCallBack' member function that accept a unique int argument (=number of bytes written so far)
123
+ # - a 'sendBlockSize' member function with no argument which return the size of block to be sent
124
+ def sendContent( method, url, contentType, filename = nil, body = nil, parameters = nil, sendcallback = nil)
125
+
126
+ # SEND DATA
127
+ conn = connectionGet()
128
+
129
+ md5, md5hex, md5base64 = md5FileOrBody(filename, body)
130
+
131
+ if filename
132
+ size = File.stat(filename).size
133
+ else
134
+ size = body.length
135
+ end
136
+
137
+ headers = {'Content-MD5' => md5base64.to_s,
138
+ 'Content-Length' => size.to_s,
139
+ 'Content-Type' => contentType}
140
+
141
+ url = signUrl(url, method, md5base64, contentType, parameters)
142
+
143
+ # LAUNCH THE REQUEST : TODO : pass filename instead of body
144
+ if method == "PUT"
145
+ answer = conn.request_put(url, args = nil, body = body, filename = filename, headers = headers, sendcallback = sendcallback)
146
+ elsif method == "POST"
147
+ answer = conn.request_post(url, args = nil, body = body, filename = filename, headers = headers)
148
+ elsif method == "DELETE"
149
+ answer = conn.request_delete(url, headers = headers)
150
+ end
151
+
152
+ headers = answer['headers']
153
+
154
+ logdebug(headers)
155
+ logdebug(answer['body'])
156
+
157
+ # NOW CHECK THAT EVERYTHING IS OK
158
+ status = headers['status']
159
+ if status != '200'
160
+ msg = sprintf( "sendContent : bad STATUS %s", status )
161
+ answer_error(answer, msg)
162
+ end
163
+
164
+ if headers['etag'] == nil
165
+ msg = "corrupted answer: no etag in headers. Response body is " + answer['body']
166
+ error(msg)
167
+ end
168
+
169
+ obtainedMD5 = headers['etag']
170
+
171
+ if obtainedMD5 != md5hex
172
+ msg = sprintf( "sendContent : bad returned etags %s =! %s (ref)", obtainedMD5, md5hex)
173
+ error(msg)
174
+ end
175
+
176
+ return answer
177
+ end
178
+
179
+ def getContentUrl( url, method, parameters)
180
+ return signUrl(url, method, "", "", parameters)
181
+ end
182
+
183
+ def getContent( url, filename = nil, parameters = nil)
184
+ sleepTime = @sleepTime
185
+
186
+ for i in 0..@maxRetry
187
+ raiseExceptionOn404 = (i + 1) == @maxRetry
188
+ ret = getContent_(url, filename, parameters, raiseExceptionOn404)
189
+ if ret["status"] != 404
190
+ return ret
191
+ end
192
+ # Wait for amazon S3 ...
193
+ sleep(1)
194
+ end
195
+ end
196
+
197
+ def getContent_( url, filename = nil, parameters = nil, raiseExceptionOn404 = true)
198
+ method = "GET"
199
+ url = getContentUrl(url, method, parameters)
200
+
201
+ # GET DATA
202
+ conn = connectionGet()
203
+ answer = conn.request_get(url)
204
+ body = answer['body']
205
+
206
+ headers = answer['headers']
207
+ status = headers['status'].to_i
208
+
209
+ if status == 204
210
+ # there was no content
211
+ obtainedSize = 0
212
+ if body.length != 0
213
+ error("204 status with non empty body.")
214
+ end
215
+ elsif status == 200
216
+ obtainedSize =headers['content-length'].to_i
217
+ elsif status == 404 and not raiseExceptionOn404
218
+ return {"url" => headers['content-location'], "status" => 404}
219
+ else
220
+ msg = sprintf( "getContent : bad STATUS %s", status )
221
+ answer_error(answer, msg)
222
+ end
223
+
224
+ if body.length != obtainedSize
225
+ error("Non matching body length and content-length")
226
+ end
227
+
228
+ if filename != nil
229
+ f = File.open(filename, 'w')
230
+ f.write(body)
231
+ f.close
232
+
233
+ if obtainedSize == 0
234
+ File.unlink(filename)
235
+ else
236
+ filesize = File.stat(filename).size
237
+ if obtainedSize != filesize
238
+ File.unlink(filename)
239
+ error(sprintf( "file size is incorrect : file size = %d, body size = %d", filesize, obtainedSize))
240
+ end
241
+ end
242
+ end
243
+
244
+ # NOW CHECK EVERYTHING IS OK
245
+ md5, md5hex, md5base64 = md5FileOrBody(filename, body)
246
+
247
+ if status != 204
248
+ obtainedMD5 = headers['etag'].gsub(/"/,"")
249
+ if obtainedMD5 != md5hex
250
+ if filename
251
+ File.unlink(filename)
252
+ end
253
+ error(sprintf( "getDefinition : bad returned etag %s =! %s (ref)", md5hex, obtainedMD5))
254
+ end
255
+ end
256
+
257
+ if status == 200
258
+ logdebug(sprintf( "headers = %s", headers) )
259
+ url = headers['content-location']
260
+ ret = {'size' => obtainedSize, 'url' => url, 'headers' => headers}
261
+ else
262
+ ret = {'size' => obtainedSize, 'url' => url}
263
+ end
264
+
265
+ if not filename
266
+ ret['body'] = body
267
+ end
268
+
269
+ ret["status"] = status
270
+
271
+ return ret
272
+ end
273
+ end
274
+ end