uricp 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.
- data/.gitignore +6 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +16 -0
- data/README.md +31 -0
- data/README.rdoc +23 -0
- data/Rakefile +66 -0
- data/bin/segment_upload +66 -0
- data/bin/uricp +103 -0
- data/features/auth_download.feature +106 -0
- data/features/cacheable_from_uri.feature +71 -0
- data/features/documented_options.feature +73 -0
- data/features/remote_upload.feature +49 -0
- data/features/segmented_upload.feature +27 -0
- data/features/segmented_upload_options.feature +65 -0
- data/features/step_definitions/orbit_steps.rb +194 -0
- data/features/step_definitions/uricp_steps.rb +34 -0
- data/features/support/env.rb +20 -0
- data/features/userid_download.feature +21 -0
- data/lib/segment_upload.rb +38 -0
- data/lib/uricp/curl_primitives.rb +45 -0
- data/lib/uricp/orbit_auth.rb +59 -0
- data/lib/uricp/segmenter.rb +88 -0
- data/lib/uricp/strategy/cache_common.rb +48 -0
- data/lib/uricp/strategy/cached_get.rb +48 -0
- data/lib/uricp/strategy/cleaner.rb +28 -0
- data/lib/uricp/strategy/common.rb +108 -0
- data/lib/uricp/strategy/local_convert.rb +36 -0
- data/lib/uricp/strategy/local_convert_common.rb +29 -0
- data/lib/uricp/strategy/local_link.rb +31 -0
- data/lib/uricp/strategy/piped_cache.rb +40 -0
- data/lib/uricp/strategy/piped_cache_convert.rb +45 -0
- data/lib/uricp/strategy/piped_compress.rb +30 -0
- data/lib/uricp/strategy/piped_decompress.rb +35 -0
- data/lib/uricp/strategy/piped_local_compress.rb +30 -0
- data/lib/uricp/strategy/piped_local_get.rb +29 -0
- data/lib/uricp/strategy/piped_local_put.rb +29 -0
- data/lib/uricp/strategy/piped_remote_get.rb +38 -0
- data/lib/uricp/strategy/pruner.rb +22 -0
- data/lib/uricp/strategy/remote_put.rb +47 -0
- data/lib/uricp/strategy/segmented_remote_put.rb +52 -0
- data/lib/uricp/strategy/sweeper.rb +28 -0
- data/lib/uricp/uri_strategy.rb +47 -0
- data/lib/uricp/version.rb +4 -0
- data/lib/uricp.rb +56 -0
- data/spec/something_spec.rb +5 -0
- data/uricp.gemspec +32 -0
- metadata +281 -0
@@ -0,0 +1,65 @@
|
|
1
|
+
Feature: Documented Help for Segmented Upload
|
2
|
+
In order to work out what segment_upload does
|
3
|
+
As a command line user
|
4
|
+
I want to get detailed usage information
|
5
|
+
|
6
|
+
Scenario: Basic UI
|
7
|
+
When I get help for "segment_upload"
|
8
|
+
Then the exit status should be 0
|
9
|
+
And the banner should include the version
|
10
|
+
And the banner should document that this app takes options
|
11
|
+
And the following options should be documented:
|
12
|
+
|--version|
|
13
|
+
|--auth-token|
|
14
|
+
|--auth-user|
|
15
|
+
|--auth-key|
|
16
|
+
|--from|
|
17
|
+
|--segment-size|
|
18
|
+
And the option "dry-run" should be documented which is negatable
|
19
|
+
And the banner should document that this app's arguments are:
|
20
|
+
|to_uri|which is required|
|
21
|
+
|
22
|
+
Scenario: auth token and auth-user are mutually exclusive
|
23
|
+
When I run `segment_upload --auth-token abcdef --auth-user xyz --auth-key abc http://source file:///target`
|
24
|
+
Then the exit status should not be 0
|
25
|
+
And the stderr should contain "needless argument"
|
26
|
+
|
27
|
+
Scenario: auth user needs auth key
|
28
|
+
When I run `segment_upload --auth-user xyz http://source`
|
29
|
+
Then the exit status should not be 0
|
30
|
+
And the stderr should contain "missing argument"
|
31
|
+
|
32
|
+
Scenario: auth key needs auth user
|
33
|
+
When I run `segment_upload --auth-user xyz http://source`
|
34
|
+
Then the exit status should not be 0
|
35
|
+
And the stderr should contain "missing argument"
|
36
|
+
|
37
|
+
Scenario: needs some sort of authentication
|
38
|
+
When I run `segment_upload http://source`
|
39
|
+
Then the exit status should not be 0
|
40
|
+
And the stderr should contain "missing argument"
|
41
|
+
|
42
|
+
Scenario: no http should fail
|
43
|
+
When I run `segment_upload ftp://source`
|
44
|
+
Then the exit status should not be 0
|
45
|
+
And the stderr should contain "unsupported url"
|
46
|
+
|
47
|
+
Scenario: authentication with normal http should fail
|
48
|
+
When I run `segment_upload --auth-user cli-xxxxx --auth-key fred https://foss-lab-manual.googlecode.com/files`
|
49
|
+
Then the exit status should not be 0
|
50
|
+
And the stderr should contain "Cannot authenticate"
|
51
|
+
|
52
|
+
Scenario: bad authentication should fail against orbit
|
53
|
+
When I run `segment_upload --auth-user cli-xxxxx --auth-key fred https://orbit.brightbox.com/v1/acc-xxxxx/test/test.img`
|
54
|
+
Then the exit status should not be 0
|
55
|
+
And the stderr should contain "Cannot authenticate"
|
56
|
+
|
57
|
+
Scenario: should not accept plain number for segment size
|
58
|
+
When I run `segment_upload --segment-size 100 --auth-token abcdef https:///orbit.brightbox.com/v1/acc-xxxxx/test/test.img`
|
59
|
+
Then the exit status should not be 0
|
60
|
+
And the stderr should contain "Unparseable filesize"
|
61
|
+
|
62
|
+
Scenario: should trap attempts to open a command
|
63
|
+
When I run `segment_upload --segment-size 100MB --auth-token abcdef --from ' |fred' https:///orbit.brightbox.com/v1/acc-xxxxx/test/test.img`
|
64
|
+
Then the exit status should not be 0
|
65
|
+
And the stderr should contain "invalid argument"
|
@@ -0,0 +1,194 @@
|
|
1
|
+
require 'inifile'
|
2
|
+
|
3
|
+
def obtain_credentials
|
4
|
+
config = IniFile.load(ENV['HOME']+ '/.brightbox/config') ||
|
5
|
+
raise(RuntimeError, "No brightbox config in home directory. Can't obtain auth token for tests")
|
6
|
+
section = config.sections.first
|
7
|
+
[config[section]['client_id'], config[section]['secret']]
|
8
|
+
end
|
9
|
+
|
10
|
+
def fetch_orbit_token
|
11
|
+
@clientid, @key = obtain_credentials
|
12
|
+
cmd = "curl -I https://orbit.brightbox.com/v1 -H 'X-Auth-User: #{@clientid}' -H 'X-Auth-Key: #{@key}'"
|
13
|
+
run_simple(unescape(cmd))
|
14
|
+
stdout_from(cmd).each_line do |line|
|
15
|
+
key, value = line.strip.split(/\s*:\s*/,2)
|
16
|
+
@current_auth_token = value if key == 'X-Auth-Token'
|
17
|
+
@current_storage_url = value if key == 'X-Storage-Url'
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def create_download_file(filetype, name, container)
|
22
|
+
upload_url = File.join(@current_storage_url, container, name)
|
23
|
+
tempfile = '/tmp/fred55322'
|
24
|
+
FileUtils.rm_f(Dir.glob(tempfile+'*'))
|
25
|
+
format = 'raw'
|
26
|
+
format = 'qcow2' if filetype == 'qcow2'
|
27
|
+
cmd = "qemu-img create -q -f #{format} #{tempfile} 1G"
|
28
|
+
system(unescape(cmd))
|
29
|
+
cmd = "cat #{tempfile}"
|
30
|
+
if filetype == 'lz4'
|
31
|
+
cmd += " | lz4c "
|
32
|
+
end
|
33
|
+
cmd += "| curl --silent --fail -T - -H 'X-Auth-Token: #{@current_auth_token}' #{upload_url} "
|
34
|
+
system(unescape(cmd))
|
35
|
+
end
|
36
|
+
|
37
|
+
def create_container(name)
|
38
|
+
url = File.join(@current_storage_url, name)
|
39
|
+
cmd = "curl --silent --fail -I -H 'X-Auth-Token: #{@current_auth_token}' #{url}>/dev/null || curl --silent --fail -I -H 'X-Auth-Token: #{@current_auth_token}' #{url} -X PUT"
|
40
|
+
system(unescape(cmd))
|
41
|
+
end
|
42
|
+
|
43
|
+
Given(/^a container called "([^"]*)"$/) do |container|
|
44
|
+
create_container(container)
|
45
|
+
end
|
46
|
+
|
47
|
+
Given(/^a cache of "(.*?)" from container "(.*?)" at "(.*?)"$/) do |name, container, cache|
|
48
|
+
url = File.join(@current_storage_url, container, name)
|
49
|
+
target = File.join(cache, 'cache', name)
|
50
|
+
steps %{
|
51
|
+
When I successfully run `curl --fail --silent -o #{target} -H 'X-Auth-Token: #{@current_auth_token}' #{url}`
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
Given /^a (\d+) byte lz4 file named "([^"]*)"$/ do |file_size, file_name|
|
56
|
+
write_fixed_size_file(file_name, file_size.to_i)
|
57
|
+
system("lz4c #{file_name}")
|
58
|
+
File.rename file_name+'.lz4', file_name
|
59
|
+
end
|
60
|
+
|
61
|
+
When(/^I store "([^"]*)" into container "([^"]*)" from "([^"]*)"$/) do |name, container, source|
|
62
|
+
steps %{
|
63
|
+
When I store "#{name}" with options "" into container "#{container}" from "#{source}"
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
When(/^I retrieve "([^"]*)" with options "([^"]*)" from container "([^"]*)" into "([^"]*)"$/) do |name, runoptions, container, target|
|
68
|
+
url = File.join(@current_storage_url, container, name)
|
69
|
+
steps %{
|
70
|
+
When I successfully run `uricp #{runoptions} --auth-token #{@current_auth_token} #{url} #{target}`
|
71
|
+
}
|
72
|
+
end
|
73
|
+
|
74
|
+
When(/^I retrieve "([^"]*)" from container "([^"]*)" into "([^"]*)" with a userid$/) do |name, container, target|
|
75
|
+
url = File.join(@current_storage_url, container, name)
|
76
|
+
steps %{
|
77
|
+
When I successfully run `uricp --auth-user '#{@clientid}' --auth-key '#{@key}' #{url} #{target}`
|
78
|
+
}
|
79
|
+
end
|
80
|
+
|
81
|
+
When(/^I store "([^"]*)" into container "([^"]*)" from "([^"]*)" with a userid$/) do |name, container, source|
|
82
|
+
@current_container = container
|
83
|
+
@current_name = name
|
84
|
+
url = File.join(@current_storage_url, container, name)
|
85
|
+
steps %{
|
86
|
+
When I successfully run `uricp --auth-user '#{@clientid}' --auth-key '#{@key}' #{source} #{url}`
|
87
|
+
}
|
88
|
+
end
|
89
|
+
|
90
|
+
When(/^I retrieve "([^"]*)" from container "([^"]*)" into "([^"]*)"$/) do |name, container, target|
|
91
|
+
steps %{
|
92
|
+
When I retrieve "#{name}" with options "" from container "#{container}" into "#{target}"
|
93
|
+
}
|
94
|
+
end
|
95
|
+
|
96
|
+
When(/^I store "([^"]*)" with options "([^"]*)" into container "([^"]*)" from "([^"]*)"$/) do |name, runoptions, container, source|
|
97
|
+
@current_container = container
|
98
|
+
@current_name = name
|
99
|
+
url = File.join(@current_storage_url, container, name)
|
100
|
+
steps %{
|
101
|
+
When I successfully run `uricp #{runoptions} --auth-token #{@current_auth_token} #{source} #{url}`
|
102
|
+
}
|
103
|
+
end
|
104
|
+
|
105
|
+
When(/^I store "([^"]*)" with segment size "([^"]*)" into container "([^"]*)" from "([^"]*)" with a userid$/) do |name, segment_size, container, source|
|
106
|
+
@current_container = container
|
107
|
+
@current_name = name
|
108
|
+
url = File.join(@current_storage_url, container, name)
|
109
|
+
steps %{
|
110
|
+
When I successfully run `uricp --auth-user '#{@clientid}' --auth-key '#{@key}' --segment-size '#{segment_size}' #{source} #{url}`
|
111
|
+
}
|
112
|
+
end
|
113
|
+
|
114
|
+
When(/^I store "([^"]*)" with segment size "([^"]*)" and options "([^"]*)" into container "([^"]*)" from "([^"]*)" with a userid$/) do |name, segment_size, runoptions, container, source|
|
115
|
+
@current_container = container
|
116
|
+
@current_name = name
|
117
|
+
url = File.join(@current_storage_url, container, name)
|
118
|
+
steps %{
|
119
|
+
When I successfully run `uricp #{runoptions} --auth-user '#{@clientid}' --auth-key '#{@key}' --segment-size '#{segment_size}' #{source} #{url}`
|
120
|
+
}
|
121
|
+
end
|
122
|
+
|
123
|
+
When(/^I upload "([^"]*)" with segment size "([^"]*)" into container "([^"]*)" from "([^"]*)"$/) do |name, segment_size, container, source|
|
124
|
+
@current_container = container
|
125
|
+
@current_name = name
|
126
|
+
url = File.join(@current_storage_url, container, name)
|
127
|
+
steps %{
|
128
|
+
When I successfully run `segment_upload --from #{source} --auth-token #{@current_auth_token} --segment-size #{segment_size} #{url}`
|
129
|
+
}
|
130
|
+
end
|
131
|
+
|
132
|
+
When(/^I upload "([^"]*)" with segment size "([^"]*)" into container "([^"]*)" from "([^"]*)" as a stream$/) do |name, segment_size, container, source|
|
133
|
+
@current_container = container
|
134
|
+
@current_name = name
|
135
|
+
url = File.join(@current_storage_url, container, name)
|
136
|
+
steps %{
|
137
|
+
When I successfully run `sh -c 'cat #{source} | segment_upload --auth-token #{@current_auth_token} --segment-size #{segment_size} #{url}'`
|
138
|
+
}
|
139
|
+
end
|
140
|
+
|
141
|
+
Then(/^a (\d+) byte entry should exist in container "(.*?)" called "(.*?)"$/) do |size, container, name|
|
142
|
+
url = File.join(@current_storage_url, container, name)
|
143
|
+
command = "curl -I --fail --silent -H 'X-Auth-Token:#{@current_auth_token}' #{url}"
|
144
|
+
steps %{
|
145
|
+
When I successfully run `#{command}`
|
146
|
+
Then the output should match /\\sContent-Length: #{size}\\s/
|
147
|
+
}
|
148
|
+
end
|
149
|
+
|
150
|
+
Then(/^the container "(.*?)" should contain (\d+) entries$/) do |container, quantity|
|
151
|
+
url = File.join(@current_storage_url, container)
|
152
|
+
command = "curl -I --fail --silent -H 'X-Auth-Token:#{@current_auth_token}' #{url}"
|
153
|
+
steps %{
|
154
|
+
When I successfully run `#{command}`
|
155
|
+
Then the output should match /\\sX-Container-Object-Count: #{quantity}\\s/
|
156
|
+
}
|
157
|
+
end
|
158
|
+
|
159
|
+
Then(/^an lz4 compressed entry should exist in container "(.*?)" called "(.*?)"$/) do |container, name|
|
160
|
+
url = File.join(@current_storage_url, container, name)
|
161
|
+
cmd="curl -r 0-3 --fail --silent -H X-Auth-Token:#{@current_auth_token} #{url}"
|
162
|
+
run_simple(unescape(cmd))
|
163
|
+
assert_exact_output([0x184D2204].pack('V'), stdout_from(cmd))
|
164
|
+
end
|
165
|
+
|
166
|
+
Then(/^a qcow2 entry should exist in container "(.*?)" called "(.*?)"$/) do |container, name|
|
167
|
+
url = File.join(@current_storage_url, container, name)
|
168
|
+
cmd="curl -r 0-3 --fail --silent -H X-Auth-Token:#{@current_auth_token} #{url}"
|
169
|
+
run_simple(unescape(cmd))
|
170
|
+
assert_exact_output(['QFI',0xfb].pack('a3C'), stdout_from(cmd))
|
171
|
+
end
|
172
|
+
|
173
|
+
Before('@orbit') do
|
174
|
+
fetch_orbit_token
|
175
|
+
end
|
176
|
+
|
177
|
+
Before('@orbitdownloads') do
|
178
|
+
$orbit_setup ||= false
|
179
|
+
unless $orbit_setup
|
180
|
+
create_container('test')
|
181
|
+
create_download_file('qcow2', 'img-qcow2', 'test')
|
182
|
+
create_download_file('lz4', 'img-lz4cy', 'test')
|
183
|
+
$orbit_setup = true
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
After('@orbit') do
|
188
|
+
if @current_auth_token && @current_container
|
189
|
+
steps %{
|
190
|
+
When I successfully run `swift --os-storage-url=#{@current_storage_url} --os-auth-token=#{@current_auth_token} delete #{@current_container} #{@current_name}`
|
191
|
+
}
|
192
|
+
@current_container = @current_name = nil
|
193
|
+
end
|
194
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
Then(/^the file named "(.*?)" should have a file format of "(.*?)"$/) do |filename, format|
|
2
|
+
case format
|
3
|
+
when 'lz4'
|
4
|
+
assert_exact_output([0x184D2204].pack('V'),
|
5
|
+
File.open(filename, 'rb') {|f| f.read(4) })
|
6
|
+
when 'qcow2v3', 'qcow3'
|
7
|
+
steps %{
|
8
|
+
When I successfully run `qemu-img info #{filename}`
|
9
|
+
Then the output from "qemu-img info #{filename}" should contain "file format: qcow2"
|
10
|
+
And the output from "qemu-img info #{filename}" should contain "compat: 1.1"
|
11
|
+
}
|
12
|
+
else
|
13
|
+
assert_no_partial_output([0x184D2204].pack('V'),
|
14
|
+
File.open(filename, 'rb') {|f| f.read(4) })
|
15
|
+
steps(%{
|
16
|
+
When I successfully run `qemu-img info #{filename}`
|
17
|
+
Then the output from "qemu-img info #{filename}" should contain "file format: #{format}"
|
18
|
+
And the output from "qemu-img info #{filename}" should not contain "compat: 1.1"
|
19
|
+
})
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
Given(/^a correctly initialised cache at "(.*?)"$/) do |basedir|
|
24
|
+
steps %{
|
25
|
+
Given a directory named "#{basedir}/cache"
|
26
|
+
And a directory named "#{basedir}/temp"
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
Given(/^an empty directory named "([^"]*)"$/) do |dir_name|
|
31
|
+
create_dir(dir_name)
|
32
|
+
FileUtils.rm_rf(File.join(dir_name, '.'), :secure => true)
|
33
|
+
end
|
34
|
+
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'aruba/cucumber'
|
2
|
+
require 'methadone/cucumber'
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
ENV['PATH'] = "#{File.expand_path(File.dirname(__FILE__) + '/../../bin')}#{File::PATH_SEPARATOR}#{ENV['PATH']}"
|
6
|
+
LIB_DIR = File.join(File.expand_path(File.dirname(__FILE__)),'..','..','lib')
|
7
|
+
|
8
|
+
Before do
|
9
|
+
# Using "announce" causes massive warnings on 1.9.2
|
10
|
+
@puts = true
|
11
|
+
@original_rubylib = ENV['RUBYLIB']
|
12
|
+
ENV['RUBYLIB'] = LIB_DIR + File::PATH_SEPARATOR + ENV['RUBYLIB'].to_s
|
13
|
+
end
|
14
|
+
|
15
|
+
After do
|
16
|
+
ENV['RUBYLIB'] = @original_rubylib
|
17
|
+
|
18
|
+
FileUtils.rm_rf(Dir.glob('/tmp/uricp/*'))
|
19
|
+
|
20
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
@orbit
|
2
|
+
@orbitdownloads
|
3
|
+
Feature: User authenticated download of images from orbit
|
4
|
+
In order to download images from orbit
|
5
|
+
As a command line user
|
6
|
+
I want to retrieve the URI via an optional cache and copy correctly to target in the right format using a user id directly
|
7
|
+
|
8
|
+
Background:
|
9
|
+
Given an empty directory named "/tmp/uricp"
|
10
|
+
And the default aruba timeout is 15 seconds
|
11
|
+
And a container called "test"
|
12
|
+
|
13
|
+
Scenario: qcow download no conversion, no cache with userid
|
14
|
+
When I retrieve "img-qcow2" from container "test" into "file:///tmp/uricp/srv-test1" with a userid
|
15
|
+
Then a file named "/tmp/uricp/srv-test1" should exist
|
16
|
+
And the file named "/tmp/uricp/srv-test1" should have a file format of "qcow2v3"
|
17
|
+
|
18
|
+
Scenario: direct upload with userid
|
19
|
+
Given a 102400 byte file named "/tmp/uricp/srv-testy"
|
20
|
+
When I store "img-usrid" into container "test" from "file:///tmp/uricp/srv-testy" with a userid
|
21
|
+
Then a 102400 byte entry should exist in container "test" called "img-usrid"
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require "uricp/version"
|
2
|
+
require "uricp/curl_primitives"
|
3
|
+
require "uricp/orbit_auth"
|
4
|
+
require "uricp/segmenter"
|
5
|
+
|
6
|
+
module Uricp
|
7
|
+
|
8
|
+
UnsupportedURLtype = Class.new(ArgumentError)
|
9
|
+
|
10
|
+
end
|
11
|
+
|
12
|
+
#Monkey patch a copy_stream facility in using 'sendfile'
|
13
|
+
unless IO.respond_to? :copy_stream
|
14
|
+
require 'sendfile'
|
15
|
+
|
16
|
+
def IO.copy_stream(src, dst, copy_length=nil, offset=nil)
|
17
|
+
unless src.stat.pipe?
|
18
|
+
current_pos = src.pos
|
19
|
+
count = dst.sendfile(src, offset || current_pos, copy_length)
|
20
|
+
src.seek(count, IO::SEEK_CUR)
|
21
|
+
return count
|
22
|
+
else
|
23
|
+
amount = copy_length.to_i
|
24
|
+
buf_size = 2**16
|
25
|
+
buffer=""
|
26
|
+
while amount > 0 do
|
27
|
+
src.read(buf_size, buffer)
|
28
|
+
amount_read = buffer.length
|
29
|
+
dst.write(buffer)
|
30
|
+
amount -= amount_read
|
31
|
+
break if src.eof?
|
32
|
+
end
|
33
|
+
return copy_length.to_i - amount
|
34
|
+
end
|
35
|
+
rescue EOFError
|
36
|
+
0
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Uricp::CurlPrimitives
|
2
|
+
|
3
|
+
attr_reader :options
|
4
|
+
|
5
|
+
def from
|
6
|
+
options['from_uri']
|
7
|
+
end
|
8
|
+
|
9
|
+
def from=(target)
|
10
|
+
options['from_uri'] = target
|
11
|
+
end
|
12
|
+
|
13
|
+
def to
|
14
|
+
options['to_uri']
|
15
|
+
end
|
16
|
+
|
17
|
+
def to=(target)
|
18
|
+
options['to_uri'] = target
|
19
|
+
end
|
20
|
+
|
21
|
+
def curl_command
|
22
|
+
"curl --fail --silent"
|
23
|
+
end
|
24
|
+
|
25
|
+
def authentication
|
26
|
+
"-H X-Auth-Token:#{options['auth-token']}" if http_authentication?
|
27
|
+
end
|
28
|
+
|
29
|
+
def http_authentication?
|
30
|
+
options['auth-token']
|
31
|
+
end
|
32
|
+
|
33
|
+
def curl_upload_from(source, destination = to)
|
34
|
+
"#{curl_command} #{authentication} -T #{source} #{destination.to_s};"
|
35
|
+
end
|
36
|
+
|
37
|
+
def curl_download_to_pipe
|
38
|
+
"#{curl_command} #{authentication} #{from.to_s} |"
|
39
|
+
end
|
40
|
+
|
41
|
+
def curl_manifest(object_manifest, destination = to)
|
42
|
+
"#{curl_command} #{authentication} -X PUT -H 'X-Object-Manifest: #{object_manifest}' #{destination.to_s} --data-binary ''"
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'open-uri'
|
2
|
+
module Uricp
|
3
|
+
|
4
|
+
class OrbitAuth
|
5
|
+
|
6
|
+
AuthenticationFailure = Class.new(ArgumentError)
|
7
|
+
|
8
|
+
def initialize(auth_uri, auth_id, auth_key)
|
9
|
+
auth_uri.open(
|
10
|
+
'X-Auth-User' => auth_id,
|
11
|
+
'X-Auth-Key' => auth_key,
|
12
|
+
'Range' => 'bytes=0-0'
|
13
|
+
) do |uri|
|
14
|
+
@storage_url = uri.meta['x-storage-url']
|
15
|
+
@token = uri.meta['x-auth-token']
|
16
|
+
end
|
17
|
+
rescue OpenURI::HTTPError => e
|
18
|
+
raise AuthenticationFailure, "Cannot authenticate against #{auth_uri}"
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :storage_url, :token
|
22
|
+
|
23
|
+
def self.validate_options(options)
|
24
|
+
if options['auth-token'] && (options['auth-key'] || options['auth-user'])
|
25
|
+
raise ::OptionParser::NeedlessArgument,
|
26
|
+
"use either key based or token based authentication"
|
27
|
+
end
|
28
|
+
if options['auth-key'].nil? ^ options['auth-user'].nil?
|
29
|
+
raise ::OptionParser::MissingArgument,
|
30
|
+
"'auth-user' requires 'auth-key'"
|
31
|
+
end
|
32
|
+
if (options['auth-token'] || options['auth-user']) && options['auth_uri'].nil?
|
33
|
+
raise ::OptionParser::NeedlessArgument,
|
34
|
+
"authentication is for http uris only"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.add_auth_token(options)
|
39
|
+
if options['auth-user']
|
40
|
+
orbit_credentials = self.new(options['auth_uri'],
|
41
|
+
options['auth-user'], options['auth-key'])
|
42
|
+
options['auth-token'] = orbit_credentials.token
|
43
|
+
options.delete('auth-key')
|
44
|
+
options.delete('auth-user')
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.add_auth_to_optionparser(app)
|
49
|
+
app.on("--auth-token AUTH_TOKEN",
|
50
|
+
"Use AUTH_TOKEN for non-local requests")
|
51
|
+
app.on("--auth-user AUTH_USER",
|
52
|
+
"Use AUTH_USER for authentication")
|
53
|
+
app.on("--auth-key AUTH_KEY",
|
54
|
+
"Use AUTH_KEY for authentication")
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
|
3
|
+
module Uricp
|
4
|
+
|
5
|
+
class Segmenter
|
6
|
+
|
7
|
+
include Methadone::CLILogging
|
8
|
+
include Methadone::SH
|
9
|
+
include Uricp::CurlPrimitives
|
10
|
+
|
11
|
+
def initialize(options)
|
12
|
+
@options = options
|
13
|
+
source = options['from']
|
14
|
+
if @source = source && open(source)
|
15
|
+
@stream = File.pipe?(source) || File.chardev?(source)
|
16
|
+
else
|
17
|
+
@source = STDIN
|
18
|
+
@stream = true
|
19
|
+
end
|
20
|
+
split_path
|
21
|
+
end
|
22
|
+
|
23
|
+
def split_path
|
24
|
+
elements = Pathname.new(to.path).enum_for(:each_filename).to_a
|
25
|
+
elements.shift
|
26
|
+
@account=elements.shift
|
27
|
+
@container=elements.shift
|
28
|
+
@object_path=File.join(elements)
|
29
|
+
end
|
30
|
+
|
31
|
+
def upload
|
32
|
+
if large_upload?
|
33
|
+
segmented_upload
|
34
|
+
else
|
35
|
+
sh! curl_upload_from(options['from']),
|
36
|
+
:on_fail => "Upload to #{to} failed"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def segment_size
|
41
|
+
options['segment-size'].to_i
|
42
|
+
end
|
43
|
+
|
44
|
+
def stream?
|
45
|
+
@stream
|
46
|
+
end
|
47
|
+
|
48
|
+
def large_upload?
|
49
|
+
stream? || @source.stat.size > segment_size
|
50
|
+
end
|
51
|
+
|
52
|
+
def manifest_suffix
|
53
|
+
@manifest_suffix ||= [Time.now.to_f, @source.stat.size, segment_size].join('/')
|
54
|
+
end
|
55
|
+
|
56
|
+
def object_manifest
|
57
|
+
@object_manifest ||= File.join(@container, @object_path, manifest_suffix)+'/'
|
58
|
+
end
|
59
|
+
|
60
|
+
def segmented_upload
|
61
|
+
debug "#{self.class.name}: Large upload detected - segmenting into #{segment_size} byte chunks."
|
62
|
+
suffix = 0
|
63
|
+
until @source.eof?
|
64
|
+
debug "#{self.class.name}: Uploading segment #{suffix}"
|
65
|
+
upload_segment(suffix)
|
66
|
+
suffix = suffix.next
|
67
|
+
end
|
68
|
+
add_manifest
|
69
|
+
end
|
70
|
+
|
71
|
+
def upload_segment(segment_number)
|
72
|
+
segment_name = File.join(to.to_s, manifest_suffix, '%08d' % segment_number)
|
73
|
+
debug "Uploading with #{curl_upload_from('-', segment_name)}"
|
74
|
+
open('|'+curl_upload_from('-', segment_name), 'w') do |destination|
|
75
|
+
copy_length = IO.copy_stream(@source, destination, segment_size)
|
76
|
+
debug "#{self.class.name}: Uploaded #{copy_length} bytes to #{segment_name}"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def add_manifest
|
81
|
+
debug "Adding DLO object_manifest #{object_manifest}"
|
82
|
+
sh! curl_manifest(object_manifest),
|
83
|
+
:on_fail => "Upload to #{to} failed"
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module Uricp::Strategy
|
4
|
+
|
5
|
+
module CacheCommon
|
6
|
+
|
7
|
+
def validate_cache!
|
8
|
+
raise Uricp::MissingCache,
|
9
|
+
"No cache found at #{cache_root}. Expected a 'cache' and 'temp' directory" unless cache_exists?
|
10
|
+
raise Uricp::MissingCache,
|
11
|
+
"No cache name found" unless cache_name
|
12
|
+
end
|
13
|
+
|
14
|
+
def in_cache?
|
15
|
+
File.readable? cache_file
|
16
|
+
end
|
17
|
+
|
18
|
+
def cache_root
|
19
|
+
options['cache']
|
20
|
+
end
|
21
|
+
|
22
|
+
def cache_name
|
23
|
+
options['cache_name']
|
24
|
+
end
|
25
|
+
|
26
|
+
def cache_exists?
|
27
|
+
cache_root && %w{temp cache}.all? do |d|
|
28
|
+
File.directory?(File.join(cache_root, d))
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def temp_cache_file
|
33
|
+
@temp_cache_file ||= File.join(cache_root, 'temp', cache_name)
|
34
|
+
end
|
35
|
+
|
36
|
+
def temp_cache_uri
|
37
|
+
URI.join('file:///', temp_cache_file)
|
38
|
+
end
|
39
|
+
|
40
|
+
def cache_file
|
41
|
+
@cache_file ||= File.join(cache_root, 'cache', cache_name)
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
module Uricp::Strategy
|
4
|
+
|
5
|
+
class CachedGet
|
6
|
+
|
7
|
+
include Uricp::Strategy::Common
|
8
|
+
include Uricp::Strategy::CacheCommon
|
9
|
+
|
10
|
+
def appropriate?
|
11
|
+
if cache_root
|
12
|
+
validate_cache!
|
13
|
+
if in_cache? || file_source?
|
14
|
+
return proposal
|
15
|
+
else
|
16
|
+
debug "#{self.class.name}: no cache entry for #{options['from_uri']}"
|
17
|
+
end
|
18
|
+
else
|
19
|
+
debug "#{self.class.name}: not appropriate"
|
20
|
+
end
|
21
|
+
false
|
22
|
+
end
|
23
|
+
|
24
|
+
def command
|
25
|
+
":;"
|
26
|
+
end
|
27
|
+
|
28
|
+
def proposal
|
29
|
+
@proposed_options = options.dup
|
30
|
+
unless file_source?
|
31
|
+
@proposed_options['from_uri'] = URI.join('file:///', cache_file)
|
32
|
+
end
|
33
|
+
@proposed_options.delete('cache')
|
34
|
+
@proposed_options.delete('cache_name')
|
35
|
+
if conversion_required?
|
36
|
+
@proposed_options['source-format'] =
|
37
|
+
File.open(@proposed_options['from_uri'].path) {|f| encoding(f)}
|
38
|
+
if @proposed_options['source-format'] == @proposed_options['target-format']
|
39
|
+
@proposed_options.delete('source-format')
|
40
|
+
@proposed_options.delete('target-format')
|
41
|
+
end
|
42
|
+
end
|
43
|
+
self
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|