blobstore_client 0.3.13

Sign up to get free protection for your applications and to get access to all the features.
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