cloudfiles 1.4.16 → 1.4.17

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -4,3 +4,4 @@ doc/*
4
4
  pkg/*
5
5
  *.gem
6
6
  .idea
7
+ Gemfile.lock
data/CHANGELOG CHANGED
@@ -1,3 +1,10 @@
1
+ ================================================================================
2
+ 1.4.17 (2011/05/27)
3
+ ================================================================================
4
+ o Added Manifest support for Large Objects
5
+ o Add support for container metadata
6
+ o Option to check the MD5 of a file uploaded via the load_from_filename method
7
+
1
8
  ================================================================================
2
9
  1.4.16 (2011/03/17)
3
10
  ================================================================================
data/CONTRIBUTORS CHANGED
@@ -29,3 +29,5 @@ megaphone
29
29
  Nugroho Herucahyono
30
30
  CvX
31
31
  Topper Bowers
32
+ Xavier Shay
33
+ Dillon Amburgey
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source :gemcutter
2
+
3
+ gemspec
data/Rakefile CHANGED
@@ -1,22 +1,3 @@
1
- require './lib/cloudfiles.rb'
2
-
3
- begin
4
- require 'jeweler'
5
- Jeweler::Tasks.new do |gemspec|
6
- gemspec.name = "cloudfiles"
7
- gemspec.summary = "A Ruby API into Rackspace Cloud Files"
8
- gemspec.description = "A Ruby version of the Rackspace Cloud Files API."
9
- gemspec.email = "minter@lunenburg.org"
10
- gemspec.homepage = "http://www.rackspacecloud.com/cloud_hosting_products/files"
11
- gemspec.authors = ["H. Wade Minter", "Rackspace Hosting"]
12
- gemspec.add_dependency('mime-types', '>= 1.16')
13
- gemspec.add_development_dependency "mocha", "~> 0.9.8"
14
- end
15
- Jeweler::GemcutterTasks.new
16
- rescue LoadError
17
- puts "Jeweler not available. Install it with: sudo gem install jeweler"
18
- end
19
-
20
1
  namespace :test do
21
2
  desc 'Check test coverage'
22
3
  task :coverage do
data/cloudfiles.gemspec CHANGED
@@ -1,15 +1,11 @@
1
- # Generated by jeweler
2
- # DO NOT EDIT THIS FILE DIRECTLY
3
- # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
- # -*- encoding: utf-8 -*-
1
+ require 'lib/cloudfiles/version'
5
2
 
6
3
  Gem::Specification.new do |s|
7
4
  s.name = %q{cloudfiles}
8
- s.version = "1.4.16"
5
+ s.version = CloudFiles::VERSION
9
6
 
10
7
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
8
  s.authors = ["H. Wade Minter", "Rackspace Hosting"]
12
- s.date = %q{2011-03-17}
13
9
  s.description = %q{A Ruby version of the Rackspace Cloud Files API.}
14
10
  s.email = %q{minter@lunenburg.org}
15
11
  s.extra_rdoc_files = [
@@ -21,10 +17,10 @@ Gem::Specification.new do |s|
21
17
  "CHANGELOG",
22
18
  "CONTRIBUTORS",
23
19
  "COPYING",
20
+ "Gemfile",
24
21
  "README.rdoc",
25
22
  "Rakefile",
26
23
  "TODO",
27
- "VERSION",
28
24
  "cloudfiles.gemspec",
29
25
  "lib/cloudfiles.rb",
30
26
  "lib/cloudfiles/authentication.rb",
@@ -32,6 +28,7 @@ Gem::Specification.new do |s|
32
28
  "lib/cloudfiles/container.rb",
33
29
  "lib/cloudfiles/exception.rb",
34
30
  "lib/cloudfiles/storage_object.rb",
31
+ "lib/cloudfiles/version.rb",
35
32
  "test/cf-testunit.rb",
36
33
  "test/cloudfiles_authentication_test.rb",
37
34
  "test/cloudfiles_connection_test.rb",
data/lib/cloudfiles.rb CHANGED
@@ -22,7 +22,6 @@ module CloudFiles
22
22
  AUTH_USA = "https://auth.api.rackspacecloud.com/v1.0"
23
23
  AUTH_UK = "https://lon.auth.api.rackspacecloud.com/v1.0"
24
24
 
25
- VERSION = IO.read(File.dirname(__FILE__) + '/../VERSION')
26
25
  require 'net/http'
27
26
  require 'net/https'
28
27
  require 'rexml/document'
@@ -39,6 +38,7 @@ module CloudFiles
39
38
  end
40
39
 
41
40
  $:.unshift(File.dirname(__FILE__))
41
+ require 'cloudfiles/version'
42
42
  require 'cloudfiles/exception'
43
43
  require 'cloudfiles/authentication'
44
44
  require 'cloudfiles/connection'
@@ -22,7 +22,7 @@ module CloudFiles
22
22
  end
23
23
  server.start
24
24
  rescue
25
- raise CloudFiles::Exception::Connection, "Unable to connect to #{server}"
25
+ raise CloudFiles::Exception::Connection, "Unable to connect to #{server.address}"
26
26
  end
27
27
  response = server.get(path, hdrhash)
28
28
  if (response.code =~ /^20./)
@@ -270,10 +270,11 @@ module CloudFiles
270
270
  # This method actually makes the HTTP calls out to the server
271
271
  def cfreq(method, server, path, port, scheme, headers = {}, data = nil, attempts = 0, &block) # :nodoc:
272
272
  start = Time.now
273
- headers['Transfer-Encoding'] = "chunked" if data.is_a?(IO)
273
+ headers['Transfer-Encoding'] = "chunked" if data.respond_to?(:read)
274
274
  hdrhash = headerprep(headers)
275
275
  start_http(server, path, port, scheme, hdrhash)
276
276
  request = Net::HTTP.const_get(method.to_s.capitalize).new(path, hdrhash)
277
+ data.rewind if data.respond_to?(:rewind)
277
278
  if data
278
279
  if data.respond_to?(:read)
279
280
  request.body_stream = data
@@ -291,7 +292,7 @@ module CloudFiles
291
292
  response
292
293
  rescue Errno::EPIPE, Timeout::Error, Errno::EINVAL, EOFError, IOError
293
294
  # Server closed the connection, retry
294
- raise CloudFiles::Exception::Connection, "Unable to reconnect to #{server} after #{count} attempts" if attempts >= 5
295
+ raise CloudFiles::Exception::Connection, "Unable to reconnect to #{server.address} after #{attempts} attempts" if attempts >= 5
295
296
  attempts += 1
296
297
  begin
297
298
  @http[server].finish
@@ -329,7 +330,7 @@ module CloudFiles
329
330
  end
330
331
  @http[server].start
331
332
  rescue
332
- raise CloudFiles::Exception::Connection, "Unable to connect to #{server}"
333
+ raise CloudFiles::Exception::Connection, "Unable to connect to #{server.address}"
333
334
  end
334
335
  end
335
336
  end
@@ -28,9 +28,9 @@ module CloudFiles
28
28
  end
29
29
  # Load the metadata now, so we'll get a CloudFiles::Exception::NoSuchContainer exception should the container
30
30
  # not exist.
31
- self.metadata
31
+ self.container_metadata
32
32
  end
33
-
33
+
34
34
  # Refreshes data about the container and populates class variables. Items are otherwise
35
35
  # loaded in a lazy loaded fashion.
36
36
  #
@@ -49,11 +49,13 @@ module CloudFiles
49
49
  alias :populate :refresh
50
50
 
51
51
  # Retrieves Metadata for the container
52
- def metadata
52
+ def container_metadata
53
53
  @metadata ||= (
54
54
  response = self.connection.cfreq("HEAD", @storagehost, @storagepath + "/", @storageport, @storagescheme)
55
55
  raise CloudFiles::Exception::NoSuchContainer, "Container #{@name} does not exist" unless (response.code =~ /^20/)
56
- {:bytes => response["x-container-bytes-used"].to_i, :count => response["x-container-object-count"].to_i}
56
+ resphash = {}
57
+ response.to_hash.select { |k,v| k.match(/^x-container-meta/) }.each { |x| resphash[x[0]] = x[1].to_s }
58
+ {:bytes => response["x-container-bytes-used"].to_i, :count => response["x-container-object-count"].to_i, :metadata => resphash}
57
59
  )
58
60
  end
59
61
 
@@ -78,7 +80,32 @@ module CloudFiles
78
80
  @cdn_metadata = {}
79
81
  end
80
82
  end
83
+
84
+ # Returns the container's metadata as a nicely formatted hash, stripping off the X-Meta-Object- prefix that the system prepends to the
85
+ # key name.
86
+ #
87
+ # object.metadata
88
+ # => {"ruby"=>"cool", "foo"=>"bar"}
89
+ def metadata
90
+ metahash = {}
91
+ self.container_metadata[:metadata].each{ |key, value| metahash[key.gsub(/x-container-meta-/, '').gsub(/%20/, ' ')] = URI.decode(value).gsub(/\+\-/, ' ') }
92
+ metahash
93
+ end
81
94
 
95
+ # Sets the metadata for an object. By passing a hash as an argument, you can set the metadata for an object.
96
+ # New calls to set metadata are additive. To remove metadata, set the value of the key to nil.
97
+ #
98
+ # Throws NoSuchObjectException if the container doesn't exist. Throws InvalidResponseException if the request
99
+ # fails.
100
+ def set_metadata(metadatahash)
101
+ headers = {}
102
+ metadatahash.each{ |key, value| headers['X-Container-Meta-' + CloudFiles.escape(key.to_s.capitalize)] = value.to_s }
103
+ response = self.connection.cfreq("POST", @storagehost, @storagepath, @storageport, @storagescheme, headers)
104
+ raise CloudFiles::Exception::NoSuchObject, "Container #{@name} does not exist" if (response.code == "404")
105
+ raise CloudFiles::Exception::InvalidResponse, "Invalid response code #{response.code}" unless (response.code =~ /^20/)
106
+ true
107
+ end
108
+
82
109
  # Size of the container (in bytes)
83
110
  def bytes
84
111
  self.metadata[:bytes]
@@ -200,6 +227,8 @@ module CloudFiles
200
227
  # If no objects exist, an empty hash is returned. Throws an InvalidResponseException if the request fails. Takes a
201
228
  # parameter hash as an argument, in the same form as the objects method.
202
229
  #
230
+ # Accepts the same options as objects to limit the returned set.
231
+ #
203
232
  # Returns a hash in the same format as the containers_detail from the CloudFiles class.
204
233
  #
205
234
  # container.objects_detail
@@ -241,7 +270,7 @@ module CloudFiles
241
270
  # full_container.empty?
242
271
  # => false
243
272
  def empty?
244
- return (metadata[:count].to_i == 0)? true : false
273
+ return (container_metadata[:count].to_i == 0)? true : false
245
274
  end
246
275
 
247
276
  # Returns true if object exists and returns false otherwise.
@@ -13,9 +13,6 @@ module CloudFiles
13
13
  # CloudFiles::Exception::NoSuchObject Exception will be raised. If not, an "empty" CloudFiles::StorageObject will be returned, ready for data
14
14
  # via CloudFiles::StorageObject.write
15
15
  def initialize(container, objectname, force_exists = false, make_path = false)
16
- if objectname.match(/\?/)
17
- raise CloudFiles::Exception::Syntax, "Object #{objectname} contains an invalid character in the name (? not allowed)"
18
- end
19
16
  @container = container
20
17
  @containername = container.name
21
18
  @name = objectname
@@ -50,6 +47,7 @@ module CloudFiles
50
47
  resphash = {}
51
48
  response.to_hash.select { |k,v| k.match(/^x-object-meta/) }.each { |x| resphash[x[0]] = x[1].to_s }
52
49
  {
50
+ :manifest => response["x-object-manifest"],
53
51
  :bytes => response["content-length"],
54
52
  :last_modified => Time.parse(response["last-modified"]),
55
53
  :etag => response["etag"],
@@ -78,7 +76,7 @@ module CloudFiles
78
76
  def content_type
79
77
  self.object_metadata[:content_type]
80
78
  end
81
-
79
+
82
80
  # Retrieves the data from an object and stores the data in memory. The data is returned as a string.
83
81
  # Throws a NoSuchObjectException if the object doesn't exist.
84
82
  #
@@ -96,6 +94,7 @@ module CloudFiles
96
94
  raise CloudFiles::Exception::NoSuchObject, "Object #{@name} does not exist" unless (response.code =~ /^20/)
97
95
  response.body
98
96
  end
97
+ alias :read :data
99
98
 
100
99
  # Retrieves the data from an object and returns a stream that must be passed to a block. Throws a
101
100
  # NoSuchObjectException if the object doesn't exist.
@@ -145,6 +144,30 @@ module CloudFiles
145
144
  raise CloudFiles::Exception::InvalidResponse, "Invalid response code #{response.code}" unless (response.code == "202")
146
145
  true
147
146
  end
147
+
148
+
149
+ # Returns the object's manifest.
150
+ #
151
+ # object.manifest
152
+ # => "container/prefix"
153
+ def manifest
154
+ self.object_metadata[:manifest]
155
+ end
156
+
157
+
158
+ # Sets the manifest for an object. By passing a string as an argument, you can set the manifest for an object.
159
+ # However, setting manifest will overwrite any existing manifest for the object.
160
+ #
161
+ # Throws NoSuchObjectException if the object doesn't exist. Throws InvalidResponseException if the request
162
+ # fails.
163
+ def set_manifest(manifest)
164
+ headers = {'X-Object-Manifest' => manifest}
165
+ response = self.container.connection.cfreq("PUT", @storagehost, @storagepath, @storageport, @storagescheme, headers)
166
+ raise CloudFiles::Exception::NoSuchObject, "Object #{@name} does not exist" if (response.code == "404")
167
+ raise CloudFiles::Exception::InvalidResponse, "Invalid response code #{response.code}" unless (response.code =~ /^20/)
168
+ true
169
+ end
170
+
148
171
 
149
172
  # Takes supplied data and writes it to the object, saving it. You can supply an optional hash of headers, including
150
173
  # Content-Type and ETag, that will be applied to the object.
@@ -244,8 +267,13 @@ module CloudFiles
244
267
  #
245
268
  # object.load_from_filename("/tmp/nonexistent.txt")
246
269
  # => Errno::ENOENT: No such file or directory - /tmp/nonexistent.txt
247
- def load_from_filename(filename, headers = {})
270
+ def load_from_filename(filename, headers = {}, check_md5 = false)
248
271
  f = open(filename)
272
+ if check_md5
273
+ require 'digest/md5'
274
+ md5_hash = Digest::MD5.file(filename)
275
+ headers["Etag"] = md5_hash.to_s()
276
+ end
249
277
  self.write(f, headers)
250
278
  f.close
251
279
  true
@@ -0,0 +1,3 @@
1
+ module CloudFiles
2
+ VERSION = '1.4.17'
3
+ end
@@ -220,7 +220,7 @@ class CloudfilesConnectionTest < Test::Unit::TestCase
220
220
  end
221
221
 
222
222
  def test_fetch_nonexistent_container
223
- CloudFiles::Container.any_instance.stubs(:metadata).raises(CloudFiles::Exception::NoSuchContainer)
223
+ CloudFiles::Container.any_instance.stubs(:container_metadata).raises(CloudFiles::Exception::NoSuchContainer)
224
224
  build_net_http_object
225
225
  assert_raise(CloudFiles::Exception::NoSuchContainer) do
226
226
  container = @connection.container('bad_container')
@@ -263,7 +263,8 @@ class CloudfilesContainerTest < Test::Unit::TestCase
263
263
 
264
264
  def build_net_http_object(args={:code => '204' }, cfreq_expectations={})
265
265
  CloudFiles::Container.any_instance.stubs(:populate).returns(true)
266
- CloudFiles::Container.any_instance.stubs(:metadata).returns({})
266
+ CloudFiles::Container.any_instance.stubs(:metadata).returns()
267
+ CloudFiles::Container.any_instance.stubs(:container_metadata).returns({:bytes => 99, :count => 2})
267
268
  connection = stub(:storagehost => 'test.storage.example', :storagepath => '/dummy/path', :storageport => 443, :storagescheme => 'https', :cdnmgmthost => 'cdm.test.example', :cdnmgmtpath => '/dummy/path', :cdnmgmtport => 443, :cdnmgmtscheme => 'https', :cdn_available? => true)
268
269
  args[:response] = {} unless args[:response]
269
270
  response = {'x-cdn-management-url' => 'http://cdn.example.com/path', 'x-storage-url' => 'http://cdn.example.com/storage', 'authtoken' => 'dummy_token', 'last-modified' => Time.now.to_s}.merge(args[:response])
@@ -27,18 +27,6 @@ class CloudfilesStorageObjectTest < Test::Unit::TestCase
27
27
  assert_equal @object.to_s, 'test_object'
28
28
  end
29
29
 
30
- def test_object_creation_with_invalid_name
31
- connection = stub(:storagehost => 'test.storage.example', :storagepath => '/dummy/path', :storageport => 443, :storagescheme => 'https', :cdnmgmthost => 'cdm.test.example', :cdnmgmtpath => '/dummy/path', :cdnmgmtport => 443, :cdnmgmtscheme => 'https', :cdn_available? => true)
32
- response = {'x-container-bytes-used' => '42', 'x-container-object-count' => '5', 'last-modified' => Time.now.to_s}
33
- response.stubs(:code).returns('204')
34
- connection.stubs(:cfreq => response)
35
- container = CloudFiles::Container.new(connection, 'test_container')
36
- assert_raises CloudFiles::Exception::Syntax do
37
- @object = CloudFiles::StorageObject.new(container, 'test_object?')
38
- end
39
- end
40
-
41
-
42
30
  def test_public_url_exists
43
31
  build_net_http_object(:public => true, :name => 'test object')
44
32
  assert_equal @object.public_url, "http://cdn.test.example/test%20object"
@@ -214,6 +202,7 @@ class CloudfilesStorageObjectTest < Test::Unit::TestCase
214
202
  def build_net_http_object(args={:code => '204' })
215
203
  CloudFiles::Container.any_instance.stubs(:metadata).returns({})
216
204
  CloudFiles::Container.any_instance.stubs(:populate).returns(true)
205
+ CloudFiles::Container.any_instance.stubs(:container_metadata).returns({:bytes => 99, :count => 2})
217
206
  connection = stub(:storagehost => 'test.storage.example', :storagepath => '/dummy/path', :storageport => 443, :storagescheme => 'https', :cdnmgmthost => 'cdm.test.example', :cdnmgmtpath => '/dummy/path', :cdnmgmtport => 443, :cdnmgmtscheme => 'https', :cdn_available? => true)
218
207
  args[:response] = {} unless args[:response]
219
208
  response = {'x-cdn-management-url' => 'http://cdn.example.com/path', 'x-storage-url' => 'http://cdn.example.com/storage', 'authtoken' => 'dummy_token', 'last-modified' => Time.now.to_s}.merge(args[:response])
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cloudfiles
3
3
  version: !ruby/object:Gem::Version
4
- hash: 39
4
+ hash: 37
5
5
  prerelease:
6
6
  segments:
7
7
  - 1
8
8
  - 4
9
- - 16
10
- version: 1.4.16
9
+ - 17
10
+ version: 1.4.17
11
11
  platform: ruby
12
12
  authors:
13
13
  - H. Wade Minter
@@ -16,8 +16,7 @@ autorequire:
16
16
  bindir: bin
17
17
  cert_chain: []
18
18
 
19
- date: 2011-03-17 00:00:00 -04:00
20
- default_executable:
19
+ date: 2011-05-27 00:00:00 Z
21
20
  dependencies:
22
21
  - !ruby/object:Gem::Dependency
23
22
  name: mime-types
@@ -64,10 +63,10 @@ files:
64
63
  - CHANGELOG
65
64
  - CONTRIBUTORS
66
65
  - COPYING
66
+ - Gemfile
67
67
  - README.rdoc
68
68
  - Rakefile
69
69
  - TODO
70
- - VERSION
71
70
  - cloudfiles.gemspec
72
71
  - lib/cloudfiles.rb
73
72
  - lib/cloudfiles/authentication.rb
@@ -75,13 +74,13 @@ files:
75
74
  - lib/cloudfiles/container.rb
76
75
  - lib/cloudfiles/exception.rb
77
76
  - lib/cloudfiles/storage_object.rb
77
+ - lib/cloudfiles/version.rb
78
78
  - test/cf-testunit.rb
79
79
  - test/cloudfiles_authentication_test.rb
80
80
  - test/cloudfiles_connection_test.rb
81
81
  - test/cloudfiles_container_test.rb
82
82
  - test/cloudfiles_storage_object_test.rb
83
83
  - test/test_helper.rb
84
- has_rdoc: true
85
84
  homepage: http://www.rackspacecloud.com/cloud_hosting_products/files
86
85
  licenses: []
87
86
 
@@ -111,7 +110,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
111
110
  requirements: []
112
111
 
113
112
  rubyforge_project:
114
- rubygems_version: 1.5.0
113
+ rubygems_version: 1.7.2
115
114
  signing_key:
116
115
  specification_version: 3
117
116
  summary: A Ruby API into Rackspace Cloud Files
data/VERSION DELETED
@@ -1 +0,0 @@
1
- 1.4.16