cloudfiles 1.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/COPYING +12 -0
- data/Manifest +16 -0
- data/README.rdoc +61 -0
- data/Rakefile +33 -0
- data/TODO +0 -0
- data/VERSION +1 -0
- data/cloudfiles.gemspec +62 -0
- data/lib/cloudfiles.rb +74 -0
- data/lib/cloudfiles/authentication.rb +42 -0
- data/lib/cloudfiles/connection.rb +284 -0
- data/lib/cloudfiles/container.rb +275 -0
- data/lib/cloudfiles/storage_object.rb +257 -0
- data/test/cf-testunit.rb +157 -0
- data/test/cloudfiles_authentication_test.rb +37 -0
- data/test/cloudfiles_connection_test.rb +279 -0
- data/test/cloudfiles_container_test.rb +198 -0
- data/test/cloudfiles_storage_object_test.rb +209 -0
- data/test/test_helper.rb +5 -0
- metadata +79 -0
@@ -0,0 +1,275 @@
|
|
1
|
+
module CloudFiles
|
2
|
+
class Container
|
3
|
+
# See COPYING for license information.
|
4
|
+
# Copyright (c) 2009, Rackspace US, Inc.
|
5
|
+
|
6
|
+
# Name of the container which corresponds to the instantiated container class
|
7
|
+
attr_reader :name
|
8
|
+
|
9
|
+
# Size of the container (in bytes)
|
10
|
+
attr_reader :bytes
|
11
|
+
|
12
|
+
# Number of objects in the container
|
13
|
+
attr_reader :count
|
14
|
+
|
15
|
+
# True if container is public, false if container is private
|
16
|
+
attr_reader :cdn_enabled
|
17
|
+
|
18
|
+
# CDN container TTL (if container is public)
|
19
|
+
attr_reader :cdn_ttl
|
20
|
+
|
21
|
+
# CDN container URL (if container if public)
|
22
|
+
attr_reader :cdn_url
|
23
|
+
|
24
|
+
# The parent CloudFiles::Connection object for this container
|
25
|
+
attr_reader :connection
|
26
|
+
|
27
|
+
# Retrieves an existing CloudFiles::Container object tied to the current CloudFiles::Connection. If the requested
|
28
|
+
# container does not exist, it will raise a NoSuchContainerException.
|
29
|
+
#
|
30
|
+
# Will likely not be called directly, instead use connection.container('container_name') to retrieve the object.
|
31
|
+
def initialize(connection,name)
|
32
|
+
@connection = connection
|
33
|
+
@name = name
|
34
|
+
@storagehost = self.connection.storagehost
|
35
|
+
@storagepath = self.connection.storagepath + "/" + URI.encode(@name).gsub(/&/,'%26')
|
36
|
+
@storageport = self.connection.storageport
|
37
|
+
@storagescheme = self.connection.storagescheme
|
38
|
+
@cdnmgmthost = self.connection.cdnmgmthost
|
39
|
+
@cdnmgmtpath = self.connection.cdnmgmtpath + "/" + URI.encode(@name).gsub(/&/,'%26')
|
40
|
+
@cdnmgmtport = self.connection.cdnmgmtport
|
41
|
+
@cdnmgmtscheme = self.connection.cdnmgmtscheme
|
42
|
+
populate
|
43
|
+
end
|
44
|
+
|
45
|
+
# Retrieves data about the container and populates class variables. It is automatically called
|
46
|
+
# when the Container class is instantiated. If you need to refresh the variables, such as
|
47
|
+
# size, count, cdn_enabled, cdn_ttl, and cdn_url, this method can be called again.
|
48
|
+
#
|
49
|
+
# container.count
|
50
|
+
# => 2
|
51
|
+
# [Upload new file to the container]
|
52
|
+
# container.count
|
53
|
+
# => 2
|
54
|
+
# container.populate
|
55
|
+
# container.count
|
56
|
+
# => 3
|
57
|
+
def populate
|
58
|
+
# Get the size and object count
|
59
|
+
response = self.connection.cfreq("HEAD",@storagehost,@storagepath+"/",@storageport,@storagescheme)
|
60
|
+
raise NoSuchContainerException, "Container #{@name} does not exist" unless (response.code == "204")
|
61
|
+
@bytes = response["x-container-bytes-used"].to_i
|
62
|
+
@count = response["x-container-object-count"].to_i
|
63
|
+
|
64
|
+
# Get the CDN-related details
|
65
|
+
response = self.connection.cfreq("HEAD",@cdnmgmthost,@cdnmgmtpath,@cdnmgmtport,@cdnmgmtscheme)
|
66
|
+
@cdn_enabled = ((response["x-cdn-enabled"] || "").downcase == "true") ? true : false
|
67
|
+
@cdn_ttl = @cdn_enabled ? response["x-ttl"] : false
|
68
|
+
@cdn_url = @cdn_enabled ? response["x-cdn-uri"] : false
|
69
|
+
if @cdn_enabled
|
70
|
+
@cdn_log = response["x-log-retention"] == "False" ? false : true
|
71
|
+
else
|
72
|
+
@cdn_log = false
|
73
|
+
end
|
74
|
+
|
75
|
+
true
|
76
|
+
end
|
77
|
+
alias :refresh :populate
|
78
|
+
|
79
|
+
# Returns true if log retention is enabled on this container, false otherwise
|
80
|
+
def log_retention?
|
81
|
+
@cdn_log
|
82
|
+
end
|
83
|
+
|
84
|
+
# Change the log retention status for this container. Values are true or false.
|
85
|
+
#
|
86
|
+
# These logs will be periodically (at unpredictable intervals) compressed and uploaded
|
87
|
+
# to a “.CDN_ACCESS_LOGS” container in the form of “container_name.YYYYMMDDHH-XXXX.gz”.
|
88
|
+
def log_retention=(value)
|
89
|
+
response = self.connection.cfreq("POST",@cdnmgmthost,@cdnmgmtpath,@cdnmgmtport,@cdnmgmtscheme,{"x-log-retention" => value.to_s.capitalize})
|
90
|
+
raise InvalidResponseException, "Invalid response code #{response.code}" unless (response.code == "201" or response.code == "202")
|
91
|
+
return true
|
92
|
+
end
|
93
|
+
|
94
|
+
|
95
|
+
# Returns the CloudFiles::StorageObject for the named object. Refer to the CloudFiles::StorageObject class for available
|
96
|
+
# methods. If the object exists, it will be returned. If the object does not exist, a NoSuchObjectException will be thrown.
|
97
|
+
#
|
98
|
+
# object = container.object('test.txt')
|
99
|
+
# object.data
|
100
|
+
# => "This is test data"
|
101
|
+
#
|
102
|
+
# object = container.object('newfile.txt')
|
103
|
+
# => NoSuchObjectException: Object newfile.txt does not exist
|
104
|
+
def object(objectname)
|
105
|
+
o = CloudFiles::StorageObject.new(self,objectname,true)
|
106
|
+
return o
|
107
|
+
end
|
108
|
+
alias :get_object :object
|
109
|
+
|
110
|
+
|
111
|
+
# Gathers a list of all available objects in the current container and returns an array of object names.
|
112
|
+
# container = cf.container("My Container")
|
113
|
+
# container.objects #=> [ "dog", "cat", "donkey", "monkeydir/capuchin"]
|
114
|
+
# Pass a limit argument to limit the list to a number of objects:
|
115
|
+
# container.objects(:limit => 1) #=> [ "dog" ]
|
116
|
+
# Pass an offset with or without a limit to start the list at a certain object:
|
117
|
+
# container.objects(:limit => 1, :offset => 2) #=> [ "donkey" ]
|
118
|
+
# Pass a prefix to search for objects that start with a certain string:
|
119
|
+
# container.objects(:prefix => "do") #=> [ "dog", "donkey" ]
|
120
|
+
# Only search within a certain pseudo-filesystem path:
|
121
|
+
# container.objects(:path => 'monkeydir') #=> ["monkeydir/capuchin"]
|
122
|
+
# All arguments to this method are optional.
|
123
|
+
#
|
124
|
+
# Returns an empty array if no object exist in the container. Throws an InvalidResponseException
|
125
|
+
# if the request fails.
|
126
|
+
def objects(params = {})
|
127
|
+
paramarr = []
|
128
|
+
paramarr << ["limit=#{URI.encode(params[:limit].to_i).gsub(/&/,'%26')}"] if params[:limit]
|
129
|
+
paramarr << ["offset=#{URI.encode(params[:offset].to_i).gsub(/&/,'%26')}"] if params[:offset]
|
130
|
+
paramarr << ["prefix=#{URI.encode(params[:prefix]).gsub(/&/,'%26')}"] if params[:prefix]
|
131
|
+
paramarr << ["path=#{URI.encode(params[:path]).gsub(/&/,'%26')}"] if params[:path]
|
132
|
+
paramstr = (paramarr.size > 0)? paramarr.join("&") : "" ;
|
133
|
+
response = self.connection.cfreq("GET",@storagehost,"#{@storagepath}?#{paramstr}",@storageport,@storagescheme)
|
134
|
+
return [] if (response.code == "204")
|
135
|
+
raise InvalidResponseException, "Invalid response code #{response.code}" unless (response.code == "200")
|
136
|
+
return CloudFiles.lines(response.body)
|
137
|
+
end
|
138
|
+
alias :list_objects :objects
|
139
|
+
|
140
|
+
# Retrieves a list of all objects in the current container along with their size in bytes, hash, and content_type.
|
141
|
+
# If no objects exist, an empty hash is returned. Throws an InvalidResponseException if the request fails. Takes a
|
142
|
+
# parameter hash as an argument, in the same form as the objects method.
|
143
|
+
#
|
144
|
+
# Returns a hash in the same format as the containers_detail from the CloudFiles class.
|
145
|
+
#
|
146
|
+
# container.objects_detail
|
147
|
+
# => {"test.txt"=>{:content_type=>"application/octet-stream",
|
148
|
+
# :hash=>"e2a6fcb4771aa3509f6b27b6a97da55b",
|
149
|
+
# :last_modified=>Mon Jan 19 10:43:36 -0600 2009,
|
150
|
+
# :bytes=>"16"},
|
151
|
+
# "new.txt"=>{:content_type=>"application/octet-stream",
|
152
|
+
# :hash=>"0aa820d91aed05d2ef291d324e47bc96",
|
153
|
+
# :last_modified=>Wed Jan 28 10:16:26 -0600 2009,
|
154
|
+
# :bytes=>"22"}
|
155
|
+
# }
|
156
|
+
def objects_detail(params = {})
|
157
|
+
paramarr = []
|
158
|
+
paramarr << ["format=xml"]
|
159
|
+
paramarr << ["limit=#{URI.encode(params[:limit].to_i).gsub(/&/,'%26')}"] if params[:limit]
|
160
|
+
paramarr << ["offset=#{URI.encode(params[:offset].to_i).gsub(/&/,'%26')}"] if params[:offset]
|
161
|
+
paramarr << ["prefix=#{URI.encode(params[:prefix]).gsub(/&/,'%26')}"] if params[:prefix]
|
162
|
+
paramarr << ["path=#{URI.encode(params[:path]).gsub(/&/,'%26')}"] if params[:path]
|
163
|
+
paramstr = (paramarr.size > 0)? paramarr.join("&") : "" ;
|
164
|
+
response = self.connection.cfreq("GET",@storagehost,"#{@storagepath}?#{paramstr}",@storageport,@storagescheme)
|
165
|
+
return {} if (response.code == "204")
|
166
|
+
raise InvalidResponseException, "Invalid response code #{response.code}" unless (response.code == "200")
|
167
|
+
doc = REXML::Document.new(response.body)
|
168
|
+
detailhash = {}
|
169
|
+
doc.elements.each("container/object") { |o|
|
170
|
+
detailhash[o.elements["name"].text] = { :bytes => o.elements["bytes"].text, :hash => o.elements["hash"].text, :content_type => o.elements["content_type"].text, :last_modified => Time.parse(o.elements["last_modified"].text) }
|
171
|
+
}
|
172
|
+
doc = nil
|
173
|
+
return detailhash
|
174
|
+
end
|
175
|
+
alias :list_objects_info :objects_detail
|
176
|
+
|
177
|
+
# Returns true if the container is public and CDN-enabled. Returns false otherwise.
|
178
|
+
#
|
179
|
+
# public_container.public?
|
180
|
+
# => true
|
181
|
+
#
|
182
|
+
# private_container.public?
|
183
|
+
# => false
|
184
|
+
def public?
|
185
|
+
return @cdn_enabled
|
186
|
+
end
|
187
|
+
|
188
|
+
# Returns true if a container is empty and returns false otherwise.
|
189
|
+
#
|
190
|
+
# new_container.empty?
|
191
|
+
# => true
|
192
|
+
#
|
193
|
+
# full_container.empty?
|
194
|
+
# => false
|
195
|
+
def empty?
|
196
|
+
return (@count.to_i == 0)? true : false
|
197
|
+
end
|
198
|
+
|
199
|
+
# Returns true if object exists and returns false otherwise.
|
200
|
+
#
|
201
|
+
# container.object_exists?('goodfile.txt')
|
202
|
+
# => true
|
203
|
+
#
|
204
|
+
# container.object_exists?('badfile.txt')
|
205
|
+
# => false
|
206
|
+
def object_exists?(objectname)
|
207
|
+
response = self.connection.cfreq("HEAD",@storagehost,"#{@storagepath}/#{URI.encode(objectname).gsub(/&/,'%26')}",@storageport,@storagescheme)
|
208
|
+
return (response.code == "204")? true : false
|
209
|
+
end
|
210
|
+
|
211
|
+
# Creates a new CloudFiles::StorageObject in the current container.
|
212
|
+
#
|
213
|
+
# If an object with the specified name exists in the current container, that object will be returned. Otherwise,
|
214
|
+
# an empty new object will be returned.
|
215
|
+
#
|
216
|
+
# Passing in the optional make_path argument as true will create zero-byte objects to simulate a filesystem path
|
217
|
+
# to the object, if an objectname with path separators ("/path/to/myfile.mp3") is supplied. These path objects can
|
218
|
+
# be used in the Container.objects method.
|
219
|
+
def create_object(objectname,make_path = false)
|
220
|
+
CloudFiles::StorageObject.new(self,objectname,false,make_path)
|
221
|
+
end
|
222
|
+
|
223
|
+
# Removes an CloudFiles::StorageObject from a container. True is returned if the removal is successful. Throws
|
224
|
+
# NoSuchObjectException if the object doesn't exist. Throws InvalidResponseException if the request fails.
|
225
|
+
#
|
226
|
+
# container.delete_object('new.txt')
|
227
|
+
# => true
|
228
|
+
#
|
229
|
+
# container.delete_object('nonexistent_file.txt')
|
230
|
+
# => NoSuchObjectException: Object nonexistent_file.txt does not exist
|
231
|
+
def delete_object(objectname)
|
232
|
+
response = self.connection.cfreq("DELETE",@storagehost,"#{@storagepath}/#{URI.encode(objectname).gsub(/&/,'%26')}",@storageport,@storagescheme)
|
233
|
+
raise NoSuchObjectException, "Object #{objectname} does not exist" if (response.code == "404")
|
234
|
+
raise InvalidResponseException, "Invalid response code #{response.code}" unless (response.code == "204")
|
235
|
+
true
|
236
|
+
end
|
237
|
+
|
238
|
+
# Makes a container publicly available via the Cloud Files CDN and returns true upon success. Throws NoSuchContainerException
|
239
|
+
# if the container doesn't exist or if the request fails.
|
240
|
+
#
|
241
|
+
# Takes an optional argument, which is the CDN cache TTL in seconds (default 86400 seconds or 1 day, maximum 259200 or 3 days)
|
242
|
+
#
|
243
|
+
# container.make_public(432000)
|
244
|
+
# => true
|
245
|
+
def make_public(ttl = 86400)
|
246
|
+
response = self.connection.cfreq("PUT",@cdnmgmthost,@cdnmgmtpath,@cdnmgmtport,@cdnmgmtscheme)
|
247
|
+
raise NoSuchContainerException, "Container #{@name} does not exist" unless (response.code == "201" || response.code == "202")
|
248
|
+
|
249
|
+
headers = { "X-TTL" => ttl.to_s }
|
250
|
+
response = self.connection.cfreq("POST",@cdnmgmthost,@cdnmgmtpath,@cdnmgmtport,@cdnmgmtscheme,headers)
|
251
|
+
raise NoSuchContainerException, "Container #{@name} does not exist" unless (response.code == "201" || response.code == "202")
|
252
|
+
true
|
253
|
+
end
|
254
|
+
|
255
|
+
# Makes a container private and returns true upon success. Throws NoSuchContainerException
|
256
|
+
# if the container doesn't exist or if the request fails.
|
257
|
+
#
|
258
|
+
# Note that if the container was previously public, it will continue to exist out on the CDN until it expires.
|
259
|
+
#
|
260
|
+
# container.make_private
|
261
|
+
# => true
|
262
|
+
def make_private
|
263
|
+
headers = { "X-CDN-Enabled" => "False" }
|
264
|
+
response = self.connection.cfreq("POST",@cdnmgmthost,@cdnmgmtpath,@cdnmgmtport,@cdnmgmtscheme,headers)
|
265
|
+
raise NoSuchContainerException, "Container #{@name} does not exist" unless (response.code == "201" || response.code == "202")
|
266
|
+
true
|
267
|
+
end
|
268
|
+
|
269
|
+
def to_s # :nodoc:
|
270
|
+
@name
|
271
|
+
end
|
272
|
+
|
273
|
+
end
|
274
|
+
|
275
|
+
end
|
@@ -0,0 +1,257 @@
|
|
1
|
+
module CloudFiles
|
2
|
+
class StorageObject
|
3
|
+
# See COPYING for license information.
|
4
|
+
# Copyright (c) 2009, Rackspace US, Inc.
|
5
|
+
|
6
|
+
# Name of the object corresponding to the instantiated object
|
7
|
+
attr_reader :name
|
8
|
+
|
9
|
+
# Size of the object (in bytes)
|
10
|
+
attr_reader :bytes
|
11
|
+
|
12
|
+
# The parent CloudFiles::Container object
|
13
|
+
attr_reader :container
|
14
|
+
|
15
|
+
# Date of the object's last modification
|
16
|
+
attr_reader :last_modified
|
17
|
+
|
18
|
+
# ETag of the object data
|
19
|
+
attr_reader :etag
|
20
|
+
|
21
|
+
# Content type of the object data
|
22
|
+
attr_reader :content_type
|
23
|
+
|
24
|
+
# Builds a new CloudFiles::StorageObject in the current container. If force_exist is set, the object must exist or a
|
25
|
+
# NoSuchObjectException will be raised. If not, an "empty" CloudFiles::StorageObject will be returned, ready for data
|
26
|
+
# via CloudFiles::StorageObject.write
|
27
|
+
def initialize(container,objectname,force_exists=false,make_path=false)
|
28
|
+
if objectname.match(/\?/)
|
29
|
+
raise SyntaxException, "Object #{objectname} contains an invalid character in the name (? not allowed)"
|
30
|
+
end
|
31
|
+
@container = container
|
32
|
+
@containername = container.name
|
33
|
+
@name = objectname
|
34
|
+
@make_path = make_path
|
35
|
+
@storagehost = self.container.connection.storagehost
|
36
|
+
@storagepath = self.container.connection.storagepath+"/#{URI.encode(@containername).gsub(/&/,'%26')}/#{URI.encode(@name).gsub(/&/,'%26')}"
|
37
|
+
@storageport = self.container.connection.storageport
|
38
|
+
@storagescheme = self.container.connection.storagescheme
|
39
|
+
if container.object_exists?(objectname)
|
40
|
+
populate
|
41
|
+
else
|
42
|
+
raise NoSuchObjectException, "Object #{@name} does not exist" if force_exists
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Caches data about the CloudFiles::StorageObject for fast retrieval. This method is automatically called when the
|
47
|
+
# class is initialized, but it can be called again if the data needs to be updated.
|
48
|
+
def populate
|
49
|
+
response = self.container.connection.cfreq("HEAD",@storagehost,@storagepath,@storageport,@storagescheme)
|
50
|
+
raise NoSuchObjectException, "Object #{@name} does not exist" if (response.code != "204")
|
51
|
+
@bytes = response["content-length"]
|
52
|
+
@last_modified = Time.parse(response["last-modified"])
|
53
|
+
@etag = response["etag"]
|
54
|
+
@content_type = response["content-type"]
|
55
|
+
resphash = {}
|
56
|
+
response.to_hash.select { |k,v| k.match(/^x-object-meta/) }.each { |x| resphash[x[0]] = x[1].to_s }
|
57
|
+
@metadata = resphash
|
58
|
+
true
|
59
|
+
end
|
60
|
+
alias :refresh :populate
|
61
|
+
|
62
|
+
# Retrieves the data from an object and stores the data in memory. The data is returned as a string.
|
63
|
+
# Throws a NoSuchObjectException if the object doesn't exist.
|
64
|
+
#
|
65
|
+
# If the optional size and range arguments are provided, the call will return the number of bytes provided by
|
66
|
+
# size, starting from the offset provided in offset.
|
67
|
+
#
|
68
|
+
# object.data
|
69
|
+
# => "This is the text stored in the file"
|
70
|
+
def data(size=-1,offset=0,headers = {})
|
71
|
+
if size.to_i > 0
|
72
|
+
range = sprintf("bytes=%d-%d", offset.to_i, (offset.to_i + size.to_i) - 1)
|
73
|
+
headers['Range'] = range
|
74
|
+
end
|
75
|
+
response = self.container.connection.cfreq("GET",@storagehost,@storagepath,@storageport,@storagescheme,headers)
|
76
|
+
raise NoSuchObjectException, "Object #{@name} does not exist" unless (response.code =~ /^20/)
|
77
|
+
response.body.chomp
|
78
|
+
end
|
79
|
+
|
80
|
+
# Retrieves the data from an object and returns a stream that must be passed to a block. Throws a
|
81
|
+
# NoSuchObjectException if the object doesn't exist.
|
82
|
+
#
|
83
|
+
# If the optional size and range arguments are provided, the call will return the number of bytes provided by
|
84
|
+
# size, starting from the offset provided in offset.
|
85
|
+
#
|
86
|
+
# data = ""
|
87
|
+
# object.data_stream do |chunk|
|
88
|
+
# data += chunk
|
89
|
+
# end
|
90
|
+
#
|
91
|
+
# data
|
92
|
+
# => "This is the text stored in the file"
|
93
|
+
def data_stream(size=-1,offset=0,headers = {},&block)
|
94
|
+
if size.to_i > 0
|
95
|
+
range = sprintf("bytes=%d-%d", offset.to_i, (offset.to_i + size.to_i) - 1)
|
96
|
+
headers['Range'] = range
|
97
|
+
end
|
98
|
+
self.container.connection.cfreq("GET",@storagehost,@storagepath,@storageport,@storagescheme,headers,nil) do |response|
|
99
|
+
raise NoSuchObjectException, "Object #{@name} does not exist" unless (response.code == "200")
|
100
|
+
response.read_body(&block)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Returns the object's metadata as a nicely formatted hash, stripping off the X-Meta-Object- prefix that the system prepends to the
|
105
|
+
# key name.
|
106
|
+
#
|
107
|
+
# object.metadata
|
108
|
+
# => {"ruby"=>"cool", "foo"=>"bar"}
|
109
|
+
def metadata
|
110
|
+
metahash = {}
|
111
|
+
@metadata.each{|key, value| metahash[key.gsub(/x-object-meta-/,'').gsub(/\+\-/, ' ')] = URI.decode(value).gsub(/\+\-/, ' ')}
|
112
|
+
metahash
|
113
|
+
end
|
114
|
+
|
115
|
+
# Sets the metadata for an object. By passing a hash as an argument, you can set the metadata for an object.
|
116
|
+
# However, setting metadata will overwrite any existing metadata for the object.
|
117
|
+
#
|
118
|
+
# Throws NoSuchObjectException if the object doesn't exist. Throws InvalidResponseException if the request
|
119
|
+
# fails.
|
120
|
+
def set_metadata(metadatahash)
|
121
|
+
headers = {}
|
122
|
+
metadatahash.each{|key, value| headers['X-Object-Meta-' + key.to_s.capitalize] = value.to_s}
|
123
|
+
response = self.container.connection.cfreq("POST",@storagehost,@storagepath,@storageport,@storagescheme,headers)
|
124
|
+
raise NoSuchObjectException, "Object #{@name} does not exist" if (response.code == "404")
|
125
|
+
raise InvalidResponseException, "Invalid response code #{response.code}" unless (response.code == "202")
|
126
|
+
true
|
127
|
+
end
|
128
|
+
|
129
|
+
# Takes supplied data and writes it to the object, saving it. You can supply an optional hash of headers, including
|
130
|
+
# Content-Type and ETag, that will be applied to the object.
|
131
|
+
#
|
132
|
+
# If you would rather stream the data in chunks, instead of reading it all into memory at once, you can pass an
|
133
|
+
# IO object for the data, such as: object.write(open('/path/to/file.mp3'))
|
134
|
+
#
|
135
|
+
# You can compute your own MD5 sum and send it in the "ETag" header. If you provide yours, it will be compared to
|
136
|
+
# the MD5 sum on the server side. If they do not match, the server will return a 422 status code and a MisMatchedChecksumException
|
137
|
+
# will be raised. If you do not provide an MD5 sum as the ETag, one will be computed on the server side.
|
138
|
+
#
|
139
|
+
# Updates the container cache and returns true on success, raises exceptions if stuff breaks.
|
140
|
+
#
|
141
|
+
# object = container.create_object("newfile.txt")
|
142
|
+
#
|
143
|
+
# object.write("This is new data")
|
144
|
+
# => true
|
145
|
+
#
|
146
|
+
# object.data
|
147
|
+
# => "This is new data"
|
148
|
+
#
|
149
|
+
# If you are passing your data in via STDIN, just do an
|
150
|
+
#
|
151
|
+
# object.write
|
152
|
+
#
|
153
|
+
# with no data (or, if you need to pass headers)
|
154
|
+
#
|
155
|
+
# object.write(nil,{'header' => 'value})
|
156
|
+
|
157
|
+
def write(data=nil,headers={})
|
158
|
+
raise SyntaxException, "No data or header updates supplied" if ((data.nil? && $stdin.tty?) and headers.empty?)
|
159
|
+
if headers['Content-Type'].nil?
|
160
|
+
type = MIME::Types.type_for(self.name).first.to_s
|
161
|
+
if type.empty?
|
162
|
+
headers['Content-Type'] = "application/octet-stream"
|
163
|
+
else
|
164
|
+
headers['Content-Type'] = type
|
165
|
+
end
|
166
|
+
end
|
167
|
+
# If we're taking data from standard input, send that IO object to cfreq
|
168
|
+
data = $stdin if (data.nil? && $stdin.tty? == false)
|
169
|
+
response = self.container.connection.cfreq("PUT",@storagehost,"#{@storagepath}",@storageport,@storagescheme,headers,data)
|
170
|
+
code = response.code
|
171
|
+
raise InvalidResponseException, "Invalid content-length header sent" if (code == "412")
|
172
|
+
raise MisMatchedChecksumException, "Mismatched etag" if (code == "422")
|
173
|
+
raise InvalidResponseException, "Invalid response code #{code}" unless (code == "201")
|
174
|
+
make_path(File.dirname(self.name)) if @make_path == true
|
175
|
+
self.populate
|
176
|
+
true
|
177
|
+
end
|
178
|
+
|
179
|
+
# A convenience method to stream data into an object from a local file (or anything that can be loaded by Ruby's open method)
|
180
|
+
#
|
181
|
+
# Throws an Errno::ENOENT if the file cannot be read.
|
182
|
+
#
|
183
|
+
# object.data
|
184
|
+
# => "This is my data"
|
185
|
+
#
|
186
|
+
# object.load_from_filename("/tmp/file.txt")
|
187
|
+
# => true
|
188
|
+
#
|
189
|
+
# object.data
|
190
|
+
# => "This data was in the file /tmp/file.txt"
|
191
|
+
#
|
192
|
+
# object.load_from_filename("/tmp/nonexistent.txt")
|
193
|
+
# => Errno::ENOENT: No such file or directory - /tmp/nonexistent.txt
|
194
|
+
def load_from_filename(filename)
|
195
|
+
f = open(filename)
|
196
|
+
self.write(f)
|
197
|
+
f.close
|
198
|
+
true
|
199
|
+
end
|
200
|
+
|
201
|
+
# A convenience method to stream data from an object into a local file
|
202
|
+
#
|
203
|
+
# Throws an Errno::ENOENT if the file cannot be opened for writing due to a path error,
|
204
|
+
# and Errno::EACCES if the file cannot be opened for writing due to permissions.
|
205
|
+
#
|
206
|
+
# object.data
|
207
|
+
# => "This is my data"
|
208
|
+
#
|
209
|
+
# object.save_to_filename("/tmp/file.txt")
|
210
|
+
# => true
|
211
|
+
#
|
212
|
+
# $ cat /tmp/file.txt
|
213
|
+
# "This is my data"
|
214
|
+
#
|
215
|
+
# object.save_to_filename("/tmp/owned_by_root.txt")
|
216
|
+
# => Errno::EACCES: Permission denied - /tmp/owned_by_root.txt
|
217
|
+
def save_to_filename(filename)
|
218
|
+
File.open(filename, 'w+') do |f|
|
219
|
+
self.data_stream do |chunk|
|
220
|
+
f.write chunk
|
221
|
+
end
|
222
|
+
end
|
223
|
+
true
|
224
|
+
end
|
225
|
+
|
226
|
+
# If the parent container is public (CDN-enabled), returns the CDN URL to this object. Otherwise, return nil
|
227
|
+
#
|
228
|
+
# public_object.public_url
|
229
|
+
# => "http://c0001234.cdn.cloudfiles.rackspacecloud.com/myfile.jpg"
|
230
|
+
#
|
231
|
+
# private_object.public_url
|
232
|
+
# => nil
|
233
|
+
def public_url
|
234
|
+
self.container.public? ? self.container.cdn_url + "/#{URI.encode(@name).gsub(/&/,'%26')}" : nil
|
235
|
+
end
|
236
|
+
|
237
|
+
def to_s # :nodoc:
|
238
|
+
@name
|
239
|
+
end
|
240
|
+
|
241
|
+
private
|
242
|
+
|
243
|
+
def make_path(path) # :nodoc:
|
244
|
+
if path == "." || path == "/"
|
245
|
+
return
|
246
|
+
else
|
247
|
+
unless self.container.object_exists?(path)
|
248
|
+
o = self.container.create_object(path)
|
249
|
+
o.write(nil,{'Content-Type' => 'application/directory'})
|
250
|
+
end
|
251
|
+
make_path(File.dirname(path))
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
end
|
256
|
+
|
257
|
+
end
|