cloudfiles-sagamore 1.5.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +7 -0
- data/CHANGELOG +56 -0
- data/CONTRIBUTORS +34 -0
- data/COPYING +12 -0
- data/Gemfile +7 -0
- data/README.rdoc +81 -0
- data/Rakefile +21 -0
- data/TODO +0 -0
- data/cloudfiles.gemspec +69 -0
- data/lib/client.rb +618 -0
- data/lib/cloudfiles.rb +85 -0
- data/lib/cloudfiles/authentication.rb +52 -0
- data/lib/cloudfiles/connection.rb +286 -0
- data/lib/cloudfiles/container.rb +451 -0
- data/lib/cloudfiles/exception.rb +65 -0
- data/lib/cloudfiles/storage_object.rb +426 -0
- data/lib/cloudfiles/version.rb +3 -0
- data/test/cf-testunit.rb +157 -0
- data/test/cloudfiles_authentication_test.rb +44 -0
- data/test/cloudfiles_client_test.rb +797 -0
- data/test/cloudfiles_connection_test.rb +214 -0
- data/test/cloudfiles_container_test.rb +494 -0
- data/test/cloudfiles_storage_object_test.rb +211 -0
- data/test/test_helper.rb +6 -0
- metadata +112 -0
data/lib/cloudfiles.rb
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# == Cloud Files API
|
4
|
+
# ==== Connects Ruby Applications to Rackspace's {Cloud Files service}[http://www.rackspacecloud.com/cloud_hosting_products/files]
|
5
|
+
# Initial work by Major Hayden <major.hayden@rackspace.com>
|
6
|
+
#
|
7
|
+
# Subsequent work by H. Wade Minter <minter@lunenburg.org> and Dan Prince <dan.prince@rackspace.com>
|
8
|
+
#
|
9
|
+
# See COPYING for license information.
|
10
|
+
# Copyright (c) 2011, 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(:username => 'user_name', :api_key => 'api_key') method.
|
19
|
+
|
20
|
+
module CloudFiles
|
21
|
+
|
22
|
+
AUTH_USA = "https://auth.api.rackspacecloud.com/v1.0"
|
23
|
+
AUTH_UK = "https://lon.auth.api.rackspacecloud.com/v1.0"
|
24
|
+
|
25
|
+
require 'net/http'
|
26
|
+
require 'net/https'
|
27
|
+
require 'rexml/document'
|
28
|
+
require 'cgi'
|
29
|
+
require 'uri'
|
30
|
+
require 'digest/md5'
|
31
|
+
require 'date'
|
32
|
+
require 'time'
|
33
|
+
require 'rubygems'
|
34
|
+
|
35
|
+
unless "".respond_to? :each_char
|
36
|
+
require "jcode"
|
37
|
+
$KCODE = 'u'
|
38
|
+
end
|
39
|
+
|
40
|
+
$:.unshift(File.dirname(__FILE__))
|
41
|
+
require 'client'
|
42
|
+
require 'cloudfiles/version'
|
43
|
+
require 'cloudfiles/exception'
|
44
|
+
require 'cloudfiles/authentication'
|
45
|
+
require 'cloudfiles/connection'
|
46
|
+
require 'cloudfiles/container'
|
47
|
+
require 'cloudfiles/storage_object'
|
48
|
+
|
49
|
+
def self.lines(str)
|
50
|
+
(str.respond_to?(:lines) ? str.lines : str).to_a.map { |x| x.chomp }
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.escape(str)
|
54
|
+
URI.encode(str)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
|
60
|
+
class SyntaxException < StandardError # :nodoc:
|
61
|
+
end
|
62
|
+
class ConnectionException < StandardError # :nodoc:
|
63
|
+
end
|
64
|
+
class AuthenticationException < StandardError # :nodoc:
|
65
|
+
end
|
66
|
+
class InvalidResponseException < StandardError # :nodoc:
|
67
|
+
end
|
68
|
+
class NonEmptyContainerException < StandardError # :nodoc:
|
69
|
+
end
|
70
|
+
class NoSuchObjectException < StandardError # :nodoc:
|
71
|
+
end
|
72
|
+
class NoSuchContainerException < StandardError # :nodoc:
|
73
|
+
end
|
74
|
+
class NoSuchAccountException < StandardError # :nodoc:
|
75
|
+
end
|
76
|
+
class MisMatchedChecksumException < StandardError # :nodoc:
|
77
|
+
end
|
78
|
+
class IOException < StandardError # :nodoc:
|
79
|
+
end
|
80
|
+
class CDNNotEnabledException < StandardError # :nodoc:
|
81
|
+
end
|
82
|
+
class ObjectExistsException < StandardError # :nodoc:
|
83
|
+
end
|
84
|
+
class ExpiredAuthTokenException < StandardError # :nodoc:
|
85
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module CloudFiles
|
2
|
+
class Authentication
|
3
|
+
# See COPYING for license information.
|
4
|
+
# Copyright (c) 2011, 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 CloudFiles::Exception::Authentication exception.
|
10
|
+
#
|
11
|
+
# Should never be called directly.
|
12
|
+
def initialize(connection)
|
13
|
+
begin
|
14
|
+
storage_url, auth_token, headers = SwiftClient.get_auth(connection.auth_url, connection.authuser, connection.authkey, connection.snet?)
|
15
|
+
rescue => e
|
16
|
+
# uncomment if you suspect a problem with this branch of code
|
17
|
+
# $stderr.puts "got error #{e.class}: #{e.message.inspect}\n" << e.traceback.map{|n| "\t#{n}"}.join("\n")
|
18
|
+
raise CloudFiles::Exception::Connection, "Unable to connect to #{connection.auth_url}", caller
|
19
|
+
end
|
20
|
+
if auth_token
|
21
|
+
if headers["x-cdn-management-url"]
|
22
|
+
connection.cdn_available = true
|
23
|
+
parsed_cdn_url = URI.parse(headers["x-cdn-management-url"])
|
24
|
+
connection.cdnmgmthost = parsed_cdn_url.host
|
25
|
+
connection.cdnmgmtpath = parsed_cdn_url.path
|
26
|
+
connection.cdnmgmtport = parsed_cdn_url.port
|
27
|
+
connection.cdnmgmtscheme = parsed_cdn_url.scheme
|
28
|
+
end
|
29
|
+
parsed_storage_url = URI.parse(headers["x-storage-url"])
|
30
|
+
connection.storagehost = set_snet(connection, parsed_storage_url.host)
|
31
|
+
connection.storagepath = parsed_storage_url.path
|
32
|
+
connection.storageport = parsed_storage_url.port
|
33
|
+
connection.storagescheme = parsed_storage_url.scheme
|
34
|
+
connection.authtoken = headers["x-auth-token"]
|
35
|
+
connection.authok = true
|
36
|
+
else
|
37
|
+
connection.authtoken = false
|
38
|
+
raise CloudFiles::Exception::Authentication, "Authentication failed"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def set_snet(connection, hostname)
|
45
|
+
if connection.snet?
|
46
|
+
"snet-#{hostname}"
|
47
|
+
else
|
48
|
+
hostname
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,286 @@
|
|
1
|
+
module CloudFiles
|
2
|
+
class Connection
|
3
|
+
# See COPYING for license information.
|
4
|
+
# Copyright (c) 2011, 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
|
+
# API host to authenticate to
|
16
|
+
attr_reader :auth_url
|
17
|
+
|
18
|
+
# Set at auth to see if a CDN is available for use
|
19
|
+
attr_accessor :cdn_available
|
20
|
+
alias :cdn_available? :cdn_available
|
21
|
+
|
22
|
+
# Hostname of the CDN management server
|
23
|
+
attr_accessor :cdnmgmthost
|
24
|
+
|
25
|
+
# Path for managing containers on the CDN management server
|
26
|
+
attr_accessor :cdnmgmtpath
|
27
|
+
|
28
|
+
# Port number for the CDN server
|
29
|
+
attr_accessor :cdnmgmtport
|
30
|
+
|
31
|
+
# URI scheme for the CDN server
|
32
|
+
attr_accessor :cdnmgmtscheme
|
33
|
+
|
34
|
+
# Hostname of the storage server
|
35
|
+
attr_accessor :storagehost
|
36
|
+
|
37
|
+
# Path for managing containers/objects on the storage server
|
38
|
+
attr_accessor :storagepath
|
39
|
+
|
40
|
+
# Port for managing the storage server
|
41
|
+
attr_accessor :storageport
|
42
|
+
|
43
|
+
# URI scheme for the storage server
|
44
|
+
attr_accessor :storagescheme
|
45
|
+
|
46
|
+
# Instance variable that is set when authorization succeeds
|
47
|
+
attr_accessor :authok
|
48
|
+
|
49
|
+
# Optional proxy variables
|
50
|
+
attr_reader :proxy_host
|
51
|
+
attr_reader :proxy_port
|
52
|
+
|
53
|
+
# Creates a new CloudFiles::Connection object. Uses CloudFiles::Authentication to perform the login for the connection.
|
54
|
+
# The authuser is the Rackspace Cloud username, the authkey is the Rackspace Cloud API key.
|
55
|
+
#
|
56
|
+
# Setting the :retry_auth option to false will cause an exception to be thrown if your authorization token expires.
|
57
|
+
# Otherwise, it will attempt to reauthenticate.
|
58
|
+
#
|
59
|
+
# Setting the :snet option to true or setting an environment variable of RACKSPACE_SERVICENET to any value will cause
|
60
|
+
# storage URLs to be returned with a prefix pointing them to the internal Rackspace service network, instead of a public URL.
|
61
|
+
#
|
62
|
+
# This is useful if you are using the library on a Rackspace-hosted system, as it provides faster speeds, keeps traffic off of
|
63
|
+
# the public network, and the bandwidth is not billed.
|
64
|
+
#
|
65
|
+
# If you need to connect to a Cloud Files installation that is NOT the standard Rackspace one, set the :auth_url option to the URL
|
66
|
+
# of your authentication endpoint. The old option name of :authurl is deprecated. The default is CloudFiles::AUTH_USA (https://auth.api.rackspacecloud.com/v1.0)
|
67
|
+
#
|
68
|
+
# There are two predefined constants to represent the United States-based authentication endpoint and the United Kingdom-based endpoint:
|
69
|
+
# CloudFiles::AUTH_USA (the default) and CloudFiles::AUTH_UK - both can be passed to the :auth_url option to quickly choose one or the other.
|
70
|
+
#
|
71
|
+
# This will likely be the base class for most operations.
|
72
|
+
#
|
73
|
+
# With gem 1.4.8, the connection style has changed. It is now a hash of arguments. Note that the proxy options are currently only
|
74
|
+
# supported in the new style.
|
75
|
+
#
|
76
|
+
# cf = CloudFiles::Connection.new(:username => "MY_USERNAME", :api_key => "MY_API_KEY", :auth_url => CloudFiles::AUTH_UK, :retry_auth => true, :snet => false, :proxy_host => "localhost", :proxy_port => "1234")
|
77
|
+
#
|
78
|
+
# The old style (positional arguments) is deprecated and will be removed at some point in the future.
|
79
|
+
#
|
80
|
+
# cf = CloudFiles::Connection.new(MY_USERNAME, MY_API_KEY, RETRY_AUTH, USE_SNET)
|
81
|
+
def initialize(*args)
|
82
|
+
if args[0].is_a?(Hash)
|
83
|
+
options = args[0]
|
84
|
+
@authuser = options[:username] ||( raise CloudFiles::Exception::Authentication, "Must supply a :username")
|
85
|
+
@authkey = options[:api_key] || (raise CloudFiles::Exception::Authentication, "Must supply an :api_key")
|
86
|
+
@auth_url = options[:authurl] || CloudFiles::AUTH_USA
|
87
|
+
@auth_url = options[:auth_url] || CloudFiles::AUTH_USA
|
88
|
+
@retry_auth = options[:retry_auth] || true
|
89
|
+
@snet = ENV['RACKSPACE_SERVICENET'] || options[:snet]
|
90
|
+
@proxy_host = options[:proxy_host]
|
91
|
+
@proxy_port = options[:proxy_port]
|
92
|
+
else
|
93
|
+
@authuser = args[0] ||( raise CloudFiles::Exception::Authentication, "Must supply the username as the first argument")
|
94
|
+
@authkey = args[1] || (raise CloudFiles::Exception::Authentication, "Must supply the API key as the second argument")
|
95
|
+
@retry_auth = args[2] || true
|
96
|
+
@snet = (ENV['RACKSPACE_SERVICENET'] || args[3]) ? true : false
|
97
|
+
@auth_url = CloudFiles::AUTH_USA
|
98
|
+
end
|
99
|
+
@authok = false
|
100
|
+
@http = {}
|
101
|
+
CloudFiles::Authentication.new(self)
|
102
|
+
end
|
103
|
+
|
104
|
+
# Returns true if the authentication was successful and returns false otherwise.
|
105
|
+
#
|
106
|
+
# cf.authok?
|
107
|
+
# => true
|
108
|
+
def authok?
|
109
|
+
@authok
|
110
|
+
end
|
111
|
+
|
112
|
+
# Returns true if the library is requesting the use of the Rackspace service network
|
113
|
+
def snet?
|
114
|
+
@snet
|
115
|
+
end
|
116
|
+
|
117
|
+
# Returns an CloudFiles::Container object that can be manipulated easily. Throws a NoSuchContainerException if
|
118
|
+
# the container doesn't exist.
|
119
|
+
#
|
120
|
+
# container = cf.container('test')
|
121
|
+
# container.count
|
122
|
+
# => 2
|
123
|
+
def container(name)
|
124
|
+
CloudFiles::Container.new(self, name)
|
125
|
+
end
|
126
|
+
alias :get_container :container
|
127
|
+
|
128
|
+
# Sets instance variables for the bytes of storage used for this account/connection, as well as the number of containers
|
129
|
+
# stored under the account. Returns a hash with :bytes and :count keys, and also sets the instance variables.
|
130
|
+
#
|
131
|
+
# cf.get_info
|
132
|
+
# => {:count=>8, :bytes=>42438527}
|
133
|
+
# cf.bytes
|
134
|
+
# => 42438527
|
135
|
+
# Hostname of the storage server
|
136
|
+
|
137
|
+
def get_info
|
138
|
+
begin
|
139
|
+
raise CloudFiles::Exception::AuthenticationException, "Not authenticated" unless self.authok?
|
140
|
+
response = SwiftClient.head_account(storageurl, self.authtoken)
|
141
|
+
@bytes = response["x-account-bytes-used"].to_i
|
142
|
+
@count = response["x-account-container-count"].to_i
|
143
|
+
{:bytes => @bytes, :count => @count}
|
144
|
+
rescue ClientException => e
|
145
|
+
raise CloudFiles::Exception::InvalidResponse, "Unable to obtain account size" unless (e.status.to_s == "204")
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# The total size in bytes under this connection
|
150
|
+
def bytes
|
151
|
+
get_info[:bytes]
|
152
|
+
end
|
153
|
+
|
154
|
+
# The total number of containers under this connection
|
155
|
+
def count
|
156
|
+
get_info[:count]
|
157
|
+
end
|
158
|
+
|
159
|
+
# Gathers a list of the containers that exist for the account and returns the list of container names
|
160
|
+
# as an array. If no containers exist, an empty array is returned. Throws an InvalidResponseException
|
161
|
+
# if the request fails.
|
162
|
+
#
|
163
|
+
# If you supply the optional limit and marker parameters, the call will return the number of containers
|
164
|
+
# specified in limit, starting after the object named in marker.
|
165
|
+
#
|
166
|
+
# cf.containers
|
167
|
+
# => ["backup", "Books", "cftest", "test", "video", "webpics"]
|
168
|
+
#
|
169
|
+
# cf.containers(2,'cftest')
|
170
|
+
# => ["test", "video"]
|
171
|
+
def containers(limit = nil, marker = nil)
|
172
|
+
begin
|
173
|
+
response = SwiftClient.get_account(storageurl, self.authtoken, marker, limit)
|
174
|
+
response[1].collect{|c| c['name']}
|
175
|
+
rescue ClientException => e
|
176
|
+
raise CloudFiles::Exception::InvalidResponse, "Invalid response code #{e.status.to_s}" unless (e.status.to_s == "200")
|
177
|
+
end
|
178
|
+
end
|
179
|
+
alias :list_containers :containers
|
180
|
+
|
181
|
+
# Retrieves a list of containers on the account along with their sizes (in bytes) and counts of the objects
|
182
|
+
# held within them. If no containers exist, an empty hash is returned. Throws an InvalidResponseException
|
183
|
+
# if the request fails.
|
184
|
+
#
|
185
|
+
# If you supply the optional limit and marker parameters, the call will return the number of containers
|
186
|
+
# specified in limit, starting after the object named in marker.
|
187
|
+
#
|
188
|
+
# cf.containers_detail
|
189
|
+
# => { "container1" => { :bytes => "36543", :count => "146" },
|
190
|
+
# "container2" => { :bytes => "105943", :count => "25" } }
|
191
|
+
def containers_detail(limit = nil, marker = nil)
|
192
|
+
begin
|
193
|
+
response = SwiftClient.get_account(storageurl, self.authtoken, marker, limit)
|
194
|
+
Hash[*response[1].collect{|c| [c['name'], {:bytes => c['bytes'], :count => c['count']}]}.flatten]
|
195
|
+
rescue ClientException => e
|
196
|
+
raise CloudFiles::Exception::InvalidResponse, "Invalid response code #{e.status.to_s}" unless (e.status.to_s == "200")
|
197
|
+
end
|
198
|
+
end
|
199
|
+
alias :list_containers_info :containers_detail
|
200
|
+
|
201
|
+
# Returns true if the requested container exists and returns false otherwise.
|
202
|
+
#
|
203
|
+
# cf.container_exists?('good_container')
|
204
|
+
# => true
|
205
|
+
#
|
206
|
+
# cf.container_exists?('bad_container')
|
207
|
+
# => false
|
208
|
+
def container_exists?(containername)
|
209
|
+
begin
|
210
|
+
response = SwiftClient.head_container(storageurl, self.authtoken, containername)
|
211
|
+
true
|
212
|
+
rescue ClientException => e
|
213
|
+
false
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
# Creates a new container and returns the CloudFiles::Container object. Throws an InvalidResponseException if the
|
218
|
+
# request fails.
|
219
|
+
#
|
220
|
+
# "/" is not valid in a container name. The container name is limited to
|
221
|
+
# 256 characters or less.
|
222
|
+
#
|
223
|
+
# container = cf.create_container('new_container')
|
224
|
+
# container.name
|
225
|
+
# => "new_container"
|
226
|
+
#
|
227
|
+
# container = cf.create_container('bad/name')
|
228
|
+
# => SyntaxException: Container name cannot contain '/'
|
229
|
+
def create_container(containername)
|
230
|
+
raise CloudFiles::Exception::Syntax, "Container name cannot contain '/'" if containername.match("/")
|
231
|
+
raise CloudFiles::Exception::Syntax, "Container name is limited to 256 characters" if containername.length > 256
|
232
|
+
begin
|
233
|
+
SwiftClient.put_container(storageurl, self.authtoken, containername)
|
234
|
+
CloudFiles::Container.new(self, containername)
|
235
|
+
rescue ClientException => e
|
236
|
+
raise CloudFiles::Exception::InvalidResponse, "Unable to create container #{containername}" unless (e.status.to_s == "201" || e.status.to_s == "202")
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
# Deletes a container from the account. Throws a NonEmptyContainerException if the container still contains
|
241
|
+
# objects. Throws a NoSuchContainerException if the container doesn't exist.
|
242
|
+
#
|
243
|
+
# cf.delete_container('new_container')
|
244
|
+
# => true
|
245
|
+
#
|
246
|
+
# cf.delete_container('video')
|
247
|
+
# => NonEmptyContainerException: Container video is not empty
|
248
|
+
#
|
249
|
+
# cf.delete_container('nonexistent')
|
250
|
+
# => NoSuchContainerException: Container nonexistent does not exist
|
251
|
+
def delete_container(containername)
|
252
|
+
begin
|
253
|
+
SwiftClient.delete_container(storageurl, self.authtoken, containername)
|
254
|
+
rescue ClientException => e
|
255
|
+
raise CloudFiles::Exception::NonEmptyContainer, "Container #{containername} is not empty" if (e.status.to_s == "409")
|
256
|
+
raise CloudFiles::Exception::NoSuchContainer, "Container #{containername} does not exist" unless (e.status.to_s == "204")
|
257
|
+
end
|
258
|
+
true
|
259
|
+
end
|
260
|
+
|
261
|
+
# Gathers a list of public (CDN-enabled) containers that exist for an account and returns the list of container names
|
262
|
+
# as an array. If no containers are public, an empty array is returned. Throws a InvalidResponseException if
|
263
|
+
# the request fails.
|
264
|
+
#
|
265
|
+
# If you pass the optional argument as true, it will only show containers that are CURRENTLY being shared on the CDN,
|
266
|
+
# as opposed to the default behavior which is to show all containers that have EVER been public.
|
267
|
+
#
|
268
|
+
# cf.public_containers
|
269
|
+
# => ["video", "webpics"]
|
270
|
+
def public_containers(enabled_only = false)
|
271
|
+
begin
|
272
|
+
response = SwiftClient.get_account(cdnurl, self.authtoken)
|
273
|
+
response[1].collect{|c| c['name']}
|
274
|
+
rescue ClientException => e
|
275
|
+
raise CloudFiles::Exception::InvalidResponse, "Invalid response code #{e.status.to_s}" unless (e.status.to_s == "200")
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
def storageurl
|
280
|
+
"#{self.storagescheme}://#{self.storagehost}:#{self.storageport.to_s}#{self.storagepath}"
|
281
|
+
end
|
282
|
+
def cdnurl
|
283
|
+
"#{self.cdnmgmtscheme}://#{self.cdnmgmthost}:#{self.cdnmgmtport.to_s}#{self.cdnmgmtpath}"
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|
@@ -0,0 +1,451 @@
|
|
1
|
+
module CloudFiles
|
2
|
+
class Container
|
3
|
+
# See COPYING for license information.
|
4
|
+
# Copyright (c) 2011, Rackspace US, Inc.
|
5
|
+
|
6
|
+
# Name of the container which corresponds to the instantiated container class
|
7
|
+
attr_reader :name
|
8
|
+
|
9
|
+
# The parent CloudFiles::Connection object for this container
|
10
|
+
attr_reader :connection
|
11
|
+
|
12
|
+
# Retrieves an existing CloudFiles::Container object tied to the current CloudFiles::Connection. If the requested
|
13
|
+
# container does not exist, it will raise a CloudFiles::Exception::NoSuchContainer Exception.
|
14
|
+
#
|
15
|
+
# Will likely not be called directly, instead use connection.container('container_name') to retrieve the object.
|
16
|
+
def initialize(connection, name)
|
17
|
+
@connection = connection
|
18
|
+
@name = name
|
19
|
+
# Load the metadata now, so we'll get a CloudFiles::Exception::NoSuchContainer exception should the container
|
20
|
+
# not exist.
|
21
|
+
self.container_metadata
|
22
|
+
end
|
23
|
+
|
24
|
+
# Refreshes data about the container and populates class variables. Items are otherwise
|
25
|
+
# loaded in a lazy loaded fashion.
|
26
|
+
#
|
27
|
+
# container.count
|
28
|
+
# => 2
|
29
|
+
# [Upload new file to the container]
|
30
|
+
# container.count
|
31
|
+
# => 2
|
32
|
+
# container.populate
|
33
|
+
# container.count
|
34
|
+
# => 3
|
35
|
+
def refresh
|
36
|
+
@metadata = @cdn_metadata = nil
|
37
|
+
true
|
38
|
+
end
|
39
|
+
alias :populate :refresh
|
40
|
+
|
41
|
+
# Retrieves Metadata for the container
|
42
|
+
def container_metadata
|
43
|
+
@metadata ||= (
|
44
|
+
begin
|
45
|
+
response = SwiftClient.head_container(self.connection.storageurl, self.connection.authtoken, escaped_name)
|
46
|
+
resphash = {}
|
47
|
+
response.to_hash.select { |k,v| k.match(/^x-container-meta/) }.each { |x| resphash[x[0]] = x[1].to_s }
|
48
|
+
{:bytes => response["x-container-bytes-used"].to_i, :count => response["x-container-object-count"].to_i, :metadata => resphash, :container_read => response["x-container-read"], :container_write => response["x-container-write"]}
|
49
|
+
rescue ClientException => e
|
50
|
+
raise CloudFiles::Exception::NoSuchContainer, "Container #{@name} does not exist" unless (e.status.to_s =~ /^20/)
|
51
|
+
end
|
52
|
+
)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Retrieves CDN-Enabled Meta Data
|
56
|
+
def cdn_metadata
|
57
|
+
return @cdn_metadata if @cdn_metadata
|
58
|
+
if cdn_available?
|
59
|
+
@cdn_metadata = (
|
60
|
+
begin
|
61
|
+
response = SwiftClient.head_container(self.connection.cdnurl, self.connection.authtoken, escaped_name)
|
62
|
+
cdn_enabled = ((response["x-cdn-enabled"] || "").downcase == "true") ? true : false
|
63
|
+
rescue ClientException => e
|
64
|
+
cdn_enabled = false
|
65
|
+
end
|
66
|
+
{
|
67
|
+
:cdn_enabled => cdn_enabled,
|
68
|
+
:cdn_ttl => cdn_enabled ? response["x-ttl"].to_i : nil,
|
69
|
+
:cdn_url => cdn_enabled ? response["x-cdn-uri"] : nil,
|
70
|
+
:cdn_ssl_url => cdn_enabled ? response["x-cdn-ssl-uri"] : nil,
|
71
|
+
:cdn_streaming_url => cdn_enabled ? response["x-cdn-streaming-uri"] : nil,
|
72
|
+
:cdn_log => (cdn_enabled and response["x-log-retention"] == "True") ? true : false
|
73
|
+
}
|
74
|
+
)
|
75
|
+
else
|
76
|
+
@cdn_metadata = {}
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Returns the container's metadata as a nicely formatted hash, stripping off the X-Meta-Object- prefix that the system prepends to the
|
81
|
+
# key name.
|
82
|
+
#
|
83
|
+
# object.metadata
|
84
|
+
# => {"ruby"=>"cool", "foo"=>"bar"}
|
85
|
+
def metadata
|
86
|
+
metahash = {}
|
87
|
+
self.container_metadata[:metadata].each{ |key, value| metahash[key.gsub(/x-container-meta-/, '').gsub(/%20/, ' ')] = URI.decode(value).gsub(/\+\-/, ' ') }
|
88
|
+
metahash
|
89
|
+
end
|
90
|
+
|
91
|
+
# Sets the metadata for an object. By passing a hash as an argument, you can set the metadata for an object.
|
92
|
+
# New calls to set metadata are additive. To remove metadata, set the value of the key to nil.
|
93
|
+
#
|
94
|
+
# Throws NoSuchObjectException if the container doesn't exist. Throws InvalidResponseException if the request
|
95
|
+
# fails.
|
96
|
+
def set_metadata(metadatahash)
|
97
|
+
headers = {}
|
98
|
+
metadatahash.each{ |key, value| headers['X-Container-Meta-' + CloudFiles.escape(key.to_s.capitalize)] = value.to_s }
|
99
|
+
begin
|
100
|
+
SwiftClient.post_container(self.connection.storageurl, self.connection.authtoken, escaped_name, headers)
|
101
|
+
self.refresh
|
102
|
+
true
|
103
|
+
rescue ClientException => e
|
104
|
+
raise CloudFiles::Exception::NoSuchObject, "Container #{@name} does not exist" if (e.status.to_s == "404")
|
105
|
+
raise CloudFiles::Exception::InvalidResponse, "Invalid response code #{e.status}" unless (e.status.to_s =~ /^20/)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# Size of the container (in bytes)
|
110
|
+
def bytes
|
111
|
+
self.container_metadata[:bytes]
|
112
|
+
end
|
113
|
+
|
114
|
+
# Number of objects in the container
|
115
|
+
def count
|
116
|
+
self.container_metadata[:count]
|
117
|
+
end
|
118
|
+
|
119
|
+
# Returns true if the container is public and CDN-enabled. Returns false otherwise.
|
120
|
+
#
|
121
|
+
# Aliased as container.public?
|
122
|
+
#
|
123
|
+
# public_container.cdn_enabled?
|
124
|
+
# => true
|
125
|
+
#
|
126
|
+
# private_container.public?
|
127
|
+
# => false
|
128
|
+
def cdn_enabled
|
129
|
+
cdn_available? && self.cdn_metadata[:cdn_enabled]
|
130
|
+
end
|
131
|
+
alias :cdn_enabled? :cdn_enabled
|
132
|
+
alias :public? :cdn_enabled
|
133
|
+
|
134
|
+
# CDN container TTL (if container is public)
|
135
|
+
def cdn_ttl
|
136
|
+
self.cdn_metadata[:cdn_ttl]
|
137
|
+
end
|
138
|
+
|
139
|
+
# CDN container URL (if container is public)
|
140
|
+
def cdn_url
|
141
|
+
self.cdn_metadata[:cdn_url]
|
142
|
+
end
|
143
|
+
|
144
|
+
# CDN SSL container URL (if container is public)
|
145
|
+
def cdn_ssl_url
|
146
|
+
self.cdn_metadata[:cdn_ssl_url]
|
147
|
+
end
|
148
|
+
|
149
|
+
# CDN Streaming container URL (if container is public)
|
150
|
+
def cdn_streaming_url
|
151
|
+
self.cdn_metadata[:cdn_streaming_url]
|
152
|
+
end
|
153
|
+
|
154
|
+
#used by openstack swift
|
155
|
+
def read_acl
|
156
|
+
self.container_metadata[:container_read]
|
157
|
+
end
|
158
|
+
|
159
|
+
#used by openstack swift
|
160
|
+
def write_acl
|
161
|
+
self.container_metadata[:container_write]
|
162
|
+
end
|
163
|
+
|
164
|
+
# Returns true if log retention is enabled on this container, false otherwise
|
165
|
+
def cdn_log
|
166
|
+
self.cdn_metadata[:cdn_log]
|
167
|
+
end
|
168
|
+
alias :log_retention? :cdn_log
|
169
|
+
alias :cdn_log? :cdn_log
|
170
|
+
|
171
|
+
|
172
|
+
# Change the log retention status for this container. Values are true or false.
|
173
|
+
#
|
174
|
+
# These logs will be periodically (at unpredictable intervals) compressed and uploaded
|
175
|
+
# to a ".CDN_ACCESS_LOGS" container in the form of "container_name.YYYYMMDDHH-XXXX.gz".
|
176
|
+
def log_retention=(value)
|
177
|
+
raise Exception::CDNNotAvailable unless cdn_available?
|
178
|
+
begin
|
179
|
+
SwiftClient.post_container(self.connection.cdnurl, self.connection.authtoken, escaped_name, {"x-log-retention" => value.to_s.capitalize})
|
180
|
+
true
|
181
|
+
rescue ClientException => e
|
182
|
+
raise CloudFiles::Exception::InvalidResponse, "Invalid response code #{e.status}" unless (e.status.to_s == "201" or e.status.to_s == "202")
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
|
187
|
+
# Returns the CloudFiles::StorageObject for the named object. Refer to the CloudFiles::StorageObject class for available
|
188
|
+
# methods. If the object exists, it will be returned. If the object does not exist, a NoSuchObjectException will be thrown.
|
189
|
+
#
|
190
|
+
# object = container.object('test.txt')
|
191
|
+
# object.data
|
192
|
+
# => "This is test data"
|
193
|
+
#
|
194
|
+
# object = container.object('newfile.txt')
|
195
|
+
# => NoSuchObjectException: Object newfile.txt does not exist
|
196
|
+
def object(objectname)
|
197
|
+
o = CloudFiles::StorageObject.new(self, objectname, true)
|
198
|
+
return o
|
199
|
+
end
|
200
|
+
alias :get_object :object
|
201
|
+
|
202
|
+
|
203
|
+
# Gathers a list of all available objects in the current container and returns an array of object names.
|
204
|
+
# container = cf.container("My Container")
|
205
|
+
# container.objects #=> [ "cat", "dog", "donkey", "monkeydir", "monkeydir/capuchin"]
|
206
|
+
# Pass a limit argument to limit the list to a number of objects:
|
207
|
+
# container.objects(:limit => 1) #=> [ "cat" ]
|
208
|
+
# Pass an marker with or without a limit to start the list at a certain object:
|
209
|
+
# container.objects(:limit => 1, :marker => 'dog') #=> [ "donkey" ]
|
210
|
+
# Pass a prefix to search for objects that start with a certain string:
|
211
|
+
# container.objects(:prefix => "do") #=> [ "dog", "donkey" ]
|
212
|
+
# Only search within a certain pseudo-filesystem path:
|
213
|
+
# container.objects(:path => 'monkeydir') #=> ["monkeydir/capuchin"]
|
214
|
+
# Only grab "virtual directories", based on a single-character delimiter (no "directory" objects required):
|
215
|
+
# container.objects(:delimiter => '/') #=> ["monkeydir"]
|
216
|
+
# All arguments to this method are optional.
|
217
|
+
#
|
218
|
+
# Returns an empty array if no object exist in the container. Throws an InvalidResponseException
|
219
|
+
# if the request fails.
|
220
|
+
def objects(params = {})
|
221
|
+
params[:marker] ||= params[:offset] unless params[:offset].nil?
|
222
|
+
query = []
|
223
|
+
params.each do |param, value|
|
224
|
+
if [:limit, :marker, :prefix, :path, :delimiter].include? param
|
225
|
+
query << "#{param}=#{CloudFiles.escape(value.to_s)}"
|
226
|
+
end
|
227
|
+
end
|
228
|
+
begin
|
229
|
+
response = SwiftClient.get_container(self.connection.storageurl, self.connection.authtoken, escaped_name, params[:marker], params[:limit], params[:prefix], params[:delimiter])
|
230
|
+
return response[1].collect{|o| o['name']}
|
231
|
+
rescue ClientException => e
|
232
|
+
raise CloudFiles::Exception::InvalidResponse, "Invalid response code #{e.status}" unless (e.status.to_s == "200")
|
233
|
+
end
|
234
|
+
end
|
235
|
+
alias :list_objects :objects
|
236
|
+
|
237
|
+
# Retrieves a list of all objects in the current container along with their size in bytes, hash, and content_type.
|
238
|
+
# If no objects exist, an empty hash is returned. Throws an InvalidResponseException if the request fails. Takes a
|
239
|
+
# parameter hash as an argument, in the same form as the objects method.
|
240
|
+
#
|
241
|
+
# Accepts the same options as objects to limit the returned set.
|
242
|
+
#
|
243
|
+
# Returns a hash in the same format as the containers_detail from the CloudFiles class.
|
244
|
+
#
|
245
|
+
# container.objects_detail
|
246
|
+
# => {"test.txt"=>{:content_type=>"application/octet-stream",
|
247
|
+
# :hash=>"e2a6fcb4771aa3509f6b27b6a97da55b",
|
248
|
+
# :last_modified=>Mon Jan 19 10:43:36 -0600 2009,
|
249
|
+
# :bytes=>"16"},
|
250
|
+
# "new.txt"=>{:content_type=>"application/octet-stream",
|
251
|
+
# :hash=>"0aa820d91aed05d2ef291d324e47bc96",
|
252
|
+
# :last_modified=>Wed Jan 28 10:16:26 -0600 2009,
|
253
|
+
# :bytes=>"22"}
|
254
|
+
# }
|
255
|
+
def objects_detail(params = {})
|
256
|
+
params[:marker] ||= params[:offset] unless params[:offset].nil?
|
257
|
+
query = ["format=xml"]
|
258
|
+
params.each do |param, value|
|
259
|
+
if [:limit, :marker, :prefix, :path, :delimiter].include? param
|
260
|
+
query << "#{param}=#{CloudFiles.escape(value.to_s)}"
|
261
|
+
end
|
262
|
+
end
|
263
|
+
begin
|
264
|
+
response = SwiftClient.get_container(self.connection.storageurl, self.connection.authtoken, escaped_name, params[:marker], params[:limit], params[:prefix], params[:delimiter])
|
265
|
+
return Hash[*response[1].collect{|o| [o['name'],{ :bytes => o["bytes"], :hash => o["hash"], :content_type => o["content_type"], :last_modified => DateTime.parse(o["last_modified"])}] }.flatten]
|
266
|
+
rescue ClientException => e
|
267
|
+
raise CloudFiles::Exception::InvalidResponse, "Invalid response code #{e.status}" unless (e.status.to_s == "200")
|
268
|
+
end
|
269
|
+
end
|
270
|
+
alias :list_objects_info :objects_detail
|
271
|
+
|
272
|
+
# Returns true if a container is empty and returns false otherwise.
|
273
|
+
#
|
274
|
+
# new_container.empty?
|
275
|
+
# => true
|
276
|
+
#
|
277
|
+
# full_container.empty?
|
278
|
+
# => false
|
279
|
+
def empty?
|
280
|
+
return (container_metadata[:count].to_i == 0)? true : false
|
281
|
+
end
|
282
|
+
|
283
|
+
# Returns true if object exists and returns false otherwise.
|
284
|
+
#
|
285
|
+
# container.object_exists?('goodfile.txt')
|
286
|
+
# => true
|
287
|
+
#
|
288
|
+
# container.object_exists?('badfile.txt')
|
289
|
+
# => false
|
290
|
+
def object_exists?(objectname)
|
291
|
+
begin
|
292
|
+
response = SwiftClient.head_object(self.connection.storageurl, self.connection.authtoken, escaped_name, objectname)
|
293
|
+
true
|
294
|
+
rescue ClientException => e
|
295
|
+
false
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
# Creates a new CloudFiles::StorageObject in the current container.
|
300
|
+
#
|
301
|
+
# If an object with the specified name exists in the current container, that object will be returned. Otherwise,
|
302
|
+
# an empty new object will be returned.
|
303
|
+
#
|
304
|
+
# Passing in the optional make_path argument as true will create zero-byte objects to simulate a filesystem path
|
305
|
+
# to the object, if an objectname with path separators ("/path/to/myfile.mp3") is supplied. These path objects can
|
306
|
+
# be used in the Container.objects method.
|
307
|
+
def create_object(objectname, make_path = false)
|
308
|
+
CloudFiles::StorageObject.new(self, objectname, false, make_path)
|
309
|
+
end
|
310
|
+
|
311
|
+
# Removes an CloudFiles::StorageObject from a container. True is returned if the removal is successful. Throws
|
312
|
+
# NoSuchObjectException if the object doesn't exist. Throws InvalidResponseException if the request fails.
|
313
|
+
#
|
314
|
+
# container.delete_object('new.txt')
|
315
|
+
# => true
|
316
|
+
#
|
317
|
+
# container.delete_object('nonexistent_file.txt')
|
318
|
+
# => NoSuchObjectException: Object nonexistent_file.txt does not exist
|
319
|
+
def delete_object(objectname)
|
320
|
+
begin
|
321
|
+
SwiftClient.delete_object(self.connection.storageurl, self.connection.authtoken, escaped_name, objectname)
|
322
|
+
true
|
323
|
+
rescue ClientException => e
|
324
|
+
raise CloudFiles::Exception::NoSuchObject, "Object #{objectname} does not exist" if (e.status.to_s == "404")
|
325
|
+
raise CloudFiles::Exception::InvalidResponse, "Invalid response code #{e.status}" unless (e.status.to_s =~ /^20/)
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
# Makes a container publicly available via the Cloud Files CDN and returns true upon success. Throws NoSuchContainerException
|
330
|
+
# if the container doesn't exist or if the request fails.
|
331
|
+
#
|
332
|
+
# Takes an optional hash of options, including:
|
333
|
+
#
|
334
|
+
# :ttl, which is the CDN cache TTL in seconds (default 86400 seconds or 1 day, minimum 3600 or 1 hour, maximum 259200 or 3 days)
|
335
|
+
#
|
336
|
+
# :user_agent_acl, a Perl-compatible regular expression limiting access to this container to user agents matching the given regular expression
|
337
|
+
#
|
338
|
+
# :referrer_acl, a Perl-compatible regular expression limiting access to this container to HTTP referral URLs matching the given regular expression
|
339
|
+
#
|
340
|
+
# container.make_public(:ttl => 8900, :user_agent_acl => "/Mozilla/", :referrer_acl => "/^http://rackspace.com")
|
341
|
+
# => true
|
342
|
+
def make_public(options = {:ttl => 86400})
|
343
|
+
raise Exception::CDNNotAvailable unless cdn_available?
|
344
|
+
if options.is_a?(Fixnum)
|
345
|
+
print "DEPRECATED: make_public takes a hash of options now, instead of a TTL number"
|
346
|
+
ttl = options
|
347
|
+
options = {:ttl => ttl}
|
348
|
+
end
|
349
|
+
|
350
|
+
begin
|
351
|
+
SwiftClient.put_container(self.connection.cdnurl, self.connection.authtoken, escaped_name)
|
352
|
+
rescue ClientException => e
|
353
|
+
raise CloudFiles::Exception::NoSuchContainer, "Container #{@name} does not exist" unless (e.status.to_s == "201" || e.status.to_s == "202")
|
354
|
+
end
|
355
|
+
headers = { "X-TTL" => options[:ttl].to_s , "X-CDN-Enabled" => "True" }
|
356
|
+
headers["X-User-Agent-ACL"] = options[:user_agent_acl] if options[:user_agent_acl]
|
357
|
+
headers["X-Referrer-ACL"] = options[:referrer_acl] if options[:referrer_acl]
|
358
|
+
|
359
|
+
post_with_headers(headers)
|
360
|
+
# raise CloudFiles::Exception::NoSuchContainer, "Container #{@name} does not exist" unless (response.code == "201" || response.code == "202")
|
361
|
+
refresh
|
362
|
+
true
|
363
|
+
end
|
364
|
+
|
365
|
+
# Only to be used with openstack swift
|
366
|
+
def set_write_acl(write_string)
|
367
|
+
refresh
|
368
|
+
headers = {"X-Container-Write" => write_string}
|
369
|
+
post_with_headers(headers)
|
370
|
+
true
|
371
|
+
end
|
372
|
+
|
373
|
+
# Only to be used with openstack swift
|
374
|
+
def set_read_acl(read_string)
|
375
|
+
refresh
|
376
|
+
headers = {"X-Container-Read" => read_string}
|
377
|
+
post_with_headers(headers)
|
378
|
+
true
|
379
|
+
end
|
380
|
+
|
381
|
+
def post_with_headers(headers = {})
|
382
|
+
begin
|
383
|
+
SwiftClient.post_container(cdn_enabled? ? self.connection.cdnurl : self.connection.storageurl, self.connection.authtoken, escaped_name, headers)
|
384
|
+
rescue ClientException => e
|
385
|
+
raise CloudFiles::Exception::NoSuchContainer, "Container #{@name} does not exist (response code: #{e.status.to_s})" unless (e.status.to_s =~ /^20/)
|
386
|
+
end
|
387
|
+
end
|
388
|
+
|
389
|
+
# Makes a container private and returns true upon success. Throws NoSuchContainerException
|
390
|
+
# if the container doesn't exist or if the request fails.
|
391
|
+
#
|
392
|
+
# Note that if the container was previously public, it will continue to exist out on the CDN until it expires.
|
393
|
+
#
|
394
|
+
# container.make_private
|
395
|
+
# => true
|
396
|
+
def make_private
|
397
|
+
raise Exception::CDNNotAvailable unless cdn_available?
|
398
|
+
headers = { "X-CDN-Enabled" => "False" }
|
399
|
+
begin
|
400
|
+
SwiftClient.post_container(self.connection.cdnurl, self.connection.authtoken, escaped_name, headers)
|
401
|
+
refresh
|
402
|
+
true
|
403
|
+
rescue ClientException => e
|
404
|
+
raise CloudFiles::Exception::NoSuchContainer, "Container #{@name} does not exist" unless (e.status.to_s == "201" || e.status.to_s == "202")
|
405
|
+
end
|
406
|
+
end
|
407
|
+
|
408
|
+
# Purges CDN Edge Cache for all objects inside of this container
|
409
|
+
#
|
410
|
+
# :email, An valid email address or comma seperated
|
411
|
+
# list of emails to be notified once purge is complete .
|
412
|
+
#
|
413
|
+
# container.purge_from_cdn
|
414
|
+
# => true
|
415
|
+
#
|
416
|
+
# or
|
417
|
+
#
|
418
|
+
# container.purge_from_cdn("User@domain.com")
|
419
|
+
# => true
|
420
|
+
#
|
421
|
+
# or
|
422
|
+
#
|
423
|
+
# container.purge_from_cdn("User@domain.com, User2@domain.com")
|
424
|
+
# => true
|
425
|
+
def purge_from_cdn(email=nil)
|
426
|
+
raise Exception::CDNNotAvailable unless cdn_available?
|
427
|
+
headers = {}
|
428
|
+
headers = {"X-Purge-Email" => email} if email
|
429
|
+
begin
|
430
|
+
SwiftClient.delete_container(self.connection.cdnurl, self.connection.authtoken, escaped_name, headers)
|
431
|
+
true
|
432
|
+
rescue ClientException => e
|
433
|
+
raise CloudFiles::Exception::Connection, "Error Unable to Purge Container: #{@name}" unless (e.status.to_s > "200" && e.status.to_s < "299")
|
434
|
+
end
|
435
|
+
end
|
436
|
+
|
437
|
+
def to_s # :nodoc:
|
438
|
+
@name
|
439
|
+
end
|
440
|
+
|
441
|
+
def cdn_available?
|
442
|
+
self.connection.cdn_available?
|
443
|
+
end
|
444
|
+
|
445
|
+
def escaped_name
|
446
|
+
CloudFiles.escape(@name)
|
447
|
+
end
|
448
|
+
|
449
|
+
end
|
450
|
+
|
451
|
+
end
|