blobstore_client 0.3.13
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/README +1 -0
- data/Rakefile +50 -0
- data/lib/blobstore_client.rb +37 -0
- data/lib/blobstore_client/atmos_blobstore_client.rb +94 -0
- data/lib/blobstore_client/base.rb +83 -0
- data/lib/blobstore_client/client.rb +19 -0
- data/lib/blobstore_client/errors.rb +10 -0
- data/lib/blobstore_client/local_client.rb +49 -0
- data/lib/blobstore_client/s3_blobstore_client.rb +127 -0
- data/lib/blobstore_client/simple_blobstore_client.rb +54 -0
- data/lib/blobstore_client/version.rb +9 -0
- data/spec/assets/file +1 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/unit/atmos_blobstore_client_spec.rb +72 -0
- data/spec/unit/blobstore_client_spec.rb +34 -0
- data/spec/unit/local_client_spec.rb +78 -0
- data/spec/unit/s3_blobstore_client_spec.rb +230 -0
- data/spec/unit/simple_blobstore_client_spec.rb +106 -0
- metadata +130 -0
data/README
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Blobstore client for VMware AppCloud Outer Shell.
|
data/Rakefile
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
# Copyright (c) 2009-2012 VMware, Inc.
|
2
|
+
|
3
|
+
$:.unshift(File.expand_path("../../rake", __FILE__))
|
4
|
+
|
5
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __FILE__)
|
6
|
+
|
7
|
+
require "rubygems"
|
8
|
+
require "bundler"
|
9
|
+
Bundler.setup(:default, :test)
|
10
|
+
|
11
|
+
require "rake"
|
12
|
+
begin
|
13
|
+
require "rspec/core/rake_task"
|
14
|
+
rescue LoadError
|
15
|
+
end
|
16
|
+
|
17
|
+
require "bundler_task"
|
18
|
+
require "ci_task"
|
19
|
+
|
20
|
+
gem_helper = Bundler::GemHelper.new(Dir.pwd)
|
21
|
+
|
22
|
+
desc "Build Blobstore Client gem into the pkg directory"
|
23
|
+
task "build" do
|
24
|
+
gem_helper.build_gem
|
25
|
+
end
|
26
|
+
|
27
|
+
desc "Build and install Blobstore Client into system gems"
|
28
|
+
task "install" do
|
29
|
+
gem_helper.install_gem
|
30
|
+
end
|
31
|
+
|
32
|
+
BundlerTask.new
|
33
|
+
|
34
|
+
if defined?(RSpec)
|
35
|
+
namespace :spec do
|
36
|
+
desc "Run Unit Tests"
|
37
|
+
rspec_task = RSpec::Core::RakeTask.new(:unit) do |t|
|
38
|
+
t.gemfile = "Gemfile"
|
39
|
+
t.pattern = "spec/unit/**/*_spec.rb"
|
40
|
+
t.rspec_opts = %w(--format progress --colour)
|
41
|
+
end
|
42
|
+
|
43
|
+
CiTask.new do |task|
|
44
|
+
task.rspec_task = rspec_task
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
desc "Install dependencies and run tests"
|
49
|
+
task :spec => %w(spec:unit)
|
50
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# Copyright (c) 2009-2012 VMware, Inc.
|
2
|
+
|
3
|
+
module Bosh; module Blobstore; end; end
|
4
|
+
|
5
|
+
require "blobstore_client/version"
|
6
|
+
require "blobstore_client/errors"
|
7
|
+
|
8
|
+
require "blobstore_client/client"
|
9
|
+
require "blobstore_client/base"
|
10
|
+
require "blobstore_client/simple_blobstore_client"
|
11
|
+
require "blobstore_client/s3_blobstore_client"
|
12
|
+
require "blobstore_client/local_client"
|
13
|
+
require "blobstore_client/atmos_blobstore_client"
|
14
|
+
|
15
|
+
module Bosh
|
16
|
+
module Blobstore
|
17
|
+
class Client
|
18
|
+
|
19
|
+
PROVIDER_MAP = {
|
20
|
+
"simple" => SimpleBlobstoreClient,
|
21
|
+
"s3" => S3BlobstoreClient,
|
22
|
+
"atmos" => AtmosBlobstoreClient,
|
23
|
+
"local" => LocalClient
|
24
|
+
}
|
25
|
+
|
26
|
+
def self.create(provider, options = {})
|
27
|
+
p = PROVIDER_MAP[provider]
|
28
|
+
if p
|
29
|
+
p.new(options)
|
30
|
+
else
|
31
|
+
providers = PROVIDER_MAP.keys.sort.join(", ")
|
32
|
+
raise "Invalid client provider, available providers are: #{providers}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# Copyright (c) 2009-2012 VMware, Inc.
|
2
|
+
|
3
|
+
require "atmos"
|
4
|
+
require "uri"
|
5
|
+
require "multi_json"
|
6
|
+
|
7
|
+
module Bosh
|
8
|
+
module Blobstore
|
9
|
+
class AtmosBlobstoreClient < BaseClient
|
10
|
+
SHARE_URL_EXP = "1893484800" # expires on 2030 Jan-1
|
11
|
+
|
12
|
+
def initialize(options)
|
13
|
+
super(options)
|
14
|
+
@atmos_options = {
|
15
|
+
:url => @options[:url],
|
16
|
+
:uid => @options[:uid],
|
17
|
+
:secret => @options[:secret]
|
18
|
+
}
|
19
|
+
@tag = @options[:tag]
|
20
|
+
@http_client = HTTPClient.new
|
21
|
+
# TODO: Remove this line once we get the proper certificate for atmos
|
22
|
+
@http_client.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
23
|
+
end
|
24
|
+
|
25
|
+
def atmos_server
|
26
|
+
unless @atmos_options[:secret]
|
27
|
+
raise "Atmos password is missing (read-only mode)"
|
28
|
+
end
|
29
|
+
@atmos ||= Atmos::Store.new(@atmos_options)
|
30
|
+
end
|
31
|
+
|
32
|
+
def create_file(file)
|
33
|
+
obj_conf = {:data => file, :length => File.size(file.path)}
|
34
|
+
obj_conf[:listable_metadata] = {@tag => true} if @tag
|
35
|
+
object_id = atmos_server.create(obj_conf).aoid
|
36
|
+
encode_object_id(object_id)
|
37
|
+
end
|
38
|
+
|
39
|
+
def get_file(object_id, file)
|
40
|
+
object_info = decode_object_id(object_id)
|
41
|
+
oid = object_info["oid"]
|
42
|
+
sig = object_info["sig"]
|
43
|
+
|
44
|
+
url = @atmos_options[:url] + "/rest/objects/#{oid}?uid=" +
|
45
|
+
URI::escape(@atmos_options[:uid]) +
|
46
|
+
"&expires=#{SHARE_URL_EXP}&signature=#{URI::escape(sig)}"
|
47
|
+
|
48
|
+
response = @http_client.get(url) do |block|
|
49
|
+
file.write(block)
|
50
|
+
end
|
51
|
+
|
52
|
+
if response.status != 200
|
53
|
+
raise BlobstoreError, "Could not fetch object, %s/%s" %
|
54
|
+
[response.status, response.content]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def delete(object_id)
|
59
|
+
object_info = decode_object_id(object_id)
|
60
|
+
oid = object_info["oid"]
|
61
|
+
atmos_server.get(:id => oid).delete
|
62
|
+
rescue Atmos::Exceptions::NoSuchObjectException => e
|
63
|
+
raise NotFound, "Atmos object '#{object_id}' not found"
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def decode_object_id(object_id)
|
69
|
+
begin
|
70
|
+
object_info = MultiJson.decode(Base64.decode64(URI::unescape(object_id)))
|
71
|
+
rescue MultiJson::DecodeError => e
|
72
|
+
raise BlobstoreError, "Failed to parse object_id. " +
|
73
|
+
"Please try updating the release"
|
74
|
+
end
|
75
|
+
|
76
|
+
if !object_info.kind_of?(Hash) || object_info["oid"].nil? ||
|
77
|
+
object_info["sig"].nil?
|
78
|
+
raise BlobstoreError, "Invalid object_id (#{object_id})"
|
79
|
+
end
|
80
|
+
object_info
|
81
|
+
end
|
82
|
+
|
83
|
+
def encode_object_id(object_id)
|
84
|
+
hash_string = "GET" + "\n" + "/rest/objects/" + object_id + "\n" +
|
85
|
+
@atmos_options[:uid] + "\n" + SHARE_URL_EXP
|
86
|
+
secret = Base64.decode64(@atmos_options[:secret])
|
87
|
+
sig = HMAC::SHA1.digest(secret, hash_string)
|
88
|
+
signature = Base64.encode64(sig.to_s).chomp
|
89
|
+
json = MultiJson.encode({:oid => object_id, :sig => signature})
|
90
|
+
URI::escape(Base64.encode64(json))
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# Copyright (c) 2009-2012 VMware, Inc.
|
2
|
+
|
3
|
+
require "tmpdir"
|
4
|
+
|
5
|
+
module Bosh
|
6
|
+
module Blobstore
|
7
|
+
class BaseClient < Client
|
8
|
+
|
9
|
+
def initialize(options)
|
10
|
+
@options = symbolize_keys(options)
|
11
|
+
end
|
12
|
+
|
13
|
+
def symbolize_keys(hash)
|
14
|
+
hash.inject({}) do |h, (key, value)|
|
15
|
+
h[key.to_sym] = value
|
16
|
+
h
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def create_file(file)
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
def get_file(id, file)
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
def create(contents)
|
29
|
+
if contents.kind_of?(File)
|
30
|
+
create_file(contents)
|
31
|
+
else
|
32
|
+
temp_path do |path|
|
33
|
+
begin
|
34
|
+
File.open(path, "w") do |file|
|
35
|
+
file.write(contents)
|
36
|
+
end
|
37
|
+
create_file(File.open(path, "r"))
|
38
|
+
rescue BlobstoreError => e
|
39
|
+
raise e
|
40
|
+
rescue Exception => e
|
41
|
+
raise BlobstoreError,
|
42
|
+
"Failed to create object, underlying error: %s %s" %
|
43
|
+
[e.message, e.backtrace.join("\n")]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def get(id, file = nil)
|
50
|
+
if file
|
51
|
+
get_file(id, file)
|
52
|
+
else
|
53
|
+
result = nil
|
54
|
+
temp_path do |path|
|
55
|
+
begin
|
56
|
+
File.open(path, "w") { |file| get_file(id, file) }
|
57
|
+
result = File.open(path, "r") { |file| file.read }
|
58
|
+
rescue BlobstoreError => e
|
59
|
+
raise e
|
60
|
+
rescue Exception => e
|
61
|
+
raise BlobstoreError,
|
62
|
+
"Failed to create object, underlying error: %s %s" %
|
63
|
+
[e.message, e.backtrace.join("\n")]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
result
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
protected
|
71
|
+
|
72
|
+
def temp_path
|
73
|
+
path = File.join(Dir::tmpdir, "temp-path-#{UUIDTools::UUID.random_create}")
|
74
|
+
begin
|
75
|
+
yield path
|
76
|
+
ensure
|
77
|
+
FileUtils.rm_f(path)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# Copyright (c) 2009-2012 VMware, Inc.
|
2
|
+
|
3
|
+
module Bosh
|
4
|
+
module Blobstore
|
5
|
+
class LocalClient < BaseClient
|
6
|
+
CHUNK_SIZE = 1024*1024
|
7
|
+
|
8
|
+
def initialize(options)
|
9
|
+
super(options)
|
10
|
+
@blobstore_path = @options[:blobstore_path]
|
11
|
+
raise "No blobstore path given" if @blobstore_path.nil?
|
12
|
+
FileUtils.mkdir_p(@blobstore_path) unless File.directory?(@blobstore_path)
|
13
|
+
end
|
14
|
+
|
15
|
+
def create_file(file)
|
16
|
+
id = UUIDTools::UUID.random_create.to_s
|
17
|
+
dst = File.join(@blobstore_path, id)
|
18
|
+
File.open(dst, 'w') do |fh|
|
19
|
+
until file.eof?
|
20
|
+
fh.write(file.read(CHUNK_SIZE))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
id
|
24
|
+
end
|
25
|
+
|
26
|
+
def get_file(id, file)
|
27
|
+
src = File.join(@blobstore_path, id)
|
28
|
+
|
29
|
+
begin
|
30
|
+
File.open(src, 'r') do |src_fh|
|
31
|
+
until src_fh.eof?
|
32
|
+
file.write(src_fh.read(CHUNK_SIZE))
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
rescue Errno::ENOENT
|
37
|
+
raise NotFound, "Blobstore object '#{id}' not found"
|
38
|
+
end
|
39
|
+
|
40
|
+
def delete(id)
|
41
|
+
file = File.join(@blobstore_path, id)
|
42
|
+
FileUtils.rm(file)
|
43
|
+
rescue Errno::ENOENT
|
44
|
+
raise NotFound, "Blobstore object '#{id}' not found"
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# Copyright (c) 2009-2012 VMware, Inc.
|
2
|
+
|
3
|
+
require "openssl"
|
4
|
+
require "digest/sha1"
|
5
|
+
require "base64"
|
6
|
+
require "aws/s3"
|
7
|
+
require "uuidtools"
|
8
|
+
|
9
|
+
module Bosh
|
10
|
+
module Blobstore
|
11
|
+
|
12
|
+
class S3BlobstoreClient < BaseClient
|
13
|
+
|
14
|
+
DEFAULT_CIPHER_NAME = "aes-128-cbc"
|
15
|
+
|
16
|
+
attr_reader :bucket_name, :encryption_key
|
17
|
+
|
18
|
+
def initialize(options)
|
19
|
+
super(options)
|
20
|
+
@bucket_name = @options[:bucket_name]
|
21
|
+
@encryption_key = @options[:encryption_key]
|
22
|
+
|
23
|
+
aws_options = {
|
24
|
+
:access_key_id => @options[:access_key_id],
|
25
|
+
:secret_access_key => @options[:secret_access_key],
|
26
|
+
:use_ssl => true,
|
27
|
+
:port => 443
|
28
|
+
}
|
29
|
+
|
30
|
+
AWS::S3::Base.establish_connection!(aws_options)
|
31
|
+
rescue AWS::S3::S3Exception => e
|
32
|
+
raise BlobstoreError, "Failed to initialize S3 blobstore: #{e.message}"
|
33
|
+
end
|
34
|
+
|
35
|
+
def create_file(file)
|
36
|
+
object_id = generate_object_id
|
37
|
+
temp_path do |path|
|
38
|
+
File.open(path, "w") do |temp_file|
|
39
|
+
encrypt_stream(file, temp_file)
|
40
|
+
end
|
41
|
+
File.open(path, "r") do |temp_file|
|
42
|
+
AWS::S3::S3Object.store(object_id, temp_file, bucket_name)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
object_id
|
46
|
+
rescue AWS::S3::S3Exception => e
|
47
|
+
raise BlobstoreError,
|
48
|
+
"Failed to create object, S3 response error: #{e.message}"
|
49
|
+
end
|
50
|
+
|
51
|
+
def get_file(object_id, file)
|
52
|
+
object = AWS::S3::S3Object.find(object_id, bucket_name)
|
53
|
+
from = lambda { |callback|
|
54
|
+
object.value { |segment|
|
55
|
+
# Looks like the aws code calls this block even if segment is empty.
|
56
|
+
# Ideally it should be fixed upstream in the aws gem.
|
57
|
+
unless segment.empty?
|
58
|
+
callback.call(segment)
|
59
|
+
end
|
60
|
+
}
|
61
|
+
}
|
62
|
+
decrypt_stream(from, file)
|
63
|
+
rescue AWS::S3::NoSuchKey => e
|
64
|
+
raise NotFound, "S3 object '#{object_id}' not found"
|
65
|
+
rescue AWS::S3::S3Exception => e
|
66
|
+
raise BlobstoreError,
|
67
|
+
"Failed to find object '#{object_id}', S3 response error: #{e.message}"
|
68
|
+
end
|
69
|
+
|
70
|
+
def delete(object_id)
|
71
|
+
AWS::S3::S3Object.delete(object_id, bucket_name)
|
72
|
+
rescue AWS::S3::S3Exception => e
|
73
|
+
raise BlobstoreError,
|
74
|
+
"Failed to delete object '#{object_id}', S3 response error: #{e.message}"
|
75
|
+
end
|
76
|
+
|
77
|
+
protected
|
78
|
+
|
79
|
+
def generate_object_id
|
80
|
+
UUIDTools::UUID.random_create.to_s
|
81
|
+
end
|
82
|
+
|
83
|
+
def encrypt_stream(from, to)
|
84
|
+
cipher = OpenSSL::Cipher::Cipher.new(DEFAULT_CIPHER_NAME)
|
85
|
+
cipher.encrypt
|
86
|
+
cipher.key = Digest::SHA1.digest(encryption_key)[0..cipher.key_len-1]
|
87
|
+
|
88
|
+
to_stream = write_stream(to)
|
89
|
+
read_stream(from) { |segment| to_stream.call(cipher.update(segment)) }
|
90
|
+
to_stream.call(cipher.final)
|
91
|
+
rescue StandardError => e
|
92
|
+
raise BlobstoreError, "Encryption error: #{e}"
|
93
|
+
end
|
94
|
+
|
95
|
+
def decrypt_stream(from, to)
|
96
|
+
cipher = OpenSSL::Cipher::Cipher.new(DEFAULT_CIPHER_NAME)
|
97
|
+
cipher.decrypt
|
98
|
+
cipher.key = Digest::SHA1.digest(encryption_key)[0..cipher.key_len-1]
|
99
|
+
|
100
|
+
to_stream = write_stream(to)
|
101
|
+
read_stream(from) { |segment| to_stream.call(cipher.update(segment)) }
|
102
|
+
to_stream.call(cipher.final)
|
103
|
+
rescue StandardError => e
|
104
|
+
raise BlobstoreError, "Decryption error: #{e}"
|
105
|
+
end
|
106
|
+
|
107
|
+
def read_stream(stream, &block)
|
108
|
+
if stream.respond_to?(:read)
|
109
|
+
while contents = stream.read(32768)
|
110
|
+
block.call(contents)
|
111
|
+
end
|
112
|
+
elsif stream.kind_of?(Proc)
|
113
|
+
stream.call(block)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def write_stream(stream)
|
118
|
+
if stream.respond_to?(:write)
|
119
|
+
lambda { |contents| stream.write(contents)}
|
120
|
+
elsif stream.kind_of?(Proc)
|
121
|
+
stream
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# Copyright (c) 2009-2012 VMware, Inc.
|
2
|
+
|
3
|
+
require "httpclient"
|
4
|
+
|
5
|
+
module Bosh
|
6
|
+
module Blobstore
|
7
|
+
class SimpleBlobstoreClient < BaseClient
|
8
|
+
|
9
|
+
def initialize(options)
|
10
|
+
super(options)
|
11
|
+
@client = HTTPClient.new
|
12
|
+
@endpoint = @options[:endpoint]
|
13
|
+
@headers = {}
|
14
|
+
user = @options[:user]
|
15
|
+
password = @options[:password]
|
16
|
+
if user && password
|
17
|
+
@headers["Authorization"] = "Basic " +
|
18
|
+
Base64.encode64("#{user}:#{password}").strip
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def url(id=nil)
|
23
|
+
["#{@endpoint}/resources", id].compact.join("/")
|
24
|
+
end
|
25
|
+
|
26
|
+
def create_file(file)
|
27
|
+
response = @client.post(url, {:content => file}, @headers)
|
28
|
+
if response.status != 200
|
29
|
+
raise BlobstoreError,
|
30
|
+
"Could not create object, #{response.status}/#{response.content}"
|
31
|
+
end
|
32
|
+
response.content
|
33
|
+
end
|
34
|
+
|
35
|
+
def get_file(id, file)
|
36
|
+
response = @client.get(url(id), {}, @headers) do |block|
|
37
|
+
file.write(block)
|
38
|
+
end
|
39
|
+
|
40
|
+
if response.status != 200
|
41
|
+
raise BlobstoreError,
|
42
|
+
"Could not fetch object, #{response.status}/#{response.content}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def delete(id)
|
47
|
+
response = @client.delete(url(id), @headers)
|
48
|
+
if response.status != 204
|
49
|
+
raise "Could not delete object, #{response.status}/#{response.content}"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
data/spec/assets/file
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
foobar
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
2
|
+
|
3
|
+
describe Bosh::Blobstore::AtmosBlobstoreClient do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
@atmos = mock("atmos")
|
7
|
+
Atmos::Store.stub!(:new).and_return(@atmos)
|
8
|
+
atmos_opt = {:url => "http://localhost",
|
9
|
+
:uid => "uid",
|
10
|
+
:secret => "secret"}
|
11
|
+
|
12
|
+
@http_client = mock("http-client")
|
13
|
+
ssl_opt = mock("ssl-opt")
|
14
|
+
ssl_opt.stub!(:verify_mode=)
|
15
|
+
@http_client.stub!(:ssl_config).and_return(ssl_opt)
|
16
|
+
|
17
|
+
HTTPClient.stub!(:new).and_return(@http_client)
|
18
|
+
@client = Bosh::Blobstore::AtmosBlobstoreClient.new(atmos_opt)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should create an object" do
|
22
|
+
data = "some content"
|
23
|
+
object = mock("object")
|
24
|
+
|
25
|
+
@atmos.should_receive(:create).with {|opt|
|
26
|
+
opt[:data].read.should eql data
|
27
|
+
opt[:length].should eql data.length
|
28
|
+
}.and_return(object)
|
29
|
+
|
30
|
+
object.should_receive(:aoid).and_return("test-key")
|
31
|
+
|
32
|
+
object_id = @client.create(data)
|
33
|
+
object_info = MultiJson.decode(Base64.decode64(URI::unescape(object_id)))
|
34
|
+
object_info["oid"].should eql("test-key")
|
35
|
+
object_info["sig"].should_not be_nil
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should delete an object" do
|
39
|
+
object = mock("object")
|
40
|
+
@atmos.should_receive(:get).with(:id => "test-key").and_return(object)
|
41
|
+
object.should_receive(:delete)
|
42
|
+
id = URI::escape(Base64.encode64(MultiJson.encode({:oid => "test-key", :sig => "sig"})))
|
43
|
+
@client.delete(id)
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should fetch an object" do
|
47
|
+
url = "http://localhost/rest/objects/test-key?uid=uid&expires=1893484800&signature=sig"
|
48
|
+
response = mock("response")
|
49
|
+
response.stub!(:status).and_return(200)
|
50
|
+
@http_client.should_receive(:get).with(url).and_yield("some-content").and_return(response)
|
51
|
+
id = URI::escape(Base64.encode64(MultiJson.encode({:oid => "test-key", :sig => "sig"})))
|
52
|
+
@client.get(id).should eql("some-content")
|
53
|
+
end
|
54
|
+
|
55
|
+
it "should refuse to create object without the password" do
|
56
|
+
lambda {
|
57
|
+
no_pass_client = Bosh::Blobstore::AtmosBlobstoreClient.new(:url => "http://localhost", :uid => "uid")
|
58
|
+
no_pass_client.create("foo")
|
59
|
+
}.should raise_error(Bosh::Blobstore::BlobstoreError)
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should be able to read without password" do
|
63
|
+
no_pass_client = Bosh::Blobstore::AtmosBlobstoreClient.new(:url => "http://localhost", :uid => "uid")
|
64
|
+
|
65
|
+
url = "http://localhost/rest/objects/test-key?uid=uid&expires=1893484800&signature=sig"
|
66
|
+
response = mock("response")
|
67
|
+
response.stub!(:status).and_return(200)
|
68
|
+
@http_client.should_receive(:get).with(url).and_yield("some-content").and_return(response)
|
69
|
+
id = URI::escape(Base64.encode64(MultiJson.encode({:oid => "test-key", :sig => "sig"})))
|
70
|
+
no_pass_client.get(id).should eql("some-content")
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
2
|
+
|
3
|
+
describe Bosh::Blobstore::Client do
|
4
|
+
|
5
|
+
it "should have a local provider" do
|
6
|
+
Dir.mktmpdir do |tmp|
|
7
|
+
bs = Bosh::Blobstore::Client.create('local', {:blobstore_path => tmp})
|
8
|
+
bs.should be_instance_of Bosh::Blobstore::LocalClient
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should have an simple provider" do
|
13
|
+
bs = Bosh::Blobstore::Client.create('simple', {})
|
14
|
+
bs.should be_instance_of Bosh::Blobstore::SimpleBlobstoreClient
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should have an atmos provider" do
|
18
|
+
bs = Bosh::Blobstore::Client.create('atmos', {})
|
19
|
+
bs.should be_instance_of Bosh::Blobstore::AtmosBlobstoreClient
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should have an s3 provider" do
|
23
|
+
options = {:access_key_id => "foo", :secret_access_key => "bar"}
|
24
|
+
bs = Bosh::Blobstore::Client.create('s3', options)
|
25
|
+
bs.should be_instance_of Bosh::Blobstore::S3BlobstoreClient
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should raise an exception on an unknown client" do
|
29
|
+
lambda {
|
30
|
+
bs = Bosh::Blobstore::Client.create('foobar', {})
|
31
|
+
}.should raise_error /^Invalid client provider/
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
2
|
+
|
3
|
+
describe Bosh::Blobstore::LocalClient do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
@tmp = Dir.mktmpdir
|
7
|
+
@options = {"blobstore_path" => @tmp}
|
8
|
+
end
|
9
|
+
|
10
|
+
after(:each) do
|
11
|
+
FileUtils.rm_rf(@tmp)
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should require blobstore_path option" do
|
15
|
+
lambda {
|
16
|
+
client = Bosh::Blobstore::LocalClient.new({})
|
17
|
+
}.should raise_error
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should create blobstore_path direcory if it doesn't exist'" do
|
21
|
+
dir = File.join(@tmp, "blobstore")
|
22
|
+
client = Bosh::Blobstore::LocalClient.new({"blobstore_path" => dir})
|
23
|
+
File.directory?(dir).should be_true
|
24
|
+
end
|
25
|
+
|
26
|
+
describe "operations" do
|
27
|
+
|
28
|
+
describe "get" do
|
29
|
+
it "should retrive the correct contents" do
|
30
|
+
File.open(File.join(@tmp, 'foo'), 'w') do |fh|
|
31
|
+
fh.puts("bar")
|
32
|
+
end
|
33
|
+
|
34
|
+
client = Bosh::Blobstore::LocalClient.new(@options)
|
35
|
+
client.get("foo").should == "bar\n"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe "create" do
|
40
|
+
it "should store a file" do
|
41
|
+
test_file = File.join(File.dirname(__FILE__), "../assets/file")
|
42
|
+
client = Bosh::Blobstore::LocalClient.new(@options)
|
43
|
+
fh = File.open(test_file)
|
44
|
+
id = client.create(fh)
|
45
|
+
fh.close
|
46
|
+
original = File.new(test_file).readlines
|
47
|
+
stored = File.new(File.join(@tmp, id)).readlines
|
48
|
+
stored.should == original
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should store a string" do
|
52
|
+
client = Bosh::Blobstore::LocalClient.new(@options)
|
53
|
+
string = "foobar"
|
54
|
+
id = client.create(string)
|
55
|
+
stored = File.new(File.join(@tmp, id)).readlines
|
56
|
+
stored.should == [string]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe "delete" do
|
61
|
+
it "should delete an id" do
|
62
|
+
client = Bosh::Blobstore::LocalClient.new(@options)
|
63
|
+
string = "foobar"
|
64
|
+
id = client.create(string)
|
65
|
+
client.delete(id)
|
66
|
+
File.exist?(File.join(@tmp, id)).should_not be_true
|
67
|
+
end
|
68
|
+
|
69
|
+
it "should raise NotFound error when trying to delete a missing id" do
|
70
|
+
client = Bosh::Blobstore::LocalClient.new(@options)
|
71
|
+
lambda {
|
72
|
+
client.delete("missing")
|
73
|
+
}.should raise_error Bosh::Blobstore::NotFound
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,230 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
2
|
+
|
3
|
+
describe Bosh::Blobstore::S3BlobstoreClient do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
@aws_mock_options = {
|
7
|
+
:access_key_id => "KEY",
|
8
|
+
:secret_access_key => "SECRET",
|
9
|
+
:use_ssl => true,
|
10
|
+
:port => 443
|
11
|
+
}
|
12
|
+
end
|
13
|
+
|
14
|
+
def s3_blobstore(options)
|
15
|
+
Bosh::Blobstore::S3BlobstoreClient.new(options)
|
16
|
+
end
|
17
|
+
|
18
|
+
describe "options" do
|
19
|
+
|
20
|
+
it "establishes S3 connection on creation" do
|
21
|
+
AWS::S3::Base.should_receive(:establish_connection!).with(@aws_mock_options)
|
22
|
+
|
23
|
+
@client = s3_blobstore("encryption_key" => "bla",
|
24
|
+
"bucket_name" => "test",
|
25
|
+
"access_key_id" => "KEY",
|
26
|
+
"secret_access_key" => "SECRET")
|
27
|
+
|
28
|
+
@client.encryption_key.should == "bla"
|
29
|
+
@client.bucket_name.should == "test"
|
30
|
+
end
|
31
|
+
|
32
|
+
it "supports Symbol option keys too" do
|
33
|
+
AWS::S3::Base.should_receive(:establish_connection!).with(@aws_mock_options)
|
34
|
+
|
35
|
+
@client = s3_blobstore(:encryption_key => "bla",
|
36
|
+
:bucket_name => "test",
|
37
|
+
:access_key_id => "KEY",
|
38
|
+
:secret_access_key => "SECRET")
|
39
|
+
|
40
|
+
@client.encryption_key.should == "bla"
|
41
|
+
@client.bucket_name.should == "test"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe "operations" do
|
46
|
+
|
47
|
+
before :each do
|
48
|
+
@client = s3_blobstore(:encryption_key => "bla",
|
49
|
+
:bucket_name => "test",
|
50
|
+
:access_key_id => "KEY",
|
51
|
+
:secret_access_key => "SECRET")
|
52
|
+
end
|
53
|
+
|
54
|
+
describe "create" do
|
55
|
+
|
56
|
+
it "should create an object" do
|
57
|
+
encrypted_file = nil
|
58
|
+
@client.should_receive(:generate_object_id).and_return("object_id")
|
59
|
+
@client.should_receive(:encrypt_stream).with { |from_file, _|
|
60
|
+
from_file.read.should eql("some content")
|
61
|
+
true
|
62
|
+
}.and_return {|_, to_file|
|
63
|
+
encrypted_file = to_file
|
64
|
+
nil
|
65
|
+
}
|
66
|
+
|
67
|
+
AWS::S3::S3Object.should_receive(:store).with { |key, data, bucket|
|
68
|
+
key.should eql("object_id")
|
69
|
+
data.path.should eql(encrypted_file.path)
|
70
|
+
bucket.should eql("test")
|
71
|
+
true
|
72
|
+
}
|
73
|
+
@client.create("some content").should eql("object_id")
|
74
|
+
end
|
75
|
+
|
76
|
+
it "should raise an exception when there is an error creating an object" do
|
77
|
+
encrypted_file = nil
|
78
|
+
@client.should_receive(:generate_object_id).and_return("object_id")
|
79
|
+
@client.should_receive(:encrypt_stream).with { |from_file, _|
|
80
|
+
from_file.read.should eql("some content")
|
81
|
+
true
|
82
|
+
}.and_return {|_, to_file|
|
83
|
+
encrypted_file = to_file
|
84
|
+
nil
|
85
|
+
}
|
86
|
+
|
87
|
+
AWS::S3::S3Object.should_receive(:store).with { |key, data, bucket|
|
88
|
+
key.should eql("object_id")
|
89
|
+
data.path.should eql(encrypted_file.path)
|
90
|
+
bucket.should eql("test")
|
91
|
+
true
|
92
|
+
}.and_raise(AWS::S3::S3Exception.new("Epic Fail"))
|
93
|
+
lambda {
|
94
|
+
@client.create("some content")
|
95
|
+
}.should raise_error(Bosh::Blobstore::BlobstoreError, "Failed to create object, S3 response error: Epic Fail")
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
|
100
|
+
describe "fetch" do
|
101
|
+
|
102
|
+
it "should fetch an object" do
|
103
|
+
mock_s3_object = mock("s3_object")
|
104
|
+
mock_s3_object.stub!(:value).and_yield("ENCRYPTED")
|
105
|
+
AWS::S3::S3Object.should_receive(:find).with("object_id", "test").and_return(mock_s3_object)
|
106
|
+
@client.should_receive(:decrypt_stream).with { |from, _|
|
107
|
+
encrypted = ""
|
108
|
+
from.call(lambda {|segment| encrypted << segment})
|
109
|
+
encrypted.should eql("ENCRYPTED")
|
110
|
+
true
|
111
|
+
}.and_return {|_, to|
|
112
|
+
to.write("stuff")
|
113
|
+
}
|
114
|
+
@client.get("object_id").should == "stuff"
|
115
|
+
end
|
116
|
+
|
117
|
+
it "should raise an exception when there is an error fetching an object" do
|
118
|
+
AWS::S3::S3Object.should_receive(:find).with("object_id", "test").and_raise(AWS::S3::S3Exception.new("Epic Fail"))
|
119
|
+
lambda {
|
120
|
+
@client.get("object_id")
|
121
|
+
}.should raise_error(Bosh::Blobstore::BlobstoreError, "Failed to find object 'object_id', S3 response error: Epic Fail")
|
122
|
+
end
|
123
|
+
|
124
|
+
it "should raise more specific NotFound exception when object is not found" do
|
125
|
+
AWS::S3::S3Object.should_receive(:find).with("object_id", "test").and_raise(AWS::S3::NoSuchKey.new("NO KEY", "test"))
|
126
|
+
lambda {
|
127
|
+
@client.get("object_id")
|
128
|
+
}.should raise_error(Bosh::Blobstore::BlobstoreError, "S3 object 'object_id' not found")
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
|
133
|
+
describe "delete" do
|
134
|
+
|
135
|
+
it "should delete an object" do
|
136
|
+
AWS::S3::S3Object.should_receive(:delete).with("object_id", "test")
|
137
|
+
@client.delete("object_id")
|
138
|
+
end
|
139
|
+
|
140
|
+
it "should raise an exception when there is an error deleting an object" do
|
141
|
+
AWS::S3::S3Object.should_receive(:delete).with("object_id", "test").and_raise(AWS::S3::S3Exception.new("Epic Fail"))
|
142
|
+
lambda {
|
143
|
+
@client.delete("object_id")
|
144
|
+
}.should raise_error(Bosh::Blobstore::BlobstoreError, "Failed to delete object 'object_id', S3 response error: Epic Fail")
|
145
|
+
end
|
146
|
+
|
147
|
+
end
|
148
|
+
|
149
|
+
describe "encryption" do
|
150
|
+
|
151
|
+
before :each do
|
152
|
+
@from_path = File.join(Dir::tmpdir, "from-#{UUIDTools::UUID.random_create}")
|
153
|
+
@to_path = File.join(Dir::tmpdir, "to-#{UUIDTools::UUID.random_create}")
|
154
|
+
end
|
155
|
+
|
156
|
+
after :each do
|
157
|
+
FileUtils.rm_f(@from_path)
|
158
|
+
FileUtils.rm_f(@to_path)
|
159
|
+
end
|
160
|
+
|
161
|
+
it "encrypt/decrypt works as long as key is the same" do
|
162
|
+
File.open(@from_path, "w") { |f| f.write("clear text") }
|
163
|
+
File.open(@from_path, "r") do |from|
|
164
|
+
File.open(@to_path, "w") do |to|
|
165
|
+
@client.send(:encrypt_stream, from, to)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
Base64.encode64(File.read(@to_path)).should eql("XCUKDXXzjh43DmNylgVpQQ==\n")
|
170
|
+
|
171
|
+
File.open(@from_path, "w") { |f| f.write(File.read(@to_path)) }
|
172
|
+
File.open(@from_path, "r") do |from|
|
173
|
+
File.open(@to_path, "w") do |to|
|
174
|
+
@client.send(:decrypt_stream, from, to)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
File.read(@to_path).should eql("clear text")
|
179
|
+
end
|
180
|
+
|
181
|
+
it "encrypt/decrypt doesn't have padding issues for very small inputs" do
|
182
|
+
File.open(@from_path, "w") { |f| f.write("c") }
|
183
|
+
File.open(@from_path, "r") do |from|
|
184
|
+
File.open(@to_path, "w") do |to|
|
185
|
+
@client.send(:encrypt_stream, from, to)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
Base64.encode64(File.read(@to_path)).should eql("S1ZnX5gPfm/rQbRCcShHSg==\n")
|
190
|
+
|
191
|
+
File.open(@from_path, "w") { |f| f.write(File.read(@to_path)) }
|
192
|
+
File.open(@from_path, "r") do |from|
|
193
|
+
File.open(@to_path, "w") do |to|
|
194
|
+
@client.send(:decrypt_stream, from, to)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
File.read(@to_path).should eql("c")
|
199
|
+
end
|
200
|
+
|
201
|
+
it "should raise an exception if incorrect encryption key is used" do
|
202
|
+
File.open(@from_path, "w") { |f| f.write("clear text") }
|
203
|
+
File.open(@from_path, "r") do |from|
|
204
|
+
File.open(@to_path, "w") do |to|
|
205
|
+
@client.send(:encrypt_stream, from, to)
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
Base64.encode64(File.read(@to_path)).should eql("XCUKDXXzjh43DmNylgVpQQ==\n")
|
210
|
+
|
211
|
+
client2 = s3_blobstore(:encryption_key => "zzz",
|
212
|
+
:bucket_name => "test",
|
213
|
+
:access_key_id => "KEY",
|
214
|
+
:secret_access_key => "SECRET")
|
215
|
+
|
216
|
+
File.open(@from_path, "w") { |f| f.write(File.read(@to_path)) }
|
217
|
+
File.open(@from_path, "r") do |from|
|
218
|
+
File.open(@to_path, "w") do |to|
|
219
|
+
lambda {
|
220
|
+
client2.send(:decrypt_stream, from, to)
|
221
|
+
}.should raise_error(Bosh::Blobstore::BlobstoreError, "Decryption error: bad decrypt")
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
end
|
227
|
+
|
228
|
+
end
|
229
|
+
|
230
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
2
|
+
|
3
|
+
describe Bosh::Blobstore::SimpleBlobstoreClient do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
@httpclient = mock("httpclient")
|
7
|
+
HTTPClient.stub!(:new).and_return(@httpclient)
|
8
|
+
end
|
9
|
+
|
10
|
+
describe "options" do
|
11
|
+
|
12
|
+
it "should set up authentication when present" do
|
13
|
+
response = mock("response")
|
14
|
+
response.stub!(:status).and_return(200)
|
15
|
+
response.stub!(:content).and_return("content_id")
|
16
|
+
|
17
|
+
@httpclient.should_receive(:get).with("http://localhost/resources/foo", {},
|
18
|
+
{"Authorization"=>"Basic am9objpzbWl0aA=="}).and_return(response)
|
19
|
+
@client = Bosh::Blobstore::SimpleBlobstoreClient.new({"endpoint" => "http://localhost",
|
20
|
+
"user" => "john",
|
21
|
+
"password" => "smith"})
|
22
|
+
@client.get("foo")
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
describe "operations" do
|
28
|
+
|
29
|
+
it "should create an object" do
|
30
|
+
response = mock("response")
|
31
|
+
response.stub!(:status).and_return(200)
|
32
|
+
response.stub!(:content).and_return("content_id")
|
33
|
+
@httpclient.should_receive(:post).with { |*args|
|
34
|
+
uri, body, _ = args
|
35
|
+
uri.should eql("http://localhost/resources")
|
36
|
+
body.should be_kind_of(Hash)
|
37
|
+
body[:content].should be_kind_of(File)
|
38
|
+
body[:content].read.should eql("some object")
|
39
|
+
true
|
40
|
+
}.and_return(response)
|
41
|
+
|
42
|
+
@client = Bosh::Blobstore::SimpleBlobstoreClient.new({"endpoint" => "http://localhost"})
|
43
|
+
@client.create("some object").should eql("content_id")
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should raise an exception when there is an error creating an object" do
|
47
|
+
response = mock("response")
|
48
|
+
response.stub!(:status).and_return(500)
|
49
|
+
|
50
|
+
@httpclient.should_receive(:post).with { |*args|
|
51
|
+
uri, body, _ = args
|
52
|
+
uri.should eql("http://localhost/resources")
|
53
|
+
body.should be_kind_of(Hash)
|
54
|
+
body[:content].should be_kind_of(File)
|
55
|
+
body[:content].read.should eql("some object")
|
56
|
+
true
|
57
|
+
}.and_return(response)
|
58
|
+
|
59
|
+
@client = Bosh::Blobstore::SimpleBlobstoreClient.new({"endpoint" => "http://localhost"})
|
60
|
+
lambda {@client.create("some object")}.should raise_error
|
61
|
+
end
|
62
|
+
|
63
|
+
it "should fetch an object" do
|
64
|
+
response = mock("response")
|
65
|
+
response.stub!(:status).and_return(200)
|
66
|
+
@httpclient.should_receive(:get).with("http://localhost/resources/some object", {}, {}).
|
67
|
+
and_yield("content_id").
|
68
|
+
and_return(response)
|
69
|
+
|
70
|
+
@client = Bosh::Blobstore::SimpleBlobstoreClient.new({"endpoint" => "http://localhost"})
|
71
|
+
@client.get("some object").should eql("content_id")
|
72
|
+
end
|
73
|
+
|
74
|
+
it "should raise an exception when there is an error fetching an object" do
|
75
|
+
response = mock("response")
|
76
|
+
response.stub!(:status).and_return(500)
|
77
|
+
response.stub!(:content).and_return("error message")
|
78
|
+
@httpclient.should_receive(:get).with("http://localhost/resources/some object", {}, {}).and_return(response)
|
79
|
+
|
80
|
+
@client = Bosh::Blobstore::SimpleBlobstoreClient.new({"endpoint" => "http://localhost"})
|
81
|
+
lambda {@client.get("some object")}.should raise_error
|
82
|
+
end
|
83
|
+
|
84
|
+
it "should delete an object" do
|
85
|
+
response = mock("response")
|
86
|
+
response.stub!(:status).and_return(204)
|
87
|
+
response.stub!(:content).and_return("")
|
88
|
+
@httpclient.should_receive(:delete).with("http://localhost/resources/some object", {}).and_return(response)
|
89
|
+
|
90
|
+
@client = Bosh::Blobstore::SimpleBlobstoreClient.new({"endpoint" => "http://localhost"})
|
91
|
+
@client.delete("some object")
|
92
|
+
end
|
93
|
+
|
94
|
+
it "should raise an exception when there is an error deleting an object" do
|
95
|
+
response = mock("response")
|
96
|
+
response.stub!(:status).and_return(404)
|
97
|
+
response.stub!(:content).and_return("")
|
98
|
+
@httpclient.should_receive(:delete).with("http://localhost/resources/some object", {}).and_return(response)
|
99
|
+
|
100
|
+
@client = Bosh::Blobstore::SimpleBlobstoreClient.new({"endpoint" => "http://localhost"})
|
101
|
+
lambda {@client.delete("some object")}.should raise_error
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
metadata
ADDED
@@ -0,0 +1,130 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: blobstore_client
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.3.13
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- VMware
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-03-23 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: aws-s3
|
16
|
+
requirement: &70295903087360 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 0.6.2
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70295903087360
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: httpclient
|
27
|
+
requirement: &70295903085860 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '2.2'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70295903085860
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: multi_json
|
38
|
+
requirement: &70295903084300 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ~>
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 1.1.0
|
44
|
+
type: :runtime
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70295903084300
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: ruby-atmos-pure
|
49
|
+
requirement: &70295903076320 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 1.0.5
|
55
|
+
type: :runtime
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *70295903076320
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: uuidtools
|
60
|
+
requirement: &70295903075580 !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ~>
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: 2.1.2
|
66
|
+
type: :runtime
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: *70295903075580
|
69
|
+
description: BOSH blobstore client
|
70
|
+
email: support@vmware.com
|
71
|
+
executables: []
|
72
|
+
extensions: []
|
73
|
+
extra_rdoc_files: []
|
74
|
+
files:
|
75
|
+
- lib/blobstore_client.rb
|
76
|
+
- lib/blobstore_client/atmos_blobstore_client.rb
|
77
|
+
- lib/blobstore_client/base.rb
|
78
|
+
- lib/blobstore_client/client.rb
|
79
|
+
- lib/blobstore_client/errors.rb
|
80
|
+
- lib/blobstore_client/local_client.rb
|
81
|
+
- lib/blobstore_client/s3_blobstore_client.rb
|
82
|
+
- lib/blobstore_client/simple_blobstore_client.rb
|
83
|
+
- lib/blobstore_client/version.rb
|
84
|
+
- README
|
85
|
+
- Rakefile
|
86
|
+
- spec/assets/file
|
87
|
+
- spec/spec_helper.rb
|
88
|
+
- spec/unit/atmos_blobstore_client_spec.rb
|
89
|
+
- spec/unit/blobstore_client_spec.rb
|
90
|
+
- spec/unit/local_client_spec.rb
|
91
|
+
- spec/unit/s3_blobstore_client_spec.rb
|
92
|
+
- spec/unit/simple_blobstore_client_spec.rb
|
93
|
+
homepage: http://www.vmware.com
|
94
|
+
licenses: []
|
95
|
+
post_install_message:
|
96
|
+
rdoc_options: []
|
97
|
+
require_paths:
|
98
|
+
- lib
|
99
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
100
|
+
none: false
|
101
|
+
requirements:
|
102
|
+
- - ! '>='
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: '0'
|
105
|
+
segments:
|
106
|
+
- 0
|
107
|
+
hash: -2357219804209612013
|
108
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
109
|
+
none: false
|
110
|
+
requirements:
|
111
|
+
- - ! '>='
|
112
|
+
- !ruby/object:Gem::Version
|
113
|
+
version: '0'
|
114
|
+
segments:
|
115
|
+
- 0
|
116
|
+
hash: -2357219804209612013
|
117
|
+
requirements: []
|
118
|
+
rubyforge_project:
|
119
|
+
rubygems_version: 1.8.10
|
120
|
+
signing_key:
|
121
|
+
specification_version: 3
|
122
|
+
summary: BOSH blobstore client
|
123
|
+
test_files:
|
124
|
+
- spec/assets/file
|
125
|
+
- spec/spec_helper.rb
|
126
|
+
- spec/unit/atmos_blobstore_client_spec.rb
|
127
|
+
- spec/unit/blobstore_client_spec.rb
|
128
|
+
- spec/unit/local_client_spec.rb
|
129
|
+
- spec/unit/s3_blobstore_client_spec.rb
|
130
|
+
- spec/unit/simple_blobstore_client_spec.rb
|