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 +7 -0
- data/bin/sony_ci_api +46 -0
- data/lib/sony_ci_api/sony_ci_admin.rb +187 -0
- data/lib/sony_ci_api/sony_ci_basic.rb +74 -0
- data/lib/sony_ci_api/sony_ci_client.rb +15 -0
- metadata +49 -0
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: []
|