waz-storage 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/waz-blobs.rb ADDED
@@ -0,0 +1,20 @@
1
+ require 'time'
2
+ require 'cgi'
3
+ require 'base64'
4
+ require 'rexml/document'
5
+ require 'rexml/xpath'
6
+ require 'restclient'
7
+ require 'hmac-sha2'
8
+
9
+ $:.unshift(File.dirname(__FILE__))
10
+ require 'waz/storage/base'
11
+ require 'waz/storage/core_service'
12
+ require 'waz/storage/exceptions'
13
+ require 'waz/storage/version'
14
+ require 'waz/blobs/blob_object'
15
+ require 'waz/blobs/container'
16
+ require 'waz/blobs/exceptions'
17
+ require 'waz/blobs/service'
18
+ require 'waz/blobs/version'
19
+
20
+
data/lib/waz-queues.rb ADDED
@@ -0,0 +1,21 @@
1
+ require 'time'
2
+ require 'cgi'
3
+ require 'base64'
4
+ require 'rexml/document'
5
+ require 'rexml/xpath'
6
+
7
+ require 'restclient'
8
+ require 'hmac-sha2'
9
+
10
+ $:.unshift(File.dirname(__FILE__))
11
+ require 'waz/storage/base'
12
+ require 'waz/storage/core_service'
13
+ require 'waz/storage/exceptions'
14
+ require 'waz/storage/version'
15
+ require 'waz/queues/exceptions'
16
+ require 'waz/queues/message'
17
+ require 'waz/queues/queue'
18
+ require 'waz/queues/service'
19
+ require 'waz/queues/version'
20
+
21
+
@@ -0,0 +1,5 @@
1
+ $:.unshift(File.dirname(__FILE__))
2
+ require 'waz/storage/base'
3
+ require 'waz/storage/core_service'
4
+ require 'waz/storage/exceptions'
5
+ require 'waz/storage/version'
@@ -0,0 +1,90 @@
1
+ module WAZ
2
+ module Blobs
3
+ # This class is used to model the Blob inside Windows Azure Blobs the usage
4
+ # it's pretty simple, here you can see a couple of samples. These are the implemented methods of the blob API up to now.
5
+ # The basics are implemented although blocks management is not yet completed, I consider that the API is pretty usable since
6
+ # it covers the basics
7
+ #
8
+ # # retrieve blob name, uri and content-type
9
+ # blob.name
10
+ # blob.url
11
+ # blob.content_type
12
+ #
13
+ # # retrieve blob value
14
+ # blob.value #=> lazy loaded payload of the blob
15
+ #
16
+ # # retrieve blob metadata (+ properties)
17
+ # blob.metadata #=> hash containing beautified metadata (:x_ms_meta_name)
18
+ #
19
+ # # put blob metadata
20
+ # blob.put_properties(:x_ms_meta_MyProperty => "my value")
21
+ #
22
+ # # update value
23
+ # blob.value = "my new value" #=> this will update the blob content on WAZ
24
+ #
25
+ # *REMARKS*: This class is not meant to be manually instanciated it's basicaly an internal
26
+ # representation returned by the WAZ::Blobs::Container.
27
+ class BlobObject
28
+ class << self
29
+ # This method is internally used by this class. It's the way we keep a single instance of the
30
+ # service that wraps the calls the Windows Azure Blobs API. It's initialized with the values
31
+ # from the default_connection on WAZ::Storage::Base initialized thru establish_connection!
32
+ def service_instance
33
+ @service_instance ||= Service.new(WAZ::Storage::Base.default_connection.merge(:type_of_service => "blob"))
34
+ end
35
+ end
36
+
37
+ attr_accessor :name, :url, :content_type
38
+
39
+ # Creates a new instance of the Blob object. This constructor is internally used by the Container
40
+ # it's initialized thru a hash that's received as parameter. It has the following requirements:
41
+ # <em>:name</em> which is the blob name (usually the file name), <em>:url</em> that is the url of the blob (used to download or access it via browser)
42
+ # and <em>:content_type</em> which is the content type of the blob and is a required parameter by the Azure API
43
+ def initialize(options = {})
44
+ raise WAZ::Storage::InvalidOption, :name unless options.keys.include?(:name) and !options[:name].empty?
45
+ raise WAZ::Storage::InvalidOption, :url unless options.keys.include?(:url) and !options[:url].empty?
46
+ raise WAZ::Storage::InvalidOption, :content_type unless options.keys.include?(:content_type) and !options[:content_type].empty?
47
+ self.name = options[:name]
48
+ self.url = options[:url]
49
+ self.content_type = options[:content_type]
50
+ end
51
+
52
+ # Returns the blob properties from Windows Azure. This properties always come as HTTP Headers and they include standard headers like
53
+ # <em>Content-Type</em>, <em>Content-Length</em>, etc. combined with custom properties like with <em>x-ms-meta-Name</em>.
54
+ def metadata
55
+ self.class.service_instance.get_blob_properties(path)
56
+ end
57
+
58
+ # Returns the actual blob content, this method is specially used to avoid retrieving the whole blob
59
+ # while iterating and retrieving the blob collection from the Container.
60
+ def value
61
+ @value ||= self.class.service_instance.get_blob(path)
62
+ end
63
+
64
+ # Assigns the given value to the blob content. It also stores a local copy of it in order to avoid round trips
65
+ # on scenarios when you do Save and Display on the same context.
66
+ def value=(new_value)
67
+ self.class.service_instance.put_blob(path, new_value, content_type, metadata)
68
+ @value = new_value
69
+ end
70
+
71
+ # Stores the blob properties. Those properties are sent as HTTP Headers it's really important that you name your custom
72
+ # properties with the <em>x-ms-meta</em> prefix, if not the won't be persisted by the Windows Azure Blob Storage API.
73
+ def put_properties!(properties = {})
74
+ self.class.service_instance.set_blob_properties(path, properties)
75
+ end
76
+
77
+ # Removes the blob from the container.
78
+ def destroy!
79
+ self.class.service_instance.delete_blob(path)
80
+ end
81
+
82
+ # Returns the blob path. This is specially important when simulating containers inside containers
83
+ # by enabling the API to point to the appropiated resource.
84
+ def path
85
+ url.gsub(/https?:\/\/[^\/]+\//i, '').scan(/([^&]+)/i).first().first()
86
+ end
87
+ end
88
+ end
89
+ end
90
+
@@ -0,0 +1,135 @@
1
+ module WAZ
2
+ module Blobs
3
+ # This class is used to model the Container inside Windows Azure Blobs the usage
4
+ # it's pretty simple, here you can see a couple of samples. Here you can find Microsoft's REST API description available on MSDN
5
+ # at http://msdn.microsoft.com/en-us/library/dd179361.aspx
6
+ #
7
+ # # list available containers
8
+ # WAZ::Blobs::Container.list
9
+ #
10
+ # # create a container
11
+ # WAZ::Blobs::Container.create('my-container')
12
+ #
13
+ # # get a specific container
14
+ # my_container = WAZ::Blobs::Container.find('my-container')
15
+ #
16
+ # # get container properties (including default headers)
17
+ # my_container.metadata #=> hash containing beautified metadata (:x_ms_meta_name)
18
+ #
19
+ # # set container properties (should follow x-ms-meta to be persisted)
20
+ # my_container.put_properties(:x_ms_meta_MyProperty => "my value")
21
+ #
22
+ # # get a the value indicating whether the container is public or not
23
+ # my_container.public_access? #=> true or false based on x-ms-prop-publicaccess
24
+ #
25
+ # # set a value indicating whether the container is public or not
26
+ # my_container.public_access = false
27
+ #
28
+ # # delete container
29
+ # my_container.destroy!
30
+ #
31
+ # # store a blob on the given container
32
+ # my_container.store('my-blob', blob_content, 'application/xml')
33
+ #
34
+ # # retrieve a particular blob from a container
35
+ # my_container['my-blob']
36
+ #
37
+ # # retrieve a blob list from a container
38
+ # my_container.blobs #=> WAZ::Blobs::BlobObject collection
39
+ #
40
+ class Container
41
+ class << self
42
+ # Creates a new container with the given name.
43
+ def create(name)
44
+ service_instance.create_container(name)
45
+ return Container.new(:name => name)
46
+ end
47
+
48
+ # Finds a container by name. It will return nil if no container was found.
49
+ def find(name)
50
+ begin
51
+ properties = service_instance.get_container_properties(name)
52
+ return Container.new(properties.merge(:name => name))
53
+ rescue RestClient::ResourceNotFound
54
+ return nil
55
+ end
56
+ end
57
+
58
+ # Returns all the containers on the given account.
59
+ def list(options = {})
60
+ service_instance.list_containers(options).map { |container| Container.new(container) }
61
+ end
62
+
63
+ # This method is internally used by this class. It's the way we keep a single instance of the
64
+ # service that wraps the calls the Windows Azure Blobs API. It's initialized with the values
65
+ # from the default_connection on WAZ::Storage::Base initialized thru establish_connection!
66
+ def service_instance
67
+ @service_instance ||= Service.new(WAZ::Storage::Base.default_connection.merge(:type_of_service => "blob"))
68
+ end
69
+ end
70
+
71
+ attr_accessor :name
72
+
73
+ # Creates a new instance of the WAZ::Blobs::Container. This class isn't intended for external use
74
+ # to access or create a container you should use the class methods provided like list, create, or find.
75
+ def initialize(options = {})
76
+ raise WAZ::Storage::InvalidOption, :name unless options.keys.include?(:name) and !options[:name].empty?
77
+ self.name = options[:name]
78
+ end
79
+
80
+ # Returns the container metadata.
81
+ def metadata
82
+ self.class.service_instance.get_container_properties(self.name)
83
+ end
84
+
85
+ # Adds metadata for the container.Those properties are sent as HTTP Headers it's really important that you name your custom
86
+ # properties with the <em>x-ms-meta</em> prefix, if not the won't be persisted by the Windows Azure Blob Storage API.
87
+ def put_properties!(properties = {})
88
+ self.class.service_instance.set_container_properties(self.name, properties)
89
+ end
90
+
91
+ # Removes the container from the current account.
92
+ def destroy!
93
+ self.class.service_instance.delete_container(self.name)
94
+ end
95
+
96
+ # Retuns a value indicating whether the container is public accessible (i.e. from a Web Browser) or not.
97
+ def public_access?
98
+ self.class.service_instance.get_container_acl(self.name)
99
+ end
100
+
101
+ # Sets a value indicating whether the container is public accessible (i.e. from a Web Browser) or not.
102
+ def public_access=(value)
103
+ self.class.service_instance.set_container_acl(self.name, value)
104
+ end
105
+
106
+ # Returns a list of blobs (WAZ::Blobs::BlobObject) contained on the current container.
107
+ def blobs
108
+ self.class.service_instance.list_blobs(name).map { |blob| WAZ::Blobs::BlobObject.new(blob) }
109
+ end
110
+
111
+ # Stores a blob on the container with under the given name, with the given content and
112
+ # the required <em>content_type</em>. <strong>The <em>options</em> parameters if provided
113
+ # will set the default metadata properties for the blob</strong>.
114
+ def store(blob_name, payload, content_type, options = {})
115
+ self.class.service_instance.put_blob("#{self.name}/#{blob_name}", payload, content_type, options)
116
+ return BlobObject.new(:name => blob_name,
117
+ :url => self.class.service_instance.generate_request_uri("#{self.name}/#{blob_name}"),
118
+ :content_type => content_type)
119
+ end
120
+
121
+ # Retrieves the blob by the given name. If the blob isn't found on the current container
122
+ # it will return nil instead of throwing an exception.
123
+ def [](blob_name)
124
+ begin
125
+ properties = self.class.service_instance.get_blob_properties("#{self.name}/#{blob_name}")
126
+ return BlobObject.new(:name => blob_name,
127
+ :url => self.class.service_instance.generate_request_uri("#{self.name}/#{blob_name}"),
128
+ :content_type => properties[:content_type])
129
+ rescue RestClient::ResourceNotFound
130
+ return nil
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,129 @@
1
+ module WAZ
2
+ module Blobs
3
+ # This is internally used by the waz-blobs part of the gem and it exposes the Windows Azure Blob API REST methods
4
+ # implementation. You can use this class to perform an specific operation that isn't provided by the current API.
5
+ class Service
6
+ include WAZ::Storage::SharedKeyCoreService
7
+
8
+ # Creates a container on the current Windows Azure Storage account.
9
+ def create_container(container_name)
10
+ url = generate_request_uri(container_name)
11
+ request = generate_request("PUT", url)
12
+ request.execute()
13
+ end
14
+
15
+ # Retrieves all the properties existing on the container.
16
+ def get_container_properties(container_name)
17
+ url = generate_request_uri(container_name)
18
+ request = generate_request("GET", url)
19
+ request.execute().headers
20
+ end
21
+
22
+ # Set the container properties (metadata).
23
+ #
24
+ # Remember that custom properties should be named as :x_ms_meta_{propertyName} in order
25
+ # to have Windows Azure to persist them.
26
+ def set_container_properties(container_name, properties = {})
27
+ url = generate_request_uri(container_name, :comp => 'metadata')
28
+ request = generate_request("PUT", url, properties)
29
+ request.execute()
30
+ end
31
+
32
+ # Retrieves the value of the :x_ms_prop_publicaccess header from the
33
+ # container properties indicating whether the container is publicly
34
+ # accessible or not.
35
+ def get_container_acl(container_name)
36
+ url = generate_request_uri(container_name, :comp => 'acl')
37
+ request = generate_request("GET", url)
38
+ request.execute().headers[:x_ms_prop_publicaccess].downcase == true.to_s
39
+ end
40
+
41
+ # Sets the value of the :x_ms_prop_publicaccess header from the
42
+ # container properties indicating whether the container is publicly
43
+ # accessible or not.
44
+ #
45
+ # Default is _false_
46
+ def set_container_acl(container_name, public_available = false)
47
+ url = generate_request_uri(container_name, :comp => 'acl')
48
+ request = generate_request("PUT", url, "x-ms-prop-publicaccess" => public_available.to_s)
49
+ request.execute()
50
+ end
51
+
52
+ # Lists all the containers existing on the current storage account.
53
+ def list_containers(options = {})
54
+ url = generate_request_uri( nil, options.merge(:comp => 'list'))
55
+ request = generate_request("GET", url)
56
+ doc = REXML::Document.new(request.execute())
57
+ containers = []
58
+ REXML::XPath.each(doc, '//Container/') do |item|
59
+ containers << { :name => REXML::XPath.first(item, "Name").text,
60
+ :url => REXML::XPath.first(item, "Url").text,
61
+ :last_modified => REXML::XPath.first(item, "LastModified").text}
62
+ end
63
+ return containers
64
+ end
65
+
66
+ # Deletes the given container from the Windows Azure Storage account.
67
+ def delete_container(container_name)
68
+ url = generate_request_uri(container_name)
69
+ request = generate_request("DELETE", url)
70
+ request.execute()
71
+ end
72
+
73
+ # Lists all the blobs inside the given container.
74
+ def list_blobs(container_name)
75
+ url = generate_request_uri(container_name, :comp => 'list')
76
+ request = generate_request("GET", url)
77
+ doc = REXML::Document.new(request.execute())
78
+ containers = []
79
+ REXML::XPath.each(doc, '//Blob/') do |item|
80
+ containers << { :name => REXML::XPath.first(item, "Name").text,
81
+ :url => REXML::XPath.first(item, "Url").text,
82
+ :content_type => REXML::XPath.first(item, "ContentType").text }
83
+ end
84
+ return containers
85
+ end
86
+
87
+ # Stores a blob on the given container.
88
+ #
89
+ # Remarks path and payload are just text.
90
+ #
91
+ # content_type is required by the blobs api, but on this method is defaulted to "application/octect-stream"
92
+ #
93
+ # metadata is a hash that stores all the properties that you want to add to the blob when creating it.
94
+ def put_blob(path, payload, content_type = "application/octet-stream", metadata = {})
95
+ url = generate_request_uri( path)
96
+ request = generate_request("PUT", url, metadata.merge("Content-Type" => content_type), payload)
97
+ request.execute()
98
+ end
99
+
100
+ # Retrieves a blob (content + headers) from the current path.
101
+ def get_blob(path)
102
+ url = generate_request_uri( path)
103
+ request = generate_request("GET", url)
104
+ request.execute()
105
+ end
106
+
107
+ # Deletes the blob existing on the current path.
108
+ def delete_blob(path)
109
+ url = generate_request_uri( path)
110
+ request = generate_request("DELETE", url)
111
+ request.execute()
112
+ end
113
+
114
+ # Retrieves the properties associated with the blob at the given path.
115
+ def get_blob_properties(path)
116
+ url = generate_request_uri( path)
117
+ request = generate_request("HEAD", url)
118
+ request.execute().headers
119
+ end
120
+
121
+ # Sets the properties (metadata) associated to the blob at given path.
122
+ def set_blob_properties(path, properties ={})
123
+ url = generate_request_uri( path, :comp => 'metadata')
124
+ request = generate_request("PUT", url, properties)
125
+ request.execute()
126
+ end
127
+ end
128
+ end
129
+ end
@@ -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,63 @@
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
+ @service_instance ||= Service.new(WAZ::Storage::Base.default_connection.merge(:type_of_service => 'queue'))
32
+ end
33
+ end
34
+
35
+ attr_accessor :message_id, :message_text, :pop_receipt, :expiration_time, :insertion_time, :time_next_visible
36
+
37
+ # Creates an instance of Message class, this method is intended to be used internally from the
38
+ # Queue.
39
+ def initialize(params = {})
40
+ self.message_id = params[:message_id]
41
+ self.message_text = params[:message_text]
42
+ self.pop_receipt = params[:pop_receipt]
43
+ self.expiration_time = params[:expiration_time]
44
+ self.insertion_time = params[:insertion_time]
45
+ self.time_next_visible = params[:time_next_visible]
46
+ @queue_name = params[:queue_name]
47
+ end
48
+
49
+ # Returns the Queue name where the Message belongs to
50
+ def queue_name
51
+ return @queue_name
52
+ end
53
+
54
+ # Marks the message for deletion (to later be removed from the queue by the garbage collector). If the message
55
+ # where the message is being actually called was peeked from the queue instead of locked it will raise the
56
+ # WAZ::Queues:InvalidOperation exception since it's not a permited operation.
57
+ def destroy!
58
+ raise WAZ::Queues::InvalidOperation if pop_receipt.nil?
59
+ self.class.service_instance.delete_message(queue_name, message_id, pop_receipt)
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,154 @@
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('my-container')
11
+ #
12
+ # # get a specific queue
13
+ # queue = WAZ::Queues::Container.find('my-container')
14
+ #
15
+ # # get queue properties (including default headers)
16
+ # queue.metadata #=> hash containing beautified metadata (:x_ms_meta_name)
17
+ #
18
+ # # set container 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
+ def list
46
+ service_instance.list_queues.map do |queue|
47
+ WAZ::Queues::Queue.new(queue)
48
+ end
49
+ end
50
+
51
+ # Creates a queue on the current account. If provided the metadata hash will specify additional
52
+ # metadata to be stored on the queue. (Remember that metadata on the storage account must start with
53
+ # :x_ms_metadata_{yourCustomPropertyName}, if not it will not be persisted).
54
+ def create(queue_name, metadata = {})
55
+ service_instance.create_queue(queue_name, metadata)
56
+ WAZ::Queues::Queue.new(:name => queue_name, :url => service_instance.generate_request_uri(queue_name))
57
+ end
58
+
59
+ # Finds a queue by it's name, in case that it isn't found on the current storage account it will
60
+ # return nil shilding the user from a ResourceNotFound exception.
61
+ def find(queue_name)
62
+ begin
63
+ service_instance.get_queue_metadata(queue_name)
64
+ WAZ::Queues::Queue.new(:name => queue_name, :url => service_instance.generate_request_uri(queue_name))
65
+ rescue RestClient::ResourceNotFound
66
+ return nil
67
+ end
68
+ end
69
+
70
+ # This method is internally used by this class. It's the way we keep a single instance of the
71
+ # service that wraps the calls the Windows Azure Queues API. It's initialized with the values
72
+ # from the default_connection on WAZ::Storage::Base initialized thru establish_connection!
73
+ def service_instance
74
+ @service_instance ||= Service.new(WAZ::Storage::Base.default_connection.merge(:type_of_service => 'queue'))
75
+ end
76
+ end
77
+
78
+ attr_accessor :name, :url
79
+
80
+ def initialize(options = {})
81
+ raise WAZ::Storage::InvalidOption, :name unless options.keys.include?(:name)
82
+ raise WAZ::Storage::InvalidOption, :url unless options.keys.include?(:url)
83
+ self.name = options[:name]
84
+ self.url = options[:url]
85
+ end
86
+
87
+ # Deletes the queue from the current storage account.
88
+ def destroy!
89
+ self.class.service_instance.delete_queue(self.name)
90
+ end
91
+
92
+ # Retrieves the metadata headers associated with the quere.
93
+ def metadata
94
+ self.class.service_instance.get_queue_metadata(self.name)
95
+ end
96
+
97
+ # Sets the metadata given on the new_metadata, when overwrite passed different
98
+ # than true it overrides the metadata for the queue (removing all existing metadata)
99
+ def put_properties!(new_metadata = {}, overwrite = false)
100
+ new_metadata.merge!(metadata.reject { |k, v| !k.to_s.start_with? "x_ms_meta"} ) unless overwrite
101
+ self.class.service_instance.set_queue_metadata(new_metadata)
102
+ end
103
+
104
+ # Enqueues a message on current queue. message is just a string that should be
105
+ # UTF-8 serializable and ttl specifies the time-to-live of the message in the queue
106
+ # (in seconds).
107
+ def enqueue!(message, ttl = 604800)
108
+ self.class.service_instance.enqueue(self.name, message, ttl)
109
+ end
110
+
111
+ # Returns the approximated queue size.
112
+ def size
113
+ metadata[:x_ms_approximate_messages_count].to_i
114
+ end
115
+
116
+ # Since Windows Azure Queues implement a Peek-Lock pattern
117
+ # the method lock will lock a message preventing other users from
118
+ # picking/locking the current message from the queue.
119
+ #
120
+ # The API supports multiple message processing by specifiying num_of_messages (up to 32)
121
+ #
122
+ # The visibility_timeout parameter (optional) specifies for how long the message will be
123
+ # hidden from other users.
124
+ def lock(num_of_messages = 1, visibility_timeout = nil)
125
+ options = {}
126
+ options[:num_of_messages] = num_of_messages
127
+ options[:visiblity_timeout] = visibility_timeout unless visibility_timeout.nil?
128
+ messages = self.class.service_instance.get_messages(self.name, options).map do |raw_message|
129
+ WAZ::Queues::Message.new(raw_message.merge(:queue_name => self.name))
130
+ end
131
+ return messages.first() if num_of_messages == 1
132
+ return messages
133
+ end
134
+
135
+ # Returns top N (default 1, up to 32) message from the queue without performing
136
+ # any modification on the message. Since the message it's retrieved read-only
137
+ # users cannot delete the peeked message.
138
+ def peek(num_of_messages = 1)
139
+ options = {}
140
+ options[:num_of_messages] = num_of_messages
141
+ messages = self.class.service_instance.peek(self.name, options).map do |raw_message|
142
+ WAZ::Queues::Message.new(raw_message.merge(:queue_name => self.name))
143
+ end
144
+ return messages.first() if num_of_messages == 1
145
+ return messages
146
+ end
147
+
148
+ # Marks every message on the queue for deletion (to be later garbage collected).
149
+ def clear
150
+ self.class.service_instance.clear_queue(self.name)
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,114 @@
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
+ def list_queues(options ={})
10
+ url = generate_request_uri(nil, :comp => 'list')
11
+ request = generate_request("GET", url)
12
+ doc = REXML::Document.new(request.execute())
13
+ queues = []
14
+ REXML::XPath.each(doc, '//Queue/') do |item|
15
+ queues << { :name => REXML::XPath.first(item, "QueueName").text,
16
+ :url => REXML::XPath.first(item, "Url").text }
17
+ end
18
+ return queues
19
+ end
20
+
21
+ # Creates a queue on the current storage account. Throws WAZ::Queues::QueueAlreadyExists when
22
+ # existing metadata and given metadata differ.
23
+ def create_queue(queue_name, metadata = {})
24
+ begin
25
+ url = generate_request_uri(queue_name)
26
+ request = generate_request("PUT", url, metadata)
27
+ request.execute()
28
+ rescue RestClient::RequestFailed
29
+ raise WAZ::Queues::QueueAlreadyExists, queue_name if $!.http_code == 409
30
+ end
31
+ end
32
+
33
+ # Deletes the given queue from the current storage account.
34
+ def delete_queue(queue_name)
35
+ url = generate_request_uri(queue_name)
36
+ request = generate_request("DELETE", url)
37
+ request.execute()
38
+ end
39
+
40
+ # Gets the given queue metadata.
41
+ def get_queue_metadata(queue_name)
42
+ url = generate_request_uri(queue_name, :comp => 'metadata')
43
+ request = generate_request("HEAD", url)
44
+ request.execute().headers
45
+ end
46
+
47
+ # Sets the given queue metadata.
48
+ def set_queue_metadata(queue_name, metadata = {})
49
+ url = generate_request_uri(queue_name, :comp => 'metadata')
50
+ request = generate_request("PUT", url, metadata)
51
+ request.execute()
52
+ end
53
+
54
+ # Enqueues a message on the current queue.
55
+ #
56
+ # ttl Specifies the time-to-live interval for the message, in seconds. The maximum time-to-live allowed is 7 days. If this parameter
57
+ # is omitted, the default time-to-live is 7 days.
58
+ def enqueue(queue_name, message_payload, ttl = 604800)
59
+ url = generate_request_uri("#{queue_name}/messages", "messagettl" => ttl)
60
+ payload = "<?xml version=\"1.0\" encoding=\"utf-8\"?><QueueMessage><MessageText>#{message_payload}</MessageText></QueueMessage>"
61
+ request = generate_request("POST", url, { "Content-Type" => "application/xml" }, payload)
62
+ request.execute()
63
+ end
64
+
65
+ # Locks N messages (1 default) from the given queue.
66
+ #
67
+ # :num_of_messages option specifies the max number of messages to get (maximum 32)
68
+ #
69
+ # :visibility_timeout option specifies the timeout of the message locking in seconds (max two hours)
70
+ def get_messages(queue_name, options = {})
71
+ 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))
72
+ 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))
73
+ url = generate_request_uri("#{queue_name}/messages", options)
74
+ request = generate_request("GET", url)
75
+ doc = REXML::Document.new(request.execute())
76
+ messages = []
77
+ REXML::XPath.each(doc, '//QueueMessage/') do |item|
78
+ message = { :message_id => REXML::XPath.first(item, "MessageId").text,
79
+ :message_text => REXML::XPath.first(item, "MessageText").text,
80
+ :expiration_time => Time.httpdate(REXML::XPath.first(item, "ExpirationTime").text),
81
+ :insertion_time => Time.httpdate(REXML::XPath.first(item, "InsertionTime").text) }
82
+
83
+ # This are only valid when peek-locking messages
84
+ message[:pop_receipt] = REXML::XPath.first(item, "PopReceipt").text unless REXML::XPath.first(item, "PopReceipt").nil?
85
+ message[:time_next_visible] = Time.httpdate(REXML::XPath.first(item, "TimeNextVisible").text) unless REXML::XPath.first(item, "TimeNextVisible").nil?
86
+ messages << message
87
+ end
88
+ return messages
89
+ end
90
+
91
+ # Peeks N messages (default 1) from the given queue.
92
+ #
93
+ # Implementation is the same of get_messages but differs on an additional parameter called :peek_only.
94
+ def peek(queue_name, options = {})
95
+ return get_messages(queue_name, {:peek_only => true}.merge(options))
96
+ end
97
+
98
+ # Deletes the given message from the queue, correlating the operation with the pop_receipt
99
+ # in order to avoid eventually inconsistent scenarios.
100
+ def delete_message(queue_name, message_id, pop_receipt)
101
+ url = generate_request_uri("#{queue_name}/messages/#{message_id}", :pop_receipt => pop_receipt)
102
+ request = generate_request("DELETE", url)
103
+ request.execute()
104
+ end
105
+
106
+ # Marks every message on the given queue for deletion.
107
+ def clear_queue(queue_name)
108
+ url = generate_request_uri("#{queue_name}/messages")
109
+ request = generate_request("DELETE", url)
110
+ request.execute()
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,38 @@
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 unless options.keys.include? :access_key
20
+ options[:use_ssl] = false unless options.keys.include? :use_ssl
21
+ @connection = options
22
+ end
23
+
24
+ # Returns the default connection (last set) parameters. It will raise an exception WAZ::Storage::NotConnected
25
+ # when there's no connection information registered.
26
+ def default_connection
27
+ raise NotConnected unless connected?
28
+ return @connection
29
+ end
30
+
31
+ # Returns a value indicating whether the current connection information has been set or not.
32
+ def connected?
33
+ return !@connection.nil?
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,70 @@
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
7
+
8
+ # Creates an instance of the implementor service (internally used by the API).
9
+ def initialize(options = {})
10
+ self.account_name = options[:account_name]
11
+ self.access_key = options[:access_key]
12
+ self.use_ssl = options[:use_ssl] or false
13
+ self.base_url = "#{options[:type_of_service] or "blobs"}.#{options[:base_url] or "core.windows.net"}"
14
+ end
15
+
16
+ # Generates a request based on Adam Wiggings' rest-client, including all the required headers
17
+ # for interacting with Windows Azure Storage API (except for Tables). This methods embeds the
18
+ # authorization key signature on the request based on the given access_key.
19
+ def generate_request(verb, url, headers = {}, payload = nil)
20
+ http_headers = {}
21
+ headers.each{ |k, v| http_headers[k.to_s.gsub(/_/, '-')] = v} unless headers.nil?
22
+ request = RestClient::Request.new(:method => verb.downcase.to_sym, :url => url, :headers => http_headers, :payload => payload)
23
+ request.headers["x-ms-Date"] = Time.new.httpdate
24
+ request.headers["Content-Length"] = (request.payload or "").length
25
+ request.headers["Authorization"] = "SharedKey #{account_name}:#{generate_signature(request)}"
26
+ return request
27
+ end
28
+
29
+ # Generates the request uri based on the resource path, the protocol, the account name and the parameters passed
30
+ # on the options hash.
31
+ def generate_request_uri(path = nil, options = {})
32
+ protocol = use_ssl ? "https" : "http"
33
+ query_params = options.keys.sort{ |a, b| a.to_s <=> b.to_s}.map{ |k| "#{k.to_s.gsub(/_/, '')}=#{options[k]}"}.join("&") unless options.nil? or options.empty?
34
+ uri = "#{protocol}://#{account_name}.#{base_url}#{(path or "").start_with?("/") ? "" : "/"}#{(path or "")}"
35
+ uri << "?#{query_params}" if query_params
36
+ return uri
37
+ end
38
+
39
+ # Canonicalizes the request headers by following Microsoft's specification on how those headers have to be sorted
40
+ # and which of the given headers apply to be canonicalized.
41
+ def canonicalize_headers(headers)
42
+ 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")
43
+ return cannonicalized_headers
44
+ end
45
+
46
+ # Creates a canonical representation of the message by combining account_name/resource_path.
47
+ def canonicalize_message(url)
48
+ uri_component = url.gsub(/https?:\/\/[^\/]+\//i, '').gsub(/\?.*/i, '')
49
+ comp_component = url.scan(/(comp=[^&]+)/i).first()
50
+ uri_component << "?#{comp_component}" if comp_component
51
+ canonicalized_message = "/#{self.account_name}/#{uri_component}"
52
+ return canonicalized_message
53
+ end
54
+
55
+ # Generates the signature based on Micosoft specs for the REST API. It includes some special headers,
56
+ # the canonicalized header line and the canonical form of the message, all of the joined by \n character. Encoded with
57
+ # Base64 and encrypted with SHA256 using the access_key as the seed.
58
+ def generate_signature(request)
59
+ signature = request.method.to_s.upcase + "\x0A" +
60
+ (request.headers["Content-MD5"] or "") + "\x0A" +
61
+ (request.headers["Content-Type"] or "") + "\x0A" +
62
+ (request.headers["Date"] or "")+ "\x0A" +
63
+ canonicalize_headers(request.headers) + "\x0A" +
64
+ canonicalize_message(request.url)
65
+
66
+ return Base64.encode64(HMAC::SHA256.new(Base64.decode64(self.access_key)).update(signature.toutf8).digest)
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,25 @@
1
+ module WAZ
2
+ module Storage
3
+ # This class is the base exception from where all the exceptions raised from this API
4
+ # inherit from. If you want to handle an exception that your code may throw and you don't
5
+ # know which specific type you should handle, handle this type of exception.
6
+ class StorageException < StandardError
7
+ end
8
+
9
+ # This exception raises whenever a required parameter for initializing any class isn't provided. From
10
+ # WAZ::Storage::Base up to WAZ::Queues::Queue all of them use this exception.
11
+ class InvalidOption < StorageException
12
+ def initialize(missing_option)
13
+ super("You did not provide one of the required parameters. Please provide the #{missing_option}.")
14
+ end
15
+ end
16
+
17
+ # This exception is raised when the user tries to perform an operation on any Storage API class
18
+ # without previously specificying the connection options.
19
+ class NotConnected < StorageException
20
+ def initialize
21
+ super("You should establish connection before using the services, the connection configuration is required.")
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,11 @@
1
+ module WAZ
2
+ module Storage
3
+ module VERSION #:nodoc:
4
+ MAJOR = '0'
5
+ MINOR = '5'
6
+ TINY = '0'
7
+ end
8
+
9
+ Version = [VERSION::MAJOR, VERSION::MINOR, VERSION::TINY].compact * '.'
10
+ end
11
+ end
data/rakefile ADDED
@@ -0,0 +1,53 @@
1
+ require 'rake'
2
+ require 'rubygems'
3
+ require 'spec/rake/spectask'
4
+ require 'rake/gempackagetask'
5
+ require 'rake/rdoctask'
6
+ require 'lib/waz-storage'
7
+
8
+
9
+ namespace :test do
10
+ Spec::Rake::SpecTask.new('run_with_rcov') do |t|
11
+ puts "running tests and code coverage"
12
+ t.spec_files = FileList['tests/waz/queues/*.rb', 'tests/waz/blobs/*.rb', 'tests/waz/storage/*.rb']
13
+ t.rcov = true
14
+ t.rcov_opts = ['--text-report', '--exclude', "exclude.*/.gem,test,Library,#{ENV['GEM_HOME']}", '--sort', 'coverage' ]
15
+ t.spec_opts = ['-cfn']
16
+ end
17
+ end
18
+
19
+ namespace :dist do
20
+ spec = Gem::Specification.new do |s|
21
+ s.name = 'waz-storage'
22
+ s.version = Gem::Version.new(WAZ::Storage::Version)
23
+ s.summary = "Client library for Windows Azure's Storage Service REST API"
24
+ s.description = "A simple implementation of Windows Azure Storage API for Ruby, inspired by the S3 gems and self experience of dealing with queues. The major goal of the whole gem is to enable ruby developers [like me =)] to leverage Windows Azure Storage features and have another option for cloud storage."
25
+ s.email = 'johnny.halife@me.com'
26
+ s.author = 'Johnny G. Halife'
27
+ s.homepage = 'http://waz-storage.heroku.com'
28
+ s.require_paths = ["lib"]
29
+ s.files = FileList['rakefile', 'lib/**/*.rb']
30
+ s.test_files = Dir['test/**/*']
31
+ s.has_rdoc = true
32
+ s.rdoc_options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object'
33
+
34
+ # Dependencies
35
+ s.add_runtime_dependency 'rest-client', '>= 1.0.3'
36
+ s.add_dependency 'ruby-hmac'
37
+ end
38
+
39
+ Rake::GemPackageTask.new(spec) do |pkg|
40
+ pkg.need_tar = true
41
+ end
42
+ end
43
+
44
+ namespace :docs do
45
+ Rake::RDocTask.new do |t|
46
+ t.rdoc_dir = 'rdoc'
47
+ t.title = "Windows Azure Storage library — simple gem for accessing WAZ‘s Storage REST API"
48
+ t.options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object'
49
+ t.options << '--charset' << 'utf-8'
50
+ t.rdoc_files.include('README.rdoc')
51
+ t.rdoc_files.include('lib/**/*.rb')
52
+ end
53
+ end
metadata ADDED
@@ -0,0 +1,90 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: waz-storage
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.0
5
+ platform: ruby
6
+ authors:
7
+ - Johnny G. Halife
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-10-15 00:00:00 -03:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rest-client
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 1.0.3
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: ruby-hmac
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: "0"
34
+ version:
35
+ description: A simple implementation of Windows Azure Storage API for Ruby, inspired by the S3 gems and self experience of dealing with queues. The major goal of the whole gem is to enable ruby developers [like me =)] to leverage Windows Azure Storage features and have another option for cloud storage.
36
+ email: johnny.halife@me.com
37
+ executables: []
38
+
39
+ extensions: []
40
+
41
+ extra_rdoc_files: []
42
+
43
+ files:
44
+ - rakefile
45
+ - lib/waz/blobs/blob_object.rb
46
+ - lib/waz/blobs/container.rb
47
+ - lib/waz/blobs/service.rb
48
+ - lib/waz/queues/exceptions.rb
49
+ - lib/waz/queues/message.rb
50
+ - lib/waz/queues/queue.rb
51
+ - lib/waz/queues/service.rb
52
+ - lib/waz/storage/base.rb
53
+ - lib/waz/storage/core_service.rb
54
+ - lib/waz/storage/exceptions.rb
55
+ - lib/waz/storage/version.rb
56
+ - lib/waz-blobs.rb
57
+ - lib/waz-queues.rb
58
+ - lib/waz-storage.rb
59
+ has_rdoc: true
60
+ homepage: http://waz-storage.heroku.com
61
+ licenses: []
62
+
63
+ post_install_message:
64
+ rdoc_options:
65
+ - --line-numbers
66
+ - --inline-source
67
+ - -A cattr_accessor=object
68
+ require_paths:
69
+ - lib
70
+ required_ruby_version: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: "0"
75
+ version:
76
+ required_rubygems_version: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: "0"
81
+ version:
82
+ requirements: []
83
+
84
+ rubyforge_project:
85
+ rubygems_version: 1.3.5
86
+ signing_key:
87
+ specification_version: 3
88
+ summary: Client library for Windows Azure's Storage Service REST API
89
+ test_files: []
90
+