waz-blobs 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/lib/waz/blobs/base.rb +22 -0
- data/lib/waz/blobs/blob_object.rb +46 -0
- data/lib/waz/blobs/container.rb +85 -0
- data/lib/waz/blobs/exceptions.rb +12 -0
- data/lib/waz/blobs/service.rb +144 -0
- data/lib/waz/blobs/version.rb +11 -0
- data/lib/waz-blobs.rb +19 -0
- data/rakefile +40 -0
- metadata +81 -0
@@ -0,0 +1,22 @@
|
|
1
|
+
module WAZ
|
2
|
+
module Blobs
|
3
|
+
class Base
|
4
|
+
class << self
|
5
|
+
def establish_connection!(options = {})
|
6
|
+
raise InvalidOption.new(:account_name) unless options.keys.include? :account_name
|
7
|
+
raise InvalidOption.new(:access_key) unless options.keys.include? :access_key
|
8
|
+
options[:use_ssl] = false unless options.keys.include? :use_ssl
|
9
|
+
@connection = options
|
10
|
+
end
|
11
|
+
|
12
|
+
def default_connection
|
13
|
+
return @connection
|
14
|
+
end
|
15
|
+
|
16
|
+
def connected?
|
17
|
+
return !@connection.nil?
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module WAZ
|
2
|
+
module Blobs
|
3
|
+
class BlobObject
|
4
|
+
attr_accessor :name, :url, :content_type
|
5
|
+
|
6
|
+
def initialize(name, url, content_type)
|
7
|
+
self.name = name
|
8
|
+
self.url = url
|
9
|
+
self.content_type = content_type
|
10
|
+
end
|
11
|
+
|
12
|
+
def metadata
|
13
|
+
@properties ||= service_instance.get_blob_properties(path)
|
14
|
+
end
|
15
|
+
|
16
|
+
def value
|
17
|
+
@value ||= service_instance.get_blob(path)
|
18
|
+
end
|
19
|
+
|
20
|
+
def value=(new_value)
|
21
|
+
service_instance.put_blob(path, new_value, content_type, metadata)
|
22
|
+
@value = new_value
|
23
|
+
end
|
24
|
+
|
25
|
+
def put_properties(properties = {})
|
26
|
+
service_instance.set_blob_properties(path, properties)
|
27
|
+
@properties = metadata.merge(properties)
|
28
|
+
end
|
29
|
+
|
30
|
+
def destroy!
|
31
|
+
service_instance.delete_blob(path)
|
32
|
+
end
|
33
|
+
|
34
|
+
def path
|
35
|
+
url.gsub(/https?:\/\/[^\/]+\//i, '').scan(/([^&]+)/i).first().first()
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
def service_instance
|
40
|
+
options = Base.default_connection
|
41
|
+
@service_instance ||= Service.new(options[:account_name], options[:access_key])
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module WAZ
|
2
|
+
module Blobs
|
3
|
+
class Container
|
4
|
+
#Singleton methods
|
5
|
+
class << self
|
6
|
+
def create(name)
|
7
|
+
service_instance.create_container(name)
|
8
|
+
return Container.new(name)
|
9
|
+
end
|
10
|
+
|
11
|
+
def find(name)
|
12
|
+
begin
|
13
|
+
properties = service_instance.get_container_properties(name)
|
14
|
+
return Container.new(name, properties)
|
15
|
+
rescue RestClient::ResourceNotFound
|
16
|
+
return nil
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
def service_instance
|
22
|
+
options = Base.default_connection
|
23
|
+
return Service.new(options[:account_name], options[:access_key])
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
attr_accessor :name, :properties, :public_access
|
28
|
+
|
29
|
+
def initialize(name, metadata = nil)
|
30
|
+
self.name = name
|
31
|
+
self.properties = metadata
|
32
|
+
end
|
33
|
+
|
34
|
+
def metadata
|
35
|
+
self.properties ||= service_instance.get_container_properties(self.name)
|
36
|
+
end
|
37
|
+
|
38
|
+
def put_properties(properties = {})
|
39
|
+
service_instance.set_container_properties(self.name, properties)
|
40
|
+
self.properties = metadata.merge!(properties)
|
41
|
+
end
|
42
|
+
|
43
|
+
def destroy!
|
44
|
+
service_instance.delete_container(self.name)
|
45
|
+
end
|
46
|
+
|
47
|
+
def public_access?
|
48
|
+
public_access ||= service_instance.get_container_acl(self.name)
|
49
|
+
end
|
50
|
+
|
51
|
+
def public_access=(value)
|
52
|
+
public_access = value
|
53
|
+
service_instance.set_container_acl(self.name, value)
|
54
|
+
end
|
55
|
+
|
56
|
+
def blobs
|
57
|
+
service_instance.list_blobs(name).map { |blob| WAZ::Blobs::BlobObject.new(blob[:name], blob[:url], blob[:content_type]) }
|
58
|
+
end
|
59
|
+
|
60
|
+
def store(blob_name, payload, content_type, options = {})
|
61
|
+
service_instance.put_blob("#{self.name}/#{blob_name}", payload, content_type, options)
|
62
|
+
return BlobObject.new(blob_name,
|
63
|
+
service_instance.generate_request_uri(nil, "#{self.name}/#{blob_name}"),
|
64
|
+
content_type)
|
65
|
+
end
|
66
|
+
|
67
|
+
def [](blob_name)
|
68
|
+
begin
|
69
|
+
properties = service_instance.get_blob_properties("#{self.name}/#{blob_name}")
|
70
|
+
return BlobObject.new(blob_name,
|
71
|
+
service_instance.generate_request_uri(nil, "#{self.name}/#{blob_name}"),
|
72
|
+
properties[:content_type])
|
73
|
+
rescue RestClient::ResourceNotFound
|
74
|
+
return nil
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
def service_instance
|
80
|
+
options = Base.default_connection
|
81
|
+
@service_instance ||= Service.new(options[:account_name], options[:access_key])
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module WAZ
|
2
|
+
module Blobs
|
3
|
+
class WAZStorageException < StandardError
|
4
|
+
end
|
5
|
+
|
6
|
+
class InvalidOption < WAZStorageException
|
7
|
+
def initialize(missing_option)
|
8
|
+
super("You did not provide both required access keys. Please provide the #{missing_option}.")
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
module WAZ
|
2
|
+
module Blobs
|
3
|
+
class Service
|
4
|
+
attr_accessor :account_name, :account_key, :use_ssl, :base_url
|
5
|
+
|
6
|
+
def initialize(account_name, account_key, use_ssl = false, base_url = "blob.core.windows.net" )
|
7
|
+
self.account_name = account_name
|
8
|
+
self.account_key = account_key
|
9
|
+
self.use_ssl = use_ssl
|
10
|
+
self.base_url = base_url
|
11
|
+
end
|
12
|
+
|
13
|
+
def create_container(container_name)
|
14
|
+
url = generate_request_uri(nil, container_name)
|
15
|
+
request = generate_request("PUT", url)
|
16
|
+
request.execute()
|
17
|
+
end
|
18
|
+
|
19
|
+
def get_container_properties(container_name)
|
20
|
+
url = generate_request_uri(nil, container_name)
|
21
|
+
request = generate_request("GET", url)
|
22
|
+
request.execute().headers
|
23
|
+
end
|
24
|
+
|
25
|
+
def set_container_properties(container_name, properties = {})
|
26
|
+
url = generate_request_uri("metadata", container_name)
|
27
|
+
request = generate_request("PUT", url, properties)
|
28
|
+
request.execute()
|
29
|
+
end
|
30
|
+
|
31
|
+
def get_container_acl(container_name)
|
32
|
+
url = generate_request_uri("acl", container_name)
|
33
|
+
request = generate_request("GET", url)
|
34
|
+
request.execute().headers[:x_ms_prop_publicaccess].downcase == true.to_s
|
35
|
+
end
|
36
|
+
|
37
|
+
def set_container_acl(container_name, public_available = false)
|
38
|
+
url = generate_request_uri("acl", container_name)
|
39
|
+
request = generate_request("PUT", url, "x-ms-prop-publicaccess" => public_available.to_s)
|
40
|
+
request.execute()
|
41
|
+
end
|
42
|
+
|
43
|
+
def list_containers(options = {})
|
44
|
+
url = generate_request_uri("list", nil, options)
|
45
|
+
request = generate_request("GET", url)
|
46
|
+
doc = REXML::Document.new(request.execute())
|
47
|
+
containers = []
|
48
|
+
REXML::XPath.each(doc, '//Container/') do |item|
|
49
|
+
containers << { :name => REXML::XPath.first(item, "Name").text,
|
50
|
+
:url => REXML::XPath.first(item, "Url").text,
|
51
|
+
:last_modified => REXML::XPath.first(item, "LastModified").text}
|
52
|
+
end
|
53
|
+
return containers
|
54
|
+
end
|
55
|
+
|
56
|
+
def delete_container(container_name)
|
57
|
+
url = generate_request_uri(nil, container_name)
|
58
|
+
request = generate_request("DELETE", url)
|
59
|
+
request.execute()
|
60
|
+
end
|
61
|
+
|
62
|
+
def list_blobs(container_name)
|
63
|
+
url = generate_request_uri("list", container_name)
|
64
|
+
request = generate_request("GET", url)
|
65
|
+
doc = REXML::Document.new(request.execute())
|
66
|
+
containers = []
|
67
|
+
REXML::XPath.each(doc, '//Blob/') do |item|
|
68
|
+
containers << { :name => REXML::XPath.first(item, "Name").text,
|
69
|
+
:url => REXML::XPath.first(item, "Url").text,
|
70
|
+
:content_type => REXML::XPath.first(item, "ContentType").text }
|
71
|
+
end
|
72
|
+
return containers
|
73
|
+
end
|
74
|
+
|
75
|
+
def put_blob(path, payload, content_type = "application/octet-stream", metadata = {})
|
76
|
+
url = generate_request_uri(nil, path)
|
77
|
+
request = generate_request("PUT", url, metadata.merge("Content-Type" => content_type), payload)
|
78
|
+
request.execute()
|
79
|
+
end
|
80
|
+
|
81
|
+
def get_blob(path)
|
82
|
+
url = generate_request_uri(nil, path)
|
83
|
+
request = generate_request("GET", url)
|
84
|
+
request.execute()
|
85
|
+
end
|
86
|
+
|
87
|
+
def delete_blob(path)
|
88
|
+
url = generate_request_uri(nil, path)
|
89
|
+
request = generate_request("DELETE", url)
|
90
|
+
request.execute()
|
91
|
+
end
|
92
|
+
|
93
|
+
def get_blob_properties(path)
|
94
|
+
url = generate_request_uri(nil, path)
|
95
|
+
request = generate_request("HEAD", url)
|
96
|
+
request.execute().headers
|
97
|
+
end
|
98
|
+
|
99
|
+
def set_blob_properties(path, properties ={})
|
100
|
+
url = generate_request_uri("metadata", path)
|
101
|
+
request = generate_request("PUT", url, properties)
|
102
|
+
request.execute()
|
103
|
+
end
|
104
|
+
|
105
|
+
def generate_request(verb, url, headers = {}, payload = nil)
|
106
|
+
http_headers = {}
|
107
|
+
headers.each{ |k, v| http_headers[k.to_s.gsub(/_/, '-')] = v}
|
108
|
+
request = RestClient::Request.new(:method => verb.downcase.to_sym, :url => url, :headers => http_headers, :payload => payload)
|
109
|
+
request.headers["x-ms-Date"] = Time.new.httpdate
|
110
|
+
request.headers["Content-Length"] = (request.payload or "").length
|
111
|
+
request.headers["Authorization"] = "SharedKey #{account_name}:#{generate_signature(request)}"
|
112
|
+
return request
|
113
|
+
end
|
114
|
+
|
115
|
+
def generate_request_uri(operation = nil, path = nil, options = {})
|
116
|
+
protocol = use_ssl ? "https" : "http"
|
117
|
+
query_params = options.keys.sort{ |a, b| a.to_s <=> b.to_s}.map{ |k| "#{k.to_s.gsub(/_/, '')}=#{options[k]}"}.join("&") unless options.empty?
|
118
|
+
uri = "#{protocol}://#{account_name}.#{base_url}#{(path or "").start_with?("/") ? "" : "/"}#{(path or "")}#{operation ? "?comp=" + operation : ""}"
|
119
|
+
uri << "#{operation ? "&" : "?"}#{query_params}" if query_params
|
120
|
+
return uri
|
121
|
+
end
|
122
|
+
|
123
|
+
def self.canonicalize_headers(headers)
|
124
|
+
cannonicalized_headers = headers.keys.select {|h| h.to_s.start_with? 'x-ms'}.map{ |h| "#{h.downcase.strip}:#{headers[h].strip}" }.sort{ |a, b| a <=> b }.join("\x0A")
|
125
|
+
return cannonicalized_headers
|
126
|
+
end
|
127
|
+
|
128
|
+
def canonicalize_message(url)
|
129
|
+
uri_component = url.gsub(/https?:\/\/[^\/]+\//i, '').scan(/([^&]+)/i).first()
|
130
|
+
cannonicalized_message = "/#{self.account_name}/#{uri_component}"
|
131
|
+
end
|
132
|
+
|
133
|
+
def generate_signature(request)
|
134
|
+
signature = request.method.to_s.upcase + "\x0A" +
|
135
|
+
(request.headers["Content-MD5"] or "") + "\x0A" +
|
136
|
+
(request.headers["Content-Type"] or "") + "\x0A" +
|
137
|
+
(request.headers["Date"] or "")+ "\x0A" +
|
138
|
+
self.class.canonicalize_headers(request.headers) + "\x0A" +
|
139
|
+
canonicalize_message(request.url)
|
140
|
+
return Base64.encode64(HMAC::SHA256.new(Base64.decode64(self.account_key)).update(signature.toutf8).digest)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
data/lib/waz-blobs.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'time'
|
2
|
+
require 'cgi'
|
3
|
+
require 'base64'
|
4
|
+
require 'rexml/document'
|
5
|
+
require 'rexml/xpath'
|
6
|
+
|
7
|
+
require 'restclient'
|
8
|
+
require 'hmac-sha2'
|
9
|
+
|
10
|
+
$:.unshift(File.dirname(__FILE__))
|
11
|
+
|
12
|
+
require 'waz/blobs/base'
|
13
|
+
require 'waz/blobs/blob_object'
|
14
|
+
require 'waz/blobs/container'
|
15
|
+
require 'waz/blobs/exceptions'
|
16
|
+
require 'waz/blobs/service'
|
17
|
+
require 'waz/blobs/version'
|
18
|
+
|
19
|
+
|
data/rakefile
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rubygems'
|
3
|
+
require 'spec/rake/spectask'
|
4
|
+
require 'rake/gempackagetask'
|
5
|
+
require 'lib/waz-blobs'
|
6
|
+
|
7
|
+
|
8
|
+
namespace :test do
|
9
|
+
Spec::Rake::SpecTask.new('run_with_rcov') do |t|
|
10
|
+
puts "running tests and code coverage"
|
11
|
+
t.spec_files = FileList['tests/waz/*.rb']
|
12
|
+
t.rcov = true
|
13
|
+
t.rcov_opts = ['--text-report', '--exclude', ".gem,test,Library,#{ENV['GEM_HOME']}", '--sort', 'coverage' ]
|
14
|
+
t.spec_opts = ['-cfn']
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
namespace :dist do
|
19
|
+
spec = Gem::Specification.new do |s|
|
20
|
+
s.name = 'waz-blobs'
|
21
|
+
s.version = Gem::Version.new(WAZ::Blobs::Version)
|
22
|
+
s.summary = "Client library for Windows Azure's Blob Storage Service's REST API"
|
23
|
+
s.description = s.summary
|
24
|
+
s.email = 'johnny.halife@me.com'
|
25
|
+
s.author = 'Johnny G. Halife'
|
26
|
+
s.homepage = 'http://github.com/johnnyhalife/waz-blobs'
|
27
|
+
s.require_paths = ["lib"]
|
28
|
+
s.files = FileList['rakefile', 'lib/**/*.rb']
|
29
|
+
s.test_files = Dir['test/**/*']
|
30
|
+
s.has_rdoc = false
|
31
|
+
|
32
|
+
# Dependencies
|
33
|
+
s.add_runtime_dependency 'rest-client', '>= 1.0.3'
|
34
|
+
s.add_dependency 'ruby-hmac'
|
35
|
+
end
|
36
|
+
|
37
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
38
|
+
pkg.need_tar = true
|
39
|
+
end
|
40
|
+
end
|
metadata
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: waz-blobs
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Johnny G. Halife
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-10-05 00:00:00 -03:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: rest-client
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 1.0.3
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: ruby-hmac
|
27
|
+
type: :runtime
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: "0"
|
34
|
+
version:
|
35
|
+
description: Client library for Windows Azure's Blob Storage Service's REST API
|
36
|
+
email: johnny.halife@me.com
|
37
|
+
executables: []
|
38
|
+
|
39
|
+
extensions: []
|
40
|
+
|
41
|
+
extra_rdoc_files: []
|
42
|
+
|
43
|
+
files:
|
44
|
+
- rakefile
|
45
|
+
- lib/waz/blobs/base.rb
|
46
|
+
- lib/waz/blobs/blob_object.rb
|
47
|
+
- lib/waz/blobs/container.rb
|
48
|
+
- lib/waz/blobs/exceptions.rb
|
49
|
+
- lib/waz/blobs/service.rb
|
50
|
+
- lib/waz/blobs/version.rb
|
51
|
+
- lib/waz-blobs.rb
|
52
|
+
has_rdoc: true
|
53
|
+
homepage: http://github.com/johnnyhalife/waz-blobs
|
54
|
+
licenses: []
|
55
|
+
|
56
|
+
post_install_message:
|
57
|
+
rdoc_options: []
|
58
|
+
|
59
|
+
require_paths:
|
60
|
+
- lib
|
61
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
62
|
+
requirements:
|
63
|
+
- - ">="
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: "0"
|
66
|
+
version:
|
67
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
68
|
+
requirements:
|
69
|
+
- - ">="
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: "0"
|
72
|
+
version:
|
73
|
+
requirements: []
|
74
|
+
|
75
|
+
rubyforge_project:
|
76
|
+
rubygems_version: 1.3.5
|
77
|
+
signing_key:
|
78
|
+
specification_version: 3
|
79
|
+
summary: Client library for Windows Azure's Blob Storage Service's REST API
|
80
|
+
test_files: []
|
81
|
+
|