popuparchive 1.0.0

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.
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: []