harbor 0.16.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/Rakefile +76 -0
- data/bin/harbor +7 -0
- data/lib/harbor.rb +17 -0
- data/lib/harbor/accessor_injector.rb +30 -0
- data/lib/harbor/application.rb +172 -0
- data/lib/harbor/auth/basic.rb +51 -0
- data/lib/harbor/block_io.rb +63 -0
- data/lib/harbor/cache.rb +90 -0
- data/lib/harbor/cache/disk.rb +99 -0
- data/lib/harbor/cache/item.rb +48 -0
- data/lib/harbor/cache/memory.rb +35 -0
- data/lib/harbor/cascade.rb +75 -0
- data/lib/harbor/console.rb +34 -0
- data/lib/harbor/container.rb +134 -0
- data/lib/harbor/contrib/debug.rb +236 -0
- data/lib/harbor/contrib/session/data_mapper.rb +74 -0
- data/lib/harbor/daemon.rb +105 -0
- data/lib/harbor/errors.rb +49 -0
- data/lib/harbor/events.rb +45 -0
- data/lib/harbor/exception_notifier.rb +59 -0
- data/lib/harbor/file.rb +66 -0
- data/lib/harbor/file_store.rb +69 -0
- data/lib/harbor/file_store/file.rb +100 -0
- data/lib/harbor/file_store/local.rb +71 -0
- data/lib/harbor/file_store/mosso.rb +154 -0
- data/lib/harbor/file_store/mosso/private.rb +8 -0
- data/lib/harbor/generator.rb +56 -0
- data/lib/harbor/generator/help.rb +34 -0
- data/lib/harbor/generator/setup.rb +82 -0
- data/lib/harbor/generator/skeletons/basic/config.ru.skel +21 -0
- data/lib/harbor/generator/skeletons/basic/lib/appname.rb.skel +49 -0
- data/lib/harbor/generator/skeletons/basic/lib/appname/controllers/home.rb +9 -0
- data/lib/harbor/generator/skeletons/basic/lib/appname/views/home/index.html.erb.skel +23 -0
- data/lib/harbor/generator/skeletons/basic/lib/appname/views/layouts/application.html.erb.skel +48 -0
- data/lib/harbor/generator/skeletons/basic/lib/appname/views/layouts/exception.html.erb.skel +13 -0
- data/lib/harbor/generator/skeletons/basic/lib/appname/views/layouts/login.html.erb.skel +11 -0
- data/lib/harbor/generator/skeletons/basic/log/development.log +0 -0
- data/lib/harbor/hooks.rb +105 -0
- data/lib/harbor/json_cookies.rb +37 -0
- data/lib/harbor/layouts.rb +61 -0
- data/lib/harbor/locale.rb +50 -0
- data/lib/harbor/locales.txt +22 -0
- data/lib/harbor/logging.rb +39 -0
- data/lib/harbor/logging/appenders/email.rb +84 -0
- data/lib/harbor/logging/request_logger.rb +34 -0
- data/lib/harbor/mail_servers/abstract.rb +12 -0
- data/lib/harbor/mail_servers/sendmail.rb +19 -0
- data/lib/harbor/mail_servers/smtp.rb +25 -0
- data/lib/harbor/mail_servers/test.rb +17 -0
- data/lib/harbor/mailer.rb +96 -0
- data/lib/harbor/messages.rb +17 -0
- data/lib/harbor/mime.rb +206 -0
- data/lib/harbor/plugin.rb +52 -0
- data/lib/harbor/plugin_list.rb +38 -0
- data/lib/harbor/request.rb +138 -0
- data/lib/harbor/response.rb +281 -0
- data/lib/harbor/router.rb +112 -0
- data/lib/harbor/script.rb +155 -0
- data/lib/harbor/session.rb +75 -0
- data/lib/harbor/session/abstract.rb +27 -0
- data/lib/harbor/session/cookie.rb +17 -0
- data/lib/harbor/shellwords.rb +26 -0
- data/lib/harbor/test/mailer.rb +10 -0
- data/lib/harbor/test/request.rb +28 -0
- data/lib/harbor/test/response.rb +17 -0
- data/lib/harbor/test/session.rb +11 -0
- data/lib/harbor/test/test.rb +22 -0
- data/lib/harbor/version.rb +3 -0
- data/lib/harbor/view.rb +89 -0
- data/lib/harbor/view_context.rb +134 -0
- data/lib/harbor/view_context/helpers.rb +7 -0
- data/lib/harbor/view_context/helpers/cache.rb +77 -0
- data/lib/harbor/view_context/helpers/form.rb +34 -0
- data/lib/harbor/view_context/helpers/html.rb +26 -0
- data/lib/harbor/view_context/helpers/text.rb +120 -0
- data/lib/harbor/view_context/helpers/url.rb +11 -0
- data/lib/harbor/xml_view.rb +57 -0
- data/lib/harbor/zipped_io.rb +203 -0
- data/lib/vendor/cloudfiles-1.3.0/cloudfiles.rb +77 -0
- data/lib/vendor/cloudfiles-1.3.0/cloudfiles/authentication.rb +46 -0
- data/lib/vendor/cloudfiles-1.3.0/cloudfiles/connection.rb +280 -0
- data/lib/vendor/cloudfiles-1.3.0/cloudfiles/container.rb +260 -0
- data/lib/vendor/cloudfiles-1.3.0/cloudfiles/storage_object.rb +253 -0
- metadata +155 -0
@@ -0,0 +1,260 @@
|
|
1
|
+
# Copyright (C) 2008 Rackspace US, Inc.
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
4
|
+
#
|
5
|
+
# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
6
|
+
#
|
7
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
8
|
+
#
|
9
|
+
# Except as contained in this notice, the name of Rackspace US, Inc. shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Software without prior written authorization from Rackspace US, Inc.
|
10
|
+
|
11
|
+
module CloudFiles
|
12
|
+
class Container
|
13
|
+
|
14
|
+
# Name of the container which corresponds to the instantiated container class
|
15
|
+
attr_reader :name
|
16
|
+
|
17
|
+
# Size of the container (in bytes)
|
18
|
+
attr_reader :bytes
|
19
|
+
|
20
|
+
# Number of objects in the container
|
21
|
+
attr_reader :count
|
22
|
+
|
23
|
+
# True if container is public, false if container is private
|
24
|
+
attr_reader :cdn_enabled
|
25
|
+
|
26
|
+
# CDN container TTL (if container is public)
|
27
|
+
attr_reader :cdn_ttl
|
28
|
+
|
29
|
+
# CDN container URL (if container if public)
|
30
|
+
attr_reader :cdn_url
|
31
|
+
|
32
|
+
# The parent CloudFiles::Connection object for this container
|
33
|
+
attr_reader :connection
|
34
|
+
|
35
|
+
# Retrieves an existing CloudFiles::Container object tied to the current CloudFiles::Connection. If the requested
|
36
|
+
# container does not exist, it will raise a NoSuchContainerException.
|
37
|
+
#
|
38
|
+
# Will likely not be called directly, instead use connection.container('container_name') to retrieve the object.
|
39
|
+
def initialize(connection,name)
|
40
|
+
@connection = connection
|
41
|
+
@name = name
|
42
|
+
@storagehost = self.connection.storagehost
|
43
|
+
@storagepath = self.connection.storagepath+"/"+@name
|
44
|
+
@cdnmgmthost = self.connection.cdnmgmthost
|
45
|
+
@cdnmgmtpath = self.connection.cdnmgmtpath+"/"+@name
|
46
|
+
populate
|
47
|
+
end
|
48
|
+
|
49
|
+
# Retrieves data about the container and populates class variables. It is automatically called
|
50
|
+
# when the Container class is instantiated. If you need to refresh the variables, such as
|
51
|
+
# size, count, cdn_enabled, cdn_ttl, and cdn_url, this method can be called again.
|
52
|
+
#
|
53
|
+
# container.count
|
54
|
+
# => 2
|
55
|
+
# [Upload new file to the container]
|
56
|
+
# container.count
|
57
|
+
# => 2
|
58
|
+
# container.populate
|
59
|
+
# container.count
|
60
|
+
# => 3
|
61
|
+
def populate
|
62
|
+
# Get the size and object count
|
63
|
+
response = self.connection.cfreq("HEAD",@storagehost,@storagepath+"/")
|
64
|
+
raise NoSuchContainerException, "Container #{@name} does not exist" unless (response.code == "204")
|
65
|
+
@bytes = response["x-container-bytes-used"].to_i
|
66
|
+
@count = response["x-container-object-count"].to_i
|
67
|
+
|
68
|
+
# Get the CDN-related details
|
69
|
+
response = self.connection.cfreq("HEAD",@cdnmgmthost,@cdnmgmtpath)
|
70
|
+
if (response.code == "204")
|
71
|
+
@cdn_enabled = true
|
72
|
+
@cdn_ttl = response["x-ttl"]
|
73
|
+
@cdn_url = response["x-cdn-uri"]
|
74
|
+
else
|
75
|
+
@cdn_enabled = false
|
76
|
+
@cdn_ttl = false
|
77
|
+
@cdn_url = false
|
78
|
+
end
|
79
|
+
true
|
80
|
+
end
|
81
|
+
alias :refresh :populate
|
82
|
+
|
83
|
+
# Returns the CloudFiles::StorageObject for the named object. Refer to the CloudFiles::StorageObject class for available
|
84
|
+
# methods. If the object exists, it will be returned. If the object does not exist, a NoSuchObjectException will be thrown.
|
85
|
+
#
|
86
|
+
# object = container.object('test.txt')
|
87
|
+
# object.data
|
88
|
+
# => "This is test data"
|
89
|
+
#
|
90
|
+
# object = container.object('newfile.txt')
|
91
|
+
# => NoSuchObjectException: Object newfile.txt does not exist
|
92
|
+
def object(objectname)
|
93
|
+
o = CloudFiles::StorageObject.new(self,objectname,true)
|
94
|
+
return o
|
95
|
+
end
|
96
|
+
alias :get_object :object
|
97
|
+
|
98
|
+
|
99
|
+
# Gathers a list of all available objects in the current container and returns an array of object names.
|
100
|
+
# container = cf.container("My Container")
|
101
|
+
# container.objects #=> [ "dog", "cat", "donkey", "monkeydir/capuchin"]
|
102
|
+
# Pass a limit argument to limit the list to a number of objects:
|
103
|
+
# container.objects(:limit => 1) #=> [ "dog" ]
|
104
|
+
# Pass an offset with or without a limit to start the list at a certain object:
|
105
|
+
# container.objects(:limit => 1, :offset => 2) #=> [ "donkey" ]
|
106
|
+
# Pass a prefix to search for objects that start with a certain string:
|
107
|
+
# container.objects(:prefix => "do") #=> [ "dog", "donkey" ]
|
108
|
+
# Only search within a certain pseudo-filesystem path:
|
109
|
+
# container.objects(:path => 'monkeydir') #=> ["monkeydir/capuchin"]
|
110
|
+
# All arguments to this method are optional.
|
111
|
+
#
|
112
|
+
# Returns an empty array if no object exist in the container. Throws an InvalidResponseException
|
113
|
+
# if the request fails.
|
114
|
+
def objects(params = {})
|
115
|
+
paramarr = []
|
116
|
+
paramarr << ["limit=#{URI.encode(params[:limit].to_i)}"] if params[:limit]
|
117
|
+
paramarr << ["offset=#{URI.encode(params[:offset].to_i)}"] if params[:offset]
|
118
|
+
paramarr << ["prefix=#{URI.encode(params[:prefix])}"] if params[:prefix]
|
119
|
+
paramarr << ["path=#{URI.encode(params[:path])}"] if params[:path]
|
120
|
+
paramstr = (paramarr.size > 0)? paramarr.join("&") : "" ;
|
121
|
+
response = self.connection.cfreq("GET",@storagehost,"#{@storagepath}?#{paramstr}")
|
122
|
+
return [] if (response.code == "204")
|
123
|
+
raise InvalidResponseException, "Invalid response code #{response.code}" unless (response.code == "200")
|
124
|
+
return response.body.to_a.map { |x| x.chomp }
|
125
|
+
end
|
126
|
+
alias :list_objects :objects
|
127
|
+
|
128
|
+
# Retrieves a list of all objects in the current container along with their size in bytes, hash, and content_type.
|
129
|
+
# If no objects exist, an empty hash is returned. Throws an InvalidResponseException if the request fails. Takes a
|
130
|
+
# parameter hash as an argument, in the same form as the objects method.
|
131
|
+
#
|
132
|
+
# Returns a hash in the same format as the containers_detail from the CloudFiles class.
|
133
|
+
#
|
134
|
+
# container.objects_detail
|
135
|
+
# => {"test.txt"=>{:content_type=>"application/octet-stream",
|
136
|
+
# :hash=>"e2a6fcb4771aa3509f6b27b6a97da55b",
|
137
|
+
# :last_modified=>Mon Jan 19 10:43:36 -0600 2009,
|
138
|
+
# :bytes=>"16"},
|
139
|
+
# "new.txt"=>{:content_type=>"application/octet-stream",
|
140
|
+
# :hash=>"0aa820d91aed05d2ef291d324e47bc96",
|
141
|
+
# :last_modified=>Wed Jan 28 10:16:26 -0600 2009,
|
142
|
+
# :bytes=>"22"}
|
143
|
+
# }
|
144
|
+
def objects_detail(params = {})
|
145
|
+
paramarr = []
|
146
|
+
paramarr << ["format=xml"]
|
147
|
+
paramarr << ["limit=#{URI.encode(params[:limit].to_i)}"] if params[:limit]
|
148
|
+
paramarr << ["offset=#{URI.encode(params[:offset].to_i)}"] if params[:offset]
|
149
|
+
paramarr << ["prefix=#{URI.encode(params[:prefix])}"] if params[:prefix]
|
150
|
+
paramarr << ["path=#{URI.encode(params[:path])}"] if params[:path]
|
151
|
+
paramstr = (paramarr.size > 0)? paramarr.join("&") : "" ;
|
152
|
+
response = self.connection.cfreq("GET",@storagehost,"#{@storagepath}?#{paramstr}")
|
153
|
+
return {} if (response.code == "204")
|
154
|
+
raise InvalidResponseException, "Invalid response code #{response.code}" unless (response.code == "200")
|
155
|
+
doc = REXML::Document.new(response.body)
|
156
|
+
detailhash = {}
|
157
|
+
doc.elements.each("container/object") { |o|
|
158
|
+
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) }
|
159
|
+
}
|
160
|
+
doc = nil
|
161
|
+
return detailhash
|
162
|
+
end
|
163
|
+
alias :list_objects_info :objects_detail
|
164
|
+
|
165
|
+
# Returns true if the container is public and CDN-enabled. Returns false otherwise.
|
166
|
+
#
|
167
|
+
# public_container.public?
|
168
|
+
# => true
|
169
|
+
#
|
170
|
+
# private_container.public?
|
171
|
+
# => false
|
172
|
+
def public?
|
173
|
+
return @cdn_enabled
|
174
|
+
end
|
175
|
+
|
176
|
+
# Returns true if a container is empty and returns false otherwise.
|
177
|
+
#
|
178
|
+
# new_container.empty?
|
179
|
+
# => true
|
180
|
+
#
|
181
|
+
# full_container.empty?
|
182
|
+
# => false
|
183
|
+
def empty?
|
184
|
+
return (@count.to_i == 0)? true : false
|
185
|
+
end
|
186
|
+
|
187
|
+
# Returns true if object exists and returns false otherwise.
|
188
|
+
#
|
189
|
+
# container.object_exists?('goodfile.txt')
|
190
|
+
# => true
|
191
|
+
#
|
192
|
+
# container.object_exists?('badfile.txt')
|
193
|
+
# => false
|
194
|
+
def object_exists?(objectname)
|
195
|
+
response = self.connection.cfreq("HEAD",@storagehost,"#{@storagepath}/#{objectname}")
|
196
|
+
return (response.code == "204")? true : false
|
197
|
+
end
|
198
|
+
|
199
|
+
# Creates a new CloudFiles::StorageObject in the current container.
|
200
|
+
#
|
201
|
+
# If an object with the specified name exists in the current container, that object will be returned. Otherwise,
|
202
|
+
# an empty new object will be returned.
|
203
|
+
#
|
204
|
+
# Passing in the optional make_path argument as true will create zero-byte objects to simulate a filesystem path
|
205
|
+
# to the object, if an objectname with path separators ("/path/to/myfile.mp3") is supplied. These path objects can
|
206
|
+
# be used in the Container.objects method.
|
207
|
+
def create_object(objectname,make_path = false)
|
208
|
+
CloudFiles::StorageObject.new(self,objectname,false,make_path)
|
209
|
+
end
|
210
|
+
|
211
|
+
# Removes an CloudFiles::StorageObject from a container. True is returned if the removal is successful. Throws
|
212
|
+
# NoSuchObjectException if the object doesn't exist. Throws InvalidResponseException if the request fails.
|
213
|
+
#
|
214
|
+
# container.delete_object('new.txt')
|
215
|
+
# => true
|
216
|
+
#
|
217
|
+
# container.delete_object('nonexistent_file.txt')
|
218
|
+
# => NoSuchObjectException: Object nonexistent_file.txt does not exist
|
219
|
+
def delete_object(objectname)
|
220
|
+
response = self.connection.cfreq("DELETE",@storagehost,"#{@storagepath}/#{objectname}")
|
221
|
+
raise NoSuchObjectException, "Object #{objectname} does not exist" if (response.code == "404")
|
222
|
+
raise InvalidResponseException, "Invalid response code #{response.code}" unless (response.code == "204")
|
223
|
+
true
|
224
|
+
end
|
225
|
+
|
226
|
+
# Makes a container publicly available via the Cloud Files CDN and returns true upon success. Throws NoSuchContainerException
|
227
|
+
# if the container doesn't exist or if the request fails.
|
228
|
+
#
|
229
|
+
# Takes an optional argument, which is the CDN cache TTL in seconds (default 86400 seconds or 1 day)
|
230
|
+
#
|
231
|
+
# container.make_public(432000)
|
232
|
+
# => true
|
233
|
+
def make_public(ttl = 86400)
|
234
|
+
headers = { "X-CDN-Enabled" => "True", "X-TTL" => ttl.to_s }
|
235
|
+
response = self.connection.cfreq("PUT",@cdnmgmthost,@cdnmgmtpath,headers)
|
236
|
+
raise NoSuchContainerException, "Container #{@name} does not exist" unless (response.code == "201" || response.code == "202")
|
237
|
+
true
|
238
|
+
end
|
239
|
+
|
240
|
+
# Makes a container private and returns true upon success. Throws NoSuchContainerException
|
241
|
+
# if the container doesn't exist or if the request fails.
|
242
|
+
#
|
243
|
+
# Note that if the container was previously public, it will continue to exist out on the CDN until it expires.
|
244
|
+
#
|
245
|
+
# container.make_private
|
246
|
+
# => true
|
247
|
+
def make_private
|
248
|
+
headers = { "X-CDN-Enabled" => "False" }
|
249
|
+
response = self.connection.cfreq("PUT",@cdnmgmthost,@cdnmgmtpath,headers)
|
250
|
+
raise NoSuchContainerException, "Container #{@name} does not exist" unless (response.code == "201" || response.code == "202")
|
251
|
+
true
|
252
|
+
end
|
253
|
+
|
254
|
+
def to_s # :nodoc:
|
255
|
+
@name
|
256
|
+
end
|
257
|
+
|
258
|
+
end
|
259
|
+
|
260
|
+
end
|
@@ -0,0 +1,253 @@
|
|
1
|
+
# Copyright (C) 2008 Rackspace US, Inc.
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
4
|
+
#
|
5
|
+
# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
6
|
+
#
|
7
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
8
|
+
#
|
9
|
+
# Except as contained in this notice, the name of Rackspace US, Inc. shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Software without prior written authorization from Rackspace US, Inc.
|
10
|
+
|
11
|
+
module CloudFiles
|
12
|
+
class StorageObject
|
13
|
+
|
14
|
+
# Name of the object corresponding to the instantiated object
|
15
|
+
attr_reader :name
|
16
|
+
|
17
|
+
# Size of the object (in bytes)
|
18
|
+
attr_reader :bytes
|
19
|
+
|
20
|
+
# The parent CloudFiles::Container object
|
21
|
+
attr_reader :container
|
22
|
+
|
23
|
+
# Date of the object's last modification
|
24
|
+
attr_reader :last_modified
|
25
|
+
|
26
|
+
# ETag of the object data
|
27
|
+
attr_reader :etag
|
28
|
+
|
29
|
+
# Content type of the object data
|
30
|
+
attr_reader :content_type
|
31
|
+
|
32
|
+
# Builds a new CloudFiles::StorageObject in the current container. If force_exist is set, the object must exist or a
|
33
|
+
# NoSuchObjectException will be raised. If not, an "empty" CloudFiles::StorageObject will be returned, ready for data
|
34
|
+
# via CloudFiles::StorageObject.write
|
35
|
+
def initialize(container,objectname,force_exists=false,make_path=false)
|
36
|
+
if objectname.match(/\?/)
|
37
|
+
raise SyntaxException, "Object #{objectname} contains an invalid character in the name (? not allowed)"
|
38
|
+
end
|
39
|
+
@container = container
|
40
|
+
@containername = container.name
|
41
|
+
@name = objectname
|
42
|
+
@make_path = make_path
|
43
|
+
@storagehost = self.container.connection.storagehost
|
44
|
+
@storagepath = self.container.connection.storagepath+"/#{@containername}/#{@name}"
|
45
|
+
if container.object_exists?(objectname)
|
46
|
+
populate
|
47
|
+
else
|
48
|
+
raise NoSuchObjectException, "Object #{@name} does not exist" if force_exists
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Caches data about the CloudFiles::StorageObject for fast retrieval. This method is automatically called when the
|
53
|
+
# class is initialized, but it can be called again if the data needs to be updated.
|
54
|
+
def populate
|
55
|
+
response = self.container.connection.cfreq("HEAD",@storagehost,@storagepath)
|
56
|
+
raise NoSuchObjectException, "Object #{@name} does not exist" if (response.code != "204")
|
57
|
+
@bytes = response["content-length"]
|
58
|
+
@last_modified = Time.parse(response["last-modified"])
|
59
|
+
@etag = response["etag"]
|
60
|
+
@content_type = response["content-type"]
|
61
|
+
resphash = {}
|
62
|
+
response.to_hash.select { |k,v| k.match(/^x-object-meta/) }.each { |x| resphash[x[0]] = x[1][0].to_s }
|
63
|
+
@metadata = resphash
|
64
|
+
true
|
65
|
+
end
|
66
|
+
alias :refresh :populate
|
67
|
+
|
68
|
+
# Retrieves the data from an object and stores the data in memory. The data is returned as a string.
|
69
|
+
# Throws a NoSuchObjectException if the object doesn't exist.
|
70
|
+
#
|
71
|
+
# If the optional size and range arguments are provided, the call will return the number of bytes provided by
|
72
|
+
# size, starting from the offset provided in offset.
|
73
|
+
#
|
74
|
+
# object.data
|
75
|
+
# => "This is the text stored in the file"
|
76
|
+
def data(size=-1,offset=0,headers = {})
|
77
|
+
if size.to_i > 0
|
78
|
+
range = sprintf("bytes=%d-%d", offset.to_i, (offset.to_i + size.to_i) - 1)
|
79
|
+
headers['Range'] = range
|
80
|
+
end
|
81
|
+
response = self.container.connection.cfreq("GET",@storagehost,@storagepath,headers)
|
82
|
+
raise NoSuchObjectException, "Object #{@name} does not exist" unless (response.code =~ /^20/)
|
83
|
+
response.body.chomp
|
84
|
+
end
|
85
|
+
|
86
|
+
# Retrieves the data from an object and returns a stream that must be passed to a block. Throws a
|
87
|
+
# NoSuchObjectException if the object doesn't exist.
|
88
|
+
#
|
89
|
+
# If the optional size and range arguments are provided, the call will return the number of bytes provided by
|
90
|
+
# size, starting from the offset provided in offset.
|
91
|
+
#
|
92
|
+
# data = ""
|
93
|
+
# object.data_stream do |chunk|
|
94
|
+
# data += chunk
|
95
|
+
# end
|
96
|
+
#
|
97
|
+
# data
|
98
|
+
# => "This is the text stored in the file"
|
99
|
+
def data_stream(size=-1,offset=0,headers = {},&block)
|
100
|
+
if size.to_i > 0
|
101
|
+
range = sprintf("bytes=%d-%d", offset.to_i, (offset.to_i + size.to_i) - 1)
|
102
|
+
headers['Range'] = range
|
103
|
+
end
|
104
|
+
self.container.connection.cfreq("GET",@storagehost,@storagepath,headers,nil) do |response|
|
105
|
+
raise NoSuchObjectException, "Object #{@name} does not exist" unless (response.code == "200")
|
106
|
+
response.read_body(&block)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Returns the object's metadata as a nicely formatted hash, stripping off the X-Meta-Object- prefix that the system prepends to the
|
111
|
+
# key name.
|
112
|
+
#
|
113
|
+
# object.metadata
|
114
|
+
# => {"ruby"=>"cool", "foo"=>"bar"}
|
115
|
+
def metadata
|
116
|
+
metahash = {}
|
117
|
+
@metadata.each{|key, value| metahash[key.gsub(/x-object-meta-/,'').gsub(/\+\-/, ' ')] = URI.decode(value).gsub(/\+\-/, ' ')}
|
118
|
+
metahash
|
119
|
+
end
|
120
|
+
|
121
|
+
# Sets the metadata for an object. By passing a hash as an argument, you can set the metadata for an object.
|
122
|
+
# However, setting metadata will overwrite any existing metadata for the object.
|
123
|
+
#
|
124
|
+
# Throws NoSuchObjectException if the object doesn't exist. Throws InvalidResponseException if the request
|
125
|
+
# fails.
|
126
|
+
def set_metadata(metadatahash)
|
127
|
+
headers = {}
|
128
|
+
metadatahash.each{|key, value| headers['X-Object-Meta-' + key.to_s.capitalize] = value.to_s}
|
129
|
+
response = self.container.connection.cfreq("POST",@storagehost,@storagepath,headers)
|
130
|
+
raise NoSuchObjectException, "Object #{@name} does not exist" if (response.code == "404")
|
131
|
+
raise InvalidResponseException, "Invalid response code #{response.code}" unless (response.code == "202")
|
132
|
+
true
|
133
|
+
end
|
134
|
+
|
135
|
+
# Takes supplied data and writes it to the object, saving it. You can supply an optional hash of headers, including
|
136
|
+
# Content-Type and ETag, that will be applied to the object.
|
137
|
+
#
|
138
|
+
# If you would rather stream the data in chunks, instead of reading it all into memory at once, you can pass an
|
139
|
+
# IO object for the data, such as: object.write(open('/path/to/file.mp3'))
|
140
|
+
#
|
141
|
+
# You can compute your own MD5 sum and send it in the "ETag" header. If you provide yours, it will be compared to
|
142
|
+
# the MD5 sum on the server side. If they do not match, the server will return a 422 status code and a MisMatchedChecksumException
|
143
|
+
# will be raised. If you do not provide an MD5 sum as the ETag, one will be computed on the server side.
|
144
|
+
#
|
145
|
+
# Updates the container cache and returns true on success, raises exceptions if stuff breaks.
|
146
|
+
#
|
147
|
+
# object = container.create_object("newfile.txt")
|
148
|
+
#
|
149
|
+
# object.write("This is new data")
|
150
|
+
# => true
|
151
|
+
#
|
152
|
+
# object.data
|
153
|
+
# => "This is new data"
|
154
|
+
def write(data=nil,headers={})
|
155
|
+
#raise SyntaxException, "No data was provided for object '#{@name}'" if (data.nil?)
|
156
|
+
# Try to get the content type
|
157
|
+
raise SyntaxException, "No data or header updates supplied" if (data.nil? and headers.empty?)
|
158
|
+
if headers['Content-Type'].nil?
|
159
|
+
type = MIME::Types.type_for(self.name).first.to_s
|
160
|
+
if type.empty?
|
161
|
+
headers['Content-Type'] = "application/octet-stream"
|
162
|
+
else
|
163
|
+
headers['Content-Type'] = type
|
164
|
+
end
|
165
|
+
end
|
166
|
+
response = self.container.connection.cfreq("PUT",@storagehost,"#{@storagepath}",headers,data)
|
167
|
+
raise InvalidResponseException, "Invalid content-length header sent" if (response.code == "412")
|
168
|
+
raise MisMatchedChecksumException, "Mismatched etag" if (response.code == "422")
|
169
|
+
raise InvalidResponseException, "Invalid response code #{response.code}" unless (response.code == "201")
|
170
|
+
make_path(File.dirname(self.name)) if @make_path == true
|
171
|
+
self.populate
|
172
|
+
true
|
173
|
+
end
|
174
|
+
|
175
|
+
# A convenience method to stream data into an object from a local file (or anything that can be loaded by Ruby's open method)
|
176
|
+
#
|
177
|
+
# Throws an Errno::ENOENT if the file cannot be read.
|
178
|
+
#
|
179
|
+
# object.data
|
180
|
+
# => "This is my data"
|
181
|
+
#
|
182
|
+
# object.load_from_filename("/tmp/file.txt")
|
183
|
+
# => true
|
184
|
+
#
|
185
|
+
# object.data
|
186
|
+
# => "This data was in the file /tmp/file.txt"
|
187
|
+
#
|
188
|
+
# object.load_from_filename("/tmp/nonexistent.txt")
|
189
|
+
# => Errno::ENOENT: No such file or directory - /tmp/nonexistent.txt
|
190
|
+
def load_from_filename(filename)
|
191
|
+
f = open(filename)
|
192
|
+
self.write(f)
|
193
|
+
f.close
|
194
|
+
true
|
195
|
+
end
|
196
|
+
|
197
|
+
# A convenience method to stream data from an object into a local file
|
198
|
+
#
|
199
|
+
# Throws an Errno::ENOENT if the file cannot be opened for writing due to a path error,
|
200
|
+
# and Errno::EACCES if the file cannot be opened for writing due to permissions.
|
201
|
+
#
|
202
|
+
# object.data
|
203
|
+
# => "This is my data"
|
204
|
+
#
|
205
|
+
# object.save_to_filename("/tmp/file.txt")
|
206
|
+
# => true
|
207
|
+
#
|
208
|
+
# $ cat /tmp/file.txt
|
209
|
+
# "This is my data"
|
210
|
+
#
|
211
|
+
# object.save_to_filename("/tmp/owned_by_root.txt")
|
212
|
+
# => Errno::EACCES: Permission denied - /tmp/owned_by_root.txt
|
213
|
+
def save_to_filename(filename)
|
214
|
+
::File.open(filename, 'w+') do |f|
|
215
|
+
self.data_stream do |chunk|
|
216
|
+
f.write chunk
|
217
|
+
end
|
218
|
+
end
|
219
|
+
true
|
220
|
+
end
|
221
|
+
|
222
|
+
# If the parent container is public (CDN-enabled), returns the CDN URL to this object. Otherwise, return nil
|
223
|
+
#
|
224
|
+
# public_object.public_url
|
225
|
+
# => "http://cdn.cloudfiles.mosso.com/c10181/rampage.jpg"
|
226
|
+
#
|
227
|
+
# private_object.public_url
|
228
|
+
# => nil
|
229
|
+
def public_url
|
230
|
+
self.container.public? ? self.container.cdn_url + "/#{URI.encode(@name)}" : nil
|
231
|
+
end
|
232
|
+
|
233
|
+
def to_s # :nodoc:
|
234
|
+
@name
|
235
|
+
end
|
236
|
+
|
237
|
+
private
|
238
|
+
|
239
|
+
def make_path(path) # :nodoc:
|
240
|
+
if path == "." || path == "/"
|
241
|
+
return
|
242
|
+
else
|
243
|
+
unless self.container.object_exists?(path)
|
244
|
+
o = self.container.create_object(path)
|
245
|
+
o.write(nil,{'Content-Type' => 'application/directory'})
|
246
|
+
end
|
247
|
+
make_path(File.dirname(path))
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
end
|
252
|
+
|
253
|
+
end
|