sparqcode-waz-storage 1.1.1
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/.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
|