rackspace-cloudfiles 1.3.0.2

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.
@@ -0,0 +1,16 @@
1
+ cloudfiles.gemspec
2
+ lib/cloudfiles/authentication.rb
3
+ lib/cloudfiles/connection.rb
4
+ lib/cloudfiles/container.rb
5
+ lib/cloudfiles/storage_object.rb
6
+ lib/cloudfiles.rb
7
+ Manifest
8
+ Rakefile
9
+ README.rdoc
10
+ test/cf-testunit.rb
11
+ test/cloudfiles_authentication_test.rb
12
+ test/cloudfiles_connection_test.rb
13
+ test/cloudfiles_container_test.rb
14
+ test/cloudfiles_storage_object_test.rb
15
+ test/test_helper.rb
16
+ TODO
@@ -0,0 +1,60 @@
1
+ = Mosso Cloud Files
2
+
3
+ == Description
4
+
5
+ This is a Ruby interface into the Rackspace[http://rackspace.com/] {Mosso Cloud Files}[http://www.mosso.com/cloudfiles.jsp] service. Cloud Files is reliable, scalable and affordable web-based storage hosting for backing up and archiving all your static content. Cloud Files is the first and only cloud service that leverages a tier one CDN provider to create such an easy and complete storage-to-delivery solution for media content.
6
+
7
+ == Installation
8
+
9
+ This gem is available on Github[http://github.com/rackspace/ruby-cloudfiles/]. To install it, do
10
+
11
+ gem sources -a http://gems.github.com
12
+
13
+ sudo gem install rackspace-cloudfiles
14
+
15
+ To use it in a Rails application, add the following information to your config/environment.rb
16
+
17
+ config.gem "rackspace-cloudfiles", :source => "http://gems.github.com", :lib => "cloudfiles"
18
+
19
+
20
+ == Examples
21
+
22
+ See the class definitions for documentation on specific methods and operations.
23
+
24
+ require 'cloudfiles'
25
+
26
+ # Log into the Cloud Files system
27
+ cf = CloudFiles::Connection.new(USERNAME, API_KEY)
28
+
29
+ # Get a listing of all containers under this account
30
+ cf.containers
31
+ => ["backup", "Books", "cftest", "test", "video", "webpics"]
32
+
33
+ # Access a specific container
34
+ container = cf.container('test')
35
+
36
+ # See how many objects are under this container
37
+ container.count
38
+ => 3
39
+
40
+ # List the objects
41
+ container.objects
42
+ => ["bigfile.txt", "new.txt", "test.txt"]
43
+
44
+ # Select an object
45
+ object = container.object('test.txt')
46
+
47
+ # Get that object's data
48
+ object.data
49
+ => "This is test data"
50
+
51
+ == Authors
52
+
53
+ Initial work by Major Hayden <major.hayden@rackspace.com>
54
+
55
+ Subsequent work by H. Wade Minter <wade.minter@rackspace.com>
56
+
57
+ == License
58
+
59
+ See COPYING for license information.
60
+ Copyright (c) 2009, Rackspace US, Inc.
@@ -0,0 +1,37 @@
1
+ require 'rubygems'
2
+ gem 'echoe', '~> 3.0.1'
3
+ require 'echoe'
4
+ require './lib/cloudfiles.rb'
5
+
6
+ echoe = Echoe.new('cloudfiles') do |p|
7
+ p.author = ["H. Wade Minter", "Rackspace Hosting"]
8
+ p.email = 'wade.minter@rackspace.com'
9
+ p.version = CloudFiles::VERSION
10
+ p.summary = "A Ruby API into Mosso Cloud Files"
11
+ p.description = 'A Ruby version of the Mosso Cloud Files API.'
12
+ p.url = "http://www.mosso.com/cloudfiles.jsp"
13
+ p.runtime_dependencies = ["mime-types >=1.0"]
14
+ end
15
+
16
+ desc 'Generate the .gemspec file in the root directory'
17
+ task :gemspec do
18
+ File.open("#{echoe.name}.gemspec", "w") {|f| f << echoe.spec.to_ruby }
19
+ end
20
+ task :package => :gemspec
21
+
22
+ namespace :test do
23
+ desc 'Check test coverage'
24
+ task :coverage do
25
+ rm_f "coverage"
26
+ system("rcov -x '/Library/Ruby/Gems/1.8/gems/' --sort coverage #{File.join(File.dirname(__FILE__), 'test/*_test.rb')}")
27
+ system("open #{File.join(File.dirname(__FILE__), 'coverage/index.html')}") if PLATFORM['darwin']
28
+ end
29
+
30
+ desc 'Remove coverage products'
31
+ task :clobber_coverage do
32
+ rm_r 'coverage' rescue nil
33
+ end
34
+
35
+ end
36
+
37
+
data/TODO ADDED
File without changes
@@ -0,0 +1,38 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{cloudfiles}
5
+ s.version = "1.3.0.2"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["H. Wade Minter, Rackspace Hosting"]
9
+ s.date = %q{2009-05-14}
10
+ s.description = %q{A Ruby version of the Mosso Cloud Files API.}
11
+ s.email = %q{wade.minter@rackspace.com}
12
+ s.extra_rdoc_files = ["lib/cloudfiles/authentication.rb", "lib/cloudfiles/connection.rb", "lib/cloudfiles/container.rb", "lib/cloudfiles/storage_object.rb", "lib/cloudfiles.rb", "README.rdoc", "TODO"]
13
+ s.files = ["cloudfiles.gemspec", "lib/cloudfiles/authentication.rb", "lib/cloudfiles/connection.rb", "lib/cloudfiles/container.rb", "lib/cloudfiles/storage_object.rb", "lib/cloudfiles.rb", "Manifest", "Rakefile", "README.rdoc", "test/cf-testunit.rb", "test/cloudfiles_authentication_test.rb", "test/cloudfiles_connection_test.rb", "test/cloudfiles_container_test.rb", "test/cloudfiles_storage_object_test.rb", "test/test_helper.rb", "TODO"]
14
+ s.has_rdoc = true
15
+ s.homepage = %q{http://www.mosso.com/cloudfiles.jsp}
16
+ s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Cloudfiles", "--main", "README.rdoc"]
17
+ s.require_paths = ["lib"]
18
+ s.rubyforge_project = %q{cloudfiles}
19
+ s.rubygems_version = %q{1.3.1}
20
+ s.summary = %q{A Ruby API into Mosso Cloud Files}
21
+ s.test_files = ["test/cloudfiles_authentication_test.rb", "test/cloudfiles_connection_test.rb", "test/cloudfiles_container_test.rb", "test/cloudfiles_storage_object_test.rb", "test/test_helper.rb"]
22
+
23
+ if s.respond_to? :specification_version then
24
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
25
+ s.specification_version = 2
26
+
27
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
28
+ s.add_runtime_dependency(%q<mime-types>, [">= 1.0"])
29
+ s.add_development_dependency(%q<echoe>, [">= 0"])
30
+ else
31
+ s.add_dependency(%q<mime-types>, [">= 1.0"])
32
+ s.add_dependency(%q<echoe>, [">= 0"])
33
+ end
34
+ else
35
+ s.add_dependency(%q<mime-types>, [">= 1.0"])
36
+ s.add_dependency(%q<echoe>, [">= 0"])
37
+ end
38
+ end
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # == Cloud Files API
4
+ # ==== Connects Ruby Applications to Rackspace's {Mosso Cloud Files service}[http://www.mosso.com/cloudfiles.jsp]
5
+ # Initial work by Major Hayden <major.hayden@rackspace.com>
6
+ #
7
+ # Subsequent work by H. Wade Minter <wade.minter@rackspace.com>
8
+ #
9
+ # See COPYING for license information.
10
+ # Copyright (c) 2009, Rackspace US, Inc.
11
+ # ----
12
+ #
13
+ # === Documentation & Examples
14
+ # To begin reviewing the available methods and examples, peruse the README file, or begin by looking at documentation for the
15
+ # CloudFiles::Connection class.
16
+ #
17
+ # The CloudFiles class is the base class. Not much of note happens here.
18
+ # To create a new CloudFiles connection, use the CloudFiles::Connection.new('user_name', 'api_key') method.
19
+ module CloudFiles
20
+
21
+ VERSION = '1.3.0.2'
22
+ require 'net/http'
23
+ require 'net/https'
24
+ require 'rexml/document'
25
+ require 'uri'
26
+ require 'digest/md5'
27
+ require 'jcode'
28
+ require 'time'
29
+ require 'rubygems'
30
+ require 'mime/types'
31
+
32
+ $KCODE = 'u'
33
+
34
+ $:.unshift(File.dirname(__FILE__))
35
+ require 'cloudfiles/authentication'
36
+ require 'cloudfiles/connection'
37
+ require 'cloudfiles/container'
38
+ require 'cloudfiles/storage_object'
39
+
40
+ end
41
+
42
+
43
+
44
+ class SyntaxException < StandardError # :nodoc:
45
+ end
46
+ class ConnectionException < StandardError # :nodoc:
47
+ end
48
+ class AuthenticationException < StandardError # :nodoc:
49
+ end
50
+ class InvalidResponseException < StandardError # :nodoc:
51
+ end
52
+ class NonEmptyContainerException < StandardError # :nodoc:
53
+ end
54
+ class NoSuchObjectException < StandardError # :nodoc:
55
+ end
56
+ class NoSuchContainerException < StandardError # :nodoc:
57
+ end
58
+ class NoSuchAccountException < StandardError # :nodoc:
59
+ end
60
+ class MisMatchedChecksumException < StandardError # :nodoc:
61
+ end
62
+ class IOException < StandardError # :nodoc:
63
+ end
64
+ class CDNNotEnabledException < StandardError # :nodoc:
65
+ end
66
+ class ObjectExistsException < StandardError # :nodoc:
67
+ end
68
+ class ExpiredAuthTokenException < StandardError # :nodoc:
69
+ end
@@ -0,0 +1,38 @@
1
+ module CloudFiles
2
+ class Authentication
3
+ # See COPYING for license information.
4
+ # Copyright (c) 2009, Rackspace US, Inc.
5
+
6
+ # Performs an authentication to the Cloud Files servers. Opens a new HTTP connection to the API server,
7
+ # sends the credentials, and looks for a successful authentication. If it succeeds, it sets the cdmmgmthost,
8
+ # cdmmgmtpath, storagehost, storagepath, authtoken, and authok variables on the connection. If it fails, it raises
9
+ # an AuthenticationException.
10
+ #
11
+ # Should probably never be called directly.
12
+ def initialize(connection)
13
+ path = '/auth'
14
+ hdrhash = { "X-Auth-User" => connection.authuser, "X-Auth-Key" => connection.authkey }
15
+ begin
16
+ server = Net::HTTP.new('api.mosso.com',443)
17
+ server.use_ssl = true
18
+ server.verify_mode = OpenSSL::SSL::VERIFY_NONE
19
+ server.start
20
+ rescue
21
+ raise ConnectionException, "Unable to connect to #{server}"
22
+ end
23
+ response = server.get(path,hdrhash)
24
+ if (response.code == "204")
25
+ connection.cdnmgmthost = URI.parse(response["x-cdn-management-url"]).host
26
+ connection.cdnmgmtpath = URI.parse(response["x-cdn-management-url"]).path
27
+ connection.storagehost = URI.parse(response["x-storage-url"]).host
28
+ connection.storagepath = URI.parse(response["x-storage-url"]).path
29
+ connection.authtoken = response["x-auth-token"]
30
+ connection.authok = true
31
+ else
32
+ connection.authtoken = false
33
+ raise AuthenticationException, "Authentication failed"
34
+ end
35
+ server.finish
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,271 @@
1
+ module CloudFiles
2
+ class Connection
3
+ # See COPYING for license information.
4
+ # Copyright (c) 2009, Rackspace US, Inc.
5
+
6
+ # Authentication key provided when the CloudFiles class was instantiated
7
+ attr_reader :authkey
8
+
9
+ # Token returned after a successful authentication
10
+ attr_accessor :authtoken
11
+
12
+ # Authentication username provided when the CloudFiles class was instantiated
13
+ attr_reader :authuser
14
+
15
+ # Hostname of the CDN management server
16
+ attr_accessor :cdnmgmthost
17
+
18
+ # Path for managing containers on the CDN management server
19
+ attr_accessor :cdnmgmtpath
20
+
21
+ # Array of requests that have been made so far
22
+ attr_reader :reqlog
23
+
24
+ # Hostname of the storage server
25
+ attr_accessor :storagehost
26
+
27
+ # Path for managing containers/objects on the storage server
28
+ attr_accessor :storagepath
29
+
30
+ # Instance variable that is set when authorization succeeds
31
+ attr_accessor :authok
32
+
33
+ # The total size in bytes under this connection
34
+ attr_reader :bytes
35
+
36
+ # The total number of containers under this connection
37
+ attr_reader :count
38
+
39
+ # Creates a new CloudFiles::Connection object. Uses CloudFiles::Authentication to perform the login for the connection.
40
+ # The authuser is the Mosso username, the authkey is the Mosso API key.
41
+ #
42
+ # Setting the optional retry_auth variable to false will cause an exception to be thrown if your authorization token expires.
43
+ # Otherwise, it will attempt to reauthenticate.
44
+ #
45
+ # This will likely be the base class for most operations.
46
+ #
47
+ # cf = CloudFiles::Connection.new(MY_USERNAME, MY_API_KEY)
48
+ def initialize(authuser,authkey,retry_auth = true)
49
+ @authuser = authuser
50
+ @authkey = authkey
51
+ @retry_auth = retry_auth
52
+ @authok = false
53
+ @http = {}
54
+ @reqlog = []
55
+ CloudFiles::Authentication.new(self)
56
+ end
57
+
58
+ # Returns true if the authentication was successful and returns false otherwise.
59
+ #
60
+ # cf.authok?
61
+ # => true
62
+ def authok?
63
+ @authok
64
+ end
65
+
66
+ # Returns an CloudFiles::Container object that can be manipulated easily. Throws a NoSuchContainerException if
67
+ # the container doesn't exist.
68
+ #
69
+ # container = cf.container('test')
70
+ # container.count
71
+ # => 2
72
+ def container(name)
73
+ CloudFiles::Container.new(self,name)
74
+ end
75
+ alias :get_container :container
76
+
77
+ # Sets instance variables for the bytes of storage used for this account/connection, as well as the number of containers
78
+ # stored under the account. Returns a hash with :bytes and :count keys, and also sets the instance variables.
79
+ #
80
+ # cf.get_info
81
+ # => {:count=>8, :bytes=>42438527}
82
+ # cf.bytes
83
+ # => 42438527
84
+ def get_info
85
+ response = cfreq("HEAD",@storagehost,@storagepath)
86
+ raise InvalidResponseException, "Unable to obtain account size" unless (response.code == "204")
87
+ @bytes = response["x-account-bytes-used"].to_i
88
+ @count = response["x-account-container-count"].to_i
89
+ {:bytes => @bytes, :count => @count}
90
+ end
91
+
92
+ # Gathers a list of the containers that exist for the account and returns the list of container names
93
+ # as an array. If no containers exist, an empty array is returned. Throws an InvalidResponseException
94
+ # if the request fails.
95
+ #
96
+ # If you supply the optional limit and marker parameters, the call will return the number of containers
97
+ # specified in limit, starting after the object named in marker.
98
+ #
99
+ # cf.containers
100
+ # => ["backup", "Books", "cftest", "test", "video", "webpics"]
101
+ #
102
+ # cf.containers(2,'cftest')
103
+ # => ["test", "video"]
104
+ def containers(limit=0,marker="")
105
+ paramarr = []
106
+ paramarr << ["limit=#{URI.encode(limit.to_s).gsub(/&/,'%26')}"] if limit.to_i > 0
107
+ paramarr << ["offset=#{URI.encode(marker.to_s).gsub(/&/,'%26')}"] unless marker.to_s.empty?
108
+ paramstr = (paramarr.size > 0)? paramarr.join("&") : "" ;
109
+ response = cfreq("GET",@storagehost,"#{@storagepath}?#{paramstr}")
110
+ return [] if (response.code == "204")
111
+ raise InvalidResponseException, "Invalid response code #{response.code}" unless (response.code == "200")
112
+ response.body.to_a.map { |x| x.chomp }
113
+ end
114
+ alias :list_containers :containers
115
+
116
+ # Retrieves a list of containers on the account along with their sizes (in bytes) and counts of the objects
117
+ # held within them. If no containers exist, an empty hash is returned. Throws an InvalidResponseException
118
+ # if the request fails.
119
+ #
120
+ # If you supply the optional limit and marker parameters, the call will return the number of containers
121
+ # specified in limit, starting after the object named in marker.
122
+ #
123
+ # cf.containers_detail
124
+ # => { "container1" => { :bytes => "36543", :count => "146" },
125
+ # "container2" => { :bytes => "105943", :count => "25" } }
126
+ def containers_detail(limit=0,marker="")
127
+ paramarr = []
128
+ paramarr << ["limit=#{URI.encode(limit.to_s).gsub(/&/,'%26')}"] if limit.to_i > 0
129
+ paramarr << ["offset=#{URI.encode(marker.to_s).gsub(/&/,'%26')}"] unless marker.to_s.empty?
130
+ paramstr = (paramarr.size > 0)? paramarr.join("&") : "" ;
131
+ response = cfreq("GET",@storagehost,"#{@storagepath}?format=xml&#{paramstr}")
132
+ return {} if (response.code == "204")
133
+ raise InvalidResponseException, "Invalid response code #{response.code}" unless (response.code == "200")
134
+ doc = REXML::Document.new(response.body)
135
+ detailhash = {}
136
+ doc.elements.each("account/container/") { |c|
137
+ detailhash[c.elements["name"].text] = { :bytes => c.elements["bytes"].text, :count => c.elements["count"].text }
138
+ }
139
+ doc = nil
140
+ return detailhash
141
+ end
142
+ alias :list_containers_info :containers_detail
143
+
144
+ # Returns true if the requested container exists and returns false otherwise.
145
+ #
146
+ # cf.container_exists?('good_container')
147
+ # => true
148
+ #
149
+ # cf.container_exists?('bad_container')
150
+ # => false
151
+ def container_exists?(containername)
152
+ response = cfreq("HEAD",@storagehost,"#{@storagepath}/#{containername}")
153
+ return (response.code == "204")? true : false ;
154
+ end
155
+
156
+ # Creates a new container and returns the CloudFiles::Container object. Throws an InvalidResponseException if the
157
+ # request fails.
158
+ #
159
+ # Slash (/) and question mark (?) are invalid characters, and will be stripped out. The container name is limited to
160
+ # 256 characters or less.
161
+ #
162
+ # container = cf.create_container('new_container')
163
+ # container.name
164
+ # => "new_container"
165
+ #
166
+ # container = cf.create_container('bad/name')
167
+ # => SyntaxException: Container name cannot contain the characters '/' or '?'
168
+ def create_container(containername)
169
+ raise SyntaxException, "Container name cannot contain the characters '/' or '?'" if containername.match(/[\/\?]/)
170
+ raise SyntaxException, "Container name is limited to 256 characters" if containername.length > 256
171
+ response = cfreq("PUT",@storagehost,"#{@storagepath}/#{containername}")
172
+ raise InvalidResponseException, "Unable to create container #{containername}" unless (response.code == "201" || response.code == "202")
173
+ CloudFiles::Container.new(self,containername)
174
+ end
175
+
176
+ # Deletes a container from the account. Throws a NonEmptyContainerException if the container still contains
177
+ # objects. Throws a NoSuchContainerException if the container doesn't exist.
178
+ #
179
+ # cf.delete_container('new_container')
180
+ # => true
181
+ #
182
+ # cf.delete_container('video')
183
+ # => NonEmptyContainerException: Container video is not empty
184
+ #
185
+ # cf.delete_container('nonexistent')
186
+ # => NoSuchContainerException: Container nonexistent does not exist
187
+ def delete_container(containername)
188
+ response = cfreq("DELETE",@storagehost,"#{@storagepath}/#{containername}")
189
+ raise NonEmptyContainerException, "Container #{containername} is not empty" if (response.code == "409")
190
+ raise NoSuchContainerException, "Container #{containername} does not exist" unless (response.code == "204")
191
+ true
192
+ end
193
+
194
+ # Gathers a list of public (CDN-enabled) containers that exist for an account and returns the list of container names
195
+ # as an array. If no containers are public, an empty array is returned. Throws a InvalidResponseException if
196
+ # the request fails.
197
+ #
198
+ # If you pass the optional argument as true, it will only show containers that are CURRENTLY being shared on the CDN,
199
+ # as opposed to the default behavior which is to show all containers that have EVER been public.
200
+ #
201
+ # cf.public_containers
202
+ # => ["video", "webpics"]
203
+ def public_containers(enabled_only = false)
204
+ paramstr = enabled_only == true ? "enabled_only=true" : ""
205
+ response = cfreq("GET",@cdnmgmthost,"#{@cdnmgmtpath}?#{paramstr}")
206
+ return [] if (response.code == "204")
207
+ raise InvalidResponseException, "Invalid response code #{response.code}" unless (response.code == "200")
208
+ response.body.to_a.map { |x| x.chomp }
209
+ end
210
+
211
+ # This method actually makes the HTTP calls out to the server
212
+ def cfreq(method,server,path,headers = {},data = nil,attempts = 0,&block) # :nodoc:
213
+ start = Time.now
214
+ hdrhash = headerprep(headers)
215
+ start_http(server,path,hdrhash)
216
+ request = Net::HTTP.const_get(method.to_s.capitalize).new(path,hdrhash)
217
+ if data
218
+ if data.respond_to?(:read)
219
+ request.body_stream = data
220
+ else
221
+ request.body = data
222
+ end
223
+ request.content_length = data.respond_to?(:lstat) ? data.stat.size : data.size
224
+ else
225
+ request.content_length = 0
226
+ end
227
+ response = @http[server].request(request,&block)
228
+ raise ExpiredAuthTokenException if response.code == "401"
229
+ response
230
+ rescue Errno::EPIPE, Timeout::Error, Errno::EINVAL, EOFError
231
+ # Server closed the connection, retry
232
+ raise ConnectionException, "Unable to reconnect to #{server} after #{count} attempts" if attempts >= 5
233
+ attempts += 1
234
+ @http[server].finish
235
+ start_http(server,path,headers)
236
+ retry
237
+ rescue ExpiredAuthTokenException
238
+ raise ConnectionException, "Authentication token expired and you have requested not to retry" if @retry_auth == false
239
+ CloudFiles::Authentication.new(self)
240
+ retry
241
+ end
242
+
243
+ private
244
+
245
+ # Sets up standard HTTP headers
246
+ def headerprep(headers = {}) # :nodoc:
247
+ default_headers = {}
248
+ default_headers["X-Auth-Token"] = @authtoken if (authok? && @account.nil?)
249
+ default_headers["X-Storage-Token"] = @authtoken if (authok? && !@account.nil?)
250
+ default_headers["Connection"] = "Keep-Alive"
251
+ default_headers["User-Agent"] = "CloudFiles Ruby API #{VERSION}"
252
+ default_headers.merge(headers)
253
+ end
254
+
255
+ # Starts (or restarts) the HTTP connection
256
+ def start_http(server,path,headers) # :nodoc:
257
+ if (@http[server].nil?)
258
+ begin
259
+ @http[server] = Net::HTTP.new(server,443)
260
+ @http[server].use_ssl = true
261
+ @http[server].verify_mode = OpenSSL::SSL::VERIFY_NONE
262
+ @http[server].start
263
+ rescue
264
+ raise ConnectionException, "Unable to connect to #{server}"
265
+ end
266
+ end
267
+ end
268
+
269
+ end
270
+
271
+ end