cloudfiles-sagamore 1.5.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,65 @@
1
+ # The deprecated old exception types. Will go away in a couple of releases.
2
+
3
+ class SyntaxException < StandardError # :nodoc:
4
+ end
5
+ class ConnectionException < StandardError # :nodoc:
6
+ end
7
+ class AuthenticationException < StandardError # :nodoc:
8
+ end
9
+ class InvalidResponseException < StandardError # :nodoc:
10
+ end
11
+ class NonEmptyContainerException < StandardError # :nodoc:
12
+ end
13
+ class NoSuchObjectException < StandardError # :nodoc:
14
+ end
15
+ class NoSuchContainerException < StandardError # :nodoc:
16
+ end
17
+ class NoSuchAccountException < StandardError # :nodoc:
18
+ end
19
+ class MisMatchedChecksumException < StandardError # :nodoc:
20
+ end
21
+ class IOException < StandardError # :nodoc:
22
+ end
23
+ class CDNNotEnabledException < StandardError # :nodoc:
24
+ end
25
+ class ObjectExistsException < StandardError # :nodoc:
26
+ end
27
+ class ExpiredAuthTokenException < StandardError # :nodoc:
28
+ end
29
+
30
+ # The new properly scoped exceptions.
31
+
32
+ module CloudFiles
33
+ class Exception
34
+
35
+ class Syntax < SyntaxException
36
+ end
37
+ class Connection < ConnectionException # :nodoc:
38
+ end
39
+ class Authentication < AuthenticationException # :nodoc:
40
+ end
41
+ class InvalidResponse < InvalidResponseException # :nodoc:
42
+ end
43
+ class NonEmptyContainer < NonEmptyContainerException # :nodoc:
44
+ end
45
+ class NoSuchObject < NoSuchObjectException # :nodoc:
46
+ end
47
+ class NoSuchContainer < NoSuchContainerException # :nodoc:
48
+ end
49
+ class NoSuchAccount < NoSuchAccountException # :nodoc:
50
+ end
51
+ class MisMatchedChecksum < MisMatchedChecksumException # :nodoc:
52
+ end
53
+ class IO < IOException # :nodoc:
54
+ end
55
+ class CDNNotEnabled < CDNNotEnabledException # :nodoc:
56
+ end
57
+ class ObjectExists < ObjectExistsException # :nodoc:
58
+ end
59
+ class ExpiredAuthToken < ExpiredAuthTokenException # :nodoc:
60
+ end
61
+ class CDNNotAvailable < StandardError
62
+ end
63
+
64
+ end
65
+ end
@@ -0,0 +1,426 @@
1
+ module CloudFiles
2
+ class StorageObject
3
+ # See COPYING for license information.
4
+ # Copyright (c) 2011, Rackspace US, Inc.
5
+
6
+ # Name of the object corresponding to the instantiated object
7
+ attr_reader :name
8
+
9
+ # The parent CloudFiles::Container object
10
+ attr_reader :container
11
+
12
+ # Builds a new CloudFiles::StorageObject in the current container. If force_exist is set, the object must exist or a
13
+ # CloudFiles::Exception::NoSuchObject Exception will be raised. If not, an "empty" CloudFiles::StorageObject will be returned, ready for data
14
+ # via CloudFiles::StorageObject.write
15
+ def initialize(container, objectname, force_exists = false, make_path = false)
16
+ @container = container
17
+ @containername = container.name
18
+ @name = objectname
19
+ @make_path = make_path
20
+ @storagepath = "#{CloudFiles.escape @containername}/#{escaped_name}"
21
+
22
+ if force_exists
23
+ raise CloudFiles::Exception::NoSuchObject, "Object #{@name} does not exist" unless container.object_exists?(objectname)
24
+ end
25
+ end
26
+
27
+ # Refreshes the object metadata
28
+ def refresh
29
+ @object_metadata = nil
30
+ true
31
+ end
32
+ alias :populate :refresh
33
+
34
+ # Retrieves Metadata for the object
35
+ def object_metadata
36
+ @object_metadata ||= (
37
+ begin
38
+ response = SwiftClient.head_object(self.container.connection.storageurl, self.container.connection.authtoken, self.container.name, @name)
39
+ rescue ClientException => e
40
+ raise CloudFiles::Exception::NoSuchObject, "Object #{@name} does not exist" unless (e.status.to_s =~ /^20/)
41
+ end
42
+ resphash = {}
43
+ metas = response.to_hash.select { |k,v| k.match(/^x-object-meta/) }
44
+
45
+ metas.each do |x,y|
46
+ resphash[x] = (y.respond_to?(:join) ? y.join('') : y.to_s)
47
+ end
48
+
49
+ {
50
+ :manifest => response["x-object-manifest"],
51
+ :bytes => response["content-length"],
52
+ :last_modified => Time.parse(response["last-modified"]),
53
+ :etag => response["etag"],
54
+ :content_type => response["content-type"],
55
+ :metadata => resphash
56
+ }
57
+ )
58
+ end
59
+
60
+ def escaped_name
61
+ @escaped_name ||= escape_name @name
62
+ end
63
+
64
+ # Size of the object (in bytes)
65
+ def bytes
66
+ self.object_metadata[:bytes]
67
+ end
68
+
69
+ # Date of the object's last modification
70
+ def last_modified
71
+ self.object_metadata[:last_modified]
72
+ end
73
+
74
+ # ETag of the object data
75
+ def etag
76
+ self.object_metadata[:etag]
77
+ end
78
+
79
+ # Content type of the object data
80
+ def content_type
81
+ self.object_metadata[:content_type]
82
+ end
83
+
84
+ def content_type=(type)
85
+ self.copy(:headers => {'Content-Type' => type})
86
+ end
87
+
88
+ # Retrieves the data from an object and stores the data in memory. The data is returned as a string.
89
+ # Throws a NoSuchObjectException if the object doesn't exist.
90
+ #
91
+ # If the optional size and range arguments are provided, the call will return the number of bytes provided by
92
+ # size, starting from the offset provided in offset.
93
+ #
94
+ # object.data
95
+ # => "This is the text stored in the file"
96
+ def data(size = -1, offset = 0, headers = {})
97
+ if size.to_i > 0
98
+ range = sprintf("bytes=%d-%d", offset.to_i, (offset.to_i + size.to_i) - 1)
99
+ headers['Range'] = range
100
+ end
101
+ begin
102
+ response = SwiftClient.get_object(self.container.connection.storageurl, self.container.connection.authtoken, self.container.name, @name)
103
+ response[1]
104
+ rescue ClientException => e
105
+ raise CloudFiles::Exception::NoSuchObject, "Object #{@name} does not exist" unless (e.status.to_s =~ /^20/)
106
+ end
107
+ end
108
+ alias :read :data
109
+
110
+ # Retrieves the data from an object and returns a stream that must be passed to a block. Throws a
111
+ # NoSuchObjectException if the object doesn't exist.
112
+ #
113
+ # If the optional size and range arguments are provided, the call will return the number of bytes provided by
114
+ # size, starting from the offset provided in offset.
115
+ #
116
+ # data = ""
117
+ # object.data_stream do |chunk|
118
+ # data += chunk
119
+ # end
120
+ #
121
+ # data
122
+ # => "This is the text stored in the file"
123
+ def data_stream(size = -1, offset = 0, headers = {}, &block)
124
+ if size.to_i > 0
125
+ range = sprintf("bytes=%d-%d", offset.to_i, (offset.to_i + size.to_i) - 1)
126
+ headers['Range'] = range
127
+ end
128
+ begin
129
+ SwiftClient.get_object(self.container.connection.storageurl, self.container.connection.authtoken, self.container.name, @name, nil, nil, &block)
130
+ end
131
+ end
132
+
133
+ # Returns the object's metadata as a nicely formatted hash, stripping off the X-Meta-Object- prefix that the system prepends to the
134
+ # key name.
135
+ #
136
+ # object.metadata
137
+ # => {"ruby"=>"cool", "foo"=>"bar"}
138
+ def metadata
139
+ metahash = {}
140
+ self.object_metadata[:metadata].each{ |key, value| metahash[key.gsub(/x-object-meta-/, '').gsub(/\+\-/, ' ')] = URI.decode(value).gsub(/\+\-/, ' ') }
141
+ metahash
142
+ end
143
+
144
+ # Sets the metadata for an object. By passing a hash as an argument, you can set the metadata for an object.
145
+ # However, setting metadata will overwrite any existing metadata for the object.
146
+ #
147
+ # Throws NoSuchObjectException if the object doesn't exist. Throws InvalidResponseException if the request
148
+ # fails.
149
+ def set_metadata(metadatahash)
150
+ headers = {}
151
+ metadatahash.each{ |key, value| headers['X-Object-Meta-' + key.to_s.capitalize] = value.to_s }
152
+ begin
153
+ SwiftClient.post_object(self.container.connection.storageurl, self.container.connection.authtoken, self.container.name, @name, headers)
154
+ true
155
+ rescue ClientException => e
156
+ raise CloudFiles::Exception::NoSuchObject, "Object #{@name} does not exist" if (e.status.to_s == "404")
157
+ raise CloudFiles::Exception::InvalidResponse, "Invalid response code #{e.status.to_s}" unless (e.status.to_s =~ /^20/)
158
+ false
159
+ end
160
+ end
161
+ alias :metadata= :set_metadata
162
+
163
+
164
+ # Returns the object's manifest.
165
+ #
166
+ # object.manifest
167
+ # => "container/prefix"
168
+ def manifest
169
+ self.object_metadata[:manifest]
170
+ end
171
+
172
+
173
+ # Sets the manifest for an object. By passing a string as an argument, you can set the manifest for an object.
174
+ # However, setting manifest will overwrite any existing manifest for the object.
175
+ #
176
+ # Throws NoSuchObjectException if the object doesn't exist. Throws InvalidResponseException if the request
177
+ # fails.
178
+ def set_manifest(manifest)
179
+ headers = {'X-Object-Manifest' => manifest}
180
+ begin
181
+ SwiftClient.post_object(self.container.connection.storageurl, self.container.connection.authtoken, self.container.name, @name, headers)
182
+ true
183
+ rescue ClientException => e
184
+ raise CloudFiles::Exception::NoSuchObject, "Object #{@name} does not exist" if (response.code == "404")
185
+ raise CloudFiles::Exception::InvalidResponse, "Invalid response code #{response.code}" unless (response.code =~ /^20/)
186
+ false
187
+ end
188
+ end
189
+
190
+
191
+ # Takes supplied data and writes it to the object, saving it. You can supply an optional hash of headers, including
192
+ # Content-Type and ETag, that will be applied to the object.
193
+ #
194
+ # If you would rather stream the data in chunks, instead of reading it all into memory at once, you can pass an
195
+ # IO object for the data, such as: object.write(open('/path/to/file.mp3'))
196
+ #
197
+ # You can compute your own MD5 sum and send it in the "ETag" header. If you provide yours, it will be compared to
198
+ # the MD5 sum on the server side. If they do not match, the server will return a 422 status code and a CloudFiles::Exception::MisMatchedChecksum Exception
199
+ # will be raised. If you do not provide an MD5 sum as the ETag, one will be computed on the server side.
200
+ #
201
+ # Updates the container cache and returns true on success, raises exceptions if stuff breaks.
202
+ #
203
+ # object = container.create_object("newfile.txt")
204
+ #
205
+ # object.write("This is new data")
206
+ # => true
207
+ #
208
+ # object.data
209
+ # => "This is new data"
210
+ #
211
+ # If you are passing your data in via STDIN, just do an
212
+ #
213
+ # object.write
214
+ #
215
+ # with no data (or, if you need to pass headers)
216
+ #
217
+ # object.write(nil,{'header' => 'value})
218
+
219
+ def write(data = nil, headers = {})
220
+ raise CloudFiles::Exception::Syntax, "No data or header updates supplied" if ((data.nil? && $stdin.tty?) and headers.empty?)
221
+ # If we're taking data from standard input, send that IO object to cfreq
222
+ data = $stdin if (data.nil? && $stdin.tty? == false)
223
+ begin
224
+ response = SwiftClient.put_object(self.container.connection.storageurl, self.container.connection.authtoken, self.container.name, @name, data, nil, nil, nil, nil, headers)
225
+ rescue ClientException => e
226
+ code = e.status.to_s
227
+ raise CloudFiles::Exception::InvalidResponse, "Invalid content-length header sent" if (code == "412")
228
+ raise CloudFiles::Exception::MisMatchedChecksum, "Mismatched etag" if (code == "422")
229
+ raise CloudFiles::Exception::InvalidResponse, "Invalid response code #{code}" unless (code =~ /^20./)
230
+ end
231
+ make_path(File.dirname(self.name)) if @make_path == true
232
+ self.refresh
233
+ true
234
+ end
235
+ # Purges CDN Edge Cache for all objects inside of this container
236
+ #
237
+ # :email, An valid email address or comma seperated
238
+ # list of emails to be notified once purge is complete .
239
+ #
240
+ # obj.purge_from_cdn
241
+ # => true
242
+ #
243
+ # or
244
+ #
245
+ # obj.purge_from_cdn("User@domain.com")
246
+ # => true
247
+ #
248
+ # or
249
+ #
250
+ # obj.purge_from_cdn("User@domain.com, User2@domain.com")
251
+ # => true
252
+ def purge_from_cdn(email=nil)
253
+ raise Exception::CDNNotAvailable unless cdn_available?
254
+ headers = {}
255
+ headers = {"X-Purge-Email" => email} if email
256
+ begin
257
+ SwiftClient.delete_object(self.container.connection.cdnurl, self.container.connection.authtoken, self.container.name, @name, nil, headers)
258
+ true
259
+ rescue ClientException => e
260
+ raise CloudFiles::Exception::Connection, "Error Unable to Purge Object: #{@name}" unless (e.status.to_s =~ /^20.$/)
261
+ false
262
+ end
263
+ end
264
+
265
+ # A convenience method to stream data into an object from a local file (or anything that can be loaded by Ruby's open method)
266
+ #
267
+ # You can provide an optional hash of headers, in case you want to do something like set the Content-Type manually.
268
+ #
269
+ # Throws an Errno::ENOENT if the file cannot be read.
270
+ #
271
+ # object.data
272
+ # => "This is my data"
273
+ #
274
+ # object.load_from_filename("/tmp/file.txt")
275
+ # => true
276
+ #
277
+ # object.load_from_filename("/home/rackspace/myfile.tmp", 'Content-Type' => 'text/plain')
278
+ #
279
+ # object.data
280
+ # => "This data was in the file /tmp/file.txt"
281
+ #
282
+ # object.load_from_filename("/tmp/nonexistent.txt")
283
+ # => Errno::ENOENT: No such file or directory - /tmp/nonexistent.txt
284
+ def load_from_filename(filename, headers = {}, check_md5 = false)
285
+ f = open(filename)
286
+ if check_md5
287
+ require 'digest/md5'
288
+ md5_hash = Digest::MD5.file(filename)
289
+ headers["Etag"] = md5_hash.to_s()
290
+ end
291
+ self.write(f, headers)
292
+ f.close
293
+ true
294
+ end
295
+
296
+ # A convenience method to stream data from an object into a local file
297
+ #
298
+ # Throws an Errno::ENOENT if the file cannot be opened for writing due to a path error,
299
+ # and Errno::EACCES if the file cannot be opened for writing due to permissions.
300
+ #
301
+ # object.data
302
+ # => "This is my data"
303
+ #
304
+ # object.save_to_filename("/tmp/file.txt")
305
+ # => true
306
+ #
307
+ # $ cat /tmp/file.txt
308
+ # "This is my data"
309
+ #
310
+ # object.save_to_filename("/tmp/owned_by_root.txt")
311
+ # => Errno::EACCES: Permission denied - /tmp/owned_by_root.txt
312
+ def save_to_filename(filename)
313
+ File.open(filename, 'wb+') do |f|
314
+ self.data_stream do |chunk|
315
+ f.write chunk
316
+ end
317
+ end
318
+ true
319
+ end
320
+
321
+ # If the parent container is public (CDN-enabled), returns the CDN URL to this object. Otherwise, return nil
322
+ #
323
+ # public_object.public_url
324
+ # => "http://c0001234.cdn.cloudfiles.rackspacecloud.com/myfile.jpg"
325
+ #
326
+ # private_object.public_url
327
+ # => nil
328
+ def public_url
329
+ self.container.public? ? self.container.cdn_url + "/#{escaped_name}" : nil
330
+ end
331
+
332
+ # If the parent container is public (CDN-enabled), returns the SSL CDN URL to this object. Otherwise, return nil
333
+ #
334
+ # public_object.public_ssl_url
335
+ # => "https://c61.ssl.cf0.rackcdn.com/myfile.jpg"
336
+ #
337
+ # private_object.public_ssl_url
338
+ # => nil
339
+ def public_ssl_url
340
+ self.container.public? ? self.container.cdn_ssl_url + "/#{escaped_name}" : nil
341
+ end
342
+
343
+ # If the parent container is public (CDN-enabled), returns the SSL CDN URL to this object. Otherwise, return nil
344
+ #
345
+ # public_object.public_streaming_url
346
+ # => "https://c61.stream.rackcdn.com/myfile.jpg"
347
+ #
348
+ # private_object.public_streaming_url
349
+ # => nil
350
+ def public_streaming_url
351
+ self.container.public? ? self.container.cdn_streaming_url + "/#{escaped_name}" : nil
352
+ end
353
+
354
+ # Copy this object to a new location (optionally in a new container)
355
+ #
356
+ # You must supply either a name for the new object or a container name, or both. If a :name is supplied without a :container,
357
+ # the object is copied within the current container. If the :container is specified with no :name, then the object is copied
358
+ # to the new container with its current name.
359
+ #
360
+ # object.copy(:name => "images/funny/lolcat.jpg", :container => "pictures")
361
+ #
362
+ # You may also supply a hash of headers in the :headers option. From there, you can set things like Content-Type, or other
363
+ # headers as available in the API document.
364
+ #
365
+ # object.copy(:name => 'newfile.tmp', :headers => {'Content-Type' => 'text/plain'})
366
+ #
367
+ # Returns the new CloudFiles::StorageObject for the copied item.
368
+ def copy(options = {})
369
+ raise CloudFiles::Exception::Syntax, "You must provide the :container, :name, or :headers for this operation" unless (options[:container] || options[:name] || options[:headers])
370
+ new_container = options[:container] || self.container.name
371
+ new_name = options[:name] || self.name
372
+ new_headers = options[:headers] || {}
373
+ raise CloudFiles::Exception::Syntax, "The :headers option must be a hash" unless new_headers.is_a?(Hash)
374
+ new_name.sub!(/^\//,'')
375
+ headers = {'X-Copy-From' => "#{self.container.name}/#{self.name}", 'Content-Type' => self.content_type.sub(/;.+/, '')}.merge(new_headers)
376
+ # , 'Content-Type' => self.content_type
377
+ new_path = "#{CloudFiles.escape new_container}/#{escape_name new_name}"
378
+ begin
379
+ response = SwiftClient.put_object(self.container.connection.storageurl, self.container.connection.authtoken, new_container, new_name, nil, nil, nil, nil, nil, headers)
380
+ return CloudFiles::Container.new(self.container.connection, new_container).object(new_name)
381
+ rescue ClientException => e
382
+ code = e.status.to_s
383
+ raise CloudFiles::Exception::InvalidResponse, "Invalid response code #{response.code}" unless (response.code =~ /^20/)
384
+ end
385
+ end
386
+
387
+ # Takes the same options as the copy method, only it does a copy followed by a delete on the original object.
388
+ #
389
+ # Returns the new CloudFiles::StorageObject for the moved item. You should not attempt to use the old object after doing
390
+ # a move.
391
+ def move(options = {})
392
+ new_object = self.copy(options)
393
+ self.container.delete_object(self.name)
394
+ self.freeze
395
+ return new_object
396
+ end
397
+
398
+ def to_s # :nodoc:
399
+ @name
400
+ end
401
+
402
+ def escape_name(name)
403
+ CloudFiles.escape name
404
+ end
405
+
406
+ private
407
+
408
+ def cdn_available?
409
+ @cdn_available ||= self.container.connection.cdn_available?
410
+ end
411
+
412
+ def make_path(path) # :nodoc:
413
+ if path == "." || path == "/"
414
+ return
415
+ else
416
+ unless self.container.object_exists?(path)
417
+ o = self.container.create_object(path)
418
+ o.write(nil, {'Content-Type' => 'application/directory'})
419
+ end
420
+ make_path(File.dirname(path))
421
+ end
422
+ end
423
+
424
+ end
425
+
426
+ end