popuparchive 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/popuparchive.rb +404 -0
  3. metadata +170 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f4df34b72d100f6c9dbe9a447ed3432eca71ca85
4
+ data.tar.gz: 4f5346cabc147f30659bb20c4d80272b3e396892
5
+ SHA512:
6
+ metadata.gz: 44d1b5a28bf0fd405d700e56a652ebd18340bf883175c60dc69a76ad980973e14ee7ba60bf6b6eb78f1e6b0fc234e4912df6ed4ec6999ba949586c66d8d39b23
7
+ data.tar.gz: bf52ef21e64941fadb907d3521a8fecd5063dfff6137babb728043506545607c1e3ffec58a2f98643843be4cd5d3ac94ad57f7cdf8658fe039ebc17b3167386e
@@ -0,0 +1,404 @@
1
+ # Pop Up Archive Ruby SDK
2
+ # Copyright 2014 - Pop Up Archive
3
+ # Licensed under Apache 2 license - see LICENSE file
4
+ #
5
+ #
6
+
7
+ require 'rubygems'
8
+ require 'json'
9
+ require 'faraday_middleware'
10
+ require 'oauth2'
11
+ require 'uri'
12
+ require 'xmlsimple'
13
+
14
+ module PopUpArchive
15
+
16
+ module Error
17
+ class NotFound < StandardError
18
+
19
+ end
20
+ end
21
+
22
+ class FaradayErrHandler < Faraday::Response::Middleware
23
+ def on_complete(env)
24
+ # Ignore any non-error response codes
25
+ return if (status = env[:status]) < 400
26
+ #puts "got response status #{status}"
27
+ case status
28
+ when 404
29
+ #raise Error::NotFound
30
+ # 404 errors not fatal
31
+ else
32
+ #puts pp(env)
33
+ super # let parent class deal with it
34
+ end
35
+ end
36
+ end
37
+
38
+ class Client
39
+
40
+ attr_accessor :host
41
+ attr_accessor :debug
42
+ attr_accessor :agent
43
+ attr_accessor :user_agent
44
+ attr_accessor :cookies
45
+ attr_accessor :api_endpoint
46
+ attr_accessor :croak_on_404
47
+
48
+ def version
49
+ return "1.0.0"
50
+ end
51
+
52
+ def initialize(args)
53
+ #puts args.inspect
54
+ @un = args[:username]
55
+ @pw = args[:password]
56
+ @oauth_id = args[:id]
57
+ @oauth_secret = args[:secret]
58
+ @oauth_redir_uri = args[:redir_uri] || 'urn:ietf:wg:oauth:2.0:oob'
59
+ @host = args[:host] || 'https://www.popuparchive.com'
60
+ @debug = args[:debug]
61
+ @user_agent = args[:user_agent] || 'popuparchive-ruby-client/'+version()
62
+ @api_endpoint = args[:api_endpoint] || '/api'
63
+ @croak_on_404 = args[:croak_on_404] || false
64
+
65
+ # normalize host
66
+ @host.gsub!(/\/$/, '')
67
+
68
+ # sanity check
69
+ begin
70
+ uri = URI.parse(@host)
71
+ rescue URI::InvalidURIError => err
72
+ raise "Bad :host value " + err
73
+ end
74
+ if (!uri.host || !uri.port)
75
+ raise "Bad :host value " + @server
76
+ end
77
+
78
+ @agent = get_agent()
79
+
80
+ end
81
+
82
+ def get_oauth_token(options={})
83
+ oauth_options = {
84
+ site: @host + @api_endpoint,
85
+ authorize_url: @host + '/oauth/authorize',
86
+ token_url: @host + '/oauth/token',
87
+ redirect_uri: @oauth_redir_uri,
88
+ connection_opts: options.merge( { :ssl => {:verify => false}, } )
89
+ }
90
+
91
+ # TODO
92
+
93
+ client = OAuth2::Client.new(@oauth_id, @oauth_secret, oauth_options) do |faraday|
94
+ faraday.request :url_encoded
95
+ faraday.response :logger if @debug
96
+ faraday.adapter :excon
97
+ end
98
+
99
+ token = nil
100
+ if @un && @pw
101
+ # TODO 3-legged oauth to @authorize_url
102
+ else
103
+ token = client.client_credentials.get_token()
104
+ end
105
+
106
+ return token
107
+ end
108
+
109
+ def get_agent()
110
+ uri = @host + @api_endpoint
111
+ opts = {
112
+ :url => uri,
113
+ :ssl => {:verify => false},
114
+ :headers => {
115
+ 'User-Agent' => @user_agent,
116
+ 'Accept' => 'application/json',
117
+ 'Cookie' => @cookies
118
+ }
119
+ }
120
+ @token = get_oauth_token
121
+ #puts "token="
122
+ #puts pp(@token)
123
+ conn = Faraday.new(opts) do |faraday|
124
+ faraday.request :url_encoded
125
+ [:mashify, :json].each{|mw| faraday.response(mw) }
126
+ if !@croak_on_404
127
+ faraday.use PopUpArchive::FaradayErrHandler
128
+ else
129
+ faraday.response(:raise_error)
130
+ end
131
+ faraday.request :authorization, 'Bearer', @token.token
132
+ faraday.response :logger if @debug
133
+ faraday.adapter :excon # IMPORTANT this is last
134
+ end
135
+
136
+ return conn
137
+ end
138
+
139
+ def get(path, params={})
140
+ resp = @agent.get @api_endpoint + path, params
141
+ @debug and puts pp(resp)
142
+ return PopUpArchive::Response.new resp
143
+ end
144
+
145
+ def post(path, body, content_type='application/json')
146
+ uri = @api_endpoint + path
147
+ resp = @agent.post do|req|
148
+ req.url uri
149
+ req.body = body
150
+ req.headers['Content-Type'] = content_type
151
+ end
152
+ return PopUpArchive::Response.new resp
153
+ end
154
+
155
+ def get_or_create_item(filename)
156
+
157
+
158
+ end
159
+
160
+ def get_collection(coll_id)
161
+ resp = get('/collections/'+coll_id.to_s)
162
+ return resp.http_resp.body
163
+ end
164
+
165
+ def get_items(coll_id)
166
+ resp = get('/collections/'+coll_id.to_s+'/items')
167
+ return resp.http_resp.body.items
168
+ end
169
+
170
+ def get_item(coll_id, item_id)
171
+ resp = get('/collections/'+coll_id.to_s+'/items/'+item_id.to_s)
172
+ return resp.http_resp.body
173
+ end
174
+
175
+ def get_my_uploads
176
+ resp = get('/collections')
177
+ my_uploads = nil
178
+ resp.collections.each do|coll|
179
+ if coll.title == 'My Uploads'
180
+ my_uploads = coll
181
+ end
182
+ end
183
+ return my_uploads
184
+ end
185
+
186
+ def create_item(coll, attrs)
187
+ resp = post("/collections/#{coll.id}/items", JSON.generate(attrs))
188
+ return resp.http_resp.body
189
+ end
190
+
191
+ def create_audio_file(item, attrs={})
192
+ file = attrs.has_key?(:file) ? URI::encode(attrs.delete(:file)) : nil
193
+ uri = "/items/#{item.id}/audio_files"
194
+ uri += "?file=#{file}" if file
195
+ body = JSON.generate({:audio_file => attrs})
196
+ resp = post(uri, body)
197
+ #puts "audio_file:"
198
+ #puts pp resp
199
+ return resp.http_resp.body
200
+ end
201
+
202
+ def start_upload(item, af_id, fileattrs)
203
+
204
+ # workflow is:
205
+ # (1) GET upload_to with audio_file.id
206
+ # (2) using response, GET get_init_signature
207
+ # (example: "key"=>"$token/$filename", "mime_type"=>"image/jpeg", "filename"=>"$filename", "filesize"=>"$n", "last_modified"=>"$e", "item_id"=>"$iid", "audio_file_id"=>"$afid"}
208
+ # (3) using upload_id, GET get_all_signatures
209
+ # (example: {"key"=>"$token/$filename", "mime_type"=>"image/jpeg", "num_chunks"=>"1", "upload_id"=>"$upid", "filename"=>"$filename", "filesize"=>"$n", "last_modified"=>"$e", "item_id"=>"$iid", "audio_file_id"=>"$afid"}
210
+ # (4) foreach chunk, GET chunk_loaded (optional, since only UI needs this, not a SDK)
211
+ # (5) finally, GET upload_finished (see finish_upload() below)
212
+
213
+ # check 'item' for validity
214
+ if !item.has_key? :token
215
+ raise ":token missing on item"
216
+ end
217
+ if !item.has_key? :id
218
+ raise ":id missing on item"
219
+ end
220
+
221
+ # check 'fileattrs' for validity
222
+ if !fileattrs.has_key? :filename
223
+ raise ":filename missing on fileattrs"
224
+ end
225
+ if !fileattrs.has_key? :lastmod
226
+ raise ":lastmod missing on fileattrs"
227
+ end
228
+ if !fileattrs.has_key? :size
229
+ raise ":size missing on fileattrs"
230
+ end
231
+ if !fileattrs.has_key? :mime_type
232
+ raise ":mime_type missing on fileattrs"
233
+ end
234
+
235
+ base_uri = "/items/#{item.id}/audio_files/#{af_id}"
236
+
237
+ upload_to_uri = "#{base_uri}/upload_to"
238
+ upload_to_resp = get(upload_to_uri)
239
+ puts "upload_to_resp:"
240
+ puts pp upload_to_resp
241
+
242
+ sig_key = "#{item[:token]}/#{fileattrs[:filename]}"
243
+ sig_params = {
244
+ :key => sig_key,
245
+ :mime_type => fileattrs[:mime_type],
246
+ :filename => fileattrs[:filename],
247
+ :filesize => fileattrs[:size],
248
+ :last_modified => fileattrs[:lastmod],
249
+ :item_id => item.id,
250
+ :audio_file_id => af_id,
251
+ }
252
+ init_sig_uri = "#{base_uri}/get_init_signature?" + Faraday::FlatParamsEncoder.encode(sig_params)
253
+ init_sig_resp = get(init_sig_uri)
254
+ puts "init_sig_resp:"
255
+ puts pp init_sig_resp
256
+
257
+ # build the authorization key
258
+ upload_key = upload_to_resp['key']
259
+ authz_key = "#{upload_to_resp.provider} #{upload_key}:#{init_sig_resp.signature}"
260
+
261
+ # special agent for aws
262
+ aws_url = "https://#{upload_to_resp.bucket}.s3.amazonaws.com/#{sig_key}"
263
+ aws_opts = {
264
+ :headers => {
265
+ 'User-Agent' => @user_agent,
266
+ 'Authorization' => authz_key,
267
+ 'x-amz-date' => init_sig_resp.date,
268
+ },
269
+ :ssl => { :verify => false }
270
+ }
271
+ aws_agent = Faraday.new(aws_opts) do |faraday|
272
+ [:mashify, :xml, :raise_error].each{|mw| faraday.response(mw) }
273
+ faraday.response :logger if @debug
274
+ faraday.adapter :excon # IMPORTANT this is last
275
+ end
276
+
277
+ # post to provider to get the upload_id
278
+ puts "aws_agent.post"
279
+ aws_resp = aws_agent.post do |req|
280
+ req.url "#{aws_url}?uploads"
281
+ req.headers['Content-Type'] = fileattrs[:mime_type]
282
+ req.headers['Content-Disposition'] = "attachment; filename=" + fileattrs[:filename]
283
+ req.headers['x-amz-acl'] = 'public-read' # TODO
284
+ end
285
+ puts "aws response:"
286
+ puts pp aws_resp
287
+
288
+ # pull out the AWS uploadId
289
+ sig_params[:upload_id] = aws_resp.body.InitiateMultipartUploadResult.UploadId
290
+
291
+ # how many chunks do we expect?
292
+ sig_params[:num_chunks] = fileattrs.has_key?(:num_chunks) ? fileattrs[:num_chunks] : 1
293
+
294
+ all_sig_resp = get("#{base_uri}/get_all_signatures?" + Faraday::FlatParamsEncoder.encode(sig_params))
295
+ puts "all_sig_resp"
296
+ puts pp all_sig_resp
297
+
298
+ return {
299
+ :signatures => all_sig_resp.http_resp.body,
300
+ :params => sig_params,
301
+ :fileattrs => fileattrs,
302
+ :upload_to => upload_to_resp.http_resp.body,
303
+ :init_sig => init_sig_resp.http_resp.body,
304
+ :aws_url => aws_url,
305
+ :aws_agent => aws_agent,
306
+ }
307
+
308
+ end
309
+
310
+ def finish_upload(upload)
311
+ puts "finish_upload:"
312
+ puts pp upload
313
+
314
+ # workflow assumes file has been successfully PUT to AWS
315
+ # (1) GET aws_url?uploadId=$uploadId
316
+ # parse xml response for ListPartsResult
317
+ # (2) POST aws_url?uplaodId=$uploadId with body
318
+ # <CompleteMultipartUpload>
319
+ # <Part><PartNumber>1</PartNumber><ETag>"$etag_from_ListPartsResult"</ETag></Part>
320
+ # </CompleteMultipartUpload>
321
+ # response to POST contains CompleteMultipartUploadResult with final location
322
+ # (3) GET pua_url/upload_finished/? with params:
323
+ # filename => $filename,
324
+ # filesize => $n,
325
+ # key => $sig_key,
326
+ # last_modified => $e,
327
+ # upload_id => $aws_uploadId,
328
+
329
+ # sanity check upload object
330
+ [:signatures, :params, :fileattrs, :upload_to, :init_sig, :aws_url, :aws_agent].each do|p|
331
+ if !upload.has_key? p
332
+ raise ":#{p} missing from upload"
333
+ end
334
+ end
335
+
336
+ aws_upload_id = upload[:params][:upload_id]
337
+ aws_agent = upload[:aws_agent]
338
+ parts_resp = aws_agent.get(upload[:aws_url], {:uploadId => aws_upload_id})
339
+
340
+ aws_parts = []
341
+ parts_resp.ListPartsResult.Part.each do|part|
342
+ aws_parts.push( { PartNumber: part.PartNumber, ETag: part.ETag })
343
+ end
344
+ aws_parts_xml = XmlSimple.xml_out({Part: aws_parts}, {rootname: 'CompleteMultipartUpload', noattr: true})
345
+ puts "aws_parts_xml: #{aws_parts_xml}"
346
+ aws_finish_resp = aws_agent.post do|req|
347
+ req.url "#{upload[:aws_url]}?uploadId=#{aws_upload_id}"
348
+ req.headers['Content-Type'] = upload[:fileattrs][:mime_type]
349
+ req.headers['Content-Disposition'] = "attachment; filename=" + upload[:fileattrs][:filename]
350
+ req.body = aws_parts_xml
351
+ end
352
+
353
+ puts "aws_finish_resp:"
354
+ puts pp aws_finish_resp
355
+
356
+ end
357
+
358
+ end # end Client
359
+
360
+ # dependent classes
361
+ class Response
362
+
363
+ attr_accessor :http_resp
364
+
365
+ def initialize(http_resp)
366
+ @http_resp = http_resp
367
+
368
+ #warn http_resp.headers.inspect
369
+ #warn "code=" + http_resp.status.to_s
370
+
371
+ @is_ok = false
372
+ if http_resp.status.to_s =~ /^2\d\d/
373
+ @is_ok = true
374
+ end
375
+
376
+ end
377
+
378
+ def status()
379
+ return @http_resp.status
380
+ end
381
+
382
+ def is_success()
383
+ return @is_ok
384
+ end
385
+
386
+ def method_missing(meth, *args, &block)
387
+ if @http_resp.body.respond_to? meth
388
+ @http_resp.body.send(meth, *args, &block)
389
+ else
390
+ super
391
+ end
392
+ end
393
+
394
+ def respond_to?(meth)
395
+ if @http_resp.body.respond_to? meth
396
+ true
397
+ else
398
+ super
399
+ end
400
+ end
401
+
402
+ end # end Response
403
+
404
+ end # end module
metadata ADDED
@@ -0,0 +1,170 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: popuparchive
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Peter Karman
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-09-25 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: faraday_middleware
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: excon
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: hashie
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: oauth2
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: xml-simple
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rspec
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - '>='
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: dotenv
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - '>='
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: mime-types
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - '>='
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - '>='
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ description: Ruby client for the Pop Up Archive API. See https://www.popuparchive.com/
140
+ email: peter@popuparchive.com
141
+ executables: []
142
+ extensions: []
143
+ extra_rdoc_files: []
144
+ files:
145
+ - lib/popuparchive.rb
146
+ homepage: https://github.com/popuparchive/pua-sdk-ruby
147
+ licenses:
148
+ - Apache
149
+ metadata: {}
150
+ post_install_message:
151
+ rdoc_options: []
152
+ require_paths:
153
+ - lib
154
+ required_ruby_version: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - '>='
157
+ - !ruby/object:Gem::Version
158
+ version: '0'
159
+ required_rubygems_version: !ruby/object:Gem::Requirement
160
+ requirements:
161
+ - - '>='
162
+ - !ruby/object:Gem::Version
163
+ version: '0'
164
+ requirements: []
165
+ rubyforge_project: nowarning
166
+ rubygems_version: 2.0.14
167
+ signing_key:
168
+ specification_version: 4
169
+ summary: Ruby client for the Pop Up Archive API
170
+ test_files: []