sony_ci_api 0.2.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 606b8f73ec065decebb07d5b157a734f66254d39
4
+ data.tar.gz: 689a7849752c7a72dd26b26fb483ab8ad9e33d99
5
+ SHA512:
6
+ metadata.gz: 6ccc8c7a290d7cbbc4a1a1540278c57c2ca8350dda7faeccb8ccd6064100ccac6a6ea25fd68b733afee4a28a612456bc0becaa94e3e4e6b12288ab1163c4bbf2
7
+ data.tar.gz: 78f9c3cc945ad04b82c9acf63925815b8ada6613602e47f750558008277d96bc16aed16790790ebbd0c61eddbf0bfa16fd0e65cc411ef97999e34caeeb9d87a1
data/bin/sony_ci_api ADDED
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/sony_ci_api/sony_ci_admin'
4
+
5
+ args = begin
6
+ Hash[ARGV.slice_before { |a| a.match(/^--/) }.to_a.map { |a| [a[0].gsub(/^--/, ''), a[1..-1]] }]
7
+ rescue
8
+ {}
9
+ end
10
+
11
+ ci = SonyCiAdmin.new(
12
+ # verbose: true,
13
+ credentials_path: 'config/ci.yml')
14
+
15
+ begin
16
+ case args.keys.sort
17
+
18
+ when %w(log up)
19
+ fail ArgumentError.new if args['log'].empty? || args['up'].empty?
20
+ args['up'].each { |path| ci.upload(path, args['log'].first) }
21
+
22
+ when ['down']
23
+ fail ArgumentError.new if args['down'].empty?
24
+ args['down'].each { |id| puts ci.download(id) }
25
+
26
+ when ['list']
27
+ fail ArgumentError.new unless args['list'].empty?
28
+ ci.each { |asset| puts "#{asset['name']}\t#{asset['id']}" }
29
+
30
+ when ['recheck']
31
+ fail ArgumentError.new if args['recheck'].empty?
32
+ args['recheck'].each do |file|
33
+ File.foreach(file) do |line|
34
+ line.chomp!
35
+ id = line.split("\t")[2]
36
+ detail = ci.detail(id).to_s.gsub("\n", ' ')
37
+ puts line + "\t" + detail
38
+ end
39
+ end
40
+
41
+ else
42
+ fail ArgumentError.new
43
+ end
44
+ rescue ArgumentError
45
+ abort 'Usage: --up GLOB --log LOG_FILE | --down ID | --list | --recheck LOG_FILE'
46
+ end
@@ -0,0 +1,187 @@
1
+ require 'yaml'
2
+ require 'curb'
3
+ require 'json'
4
+ require_relative 'sony_ci_basic'
5
+
6
+ class SonyCiAdmin < SonyCiBasic
7
+ include Enumerable
8
+
9
+ # Upload a document to Ci. Underlying API treats large and small files
10
+ # differently, but this should treat both alike.
11
+ def upload(file_path, log_file)
12
+ Uploader.new(self, file_path, log_file).upload
13
+ end
14
+
15
+ # Just the names of items in the workspace. This may include directories.
16
+ def list_names
17
+ list.map { |item| item['name'] } - ['Workspace']
18
+ # A self reference is present even in an empty workspace.
19
+ end
20
+
21
+ # Full metadata for a windowed set of items.
22
+ def list(limit = 50, offset = 0)
23
+ Lister.new(self).list(limit, offset)
24
+ end
25
+
26
+ # Iterate over all items.
27
+ def each
28
+ Lister.new(self).each { |asset| yield asset }
29
+ end
30
+
31
+ # Delete items by asset ID.
32
+ def delete(asset_id)
33
+ Deleter.new(self).delete(asset_id)
34
+ end
35
+
36
+ # Get detailed metadata by asset ID.
37
+ def detail(asset_id)
38
+ Detailer.new(self).detail(asset_id)
39
+ end
40
+
41
+ def multi_details(asset_ids, fields)
42
+ Detailer.new(self).multi_details(asset_ids, fields)
43
+ end
44
+
45
+ class Detailer < SonyCiClient #:nodoc:
46
+ def initialize(ci)
47
+ @ci = ci
48
+ end
49
+
50
+ def detail(asset_id)
51
+ curl = Curl::Easy.http_get('https:'"//api.cimediacloud.com/assets/#{asset_id}") do |c|
52
+ add_headers(c)
53
+ end
54
+ handle_errors(curl)
55
+ JSON.parse(curl.body_str)
56
+ end
57
+
58
+ def multi_details(asset_ids, fields)
59
+ curl = Curl::Easy.http_post('https:''//api.cimediacloud.com/assets/details/bulk',
60
+ JSON.generate('assetIds' => asset_ids,
61
+ 'fields' => fields)
62
+ ) do |c|
63
+ add_headers(c, 'application/json')
64
+ end
65
+ handle_errors(curl)
66
+ JSON.parse(curl.body_str)
67
+ end
68
+ end
69
+
70
+ class Deleter < SonyCiClient #:nodoc:
71
+ def initialize(ci)
72
+ @ci = ci
73
+ end
74
+
75
+ def delete(asset_id)
76
+ curl = Curl::Easy.http_delete('https:'"//api.cimediacloud.com/assets/#{asset_id}") do |c|
77
+ add_headers(c)
78
+ end
79
+ handle_errors(curl)
80
+ end
81
+ end
82
+
83
+ class Lister < SonyCiClient #:nodoc:
84
+ include Enumerable
85
+
86
+ def initialize(ci)
87
+ @ci = ci
88
+ end
89
+
90
+ def list(limit, offset)
91
+ curl = Curl::Easy.http_get('https:''//api.cimediacloud.com/workspaces/' \
92
+ "#{@ci.workspace_id}/contents?limit=#{limit}&offset=#{offset}") do |c|
93
+ add_headers(c)
94
+ end
95
+ handle_errors(curl)
96
+ JSON.parse(curl.body_str)['items']
97
+ end
98
+
99
+ def each
100
+ limit = 5 # Small chunks so it's easy to spot windowing problems
101
+ offset = 0
102
+ loop do
103
+ assets = list(limit, offset)
104
+ break if assets.empty?
105
+ assets.each { |asset| yield asset }
106
+ offset += limit
107
+ end
108
+ end
109
+ end
110
+
111
+ class Uploader < SonyCiClient #:nodoc:
112
+
113
+ # Chunk size 10 for multipart upload.
114
+ CHUNK_SIZE = 10 * 1024 * 1024
115
+
116
+ def initialize(ci, path, log_path)
117
+ @ci = ci
118
+ @path = path
119
+ @log_file = File.open(log_path, 'a')
120
+ end
121
+
122
+ def upload
123
+ file = File.new(@path)
124
+ if file.size >= CHUNK_SIZE
125
+ initiate_multipart_upload(file)
126
+ part = 0
127
+ part = do_multipart_upload_part(file, part) while part
128
+ complete_multipart_upload
129
+ else
130
+ singlepart_upload(file)
131
+ end
132
+
133
+ row = [Time.now, File.basename(@path), @asset_id,
134
+ @ci.detail(@asset_id).to_s.gsub("\n", ' ')]
135
+ @log_file.write(row.join("\t") + "\n")
136
+ @log_file.flush
137
+
138
+ @asset_id
139
+ end
140
+
141
+ private
142
+
143
+ SINGLEPART_URI = 'https://io.cimediacloud.com/upload'
144
+ MULTIPART_URI = 'https://io.cimediacloud.com/upload/multipart'
145
+
146
+ def singlepart_upload(file)
147
+ params = [
148
+ Curl::PostField.file('filename', file.path, File.basename(file.path)),
149
+ Curl::PostField.content('metadata', JSON.generate('workspaceId' => @ci.workspace_id))
150
+ ]
151
+ curl = Curl::Easy.http_post(SINGLEPART_URI, params) do |c|
152
+ c.multipart_form_post = true
153
+ add_headers(c)
154
+ end
155
+ handle_errors(curl)
156
+ @asset_id = JSON.parse(curl.body_str)['assetId']
157
+ end
158
+
159
+ def initiate_multipart_upload(file)
160
+ params = JSON.generate('name' => File.basename(file),
161
+ 'size' => file.size,
162
+ 'workspaceId' => @ci.workspace_id)
163
+ curl = Curl::Easy.http_post(MULTIPART_URI, params) do |c|
164
+ add_headers(c, 'application/json')
165
+ end
166
+ handle_errors(curl)
167
+ @asset_id = JSON.parse(curl.body_str)['assetId']
168
+ end
169
+
170
+ def do_multipart_upload_part(file, part)
171
+ fragment = file.read(CHUNK_SIZE)
172
+ return unless fragment
173
+ curl = Curl::Easy.http_put("#{MULTIPART_URI}/#{@asset_id}/#{part + 1}", fragment) do |c|
174
+ add_headers(c, 'application/octet-stream')
175
+ end
176
+ handle_errors(curl)
177
+ part + 1
178
+ end
179
+
180
+ def complete_multipart_upload
181
+ curl = Curl::Easy.http_post("#{MULTIPART_URI}/#{@asset_id}/complete") do |c|
182
+ add_headers(c)
183
+ end
184
+ handle_errors(curl)
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,74 @@
1
+ require 'yaml'
2
+ require 'curb'
3
+ require 'json'
4
+ require_relative 'sony_ci_client'
5
+
6
+ class SonyCiBasic
7
+ attr_reader :access_token
8
+ attr_reader :verbose
9
+ attr_reader :workspace_id
10
+
11
+ # Either +credentials_path+ or a +credentials+ object itself must be supplied.
12
+ def initialize(opts = {}) # rubocop:disable PerceivedComplexity, CyclomaticComplexity
13
+ unrecognized_opts = opts.keys - [:verbose, :credentials_path, :credentials]
14
+ fail "Unrecognized options #{unrecognized_opts}" unless unrecognized_opts == []
15
+
16
+ @verbose = opts[:verbose] ? true : false
17
+
18
+ fail 'Credentials specified twice' if opts[:credentials_path] && opts[:credentials]
19
+ fail 'No credentials given' if !opts[:credentials_path] && !opts[:credentials]
20
+ credentials = opts[:credentials] || YAML.load_file(opts[:credentials_path])
21
+
22
+ credentials.keys.sort.tap do |actual|
23
+ expected = %w(username password client_id client_secret workspace_id).sort
24
+ fail "Expected #{expected} in ci credentials, not #{actual}" if actual != expected
25
+ end
26
+
27
+ params = {
28
+ 'grant_type' => 'password',
29
+ 'client_id' => credentials['client_id'],
30
+ 'client_secret' => credentials['client_secret']
31
+ }.map { |k, v| Curl::PostField.content(k, v) }
32
+
33
+ curl = Curl::Easy.http_post('https://api.cimediacloud.com/oauth2/token', *params) do |c|
34
+ c.verbose = @verbose
35
+ c.http_auth_types = :basic
36
+ c.username = credentials['username']
37
+ c.password = credentials['password']
38
+ # c.on_missing { |curl, data| puts "4xx: #{data}" }
39
+ # c.on_failure { |curl, data| puts "5xx: #{data}" }
40
+ end
41
+
42
+ @access_token = JSON.parse(curl.body_str)['access_token']
43
+ fail 'OAuth failed' unless @access_token
44
+
45
+ @workspace_id = credentials['workspace_id']
46
+ end
47
+
48
+ # Generate a temporary download URL for an asset.
49
+ def download(asset_id)
50
+ Downloader.new(self).download(asset_id)
51
+ end
52
+
53
+ class Downloader < SonyCiClient #:nodoc:
54
+ @@cache = {}
55
+
56
+ def initialize(ci)
57
+ @ci = ci
58
+ end
59
+
60
+ def download(asset_id)
61
+ hit = @@cache[asset_id]
62
+ if !hit || hit[:expires] < Time.now
63
+
64
+ curl = Curl::Easy.http_get('https'"://api.cimediacloud.com/assets/#{asset_id}/download") do |c|
65
+ add_headers(c)
66
+ end
67
+ handle_errors(curl)
68
+ url = JSON.parse(curl.body_str)['location']
69
+ @@cache[asset_id] = { url: url, expires: Time.now + 3 * 60 * 60 }
70
+ end
71
+ @@cache[asset_id][:url]
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,15 @@
1
+ class SonyCiClient #:nodoc:
2
+ def add_headers(curl, mime = nil)
3
+ # on_missing and on_failure exist...
4
+ # but any exceptions are caught and turned into warnings:
5
+ # You need to check the response code at the end
6
+ # if you want the execution path to change.
7
+ curl.verbose = @ci.verbose
8
+ curl.headers['Authorization'] = "Bearer #{@ci.access_token}"
9
+ curl.headers['Content-Type'] = mime if mime
10
+ end
11
+
12
+ def handle_errors(curl)
13
+ fail "#{curl.status}: #{curl.url}\nHEADERS: #{curl.headers}\nPOST: #{curl.post_body}\nRESPONSE: #{curl.body}" if curl.response_code.to_s !~ /^2../
14
+ end
15
+ end
metadata ADDED
@@ -0,0 +1,49 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sony_ci_api
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Chuck McCallum
8
+ - Andrew Myers
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2015-10-06 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: Wrapper for the Sony Ci API (http://developers.cimediacloud.com/)
15
+ email: andrew_myers@wgbh.org
16
+ executables:
17
+ - sony_ci_api
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - bin/sony_ci_api
22
+ - lib/sony_ci_api/sony_ci_admin.rb
23
+ - lib/sony_ci_api/sony_ci_basic.rb
24
+ - lib/sony_ci_api/sony_ci_client.rb
25
+ homepage: https://github.com/WGBH/sony_ci_api
26
+ licenses:
27
+ - MIT
28
+ metadata: {}
29
+ post_install_message:
30
+ rdoc_options: []
31
+ require_paths:
32
+ - lib
33
+ required_ruby_version: !ruby/object:Gem::Requirement
34
+ requirements:
35
+ - - ">="
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ required_rubygems_version: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '0'
43
+ requirements: []
44
+ rubyforge_project:
45
+ rubygems_version: 2.5.1
46
+ signing_key:
47
+ specification_version: 4
48
+ summary: Sony Ci API
49
+ test_files: []