briancollins-cloudfiles 1.3.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/Manifest ADDED
@@ -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
data/README.rdoc ADDED
@@ -0,0 +1,47 @@
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
+ == Examples
8
+
9
+ See the class definitions for documentation on specific methods and operations.
10
+
11
+ require 'cloudfiles'
12
+
13
+ # Log into the Cloud Files system
14
+ cf = CloudFiles::Connection.new(USERNAME, API_KEY)
15
+
16
+ # Get a listing of all containers under this account
17
+ cf.containers
18
+ => ["backup", "Books", "cftest", "test", "video", "webpics"]
19
+
20
+ # Access a specific container
21
+ container = cf.container('test')
22
+
23
+ # See how many objects are under this container
24
+ container.count
25
+ => 3
26
+
27
+ # List the objects
28
+ container.objects
29
+ => ["bigfile.txt", "new.txt", "test.txt"]
30
+
31
+ # Select an object
32
+ object = container.object('test.txt')
33
+
34
+ # Get that object's data
35
+ object.data
36
+ => "This is test data"
37
+
38
+ == Authors
39
+
40
+ Initial work by Major Hayden <major.hayden@rackspace.com>
41
+
42
+ Subsequent work by H. Wade Minter <wade.minter@rackspace.com>
43
+
44
+ == License
45
+
46
+ See COPYING for license information.
47
+ Copyright (c) 2009, Rackspace US, Inc.
data/Rakefile ADDED
@@ -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.1"
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-02-05}
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
data/lib/cloudfiles.rb ADDED
@@ -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'
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,272 @@
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)}"] if limit.to_i > 0
107
+ paramarr << ["offset=#{URI.encode(marker.to_s)}"] 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)}"] if limit.to_i > 0
129
+ paramarr << ["offset=#{URI.encode(marker.to_s)}"] 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
+ path = URI.escape(path)
216
+ start_http(server,path,hdrhash)
217
+ request = Net::HTTP.const_get(method.to_s.capitalize).new(path,hdrhash)
218
+ if data
219
+ if data.respond_to?(:read)
220
+ request.body_stream = data
221
+ else
222
+ request.body = data
223
+ end
224
+ request.content_length = data.respond_to?(:lstat) ? data.stat.size : data.size
225
+ else
226
+ request.content_length = 0
227
+ end
228
+ response = @http[server].request(request,&block)
229
+ raise ExpiredAuthTokenException if response.code == "401"
230
+ response
231
+ rescue Errno::EPIPE, Timeout::Error, Errno::EINVAL, EOFError
232
+ # Server closed the connection, retry
233
+ raise ConnectionException, "Unable to reconnect to #{server} after #{count} attempts" if attempts >= 5
234
+ attempts += 1
235
+ @http[server].finish
236
+ start_http(server,path,headers)
237
+ retry
238
+ rescue ExpiredAuthTokenException
239
+ raise ConnectionException, "Authentication token expired and you have requested not to retry" if @retry_auth == false
240
+ CloudFiles::Authentication.new(self)
241
+ retry
242
+ end
243
+
244
+ private
245
+
246
+ # Sets up standard HTTP headers
247
+ def headerprep(headers = {}) # :nodoc:
248
+ default_headers = {}
249
+ default_headers["X-Auth-Token"] = @authtoken if (authok? && @account.nil?)
250
+ default_headers["X-Storage-Token"] = @authtoken if (authok? && !@account.nil?)
251
+ default_headers["Connection"] = "Keep-Alive"
252
+ default_headers["User-Agent"] = "Ruby-CloudFiles/#{VERSION}"
253
+ default_headers.merge(headers)
254
+ end
255
+
256
+ # Starts (or restarts) the HTTP connection
257
+ def start_http(server,path,headers) # :nodoc:
258
+ if (@http[server].nil?)
259
+ begin
260
+ @http[server] = Net::HTTP.new(server,443)
261
+ @http[server].use_ssl = true
262
+ @http[server].verify_mode = OpenSSL::SSL::VERIFY_NONE
263
+ @http[server].start
264
+ rescue
265
+ raise ConnectionException, "Unable to connect to #{server}"
266
+ end
267
+ end
268
+ end
269
+
270
+ end
271
+
272
+ end