aws-s3 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. data/COPYING +19 -0
  2. data/INSTALL +35 -0
  3. data/README +529 -0
  4. data/Rakefile +284 -0
  5. data/bin/s3sh +4 -0
  6. data/bin/setup.rb +10 -0
  7. data/lib/aws/s3.rb +64 -0
  8. data/lib/aws/s3/acl.rb +631 -0
  9. data/lib/aws/s3/authentication.rb +218 -0
  10. data/lib/aws/s3/base.rb +232 -0
  11. data/lib/aws/s3/bittorrent.rb +58 -0
  12. data/lib/aws/s3/bucket.rb +323 -0
  13. data/lib/aws/s3/connection.rb +212 -0
  14. data/lib/aws/s3/error.rb +69 -0
  15. data/lib/aws/s3/exceptions.rb +130 -0
  16. data/lib/aws/s3/extensions.rb +186 -0
  17. data/lib/aws/s3/logging.rb +163 -0
  18. data/lib/aws/s3/object.rb +565 -0
  19. data/lib/aws/s3/owner.rb +44 -0
  20. data/lib/aws/s3/parsing.rb +138 -0
  21. data/lib/aws/s3/response.rb +180 -0
  22. data/lib/aws/s3/service.rb +43 -0
  23. data/lib/aws/s3/version.rb +12 -0
  24. data/support/faster-xml-simple/lib/faster_xml_simple.rb +115 -0
  25. data/support/faster-xml-simple/test/regression_test.rb +16 -0
  26. data/support/faster-xml-simple/test/xml_simple_comparison_test.rb +22 -0
  27. data/support/rdoc/code_info.rb +211 -0
  28. data/test/acl_test.rb +243 -0
  29. data/test/authentication_test.rb +96 -0
  30. data/test/base_test.rb +143 -0
  31. data/test/bucket_test.rb +48 -0
  32. data/test/connection_test.rb +120 -0
  33. data/test/error_test.rb +75 -0
  34. data/test/extensions_test.rb +282 -0
  35. data/test/fixtures.rb +89 -0
  36. data/test/fixtures/buckets.yml +102 -0
  37. data/test/fixtures/errors.yml +34 -0
  38. data/test/fixtures/headers.yml +3 -0
  39. data/test/fixtures/logging.yml +15 -0
  40. data/test/fixtures/policies.yml +16 -0
  41. data/test/logging_test.rb +36 -0
  42. data/test/mocks/base.rb +89 -0
  43. data/test/object_test.rb +177 -0
  44. data/test/parsing_test.rb +82 -0
  45. data/test/remote/acl_test.rb +117 -0
  46. data/test/remote/bittorrent_test.rb +45 -0
  47. data/test/remote/bucket_test.rb +127 -0
  48. data/test/remote/logging_test.rb +82 -0
  49. data/test/remote/object_test.rb +267 -0
  50. data/test/remote/test_file.data +0 -0
  51. data/test/remote/test_helper.rb +30 -0
  52. data/test/response_test.rb +70 -0
  53. data/test/service_test.rb +26 -0
  54. data/test/test_helper.rb +82 -0
  55. metadata +125 -0
