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 ADDED
@@ -0,0 +1 @@
1
+ Blobstore client for VMware AppCloud Outer Shell.
@@ -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,19 @@
1
+ # Copyright (c) 2009-2012 VMware, Inc.
2
+
3
+ module Bosh
4
+ module Blobstore
5
+ class Client
6
+ def create(contents)
7
+ end
8
+
9
+ def get(id, file = nil)
10
+ end
11
+
12
+ def delete(id)
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+
19
+
@@ -0,0 +1,10 @@
1
+ # Copyright (c) 2009-2012 VMware, Inc.
2
+
3
+ module Bosh
4
+ module Blobstore
5
+
6
+ class BlobstoreError < StandardError; end
7
+ class NotFound < BlobstoreError; end
8
+
9
+ end
10
+ 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
@@ -0,0 +1,9 @@
1
+ # Copyright (c) 2009-2012 VMware, Inc.
2
+
3
+ module Bosh
4
+ module Blobstore
5
+ class Client
6
+ VERSION = "0.3.13"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1 @@
1
+ foobar
@@ -0,0 +1,8 @@
1
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", __FILE__)
2
+
3
+ require "rubygems"
4
+ require "bundler"
5
+ Bundler.setup(:default, :test)
6
+
7
+ require "rspec"
8
+ require "blobstore_client"
@@ -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