cloudfiles 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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