sony-ci-api 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.
- checksums.yaml +7 -0
- data/lib/sony_ci_admin.rb +220 -0
- data/lib/sony_ci_basic.rb +132 -0
- metadata +46 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 4e46eaec1fb159a4878b3de71bb97d137edac1fa
|
4
|
+
data.tar.gz: aec8169706b592d208d5450104e9db8726b8c6b1
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 495f880cc910fe823189d0bf2f5ba780fbf65a511f9eec51a5e8dcd06a9ad149efe67a4cbc6282bde04f22334620dccd6346273d735f34d171146a589b9f522c
|
7
|
+
data.tar.gz: 703f9211ff50685b2144fe96b6e8428bf2453d718618e2c5b90730a789899e37b04dab2c10e440d9e4e6f1916906801d28b0bdf926fe673004c3266af8e652dc
|
@@ -0,0 +1,220 @@
|
|
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
|
+
def upload(file_path, log_file)
|
10
|
+
Uploader.new(self, file_path, log_file).upload
|
11
|
+
end
|
12
|
+
|
13
|
+
def list_names
|
14
|
+
list.map { |item| item['name'] } - ['Workspace']
|
15
|
+
# A self reference is present even in an empty workspace.
|
16
|
+
end
|
17
|
+
|
18
|
+
def list(limit=50, offset=0)
|
19
|
+
Lister.new(self).list(limit, offset)
|
20
|
+
end
|
21
|
+
|
22
|
+
def each
|
23
|
+
Lister.new(self).each { |asset| yield asset }
|
24
|
+
end
|
25
|
+
|
26
|
+
def delete(asset_id)
|
27
|
+
Deleter.new(self).delete(asset_id)
|
28
|
+
end
|
29
|
+
|
30
|
+
def detail(asset_id)
|
31
|
+
Detailer.new(self).detail(asset_id)
|
32
|
+
end
|
33
|
+
|
34
|
+
class CiClient
|
35
|
+
# This class hierarchy might be excessive, but it gives us:
|
36
|
+
# - a single place for the `perform` method
|
37
|
+
# - and an isolated container for related private methods
|
38
|
+
|
39
|
+
def perform(curl, mime=nil)
|
40
|
+
# TODO: Is this actually working?
|
41
|
+
# curl.on_missing { |data| puts "4xx: #{data}" }
|
42
|
+
# curl.on_failure { |data| puts "5xx: #{data}" }
|
43
|
+
curl.verbose = @ci.verbose
|
44
|
+
curl.headers['Authorization'] = "Bearer #{@ci.access_token}"
|
45
|
+
curl.headers['Content-Type'] = mime if mime
|
46
|
+
curl.perform
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class Detailer < CiClient
|
51
|
+
def initialize(ci)
|
52
|
+
@ci = ci
|
53
|
+
end
|
54
|
+
|
55
|
+
def detail(asset_id)
|
56
|
+
curl = Curl::Easy.http_get('https:'"//api.cimediacloud.com/assets/#{asset_id}") do |c|
|
57
|
+
perform(c)
|
58
|
+
end
|
59
|
+
JSON.parse(curl.body_str)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class Deleter < CiClient
|
64
|
+
def initialize(ci)
|
65
|
+
@ci = ci
|
66
|
+
end
|
67
|
+
|
68
|
+
def delete(asset_id)
|
69
|
+
Curl::Easy.http_delete('https:'"//api.cimediacloud.com/assets/#{asset_id}") do |c|
|
70
|
+
perform(c)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
class Lister < CiClient
|
76
|
+
include Enumerable
|
77
|
+
|
78
|
+
def initialize(ci)
|
79
|
+
@ci = ci
|
80
|
+
end
|
81
|
+
|
82
|
+
def list(limit, offset)
|
83
|
+
curl = Curl::Easy.http_get('https:''//api.cimediacloud.com/workspaces/' \
|
84
|
+
"#{@ci.workspace_id}/contents?limit=#{limit}&offset=#{offset}") do |c|
|
85
|
+
perform(c)
|
86
|
+
end
|
87
|
+
JSON.parse(curl.body_str)['items']
|
88
|
+
end
|
89
|
+
|
90
|
+
def each
|
91
|
+
limit = 5 # Small chunks so it's easy to spot windowing problems
|
92
|
+
offset = 0
|
93
|
+
loop do
|
94
|
+
assets = list(limit, offset)
|
95
|
+
break if assets.empty?
|
96
|
+
assets.each { |asset| yield asset }
|
97
|
+
offset += limit
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
class Uploader < CiClient
|
103
|
+
def initialize(ci, path, log_path)
|
104
|
+
@ci = ci
|
105
|
+
@path = path
|
106
|
+
@log_file = File.open(log_path, 'a')
|
107
|
+
end
|
108
|
+
|
109
|
+
def upload
|
110
|
+
file = File.new(@path)
|
111
|
+
if file.size > 5 * 1024 * 1024
|
112
|
+
initiate_multipart_upload(file)
|
113
|
+
do_multipart_upload_part(file)
|
114
|
+
complete_multipart_upload
|
115
|
+
else
|
116
|
+
singlepart_upload(file)
|
117
|
+
end
|
118
|
+
|
119
|
+
row = [Time.now, File.basename(@path), @asset_id,
|
120
|
+
@ci.detail(@asset_id).to_s.gsub("\n", ' ')]
|
121
|
+
@log_file.write(row.join("\t") + "\n")
|
122
|
+
@log_file.flush
|
123
|
+
|
124
|
+
@asset_id
|
125
|
+
end
|
126
|
+
|
127
|
+
private
|
128
|
+
|
129
|
+
SINGLEPART_URI = 'https://io.cimediacloud.com/upload'
|
130
|
+
MULTIPART_URI = 'https://io.cimediacloud.com/upload/multipart'
|
131
|
+
|
132
|
+
def singlepart_upload(file)
|
133
|
+
curl = "curl -s -XPOST '#{SINGLEPART_URI}'" \
|
134
|
+
" -H 'Authorization: Bearer #{@ci.access_token}'" \
|
135
|
+
" -F filename='@#{file.path}'" \
|
136
|
+
" -F metadata=\"{'workspaceId': '#{@ci.workspace_id}'}\""
|
137
|
+
body_str = `#{curl}`
|
138
|
+
@asset_id = JSON.parse(body_str)['assetId']
|
139
|
+
fail "Upload failed: #{body_str}" unless @asset_id
|
140
|
+
# TODO: This shouldn't be hard, but it just hasn't worked for me.
|
141
|
+
# params = {
|
142
|
+
# File.basename(file) => file.read,
|
143
|
+
# 'metadata' => JSON.generate({})
|
144
|
+
# }.map { |k,v| Curl::PostField.content(k,v) }
|
145
|
+
# curl = Curl::Easy.http_post(SINGLEPART_URI, params) do |c|
|
146
|
+
# c.multipart_form_post = true
|
147
|
+
# perform(c)
|
148
|
+
# end
|
149
|
+
end
|
150
|
+
|
151
|
+
def initiate_multipart_upload(file)
|
152
|
+
params = JSON.generate('name' => File.basename(file),
|
153
|
+
'size' => file.size,
|
154
|
+
'workspaceId' => @ci.workspace_id)
|
155
|
+
curl = Curl::Easy.http_post(MULTIPART_URI, params) do |c|
|
156
|
+
perform(c, 'application/json')
|
157
|
+
end
|
158
|
+
@asset_id = JSON.parse(curl.body_str)['assetId']
|
159
|
+
end
|
160
|
+
|
161
|
+
def do_multipart_upload_part(file)
|
162
|
+
Curl::Easy.http_put("#{MULTIPART_URI}/#{@asset_id}/1", file.read) do |c|
|
163
|
+
perform(c, 'application/octet-stream')
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def complete_multipart_upload
|
168
|
+
Curl::Easy.http_post("#{MULTIPART_URI}/#{@asset_id}/complete") do |c|
|
169
|
+
perform(c)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
if __FILE__ == $PROGRAM_NAME
|
176
|
+
args = begin
|
177
|
+
Hash[ARGV.slice_before { |a| a.match(/^--/) }.to_a.map { |a| [a[0].gsub(/^--/, ''), a[1..-1]] }]
|
178
|
+
rescue
|
179
|
+
{}
|
180
|
+
end
|
181
|
+
|
182
|
+
ci = Ci.new(
|
183
|
+
# verbose: true,
|
184
|
+
credentials_path: Rails.root + 'config/ci.yml')
|
185
|
+
|
186
|
+
begin
|
187
|
+
case args.keys.sort
|
188
|
+
|
189
|
+
when ['log', 'up']
|
190
|
+
fail ArgumentError.new if args['log'].empty? || args['up'].empty?
|
191
|
+
args['up'].each { |path| ci.upload(path, args['log'].first) }
|
192
|
+
|
193
|
+
when ['down']
|
194
|
+
fail ArgumentError.new if args['down'].empty?
|
195
|
+
args['down'].each { |id| puts ci.download(id) }
|
196
|
+
|
197
|
+
when ['list']
|
198
|
+
fail ArgumentError.new unless args['list'].empty?
|
199
|
+
ci.each { |asset| puts "#{asset['name']}\t#{asset['id']}" }
|
200
|
+
|
201
|
+
when ['recheck']
|
202
|
+
fail ArgumentError.new if args['recheck'].empty?
|
203
|
+
args['recheck'].each do |file|
|
204
|
+
File.foreach(file) do |line|
|
205
|
+
line.chomp!
|
206
|
+
id = line.split("\t")[2]
|
207
|
+
detail = ci.detail(id).to_s.gsub("\n", ' ')
|
208
|
+
puts line + "\t" + detail
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
else
|
213
|
+
fail ArgumentError.new
|
214
|
+
end
|
215
|
+
rescue ArgumentError
|
216
|
+
abort 'Usage: --up GLOB --log LOG_FILE | --down ID | --list | --recheck LOG_FILE'
|
217
|
+
end
|
218
|
+
|
219
|
+
end
|
220
|
+
|
@@ -0,0 +1,132 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'curb'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
class SonyCiBasic
|
6
|
+
attr_reader :access_token
|
7
|
+
attr_reader :verbose
|
8
|
+
attr_reader :workspace_id
|
9
|
+
|
10
|
+
def initialize(opts={}) # rubocop:disable PerceivedComplexity, CyclomaticComplexity
|
11
|
+
unrecognized_opts = opts.keys - [:verbose, :credentials_path, :credentials]
|
12
|
+
fail "Unrecognized options #{unrecognized_opts}" unless unrecognized_opts == []
|
13
|
+
|
14
|
+
@verbose = opts[:verbose] ? true : false
|
15
|
+
|
16
|
+
fail 'Credentials specified twice' if opts[:credentials_path] && opts[:credentials]
|
17
|
+
fail 'No credentials given' if !opts[:credentials_path] && !opts[:credentials]
|
18
|
+
credentials = opts[:credentials] || YAML.load_file(opts[:credentials_path])
|
19
|
+
|
20
|
+
credentials.keys.sort.tap do |actual|
|
21
|
+
expected = ['username', 'password', 'client_id', 'client_secret', 'workspace_id'].sort
|
22
|
+
fail "Expected #{expected} in ci credentials, not #{actual}" if actual != expected
|
23
|
+
end
|
24
|
+
|
25
|
+
params = {
|
26
|
+
'grant_type' => 'password',
|
27
|
+
'client_id' => credentials['client_id'],
|
28
|
+
'client_secret' => credentials['client_secret']
|
29
|
+
}.map { |k, v| Curl::PostField.content(k, v) }
|
30
|
+
|
31
|
+
curl = Curl::Easy.http_post('https://api.cimediacloud.com/oauth2/token', *params) do |c|
|
32
|
+
c.verbose = @verbose
|
33
|
+
c.http_auth_types = :basic
|
34
|
+
c.username = credentials['username']
|
35
|
+
c.password = credentials['password']
|
36
|
+
# c.on_missing { |curl, data| puts "4xx: #{data}" }
|
37
|
+
# c.on_failure { |curl, data| puts "5xx: #{data}" }
|
38
|
+
c.perform
|
39
|
+
end
|
40
|
+
|
41
|
+
@access_token = JSON.parse(curl.body_str)['access_token']
|
42
|
+
fail 'OAuth failed' unless @access_token
|
43
|
+
|
44
|
+
@workspace_id = credentials['workspace_id']
|
45
|
+
end
|
46
|
+
|
47
|
+
def download(asset_id)
|
48
|
+
Downloader.new(self).download(asset_id)
|
49
|
+
end
|
50
|
+
|
51
|
+
class CiClient
|
52
|
+
# This class hierarchy might be excessive, but it gives us:
|
53
|
+
# - a single place for the `perform` method
|
54
|
+
# - and an isolated container for related private methods
|
55
|
+
|
56
|
+
def perform(curl, mime=nil)
|
57
|
+
# TODO: Is this actually working?
|
58
|
+
# curl.on_missing { |data| puts "4xx: #{data}" }
|
59
|
+
# curl.on_failure { |data| puts "5xx: #{data}" }
|
60
|
+
curl.verbose = @ci.verbose
|
61
|
+
curl.headers['Authorization'] = "Bearer #{@ci.access_token}"
|
62
|
+
curl.headers['Content-Type'] = mime if mime
|
63
|
+
curl.perform
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
class Downloader < CiClient
|
68
|
+
@@cache = {}
|
69
|
+
|
70
|
+
def initialize(ci)
|
71
|
+
@ci = ci
|
72
|
+
end
|
73
|
+
|
74
|
+
def download(asset_id)
|
75
|
+
hit = @@cache[asset_id]
|
76
|
+
if !hit || hit[:expires] < Time.now
|
77
|
+
curl = Curl::Easy.http_get('https'"://api.cimediacloud.com/assets/#{asset_id}/download") do |c|
|
78
|
+
perform(c)
|
79
|
+
end
|
80
|
+
url = JSON.parse(curl.body_str)['location']
|
81
|
+
@@cache[asset_id] = { url: url, expires: Time.now + 3 * 60 * 60 }
|
82
|
+
end
|
83
|
+
@@cache[asset_id][:url]
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
if __FILE__ == $PROGRAM_NAME
|
89
|
+
args = begin
|
90
|
+
Hash[ARGV.slice_before { |a| a.match(/^--/) }.to_a.map { |a| [a[0].gsub(/^--/, ''), a[1..-1]] }]
|
91
|
+
rescue
|
92
|
+
{}
|
93
|
+
end
|
94
|
+
|
95
|
+
ci = Ci.new(
|
96
|
+
# verbose: true,
|
97
|
+
credentials_path: Rails.root + 'config/ci.yml')
|
98
|
+
|
99
|
+
begin
|
100
|
+
case args.keys.sort
|
101
|
+
|
102
|
+
when ['log', 'up']
|
103
|
+
fail ArgumentError.new if args['log'].empty? || args['up'].empty?
|
104
|
+
args['up'].each { |path| ci.upload(path, args['log'].first) }
|
105
|
+
|
106
|
+
when ['down']
|
107
|
+
fail ArgumentError.new if args['down'].empty?
|
108
|
+
args['down'].each { |id| puts ci.download(id) }
|
109
|
+
|
110
|
+
when ['list']
|
111
|
+
fail ArgumentError.new unless args['list'].empty?
|
112
|
+
ci.each { |asset| puts "#{asset['name']}\t#{asset['id']}" }
|
113
|
+
|
114
|
+
when ['recheck']
|
115
|
+
fail ArgumentError.new if args['recheck'].empty?
|
116
|
+
args['recheck'].each do |file|
|
117
|
+
File.foreach(file) do |line|
|
118
|
+
line.chomp!
|
119
|
+
id = line.split("\t")[2]
|
120
|
+
detail = ci.detail(id).to_s.gsub("\n", ' ')
|
121
|
+
puts line + "\t" + detail
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
else
|
126
|
+
fail ArgumentError.new
|
127
|
+
end
|
128
|
+
rescue ArgumentError
|
129
|
+
abort 'Usage: --up GLOB --log LOG_FILE | --down ID | --list | --recheck LOG_FILE'
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
metadata
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sony-ci-api
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Chuck McCallum
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-05-28 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Wrapper for the Sony Ci API (http://developers.cimediacloud.com/)
|
14
|
+
email: chuck_mccallum@wgbh.org
|
15
|
+
executables: []
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- lib/sony_ci_admin.rb
|
20
|
+
- lib/sony_ci_basic.rb
|
21
|
+
homepage: https://github.com/WGBH/sony-ci-api
|
22
|
+
licenses:
|
23
|
+
- MIT
|
24
|
+
metadata: {}
|
25
|
+
post_install_message:
|
26
|
+
rdoc_options: []
|
27
|
+
require_paths:
|
28
|
+
- lib
|
29
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
35
|
+
requirements:
|
36
|
+
- - '>='
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: '0'
|
39
|
+
requirements: []
|
40
|
+
rubyforge_project:
|
41
|
+
rubygems_version: 2.2.3
|
42
|
+
signing_key:
|
43
|
+
specification_version: 4
|
44
|
+
summary: Sony Ci API
|
45
|
+
test_files: []
|
46
|
+
has_rdoc:
|