sparqcode-waz-storage 1.1.1

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