@@ -0,0 +1,284 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/testtask'
4
+ require 'rake/rdoctask'
5
+ require 'rake/packagetask'
6
+ require 'rake/gempackagetask'
7
+
8
+ require File.dirname(__FILE__) + '/lib/aws/s3'
9
+
10
+ def library_root
11
+ File.dirname(__FILE__)
12
+ end
13
+
14
+ task :default => :test
15
+
16
+ Rake::TestTask.new do |test|
17
+ test.pattern = 'test/*_test.rb'
18
+ test.verbose = true
19
+ end
20
+
21
+ namespace :doc do
22
+ Rake::RDocTask.new do |rdoc|
23
+ rdoc.rdoc_dir = 'doc'
24
+ rdoc.title = "AWS::S3 -- Support for Amazon S3's REST api"
25
+ rdoc.options << '--line-numbers' << '--inline-source'
26
+ rdoc.rdoc_files.include('README')
27
+ rdoc.rdoc_files.include('lib/**/*.rb')
28
+ end
29
+
30
+ task :rdoc => 'doc:readme'
31
+
32
+ task :refresh => :rerdoc do
33
+ system 'open doc/index.html'
34
+ end
35
+
36
+ task :readme do
37
+ require 'support/rdoc/code_info'
38
+ RDoc::CodeInfo.parse('lib/**/*.rb')
39
+
40
+ strip_comments = lambda {|comment| comment.gsub(/^# ?/, '')}
41
+ docs_for = lambda do |location|
42
+ info = RDoc::CodeInfo.for(location)
43
+ raise RuntimeError, "Couldn't find documentation for `#{location}'" unless info
44
+ strip_comments[info.comment]
45
+ end
46
+
47
+ File.open('README', 'w') do |file|
48
+ file.write ERB.new(IO.read('README.erb')).result(binding)
49
+ end
50
+ end
51
+ end
52
+
53
+ namespace :dist do
54
+ spec = Gem::Specification.new do |s|
55
+ s.name = 'aws-s3'
56
+ s.version = Gem::Version.new(AWS::S3::Version)
57
+ s.summary = "Client library for Amazon's Simple Storage Service's REST API"
58
+ s.description = s.summary
59
+ s.email = 'marcel@vernix.org'
60
+ s.author = 'Marcel Molina Jr.'
61
+ s.has_rdoc = true
62
+ s.extra_rdoc_files = %w(README COPYING INSTALL)
63
+ s.homepage = 'http://amazon.rubyforge.org'
64
+ s.rubyforge_project = 'amazon'
65
+ s.files = FileList['Rakefile', 'lib/**/*.rb', 'bin/*', 'support/**/*.rb']
66
+ s.executables << 's3sh'
67
+ s.test_files = Dir['test/**/*']
68
+
69
+ s.add_dependency 'xml-simple'
70
+ s.add_dependency 'builder'
71
+ s.rdoc_options = ['--title', "AWS::S3 -- Support for Amazon S3's REST api",
72
+ '--main', 'README',
73
+ '--line-numbers', '--inline-source']
74
+ end
75
+
76
+ # Regenerate README before packaging
77
+ task :package => 'doc:readme'
78
+ Rake::GemPackageTask.new(spec) do |pkg|
79
+ pkg.need_tar_gz = true
80
+ pkg.package_files.include('{lib,script,test,support}/**/*')
81
+ pkg.package_files.include('README')
82
+ pkg.package_files.include('COPYING')
83
+ pkg.package_files.include('INSTALL')
84
+ pkg.package_files.include('Rakefile')
85
+ end
86
+
87
+ desc 'Install with gems'
88
+ task :install => :repackage do
89
+ sh "sudo gem i pkg/#{spec.name}-#{spec.version}.gem"
90
+ end
91
+
92
+ desc 'Uninstall gem'
93
+ task :uninstall do
94
+ sh "sudo gem uninstall #{spec.name} -x"
95
+ end
96
+
97
+ desc 'Reinstall gem'
98
+ task :reinstall => [:uninstall, :install]
99
+
100
+ task :confirm_release do
101
+ print "Releasing version #{spec.version}. Are you sure you want to proceed? [Yn] "
102
+ abort if STDIN.getc == ?n
103
+ end
104
+
105
+ desc 'Tag release'
106
+ task :tag do
107
+ svn_root = 'svn+ssh://marcel@rubyforge.org/var/svn/amazon/s3'
108
+ sh %(svn cp #{svn_root}/trunk #{svn_root}/tags/rel-#{spec.version} -m "Tag #{spec.name} release #{spec.version}")
109
+ end
110
+
111
+ desc 'Update changelog to include a release marker'
112
+ task :add_release_marker_to_changelog do
113
+ changelog = IO.read('CHANGELOG')
114
+ changelog.sub!(/^trunk:/, "#{spec.version}:")
115
+
116
+ File.open('CHANGELOG', 'w') do |file|
117
+ file.write "trunk:\n\n#{changelog}"
118
+ end
119
+ end
120
+
121
+ task :commit_changelog do
122
+ sh %(svn ci CHANGELOG -m "Bump changelog version marker for release")
123
+ end
124
+
125
+ desc 'Push a release to rubyforge'
126
+ task :release => [:confirm_release, :clean, :add_release_marker_to_changelog, :package, :commit_changelog, :tag] do
127
+ require 'rubyforge'
128
+ package = File.join('pkg', "#{spec.name}-#{spec.version}")
129
+
130
+ rubyforge = RubyForge.new
131
+ rubyforge.login
132
+
133
+ version_already_released = lambda do
134
+ releases = rubyforge.config['rubyforge']['release_ids']
135
+ releases.has_key?(spec.name) && releases[spec.name][spec.version]
136
+ end
137
+
138
+ abort("Release #{spec.version} already exists!") if version_already_released.call
139
+
140
+ if release_id = rubyforge.add_release(spec.rubyforge_project, spec.name, spec.version, "#{package}.tar.gz")
141
+ rubyforge.add_file(spec.rubyforge_project, spec.name, release_id, "#{package}.gem")
142
+ else
143
+ puts 'Release failed!'
144
+ end
145
+ end
146
+
147
+ task :spec do
148
+ puts spec.to_ruby
149
+ end
150
+ end
151
+
152
+ desc 'Check code to test ratio'
153
+ task :stats do
154
+ library_files = FileList["#{library_root}/lib/**/*.rb"]
155
+ test_files = FileList["#{library_root}/test/**/*_test.rb"]
156
+ count_code_lines = Proc.new do |lines|
157
+ lines.inject(0) do |code_lines, line|
158
+ next code_lines if [/^\s*$/, /^\s*#/].any? {|non_code_line| non_code_line === line}
159
+ code_lines + 1
160
+ end
161
+ end
162
+
163
+ count_code_lines_for_files = Proc.new do |files|
164
+ files.inject(0) {|code_lines, file| code_lines + count_code_lines[IO.read(file)]}
165
+ end
166
+
167
+ library_code_lines = count_code_lines_for_files[library_files]
168
+ test_code_lines = count_code_lines_for_files[test_files]
169
+ ratio = Proc.new { sprintf('%.2f', test_code_lines.to_f / library_code_lines)}
170
+
171
+ puts "Code LOC: #{library_code_lines} Test LOC: #{test_code_lines} Code to Test Ratio: 1:#{ratio.call}"
172
+ end
173
+
174
+ namespace :test do
175
+ desc 'Check test coverage'
176
+ task :coverage do
177
+ system("rcov --sort coverage #{File.join(library_root, 'test/*_test.rb')}")
178
+ show_test_coverage_results
179
+ end
180
+
181
+ Rake::TestTask.new(:remote) do |test|
182
+ test.pattern = 'test/remote/*_test.rb'
183
+ test.verbose = true
184
+ end
185
+
186
+ Rake::TestTask.new(:all) do |test|
187
+ test.pattern = 'test/**/*_test.rb'
188
+ test.verbose = true
189
+ end
190
+
191
+ desc 'Check test coverage of full stack remote tests'
192
+ task :full_coverage do
193
+ system("rcov --sort coverage #{File.join(library_root, 'test/remote/*_test.rb')} #{File.join(library_root, 'test/*_test.rb')}")
194
+ show_test_coverage_results
195
+ end
196
+
197
+ def show_test_coverage_results
198
+ system("open #{File.join(library_root, 'coverage/index.html')}") if PLATFORM['darwin']
199
+ end
200
+
201
+ desc 'Remove coverage products'
202
+ task :clobber_coverage do
203
+ rm_r 'coverage' rescue nil
204
+ end
205
+ end
206
+
207
+ namespace :todo do
208
+ class << TODOS = IO.read(File.join(library_root, 'TODO'))
209
+ def items
210
+ split("\n").grep(/^\[\s|X\]/)
211
+ end
212
+
213
+ def completed
214
+ find_items_matching(/^\[X\]/)
215
+ end
216
+
217
+ def uncompleted
218
+ find_items_matching(/^\[\s\]/)
219
+ end
220
+
221
+ def find_items_matching(regexp)
222
+ items.grep(regexp).instance_eval do
223
+ def display
224
+ puts map {|item| "* #{item.sub(/^\[[^\]]\]\s/, '')}"}
225
+ end
226
+ self
227
+ end
228
+ end
229
+ end
230
+
231
+ desc 'Completed todo items'
232
+ task :completed do
233
+ TODOS.completed.display
234
+ end
235
+
236
+ desc 'Incomplete todo items'
237
+ task :uncompleted do
238
+ TODOS.uncompleted.display
239
+ end
240
+ end if File.exists?(File.join(library_root, 'TODO'))
241
+
242
+ namespace :site do
243
+ require 'erb'
244
+ require 'rdoc/markup/simple_markup'
245
+ require 'rdoc/markup/simple_markup/to_html'
246
+
247
+ readme = lambda { IO.read('README')[/^== Getting started\n(.*)/m, 1] }
248
+ ruby_code = /^\s*\n(\s+\S+.*?)\n\s*\n/m
249
+
250
+ readme_to_html = lambda do
251
+ handler = SM::ToHtml.new
252
+ handler.instance_eval do
253
+ require 'syntax'
254
+ require 'syntax/convertors/html'
255
+ def accept_verbatim(am, fragment)
256
+ syntax = Syntax::Convertors::HTML.for_syntax('ruby')
257
+ @res << %(<div class="ruby">#{syntax.convert(fragment.txt, true)}</div>)
258
+ end
259
+ end
260
+ SM::SimpleMarkup.new.convert(readme.call, handler)
261
+ end
262
+
263
+ desc 'Regenerate the public website page'
264
+ task :build => 'doc:readme' do
265
+ File.open('site/public/index.html', 'w') do |file|
266
+ erb_data = {}
267
+ erb_data[:readme] = readme_to_html.call
268
+ file.write ERB.new(IO.read('site/index.erb')).result(binding)
269
+ end
270
+ end
271
+
272
+ task :refresh => :build do
273
+ system 'open site/public/index.html'
274
+ end
275
+
276
+ desc 'Update the live website'
277
+ task :deploy => :build do
278
+ site_files = FileList['site/public/*']
279
+ site_files.delete_if {|file| File.directory?(file)}
280
+ sh %(scp #{site_files.join ' '} marcel@rubyforge.org:/var/www/gforge-projects/amazon/)
281
+ end
282
+ end
283
+
284
+ task :clean => ['dist:clobber_package', 'doc:clobber_rdoc', 'test:clobber_coverage']
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ s3_lib = File.dirname(__FILE__) + '/../lib/aws/s3'
3
+ setup = File.dirname(__FILE__) + '/setup'
4
+ exec "irb -r #{s3_lib} -r #{setup} --simple-prompt"
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+ if ENV['AMAZON_ACCESS_KEY_ID'] && ENV['AMAZON_SECRET_ACCESS_KEY']
3
+ AWS::S3::Base.establish_connection!(
4
+ :access_key_id => ENV['AMAZON_ACCESS_KEY_ID'],
5
+ :secret_access_key => ENV['AMAZON_SECRET_ACCESS_KEY']
6
+ )
7
+ end
8
+
9
+ require File.dirname(__FILE__) + '/../test/fixtures'
10
+ include AWS::S3
@@ -0,0 +1,64 @@
1
+ require 'base64'
2
+ require 'cgi'
3
+ require 'uri'
4
+ require 'openssl'
5
+ require 'digest/sha1'
6
+ require 'net/https'
7
+ require 'time'
8
+ require 'date'
9
+ require 'generator'
10
+
11
+ $:.unshift(File.dirname(__FILE__))
12
+ require 's3/extensions'
13
+ require_library_or_gem 'builder' unless defined? Builder
14
+
15
+ require 's3/base'
16
+ require 's3/version'
17
+ require 's3/parsing'
18
+ require 's3/acl'
19
+ require 's3/logging'
20
+ require 's3/bittorrent'
21
+ require 's3/service'
22
+ require 's3/owner'
23
+ require 's3/bucket'
24
+ require 's3/object'
25
+ require 's3/error'
26
+ require 's3/exceptions'
27
+ require 's3/connection'
28
+ require 's3/authentication'
29
+ require 's3/response'
30
+
31
+ AWS::S3::Base.class_eval do
32
+ include AWS::S3::Connection::Management
33
+ end
34
+
35
+ AWS::S3::Bucket.class_eval do
36
+ include AWS::S3::Logging::Management
37
+ include AWS::S3::ACL::Bucket
38
+ end
39
+
40
+ AWS::S3::S3Object.class_eval do
41
+ include AWS::S3::ACL::S3Object
42
+ include AWS::S3::BitTorrent
43
+ end
44
+
45
+ # XmlSimple is a very convenient way of dealing with the xml needs of this library, but unfortunately
46
+ # it is powered by REXML which is both very slow (even for me who does not privilege performance)
47
+ # and consumes way too much memory (beach balls a MacBookPro with 2GB of memory for 2 minutes while parsing 60KB of xml).
48
+ #
49
+ # So, if libxml is installed, we use the FasterXmlSimple library, that provides most of the functionality of XmlSimple
50
+ # except it uses the xml/libxml library for xml parsing. If libxml isn't installed, we just fall back on
51
+ # XmlSimple.
52
+ AWS::S3::Parsing.parser =
53
+ begin
54
+ # N.B. Push, don't shift
55
+ $:.push(File.join(File.dirname(__FILE__), '..', '..', 'support', 'faster-xml-simple', 'lib'))
56
+ require_library_or_gem 'faster_xml_simple'
57
+ # Older version of libxml aren't stable (bus error when requesting attributes that don't exist) so we
58
+ # have to use a version greater than '0.3.8.2'.
59
+ raise LoadError unless XML::Parser::VERSION > '0.3.8.2'
60
+ FasterXmlSimple
61
+ rescue LoadError
62
+ require_library_or_gem 'xmlsimple' unless defined? XmlSimple
63
+ XmlSimple
64
+ end
@@ -0,0 +1,631 @@
1
+ module AWS
2
+ module S3
3
+ # By default buckets are private. This means that only the owner has access rights to the bucket and its objects.
4
+ # Objects in that bucket inherit the permission of the bucket unless otherwise specified. When an object is private, the owner can
5
+ # generate a signed url that exposes the object to anyone who has that url. Alternatively, buckets and objects can be given other
6
+ # access levels. Several canned access levels are defined:
7
+ #
8
+ # * <tt>:private</tt> - Owner gets FULL_CONTROL. No one else has any access rights. This is the default.
9
+ # * <tt>:public_read</tt> - Owner gets FULL_CONTROL and the anonymous principal is granted READ access. If this policy is used on an object, it can be read from a browser with no authentication.
10
+ # * <tt>:public_read_write</tt> - Owner gets FULL_CONTROL, the anonymous principal is granted READ and WRITE access. This is a useful policy to apply to a bucket, if you intend for any anonymous user to PUT objects into the bucket.
11
+ # * <tt>:authenticated_read</tt> - Owner gets FULL_CONTROL, and any principal authenticated as a registered Amazon S3 user is granted READ access.
12
+ #
13
+ # You can set a canned access level when you create a bucket or an object by using the <tt>:access</tt> option:
14
+ #
15
+ # S3Object.store(
16
+ # 'kiss.jpg',
17
+ # data,
18
+ # 'marcel',
19
+ # :content_type => 'image/jpg', :access => :public_read
20
+ # )
21
+ #
22
+ # Since the image we created is publicly readable, we can access it directly from a browser by going to the corresponding bucket name
23
+ # and specifying the object's key without a special authenticated url:
24
+ #
25
+ # http://s3.amazonaws.com/marcel/kiss.jpg
26
+ #
27
+ # ==== Building costum access policies
28
+ #
29
+ # For both buckets and objects, you can use the <tt>acl</tt> method to see its access control policy:
30
+ #
31
+ # policy = S3Object.acl('kiss.jpg', 'marcel')
32
+ # pp policy.grants
33
+ # [#<AWS::S3::ACL::Grant FULL_CONTROL to noradio>,
34
+ # #<AWS::S3::ACL::Grant READ to AllUsers Group>]
35
+ #
36
+ # Policies are made up of one or more grants which grant a specific permission to some grantee. Here we see the default FULL_CONTROL grant
37
+ # to the owner of this object. There is also READ permission granted to the Allusers Group, which means anyone has read access for the object.
38
+ #
39
+ # Say we wanted to grant access to anyone to read the access policy of this object. The current READ permission only grants them permission to read
40
+ # the object itself (for example, from a browser) but it does not allow them to read the access policy. For that we will need to grant the AllUsers group the READ_ACP permission.
41
+ #
42
+ # First we'll create a new grant object:
43
+ #
44
+ # grant = ACL::Grant.new
45
+ # # => #<AWS::S3::ACL::Grant (permission) to (grantee)>
46
+ # grant.permission = 'READ_ACP'
47
+ #
48
+ # Now we need to indicate who this grant is for. In other words, who the grantee is:
49
+ #
50
+ # grantee = ACL::Grantee.new
51
+ # # => #<AWS::S3::ACL::Grantee (xsi not set yet)>
52
+ #
53
+ # There are three ways to specify a grantee: 1) by their internal amazon id, such as the one returned with an object's Owner,
54
+ # 2) by their Amazon account email address or 3) by specifying a group. As of this writing you can not create custom groups, but
55
+ # Amazon does provide three already: AllUsers, Authenticated and LogDelivery. In this case we want to provide the grant to all users.
56
+ # This effectively means "anyone".
57
+ #
58
+ # grantee.group = 'AllUsers'
59
+ #
60
+ # Now that our grantee is setup, we'll associate it with the grant:
61
+ #
62
+ # grant.grantee = grantee
63
+ # grant
64
+ # # => #<AWS::S3::ACL::Grant READ_ACP to AllUsers Group>
65
+ #
66
+ # Are grant has all the information we need. Now that it's ready, we'll add it on to the object's access control policy's list of grants:
67
+ #
68
+ # policy.grants << grant
69
+ # pp policy.grants
70
+ # [#<AWS::S3::ACL::Grant FULL_CONTROL to noradio>,
71
+ # #<AWS::S3::ACL::Grant READ to AllUsers Group>,
72
+ # #<AWS::S3::ACL::Grant READ_ACP to AllUsers Group>]
73
+ #
74
+ # Now that the policy has the new grant, we reuse the <tt>acl</tt> method to persist the policy change:
75
+ #
76
+ # S3Object.acl('kiss.jpg', 'marcel', policy)
77
+ #
78
+ # If we fetch the object's policy again, we see that the grant has been added:
79
+ #
80
+ # pp S3Object.acl('kiss.jpg', 'marcel').grants
81
+ # [#<AWS::S3::ACL::Grant FULL_CONTROL to noradio>,
82
+ # #<AWS::S3::ACL::Grant READ to AllUsers Group>,
83
+ # #<AWS::S3::ACL::Grant READ_ACP to AllUsers Group>]
84
+ #
85
+ # If we were to access this object's acl url from a browser:
86
+ #
87
+ # http://s3.amazonaws.com/marcel/kiss.jpg?acl
88
+ #
89
+ # we would be shown its access control policy.
90
+ #
91
+ # ==== Pre-prepared grants
92
+ #
93
+ # Alternatively, the ACL::Grant class defines a set of stock grant policies that you can fetch by name. In most cases, you can
94
+ # just use one of these pre-prepared grants rather than building grants by hand. Two of these stock policies are <tt>:public_read</tt>
95
+ # and <tt>:public_read_acp</tt>, which happen to be the two grants that we built by hand above. In this case we could have simply written:
96
+ #
97
+ # policy.grants << ACL::Grant.grant(:public_read)
98
+ # policy.grants << ACL::Grant.grant(:public_read_acp)
99
+ # S3Object.acl('kiss.jpg', 'marcel', policy)
100
+ #
101
+ # The full details can be found in ACL::Policy, ACL::Grant and ACL::Grantee.
102
+ module ACL
103
+ # The ACL::Policy class lets you inspect and modify access controls for buckets and objects.
104
+ # A policy is made up of one or more Grants which specify a permission and a Grantee to whom that permission is granted.
105
+ #
106
+ # Buckets and objects are given a default access policy which contains one grant permitting the owner of the bucket or object
107
+ # FULL_CONTROL over its contents. This means they can read the object, write to the object, as well as read and write its
108
+ # policy.
109
+ #
110
+ # The <tt>acl</tt> method for both buckets and objects returns the policy object for that entity:
111
+ #
112
+ # policy = Bucket.acl('some-bucket')
113
+ #
114
+ # The <tt>grants</tt> method of a policy exposes its grants. You can treat this collection as an array and push new grants onto it:
115
+ #
116
+ # policy.grants << grant
117
+ #
118
+ # Check the documentation for Grant and Grantee for more details on how to create new grants.
119
+ class Policy
120
+ include SelectiveAttributeProxy #:nodoc:
121
+ attr_accessor :owner, :grants
122
+
123
+ def initialize(attributes = {})
124
+ @attributes = attributes
125
+ @grants = [].extend(GrantListExtensions)
126
+ extract_owner! if owner?
127
+ extract_grants! if grants?
128
+ end
129
+
130
+ # The xml representation of the policy.
131
+ def to_xml
132
+ Builder.new(owner, grants).to_s
133
+ end
134
+
135
+ private
136
+
137
+ def owner?
138
+ attributes.has_key?('owner') || !owner.nil?
139
+ end
140
+
141
+ def grants?
142
+ (attributes.has_key?('access_control_list') && attributes['access_control_list']['grant']) || !grants.empty?
143
+ end
144
+
145
+ def extract_owner!
146
+ @owner = Owner.new(attributes.delete('owner'))
147
+ end
148
+
149
+ def extract_grants!
150
+ attributes['access_control_list']['grant'].each do |grant|
151
+ grants << Grant.new(grant)
152
+ end
153
+ end
154
+
155
+ module GrantListExtensions #:nodoc:
156
+ def include?(grant)
157
+ case grant
158
+ when Symbol
159
+ super(ACL::Grant.grant(grant))
160
+ else
161
+ super
162
+ end
163
+ end
164
+
165
+ def delete(grant)
166
+ case grant
167
+ when Symbol
168
+ super(ACL::Grant.grant(grant))
169
+ else
170
+ super
171
+ end
172
+ end
173
+ end
174
+
175
+ class Builder < XmlGenerator #:nodoc:
176
+ attr_reader :owner, :grants
177
+ def initialize(owner, grants)
178
+ @owner = owner
179
+ @grants = grants.uniq # There could be some duplicate grants
180
+ super()
181
+ end
182
+
183
+ def build
184
+ xml.tag!('AccessControlPolicy', 'xmlns' => 'http://s3.amazonaws.com/doc/2006-03-01/') do
185
+ xml.Owner do
186
+ xml.ID owner.id
187
+ xml.DisplayName owner.display_name
188
+ end
189
+
190
+ xml.AccessControlList do
191
+ xml << grants.map {|grant| grant.to_xml}.join("\n")
192
+ end
193
+ end
194
+ end
195
+ end
196
+ end
197
+
198
+ # A Policy is made up of one or more Grant objects. A grant sets a specific permission and grants it to the associated grantee.
199
+ #
200
+ # When creating a new grant to add to a policy, you need only set its permission and then associate with a Grantee.
201
+ #
202
+ # grant = ACL::Grant.new
203
+ # => #<AWS::S3::ACL::Grant (permission) to (grantee)>
204
+ #
205
+ # Here we see that neither the permission nor the grantee have been set. Let's make this grant provide the READ permission.
206
+ #
207
+ # grant.permission = 'READ'
208
+ # grant
209
+ # => #<AWS::S3::ACL::Grant READ to (grantee)>
210
+ #
211
+ # Now let's assume we have a grantee to the AllUsers group already set up. Just associate that grantee with our grant.
212
+ #
213
+ # grant.grantee = all_users_group_grantee
214
+ # grant
215
+ # => #<AWS::S3::ACL::Grant READ to AllUsers Group>
216
+ #
217
+ # And now are grant is complete. It provides READ permission to the AllUsers group, effectively making this object publicly readable
218
+ # without any authorization.
219
+ #
220
+ # Assuming we have some object's policy available in a local variable called <tt>policy</tt>, we can now add this grant onto its
221
+ # collection of grants.
222
+ #
223
+ # policy.grants << grant
224
+ #
225
+ # And then we send the updated policy to the S3 servers.
226
+ #
227
+ # some_s3object.acl(policy)
228
+ class Grant
229
+ include SelectiveAttributeProxy #:nodoc:
230
+ constant :VALID_PERMISSIONS, %w(READ WRITE READ_ACP WRITE_ACP FULL_CONTROL)
231
+ attr_accessor :grantee
232
+
233
+ class << self
234
+ # Returns stock grants with name <tt>type</tt>.
235
+ #
236
+ # public_read_grant = ACL::Grant.grant :public_read
237
+ # => #<AWS::S3::ACL::Grant READ to AllUsers Group>
238
+ #
239
+ # Valid stock grant types are:
240
+ #
241
+ # * <tt>:authenticated_read</tt>
242
+ # * <tt>:authenticated_read_acp</tt>
243
+ # * <tt>:authenticated_write</tt>
244
+ # * <tt>:authenticated_write_acp</tt>
245
+ # * <tt>:logging_read</tt>
246
+ # * <tt>:logging_read_acp</tt>
247
+ # * <tt>:logging_write</tt>
248
+ # * <tt>:logging_write_acp</tt>
249
+ # * <tt>:public_read</tt>
250
+ # * <tt>:public_read_acp</tt>
251
+ # * <tt>:public_write</tt>
252
+ # * <tt>:public_write_acp</tt>
253
+ def grant(type)
254
+ case type
255
+ when *stock_grant_map.keys
256
+ build_stock_grant_for type
257
+ else
258
+ raise ArgumentError, "Unknown grant type `#{type}'"
259
+ end
260
+ end
261
+
262
+ private
263
+ def stock_grant_map
264
+ grant = lambda {|permission, group| {:permission => permission, :group => group}}
265
+ groups = {:public => 'AllUsers', :authenticated => 'Authenticated', :logging => 'LogDelivery'}
266
+ permissions = %w(READ WRITE READ_ACP WRITE_ACP)
267
+ stock_grants = {}
268
+ groups.each do |grant_group_name, group_name|
269
+ permissions.each do |permission|
270
+ stock_grants["#{grant_group_name}_#{permission.downcase}".to_sym] = grant[permission, group_name]
271
+ end
272
+ end
273
+ stock_grants
274
+ end
275
+ memoized :stock_grant_map
276
+
277
+ def build_stock_grant_for(type)
278
+ stock_grant = stock_grant_map[type]
279
+ grant = new do |g|
280
+ g.permission = stock_grant[:permission]
281
+ end
282
+ grant.grantee = Grantee.new do |gr|
283
+ gr.group = stock_grant[:group]
284
+ end
285
+ grant
286
+ end
287
+ end
288
+
289
+ def initialize(attributes = {})
290
+ attributes = {'permission' => nil}.merge(attributes)
291
+ @attributes = attributes
292
+ extract_grantee!
293
+ yield self if block_given?
294
+ end
295
+
296
+ # Set the permission for this grant.
297
+ #
298
+ # grant.permission = 'READ'
299
+ # grant
300
+ # => #<AWS::S3::ACL::Grant READ to (grantee)>
301
+ #
302
+ # If the specified permisison level is not valid, an <tt>InvalidAccessControlLevel</tt> exception will be raised.
303
+ def permission=(permission_level)
304
+ unless self.class.valid_permissions.include?(permission_level)
305
+ raise InvalidAccessControlLevel.new(self.class.valid_permissions, permission_level)
306
+ end
307
+ attributes['permission'] = permission_level
308
+ end
309
+
310
+ # The xml representation of this grant.
311
+ def to_xml
312
+ Builder.new(permission, grantee).to_s
313
+ end
314
+
315
+ def inspect #:nodoc:
316
+ "#<#{self.class}:##{object_id} #{self}>"
317
+ end
318
+
319
+ def to_s #:nodoc:
320
+ [permission || '(permission)', 'to', grantee ? grantee.type_representation : '(grantee)'].join ' '
321
+ end
322
+
323
+ def eql?(grant) #:nodoc:
324
+ # This won't work for an unposted AmazonCustomerByEmail because of the normalization
325
+ # to CanonicalUser but it will work for groups.
326
+ to_s == grant.to_s
327
+ end
328
+ alias_method :==, :eql?
329
+
330
+ def hash #:nodoc:
331
+ to_s.hash
332
+ end
333
+
334
+ private
335
+
336
+ def extract_grantee!
337
+ @grantee = Grantee.new(attributes['grantee']) if attributes['grantee']
338
+ end
339
+
340
+ class Builder < XmlGenerator #:nodoc:
341
+ attr_reader :grantee, :permission
342
+
343
+ def initialize(permission, grantee)
344
+ @permission = permission
345
+ @grantee = grantee
346
+ super()
347
+ end
348
+
349
+ def build
350
+ xml.Grant do
351
+ xml << grantee.to_xml
352
+ xml.Permission permission
353
+ end
354
+ end
355
+ end
356
+ end
357
+
358
+ # Grants bestow a access permission to grantees. Each grant of some access control list Policy is associated with a grantee.
359
+ # There are three ways of specifying a grantee at the time of this writing.
360
+ #
361
+ # * By canonical user - This format uses the <tt>id</tt> of a given Amazon account. The id value for a given account is available in the
362
+ # Owner object of a bucket, object or policy.
363
+ #
364
+ # grantee.id = 'bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1'
365
+ #
366
+ # Often the id will just be fetched from some owner object.
367
+ #
368
+ # grantee.id = some_object.owner.id
369
+ #
370
+ # * By amazon email address - You can specify an email address for any Amazon account. The Amazon account need not be signed up with the S3 service.
371
+ # though it must be unique across the entire Amazon system. This email address is normalized into a canonical user representation once the grant
372
+ # has been sent back up to the S3 servers.
373
+ #
374
+ # grantee.email_address = 'joe@example.org'
375
+ #
376
+ # * By group - As of this writing you can not create custom groups, but Amazon provides three group that you can use. See the documentation for the
377
+ # Grantee.group= method for details.
378
+ #
379
+ # grantee.group = 'Authenticated'
380
+ class Grantee
381
+ include SelectiveAttributeProxy #:nodoc:
382
+
383
+ undef_method :id # Get rid of Object#id
384
+
385
+ def initialize(attributes = {})
386
+ # Set default values for attributes that may not be passed in but we still want the object
387
+ # to respond to
388
+ attributes = {'id' => nil, 'display_name' => nil, 'email_address' => nil, 'uri' => nil}.merge(attributes)
389
+ @attributes = attributes
390
+ extract_type!
391
+ yield self if block_given?
392
+ end
393
+
394
+ # The xml representation of the current grantee object.
395
+ def to_xml
396
+ Builder.new(self).to_s
397
+ end
398
+
399
+ # Returns the type of grantee. Will be one of <tt>CanonicalUser</tt>, <tt>AmazonCustomerByEmail</tt> or <tt>Group</tt>.
400
+ def type
401
+ return attributes['type'] if attributes['type']
402
+
403
+ # Lookups are in order of preference so if, for example, you set the uri but display_name and id are also
404
+ # set, we'd rather go with the canonical representation.
405
+ if display_name && id
406
+ 'CanonicalUser'
407
+ elsif email_address
408
+ 'AmazonCustomerByEmail'
409
+ elsif uri
410
+ 'Group'
411
+ end
412
+ end
413
+
414
+ # Sets the grantee's group by name.
415
+ #
416
+ # grantee.group = 'AllUsers'
417
+ #
418
+ # Currently, valid groups defined by S3 are:
419
+ #
420
+ # * <tt>AllUsers</tt>: This group represents anyone. In other words, an anonymous request.
421
+ # * <tt>Authenticated</tt>: Any authenticated account on the S3 service.
422
+ # * <tt>LogDelivery</tt>: The entity that delivers bucket access logs.
423
+ def group=(group_name)
424
+ section = %w(AllUsers Authenticated).include?(group_name) ? 'global' : 's3'
425
+ self.uri = "http://acs.amazonaws.com/groups/#{section}/#{group_name}"
426
+ end
427
+
428
+ # Returns the grantee's group. If the grantee is not a group, <tt>nil</tt> is returned.
429
+ def group
430
+ return unless uri
431
+ uri[%r([^/]+$)]
432
+ end
433
+
434
+ def type_representation #:nodoc:
435
+ case type
436
+ when 'CanonicalUser' then display_name || id
437
+ when 'AmazonCustomerByEmail' then email_address
438
+ when 'Group' then "#{group} Group"
439
+ end
440
+ end
441
+
442
+ def inspect #:nodoc:
443
+ "#<#{self.class}:##{object_id} #{type_representation || '(xsi not set yet)'}>"
444
+ end
445
+
446
+ private
447
+ def extract_type!
448
+ attributes['type'] = attributes.delete('xsi:type')
449
+ end
450
+
451
+ class Builder < XmlGenerator #:nodoc:
452
+
453
+ def initialize(grantee)
454
+ @grantee = grantee
455
+ super()
456
+ end
457
+
458
+ def build
459
+ xml.tag!('Grantee', attributes) do
460
+ representation
461
+ end
462
+ end
463
+
464
+ private
465
+ attr_reader :grantee
466
+
467
+ def representation
468
+ case grantee.type
469
+ when 'CanonicalUser'
470
+ xml.ID grantee.id
471
+ xml.DisplayName grantee.display_name
472
+ when 'AmazonCustomerByEmail'
473
+ xml.EmailAddress grantee.email_address
474
+ when 'Group'
475
+ xml.URI grantee.uri
476
+ end
477
+ end
478
+
479
+ def attributes
480
+ {'xsi:type' => grantee.type, 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance'}
481
+ end
482
+ end
483
+ end
484
+
485
+ module Bucket
486
+ def self.included(klass) #:nodoc:
487
+ klass.extend(ClassMethods)
488
+ end
489
+
490
+ module ClassMethods
491
+ # The acl method is the single point of entry for reading and writing access control list policies for a given bucket.
492
+ #
493
+ # # Fetch the acl for the 'marcel' bucket
494
+ # policy = Bucket.acl 'marcel'
495
+ #
496
+ # # Modify the policy ...
497
+ # # ...
498
+ #
499
+ # # Send updated policy back to the S3 servers
500
+ # Bucket.acl 'marcel', policy
501
+ def acl(name = nil, policy = nil)
502
+ if name.is_a?(ACL::Policy)
503
+ policy = name
504
+ name = nil
505
+ end
506
+
507
+ path = path(name) << '?acl'
508
+ respond_with ACL::Policy::Response do
509
+ policy ? put(path, {}, policy.to_xml) : ACL::Policy.new(get(path(name) << '?acl').policy)
510
+ end
511
+ end
512
+ end
513
+
514
+ # The acl method returns and updates the acl for a given bucket.
515
+ #
516
+ # # Fetch a bucket
517
+ # bucket = Bucket.find 'marcel'
518
+ #
519
+ # # Add a grant to the bucket's policy
520
+ # bucket.acl.grants << some_grant
521
+ #
522
+ # # Write the changes to the policy
523
+ # bucket.acl(bucket.acl)
524
+ def acl(reload = false)
525
+ policy = reload.is_a?(ACL::Policy) ? reload : nil
526
+ memoize(reload) do
527
+ self.class.acl(name, policy) if policy
528
+ self.class.acl(name)
529
+ end
530
+ end
531
+ end
532
+
533
+ module S3Object
534
+ def self.included(klass) #:nodoc:
535
+ klass.extend(ClassMethods)
536
+ end
537
+
538
+ module ClassMethods
539
+ # The acl method is the single point of entry for reading and writing access control list policies for a given object.
540
+ #
541
+ # # Fetch the acl for the 'kiss.jpg' object in the 'marcel' bucket
542
+ # policy = S3Object.acl 'kiss.jpg', 'marcel'
543
+ #
544
+ # # Modify the policy ...
545
+ # # ...
546
+ #
547
+ # # Send updated policy back to the S3 servers
548
+ # S3Object.acl 'kiss.jpg', 'marcel', policy
549
+ def acl(name, bucket = nil, policy = nil)
550
+ # We're using the second argument as the ACL::Policy
551
+ if bucket.is_a?(ACL::Policy)
552
+ policy = bucket
553
+ bucket = nil
554
+ end
555
+
556
+ bucket = bucket_name(bucket)
557
+ path = path!(bucket, name) << '?acl'
558
+
559
+ respond_with ACL::Policy::Response do
560
+ policy ? put(path, {}, policy.to_xml) : ACL::Policy.new(get(path).policy)
561
+ end
562
+ end
563
+ end
564
+
565
+ # The acl method returns and updates the acl for a given s3 object.
566
+ #
567
+ # # Fetch a the object
568
+ # object = S3Object.find 'kiss.jpg', 'marcel'
569
+ #
570
+ # # Add a grant to the object's
571
+ # object.acl.grants << some_grant
572
+ #
573
+ # # Write the changes to the policy
574
+ # object.acl(object.acl)
575
+ def acl(reload = false)
576
+ policy = reload.is_a?(ACL::Policy) ? reload : nil
577
+ memoize(reload) do
578
+ self.class.acl(key, bucket.name, policy) if policy
579
+ self.class.acl(key, bucket.name)
580
+ end
581
+ end
582
+ end
583
+
584
+ class OptionProcessor #:nodoc:
585
+ attr_reader :options
586
+ class << self
587
+ def process!(options)
588
+ new(options).process!
589
+ end
590
+ end
591
+
592
+ def initialize(options)
593
+ options.to_normalized_options!
594
+ @options = options
595
+ @access_level = extract_access_level
596
+ end
597
+
598
+ def process!
599
+ return unless access_level_specified?
600
+ validate!
601
+ options['x-amz-acl'] = access_level
602
+ end
603
+
604
+ private
605
+ def extract_access_level
606
+ options.delete('access') || options.delete('x-amz-acl')
607
+ end
608
+
609
+ def validate!
610
+ raise InvalidAccessControlLevel.new(valid_levels, access_level) unless valid?
611
+ end
612
+
613
+ def valid?
614
+ valid_levels.include?(access_level)
615
+ end
616
+
617
+ def access_level_specified?
618
+ !@access_level.nil?
619
+ end
620
+
621
+ def valid_levels
622
+ %w(private public-read public-read-write authenticated-read)
623
+ end
624
+
625
+ def access_level
626
+ @normalized_access_level ||= @access_level.to_header
627
+ end
628
+ end
629
+ end
630
+ end
631
+ end