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.
Files changed (42) hide show
  1. data/.gitignore +7 -0
  2. data/CHANGELOG.rdoc +72 -0
  3. data/Gemfile +4 -0
  4. data/Gemfile.lock +36 -0
  5. data/LICENSE +19 -0
  6. data/README.rdoc +299 -0
  7. data/lib/waz-blobs.rb +5 -0
  8. data/lib/waz-queues.rb +6 -0
  9. data/lib/waz-storage.rb +39 -0
  10. data/lib/waz-tables.rb +5 -0
  11. data/lib/waz/blobs/blob_object.rb +121 -0
  12. data/lib/waz/blobs/container.rb +160 -0
  13. data/lib/waz/blobs/exceptions.rb +11 -0
  14. data/lib/waz/blobs/service.rb +156 -0
  15. data/lib/waz/queues/exceptions.rb +29 -0
  16. data/lib/waz/queues/message.rb +65 -0
  17. data/lib/waz/queues/queue.rb +165 -0
  18. data/lib/waz/queues/service.rb +106 -0
  19. data/lib/waz/storage/base.rb +70 -0
  20. data/lib/waz/storage/core_service.rb +122 -0
  21. data/lib/waz/storage/exceptions.rb +33 -0
  22. data/lib/waz/storage/validation_rules.rb +26 -0
  23. data/lib/waz/storage/version.rb +11 -0
  24. data/lib/waz/tables/edm_type_helper.rb +45 -0
  25. data/lib/waz/tables/exceptions.rb +45 -0
  26. data/lib/waz/tables/service.rb +178 -0
  27. data/lib/waz/tables/table.rb +75 -0
  28. data/lib/waz/tables/table_array.rb +11 -0
  29. data/rakefile +23 -0
  30. data/tests/configuration.rb +14 -0
  31. data/tests/waz/blobs/blob_object_test.rb +80 -0
  32. data/tests/waz/blobs/container_test.rb +162 -0
  33. data/tests/waz/blobs/service_test.rb +282 -0
  34. data/tests/waz/queues/message_test.rb +33 -0
  35. data/tests/waz/queues/queue_test.rb +206 -0
  36. data/tests/waz/queues/service_test.rb +299 -0
  37. data/tests/waz/storage/base_tests.rb +81 -0
  38. data/tests/waz/storage/shared_key_core_service_test.rb +142 -0
  39. data/tests/waz/tables/service_test.rb +614 -0
  40. data/tests/waz/tables/table_test.rb +98 -0
  41. data/waz-storage.gemspec +29 -0
  42. 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