sparqcode-waz-storage 1.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +7 -0
- data/CHANGELOG.rdoc +72 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +36 -0
- data/LICENSE +19 -0
- data/README.rdoc +299 -0
- data/lib/waz-blobs.rb +5 -0
- data/lib/waz-queues.rb +6 -0
- data/lib/waz-storage.rb +39 -0
- data/lib/waz-tables.rb +5 -0
- data/lib/waz/blobs/blob_object.rb +121 -0
- data/lib/waz/blobs/container.rb +160 -0
- data/lib/waz/blobs/exceptions.rb +11 -0
- data/lib/waz/blobs/service.rb +156 -0
- data/lib/waz/queues/exceptions.rb +29 -0
- data/lib/waz/queues/message.rb +65 -0
- data/lib/waz/queues/queue.rb +165 -0
- data/lib/waz/queues/service.rb +106 -0
- data/lib/waz/storage/base.rb +70 -0
- data/lib/waz/storage/core_service.rb +122 -0
- data/lib/waz/storage/exceptions.rb +33 -0
- data/lib/waz/storage/validation_rules.rb +26 -0
- data/lib/waz/storage/version.rb +11 -0
- data/lib/waz/tables/edm_type_helper.rb +45 -0
- data/lib/waz/tables/exceptions.rb +45 -0
- data/lib/waz/tables/service.rb +178 -0
- data/lib/waz/tables/table.rb +75 -0
- data/lib/waz/tables/table_array.rb +11 -0
- data/rakefile +23 -0
- data/tests/configuration.rb +14 -0
- data/tests/waz/blobs/blob_object_test.rb +80 -0
- data/tests/waz/blobs/container_test.rb +162 -0
- data/tests/waz/blobs/service_test.rb +282 -0
- data/tests/waz/queues/message_test.rb +33 -0
- data/tests/waz/queues/queue_test.rb +206 -0
- data/tests/waz/queues/service_test.rb +299 -0
- data/tests/waz/storage/base_tests.rb +81 -0
- data/tests/waz/storage/shared_key_core_service_test.rb +142 -0
- data/tests/waz/tables/service_test.rb +614 -0
- data/tests/waz/tables/table_test.rb +98 -0
- data/waz-storage.gemspec +29 -0
- metadata +187 -0
@@ -0,0 +1,29 @@
|
|
1
|
+
module WAZ
|
2
|
+
module Queues
|
3
|
+
# This exception is raised while trying when calling WAZ::Queues::Queue.create('queue_name') and
|
4
|
+
# the metadata existing on the Queues Storage subsytem on the cloud contains different metadata from
|
5
|
+
# the given one.
|
6
|
+
class QueueAlreadyExists < WAZ::Storage::StorageException
|
7
|
+
def initialize(name)
|
8
|
+
super("The queue #{name} already exists on your account.")
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
# This exception is raised when an initialization parameter of any of the WAZ::Queues classes falls of
|
13
|
+
# the specified values.
|
14
|
+
class OptionOutOfRange < WAZ::Storage::StorageException
|
15
|
+
def initialize(args = {})
|
16
|
+
super("The #{args[:name]} parameter is out of range allowed values go from #{args[:min]} to #{args[:max]}.")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# This exception is raised when the user tries to perform a delete operation over a peeked message. Since
|
21
|
+
# peeked messages cannot by deleted given the fact that there's no pop_receipt associated with it
|
22
|
+
# this exception will be raised.
|
23
|
+
class InvalidOperation < WAZ::Storage::StorageException
|
24
|
+
def initialize()
|
25
|
+
super("A peeked message cannot be delete, you need to lock it first (pop_receipt required).")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module WAZ
|
2
|
+
module Queues
|
3
|
+
# This class is used to model a Message inside Windows Azure Queues the usage
|
4
|
+
# it's pretty simple, here you can see a couple of samples. Messages consist on an UTF-8 string up-to 8KB and some metadata
|
5
|
+
# regarding its status and visibility. Here are all the things that you can do with a message:
|
6
|
+
#
|
7
|
+
# message.message_id #=> returns message id
|
8
|
+
#
|
9
|
+
# # this is the most important method regarding messages
|
10
|
+
# message.message_text #=> returns message contents
|
11
|
+
#
|
12
|
+
# message.pop_receipt #=> used for correlating your dequeue request + a delete operation
|
13
|
+
#
|
14
|
+
# message.expiration_time #=> returns when the message will be removed from the queue
|
15
|
+
#
|
16
|
+
# message.time_next_visible #=> when the message will be visible to other users
|
17
|
+
#
|
18
|
+
# message.insertion_time #=> when the message will be visible to other users
|
19
|
+
#
|
20
|
+
# message.queue_name #=> returns the queue name where the message belongs
|
21
|
+
#
|
22
|
+
# # remove the message from the queue
|
23
|
+
# message.destroy!
|
24
|
+
#
|
25
|
+
class Message
|
26
|
+
class << self
|
27
|
+
# This method is internally used by this class. It's the way we keep a single instance of the
|
28
|
+
# service that wraps the calls the Windows Azure Queues API. It's initialized with the values
|
29
|
+
# from the default_connection on WAZ::Storage::Base initialized thru establish_connection!
|
30
|
+
def service_instance
|
31
|
+
options = WAZ::Storage::Base.default_connection.merge(:type_of_service => "queue")
|
32
|
+
(@service_instances ||= {})[options[:account_name]] ||= Service.new(options)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
attr_accessor :message_id, :message_text, :pop_receipt, :expiration_time, :insertion_time, :time_next_visible, :dequeue_count
|
37
|
+
|
38
|
+
# Creates an instance of Message class, this method is intended to be used internally from the
|
39
|
+
# Queue.
|
40
|
+
def initialize(params = {})
|
41
|
+
self.message_id = params[:message_id]
|
42
|
+
self.message_text = params[:message_text]
|
43
|
+
self.pop_receipt = params[:pop_receipt]
|
44
|
+
self.expiration_time = params[:expiration_time]
|
45
|
+
self.insertion_time = params[:insertion_time]
|
46
|
+
self.time_next_visible = params[:time_next_visible]
|
47
|
+
self.dequeue_count = params[:dequeue_count]
|
48
|
+
@queue_name = params[:queue_name]
|
49
|
+
end
|
50
|
+
|
51
|
+
# Returns the Queue name where the Message belongs to
|
52
|
+
def queue_name
|
53
|
+
return @queue_name
|
54
|
+
end
|
55
|
+
|
56
|
+
# Marks the message for deletion (to later be removed from the queue by the garbage collector). If the message
|
57
|
+
# where the message is being actually called was peeked from the queue instead of locked it will raise the
|
58
|
+
# WAZ::Queues:InvalidOperation exception since it's not a permited operation.
|
59
|
+
def destroy!
|
60
|
+
raise WAZ::Queues::InvalidOperation if pop_receipt.nil?
|
61
|
+
self.class.service_instance.delete_message(queue_name, message_id, pop_receipt)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
module WAZ
|
2
|
+
module Queues
|
3
|
+
# This class represents a Queue on Windows Azure Queues API. These are the methods implemented from Microsoft's API description
|
4
|
+
# available on MSDN at http://msdn.microsoft.com/en-us/library/dd179363.aspx
|
5
|
+
#
|
6
|
+
# # list available queues
|
7
|
+
# WAZ::Queues::Queue.list
|
8
|
+
#
|
9
|
+
# # create a queue (here you can also send hashed metadata)
|
10
|
+
# WAZ::Queues::Queue.create('test-queue')
|
11
|
+
#
|
12
|
+
# # get a specific queue
|
13
|
+
# queue = WAZ::Queues::Queue.find('test-queue')
|
14
|
+
#
|
15
|
+
# # get queue properties (including default headers)
|
16
|
+
# queue.metadata #=> hash containing beautified metadata (:x_ms_meta_name)
|
17
|
+
#
|
18
|
+
# # set queue properties (should follow x-ms-meta to be persisted)
|
19
|
+
# # if you specify the optional parameter overwrite, existing metadata
|
20
|
+
# # will be deleted else merged with new one.
|
21
|
+
# queue.put_properties!(:x_ms_meta_MyProperty => "my value")
|
22
|
+
#
|
23
|
+
# # delete queue
|
24
|
+
# queue.destroy!
|
25
|
+
#
|
26
|
+
# # clear queue contents
|
27
|
+
# queue.clear
|
28
|
+
#
|
29
|
+
# # enqueue a message
|
30
|
+
# queue.enqueue!("payload of the message")
|
31
|
+
#
|
32
|
+
# # peek a message/s (do not alter visibility, it can't be deleted neither)
|
33
|
+
# # num_of_messages (1 to 32) to be peeked (default 1)
|
34
|
+
# message = queue.peek
|
35
|
+
#
|
36
|
+
# # lock a message/s.
|
37
|
+
# # num_of_messages (1 to 32) to be peeked (default 1)
|
38
|
+
# # visiblity_timeout (default 60 sec. max 7200 [2hrs])
|
39
|
+
# message = queue.lock
|
40
|
+
#
|
41
|
+
class Queue
|
42
|
+
class << self
|
43
|
+
# Returns an array of the queues (WAZ::Queues::Queue) existing on the current
|
44
|
+
# Windows Azure Storage account.
|
45
|
+
#
|
46
|
+
# include_metadata defines if the metadata is retrieved along with queue data.
|
47
|
+
def list(include_metadata = false)
|
48
|
+
options = include_metadata ? { :include => 'metadata' } : {}
|
49
|
+
service_instance.list_queues(options).map do |queue|
|
50
|
+
WAZ::Queues::Queue.new(queue)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Creates a queue on the current account. If provided the metadata hash will specify additional
|
55
|
+
# metadata to be stored on the queue. (Remember that metadata on the storage account must start with
|
56
|
+
# :x_ms_metadata_{yourCustomPropertyName}, if not it will not be persisted).
|
57
|
+
def create(queue_name, metadata = {})
|
58
|
+
raise WAZ::Storage::InvalidParameterValue, {:name => "name", :values => ["lower letters, numbers or - (hypen), and must not start or end with - (hyphen)"]} unless WAZ::Storage::ValidationRules.valid_name?(queue_name)
|
59
|
+
service_instance.create_queue(queue_name, metadata)
|
60
|
+
WAZ::Queues::Queue.new(:name => queue_name, :url => service_instance.generate_request_uri(queue_name))
|
61
|
+
end
|
62
|
+
|
63
|
+
# Finds a queue by it's name, in case that it isn't found on the current storage account it will
|
64
|
+
# return nil shilding the user from a ResourceNotFound exception.
|
65
|
+
def find(queue_name)
|
66
|
+
begin
|
67
|
+
metadata = service_instance.get_queue_metadata(queue_name)
|
68
|
+
WAZ::Queues::Queue.new(:name => queue_name, :url => service_instance.generate_request_uri(queue_name), :metadata => metadata)
|
69
|
+
rescue RestClient::ResourceNotFound
|
70
|
+
return nil
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Syntax's sugar for find(:queue_name) or create(:queue_name)
|
75
|
+
def ensure(queue_name)
|
76
|
+
return (self.find(queue_name) or self.create(queue_name))
|
77
|
+
end
|
78
|
+
|
79
|
+
# This method is internally used by this class. It's the way we keep a single instance of the
|
80
|
+
# service that wraps the calls the Windows Azure Queues API. It's initialized with the values
|
81
|
+
# from the default_connection on WAZ::Storage::Base initialized thru establish_connection!
|
82
|
+
def service_instance
|
83
|
+
options = WAZ::Storage::Base.default_connection.merge(:type_of_service => "queue")
|
84
|
+
(@service_instances ||= {})[options[:account_name]] ||= Service.new(options)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
attr_accessor :name, :url, :metadata
|
89
|
+
|
90
|
+
def initialize(options = {})
|
91
|
+
raise WAZ::Storage::InvalidOption, :name unless options.keys.include?(:name)
|
92
|
+
raise WAZ::Storage::InvalidOption, :url unless options.keys.include?(:url)
|
93
|
+
self.name = options[:name]
|
94
|
+
self.url = options[:url]
|
95
|
+
self.metadata = options[:metadata]
|
96
|
+
end
|
97
|
+
|
98
|
+
# Deletes the queue from the current storage account.
|
99
|
+
def destroy!
|
100
|
+
self.class.service_instance.delete_queue(self.name)
|
101
|
+
end
|
102
|
+
|
103
|
+
# Retrieves the metadata headers associated with the quere.
|
104
|
+
def metadata
|
105
|
+
metadata ||= self.class.service_instance.get_queue_metadata(self.name)
|
106
|
+
end
|
107
|
+
|
108
|
+
# Sets the metadata given on the new_metadata, when overwrite passed different
|
109
|
+
# than true it overrides the metadata for the queue (removing all existing metadata)
|
110
|
+
def put_properties!(new_metadata = {}, overwrite = false)
|
111
|
+
new_metadata.merge!(metadata.reject { |k, v| !k.to_s.start_with? "x_ms_meta"} ) unless overwrite
|
112
|
+
self.class.service_instance.set_queue_metadata(new_metadata)
|
113
|
+
end
|
114
|
+
|
115
|
+
# Enqueues a message on current queue. message is just a string that should be
|
116
|
+
# UTF-8 serializable and ttl specifies the time-to-live of the message in the queue
|
117
|
+
# (in seconds).
|
118
|
+
def enqueue!(message, ttl = 604800)
|
119
|
+
self.class.service_instance.enqueue(self.name, message, ttl)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Returns the approximated queue size.
|
123
|
+
def size
|
124
|
+
metadata[:x_ms_approximate_messages_count].to_i
|
125
|
+
end
|
126
|
+
|
127
|
+
# Since Windows Azure Queues implement a Peek-Lock pattern
|
128
|
+
# the method lock will lock a message preventing other users from
|
129
|
+
# picking/locking the current message from the queue.
|
130
|
+
#
|
131
|
+
# The API supports multiple message processing by specifiying num_of_messages (up to 32)
|
132
|
+
#
|
133
|
+
# The visibility_timeout parameter (optional) specifies for how long the message will be
|
134
|
+
# hidden from other users.
|
135
|
+
def lock(num_of_messages = 1, visibility_timeout = nil)
|
136
|
+
options = {}
|
137
|
+
options[:num_of_messages] = num_of_messages
|
138
|
+
options[:visiblity_timeout] = visibility_timeout unless visibility_timeout.nil?
|
139
|
+
messages = self.class.service_instance.get_messages(self.name, options).map do |raw_message|
|
140
|
+
WAZ::Queues::Message.new(raw_message.merge(:queue_name => self.name))
|
141
|
+
end
|
142
|
+
return messages.first() if num_of_messages == 1
|
143
|
+
return messages
|
144
|
+
end
|
145
|
+
|
146
|
+
# Returns top N (default 1, up to 32) message from the queue without performing
|
147
|
+
# any modification on the message. Since the message it's retrieved read-only
|
148
|
+
# users cannot delete the peeked message.
|
149
|
+
def peek(num_of_messages = 1)
|
150
|
+
options = {}
|
151
|
+
options[:num_of_messages] = num_of_messages
|
152
|
+
messages = self.class.service_instance.peek(self.name, options).map do |raw_message|
|
153
|
+
WAZ::Queues::Message.new(raw_message.merge(:queue_name => self.name))
|
154
|
+
end
|
155
|
+
return messages.first() if num_of_messages == 1
|
156
|
+
return messages
|
157
|
+
end
|
158
|
+
|
159
|
+
# Marks every message on the queue for deletion (to be later garbage collected).
|
160
|
+
def clear
|
161
|
+
self.class.service_instance.clear_queue(self.name)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module WAZ
|
2
|
+
module Queues
|
3
|
+
# This is internally used by the waz-queues part of the gem and it exposes the Windows Azure Queue API REST methods
|
4
|
+
# implementation. You can use this class to perform an specific operation that aren't provided by the current API.
|
5
|
+
class Service
|
6
|
+
include WAZ::Storage::SharedKeyCoreService
|
7
|
+
|
8
|
+
# Lists the queues on the given storage account.
|
9
|
+
#
|
10
|
+
# When the options :include => 'metadata' is passed it returns
|
11
|
+
# the corresponding metadata for each queue on the listing.
|
12
|
+
def list_queues(options ={})
|
13
|
+
content = execute(:get, nil, { :comp => 'list' }.merge!(options), { :x_ms_version => "2009-09-19" })
|
14
|
+
doc = REXML::Document.new(content)
|
15
|
+
queues = []
|
16
|
+
|
17
|
+
REXML::XPath.each(doc, '//Queue/') do |item|
|
18
|
+
metadata = {}
|
19
|
+
|
20
|
+
item.elements['Metadata'].elements.each do |element|
|
21
|
+
metadata.merge!(element.name.gsub(/-/, '_').downcase.to_sym => element.text)
|
22
|
+
end unless item.elements['Metadata'].nil?
|
23
|
+
|
24
|
+
queues << { :name => REXML::XPath.first(item, "Name").text,
|
25
|
+
:url => REXML::XPath.first(item, "Url").text,
|
26
|
+
:metadata => metadata}
|
27
|
+
end
|
28
|
+
return queues
|
29
|
+
end
|
30
|
+
|
31
|
+
# Creates a queue on the current storage account. Throws WAZ::Queues::QueueAlreadyExists when
|
32
|
+
# existing metadata and given metadata differ.
|
33
|
+
def create_queue(queue_name, metadata = {})
|
34
|
+
execute(:put, queue_name, nil, metadata.merge!(:x_ms_version => '2009-09-19'))
|
35
|
+
end
|
36
|
+
|
37
|
+
# Deletes the given queue from the current storage account.
|
38
|
+
def delete_queue(queue_name)
|
39
|
+
execute(:delete, queue_name, {}, {:x_ms_version => '2009-09-19'})
|
40
|
+
end
|
41
|
+
|
42
|
+
# Gets the given queue metadata.
|
43
|
+
def get_queue_metadata(queue_name)
|
44
|
+
execute(:head, queue_name, { :comp => 'metadata'}, :x_ms_version => '2009-09-19').headers
|
45
|
+
end
|
46
|
+
|
47
|
+
# Sets the given queue metadata.
|
48
|
+
def set_queue_metadata(queue_name, metadata = {})
|
49
|
+
execute(:put, queue_name, { :comp => 'metadata' }, metadata.merge!(:x_ms_version => '2009-09-19'))
|
50
|
+
end
|
51
|
+
|
52
|
+
# Enqueues a message on the current queue.
|
53
|
+
#
|
54
|
+
# ttl Specifies the time-to-live interval for the message, in seconds. The maximum time-to-live allowed is 7 days. If this parameter
|
55
|
+
# is omitted, the default time-to-live is 7 days.
|
56
|
+
def enqueue(queue_name, message_payload, ttl = 604800)
|
57
|
+
payload = "<?xml version=\"1.0\" encoding=\"utf-8\"?><QueueMessage><MessageText>#{message_payload}</MessageText></QueueMessage>"
|
58
|
+
execute(:post, "#{queue_name}/messages", { :messagettl => ttl }, { 'Content-Type' => 'application/xml', :x_ms_version => "2009-09-19"}, payload)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Locks N messages (1 default) from the given queue.
|
62
|
+
#
|
63
|
+
# :num_of_messages option specifies the max number of messages to get (maximum 32)
|
64
|
+
#
|
65
|
+
# :visibility_timeout option specifies the timeout of the message locking in seconds (max two hours)
|
66
|
+
def get_messages(queue_name, options = {})
|
67
|
+
raise WAZ::Queues::OptionOutOfRange, {:name => :num_of_messages, :min => 1, :max => 32} if (options.keys.include?(:num_of_messages) && (options[:num_of_messages].to_i < 1 || options[:num_of_messages].to_i > 32))
|
68
|
+
raise WAZ::Queues::OptionOutOfRange, {:name => :visibility_timeout, :min => 1, :max => 7200} if (options.keys.include?(:visibility_timeout) && (options[:visibility_timeout].to_i < 1 || options[:visibility_timeout].to_i > 7200))
|
69
|
+
content = execute(:get, "#{queue_name}/messages", options, {:x_ms_version => "2009-09-19"})
|
70
|
+
doc = REXML::Document.new(content)
|
71
|
+
messages = []
|
72
|
+
REXML::XPath.each(doc, '//QueueMessage/') do |item|
|
73
|
+
message = { :message_id => REXML::XPath.first(item, "MessageId").text,
|
74
|
+
:message_text => REXML::XPath.first(item, "MessageText").text,
|
75
|
+
:dequeue_count => REXML::XPath.first(item, "DequeueCount").nil? ? nil : REXML::XPath.first(item, "DequeueCount").text.to_i,
|
76
|
+
:expiration_time => Time.httpdate(REXML::XPath.first(item, "ExpirationTime").text),
|
77
|
+
:insertion_time => Time.httpdate(REXML::XPath.first(item, "InsertionTime").text) }
|
78
|
+
|
79
|
+
# This are only valid when peek-locking messages
|
80
|
+
message[:pop_receipt] = REXML::XPath.first(item, "PopReceipt").text unless REXML::XPath.first(item, "PopReceipt").nil?
|
81
|
+
message[:time_next_visible] = Time.httpdate(REXML::XPath.first(item, "TimeNextVisible").text) unless REXML::XPath.first(item, "TimeNextVisible").nil?
|
82
|
+
messages << message
|
83
|
+
end
|
84
|
+
return messages
|
85
|
+
end
|
86
|
+
|
87
|
+
# Peeks N messages (default 1) from the given queue.
|
88
|
+
#
|
89
|
+
# Implementation is the same of get_messages but differs on an additional parameter called :peek_only.
|
90
|
+
def peek(queue_name, options = {})
|
91
|
+
return get_messages(queue_name, {:peek_only => true}.merge(options))
|
92
|
+
end
|
93
|
+
|
94
|
+
# Deletes the given message from the queue, correlating the operation with the pop_receipt
|
95
|
+
# in order to avoid eventually inconsistent scenarios.
|
96
|
+
def delete_message(queue_name, message_id, pop_receipt)
|
97
|
+
execute :delete, "#{queue_name}/messages/#{message_id}", { :pop_receipt => pop_receipt }
|
98
|
+
end
|
99
|
+
|
100
|
+
# Marks every message on the given queue for deletion.
|
101
|
+
def clear_queue(queue_name)
|
102
|
+
execute :delete, "#{queue_name}/messages", {}, :x_ms_version => '2009-09-19'
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module WAZ
|
2
|
+
module Storage
|
3
|
+
# This class is used to handle a general connection with Windows Azure Storage Account and it
|
4
|
+
# should be used at least once on the application bootstrap or configuration file.
|
5
|
+
#
|
6
|
+
# The usage is pretty simple as it's depicted on the following sample
|
7
|
+
# WAZ::Storage::establish_connection!(:account_name => "my_account_name",
|
8
|
+
# :access_key => "your_base64_key",
|
9
|
+
# :use_ssl => false)
|
10
|
+
#
|
11
|
+
class Base
|
12
|
+
class << self
|
13
|
+
# Sets the basic configuration parameters to use the API on the current context
|
14
|
+
# required parameters are :account_name, :access_key.
|
15
|
+
#
|
16
|
+
# All other parameters are optional.
|
17
|
+
def establish_connection!(options = {})
|
18
|
+
raise InvalidOption, :account_name unless options.keys.include? :account_name
|
19
|
+
raise InvalidOption, :access_key if !options.keys.include? :use_sas_auth_only unless options.keys.include? :access_key
|
20
|
+
raise InvalidOption, :use_sas_auth_only if !options.keys.include? :access_key unless options.keys.include? :use_sas_auth_only
|
21
|
+
raise InvalidOption, :sharedaccesssignature if !options.keys.include? :access_key unless options.keys.include? :sharedaccesssignature and options.keys.include? :use_sas_auth_only
|
22
|
+
options[:use_ssl] = false unless options.keys.include? :use_ssl
|
23
|
+
(@connections ||= []) << options
|
24
|
+
end
|
25
|
+
|
26
|
+
# Block Syntax
|
27
|
+
#
|
28
|
+
# Pushes the named repository onto the context-stack,
|
29
|
+
# yields a new session, and pops the context-stack.
|
30
|
+
#
|
31
|
+
# This helps you set contextual operations like in the following sample
|
32
|
+
#
|
33
|
+
# Base.establish_connection(options) do
|
34
|
+
# # do some operations on the given context
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# The context is restored to the previous one (what you configured on establish_connection!)
|
38
|
+
# or nil.
|
39
|
+
#
|
40
|
+
# Non-Block Syntax
|
41
|
+
#
|
42
|
+
# Behaves exactly as establish_connection!
|
43
|
+
def establish_connection(options = {}) # :yields: current_context
|
44
|
+
establish_connection!(options)
|
45
|
+
if (block_given?)
|
46
|
+
begin
|
47
|
+
return yield
|
48
|
+
ensure
|
49
|
+
@connections.pop() if connected?
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Returns the default connection (last set) parameters. It will raise an exception WAZ::Storage::NotConnected
|
55
|
+
# when there's no connection information registered.
|
56
|
+
def default_connection
|
57
|
+
raise NotConnected unless connected?
|
58
|
+
return @connections.last
|
59
|
+
end
|
60
|
+
|
61
|
+
# Returns a value indicating whether the current connection information has been set or not.
|
62
|
+
def connected?
|
63
|
+
return false if (@connections.nil?)
|
64
|
+
return false if (@connections.empty?)
|
65
|
+
return true
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
module WAZ
|
2
|
+
module Storage
|
3
|
+
# This module is imported by the specific services that use Shared Key authentication profile. On the current implementation
|
4
|
+
# this module is imported from WAZ::Queues::Service and WAZ::Blobs::Service.
|
5
|
+
module SharedKeyCoreService
|
6
|
+
attr_accessor :account_name, :access_key, :use_ssl, :base_url, :type_of_service, :use_devenv, :use_sas_auth_only, :sharedaccesssignature
|
7
|
+
|
8
|
+
# Creates an instance of the implementor service (internally used by the API).
|
9
|
+
def initialize(options = {})
|
10
|
+
# Flag to define the use of shared access signature only
|
11
|
+
self.use_sas_auth_only = options[:use_sas_auth_only] or false
|
12
|
+
self.sharedaccesssignature = options[:sharedaccesssignature]
|
13
|
+
|
14
|
+
self.account_name = options[:account_name]
|
15
|
+
self.access_key = options[:access_key]
|
16
|
+
self.type_of_service = options[:type_of_service]
|
17
|
+
self.use_ssl = options[:use_ssl] or false
|
18
|
+
self.use_devenv = !!options[:use_devenv]
|
19
|
+
self.base_url = "#{options[:type_of_service] or "blobs"}.#{options[:base_url] or "core.windows.net"}" unless self.use_devenv
|
20
|
+
self.base_url ||= (options[:base_url] or "core.windows.net")
|
21
|
+
end
|
22
|
+
|
23
|
+
# Generates a request based on Adam Wiggings' rest-client, including all the required headers
|
24
|
+
# for interacting with Windows Azure Storage API (except for Tables). This methods embeds the
|
25
|
+
# authorization key signature on the request based on the given access_key.
|
26
|
+
def generate_request(verb, url, headers = {}, payload = nil)
|
27
|
+
http_headers = {}
|
28
|
+
headers.each{ |k, v| http_headers[k.to_s.gsub(/_/, '-')] = v} unless headers.nil?
|
29
|
+
http_headers.merge!("x-ms-Date" => Time.new.httpdate)
|
30
|
+
http_headers.merge!("Content-Length" => (payload or "").size)
|
31
|
+
request = {:headers => http_headers, :method => verb.to_s.downcase.to_sym, :url => url, :payload => payload}
|
32
|
+
request[:headers].merge!("Authorization" => "SharedKey #{account_name}:#{generate_signature(request)}") unless self.use_sas_auth_only
|
33
|
+
return RestClient::Request.new(request)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Generates the request uri based on the resource path, the protocol, the account name and the parameters passed
|
37
|
+
# on the options hash.
|
38
|
+
def generate_request_uri(path = nil, options = {})
|
39
|
+
protocol = use_ssl ? "https" : "http"
|
40
|
+
query_params = options.keys.sort{ |a, b| a.to_s <=> b.to_s}.map{ |k| "#{k.to_s.gsub(/_/, '')}=#{CGI.escape(options[k].to_s)}"}.join("&") unless options.nil? or options.empty?
|
41
|
+
uri = "#{protocol}://#{base_url}/#{path.start_with?(account_name) ? "" : account_name }#{((path or "").start_with?("/") or path.start_with?(account_name)) ? "" : "/"}#{(path or "")}" if !self.use_devenv.nil? and self.use_devenv
|
42
|
+
uri ||= "#{protocol}://#{account_name}.#{base_url}#{(path or "").start_with?("/") ? "" : "/"}#{(path or "")}"
|
43
|
+
if self.use_sas_auth_only
|
44
|
+
uri << "?#{self.sharedaccesssignature.gsub(/\?/,'')}"
|
45
|
+
else
|
46
|
+
uri << "?#{query_params}" if query_params
|
47
|
+
end
|
48
|
+
return uri
|
49
|
+
end
|
50
|
+
|
51
|
+
# Canonicalizes the request headers by following Microsoft's specification on how those headers have to be sorted
|
52
|
+
# and which of the given headers apply to be canonicalized.
|
53
|
+
def canonicalize_headers(headers)
|
54
|
+
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")
|
55
|
+
return cannonicalized_headers
|
56
|
+
end
|
57
|
+
|
58
|
+
# Creates a canonical representation of the message by combining account_name/resource_path.
|
59
|
+
def canonicalize_message(url)
|
60
|
+
uri_component = url.gsub(/https?:\/\/[^\/]+\//i, '').gsub(/\?.*/i, '')
|
61
|
+
comp_component = url.scan(/comp=[^&]+/i).first()
|
62
|
+
uri_component << "?#{comp_component}" if comp_component
|
63
|
+
canonicalized_message = "/#{self.account_name}/#{uri_component}"
|
64
|
+
return canonicalized_message
|
65
|
+
end
|
66
|
+
|
67
|
+
# Generates the signature based on Micosoft specs for the REST API. It includes some special headers,
|
68
|
+
# the canonicalized header line and the canonical form of the message, all of the joined by \n character. Encoded with
|
69
|
+
# Base64 and encrypted with SHA256 using the access_key as the seed.
|
70
|
+
def generate_signature(options = {})
|
71
|
+
return generate_signature20090919(options) if options[:headers]["x-ms-version"] == "2009-09-19"
|
72
|
+
|
73
|
+
signature = options[:method].to_s.upcase + "\x0A" +
|
74
|
+
(options[:headers]["Content-MD5"] or "") + "\x0A" +
|
75
|
+
(options[:headers]["Content-Type"] or "") + "\x0A" +
|
76
|
+
(options[:headers]["Date"] or "")+ "\x0A"
|
77
|
+
|
78
|
+
signature += canonicalize_headers(options[:headers]) + "\x0A" unless self.type_of_service == 'table'
|
79
|
+
signature += canonicalize_message(options[:url])
|
80
|
+
signature = signature.toutf8 if(signature.respond_to? :toutf8)
|
81
|
+
Base64.encode64(HMAC::SHA256.new(Base64.decode64(self.access_key)).update(signature).digest)
|
82
|
+
end
|
83
|
+
|
84
|
+
def generate_signature20090919(options = {})
|
85
|
+
signature = options[:method].to_s.upcase + "\x0A" +
|
86
|
+
(options[:headers]["Content-Encoding"] or "") + "\x0A" +
|
87
|
+
(options[:headers]["Content-Language"] or "") + "\x0A" +
|
88
|
+
(options[:headers]["Content-Length"] or "").to_s + "\x0A" +
|
89
|
+
(options[:headers]["Content-MD5"] or "") + "\x0A" +
|
90
|
+
(options[:headers]["Content-Type"] or "") + "\x0A" +
|
91
|
+
(options[:headers]["Date"] or "")+ "\x0A" +
|
92
|
+
(options[:headers]["If-Modified-Since"] or "")+ "\x0A" +
|
93
|
+
(options[:headers]["If-Match"] or "")+ "\x0A" +
|
94
|
+
(options[:headers]["If-None-Match"] or "")+ "\x0A" +
|
95
|
+
(options[:headers]["If-Unmodified-Since"] or "")+ "\x0A" +
|
96
|
+
(options[:headers]["Range"] or "")+ "\x0A" +
|
97
|
+
canonicalize_headers(options[:headers]) + "\x0A" +
|
98
|
+
canonicalize_message20090919(options[:url])
|
99
|
+
|
100
|
+
signature = signature.toutf8 if(signature.respond_to? :toutf8)
|
101
|
+
Base64.encode64(HMAC::SHA256.new(Base64.decode64(self.access_key)).update(signature).digest)
|
102
|
+
end
|
103
|
+
|
104
|
+
def canonicalize_message20090919(url)
|
105
|
+
uri_component = url.gsub(/https?:\/\/[^\/]+\//i, '').gsub(/\?.*/i, '')
|
106
|
+
query_component = (url.scan(/\?(.*)/i).first() or []).first()
|
107
|
+
query_component = query_component.split('&').sort{|a, b| a <=> b}.map{ |p| CGI::unescape(p.split('=').join(':')) }.join("\n") if query_component
|
108
|
+
canonicalized_message = "/#{self.account_name}/#{uri_component}"
|
109
|
+
canonicalized_message << "\n#{query_component}" if query_component
|
110
|
+
return canonicalized_message
|
111
|
+
end
|
112
|
+
|
113
|
+
# Generates a Windows Azure Storage call, it internally calls url generation method
|
114
|
+
# and the request generation message.
|
115
|
+
def execute(verb, path, query = {}, headers = {}, payload = nil)
|
116
|
+
url = generate_request_uri(path, query)
|
117
|
+
request = generate_request(verb, url, headers, payload)
|
118
|
+
request.execute()
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|