rackspace-cloudfiles 1.3.0.2

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