├── .gitignore ├── CHANGELOG ├── COPYING ├── INSTALL ├── README.erb ├── README.rdoc ├── Rakefile ├── TODO ├── bin ├── s3sh └── setup.rb ├── lib └── aws │ ├── s3.rb │ └── s3 │ ├── acl.rb │ ├── authentication.rb │ ├── base.rb │ ├── bittorrent.rb │ ├── bucket.rb │ ├── connection.rb │ ├── error.rb │ ├── exceptions.rb │ ├── extensions.rb │ ├── logging.rb │ ├── object.rb │ ├── owner.rb │ ├── parsing.rb │ ├── response.rb │ ├── service.rb │ └── version.rb ├── site ├── index.erb └── public │ ├── images │ ├── box-and-gem.gif │ └── favicon.ico │ ├── ruby.css │ └── screen.css ├── support ├── faster-xml-simple │ ├── COPYING │ ├── README │ ├── Rakefile │ ├── lib │ │ └── faster_xml_simple.rb │ └── test │ │ ├── fixtures │ │ ├── test-1.rails.yml │ │ ├── test-1.xml │ │ ├── test-1.yml │ │ ├── test-2.rails.yml │ │ ├── test-2.xml │ │ ├── test-2.yml │ │ ├── test-3.rails.yml │ │ ├── test-3.xml │ │ ├── test-3.yml │ │ ├── test-4.rails.yml │ │ ├── test-4.xml │ │ ├── test-4.yml │ │ ├── test-5.rails.yml │ │ ├── test-5.xml │ │ ├── test-5.yml │ │ ├── test-6.rails.yml │ │ ├── test-6.xml │ │ ├── test-6.yml │ │ ├── test-7.rails.yml │ │ ├── test-7.xml │ │ ├── test-7.yml │ │ ├── test-8.rails.yml │ │ ├── test-8.xml │ │ └── test-8.yml │ │ ├── regression_test.rb │ │ ├── test_helper.rb │ │ └── xml_simple_comparison_test.rb └── rdoc │ └── code_info.rb └── test ├── acl_test.rb ├── authentication_test.rb ├── base_test.rb ├── bucket_test.rb ├── connection_test.rb ├── error_test.rb ├── extensions_test.rb ├── fixtures.rb ├── fixtures ├── buckets.yml ├── errors.yml ├── headers.yml ├── logging.yml ├── loglines.yml ├── logs.yml └── policies.yml ├── logging_test.rb ├── mocks └── fake_response.rb ├── object_test.rb ├── parsing_test.rb ├── remote ├── acl_test.rb ├── bittorrent_test.rb ├── bucket_test.rb ├── logging_test.rb ├── object_test.rb ├── test_file.data └── test_helper.rb ├── response_test.rb ├── service_test.rb └── test_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .rake_tasks 2 | site/public/index.html 3 | pkg/ 4 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | head: 2 | 3 | 0.6.2: 4 | 5 | - Apparently need to use custom __method__ in Ruby versions right up to 1.8.7. 6 | 7 | 0.6.1: 8 | 9 | - Use custom __method__ in Ruby versions *prior* to 1.8.7 not *up to* 1.8.7. 10 | 11 | - Rename Kernel#memoize to Kernel#expirable_memoize so that it doesn't conflict with memoize method in ActiveSupport which has an entirely different API and semantics. Reported by [Florian Dütsc (mail@florian-duetsch.de)]. 12 | 13 | 0.6.0: 14 | 15 | - Full 1.9 compatibility (all tests passing against 1.9 & 1.8.6). Thanks to [David (dvdplm@gmail.com), Cyril David (cyx.ucron@gmail.com)] 16 | 17 | 0.5.1: 18 | 19 | - For now just redefine __method__ to take arguments so we don't break 1.8.7 use today 20 | 21 | 0.5.0: 22 | 23 | - Bug #17458 fixed. Normalize the :expires_in option to always be an integer even if the actual object passed in is a proxy to an integer, such as is the case with 2.hours from ActiveSupport which is actually an instance of ActiveSupport::Duration. Reported by [Steve Kickert steve@riverocktech.com] 24 | 25 | - Bug #19158 fixed. Don't prepend leading slash onto bucket name when deleting a bucket with the :force => true option. 26 | 27 | - Bug #17628 fixed. Don't ignore :use_ssl => false in url_for when the connection is established over ssl. Reported by [Tom Fixed (tkat11)] 28 | 29 | - Bug #13052 fixed. Appease some proxies by always including a Content-Length with all requests. Reported by [James Murty (jmurty)] 30 | 31 | - Bug #13756 fixed. Attributes that are false should not raise NoMethodError in Base#method_missing. Fixed by [Scott Patten] 32 | 33 | - Bug #19189 fixed. No longer reference Date::ABBR_MONTHS constant which was removed in Ruby 1.8.6. Reported by [Khurram Virani (kvirani)] 34 | 35 | - Bug #20487 fixed. If a request fails and is retried, only escape the request path the first time. Reported by [anonymous] 36 | 37 | - Replace ad-hoc S3Object.copy method with newly support built in API call. 38 | 39 | - Do not make connections persistent by default. This "feature" causes far more broken pipes than it is worth. Use with caution. 40 | 41 | 0.4.0: 42 | 43 | - Various adjustments to connection handling to try to mitigate exceptions raised from deep within Net::HTTP. 44 | 45 | - Don't coerce numbers that start with a zero because the zero will be lost. If a bucket, for example, has a name like '0815', all operation it will fail. Closes ticket #10089 [reported anonymously]" 46 | 47 | - Add ability to connect through a proxy using the :proxy option when establishing a connection. Suggested by [Simon Horne ] 48 | 49 | - Add :authenticated option to url_for. When passing false, don't generate signature parameters for query string. 50 | 51 | - Make url_for accept custom port settings. [Rich Olson] 52 | 53 | 0.3.0: 54 | 55 | - Ensure content type is eventually set to account for changes made to Net::HTTP in Ruby version 1.8.5. Reported by [David Hanson, Stephen Caudill, Tom Mornini ] 56 | 57 | - Add :persistent option to connections which keeps a persistent connection rather than creating a new one per request, defaulting to true. Based on a patch by [Metalhead ] 58 | 59 | - If we are retrying a request after rescuing one of the retry exceptions, rewind the body if its an IO stream so it starts at the beginning. [Jamis Buck] 60 | 61 | - Ensure that all paths being submitted to S3 are valid utf8. If they are not, we remove the extended characters. Ample help from [Jamis Buck] 62 | 63 | - Wrap logs in Log objects which exposes each line as a Log::Line that has accessors by name for each field. 64 | 65 | - Various performance optimizations for the extensions code. [Roman LE NEGRATE ] 66 | 67 | - Make S3Object.copy more efficient by streaming in both directions in parallel. 68 | 69 | - Open up Net:HTTPGenericRequest to make the chunk size 1 megabyte, up from 1 kilobyte. 70 | 71 | - Add S3Object.exists? 72 | 73 | 0.2.1: 74 | 75 | - When the bucket name argument (for e.g. Bucket.objects) is being used as the option hash, reassign it to the options variable and set the bucket to nil so bucket inference + options works. 76 | 77 | - Don't call CGI.escape on query string parameters in Hash#to_query_string since all paths get passed through URI.escape right before the request is made. Paths were getting double escaped. Bug spotted by [David Hanson] 78 | 79 | - Make s3sh exec irb.bat if on Windows. Bug spotted by [N. Sathish Kumar ] 80 | 81 | - Avoid class_variable_(get|set) since it was only recently added to Ruby. Spotted by [N. Sathish Kumar ] 82 | 83 | - Raise NoSuchKey if S3Object.about requests a key that does not exist. 84 | 85 | - If the response body is an empty string, don't try to parse it as xml. 86 | 87 | - Don't reject every body type save for IO and String at the door when making a request. Suggested by [Alex MacCaw ] 88 | 89 | - Allow dots in bucket names. [Jesse Newland] 90 | 91 | 0.2.0: 92 | 93 | - Infer content type for an object when calling S3Object.store without explicitly passing in the :content_type option. 94 | 95 | 0.1.2: 96 | 97 | - Scrap (overly) fancy generator based version of CoercibleString with a much simpler and clearer case statement. Continuations are really slow and the specific use of the generator was leaking memory. Bug spotted by [Remco van't Veer] 98 | 99 | 0.1.1: 100 | 101 | - Don't add the underscore method to String if it is already defined (like, for example, from ActiveSupport). Bug spotted by [Matt White ] 102 | 103 | 0.1.0: 104 | 105 | - Initial public release 106 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2006-2009 Marcel Molina Jr. 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | # this software and associated documentation files (the "Software"), to deal in the 6 | # Software without restriction, including without limitation the rights to use, 7 | # copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the 8 | # Software, and to permit persons to whom the Software is furnished to do so, 9 | # subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN 18 | # AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 19 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /INSTALL: -------------------------------------------------------------------------------- 1 | == Rubygems 2 | 3 | The easiest way to install aws/s3 is with Rubygems: 4 | 5 | % sudo gem i aws-s3 -ry 6 | 7 | == Directly from svn 8 | 9 | % svn co svn://rubyforge.org/var/svn/amazon/s3/trunk aws 10 | 11 | == As a Rails plugin 12 | 13 | If you want to use aws/s3 with a Rails application, you can export the repository 14 | into your plugins directory and then check it in: 15 | 16 | % cd my-rails-application/vendor/plugins 17 | % svn export svn://rubyforge.org/var/svn/amazon/s3/trunk aws 18 | % svn add aws 19 | 20 | Or you could pull it down with an svn:externals: 21 | 22 | % cd my-rails-application/vendor/plugins 23 | % svn propedit svn:externals . 24 | 25 | Then add the following line, save and exit: 26 | 27 | aws svn://rubyforge.org/var/svn/amazon/s3/trunk 28 | 29 | If you go the svn route, be sure that you have all the dependencies installed. The list of dependencies follow. 30 | 31 | == Dependencies 32 | 33 | AWS::S3 requires Ruby 1.8.4 or greater. 34 | 35 | It also has the following dependencies: 36 | 37 | sudo gem i xml-simple -ry 38 | sudo gem i builder -ry 39 | sudo gem i mime-types -ry 40 | 41 | === XML parsing (xml-simple) 42 | 43 | AWS::S3 depends on XmlSimple (http://xml-simple.rubyforge.org/). When installing aws/s3 with 44 | Rubygems, this dependency will be taken care of for you. Otherwise, installation instructions are listed on the xml-simple 45 | site. 46 | 47 | If your system has the Ruby libxml bindings installed (http://libxml.rubyforge.org/) they will be used instead of REXML (which is what XmlSimple uses). For those concerned with speed and efficiency, it would behoove you to install libxml (instructions here: http://libxml.rubyforge.org/install.html) as it is considerably faster and less expensive than REXML. 48 | 49 | === XML generation (builder) 50 | 51 | AWS::S3 also depends on the Builder library (http://builder.rubyforge.org/ and http://rubyforge.org/projects/builder/). This will also automatically be installed for you when using Rubygems. 52 | 53 | === Content type inference (mime-types) 54 | 55 | AWS::S3 depends on the MIME::Types library (http://mime-types.rubyforge.org/) to infer the content type of an object that does not explicitly specify it. This library will automatically be installed for you when using Rubygems. -------------------------------------------------------------------------------- /README.erb: -------------------------------------------------------------------------------- 1 | = AWS::S3 2 | 3 | <%= docs_for['AWS::S3'] %> 4 | 5 | == AWS::S3 Basics 6 | === The service, buckets and objects 7 | 8 | The three main concepts of S3 are the service, buckets and objects. 9 | 10 | ==== The service 11 | 12 | <%= docs_for['AWS::S3::Service'] %> 13 | 14 | ==== Buckets 15 | 16 | <%= docs_for['AWS::S3::Bucket'] %> 17 | 18 | ==== Objects 19 | 20 | <%= docs_for['AWS::S3::S3Object'] %> 21 | 22 | ==== Streaming uploads 23 | 24 | <%= docs_for['AWS::S3::S3Object::store'] %> 25 | 26 | == Setting the current bucket 27 | ==== Scoping operations to a specific bucket 28 | 29 | <%= docs_for['AWS::S3::Base.set_current_bucket_to'] %> 30 | 31 | == BitTorrent 32 | ==== Another way to download large files 33 | 34 | <%= docs_for['AWS::S3::BitTorrent'] %> 35 | 36 | == Access control 37 | ==== Using canned access control policies 38 | 39 | <%= docs_for['AWS::S3::ACL'] %> 40 | 41 | ==== Accessing private objects from a browser 42 | 43 | <%= docs_for['AWS::S3::S3Object.url_for'] %> 44 | 45 | == Logging 46 | ==== Tracking requests made on a bucket 47 | 48 | <%= docs_for['AWS::S3::Logging'] %> 49 | 50 | == Errors 51 | ==== When things go wrong 52 | 53 | <%= docs_for['AWS::S3::Error'] %> 54 | 55 | ==== Accessing the last request's response 56 | 57 | <%= docs_for['AWS::S3::Service.response'] %> 58 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 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('COPYING') 28 | rdoc.rdoc_files.include('INSTALL') 29 | rdoc.rdoc_files.include('lib/**/*.rb') 30 | end 31 | 32 | task :rdoc => 'doc:readme' 33 | 34 | task :refresh => :rerdoc do 35 | system 'open doc/index.html' 36 | end 37 | 38 | task :readme do 39 | require 'support/rdoc/code_info' 40 | RDoc::CodeInfo.parse('lib/**/*.rb') 41 | 42 | strip_comments = lambda {|comment| comment.gsub(/^# ?/, '')} 43 | docs_for = lambda do |location| 44 | info = RDoc::CodeInfo.for(location) 45 | raise RuntimeError, "Couldn't find documentation for `#{location}'" unless info 46 | strip_comments[info.comment] 47 | end 48 | 49 | open('README', 'w') do |file| 50 | file.write ERB.new(IO.read('README.erb')).result(binding) 51 | end 52 | end 53 | 54 | task :deploy => :rerdoc do 55 | sh %(scp -r doc marcel@rubyforge.org:/var/www/gforge-projects/amazon/) 56 | end 57 | end 58 | 59 | namespace :dist do 60 | spec = Gem::Specification.new do |s| 61 | s.name = 'aws-s3' 62 | s.version = Gem::Version.new(AWS::S3::Version) 63 | s.summary = "Client library for Amazon's Simple Storage Service's REST API" 64 | s.description = s.summary 65 | s.email = 'marcel@vernix.org' 66 | s.author = 'Marcel Molina Jr.' 67 | s.has_rdoc = true 68 | s.extra_rdoc_files = %w(README COPYING INSTALL) 69 | s.homepage = 'http://amazon.rubyforge.org' 70 | s.rubyforge_project = 'amazon' 71 | s.files = FileList['Rakefile', 'lib/**/*.rb', 'bin/*', 'support/**/*.rb'] 72 | s.executables << 's3sh' 73 | s.test_files = Dir['test/**/*'] 74 | 75 | s.add_dependency 'xml-simple' 76 | s.add_dependency 'builder' 77 | s.add_dependency 'mime-types' 78 | s.rdoc_options = ['--title', "AWS::S3 -- Support for Amazon S3's REST api", 79 | '--main', 'README', 80 | '--line-numbers', '--inline-source'] 81 | s.license = "MIT" 82 | end 83 | 84 | # Regenerate README before packaging 85 | task :package => 'doc:readme' 86 | Rake::GemPackageTask.new(spec) do |pkg| 87 | pkg.need_tar_gz = true 88 | pkg.package_files.include('{lib,script,test,support}/**/*') 89 | pkg.package_files.include('README') 90 | pkg.package_files.include('COPYING') 91 | pkg.package_files.include('INSTALL') 92 | pkg.package_files.include('Rakefile') 93 | end 94 | 95 | desc 'Install with gems' 96 | task :install => :repackage do 97 | sh "sudo gem i pkg/#{spec.name}-#{spec.version}.gem" 98 | end 99 | 100 | desc 'Uninstall gem' 101 | task :uninstall do 102 | sh "sudo gem uninstall #{spec.name} -x" 103 | end 104 | 105 | desc 'Reinstall gem' 106 | task :reinstall => [:uninstall, :install] 107 | 108 | task :confirm_release do 109 | print "Releasing version #{spec.version}. Are you sure you want to proceed? [Yn] " 110 | abort if STDIN.getc == ?n 111 | end 112 | 113 | desc 'Tag release' 114 | task :tag do 115 | sh %(git tag -a '#{spec.version}-release' -m 'Tagging #{spec.version} release') 116 | sh 'git push --tags' 117 | end 118 | 119 | desc 'Update changelog to include a release marker' 120 | task :add_release_marker_to_changelog do 121 | changelog = IO.read('CHANGELOG') 122 | changelog.sub!(/^head:/, "#{spec.version}:") 123 | 124 | open('CHANGELOG', 'w') do |file| 125 | file.write "head:\n\n#{changelog}" 126 | end 127 | end 128 | 129 | task :commit_changelog do 130 | sh %(git commit CHANGELOG -m "Bump changelog version marker for release") 131 | sh 'git push' 132 | end 133 | 134 | package_name = lambda {|specification| File.join('pkg', "#{specification.name}-#{specification.version}")} 135 | 136 | desc 'Push a release to rubyforge' 137 | task :release => [:confirm_release, :clean, :add_release_marker_to_changelog, :package, :commit_changelog, :tag] do 138 | require 'rubyforge' 139 | package = package_name[spec] 140 | 141 | rubyforge = RubyForge.new.configure 142 | rubyforge.login 143 | 144 | user_config = rubyforge.userconfig 145 | user_config['release_changes'] = YAML.load_file('CHANGELOG')[spec.version.to_s].join("\n") 146 | 147 | version_already_released = lambda do 148 | releases = rubyforge.autoconfig['release_ids'] 149 | releases.has_key?(spec.name) && releases[spec.name][spec.version.to_s] 150 | end 151 | 152 | abort("Release #{spec.version} already exists!") if version_already_released.call 153 | 154 | begin 155 | rubyforge.add_release(spec.rubyforge_project, spec.name, spec.version.to_s, "#{package}.tar.gz", "#{package}.gem") 156 | puts "Version #{spec.version} released!" 157 | rescue Exception => exception 158 | puts 'Release failed!' 159 | raise 160 | end 161 | end 162 | 163 | desc 'Upload a beta gem' 164 | task :push_beta_gem => [:clobber_package, :package] do 165 | beta_gem = package_name[spec] 166 | sh %(scp #{beta_gem}.gem marcel@rubyforge.org:/var/www/gforge-projects/amazon/beta) 167 | end 168 | 169 | task :spec do 170 | puts spec.to_ruby 171 | end 172 | end 173 | 174 | desc 'Check code to test ratio' 175 | task :stats do 176 | library_files = FileList["#{library_root}/lib/**/*.rb"] 177 | test_files = FileList["#{library_root}/test/**/*_test.rb"] 178 | count_code_lines = Proc.new do |lines| 179 | lines.inject(0) do |code_lines, line| 180 | next code_lines if [/^\s*$/, /^\s*#/].any? {|non_code_line| non_code_line === line} 181 | code_lines + 1 182 | end 183 | end 184 | 185 | count_code_lines_for_files = Proc.new do |files| 186 | files.inject(0) {|code_lines, file| code_lines + count_code_lines[IO.read(file)]} 187 | end 188 | 189 | library_code_lines = count_code_lines_for_files[library_files] 190 | test_code_lines = count_code_lines_for_files[test_files] 191 | ratio = Proc.new { sprintf('%.2f', test_code_lines.to_f / library_code_lines)} 192 | 193 | puts "Code LOC: #{library_code_lines} Test LOC: #{test_code_lines} Code to Test Ratio: 1:#{ratio.call}" 194 | end 195 | 196 | namespace :test do 197 | find_file = lambda do |name| 198 | file_name = lambda {|path| File.join(path, "#{name}.rb")} 199 | root = $:.detect do |path| 200 | File.exist?(file_name[path]) 201 | end 202 | file_name[root] if root 203 | end 204 | 205 | TEST_LOADER = find_file['rake/rake_test_loader'] 206 | multiruby = lambda do |glob| 207 | system 'multiruby', TEST_LOADER, *Dir.glob(glob) 208 | end 209 | 210 | desc 'Check test coverage' 211 | task :coverage do 212 | system("rcov -x Library -x support --sort coverage #{File.join(library_root, 'test/*_test.rb')}") 213 | show_test_coverage_results 214 | end 215 | 216 | Rake::TestTask.new(:remote) do |test| 217 | test.pattern = 'test/remote/*_test.rb' 218 | test.verbose = true 219 | end 220 | 221 | Rake::TestTask.new(:all) do |test| 222 | test.pattern = 'test/**/*_test.rb' 223 | test.verbose = true 224 | end 225 | 226 | desc 'Check test coverage of full stack remote tests' 227 | task :full_coverage do 228 | system("rcov -x Library -x support --sort coverage #{File.join(library_root, 'test/remote/*_test.rb')} #{File.join(library_root, 'test/*_test.rb')}") 229 | show_test_coverage_results 230 | end 231 | 232 | desc 'Run local tests against multiple versions of Ruby' 233 | task :version_audit do 234 | multiruby['test/*_test.rb'] 235 | end 236 | 237 | namespace :version_audit do 238 | desc 'Run remote tests against multiple versions of Ruby' 239 | task :remote do 240 | multiruby['test/remote/*_test.rb'] 241 | end 242 | 243 | desc 'Run all tests against multiple versions of Ruby' 244 | task :all do 245 | multiruby['test/**/*_test.rb'] 246 | end 247 | end 248 | 249 | def show_test_coverage_results 250 | system("open #{File.join(library_root, 'coverage/index.html')}") if PLATFORM['darwin'] 251 | end 252 | 253 | desc 'Remove coverage products' 254 | task :clobber_coverage do 255 | rm_r 'coverage' rescue nil 256 | end 257 | end 258 | 259 | namespace :todo do 260 | class << TODOS = IO.read(File.join(library_root, 'TODO')) 261 | def items 262 | split("\n").grep(/^\[\s|X\]/) 263 | end 264 | 265 | def completed 266 | find_items_matching(/^\[X\]/) 267 | end 268 | 269 | def uncompleted 270 | find_items_matching(/^\[\s\]/) 271 | end 272 | 273 | def find_items_matching(regexp) 274 | items.grep(regexp).instance_eval do 275 | def display 276 | puts map {|item| "* #{item.sub(/^\[[^\]]\]\s/, '')}"} 277 | end 278 | self 279 | end 280 | end 281 | end 282 | 283 | desc 'Completed todo items' 284 | task :completed do 285 | TODOS.completed.display 286 | end 287 | 288 | desc 'Incomplete todo items' 289 | task :uncompleted do 290 | TODOS.uncompleted.display 291 | end 292 | end if File.exists?(File.join(library_root, 'TODO')) 293 | 294 | namespace :site do 295 | require 'erb' 296 | require 'rdoc/markup/simple_markup' 297 | require 'rdoc/markup/simple_markup/to_html' 298 | 299 | readme = lambda { IO.read('README')[/^== Getting started\n(.*)/m, 1] } 300 | 301 | readme_to_html = lambda do 302 | handler = SM::ToHtml.new 303 | handler.instance_eval do 304 | require 'syntax' 305 | require 'syntax/convertors/html' 306 | def accept_verbatim(am, fragment) 307 | syntax = Syntax::Convertors::HTML.for_syntax('ruby') 308 | @res << %(
#{syntax.convert(fragment.txt, true)}
) 309 | end 310 | end 311 | SM::SimpleMarkup.new.convert(readme.call, handler) 312 | end 313 | 314 | desc 'Regenerate the public website page' 315 | task :build => 'doc:readme' do 316 | open('site/public/index.html', 'w') do |file| 317 | erb_data = {} 318 | erb_data[:readme] = readme_to_html.call 319 | file.write ERB.new(IO.read('site/index.erb')).result(binding) 320 | end 321 | end 322 | 323 | task :refresh => :build do 324 | system 'open site/public/index.html' 325 | end 326 | 327 | desc 'Update the live website' 328 | task :deploy => :build do 329 | site_files = FileList['site/public/*'] 330 | site_files.delete_if {|file| File.directory?(file)} 331 | sh %(scp #{site_files.join ' '} marcel@rubyforge.org:/var/www/gforge-projects/amazon/) 332 | end 333 | end 334 | 335 | task :clean => ['dist:clobber_package', 'doc:clobber_rdoc', 'test:clobber_coverage'] 336 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | 0.3.0 2 | 3 | [ ] Alias make alias for establish_connection! that is non-bang 4 | 5 | [ ] Pass filter criteria like :max_keys onto methods like logs_for and logs which return logs. 6 | [ ] Add high level support to custom logging information as documented in the "Adding Custom Information..." here http://docs.amazonwebservices.com/AmazonS3/2006-03-01/LogFormat.html 7 | 8 | [ ] Bucket.delete(:force => true) needs to fetch all objects in the bucket until there are no more, taking into account the max-keys limit of 1000 objects at a time and it needs to do so in a very efficient manner so it can handle very large buckets (using :prefix and :marker) 9 | [ ] Ability to set content_type on S3Object that has not been stored yet 10 | [ ] Allow symbol and abbreviated version of logging options ('target_prefix' => :prefix, 'target_bucket' => :bucket) 11 | [ ] Allow symbol options for grant's constructor ('permission' => :permission) 12 | [ ] Reconsider save method to Policies returned by Bucket and S3Object's acl instance method so you can do some_object.acl.save after modifying it rather than some_object.acl(some_object.acl) 13 | 14 | [X] S3Object.copy and S3Object.move should preserve the acl 15 | [X] Consider opening up Net::HTTPGenericRequest to replace hardcoded chunk_size to something greater than 1k (maybe 500k since the files are presumed to be quite large) 16 | [X] Add S3Object.exists? 17 | [X] See about replacing XmlSimple with libxml if it's installed since XmlSimple can be rather slow (due to wrapping REXML) 18 | [X] Ability to build up the README from internal docs so documentation for various classes and the README can feed from a single source 19 | [X] Bittorrent documentation 20 | [X] Document logging methods 21 | [X] Bittorrent 22 | [X] ACL documentation 23 | [X] Log management ([de]activation & retrieval) 24 | [X] Remote ACL tests 25 | [X] ACL requesting and parsing 26 | [X] ACL updating for already stored objects which merges with existing ACL 27 | -------------------------------------------------------------------------------- /bin/s3sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | s3_lib = File.dirname(__FILE__) + '/../lib/aws/s3' 3 | setup = File.dirname(__FILE__) + '/setup' 4 | irb_name = RUBY_PLATFORM =~ /mswin32/ ? 'irb.bat' : 'irb' 5 | 6 | exec "#{irb_name} -r #{s3_lib} -r #{setup} --simple-prompt" -------------------------------------------------------------------------------- /bin/setup.rb: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /lib/aws/s3.rb: -------------------------------------------------------------------------------- 1 | require 'cgi' 2 | require 'uri' 3 | require 'openssl' 4 | require 'digest/sha1' 5 | require 'net/https' 6 | require 'time' 7 | require 'date' 8 | require 'open-uri' 9 | 10 | $:.unshift(File.dirname(__FILE__)) 11 | require 's3/extensions' 12 | require_library_or_gem 'builder' unless defined? Builder 13 | require_library_or_gem 'mime/types', 'mime-types' unless defined? MIME::Types 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 | module AWS 32 | module S3 33 | UNSAFE_URI = /[^-_.!~*'()a-zA-Z\d;\/?:@&=$,\[\]]/n 34 | 35 | def self.escape_uri(path) 36 | URI.escape(path.to_s, UNSAFE_URI) 37 | end 38 | 39 | def self.escape_uri_component(path) 40 | escaped = escape_uri(path) 41 | escaped.gsub!(/=/, '%3D') 42 | escaped.gsub!(/&/, '%26') 43 | escaped.gsub!(/;/, '%3B') 44 | escaped 45 | end 46 | 47 | Base.class_eval do 48 | include AWS::S3::Connection::Management 49 | end 50 | 51 | Bucket.class_eval do 52 | include AWS::S3::Logging::Management 53 | include AWS::S3::ACL::Bucket 54 | end 55 | 56 | S3Object.class_eval do 57 | include AWS::S3::ACL::S3Object 58 | include AWS::S3::BitTorrent 59 | end 60 | end 61 | end 62 | 63 | 64 | require_library_or_gem 'xmlsimple', 'xml-simple' unless defined? XmlSimple 65 | # If libxml is installed, we use the FasterXmlSimple library, that provides most of the functionality of XmlSimple 66 | # except it uses the xml/libxml library for xml parsing (rather than REXML). If libxml isn't installed, we just fall back on 67 | # XmlSimple. 68 | AWS::S3::Parsing.parser = 69 | begin 70 | require_library_or_gem 'xml/libxml' 71 | # Older version of libxml aren't stable (bus error when requesting attributes that don't exist) so we 72 | # have to use a version greater than '0.3.8.2'. 73 | raise LoadError unless XML::Parser::VERSION > '0.3.8.2' 74 | $:.push(File.join(File.dirname(__FILE__), '..', '..', 'support', 'faster-xml-simple', 'lib')) 75 | require_library_or_gem 'faster_xml_simple' 76 | FasterXmlSimple 77 | rescue LoadError 78 | XmlSimple 79 | end 80 | -------------------------------------------------------------------------------- /lib/aws/s3/authentication.rb: -------------------------------------------------------------------------------- 1 | module AWS 2 | module S3 3 | # All authentication is taken care of for you by the AWS::S3 library. None the less, some details of the two types 4 | # of authentication and when they are used may be of interest to some. 5 | # 6 | # === Header based authentication 7 | # 8 | # Header based authentication is achieved by setting a special Authorization header whose value 9 | # is formatted like so: 10 | # 11 | # "AWS #{access_key_id}:#{encoded_canonical}" 12 | # 13 | # The access_key_id is the public key that is assigned by Amazon for a given account which you use when 14 | # establishing your initial connection. The encoded_canonical is computed according to rules layed out 15 | # by Amazon which we will describe presently. 16 | # 17 | # ==== Generating the encoded canonical string 18 | # 19 | # The "canonical string", generated by the CanonicalString class, is computed by collecting the current request method, 20 | # a set of significant headers of the current request, and the current request path into a string. 21 | # That canonical string is then encrypted with the secret_access_key assigned by Amazon. The resulting encrypted canonical 22 | # string is then base 64 encoded. 23 | # 24 | # === Query string based authentication 25 | # 26 | # When accessing a restricted object from the browser, you can authenticate via the query string, by setting the following parameters: 27 | # 28 | # "AWSAccessKeyId=#{access_key_id}&Expires=#{expires}&Signature=#{encoded_canonical}" 29 | # 30 | # The QueryString class is responsible for generating the appropriate parameters for authentication via the 31 | # query string. 32 | # 33 | # The access_key_id and encoded_canonical are the same as described in the Header based authentication section. 34 | # The expires value dictates for how long the current url is valid (by default, it will expire in 5 minutes). Expiration can be specified 35 | # either by an absolute time (expressed in seconds since the epoch), or in relative time (in number of seconds from now). 36 | # Details of how to customize the expiration of the url are provided in the documentation for the QueryString class. 37 | # 38 | # All requests made by this library use header authentication. When a query string authenticated url is needed, 39 | # the S3Object#url method will include the appropriate query string parameters. 40 | # 41 | # === Full authentication specification 42 | # 43 | # The full specification of the authentication protocol can be found at 44 | # http://docs.amazonwebservices.com/AmazonS3/2006-03-01/RESTAuthentication.html 45 | class Authentication 46 | constant :AMAZON_HEADER_PREFIX, 'x-amz-' 47 | 48 | # Signature is the abstract super class for the Header and QueryString authentication methods. It does the job 49 | # of computing the canonical_string using the CanonicalString class as well as encoding the canonical string. The subclasses 50 | # parameterize these computations and arrange them in a string form appropriate to how they are used, in one case a http request 51 | # header value, and in the other case key/value query string parameter pairs. 52 | class Signature < String #:nodoc: 53 | attr_reader :request, :access_key_id, :secret_access_key, :options 54 | 55 | def initialize(request, access_key_id, secret_access_key, options = {}) 56 | super() 57 | @request, @access_key_id, @secret_access_key = request, access_key_id, secret_access_key 58 | @options = options 59 | end 60 | 61 | private 62 | 63 | def canonical_string 64 | options = {} 65 | options[:expires] = expires if expires? 66 | CanonicalString.new(request, options) 67 | end 68 | memoized :canonical_string 69 | 70 | def encoded_canonical 71 | klass = OpenSSL::Digest.respond_to?(:new) ? OpenSSL::Digest : OpenSSL::Digest::Digest 72 | digest = klass.new('sha1') 73 | b64_hmac = [OpenSSL::HMAC.digest(digest, secret_access_key, canonical_string)].pack("m").strip 74 | url_encode? ? CGI.escape(b64_hmac) : b64_hmac 75 | end 76 | 77 | def url_encode? 78 | !@options[:url_encode].nil? 79 | end 80 | 81 | def expires? 82 | is_a? QueryString 83 | end 84 | 85 | def date 86 | request['date'].to_s.strip.empty? ? Time.now : Time.parse(request['date']) 87 | end 88 | end 89 | 90 | # Provides header authentication by computing the value of the Authorization header. More details about the 91 | # various authentication schemes can be found in the docs for its containing module, Authentication. 92 | class Header < Signature #:nodoc: 93 | def initialize(*args) 94 | super 95 | self << "AWS #{access_key_id}:#{encoded_canonical}" 96 | end 97 | end 98 | 99 | # Provides query string authentication by computing the three authorization parameters: AWSAccessKeyId, Expires and Signature. 100 | # More details about the various authentication schemes can be found in the docs for its containing module, Authentication. 101 | class QueryString < Signature #:nodoc: 102 | constant :DEFAULT_EXPIRY, 300 # 5 minutes 103 | def initialize(*args) 104 | super 105 | options[:url_encode] = true 106 | self << build 107 | end 108 | 109 | private 110 | 111 | # Will return one of three values, in the following order of precedence: 112 | # 113 | # 1) Seconds since the epoch explicitly passed in the +:expires+ option 114 | # 2) The current time in seconds since the epoch plus the number of seconds passed in 115 | # the +:expires_in+ option 116 | # 3) The current time in seconds since the epoch plus the default number of seconds (60 seconds) 117 | def expires 118 | return options[:expires] if options[:expires] 119 | date.to_i + expires_in 120 | end 121 | 122 | def expires_in 123 | options.has_key?(:expires_in) ? Integer(options[:expires_in]) : DEFAULT_EXPIRY 124 | end 125 | 126 | def build 127 | "AWSAccessKeyId=#{access_key_id}&Expires=#{expires}&Signature=#{encoded_canonical}" 128 | end 129 | end 130 | 131 | # The CanonicalString is used to generate an encrypted signature, signed with your secrect access key. It is composed of 132 | # data related to the given request for which it provides authentication. This data includes the request method, request headers, 133 | # and the request path. Both Header and QueryString use it to generate their signature. 134 | class CanonicalString < String #:nodoc: 135 | class << self 136 | def default_headers 137 | %w(content-type content-md5) 138 | end 139 | 140 | def interesting_headers 141 | ['content-md5', 'content-type', 'date', amazon_header_prefix] 142 | end 143 | 144 | def amazon_header_prefix 145 | /^#{AMAZON_HEADER_PREFIX}/io 146 | end 147 | 148 | def query_parameters 149 | %w(acl location logging notification partNumber policy 150 | requestPayment torrent uploadId uploads versionId 151 | versioning versions delete lifecycle tagging cors 152 | response-content-type response-content-language 153 | response-expires response-cache-control 154 | response-content-disposition response-content-encoding) 155 | end 156 | 157 | def query_parameters_for_signature(params) 158 | params.select {|k, v| query_parameters.include?(k)} 159 | end 160 | 161 | def resource_parameters 162 | Set.new %w(acl logging torrent) 163 | end 164 | 165 | memoized :default_headers 166 | memoized :interesting_headers 167 | memoized :query_parameters 168 | memoized :resource_parameters 169 | end 170 | 171 | attr_reader :request, :headers 172 | 173 | def initialize(request, options = {}) 174 | super() 175 | @request = request 176 | @headers = {} 177 | @options = options 178 | # "For non-authenticated or anonymous requests. A NotImplemented error result code will be returned if 179 | # an authenticated (signed) request specifies a Host: header other than 's3.amazonaws.com'" 180 | # (from http://docs.amazonwebservices.com/AmazonS3/2006-03-01/VirtualHosting.html) 181 | request['Host'] = DEFAULT_HOST 182 | build 183 | end 184 | 185 | private 186 | def build 187 | self << "#{request.method}\n" 188 | ensure_date_is_valid 189 | 190 | initialize_headers 191 | set_expiry! 192 | 193 | headers.sort_by {|k, _| k}.each do |key, value| 194 | value = value.to_s.strip 195 | self << (key =~ self.class.amazon_header_prefix ? "#{key}:#{value}" : value) 196 | self << "\n" 197 | end 198 | self << path 199 | end 200 | 201 | def initialize_headers 202 | identify_interesting_headers 203 | set_default_headers 204 | end 205 | 206 | def set_expiry! 207 | self.headers['date'] = @options[:expires] if @options[:expires] 208 | end 209 | 210 | def ensure_date_is_valid 211 | request['Date'] ||= Time.now.httpdate 212 | end 213 | 214 | def identify_interesting_headers 215 | request.each do |key, value| 216 | key = key.downcase # Can't modify frozen string so no bang 217 | if self.class.interesting_headers.any? {|header| header === key} 218 | self.headers[key] = value.to_s.strip 219 | end 220 | end 221 | end 222 | 223 | def set_default_headers 224 | self.class.default_headers.each do |header| 225 | self.headers[header] ||= '' 226 | end 227 | end 228 | 229 | def path 230 | [only_path, extract_significant_parameter].compact.join('?') 231 | end 232 | 233 | def extract_significant_parameter 234 | query = URI.parse(request.path).query 235 | return nil if query.nil? 236 | params = CGI.parse(query) #this automatically unescapes query params 237 | params = self.class.query_parameters_for_signature(params).to_a 238 | return nil if params.empty? 239 | params.sort! { |(x_key, _), (y_key, _)| x_key <=> y_key } 240 | params.map! do |(key, value)| 241 | if value.nil? || resource_parameter?(key) 242 | key 243 | else 244 | value = value.join if value.respond_to?(:join) 245 | "#{key}=#{value}" 246 | end 247 | end.join("&") 248 | end 249 | 250 | def resource_parameter?(key) 251 | self.class.resource_parameters.include? key 252 | end 253 | 254 | def only_path 255 | request.path[/^[^?]*/] 256 | end 257 | end 258 | end 259 | end 260 | end 261 | -------------------------------------------------------------------------------- /lib/aws/s3/base.rb: -------------------------------------------------------------------------------- 1 | module AWS #:nodoc: 2 | # AWS::S3 is a Ruby library for Amazon's Simple Storage Service's REST API (http://aws.amazon.com/s3). 3 | # Full documentation of the currently supported API can be found at http://docs.amazonwebservices.com/AmazonS3/2006-03-01. 4 | # 5 | # == Getting started 6 | # 7 | # To get started you need to require 'aws/s3': 8 | # 9 | # % irb -rubygems 10 | # irb(main):001:0> require 'aws/s3' 11 | # # => true 12 | # 13 | # The AWS::S3 library ships with an interactive shell called s3sh. From within it, you have access to all the operations the library exposes from the command line. 14 | # 15 | # % s3sh 16 | # >> Version 17 | # 18 | # Before you can do anything, you must establish a connection using Base.establish_connection!. A basic connection would look something like this: 19 | # 20 | # AWS::S3::Base.establish_connection!( 21 | # :access_key_id => 'abc', 22 | # :secret_access_key => '123' 23 | # ) 24 | # 25 | # The minimum connection options that you must specify are your access key id and your secret access key. 26 | # 27 | # (If you don't already have your access keys, all you need to sign up for the S3 service is an account at Amazon. You can sign up for S3 and get access keys by visiting http://aws.amazon.com/s3.) 28 | # 29 | # For convenience, if you set two special environment variables with the value of your access keys, the console will automatically create a default connection for you. For example: 30 | # 31 | # % cat .amazon_keys 32 | # export AMAZON_ACCESS_KEY_ID='abcdefghijklmnop' 33 | # export AMAZON_SECRET_ACCESS_KEY='1234567891012345' 34 | # 35 | # Then load it in your shell's rc file. 36 | # 37 | # % cat .zshrc 38 | # if [[ -f "$HOME/.amazon_keys" ]]; then 39 | # source "$HOME/.amazon_keys"; 40 | # fi 41 | # 42 | # See more connection details at AWS::S3::Connection::Management::ClassMethods. 43 | module S3 44 | constant :DEFAULT_HOST, 's3.amazonaws.com' 45 | 46 | # AWS::S3::Base is the abstract super class of all classes who make requests against S3, such as the built in 47 | # Service, Bucket and S3Object classes. It provides methods for making requests, inferring or setting response classes, 48 | # processing request options, and accessing attributes from S3's response data. 49 | # 50 | # Establishing a connection with the Base class is the entry point to using the library: 51 | # 52 | # AWS::S3::Base.establish_connection!(:access_key_id => '...', :secret_access_key => '...') 53 | # 54 | # The :access_key_id and :secret_access_key are the two required connection options. More 55 | # details can be found in the docs for Connection::Management::ClassMethods. 56 | # 57 | # Extensive examples can be found in the README[link:files/README.html]. 58 | class Base 59 | class << self 60 | # Wraps the current connection's request method and picks the appropriate response class to wrap the response in. 61 | # If the response is an error, it will raise that error as an exception. All such exceptions can be caught by rescuing 62 | # their superclass, the ResponseError exception class. 63 | # 64 | # It is unlikely that you would call this method directly. Subclasses of Base have convenience methods for each http request verb 65 | # that wrap calls to request. 66 | def request(verb, path, options = {}, body = nil, attempts = 0, &block) 67 | Service.response = nil 68 | process_options!(options, verb) 69 | response = response_class.new(connection.request(verb, path, options, body, attempts, &block)) 70 | Service.response = response 71 | 72 | Error::Response.new(response.response).error.raise if response.error? 73 | response 74 | # Once in a while, a request to S3 returns an internal error. A glitch in the matrix I presume. Since these 75 | # errors are few and far between the request method will rescue InternalErrors the first three times they encouter them 76 | # and will retry the request again. Most of the time the second attempt will work. 77 | rescue InternalError, RequestTimeout 78 | if attempts == 3 79 | raise 80 | else 81 | attempts += 1 82 | retry 83 | end 84 | end 85 | 86 | [:get, :post, :put, :delete, :head].each do |verb| 87 | class_eval(<<-EVAL, __FILE__, __LINE__) 88 | def #{verb}(path, headers = {}, body = nil, &block) 89 | request(:#{verb}, path, headers, body, &block) 90 | end 91 | EVAL 92 | end 93 | 94 | # Called when a method which requires a bucket name is called without that bucket name specified. It will try to 95 | # infer the current bucket by looking for it as the subdomain of the current connection's address. If no subdomain 96 | # is found, CurrentBucketNotSpecified will be raised. 97 | # 98 | # MusicBucket.establish_connection! :server => 'jukeboxzero.s3.amazonaws.com' 99 | # MusicBucket.connection.server 100 | # => 'jukeboxzero.s3.amazonaws.com' 101 | # MusicBucket.current_bucket 102 | # => 'jukeboxzero' 103 | # 104 | # Rather than infering the current bucket from the subdomain, the current class' bucket can be explicitly set with 105 | # set_current_bucket_to. 106 | def current_bucket 107 | connection.subdomain or raise CurrentBucketNotSpecified.new(connection.http.address) 108 | end 109 | 110 | # If you plan on always using a specific bucket for certain files, you can skip always having to specify the bucket by creating 111 | # a subclass of Bucket or S3Object and telling it what bucket to use: 112 | # 113 | # class JukeBoxSong < AWS::S3::S3Object 114 | # set_current_bucket_to 'jukebox' 115 | # end 116 | # 117 | # For all methods that take a bucket name as an argument, the current bucket will be used if the bucket name argument is omitted. 118 | # 119 | # other_song = 'baby-please-come-home.mp3' 120 | # JukeBoxSong.store(other_song, open(other_song)) 121 | # 122 | # This time we didn't have to explicitly pass in the bucket name, as the JukeBoxSong class knows that it will 123 | # always use the 'jukebox' bucket. 124 | # 125 | # "Astute readers", as they say, may have noticed that we used the third parameter to pass in the content type, 126 | # rather than the fourth parameter as we had the last time we created an object. If the bucket can be inferred, or 127 | # is explicitly set, as we've done in the JukeBoxSong class, then the third argument can be used to pass in 128 | # options. 129 | # 130 | # Now all operations that would have required a bucket name no longer do. 131 | # 132 | # other_song = JukeBoxSong.find('baby-please-come-home.mp3') 133 | def set_current_bucket_to(name) 134 | raise ArgumentError, "`#{__method__}' must be called on a subclass of #{self.name}" if self == AWS::S3::Base 135 | instance_eval(<<-EVAL) 136 | def current_bucket 137 | '#{name}' 138 | end 139 | EVAL 140 | end 141 | alias_method :current_bucket=, :set_current_bucket_to 142 | 143 | private 144 | 145 | def response_class 146 | FindResponseClass.for(self) 147 | end 148 | 149 | def process_options!(options, verb) 150 | options.replace(RequestOptions.process(options, verb)) 151 | end 152 | 153 | # Using the conventions layed out in the response_class works for more than 80% of the time. 154 | # There are a few edge cases though where we want a given class to wrap its responses in different 155 | # response classes depending on which method is being called. 156 | def respond_with(klass) 157 | eval(<<-EVAL, binding, __FILE__, __LINE__) 158 | def new_response_class 159 | #{klass} 160 | end 161 | 162 | class << self 163 | alias_method :old_response_class, :response_class 164 | alias_method :response_class, :new_response_class 165 | end 166 | EVAL 167 | 168 | yield 169 | ensure 170 | # Restore the original version 171 | eval(<<-EVAL, binding, __FILE__, __LINE__) 172 | class << self 173 | alias_method :response_class, :old_response_class 174 | end 175 | EVAL 176 | end 177 | 178 | def bucket_name(name) 179 | name || current_bucket 180 | end 181 | 182 | class RequestOptions < Hash #:nodoc: 183 | attr_reader :options, :verb 184 | 185 | class << self 186 | def process(*args, &block) 187 | new(*args, &block).process! 188 | end 189 | end 190 | 191 | def initialize(options, verb = :get) 192 | @options = options.to_normalized_options 193 | @verb = verb 194 | super() 195 | end 196 | 197 | def process! 198 | set_access_controls! if verb == :put 199 | replace(options) 200 | end 201 | 202 | private 203 | def set_access_controls! 204 | ACL::OptionProcessor.process!(options) 205 | end 206 | end 207 | end 208 | 209 | def initialize(attributes = {}) #:nodoc: 210 | @attributes = attributes 211 | end 212 | 213 | private 214 | attr_reader :attributes 215 | 216 | def connection 217 | self.class.connection 218 | end 219 | 220 | def http 221 | connection.http 222 | end 223 | 224 | def request(*args, &block) 225 | self.class.request(*args, &block) 226 | end 227 | 228 | def method_missing(method, *args, &block) 229 | case 230 | when attributes.has_key?(method.to_s) 231 | attributes[method.to_s] 232 | when attributes.has_key?(method) 233 | attributes[method] 234 | else 235 | super 236 | end 237 | end 238 | end 239 | end 240 | end 241 | -------------------------------------------------------------------------------- /lib/aws/s3/bittorrent.rb: -------------------------------------------------------------------------------- 1 | module AWS 2 | module S3 3 | # Objects on S3 can be distributed via the BitTorrent file sharing protocol. 4 | # 5 | # You can get a torrent file for an object by calling torrent_for: 6 | # 7 | # S3Object.torrent_for 'kiss.jpg', 'marcel' 8 | # 9 | # Or just call the torrent method if you already have the object: 10 | # 11 | # song = S3Object.find 'kiss.jpg', 'marcel' 12 | # song.torrent 13 | # 14 | # Calling grant_torrent_access_to on a object will allow anyone to anonymously 15 | # fetch the torrent file for that object: 16 | # 17 | # S3Object.grant_torrent_access_to 'kiss.jpg', 'marcel' 18 | # 19 | # Anonymous requests to 20 | # 21 | # http://s3.amazonaws.com/marcel/kiss.jpg?torrent 22 | # 23 | # will serve up the torrent file for that object. 24 | module BitTorrent 25 | def self.included(klass) #:nodoc: 26 | klass.extend ClassMethods 27 | end 28 | 29 | # Adds methods to S3Object for accessing the torrent of a given object. 30 | module ClassMethods 31 | # Returns the torrent file for the object with the given key. 32 | def torrent_for(key, bucket = nil) 33 | get(path!(bucket, key) << '?torrent').body 34 | end 35 | alias_method :torrent, :torrent_for 36 | 37 | # Grants access to the object with the given key to be accessible as a torrent. 38 | def grant_torrent_access_to(key, bucket = nil) 39 | policy = acl(key, bucket) 40 | return true if policy.grants.include?(:public_read) 41 | policy.grants << ACL::Grant.grant(:public_read) 42 | acl(key, bucket, policy) 43 | end 44 | alias_method :grant_torrent_access, :grant_torrent_access_to 45 | end 46 | 47 | # Returns the torrent file for the object. 48 | def torrent 49 | self.class.torrent_for(key, bucket.name) 50 | end 51 | 52 | # Grants torrent access publicly to anyone who requests it on this object. 53 | def grant_torrent_access 54 | self.class.grant_torrent_access_to(key, bucket.name) 55 | end 56 | end 57 | end 58 | end -------------------------------------------------------------------------------- /lib/aws/s3/error.rb: -------------------------------------------------------------------------------- 1 | module AWS 2 | module S3 3 | # Anything you do that makes a request to S3 could result in an error. If it does, the AWS::S3 library will raise an exception 4 | # specific to the error. All exception that are raised as a result of a request returning an error response inherit from the 5 | # ResponseError exception. So should you choose to rescue any such exception, you can simple rescue ResponseError. 6 | # 7 | # Say you go to delete a bucket, but the bucket turns out to not be empty. This results in a BucketNotEmpty error (one of the many 8 | # errors listed at http://docs.amazonwebservices.com/AmazonS3/2006-03-01/ErrorCodeList.html): 9 | # 10 | # begin 11 | # Bucket.delete('jukebox') 12 | # rescue ResponseError => error 13 | # # ... 14 | # end 15 | # 16 | # Once you've captured the exception, you can extract the error message from S3, as well as the full error response, which includes 17 | # things like the HTTP response code: 18 | # 19 | # error 20 | # # => # 21 | # error.message 22 | # # => "The bucket you tried to delete is not empty" 23 | # error.response.code 24 | # # => 409 25 | # 26 | # You could use this information to redisplay the error in a way you see fit, or just to log the error and continue on. 27 | class Error 28 | #:stopdoc: 29 | attr_accessor :response 30 | def initialize(error, response = nil) 31 | @error = error 32 | @response = response 33 | @container = AWS::S3 34 | find_or_create_exception! 35 | end 36 | 37 | def raise 38 | Kernel.raise exception.new(message, response) 39 | end 40 | 41 | private 42 | attr_reader :error, :exception, :container 43 | 44 | def find_or_create_exception! 45 | @exception = container.const_defined?(code) ? find_exception : create_exception 46 | end 47 | 48 | def find_exception 49 | exception_class = container.const_get(code) 50 | Kernel.raise ExceptionClassClash.new(exception_class) unless exception_class.ancestors.include?(ResponseError) 51 | exception_class 52 | end 53 | 54 | def create_exception 55 | container.const_set(code, Class.new(ResponseError)) 56 | end 57 | 58 | def method_missing(method, *args, &block) 59 | # We actually want nil if the attribute is nil. So we use has_key? rather than [] + ||. 60 | if error.has_key?(method.to_s) 61 | error[method.to_s] 62 | else 63 | super 64 | end 65 | end 66 | end 67 | end 68 | end 69 | #:startdoc: -------------------------------------------------------------------------------- /lib/aws/s3/exceptions.rb: -------------------------------------------------------------------------------- 1 | module AWS 2 | module S3 3 | 4 | # Abstract super class of all AWS::S3 exceptions 5 | class S3Exception < StandardError 6 | end 7 | 8 | # All responses with a code between 300 and 599 that contain an body are wrapped in an 9 | # ErrorResponse which contains an Error object. This Error class generates a custom exception with the name 10 | # of the xml Error and its message. All such runtime generated exception classes descend from ResponseError 11 | # and contain the ErrorResponse object so that all code that makes a request can rescue ResponseError and get 12 | # access to the ErrorResponse. 13 | class ResponseError < S3Exception 14 | attr_reader :response 15 | def initialize(message, response) 16 | @response = response 17 | super(message) 18 | end 19 | end 20 | 21 | #:stopdoc: 22 | 23 | # Most ResponseError's are created just time on a need to have basis, but we explicitly define the 24 | # InternalError exception because we want to explicitly rescue InternalError in some cases. 25 | class InternalError < ResponseError 26 | end 27 | 28 | class NoSuchKey < ResponseError 29 | end 30 | 31 | class RequestTimeout < ResponseError 32 | end 33 | 34 | # Abstract super class for all invalid options. 35 | class InvalidOption < S3Exception 36 | end 37 | 38 | # Raised if an invalid value is passed to the :access option when creating a Bucket or an S3Object. 39 | class InvalidAccessControlLevel < InvalidOption 40 | def initialize(valid_levels, access_level) 41 | super("Valid access control levels are #{valid_levels.inspect}. You specified `#{access_level}'.") 42 | end 43 | end 44 | 45 | # Raised if either the access key id or secret access key arguments are missing when establishing a connection. 46 | class MissingAccessKey < InvalidOption 47 | def initialize(missing_keys) 48 | key_list = missing_keys.map {|key| key.to_s}.join(' and the ') 49 | super("You did not provide both required access keys. Please provide the #{key_list}.") 50 | end 51 | end 52 | 53 | # Raised if a request is attempted before any connections have been established. 54 | class NoConnectionEstablished < S3Exception 55 | end 56 | 57 | # Raised if an unrecognized option is passed when establishing a connection. 58 | class InvalidConnectionOption < InvalidOption 59 | def initialize(invalid_options) 60 | message = "The following connection options are invalid: #{invalid_options.join(', ')}. " + 61 | "The valid connection options are: #{Connection::Options::VALID_OPTIONS.join(', ')}." 62 | super(message) 63 | end 64 | end 65 | 66 | # Raised if an invalid bucket name is passed when creating a new Bucket. 67 | class InvalidBucketName < S3Exception 68 | def initialize(invalid_name) 69 | message = "`#{invalid_name}' is not a valid bucket name. " + 70 | "Bucket names must be between 3 and 255 bytes and " + 71 | "can contain letters, numbers, dashes and underscores." 72 | super(message) 73 | end 74 | end 75 | 76 | # Raised if an invalid key name is passed when creating an S3Object. 77 | class InvalidKeyName < S3Exception 78 | def initialize(invalid_name) 79 | message = "`#{invalid_name}' is not a valid key name. " + 80 | "Key names must be no more than 1024 bytes long." 81 | super(message) 82 | end 83 | end 84 | 85 | # Raised if an invalid value is assigned to an S3Object's specific metadata name. 86 | class InvalidMetadataValue < S3Exception 87 | def initialize(invalid_names) 88 | message = "The following metadata names have invalid values: #{invalid_names.join(', ')}. " + 89 | "Metadata can not be larger than 2kilobytes." 90 | super(message) 91 | end 92 | end 93 | 94 | # Raised if the current bucket can not be inferred when not explicitly specifying the target bucket in the calling 95 | # method's arguments. 96 | class CurrentBucketNotSpecified < S3Exception 97 | def initialize(address) 98 | message = "No bucket name can be inferred from your current connection's address (`#{address}')" 99 | super(message) 100 | end 101 | end 102 | 103 | # Raised when an orphaned S3Object belonging to no bucket tries to access its (non-existant) bucket. 104 | class NoBucketSpecified < S3Exception 105 | def initialize 106 | super('The current object must have its bucket set') 107 | end 108 | end 109 | 110 | # Raised if an attempt is made to save an S3Object that does not have a key set. 111 | class NoKeySpecified < S3Exception 112 | def initialize 113 | super('The current object must have its key set') 114 | end 115 | end 116 | 117 | # Raised if you try to save a deleted object. 118 | class DeletedObject < S3Exception 119 | def initialize 120 | super('You can not save a deleted object') 121 | end 122 | end 123 | 124 | class ExceptionClassClash < S3Exception #:nodoc: 125 | def initialize(klass) 126 | message = "The exception class you tried to create (`#{klass}') exists and is not an exception" 127 | super(message) 128 | end 129 | end 130 | 131 | #:startdoc: 132 | end 133 | end -------------------------------------------------------------------------------- /lib/aws/s3/extensions.rb: -------------------------------------------------------------------------------- 1 | #encoding: BINARY 2 | #:stopdoc: 3 | 4 | class Hash 5 | def to_query_string(include_question_mark = true) 6 | query_string = '' 7 | unless empty? 8 | query_string << '?' if include_question_mark 9 | query_string << inject([]) do |params, (key, value)| 10 | params << "#{key}=#{AWS::S3.escape_uri_component(value)}" 11 | end.join('&') 12 | end 13 | query_string 14 | end 15 | 16 | def to_normalized_options 17 | # Convert all option names to downcased strings, and replace underscores with hyphens 18 | inject({}) do |normalized_options, (name, value)| 19 | normalized_options[name.to_header] = value.to_s 20 | normalized_options 21 | end 22 | end 23 | 24 | def to_normalized_options! 25 | replace(to_normalized_options) 26 | end 27 | end 28 | 29 | class String 30 | if RUBY_VERSION <= '1.9' 31 | def previous! 32 | self[-1] -= 1 33 | self 34 | end 35 | else 36 | def previous! 37 | self[-1] = (self[-1].ord - 1).chr 38 | self 39 | end 40 | end 41 | 42 | def tap 43 | yield(self) 44 | self 45 | end unless ''.respond_to?(:tap) 46 | 47 | def previous 48 | dup.previous! 49 | end 50 | 51 | def to_header 52 | downcase.tr('_', '-') 53 | end 54 | 55 | # ActiveSupport adds an underscore method to String so let's just use that one if 56 | # we find that the method is already defined 57 | def underscore 58 | gsub(/::/, '/'). 59 | gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2'). 60 | gsub(/([a-z\d])([A-Z])/,'\1_\2'). 61 | tr("-", "_").downcase 62 | end unless public_method_defined? :underscore 63 | 64 | if RUBY_VERSION >= '1.9' 65 | def valid_utf8? 66 | dup.force_encoding('UTF-8').valid_encoding? 67 | end 68 | else 69 | def valid_utf8? 70 | scan(Regexp.new('[^\x00-\xa0]', nil, 'u')) { |s| s.unpack('U') } 71 | true 72 | rescue ArgumentError 73 | false 74 | end 75 | end 76 | 77 | # All paths in in S3 have to be valid unicode so this takes care of 78 | # cleaning up any strings that aren't valid utf-8 according to String#valid_utf8? 79 | if RUBY_VERSION >= '1.9' 80 | def remove_extended! 81 | sanitized_string = '' 82 | each_byte do |byte| 83 | character = byte.chr 84 | sanitized_string << character if character.ascii_only? 85 | end 86 | sanitized_string 87 | end 88 | else 89 | def remove_extended! 90 | gsub!(/[\x80-\xFF]/) { "%02X" % $&[0] } 91 | end 92 | end 93 | 94 | def remove_extended 95 | dup.remove_extended! 96 | end 97 | end 98 | 99 | class CoercibleString < String 100 | class << self 101 | def coerce(string) 102 | new(string).coerce 103 | end 104 | end 105 | 106 | def coerce 107 | case self 108 | when 'true'; true 109 | when 'false'; false 110 | # Don't coerce numbers that start with zero 111 | when /^[1-9]+\d*$/; Integer(self) 112 | when datetime_format; Time.parse(self) 113 | else 114 | self 115 | end 116 | end 117 | 118 | private 119 | # Lame hack since Date._parse is so accepting. S3 dates are of the form: '2006-10-29T23:14:47.000Z' 120 | # so unless the string looks like that, don't even try, otherwise it might convert an object's 121 | # key from something like '03 1-2-3-Apple-Tree.mp3' to Sat Feb 03 00:00:00 CST 2001. 122 | def datetime_format 123 | /^\d{4}-\d{2}-\d{2}\w\d{2}:\d{2}:\d{2}/ 124 | end 125 | end 126 | 127 | class Symbol 128 | def to_header 129 | to_s.to_header 130 | end 131 | end 132 | 133 | module Kernel 134 | def __method__(depth = 0) 135 | caller[depth][/`([^']+)'/, 1] 136 | end if RUBY_VERSION <= '1.8.7' 137 | 138 | def __called_from__ 139 | caller[1][/`([^']+)'/, 1] 140 | end if RUBY_VERSION > '1.8.7' 141 | 142 | def expirable_memoize(reload = false, storage = nil) 143 | current_method = RUBY_VERSION > '1.8.7' ? __called_from__ : __method__(1) 144 | storage = "@#{storage || current_method}" 145 | if reload 146 | instance_variable_set(storage, nil) 147 | else 148 | if cache = instance_variable_get(storage) 149 | return cache 150 | end 151 | end 152 | instance_variable_set(storage, yield) 153 | end 154 | 155 | def require_library_or_gem(library, gem_name = nil) 156 | if RUBY_VERSION >= '1.9' 157 | gem(gem_name || library, '>=0') 158 | end 159 | require library 160 | rescue LoadError => library_not_installed 161 | begin 162 | require 'rubygems' 163 | require library 164 | rescue LoadError 165 | raise library_not_installed 166 | end 167 | end 168 | end 169 | 170 | class Object 171 | def returning(value) 172 | yield(value) 173 | value 174 | end 175 | end 176 | 177 | class Module 178 | def memoized(method_name) 179 | original_method = "unmemoized_#{method_name}_#{Time.now.to_i}" 180 | alias_method original_method, method_name 181 | module_eval(<<-EVAL, __FILE__, __LINE__) 182 | def #{method_name}(reload = false, *args, &block) 183 | expirable_memoize(reload) do 184 | send(:#{original_method}, *args, &block) 185 | end 186 | end 187 | EVAL 188 | end 189 | 190 | def constant(name, value) 191 | unless const_defined?(name) 192 | const_set(name, value) 193 | module_eval(<<-EVAL, __FILE__, __LINE__) 194 | def self.#{name.to_s.downcase} 195 | #{name.to_s} 196 | end 197 | EVAL 198 | end 199 | end 200 | end 201 | 202 | class Class # :nodoc: 203 | def cattr_reader(*syms) 204 | syms.flatten.each do |sym| 205 | class_eval(<<-EOS, __FILE__, __LINE__) 206 | unless defined? @@#{sym} 207 | @@#{sym} = nil 208 | end 209 | 210 | def self.#{sym} 211 | @@#{sym} 212 | end 213 | 214 | def #{sym} 215 | @@#{sym} 216 | end 217 | EOS 218 | end 219 | end 220 | 221 | def cattr_writer(*syms) 222 | syms.flatten.each do |sym| 223 | class_eval(<<-EOS, __FILE__, __LINE__) 224 | unless defined? @@#{sym} 225 | @@#{sym} = nil 226 | end 227 | 228 | def self.#{sym}=(obj) 229 | @@#{sym} = obj 230 | end 231 | 232 | def #{sym}=(obj) 233 | @@#{sym} = obj 234 | end 235 | EOS 236 | end 237 | end 238 | 239 | def cattr_accessor(*syms) 240 | cattr_reader(*syms) 241 | cattr_writer(*syms) 242 | end 243 | end if Class.instance_methods(false).grep(/^cattr_(?:reader|writer|accessor)$/).empty? 244 | 245 | module SelectiveAttributeProxy 246 | def self.included(klass) 247 | klass.extend(ClassMethods) 248 | klass.class_eval(<<-EVAL, __FILE__, __LINE__) 249 | cattr_accessor :attribute_proxy 250 | cattr_accessor :attribute_proxy_options 251 | 252 | # Default name for attribute storage 253 | self.attribute_proxy = :attributes 254 | self.attribute_proxy_options = {:exclusively => true} 255 | 256 | private 257 | # By default proxy all attributes 258 | def proxiable_attribute?(name) 259 | return true unless self.class.attribute_proxy_options[:exclusively] 260 | send(self.class.attribute_proxy).has_key?(name) 261 | end 262 | 263 | def method_missing(method, *args, &block) 264 | # Autovivify attribute storage 265 | if method == self.class.attribute_proxy 266 | ivar = "@\#{method}" 267 | instance_variable_set(ivar, {}) unless instance_variable_get(ivar).is_a?(Hash) 268 | instance_variable_get(ivar) 269 | # Delegate to attribute storage 270 | elsif method.to_s =~ /^(\\w+)(=?)$/ && proxiable_attribute?($1) 271 | attributes_hash_name = self.class.attribute_proxy 272 | $2.empty? ? send(attributes_hash_name)[$1] : send(attributes_hash_name)[$1] = args.first 273 | else 274 | super 275 | end 276 | end 277 | EVAL 278 | end 279 | 280 | module ClassMethods 281 | def proxy_to(attribute_name, options = {}) 282 | if attribute_name.is_a?(Hash) 283 | options = attribute_name 284 | else 285 | self.attribute_proxy = attribute_name 286 | end 287 | self.attribute_proxy_options = options 288 | end 289 | end 290 | end 291 | 292 | # When streaming data up, Net::HTTPGenericRequest hard codes a chunk size of 1k. For large files this 293 | # is an unfortunately low chunk size, so here we make it use a much larger default size and move it into a method 294 | # so that the implementation of send_request_with_body_stream doesn't need to be changed to change the chunk size (at least not anymore 295 | # than I've already had to...). 296 | module Net 297 | class HTTPGenericRequest 298 | def send_request_with_body_stream(sock, ver, path, f) 299 | raise ArgumentError, "Content-Length not given and Transfer-Encoding is not `chunked'" unless content_length() or chunked? 300 | unless content_type() 301 | warn 'net/http: warning: Content-Type did not set; using application/x-www-form-urlencoded' if $VERBOSE 302 | set_content_type 'application/x-www-form-urlencoded' 303 | end 304 | write_header sock, ver, path 305 | if chunked? 306 | while s = f.read(chunk_size) 307 | sock.write(sprintf("%x\r\n", s.length) << s << "\r\n") 308 | end 309 | sock.write "0\r\n\r\n" 310 | else 311 | while s = f.read(chunk_size) 312 | sock.write s 313 | end 314 | end 315 | end 316 | 317 | def chunk_size 318 | 1048576 # 1 megabyte 319 | end 320 | end 321 | 322 | # Net::HTTP before 1.8.4 doesn't have the use_ssl? method or the Delete request type 323 | class HTTP 324 | def use_ssl? 325 | @use_ssl 326 | end unless public_method_defined? :use_ssl? 327 | 328 | class Delete < HTTPRequest 329 | METHOD = 'DELETE' 330 | REQUEST_HAS_BODY = false 331 | RESPONSE_HAS_BODY = true 332 | end unless const_defined? :Delete 333 | end 334 | end 335 | 336 | class XmlGenerator < String #:nodoc: 337 | attr_reader :xml 338 | def initialize 339 | @xml = Builder::XmlMarkup.new(:indent => 2, :target => self) 340 | super() 341 | build 342 | end 343 | end 344 | #:startdoc: 345 | -------------------------------------------------------------------------------- /lib/aws/s3/owner.rb: -------------------------------------------------------------------------------- 1 | module AWS 2 | module S3 3 | # Entities in S3 have an associated owner (the person who created them). The owner is a canonical representation of an 4 | # entity in the S3 system. It has an id and a display_name. 5 | # 6 | # These attributes can be used when specifying a ACL::Grantee for an ACL::Grant. 7 | # 8 | # You can retrieve the owner of the current account by calling Owner.current. 9 | class Owner 10 | undef_method :id if method_defined?(:id) # Get rid of Object#id 11 | include SelectiveAttributeProxy 12 | 13 | class << self 14 | # The owner of the current account. 15 | def current 16 | response = Service.get('/') 17 | new(response.parsed['owner']) if response.parsed['owner'] 18 | end 19 | memoized :current 20 | end 21 | 22 | def initialize(attributes = {}) #:nodoc: 23 | @attributes = attributes 24 | end 25 | 26 | def ==(other_owner) #:nodoc: 27 | hash == other_owner.hash 28 | end 29 | 30 | def hash #:nodoc 31 | [id, display_name].join.hash 32 | end 33 | 34 | private 35 | def proxiable_attribute?(name) 36 | valid_attributes.include?(name) 37 | end 38 | 39 | def valid_attributes 40 | %w(id display_name) 41 | end 42 | end 43 | end 44 | end -------------------------------------------------------------------------------- /lib/aws/s3/parsing.rb: -------------------------------------------------------------------------------- 1 | #:stopdoc: 2 | module AWS 3 | module S3 4 | module Parsing 5 | class << self 6 | def parser=(parsing_library) 7 | XmlParser.parsing_library = parsing_library 8 | end 9 | 10 | def parser 11 | XmlParser.parsing_library 12 | end 13 | end 14 | 15 | module Typecasting 16 | def typecast(object) 17 | case object 18 | when Hash 19 | typecast_hash(object) 20 | when Array 21 | object.map {|element| typecast(element)} 22 | when String 23 | CoercibleString.coerce(object) 24 | else 25 | object 26 | end 27 | end 28 | 29 | def typecast_hash(hash) 30 | if content = hash['__content__'] 31 | typecast(content) 32 | else 33 | keys = hash.keys.map {|key| key.underscore} 34 | values = hash.values.map {|value| typecast(value)} 35 | keys.inject({}) do |new_hash, key| 36 | new_hash[key] = values.slice!(0) 37 | new_hash 38 | end 39 | end 40 | end 41 | end 42 | 43 | class XmlParser < Hash 44 | include Typecasting 45 | 46 | class << self 47 | attr_accessor :parsing_library 48 | end 49 | 50 | attr_reader :body, :xml_in, :root 51 | 52 | def initialize(body) 53 | @body = body 54 | unless body.strip.empty? 55 | parse 56 | set_root 57 | typecast_xml_in 58 | end 59 | end 60 | 61 | private 62 | 63 | def parse 64 | @xml_in = self.class.parsing_library.xml_in(body, parsing_options) 65 | end 66 | 67 | def parsing_options 68 | { 69 | # Includes the enclosing tag as the top level key 70 | 'keeproot' => true, 71 | # Makes tag value available via the '__content__' key 72 | 'contentkey' => '__content__', 73 | # Always parse tags into a hash, even when there are no attributes 74 | # (unless there is also no value, in which case it is nil) 75 | 'forcecontent' => true, 76 | # If a tag is empty, makes its content nil 77 | 'suppressempty' => nil, 78 | # Force nested elements to be put into an array, even if there is only one of them 79 | 'forcearray' => ['Contents', 'Bucket', 'Grant'] 80 | } 81 | end 82 | 83 | def set_root 84 | @root = @xml_in.keys.first.underscore 85 | end 86 | 87 | def typecast_xml_in 88 | typecast_xml = {} 89 | @xml_in.dup.each do |key, value| # Some typecasting is destructive so we dup 90 | typecast_xml[key.underscore] = typecast(value) 91 | end 92 | # An empty body will try to update with a string so only update if the result is a hash 93 | update(typecast_xml[root]) if typecast_xml[root].is_a?(Hash) 94 | end 95 | end 96 | end 97 | end 98 | end 99 | #:startdoc: -------------------------------------------------------------------------------- /lib/aws/s3/response.rb: -------------------------------------------------------------------------------- 1 | #:stopdoc: 2 | module AWS 3 | module S3 4 | class Base 5 | class Response < String 6 | attr_reader :response, :body, :parsed 7 | def initialize(response) 8 | @response = response 9 | @body = response.body.to_s 10 | super(body) 11 | end 12 | 13 | def headers 14 | headers = {} 15 | response.each do |header, value| 16 | headers[header] = value 17 | end 18 | headers 19 | end 20 | memoized :headers 21 | 22 | def [](header) 23 | headers[header] 24 | end 25 | 26 | def each(&block) 27 | headers.each(&block) 28 | end 29 | 30 | def code 31 | response.code.to_i 32 | end 33 | 34 | {:success => 200..299, :redirect => 300..399, 35 | :client_error => 400..499, :server_error => 500..599}.each do |result, code_range| 36 | class_eval(<<-EVAL, __FILE__, __LINE__) 37 | def #{result}? 38 | return false unless response 39 | (#{code_range}).include? code 40 | end 41 | EVAL 42 | end 43 | 44 | def error? 45 | !success? && response['content-type'] == 'application/xml' && parsed.root == 'error' 46 | end 47 | 48 | def error 49 | Error.new(parsed, self) 50 | end 51 | memoized :error 52 | 53 | def parsed 54 | # XmlSimple is picky about what kind of object it parses, so we pass in body rather than self 55 | Parsing::XmlParser.new(body) 56 | end 57 | memoized :parsed 58 | 59 | def inspect 60 | "#<%s:0x%s %s %s>" % [self.class, object_id, response.code, response.message] 61 | end 62 | end 63 | end 64 | 65 | class Bucket 66 | class Response < Base::Response 67 | def bucket 68 | parsed 69 | end 70 | end 71 | end 72 | 73 | class S3Object 74 | class Response < Base::Response 75 | def etag 76 | headers['etag'][1...-1] 77 | end 78 | end 79 | end 80 | 81 | class Service 82 | class Response < Base::Response 83 | def empty? 84 | parsed['buckets'].nil? 85 | end 86 | 87 | def buckets 88 | parsed['buckets']['bucket'] || [] 89 | end 90 | end 91 | end 92 | 93 | module ACL 94 | class Policy 95 | class Response < Base::Response 96 | alias_method :policy, :parsed 97 | end 98 | end 99 | end 100 | 101 | # Requests whose response code is between 300 and 599 and contain an in their body 102 | # are wrapped in an Error::Response. This Error::Response contains an Error object which raises an exception 103 | # that corresponds to the error in the response body. The exception object contains the ErrorResponse, so 104 | # in all cases where a request happens, you can rescue ResponseError and have access to the ErrorResponse and 105 | # its Error object which contains information about the ResponseError. 106 | # 107 | # begin 108 | # Bucket.create(..) 109 | # rescue ResponseError => exception 110 | # exception.response 111 | # # => 112 | # exception.response.error 113 | # # => 114 | # end 115 | class Error 116 | class Response < Base::Response 117 | def error? 118 | true 119 | end 120 | 121 | def inspect 122 | "#<%s:0x%s %s %s: '%s'>" % [self.class.name, object_id, response.code, error.code, error.message] 123 | end 124 | end 125 | end 126 | 127 | # Guess response class name from current class name. If the guessed response class doesn't exist 128 | # do the same thing to the current class's parent class, up the inheritance heirarchy until either 129 | # a response class is found or until we get to the top of the heirarchy in which case we just use 130 | # the the Base response class. 131 | # 132 | # Important: This implemantation assumes that the Base class has a corresponding Base::Response. 133 | class FindResponseClass #:nodoc: 134 | class << self 135 | def for(start) 136 | new(start).find 137 | end 138 | end 139 | 140 | def initialize(start) 141 | @container = AWS::S3 142 | @current_class = start 143 | end 144 | 145 | def find 146 | self.current_class = current_class.superclass until response_class_found? 147 | target.const_get(class_to_find) 148 | end 149 | 150 | private 151 | attr_reader :container 152 | attr_accessor :current_class 153 | 154 | def target 155 | container.const_get(current_name) 156 | end 157 | 158 | def target? 159 | container.const_defined?(current_name) 160 | end 161 | 162 | def response_class_found? 163 | target? && target.const_defined?(class_to_find) 164 | end 165 | 166 | def class_to_find 167 | :Response 168 | end 169 | 170 | def current_name 171 | truncate(current_class) 172 | end 173 | 174 | def truncate(klass) 175 | klass.name[/[^:]+$/] 176 | end 177 | end 178 | end 179 | end 180 | #:startdoc: -------------------------------------------------------------------------------- /lib/aws/s3/service.rb: -------------------------------------------------------------------------------- 1 | module AWS 2 | module S3 3 | # The service lets you find out general information about your account, like what buckets you have. 4 | # 5 | # Service.buckets 6 | # # => [] 7 | class Service < Base 8 | @@response = nil #:nodoc: 9 | 10 | class << self 11 | # List all your buckets. 12 | # 13 | # Service.buckets 14 | # # => [] 15 | # 16 | # For performance reasons, the bucket list will be cached. If you want avoid all caching, pass the :reload 17 | # as an argument: 18 | # 19 | # Service.buckets(:reload) 20 | def buckets 21 | response = get('/') 22 | if response.empty? 23 | [] 24 | else 25 | response.buckets.map {|attributes| Bucket.new(attributes)} 26 | end 27 | end 28 | memoized :buckets 29 | 30 | # Sometimes methods that make requests to the S3 servers return some object, like a Bucket or an S3Object. 31 | # Other times they return just true. Other times they raise an exception that you may want to rescue. Despite all these 32 | # possible outcomes, every method that makes a request stores its response object for you in Service.response. You can always 33 | # get to the last request's response via Service.response. 34 | # 35 | # objects = Bucket.objects('jukebox') 36 | # Service.response.success? 37 | # # => true 38 | # 39 | # This is also useful when an error exception is raised in the console which you weren't expecting. You can 40 | # root around in the response to get more details of what might have gone wrong. 41 | def response 42 | @@response 43 | end 44 | 45 | def response=(response) #:nodoc: 46 | @@response = response 47 | end 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/aws/s3/version.rb: -------------------------------------------------------------------------------- 1 | module AWS 2 | module S3 3 | module VERSION #:nodoc: 4 | MAJOR = '0' 5 | MINOR = '6' 6 | TINY = '3' 7 | BETA = nil # Time.now.to_i.to_s 8 | end 9 | 10 | Version = [VERSION::MAJOR, VERSION::MINOR, VERSION::TINY, VERSION::BETA].compact * '.' 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /site/index.erb: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | AWS::S3 - Ruby Library for Amazon Simple Storage Service (S3) 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 | 18 |
19 |
20 |
21 |

AWS::S3

22 |

A Ruby Library for Amazon's Simple
Storage Service's (S3) REST API.

23 | 29 |

Download with RubyGems

30 |
sudo gem i aws-s3
31 |

Clone from the master git repository

32 |
git clone git://github.com/marcel/aws-s3.git
33 |
34 | 35 |
36 | 37 |

Readme

38 |

Getting started

39 | <%= erb_data[:readme] %> 40 | 41 | 42 | -------------------------------------------------------------------------------- /site/public/images/box-and-gem.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcel/aws-s3/c4a99b34618ecc2990305fb52c685a9b0b7b8389/site/public/images/box-and-gem.gif -------------------------------------------------------------------------------- /site/public/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcel/aws-s3/c4a99b34618ecc2990305fb52c685a9b0b7b8389/site/public/images/favicon.ico -------------------------------------------------------------------------------- /site/public/ruby.css: -------------------------------------------------------------------------------- 1 | .ruby {border: 1px solid #333; } 2 | .ruby .normal {} 3 | .ruby .comment { color: #666; background-color: #eee; font-style: italic; } 4 | .ruby .keyword { color: #c96; font-weight: bold; } 5 | .ruby .method { color: #333; } 6 | .ruby .class { color: #333; } 7 | .ruby .module { color: #333; } 8 | .ruby .punct { color: #333; font-weight: bold; } 9 | .ruby .symbol { color: #333; } 10 | .ruby .string { color: #996; } 11 | .ruby .char { color: #999; } 12 | .ruby .ident { color: #333; } 13 | .ruby .constant { color: #69c; font-weight: bold;} 14 | .ruby .regex { color: #333; } 15 | .ruby .number { color: #333; } 16 | .ruby .attribute { color: #333; } 17 | .ruby .global { color: #333; } 18 | .ruby .expr { color: #333; } -------------------------------------------------------------------------------- /site/public/screen.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #fff; 3 | color: #333; 4 | font-family: verdana, sans-serif; 5 | font-size: 12px; 6 | margin: 0; 7 | } 8 | 9 | div.left_column { 10 | text-align: right; 11 | float: left; 12 | width: 185px; 13 | font-size: 14px; 14 | padding-right: 15px; 15 | } 16 | 17 | div.left_column img { 18 | margin-right: -15px; 19 | } 20 | 21 | 22 | pre { 23 | color: #555; 24 | margin-left: 2em; 25 | overflow: auto; 26 | width: 600px; 27 | } 28 | 29 | div.page_area { 30 | margin: 40px 0 120px 50px; 31 | } 32 | 33 | div.column { 34 | margin-left: 220px; 35 | padding-top: 3px; 36 | width: 450px; 37 | } 38 | 39 | a:link, 40 | a:visited { 41 | color: #039; 42 | } 43 | 44 | h1 { 45 | margin: 0 0 50px 0; 46 | font-size: 14px; 47 | } 48 | 49 | h2 { 50 | color: #bc0707; 51 | font-size: 24px; 52 | margin: 0; 53 | } 54 | 55 | h3 { 56 | font-size: 16px; 57 | line-height: 20px; 58 | margin: 10px 0 0 0; 59 | } 60 | 61 | 62 | div.header { 63 | font-size: 14px; 64 | } 65 | 66 | div.header p.links { 67 | color: #ccc; 68 | } 69 | 70 | div.header h4 { 71 | margin-bottom: 7px; 72 | font-size: 16px; 73 | } 74 | 75 | div.header pre { 76 | margin-top: 0; 77 | } 78 | 79 | 80 | div.readme { 81 | font-size: 12px; 82 | line-height: 140%; 83 | } 84 | 85 | div.readme h2 { 86 | font-size: 20px; 87 | margin: 2em 0 0 0; 88 | } 89 | 90 | div.readme h4 { 91 | font-size: 12px; 92 | color: #bc0707; 93 | color: #000; 94 | margin-top: 3em; 95 | } 96 | 97 | div.readme h4.first { 98 | margin-top: 1.5em; 99 | } 100 | -------------------------------------------------------------------------------- /support/faster-xml-simple/COPYING: -------------------------------------------------------------------------------- 1 | Copyright (c) 2006 Michael Koziarski 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in the 5 | Software without restriction, including without limitation the rights to use, 6 | copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the 7 | Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN 17 | AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 18 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /support/faster-xml-simple/README: -------------------------------------------------------------------------------- 1 | FasterXmlSimple 2 | 3 | FasterXS is intended to be a drop in replacement for the xml input functionality 4 | from XmlSimple. Instead of using rexml, it uses libxml and the associated ruby 5 | bindings. This reduces CPU utilisation and memory consumption considerably. 6 | 7 | Preliminary benchmarks show it between 3 and 10 times as fast, and using a 8 | fraction of the ram. -------------------------------------------------------------------------------- /support/faster-xml-simple/Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake' 3 | require 'rake/testtask' 4 | require 'rake/rdoctask' 5 | require 'rake/packagetask' 6 | require 'rake/gempackagetask' 7 | require 'lib/faster_xml_simple' 8 | 9 | task :default => :test 10 | 11 | Rake::TestTask.new do |test| 12 | test.pattern = 'test/*_test.rb' 13 | test.verbose = true 14 | end 15 | 16 | 17 | Rake::RDocTask.new do |rdoc| 18 | rdoc.rdoc_dir = 'doc' 19 | rdoc.title = "FasterXmlSimple, a libxml based replacement for XmlSimple" 20 | rdoc.options << '--line-numbers' << '--inline-source' 21 | rdoc.rdoc_files.include('README') 22 | rdoc.rdoc_files.include('COPYING') 23 | rdoc.rdoc_files.include('lib/**/*.rb') 24 | end 25 | 26 | namespace :dist do 27 | 28 | spec = Gem::Specification.new do |s| 29 | s.name = 'faster_xml_simple' 30 | s.version = Gem::Version.new(FasterXmlSimple::Version) 31 | s.summary = "A libxml based replacement for XmlSimple" 32 | s.description = s.summary 33 | s.email = 'michael@koziarski.com' 34 | s.author = 'Michael Koziarski' 35 | s.has_rdoc = true 36 | s.extra_rdoc_files = %w(README COPYING) 37 | s.homepage = 'http://fasterxs.rubyforge.org' 38 | s.rubyforge_project = 'fasterxs' 39 | s.files = FileList['Rakefile', 'lib/**/*.rb'] 40 | s.test_files = Dir['test/**/*'] 41 | 42 | s.add_dependency 'libxml-ruby', '>= 0.3.8.4' 43 | s.rdoc_options = ['--title', "", 44 | '--main', 'README', 45 | '--line-numbers', '--inline-source'] 46 | end 47 | Rake::GemPackageTask.new(spec) do |pkg| 48 | pkg.need_tar_gz = true 49 | pkg.package_files.include('{lib,test}/**/*') 50 | pkg.package_files.include('README') 51 | pkg.package_files.include('COPYING') 52 | pkg.package_files.include('Rakefile') 53 | end 54 | end -------------------------------------------------------------------------------- /support/faster-xml-simple/lib/faster_xml_simple.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2006 Michael Koziarski 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | # this software and associated documentation files (the "Software"), to deal in the 6 | # Software without restriction, including without limitation the rights to use, 7 | # copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the 8 | # Software, and to permit persons to whom the Software is furnished to do so, 9 | # subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN 18 | # AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 19 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | require 'rubygems' 22 | require 'xml/libxml' 23 | 24 | class FasterXmlSimple 25 | Version = '0.5.0' 26 | class << self 27 | # Take an string containing XML, and returns a hash representing that 28 | # XML document. For example: 29 | # 30 | # FasterXmlSimple.xml_in("1") 31 | # {"root"=>{"something"=>{"__content__"=>"1"}}} 32 | # 33 | # Faster XML Simple is designed to be a drop in replacement for the xml_in 34 | # functionality of http://xml-simple.rubyforge.org 35 | # 36 | # The following options are supported: 37 | # 38 | # * contentkey: The key to use for the content of text elements, 39 | # defaults to '\_\_content__' 40 | # * forcearray: The list of elements which should always be returned 41 | # as arrays. Under normal circumstances single element arrays are inlined. 42 | # * suppressempty: The value to return for empty elements, pass +true+ 43 | # to remove empty elements entirely. 44 | # * keeproot: By default the hash returned has a single key with the 45 | # name of the root element. If the name of the root element isn't 46 | # interesting to you, pass +false+. 47 | # * forcecontent: By default a text element with no attributes, will 48 | # be collapsed to just a string instead of a hash with a single key. 49 | # Pass +true+ to prevent this. 50 | # 51 | # 52 | def xml_in(string, options={}) 53 | new(string, options).out 54 | end 55 | end 56 | 57 | def initialize(string, options) #:nodoc: 58 | @doc = parse(string) 59 | @options = default_options.merge options 60 | end 61 | 62 | def out #:nodoc: 63 | if @options['keeproot'] 64 | {@doc.root.name => collapse(@doc.root)} 65 | else 66 | collapse(@doc.root) 67 | end 68 | end 69 | 70 | private 71 | def default_options 72 | {'contentkey' => '__content__', 'forcearray' => [], 'keeproot'=>true} 73 | end 74 | 75 | def collapse(element) 76 | result = hash_of_attributes(element) 77 | if text_node? element 78 | text = collapse_text(element) 79 | result[content_key] = text if text =~ /\S/ 80 | elsif element.children? 81 | element.inject(result) do |hash, child| 82 | unless child.text? 83 | child_result = collapse(child) 84 | (hash[child.name] ||= []) << child_result 85 | end 86 | hash 87 | end 88 | end 89 | if result.empty? 90 | return empty_element 91 | end 92 | # Compact them to ensure it complies with the user's requests 93 | inline_single_element_arrays(result) 94 | remove_empty_elements(result) if suppress_empty? 95 | if content_only?(result) && !force_content? 96 | result[content_key] 97 | else 98 | result 99 | end 100 | end 101 | 102 | def content_only?(result) 103 | result.keys == [content_key] 104 | end 105 | 106 | def content_key 107 | @options['contentkey'] 108 | end 109 | 110 | def force_array?(key_name) 111 | Array(@options['forcearray']).include?(key_name) 112 | end 113 | 114 | def inline_single_element_arrays(result) 115 | result.each do |key, value| 116 | if value.size == 1 && value.is_a?(Array) && !force_array?(key) 117 | result[key] = value.first 118 | end 119 | end 120 | end 121 | 122 | def remove_empty_elements(result) 123 | result.each do |key, value| 124 | if value == empty_element 125 | result.delete key 126 | end 127 | end 128 | end 129 | 130 | def suppress_empty? 131 | @options['suppressempty'] == true 132 | end 133 | 134 | def empty_element 135 | if !@options.has_key? 'suppressempty' 136 | {} 137 | else 138 | @options['suppressempty'] 139 | end 140 | end 141 | 142 | # removes the content if it's nothing but blanks, prevents 143 | # the hash being polluted with lots of content like "\n\t\t\t" 144 | def suppress_empty_content(result) 145 | result.delete content_key if result[content_key] !~ /\S/ 146 | end 147 | 148 | def force_content? 149 | @options['forcecontent'] 150 | end 151 | 152 | # a text node is one with 1 or more child nodes which are 153 | # text nodes, and no non-text children, there's no sensible 154 | # way to support nodes which are text and markup like: 155 | #

Something Bold

156 | def text_node?(element) 157 | !element.text? && element.all? {|c| c.text?} 158 | end 159 | 160 | # takes a text node, and collapses it into a string 161 | def collapse_text(element) 162 | element.map {|c| c.content } * '' 163 | end 164 | 165 | def hash_of_attributes(element) 166 | result = {} 167 | element.each_attr do |attribute| 168 | name = attribute.name 169 | name = [attribute.ns, attribute.name].join(':') if attribute.ns? 170 | result[name] = attribute.value 171 | end 172 | result 173 | end 174 | 175 | def parse(string) 176 | if string == '' 177 | string = ' ' 178 | end 179 | XML::Parser.string(string).parse 180 | end 181 | end 182 | 183 | class XmlSimple # :nodoc: 184 | def self.xml_in(*args) 185 | FasterXmlSimple.xml_in *args 186 | end 187 | end -------------------------------------------------------------------------------- /support/faster-xml-simple/test/fixtures/test-1.rails.yml: -------------------------------------------------------------------------------- 1 | --- 2 | something: 3 | something-else: 4 | __content__: testing 5 | -------------------------------------------------------------------------------- /support/faster-xml-simple/test/fixtures/test-1.xml: -------------------------------------------------------------------------------- 1 | 2 | testing 3 | -------------------------------------------------------------------------------- /support/faster-xml-simple/test/fixtures/test-1.yml: -------------------------------------------------------------------------------- 1 | --- 2 | something: 3 | something-else: 4 | - __content__: testing 5 | -------------------------------------------------------------------------------- /support/faster-xml-simple/test/fixtures/test-2.rails.yml: -------------------------------------------------------------------------------- 1 | --- 2 | something: 3 | something-else: 4 | __content__: testing 5 | child_attribute: "15" 6 | root_attribute: "12" 7 | -------------------------------------------------------------------------------- /support/faster-xml-simple/test/fixtures/test-2.xml: -------------------------------------------------------------------------------- 1 | 2 | testing 3 | -------------------------------------------------------------------------------- /support/faster-xml-simple/test/fixtures/test-2.yml: -------------------------------------------------------------------------------- 1 | --- 2 | something: 3 | something-else: 4 | - __content__: testing 5 | child_attribute: "15" 6 | root_attribute: "12" 7 | -------------------------------------------------------------------------------- /support/faster-xml-simple/test/fixtures/test-3.rails.yml: -------------------------------------------------------------------------------- 1 | --- 2 | something: 3 | something-else: 4 | __content__: "\n\ 5 | \t\ttesting\n\ 6 | \t" 7 | -------------------------------------------------------------------------------- /support/faster-xml-simple/test/fixtures/test-3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | testing 4 | 5 | -------------------------------------------------------------------------------- /support/faster-xml-simple/test/fixtures/test-3.yml: -------------------------------------------------------------------------------- 1 | --- 2 | something: 3 | something-else: 4 | - __content__: "\n\ 5 | \t\ttesting\n\ 6 | \t" 7 | -------------------------------------------------------------------------------- /support/faster-xml-simple/test/fixtures/test-4.rails.yml: -------------------------------------------------------------------------------- 1 | --- 2 | something: 3 | something-else: 4 | another_child: 5 | attribute: "4" 6 | -------------------------------------------------------------------------------- /support/faster-xml-simple/test/fixtures/test-4.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | testing 4 | 5 | testing 6 | 7 | -------------------------------------------------------------------------------- /support/faster-xml-simple/test/fixtures/test-4.yml: -------------------------------------------------------------------------------- 1 | --- 2 | something: 3 | something-else: 4 | - another_child: 5 | attribute: "4" 6 | -------------------------------------------------------------------------------- /support/faster-xml-simple/test/fixtures/test-5.rails.yml: -------------------------------------------------------------------------------- 1 | --- 2 | something: 3 | something-else: 4 | - __content__: testing 5 | - __content__: testing 6 | - __content__: testing 7 | - __content__: testing 8 | - __content__: testing 9 | -------------------------------------------------------------------------------- /support/faster-xml-simple/test/fixtures/test-5.xml: -------------------------------------------------------------------------------- 1 | 2 | testing 3 | testing 4 | testing 5 | testing 6 | testing 7 | -------------------------------------------------------------------------------- /support/faster-xml-simple/test/fixtures/test-5.yml: -------------------------------------------------------------------------------- 1 | --- 2 | something: 3 | something-else: 4 | - __content__: testing 5 | - __content__: testing 6 | - __content__: testing 7 | - __content__: testing 8 | - __content__: testing 9 | -------------------------------------------------------------------------------- /support/faster-xml-simple/test/fixtures/test-6.rails.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ListBucketResult: 3 | Prefix: {} 4 | 5 | Name: 6 | __content__: projectionist 7 | MaxKeys: 8 | __content__: "1000" 9 | Contents: 10 | - StorageClass: 11 | __content__: STANDARD 12 | Owner: 13 | DisplayName: 14 | __content__: noradio 15 | ID: 16 | __content__: bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 17 | Size: 18 | __content__: "186870" 19 | ETag: 20 | __content__: "\"2ac1aa042e20ab7e1a9879b0df9f17b7\"" 21 | LastModified: 22 | __content__: "2006-11-15T05:49:39.000Z" 23 | Key: 24 | __content__: 1973-plymouth-satellite-sebring.jpg 25 | - StorageClass: 26 | __content__: STANDARD 27 | Owner: 28 | DisplayName: 29 | __content__: noradio 30 | ID: 31 | __content__: bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 32 | Size: 33 | __content__: "43562" 34 | ETag: 35 | __content__: "\"4ead118ba91491f9c9697153264a1943\"" 36 | LastModified: 37 | __content__: "2006-11-15T05:51:20.000Z" 38 | Key: 39 | __content__: 37-cluster.jpg 40 | Marker: {} 41 | 42 | IsTruncated: 43 | __content__: "false" 44 | -------------------------------------------------------------------------------- /support/faster-xml-simple/test/fixtures/test-6.xml: -------------------------------------------------------------------------------- 1 | 2 | projectionist 3 | 4 | 5 | 1000 6 | false 7 | 8 | 1973-plymouth-satellite-sebring.jpg 9 | 2006-11-15T05:49:39.000Z 10 | "2ac1aa042e20ab7e1a9879b0df9f17b7" 11 | 186870 12 | 13 | bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 14 | noradio 15 | 16 | STANDARD 17 | 18 | 19 | 37-cluster.jpg 20 | 2006-11-15T05:51:20.000Z 21 | "4ead118ba91491f9c9697153264a1943" 22 | 43562 23 | 24 | bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 25 | noradio 26 | 27 | STANDARD 28 | 29 | -------------------------------------------------------------------------------- /support/faster-xml-simple/test/fixtures/test-6.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ListBucketResult: 3 | Prefix: 4 | Name: 5 | __content__: projectionist 6 | MaxKeys: 7 | __content__: "1000" 8 | Contents: 9 | - StorageClass: 10 | __content__: STANDARD 11 | Owner: 12 | DisplayName: 13 | __content__: noradio 14 | ID: 15 | __content__: bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 16 | Size: 17 | __content__: "186870" 18 | ETag: 19 | __content__: "\"2ac1aa042e20ab7e1a9879b0df9f17b7\"" 20 | LastModified: 21 | __content__: "2006-11-15T05:49:39.000Z" 22 | Key: 23 | __content__: 1973-plymouth-satellite-sebring.jpg 24 | - StorageClass: 25 | __content__: STANDARD 26 | Owner: 27 | DisplayName: 28 | __content__: noradio 29 | ID: 30 | __content__: bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 31 | Size: 32 | __content__: "43562" 33 | ETag: 34 | __content__: "\"4ead118ba91491f9c9697153264a1943\"" 35 | LastModified: 36 | __content__: "2006-11-15T05:51:20.000Z" 37 | Key: 38 | __content__: 37-cluster.jpg 39 | Marker: 40 | IsTruncated: 41 | __content__: "false" 42 | -------------------------------------------------------------------------------- /support/faster-xml-simple/test/fixtures/test-7.rails.yml: -------------------------------------------------------------------------------- 1 | --- 2 | AccessControlPolicy: 3 | AccessControlList: 4 | Grant: 5 | - Permission: 6 | __content__: FULL_CONTROL 7 | Grantee: 8 | DisplayName: 9 | __content__: noradio 10 | ID: 11 | __content__: bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 12 | xsi:type: CanonicalUser 13 | - Permission: 14 | __content__: READ 15 | Grantee: 16 | URI: 17 | __content__: http://acs.amazonaws.com/groups/global/AllUsers 18 | xsi:type: Group 19 | Owner: 20 | DisplayName: 21 | __content__: noradio 22 | ID: 23 | __content__: bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 24 | -------------------------------------------------------------------------------- /support/faster-xml-simple/test/fixtures/test-7.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 5 | noradio 6 | 7 | 8 | 9 | 10 | bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 11 | noradio 12 | 13 | FULL_CONTROL 14 | 15 | 16 | 17 | http://acs.amazonaws.com/groups/global/AllUsers 18 | 19 | READ 20 | 21 | 22 | -------------------------------------------------------------------------------- /support/faster-xml-simple/test/fixtures/test-7.yml: -------------------------------------------------------------------------------- 1 | AccessControlPolicy: 2 | Owner: 3 | DisplayName: 4 | __content__: noradio 5 | ID: 6 | __content__: bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 7 | AccessControlList: 8 | Grant: 9 | - Permission: 10 | __content__: FULL_CONTROL 11 | Grantee: 12 | DisplayName: 13 | __content__: noradio 14 | ID: 15 | __content__: bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 16 | xsi:type: CanonicalUser 17 | - Permission: 18 | __content__: READ 19 | Grantee: 20 | URI: 21 | __content__: http://acs.amazonaws.com/groups/global/AllUsers 22 | xsi:type: Group 23 | -------------------------------------------------------------------------------- /support/faster-xml-simple/test/fixtures/test-8.rails.yml: -------------------------------------------------------------------------------- 1 | --- 2 | topic: 3 | parent-id: {} 4 | 5 | title: {} 6 | 7 | approved: 8 | type: boolean 9 | id: 10 | type: integer 11 | viewed-at: 12 | type: datetime 13 | written-on: 14 | type: date 15 | -------------------------------------------------------------------------------- /support/faster-xml-simple/test/fixtures/test-8.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /support/faster-xml-simple/test/fixtures/test-8.yml: -------------------------------------------------------------------------------- 1 | topic: 2 | title: 3 | id: 4 | type: integer 5 | approved: 6 | type: boolean 7 | parent-id: 8 | viewed-at: 9 | type: datetime 10 | written-on: 11 | type: date -------------------------------------------------------------------------------- /support/faster-xml-simple/test/regression_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/test_helper' 2 | 3 | class RegressionTest < FasterXSTest 4 | def test_content_nil_regressions 5 | expected = {"asdf"=>{"jklsemicolon"=>{}}} 6 | assert_equal expected, FasterXmlSimple.xml_in("") 7 | assert_equal expected, FasterXmlSimple.xml_in("", 'forcearray'=>['asdf']) 8 | end 9 | 10 | def test_s3_regression 11 | str = File.read("test/fixtures/test-7.xml") 12 | assert_nil FasterXmlSimple.xml_in(str)["AccessControlPolicy"]["AccessControlList"]["__content__"] 13 | end 14 | 15 | def test_xml_simple_transparency 16 | assert_equal XmlSimple.xml_in(""), FasterXmlSimple.xml_in("") 17 | end 18 | 19 | def test_suppress_empty_variations 20 | str = "" 21 | 22 | assert_equal Hash.new, FasterXmlSimple.xml_in(str)["asdf"]["fdsa"] 23 | assert_nil FasterXmlSimple.xml_in(str, 'suppressempty'=>nil)["asdf"]["fdsa"] 24 | assert_equal '', FasterXmlSimple.xml_in(str, 'suppressempty'=>'')["asdf"]["fdsa"] 25 | assert !FasterXmlSimple.xml_in(str, 'suppressempty'=>true)["asdf"].has_key?("fdsa") 26 | end 27 | 28 | def test_empty_string_doesnt_crash 29 | assert_raise(XML::Parser::ParseError) do 30 | silence_stderr do 31 | FasterXmlSimple.xml_in('') 32 | end 33 | end 34 | end 35 | 36 | def test_keeproot_false 37 | str = "1" 38 | expected = {"fdsa"=>"1"} 39 | assert_equal expected, FasterXmlSimple.xml_in(str, 'keeproot'=>false) 40 | end 41 | 42 | def test_keeproot_false_with_force_content 43 | str = "1" 44 | expected = {"fdsa"=>{"__content__"=>"1"}} 45 | assert_equal expected, FasterXmlSimple.xml_in(str, 'keeproot'=>false, 'forcecontent'=>true) 46 | end 47 | end -------------------------------------------------------------------------------- /support/faster-xml-simple/test/test_helper.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'test/unit' 3 | require 'faster_xml_simple' 4 | 5 | class FasterXSTest < Test::Unit::TestCase 6 | def default_test 7 | end 8 | 9 | def silence_stderr 10 | str = STDERR.dup 11 | STDERR.reopen("/dev/null") 12 | STDERR.sync=true 13 | yield 14 | ensure 15 | STDERR.reopen(str) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /support/faster-xml-simple/test/xml_simple_comparison_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/test_helper' 2 | require 'yaml' 3 | 4 | class XmlSimpleComparisonTest < FasterXSTest 5 | 6 | # Define test methods 7 | 8 | Dir["test/fixtures/test-*.xml"].each do |file_name| 9 | xml_file_name = file_name 10 | method_name = File.basename(file_name, ".xml").gsub('-', '_') 11 | yml_file_name = file_name.gsub('xml', 'yml') 12 | rails_yml_file_name = file_name.gsub('xml', 'rails.yml') 13 | class_eval <<-EOV, __FILE__, __LINE__ 14 | def #{method_name} 15 | assert_equal YAML.load(File.read('#{yml_file_name}')), 16 | FasterXmlSimple.xml_in(File.read('#{xml_file_name}'), default_options ) 17 | end 18 | 19 | def #{method_name}_rails 20 | assert_equal YAML.load(File.read('#{rails_yml_file_name}')), 21 | FasterXmlSimple.xml_in(File.read('#{xml_file_name}'), rails_options) 22 | end 23 | EOV 24 | end 25 | 26 | def default_options 27 | { 28 | 'keeproot' => true, 29 | 'contentkey' => '__content__', 30 | 'forcecontent' => true, 31 | 'suppressempty' => nil, 32 | 'forcearray' => ['something-else'] 33 | } 34 | end 35 | 36 | def rails_options 37 | { 38 | 'forcearray' => false, 39 | 'forcecontent' => true, 40 | 'keeproot' => true, 41 | 'contentkey' => '__content__' 42 | } 43 | end 44 | 45 | 46 | end -------------------------------------------------------------------------------- /support/rdoc/code_info.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake' 3 | require 'rdoc/rdoc' 4 | 5 | module RDoc 6 | class CodeInfo 7 | class << self 8 | def parse(wildcard_pattern = nil) 9 | @info_for_corpus = parse_files(wildcard_pattern) 10 | end 11 | 12 | def for(constant) 13 | new(constant).info 14 | end 15 | 16 | def info_for_corpus 17 | raise RuntimeError, "You must first generate a corpus to search by using RDoc::CodeInfo.parse" unless @info_for_corpus 18 | @info_for_corpus 19 | end 20 | 21 | def parsed_files 22 | info_for_corpus.map {|info| info.file_absolute_name} 23 | end 24 | 25 | def files_to_parse 26 | @files_to_parse ||= Rake::FileList.new 27 | end 28 | 29 | private 30 | def parse_files(pattern) 31 | files = pattern ? Rake::FileList[pattern] : files_to_parse 32 | options = Options.instance 33 | options.parse(files << '-q', RDoc::GENERATORS) 34 | rdoc.send(:parse_files, options) 35 | end 36 | 37 | def rdoc 38 | TopLevel.reset 39 | rdoc = RDoc.new 40 | stats = Stats.new 41 | # We don't want any output so we'll override the print method 42 | stats.instance_eval { def print; nil end } 43 | rdoc.instance_variable_set(:@stats, stats) 44 | rdoc 45 | end 46 | end 47 | 48 | attr_reader :info 49 | def initialize(location) 50 | @location = CodeLocation.new(location) 51 | find_constant 52 | find_method if @location.has_method? 53 | end 54 | 55 | private 56 | attr_reader :location 57 | attr_writer :info 58 | def find_constant 59 | parts = location.namespace_parts 60 | self.class.info_for_corpus.each do |file_info| 61 | @info = parts.inject(file_info) do |result, const_part| 62 | (result.find_module_named(const_part) || result.find_class_named(const_part)) || break 63 | end 64 | return if info 65 | end 66 | end 67 | 68 | def find_method 69 | return unless info 70 | self.info = info.method_list.detect do |method_info| 71 | next unless method_info.name == location.method_name 72 | if location.class_method? 73 | method_info.singleton 74 | elsif location.instance_method? 75 | !method_info.singleton 76 | else 77 | true 78 | end 79 | end 80 | end 81 | end 82 | 83 | class CodeLocation 84 | attr_reader :location 85 | 86 | def initialize(location) 87 | @location = location 88 | end 89 | 90 | def parts 91 | location.split(/::|\.|#/) 92 | end 93 | 94 | def namespace_parts 95 | has_method? ? parts[0...-1] : parts 96 | end 97 | 98 | def has_method? 99 | ('a'..'z').include?(parts.last[0, 1]) 100 | end 101 | 102 | def instance_method? 103 | !location['#'].nil? 104 | end 105 | 106 | def class_method? 107 | has_method? && !location[/#|\./] 108 | end 109 | 110 | def method_name 111 | parts.last if has_method? 112 | end 113 | end 114 | end 115 | 116 | if __FILE__ == $0 117 | require 'test/unit' 118 | class CodeInfoTest < Test::Unit::TestCase 119 | def setup 120 | RDoc::CodeInfo.parse(__FILE__) 121 | end 122 | 123 | def test_constant_lookup 124 | assert RDoc::CodeInfo.for('RDoc') 125 | 126 | info = RDoc::CodeInfo.for('RDoc::CodeInfo') 127 | assert_equal 'CodeInfo', info.name 128 | end 129 | 130 | def test_method_lookup 131 | {'RDoc::CodeInfo.parse' => true, 132 | 'RDoc::CodeInfo::parse' => true, 133 | 'RDoc::CodeInfo#parse' => false, 134 | 'RDoc::CodeInfo.find_method' => true, 135 | 'RDoc::CodeInfo::find_method' => false, 136 | 'RDoc::CodeInfo#find_method' => true, 137 | 'RDoc::CodeInfo#no_such_method' => false, 138 | 'RDoc::NoSuchConst#foo' => false}.each do |location, result_of_lookup| 139 | assert_equal result_of_lookup, !RDoc::CodeInfo.for(location).nil? 140 | end 141 | end 142 | end 143 | 144 | class CodeLocationTest < Test::Unit::TestCase 145 | def test_parts 146 | {'Foo' => %w(Foo), 147 | 'Foo::Bar' => %w(Foo Bar), 148 | 'Foo::Bar#baz' => %w(Foo Bar baz), 149 | 'Foo::Bar.baz' => %w(Foo Bar baz), 150 | 'Foo::Bar::baz' => %w(Foo Bar baz), 151 | 'Foo::Bar::Baz' => %w(Foo Bar Baz)}.each do |location, parts| 152 | assert_equal parts, RDoc::CodeLocation.new(location).parts 153 | end 154 | end 155 | 156 | def test_namespace_parts 157 | {'Foo' => %w(Foo), 158 | 'Foo::Bar' => %w(Foo Bar), 159 | 'Foo::Bar#baz' => %w(Foo Bar), 160 | 'Foo::Bar.baz' => %w(Foo Bar), 161 | 'Foo::Bar::baz' => %w(Foo Bar), 162 | 'Foo::Bar::Baz' => %w(Foo Bar Baz)}.each do |location, namespace_parts| 163 | assert_equal namespace_parts, RDoc::CodeLocation.new(location).namespace_parts 164 | end 165 | end 166 | 167 | def test_has_method? 168 | {'Foo' => false, 169 | 'Foo::Bar' => false, 170 | 'Foo::Bar#baz' => true, 171 | 'Foo::Bar.baz' => true, 172 | 'Foo::Bar::baz' => true, 173 | 'Foo::Bar::Baz' => false}.each do |location, has_method_result| 174 | assert_equal has_method_result, RDoc::CodeLocation.new(location).has_method? 175 | end 176 | end 177 | 178 | def test_instance_method? 179 | {'Foo' => false, 180 | 'Foo::Bar' => false, 181 | 'Foo::Bar#baz' => true, 182 | 'Foo::Bar.baz' => false, 183 | 'Foo::Bar::baz' => false, 184 | 'Foo::Bar::Baz' => false}.each do |location, is_instance_method| 185 | assert_equal is_instance_method, RDoc::CodeLocation.new(location).instance_method? 186 | end 187 | end 188 | 189 | def test_class_method? 190 | {'Foo' => false, 191 | 'Foo::Bar' => false, 192 | 'Foo::Bar#baz' => false, 193 | 'Foo::Bar.baz' => false, 194 | 'Foo::Bar::baz' => true, 195 | 'Foo::Bar::Baz' => false}.each do |location, is_class_method| 196 | assert_equal is_class_method, RDoc::CodeLocation.new(location).class_method? 197 | end 198 | end 199 | 200 | def test_method_name 201 | {'Foo' => nil, 202 | 'Foo::Bar' => nil, 203 | 'Foo::Bar#baz' => 'baz', 204 | 'Foo::Bar.baz' => 'baz', 205 | 'Foo::Bar::baz' => 'baz', 206 | 'Foo::Bar::Baz' => nil}.each do |location, method_name| 207 | assert_equal method_name, RDoc::CodeLocation.new(location).method_name 208 | end 209 | end 210 | end 211 | end -------------------------------------------------------------------------------- /test/acl_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/test_helper' 2 | 3 | class PolicyReadingTest < Test::Unit::TestCase 4 | 5 | def setup 6 | @policy = prepare_policy 7 | end 8 | 9 | def test_policy_owner 10 | assert_kind_of Owner, @policy.owner 11 | assert_equal 'bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1', @policy.owner.id 12 | assert_equal 'mmolina@onramp.net', @policy.owner.display_name 13 | end 14 | 15 | def test_grants 16 | assert @policy.grants 17 | assert !@policy.grants.empty? 18 | grant = @policy.grants.first 19 | assert_kind_of ACL::Grant, grant 20 | assert_equal 'FULL_CONTROL', grant.permission 21 | end 22 | 23 | def test_grants_have_grantee 24 | grant = @policy.grants.first 25 | assert grantee = grant.grantee 26 | assert_kind_of ACL::Grantee, grantee 27 | assert_equal 'bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1', grantee.id 28 | assert_equal 'mmolina@onramp.net', grantee.display_name 29 | assert_equal 'CanonicalUser', grantee.type 30 | end 31 | 32 | def test_grantee_always_responds_to_email_address 33 | assert_nothing_raised do 34 | @policy.grants.first.grantee.email_address 35 | end 36 | end 37 | 38 | private 39 | def prepare_policy 40 | ACL::Policy.new(parsed_policy) 41 | end 42 | 43 | def parsed_policy 44 | Parsing::XmlParser.new Fixtures::Policies.policy_with_one_grant 45 | end 46 | end 47 | 48 | class PolicyWritingTest < PolicyReadingTest 49 | 50 | def setup 51 | policy = prepare_policy 52 | # Dump the policy to xml and retranslate it back from the xml then run all the tests in the xml reading 53 | # test. This round tripping indirectly asserts that the original xml document is the same as the to_xml 54 | # dump. 55 | @policy = ACL::Policy.new(Parsing::XmlParser.new(policy.to_xml)) 56 | end 57 | 58 | end 59 | 60 | class PolicyTest < Test::Unit::TestCase 61 | def test_building_policy_by_hand 62 | policy = grant = grantee = nil 63 | assert_nothing_raised do 64 | policy = ACL::Policy.new 65 | grant = ACL::Grant.new 66 | grantee = ACL::Grantee.new 67 | grantee.email_address = 'marcel@vernix.org' 68 | grant.permission = 'READ_ACP' 69 | grant.grantee = grantee 70 | policy.grants << grant 71 | policy.owner = Owner.new('id' => '123456789', 'display_name' => 'noradio') 72 | end 73 | 74 | assert_nothing_raised do 75 | policy.to_xml 76 | end 77 | 78 | assert !policy.grants.empty? 79 | assert_equal 1, policy.grants.size 80 | assert_equal 'READ_ACP', policy.grants.first.permission 81 | end 82 | 83 | def test_include? 84 | policy = ACL::Policy.new(Parsing::XmlParser.new(Fixtures::Policies.policy_with_one_grant)) 85 | assert !policy.grants.include?(:public_read) 86 | policy.grants << ACL::Grant.grant(:public_read) 87 | assert policy.grants.include?(:public_read) 88 | 89 | assert policy.grants.include?(ACL::Grant.grant(:public_read)) 90 | [false, 1, '1'].each do |non_grant| 91 | assert !policy.grants.include?(non_grant) 92 | end 93 | end 94 | 95 | def test_delete 96 | policy = ACL::Policy.new(Parsing::XmlParser.new(Fixtures::Policies.policy_with_one_grant)) 97 | policy.grants << ACL::Grant.grant(:public_read) 98 | assert policy.grants.include?(:public_read) 99 | assert policy.grants.delete(:public_read) 100 | assert !policy.grants.include?(:public_read) 101 | [false, 1, '1'].each do |non_grant| 102 | assert_nil policy.grants.delete(non_grant) 103 | end 104 | end 105 | 106 | def test_grant_list_comparison 107 | policy = ACL::Policy.new 108 | policy2 = ACL::Policy.new 109 | 110 | grant_names = [:public_read, :public_read_acp, :authenticated_write] 111 | grant_names.each {|grant_name| policy.grants << ACL::Grant.grant(grant_name)} 112 | grant_names.reverse_each {|grant_name| policy2.grants << ACL::Grant.grant(grant_name)} 113 | 114 | assert_equal policy.grants, policy2.grants 115 | end 116 | end 117 | 118 | class GrantTest < Test::Unit::TestCase 119 | def test_permission_must_be_valid 120 | grant = ACL::Grant.new 121 | assert_nothing_raised do 122 | grant.permission = 'READ_ACP' 123 | end 124 | 125 | assert_raises(InvalidAccessControlLevel) do 126 | grant.permission = 'not a valid permission' 127 | end 128 | end 129 | 130 | def test_stock_grants 131 | assert_raises(ArgumentError) do 132 | ACL::Grant.grant :this_is_not_a_stock_grant 133 | end 134 | 135 | grant = nil 136 | assert_nothing_raised do 137 | grant = ACL::Grant.grant(:public_read) 138 | end 139 | 140 | assert grant 141 | assert_kind_of ACL::Grant, grant 142 | assert_equal 'READ', grant.permission 143 | assert grant.grantee 144 | assert_kind_of ACL::Grantee, grant.grantee 145 | assert_equal 'AllUsers', grant.grantee.group 146 | end 147 | end 148 | 149 | class GranteeTest < Test::Unit::TestCase 150 | def test_type_inference 151 | grantee = ACL::Grantee.new 152 | 153 | assert_nothing_raised do 154 | grantee.type 155 | end 156 | 157 | assert_nil grantee.type 158 | grantee.group = 'AllUsers' 159 | assert_equal 'AllUsers', grantee.group 160 | assert_equal 'Group', grantee.type 161 | grantee.email_address = 'marcel@vernix.org' 162 | assert_equal 'AmazonCustomerByEmail', grantee.type 163 | grantee.display_name = 'noradio' 164 | assert_equal 'AmazonCustomerByEmail', grantee.type 165 | grantee.id = '123456789' 166 | assert_equal 'CanonicalUser', grantee.type 167 | end 168 | 169 | def test_type_is_extracted_if_present 170 | grantee = ACL::Grantee.new('xsi:type' => 'CanonicalUser') 171 | assert_equal 'CanonicalUser', grantee.type 172 | end 173 | 174 | def test_type_representation 175 | grantee = ACL::Grantee.new('uri' => 'http://acs.amazonaws.com/groups/global/AllUsers') 176 | 177 | assert_equal 'AllUsers Group', grantee.type_representation 178 | grantee.group = 'AuthenticatedUsers' 179 | assert_equal 'AuthenticatedUsers Group', grantee.type_representation 180 | grantee.email_address = 'marcel@vernix.org' 181 | assert_equal 'marcel@vernix.org', grantee.type_representation 182 | grantee.display_name = 'noradio' 183 | grantee.id = '123456789' 184 | assert_equal 'noradio', grantee.type_representation 185 | end 186 | end 187 | 188 | class ACLOptionProcessorTest < Test::Unit::TestCase 189 | def test_empty_options 190 | options = {} 191 | assert_nothing_raised do 192 | process! options 193 | end 194 | assert_equal({}, options) 195 | end 196 | 197 | def test_invalid_access_level 198 | options = {:access => :foo} 199 | assert_raises(InvalidAccessControlLevel) do 200 | process! options 201 | end 202 | end 203 | 204 | def test_valid_access_level_is_normalized 205 | valid_access_levels = [ 206 | {:access => :private}, 207 | {'access' => 'private'}, 208 | {:access => 'private'}, 209 | {'access' => :private}, 210 | {'x-amz-acl' => 'private'}, 211 | {:x_amz_acl => :private}, 212 | {:x_amz_acl => 'private'}, 213 | {'x_amz_acl' => :private} 214 | ] 215 | 216 | valid_access_levels.each do |options| 217 | assert_nothing_raised do 218 | process! options 219 | end 220 | assert_equal 'private', acl(options) 221 | end 222 | 223 | valid_hyphenated_access_levels = [ 224 | {:access => :public_read}, 225 | {'access' => 'public_read'}, 226 | {'access' => 'public-read'}, 227 | {:access => 'public_read'}, 228 | {:access => 'public-read'}, 229 | {'access' => :public_read}, 230 | 231 | {'x-amz-acl' => 'public_read'}, 232 | {:x_amz_acl => :public_read}, 233 | {:x_amz_acl => 'public_read'}, 234 | {:x_amz_acl => 'public-read'}, 235 | {'x_amz_acl' => :public_read} 236 | ] 237 | 238 | valid_hyphenated_access_levels.each do |options| 239 | assert_nothing_raised do 240 | process! options 241 | end 242 | assert_equal 'public-read', acl(options) 243 | end 244 | end 245 | 246 | private 247 | def process!(options) 248 | ACL::OptionProcessor.process!(options) 249 | end 250 | 251 | def acl(options) 252 | options['x-amz-acl'] 253 | end 254 | end 255 | -------------------------------------------------------------------------------- /test/authentication_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/test_helper' 2 | 3 | class HeaderAuthenticationTest < Test::Unit::TestCase 4 | def test_encoded_canonical 5 | signature = Authentication::Signature.new(request, key_id, secret) 6 | assert_equal AmazonDocExampleData::Example1.canonical_string, signature.send(:canonical_string) 7 | assert_equal AmazonDocExampleData::Example1.signature, signature.send(:encoded_canonical) 8 | end 9 | 10 | def test_authorization_header 11 | header = Authentication::Header.new(request, key_id, secret) 12 | assert_equal AmazonDocExampleData::Example1.canonical_string, header.send(:canonical_string) 13 | assert_equal AmazonDocExampleData::Example1.authorization_header, header 14 | end 15 | 16 | private 17 | def request; AmazonDocExampleData::Example1.request end 18 | def key_id ; AmazonDocExampleData::Example1.access_key_id end 19 | def secret ; AmazonDocExampleData::Example1.secret_access_key end 20 | end 21 | 22 | class QueryStringAuthenticationTest < Test::Unit::TestCase 23 | def test_query_string 24 | query_string = Authentication::QueryString.new(request, key_id, secret, :expires_in => 60) 25 | assert_equal AmazonDocExampleData::Example3.canonical_string, query_string.send(:canonical_string) 26 | assert_equal AmazonDocExampleData::Example3.query_string, query_string 27 | end 28 | 29 | def test_query_string_with_explicit_expiry 30 | query_string = Authentication::QueryString.new(request, key_id, secret, :expires => expires) 31 | assert_equal expires, query_string.send(:canonical_string).instance_variable_get(:@options)[:expires] 32 | assert_equal AmazonDocExampleData::Example3.query_string, query_string 33 | end 34 | 35 | def test_expires_in_is_coerced_to_being_an_integer_in_case_it_is_a_special_integer_proxy 36 | # References bug: http://rubyforge.org/tracker/index.php?func=detail&aid=17458&group_id=2409&atid=9356 37 | integer_proxy = Class.new do 38 | attr_reader :integer 39 | def initialize(integer) 40 | @integer = integer 41 | end 42 | 43 | def to_int 44 | integer 45 | end 46 | end 47 | 48 | actual_integer = 25 49 | query_string = Authentication::QueryString.new(request, key_id, secret, :expires_in => integer_proxy.new(actual_integer)) 50 | assert_equal actual_integer, query_string.send(:expires_in) 51 | end 52 | 53 | private 54 | def request; AmazonDocExampleData::Example3.request end 55 | def key_id ; AmazonDocExampleData::Example3.access_key_id end 56 | def secret ; AmazonDocExampleData::Example3.secret_access_key end 57 | def expires; AmazonDocExampleData::Example3.expires end 58 | end 59 | 60 | class CanonicalStringTest < Test::Unit::TestCase 61 | def setup 62 | @request = Net::HTTP::Post.new('/test') 63 | @canonical_string = Authentication::CanonicalString.new(@request) 64 | end 65 | 66 | def test_path_does_not_include_query_string 67 | request = Net::HTTP::Get.new('/test/query/string?foo=bar&baz=quux') 68 | assert_equal '/test/query/string', Authentication::CanonicalString.new(request).send(:path) 69 | 70 | # Make sure things still work when there is *no* query string 71 | request = Net::HTTP::Get.new('/') 72 | assert_equal '/', Authentication::CanonicalString.new(request).send(:path) 73 | request = Net::HTTP::Get.new('/foo/bar') 74 | assert_equal '/foo/bar', Authentication::CanonicalString.new(request).send(:path) 75 | end 76 | 77 | def test_path_includes_significant_query_strings 78 | significant_query_strings = [ 79 | ['/test/query/string?acl', '/test/query/string?acl'], 80 | ['/test/query/string?acl&foo=bar', '/test/query/string?acl'], 81 | ['/test/query/string?foo=bar&acl', '/test/query/string?acl'], 82 | ['/test/query/string?acl=foo', '/test/query/string?acl'], 83 | ['/test/query/string?torrent=foo', '/test/query/string?torrent'], 84 | ['/test/query/string?logging=foo', '/test/query/string?logging'], 85 | ['/test/query/string?bar=baz&acl=foo', '/test/query/string?acl'], 86 | ['/test/query/string?acl&response-content-disposition=1', '/test/query/string?acl&response-content-disposition=1'] 87 | ] 88 | 89 | significant_query_strings.each do |uncleaned_path, expected_cleaned_path| 90 | assert_equal expected_cleaned_path, Authentication::CanonicalString.new(Net::HTTP::Get.new(uncleaned_path)).send(:path) 91 | end 92 | end 93 | 94 | def test_default_headers_set 95 | Authentication::CanonicalString.default_headers.each do |header| 96 | assert @canonical_string.headers.include?(header) 97 | end 98 | end 99 | 100 | def test_interesting_headers_are_copied_over 101 | an_interesting_header = 'content-md5' 102 | string_without_interesting_header = Authentication::CanonicalString.new(@request) 103 | assert string_without_interesting_header.headers[an_interesting_header].empty? 104 | 105 | # Add an interesting header 106 | @request[an_interesting_header] = 'foo' 107 | string_with_interesting_header = Authentication::CanonicalString.new(@request) 108 | assert_equal 'foo', string_with_interesting_header.headers[an_interesting_header] 109 | end 110 | 111 | def test_canonical_string 112 | request = AmazonDocExampleData::Example1.request 113 | assert_equal AmazonDocExampleData::Example1.canonical_string, Authentication::CanonicalString.new(request) 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /test/base_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/test_helper' 2 | 3 | class BaseTest < Test::Unit::TestCase 4 | def test_connection_established 5 | assert_raises(NoConnectionEstablished) do 6 | Base.connection 7 | end 8 | 9 | Base.establish_connection!(:access_key_id => '123', :secret_access_key => 'abc') 10 | assert_kind_of Connection, Base.connection 11 | 12 | instance = Base.new 13 | assert_equal instance.send(:connection), Base.connection 14 | assert_equal instance.send(:http), Base.connection.http 15 | end 16 | 17 | def test_respond_with 18 | assert_equal Base::Response, Base.send(:response_class) 19 | Base.send(:respond_with, Bucket::Response) do 20 | assert_equal Bucket::Response, Base.send(:response_class) 21 | end 22 | assert_equal Base::Response, Base.send(:response_class) 23 | end 24 | 25 | def test_request_tries_again_when_encountering_an_internal_error 26 | mock_connection_for(Bucket, :returns => [ 27 | # First request is an internal error 28 | {:body => Fixtures::Errors.internal_error, :code => 500, :error => true}, 29 | # Second request is a success 30 | {:body => Fixtures::Buckets.empty_bucket, :code => 200} 31 | ]) 32 | bucket = nil # Block scope hack 33 | assert_nothing_raised do 34 | bucket = Bucket.find('marcel') 35 | end 36 | # Don't call objects 'cause we don't want to make another request 37 | assert bucket.object_cache.empty? 38 | end 39 | 40 | def test_request_tries_up_to_three_times 41 | mock_connection_for(Bucket, :returns => [ 42 | # First request is an internal error 43 | {:body => Fixtures::Errors.internal_error, :code => 500, :error => true}, 44 | # Second request is also an internal error 45 | {:body => Fixtures::Errors.internal_error, :code => 500, :error => true}, 46 | # Ditto third 47 | {:body => Fixtures::Errors.internal_error, :code => 500, :error => true}, 48 | # Fourth works 49 | {:body => Fixtures::Buckets.empty_bucket, :code => 200} 50 | ]) 51 | bucket = nil # Block scope hack 52 | assert_nothing_raised do 53 | bucket = Bucket.find('marcel') 54 | end 55 | # Don't call objects 'cause we don't want to make another request 56 | assert bucket.object_cache.empty? 57 | end 58 | 59 | def test_request_tries_again_three_times_and_gives_up 60 | mock_connection_for(Bucket, :returns => [ 61 | # First request is an internal error 62 | {:body => Fixtures::Errors.internal_error, :code => 500, :error => true}, 63 | # Second request is also an internal error 64 | {:body => Fixtures::Errors.internal_error, :code => 500, :error => true}, 65 | # Ditto third 66 | {:body => Fixtures::Errors.internal_error, :code => 500, :error => true}, 67 | # Ditto fourth 68 | {:body => Fixtures::Errors.internal_error, :code => 500, :error => true}, 69 | ]) 70 | assert_raises(InternalError) do 71 | Bucket.find('marcel') 72 | end 73 | end 74 | end 75 | 76 | class MultiConnectionsTest < Test::Unit::TestCase 77 | class ClassToTestSettingCurrentBucket < Base 78 | set_current_bucket_to 'foo' 79 | end 80 | 81 | def setup 82 | Base.send(:connections).clear 83 | end 84 | 85 | def test_default_connection_options_are_used_for_subsequent_connections 86 | assert !Base.connected? 87 | 88 | assert_raises(MissingAccessKey) do 89 | Base.establish_connection! 90 | end 91 | 92 | assert !Base.connected? 93 | 94 | assert_raises(NoConnectionEstablished) do 95 | Base.connection 96 | end 97 | 98 | assert_nothing_raised do 99 | Base.establish_connection!(:access_key_id => '123', :secret_access_key => 'abc') 100 | end 101 | 102 | assert Base.connected? 103 | 104 | assert_nothing_raised do 105 | Base.connection 106 | end 107 | 108 | # All subclasses are currently using the default connection 109 | assert_equal Base.connection, Bucket.connection 110 | 111 | # No need to pass in the required options. The default connection will supply them 112 | assert_nothing_raised do 113 | Bucket.establish_connection!(:server => 'foo.s3.amazonaws.com') 114 | end 115 | 116 | assert Base.connection != Bucket.connection 117 | assert_equal '123', Bucket.connection.access_key_id 118 | assert_equal 'foo', Bucket.connection.subdomain 119 | end 120 | 121 | def test_current_bucket 122 | Base.establish_connection!(:access_key_id => '123', :secret_access_key => 'abc') 123 | assert_raises(CurrentBucketNotSpecified) do 124 | Base.current_bucket 125 | end 126 | 127 | S3Object.establish_connection!(:server => 'foo-bucket.s3.amazonaws.com') 128 | assert_nothing_raised do 129 | assert_equal 'foo-bucket', S3Object.current_bucket 130 | end 131 | end 132 | 133 | def test_setting_the_current_bucket 134 | assert_equal 'foo', ClassToTestSettingCurrentBucket.current_bucket 135 | end 136 | end 137 | -------------------------------------------------------------------------------- /test/bucket_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/test_helper' 2 | 3 | class BucketTest < Test::Unit::TestCase 4 | def test_bucket_name_validation 5 | valid_names = %w(123 joe step-one step_two step3 step_4 step-5 step.six) 6 | invalid_names = ['12', 'jo', 'kevin spacey', 'larry@wall', '', 'a' * 256] 7 | validate_name = Proc.new {|name| Bucket.send(:validate_name!, name)} 8 | valid_names.each do |valid_name| 9 | assert_nothing_raised { validate_name[valid_name] } 10 | end 11 | 12 | invalid_names.each do |invalid_name| 13 | assert_raises(InvalidBucketName) { validate_name[invalid_name] } 14 | end 15 | end 16 | 17 | def test_empty_bucket 18 | mock_connection_for(Bucket, :returns => {:body => Fixtures::Buckets.empty_bucket, :code => 200}) 19 | bucket = Bucket.find('marcel_molina') 20 | assert bucket.empty? 21 | end 22 | 23 | def test_bucket_with_one_file 24 | mock_connection_for(Bucket, :returns => {:body => Fixtures::Buckets.bucket_with_one_key, :code => 200}) 25 | bucket = Bucket.find('marcel_molina') 26 | assert !bucket.empty? 27 | assert_equal 1, bucket.size 28 | assert_equal %w(tongue_overload.jpg), bucket.objects.map {|object| object.key} 29 | assert bucket['tongue_overload.jpg'] 30 | end 31 | 32 | def test_bucket_with_more_than_one_file 33 | mock_connection_for(Bucket, :returns => {:body => Fixtures::Buckets.bucket_with_more_than_one_key, :code => 200}) 34 | bucket = Bucket.find('marcel_molina') 35 | assert !bucket.empty? 36 | assert_equal 2, bucket.size 37 | assert_equal %w(beluga_baby.jpg tongue_overload.jpg), bucket.objects.map {|object| object.key}.sort 38 | assert bucket['tongue_overload.jpg'] 39 | end 40 | 41 | def test_bucket_path 42 | assert_equal '/bucket_name?max-keys=2', Bucket.send(:path, 'bucket_name', :max_keys => 2) 43 | assert_equal '/bucket_name', Bucket.send(:path, 'bucket_name', {}) 44 | end 45 | 46 | def test_should_not_be_truncated 47 | mock_connection_for(Bucket, :returns => {:body => Fixtures::Buckets.bucket_with_more_than_one_key, :code => 200}) 48 | bucket = Bucket.find('marcel_molina') 49 | assert !bucket.is_truncated 50 | end 51 | 52 | def test_should_be_truncated 53 | mock_connection_for(Bucket, :returns => {:body => Fixtures::Buckets.truncated_bucket_with_more_than_one_key, :code => 200}) 54 | bucket = Bucket.find('marcel_molina') 55 | assert bucket.is_truncated 56 | end 57 | 58 | def test_bucket_name_should_have_leading_slash_prepended_only_once_when_forcing_a_delete 59 | # References bug: http://rubyforge.org/tracker/index.php?func=detail&aid=19158&group_id=2409&atid=9356 60 | bucket_name = 'foo' 61 | expected_bucket_path = "/#{bucket_name}" 62 | 63 | mock_bucket = flexmock('Mock bucket') do |mock| 64 | mock.should_receive(:delete_all).once 65 | end 66 | mock_response = flexmock('Mock delete response') do |mock| 67 | mock.should_receive(:success?).once 68 | end 69 | 70 | flexmock(Bucket).should_receive(:find).with(bucket_name).once.and_return(mock_bucket) 71 | flexmock(Base).should_receive(:delete).with(expected_bucket_path).once.and_return(mock_response) 72 | Bucket.delete(bucket_name, :force => true) 73 | end 74 | end -------------------------------------------------------------------------------- /test/connection_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/test_helper' 2 | 3 | class ConnectionTest < Test::Unit::TestCase 4 | attr_reader :keys 5 | def setup 6 | @keys = {:access_key_id => '123', :secret_access_key => 'abc'}.freeze 7 | end 8 | 9 | def test_creating_a_connection 10 | connection = Connection.new(keys) 11 | assert_kind_of Net::HTTP, connection.http 12 | end 13 | 14 | def test_use_ssl_option_is_set_in_connection 15 | connection = Connection.new(keys.merge(:use_ssl => true)) 16 | assert connection.http.use_ssl? 17 | end 18 | 19 | def test_use_ssl_option_defaults_to_false_in_connection 20 | connection = Connection.new(keys) 21 | assert !connection.http.use_ssl? 22 | end 23 | 24 | def test_use_ssl_option_is_set_to_false_in_connection 25 | connection = Connection.new(keys.merge(:use_ssl => false)) 26 | assert !connection.http.use_ssl? 27 | end 28 | 29 | def test_setting_port_to_443_implies_use_ssl 30 | connection = Connection.new(keys.merge(:port => 443)) 31 | assert connection.http.use_ssl? 32 | end 33 | 34 | def test_protocol 35 | connection = Connection.new(keys) 36 | assert_equal 'http://', connection.protocol 37 | connection = Connection.new(keys.merge(:use_ssl => true)) 38 | assert_equal 'https://', connection.protocol 39 | end 40 | 41 | def test_url_for_honors_use_ssl_option_if_it_is_false_even_if_connection_has_use_ssl_option_set 42 | # References bug: http://rubyforge.org/tracker/index.php?func=detail&aid=17628&group_id=2409&atid=9356 43 | connection = Connection.new(keys.merge(:use_ssl => true)) 44 | assert_match %r(^http://), connection.url_for('/pathdoesnotmatter', :authenticated => false, :use_ssl => false) 45 | end 46 | 47 | def test_connection_is_not_persistent_by_default 48 | connection = Connection.new(keys) 49 | assert !connection.persistent? 50 | 51 | connection = Connection.new(keys.merge(:persistent => true)) 52 | assert connection.persistent? 53 | end 54 | 55 | def test_server_and_port_are_passed_onto_connection 56 | connection = Connection.new(keys) 57 | options = connection.instance_variable_get('@options') 58 | assert_equal connection.http.address, options[:server] 59 | assert_equal connection.http.port, options[:port] 60 | end 61 | 62 | def test_not_including_required_access_keys_raises 63 | assert_raises(MissingAccessKey) do 64 | Connection.new 65 | end 66 | 67 | assert_raises(MissingAccessKey) do 68 | Connection.new(:access_key_id => '123') 69 | end 70 | 71 | assert_nothing_raised do 72 | Connection.new(keys) 73 | end 74 | end 75 | 76 | def test_access_keys_extracted 77 | connection = Connection.new(keys) 78 | assert_equal '123', connection.access_key_id 79 | assert_equal 'abc', connection.secret_access_key 80 | end 81 | 82 | def test_request_method_class_lookup 83 | connection = Connection.new(keys) 84 | expectations = { 85 | :get => Net::HTTP::Get, :post => Net::HTTP::Post, 86 | :put => Net::HTTP::Put, :delete => Net::HTTP::Delete, 87 | :head => Net::HTTP::Head 88 | } 89 | 90 | expectations.each do |verb, klass| 91 | assert_equal klass, connection.send(:request_method, verb) 92 | end 93 | end 94 | 95 | def test_url_for_uses_default_protocol_server_and_port 96 | connection = Connection.new(:access_key_id => '123', :secret_access_key => 'abc', :port => 80) 97 | assert_match %r(^http://s3\.amazonaws\.com/foo\?), connection.url_for('/foo') 98 | 99 | connection = Connection.new(:access_key_id => '123', :secret_access_key => 'abc', :use_ssl => true, :port => 443) 100 | assert_match %r(^https://s3\.amazonaws\.com/foo\?), connection.url_for('/foo') 101 | end 102 | 103 | def test_url_for_remembers_custom_protocol_server_and_port 104 | connection = Connection.new(:access_key_id => '123', :secret_access_key => 'abc', :server => 'example.org', :port => 555, :use_ssl => true) 105 | assert_match %r(^https://example\.org:555/foo\?), connection.url_for('/foo') 106 | end 107 | 108 | def test_url_for_with_and_without_authenticated_urls 109 | connection = Connection.new(:access_key_id => '123', :secret_access_key => 'abc', :server => 'example.org') 110 | authenticated = lambda {|url| url['?AWSAccessKeyId']} 111 | assert authenticated[connection.url_for('/foo')] 112 | assert authenticated[connection.url_for('/foo', :authenticated => true)] 113 | assert !authenticated[connection.url_for('/foo', :authenticated => false)] 114 | end 115 | 116 | def test_url_for_with_canonical_query_params 117 | connection = Connection.new(:access_key_id => '123', :secret_access_key => 'abc', :server => 'example.org') 118 | dispositioned = lambda {|url| url['?response-content-disposition=a']} 119 | assert dispositioned[connection.url_for("/foo?response-content-disposition=a")] 120 | end 121 | 122 | def test_connecting_through_a_proxy 123 | connection = nil 124 | assert_nothing_raised do 125 | connection = Connection.new(keys.merge(:proxy => sample_proxy_settings)) 126 | end 127 | assert connection.http.proxy? 128 | end 129 | 130 | def test_request_only_escapes_the_path_the_first_time_it_runs_and_not_subsequent_times 131 | connection = Connection.new(@keys) 132 | unescaped_path = 'path with spaces' 133 | escaped_path = 'path%20with%20spaces' 134 | 135 | flexmock(Connection).should_receive(:prepare_path).with(unescaped_path).once.and_return(escaped_path).ordered 136 | flexmock(connection.http).should_receive(:request).and_raise(Errno::EPIPE).ordered 137 | flexmock(connection.http).should_receive(:request).ordered 138 | connection.request :put, unescaped_path 139 | end 140 | 141 | def test_if_request_has_no_body_then_the_content_length_is_set_to_zero 142 | # References bug: http://rubyforge.org/tracker/index.php?func=detail&aid=13052&group_id=2409&atid=9356 143 | connection = Connection.new(@keys) 144 | flexmock(Net::HTTP::Put).new_instances.should_receive(:content_length=).once.with(0).ordered 145 | flexmock(connection.http).should_receive(:request).once.ordered 146 | connection.request :put, 'path does not matter' 147 | end 148 | end 149 | 150 | class ConnectionOptionsTest < Test::Unit::TestCase 151 | 152 | def setup 153 | @options = generate_options(:server => 'example.org', :port => 555) 154 | @default_options = generate_options 155 | end 156 | 157 | def test_server_extracted 158 | assert_key_transfered(:server, 'example.org', @options) 159 | end 160 | 161 | def test_port_extracted 162 | assert_key_transfered(:port, 555, @options) 163 | end 164 | 165 | def test_server_defaults_to_default_host 166 | assert_equal DEFAULT_HOST, @default_options[:server] 167 | end 168 | 169 | def test_port_defaults_to_80_if_use_ssl_is_false 170 | assert_equal 80, @default_options[:port] 171 | end 172 | 173 | def test_port_is_set_to_443_if_use_ssl_is_true 174 | options = generate_options(:use_ssl => true) 175 | assert_equal 443, options[:port] 176 | end 177 | 178 | def test_explicit_port_trumps_use_ssl 179 | options = generate_options(:port => 555, :use_ssl => true) 180 | assert_equal 555, options[:port] 181 | end 182 | 183 | def test_invalid_options_raise 184 | assert_raises(InvalidConnectionOption) do 185 | generate_options(:host => 'campfire.s3.amazonaws.com') 186 | end 187 | end 188 | 189 | def test_not_specifying_all_required_proxy_settings_raises 190 | assert_raises(ArgumentError) do 191 | generate_options(:proxy => {}) 192 | end 193 | end 194 | 195 | def test_not_specifying_proxy_option_at_all_does_not_raise 196 | assert_nothing_raised do 197 | generate_options 198 | end 199 | end 200 | 201 | def test_specifying_all_required_proxy_settings 202 | assert_nothing_raised do 203 | generate_options(:proxy => sample_proxy_settings) 204 | end 205 | end 206 | 207 | def test_only_host_setting_is_required 208 | assert_nothing_raised do 209 | generate_options(:proxy => {:host => 'http://google.com'}) 210 | end 211 | end 212 | 213 | def test_proxy_settings_are_extracted 214 | options = generate_options(:proxy => sample_proxy_settings) 215 | assert_equal sample_proxy_settings.values.map {|value| value.to_s}.sort, options.proxy_settings.map {|value| value.to_s}.sort 216 | end 217 | 218 | def test_recognizing_that_the_settings_want_to_connect_through_a_proxy 219 | options = generate_options(:proxy => sample_proxy_settings) 220 | assert options.connecting_through_proxy? 221 | end 222 | 223 | private 224 | def assert_key_transfered(key, value, options) 225 | assert_equal value, options[key] 226 | end 227 | 228 | def generate_options(options = {}) 229 | Connection::Options.new(options) 230 | end 231 | end 232 | -------------------------------------------------------------------------------- /test/error_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/test_helper' 2 | 3 | class ErrorTest < Test::Unit::TestCase 4 | def setup 5 | @container = AWS::S3 6 | @error = Error.new(Parsing::XmlParser.new(Fixtures::Errors.access_denied)) 7 | @container.send(:remove_const, :NotImplemented) if @container.const_defined?(:NotImplemented) 8 | end 9 | 10 | def test_error_class_is_automatically_generated 11 | assert !@container.const_defined?('NotImplemented') 12 | error = Error.new(Parsing::XmlParser.new(Fixtures::Errors.not_implemented)) 13 | assert @container.const_defined?('NotImplemented') 14 | end 15 | 16 | def test_error_contains_attributes 17 | assert_equal 'Access Denied', @error.message 18 | end 19 | 20 | def test_error_is_raisable_as_exception 21 | assert_raises(@container::AccessDenied) do 22 | @error.raise 23 | end 24 | end 25 | 26 | def test_error_message_is_passed_along_to_exception 27 | @error.raise 28 | rescue @container::AccessDenied => e 29 | assert_equal 'Access Denied', e.message 30 | end 31 | 32 | def test_response_is_passed_along_to_exception 33 | response = Error::Response.new(FakeResponse.new(:code => 409, :body => Fixtures::Errors.access_denied)) 34 | response.error.raise 35 | rescue @container::ResponseError => e 36 | assert e.response 37 | assert_kind_of Error::Response, e.response 38 | assert_equal response.error, e.response.error 39 | end 40 | 41 | def test_exception_class_clash 42 | assert !@container.const_defined?(:NotImplemented) 43 | # Create a class that does not inherit from exception that has the same name as the class 44 | # the Error instance is about to attempt to find or create 45 | @container.const_set(:NotImplemented, Class.new) 46 | assert @container.const_defined?(:NotImplemented) 47 | 48 | assert_raises(ExceptionClassClash) do 49 | Error.new(Parsing::XmlParser.new(Fixtures::Errors.not_implemented)) 50 | end 51 | end 52 | 53 | def test_error_response_handles_attributes_with_no_value 54 | mock_connection_for(Bucket, :returns => {:body => Fixtures::Errors.error_with_no_message, :code => 500}) 55 | 56 | begin 57 | Bucket.create('foo', 'invalid-argument' => 'bad juju') 58 | rescue ResponseError => error 59 | end 60 | 61 | assert_nothing_raised do 62 | error.response.error.message 63 | end 64 | assert_nil error.response.error.message 65 | 66 | assert_raises(NoMethodError) do 67 | error.response.error.non_existant_method 68 | end 69 | end 70 | end -------------------------------------------------------------------------------- /test/extensions_test.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | require File.dirname(__FILE__) + '/test_helper' 3 | 4 | class HashExtensionsTest < Test::Unit::TestCase 5 | def test_to_query_string 6 | # Because hashes aren't ordered, I'm mostly testing against hashes with just one key 7 | symbol_keys = {:one => 1} 8 | string_keys = {'one' => 1} 9 | expected = '?one=1' 10 | [symbol_keys, string_keys].each do |hash| 11 | assert_equal expected, hash.to_query_string 12 | end 13 | end 14 | 15 | def test_empty_hash_returns_no_query_string 16 | assert_equal '', {}.to_query_string 17 | end 18 | 19 | def test_include_question_mark 20 | hash = {:one => 1} 21 | assert_equal '?one=1', hash.to_query_string 22 | assert_equal 'one=1', hash.to_query_string(false) 23 | end 24 | 25 | def test_elements_joined_by_ampersand 26 | hash = {:one => 1, :two => 2} 27 | qs = hash.to_query_string 28 | assert qs['one=1&two=2'] || qs['two=2&one=1'] 29 | end 30 | 31 | def test_escape_values 32 | hash = {:one => '5+ 1=6&'} 33 | assert_equal '?one=5%2B%201%3D6%26', hash.to_query_string 34 | end 35 | 36 | def test_normalized_options 37 | expectations = [ 38 | [{:foo_bar => 1}, {'foo-bar' => '1'}], 39 | [{'foo_bar' => 1}, {'foo-bar' => '1'}], 40 | [{'foo-bar' => 1}, {'foo-bar' => '1'}], 41 | [{}, {}] 42 | ] 43 | 44 | expectations.each do |(before, after)| 45 | assert_equal after, before.to_normalized_options 46 | end 47 | end 48 | end 49 | 50 | class StringExtensionsTest < Test::Unit::TestCase 51 | def test_previous 52 | expectations = {'abc' => 'abb', '123' => '122', '1' => '0'} 53 | expectations.each do |before, after| 54 | assert_equal after, before.previous 55 | end 56 | end 57 | 58 | def test_to_header 59 | transformations = { 60 | 'foo' => 'foo', 61 | :foo => 'foo', 62 | 'foo-bar' => 'foo-bar', 63 | 'foo_bar' => 'foo-bar', 64 | :foo_bar => 'foo-bar', 65 | 'Foo-Bar' => 'foo-bar', 66 | 'Foo_Bar' => 'foo-bar' 67 | } 68 | 69 | transformations.each do |before, after| 70 | assert_equal after, before.to_header 71 | end 72 | end 73 | 74 | def test_valid_utf8? 75 | assert !"318597/620065/GTL_75\24300_A600_A610.zip".valid_utf8? 76 | assert "318597/620065/GTL_75£00_A600_A610.zip".valid_utf8? 77 | end 78 | 79 | def test_remove_extended 80 | assert "318597/620065/GTL_75\24300_A600_A610.zip".remove_extended.valid_utf8? 81 | assert "318597/620065/GTL_75£00_A600_A610.zip".remove_extended.valid_utf8? 82 | end 83 | 84 | def test_tap 85 | assert_equal("http://google.com/foo/", "http://google.com".tap {|url| url << "/foo/" }) 86 | end 87 | 88 | end 89 | 90 | class CoercibleStringTest < Test::Unit::TestCase 91 | 92 | def test_coerce 93 | coercions = [ 94 | ['1', 1], 95 | ['false', false], 96 | ['true', true], 97 | ['2006-10-29T23:14:47.000Z', Time.parse('2006-10-29T23:14:47.000Z')], 98 | ['Hello!', 'Hello!'], 99 | ['false23', 'false23'], 100 | ['03 1-2-3-Apple-Tree.mp3', '03 1-2-3-Apple-Tree.mp3'], 101 | ['0815', '0815'] # This number isn't coerced because the leading zero would be lost 102 | ] 103 | 104 | coercions.each do |before, after| 105 | assert_nothing_raised do 106 | assert_equal after, CoercibleString.coerce(before) 107 | end 108 | end 109 | end 110 | end 111 | 112 | class KerneltExtensionsTest < Test::Unit::TestCase 113 | class Foo 114 | def foo 115 | __method__ 116 | end 117 | 118 | def bar 119 | foo 120 | end 121 | 122 | def baz 123 | bar 124 | end 125 | end 126 | 127 | class Bar 128 | def foo 129 | calling_method 130 | end 131 | 132 | def bar 133 | calling_method 134 | end 135 | 136 | def calling_method 137 | __method__(1) 138 | end 139 | end 140 | 141 | def test___method___works_regardless_of_nesting 142 | f = Foo.new 143 | [:foo, :bar, :baz].each do |method| 144 | assert_equal 'foo', f.send(method) 145 | end 146 | end 147 | 148 | def test___method___depth 149 | b = Bar.new 150 | assert_equal 'foo', b.foo 151 | assert_equal 'bar', b.bar 152 | end 153 | end if RUBY_VERSION <= '1.8.7' 154 | 155 | class ModuleExtensionsTest < Test::Unit::TestCase 156 | class Foo 157 | def foo(reload = false) 158 | expirable_memoize(reload) do 159 | Time.now 160 | end 161 | end 162 | 163 | def bar(reload = false) 164 | expirable_memoize(reload, :baz) do 165 | Time.now 166 | end 167 | end 168 | 169 | def quux 170 | Time.now 171 | end 172 | memoized :quux 173 | end 174 | 175 | def setup 176 | @instance = Foo.new 177 | end 178 | 179 | def test_memoize 180 | assert !instance_variables_of(@instance).include?('@foo') 181 | cached_result = @instance.foo 182 | assert_equal cached_result, @instance.foo 183 | assert instance_variables_of(@instance).include?('@foo') 184 | assert_equal cached_result, @instance.send(:instance_variable_get, :@foo) 185 | assert_not_equal cached_result, new_cache = @instance.foo(:reload) 186 | assert_equal new_cache, @instance.foo 187 | assert_equal new_cache, @instance.send(:instance_variable_get, :@foo) 188 | end 189 | 190 | def test_customizing_memoize_storage 191 | assert !instance_variables_of(@instance).include?('@bar') 192 | assert !instance_variables_of(@instance).include?('@baz') 193 | cached_result = @instance.bar 194 | assert !instance_variables_of(@instance).include?('@bar') 195 | assert instance_variables_of(@instance).include?('@baz') 196 | assert_equal cached_result, @instance.bar 197 | assert_equal cached_result, @instance.send(:instance_variable_get, :@baz) 198 | assert_nil @instance.send(:instance_variable_get, :@bar) 199 | end 200 | 201 | def test_memoized 202 | assert !instance_variables_of(@instance).include?('@quux') 203 | cached_result = @instance.quux 204 | assert_equal cached_result, @instance.quux 205 | assert instance_variables_of(@instance).include?('@quux') 206 | assert_equal cached_result, @instance.send(:instance_variable_get, :@quux) 207 | assert_not_equal cached_result, new_cache = @instance.quux(:reload) 208 | assert_equal new_cache, @instance.quux 209 | assert_equal new_cache, @instance.send(:instance_variable_get, :@quux) 210 | end 211 | 212 | def test_constant_setting 213 | some_module = Module.new 214 | assert !some_module.const_defined?(:FOO) 215 | assert_nothing_raised do 216 | some_module.constant :FOO, 'bar' 217 | end 218 | 219 | assert some_module.const_defined?(:FOO) 220 | assert_nothing_raised do 221 | some_module::FOO 222 | some_module.foo 223 | end 224 | assert_equal 'bar', some_module::FOO 225 | assert_equal 'bar', some_module.foo 226 | 227 | assert_nothing_raised do 228 | some_module.constant :FOO, 'baz' 229 | end 230 | 231 | assert_equal 'bar', some_module::FOO 232 | assert_equal 'bar', some_module.foo 233 | end 234 | 235 | private 236 | # For 1.9 compatibility 237 | def instance_variables_of(object) 238 | object.instance_variables.map do |instance_variable| 239 | instance_variable.to_s 240 | end 241 | end 242 | 243 | end 244 | 245 | class AttributeProxyTest < Test::Unit::TestCase 246 | class BlindProxyUsingDefaultAttributesHash 247 | include SelectiveAttributeProxy 248 | proxy_to :exlusively => false 249 | end 250 | 251 | class BlindProxyUsingCustomAttributeHash 252 | include SelectiveAttributeProxy 253 | proxy_to :settings 254 | end 255 | 256 | class ProxyUsingPassedInAttributeHash 257 | include SelectiveAttributeProxy 258 | 259 | def initialize(attributes = {}) 260 | @attributes = attributes 261 | end 262 | end 263 | 264 | class RestrictedProxy 265 | include SelectiveAttributeProxy 266 | 267 | private 268 | def proxiable_attribute?(name) 269 | %w(foo bar baz).include?(name) 270 | end 271 | end 272 | 273 | class NonExclusiveProxy 274 | include SelectiveAttributeProxy 275 | proxy_to :settings, :exclusively => false 276 | end 277 | 278 | def test_using_all_defaults 279 | b = BlindProxyUsingDefaultAttributesHash.new 280 | assert_nothing_raised do 281 | b.foo = 'bar' 282 | end 283 | 284 | assert_nothing_raised do 285 | b.foo 286 | end 287 | 288 | assert_equal 'bar', b.foo 289 | end 290 | 291 | def test_storage_is_autovivified 292 | b = BlindProxyUsingDefaultAttributesHash.new 293 | assert_nothing_raised do 294 | b.send(:attributes)['foo'] = 'bar' 295 | end 296 | 297 | assert_nothing_raised do 298 | b.foo 299 | end 300 | 301 | assert_equal 'bar', b.foo 302 | end 303 | 304 | def test_limiting_which_attributes_are_proxiable 305 | r = RestrictedProxy.new 306 | assert_nothing_raised do 307 | r.foo = 'bar' 308 | end 309 | 310 | assert_nothing_raised do 311 | r.foo 312 | end 313 | 314 | assert_equal 'bar', r.foo 315 | 316 | assert_raises(NoMethodError) do 317 | r.quux = 'foo' 318 | end 319 | 320 | assert_raises(NoMethodError) do 321 | r.quux 322 | end 323 | end 324 | 325 | def test_proxying_is_exclusive_by_default 326 | p = ProxyUsingPassedInAttributeHash.new('foo' => 'bar') 327 | assert_nothing_raised do 328 | p.foo 329 | p.foo = 'baz' 330 | end 331 | 332 | assert_equal 'baz', p.foo 333 | 334 | assert_raises(NoMethodError) do 335 | p.quux 336 | end 337 | end 338 | 339 | def test_setting_the_proxy_as_non_exclusive 340 | n = NonExclusiveProxy.new 341 | assert_nothing_raised do 342 | n.foo = 'baz' 343 | end 344 | 345 | assert_nothing_raised do 346 | n.foo 347 | end 348 | 349 | assert_equal 'baz', n.foo 350 | end 351 | end 352 | -------------------------------------------------------------------------------- /test/fixtures.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | 3 | module AWS 4 | module S3 5 | # When this file is loaded, for each fixture file, a module is created within the Fixtures module 6 | # with the same name as the fixture file. For each fixture in that fixture file, a singleton method is 7 | # added to the module with the name of the given fixture, returning the value of the fixture. 8 | # 9 | # For example: 10 | # 11 | # A fixture in buckets.yml named empty_bucket_list with value hi! 12 | # would be made available like so: 13 | # 14 | # Fixtures::Buckets.empty_bucket_list 15 | # => "hi!" 16 | # 17 | # Alternatively you can treat the fixture module like a hash 18 | # 19 | # Fixtures::Buckets[:empty_bucket_list] 20 | # => "hi!" 21 | # 22 | # You can find out all available fixtures by calling 23 | # 24 | # Fixtures.fixtures 25 | # => ["Buckets"] 26 | # 27 | # And all the fixtures contained in a given fixture by calling 28 | # 29 | # Fixtures::Buckets.fixtures 30 | # => ["bucket_list_with_more_than_one_bucket", "bucket_list_with_one_bucket", "empty_bucket_list"] 31 | module Fixtures 32 | class << self 33 | def create_fixtures 34 | files.each do |file| 35 | create_fixture_for(file) 36 | end 37 | end 38 | 39 | def create_fixture_for(file) 40 | fixtures = YAML.load_file(path(file)) 41 | fixture_module = Module.new 42 | 43 | fixtures.each do |name, value| 44 | fixture_module.module_eval(<<-EVAL, __FILE__, __LINE__) 45 | def #{name} 46 | #{value.inspect} 47 | end 48 | module_function :#{name} 49 | EVAL 50 | end 51 | 52 | fixture_module.module_eval(<<-EVAL, __FILE__, __LINE__) 53 | module_function 54 | 55 | def fixtures 56 | #{fixtures.keys.sort.inspect} 57 | end 58 | 59 | def [](name) 60 | send(name) if fixtures.include?(name.to_s) 61 | end 62 | EVAL 63 | 64 | const_set(module_name(file), fixture_module) 65 | end 66 | 67 | def fixtures 68 | constants.sort 69 | end 70 | 71 | private 72 | 73 | def files 74 | Dir.glob(File.dirname(__FILE__) + '/fixtures/*.yml').map {|fixture| File.basename(fixture)} 75 | end 76 | 77 | def module_name(file_name) 78 | File.basename(file_name, '.*').capitalize 79 | end 80 | 81 | def path(file_name) 82 | File.join(File.dirname(__FILE__), 'fixtures', file_name) 83 | end 84 | end 85 | 86 | create_fixtures 87 | end 88 | end 89 | end -------------------------------------------------------------------------------- /test/fixtures/buckets.yml: -------------------------------------------------------------------------------- 1 | empty_bucket_list: > 2 | 3 | 4 | ab00c3106e091f8fe23154c85678cda66628adb330bc00f02cf4a1c36d76bc48 5 | amazon 6 | 7 | 8 | 9 | 10 | 11 | bucket_list_with_one_bucket: > 12 | 13 | 14 | ab00c3106e091f8fe23154c85678cda66628adb330bc00f02cf4a1c36d76bc48 15 | amazon 16 | 17 | 18 | 19 | marcel_molina 20 | 2006-10-04T15:58:38.000Z 21 | 22 | 23 | 24 | 25 | 26 | bucket_list_with_more_than_one_bucket: > 27 | 28 | 29 | ab00c3106e091f8fe23154c85678cda66628adb330bc00f02cf4a1c36d76bc48 30 | amazon 31 | 32 | 33 | 34 | marcel_molina 35 | 2006-10-04T15:58:38.000Z 36 | 37 | 38 | marcel_molina_jr 39 | 2006-10-04T16:01:30.000Z 40 | 41 | 42 | 43 | 44 | empty_bucket: > 45 | 46 | marcel_molina 47 | 48 | 49 | 1000 50 | false 51 | 52 | 53 | bucket_with_one_key: > 54 | 55 | marcel_molina 56 | 57 | 58 | 1000 59 | false 60 | 61 | tongue_overload.jpg 62 | 2006-10-05T02:42:22.000Z 63 | "f21f7c4e8ea6e34b268887b07d6da745" 64 | 60673 65 | 66 | bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 67 | mmolina@onramp.net 68 | 69 | STANDARD 70 | 71 | 72 | 73 | bucket_with_more_than_one_key: > 74 | 75 | marcel_molina 76 | 77 | 78 | 1000 79 | false 80 | 81 | beluga_baby.jpg 82 | 2006-10-05T02:55:10.000Z 83 | "b2453d4a39a7387674a8c505112a2f0b" 84 | 35807 85 | 86 | bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 87 | mmolina@onramp.net 88 | 89 | STANDARD 90 | 91 | 92 | tongue_overload.jpg 93 | 2006-10-05T02:42:22.000Z 94 | "f21f7c4e8ea6e34b268887b07d6da745" 95 | 60673 96 | 97 | bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 98 | mmolina@onramp.net 99 | 100 | STANDARD 101 | 102 | 103 | 104 | truncated_bucket_with_more_than_one_key: > 105 | 106 | marcel_molina 107 | 108 | 109 | 2 110 | true 111 | 112 | beluga_baby.jpg 113 | 2006-10-05T02:55:10.000Z 114 | "b2453d4a39a7387674a8c505112a2f0b" 115 | 35807 116 | 117 | bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 118 | mmolina@onramp.net 119 | 120 | STANDARD 121 | 122 | 123 | tongue_overload.jpg 124 | 2006-10-05T02:42:22.000Z 125 | "f21f7c4e8ea6e34b268887b07d6da745" 126 | 60673 127 | 128 | bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 129 | mmolina@onramp.net 130 | 131 | STANDARD 132 | 133 | 134 | -------------------------------------------------------------------------------- /test/fixtures/errors.yml: -------------------------------------------------------------------------------- 1 | not_implemented: > 2 | 3 | NotImplemented 4 | A header you provided implies functionality that is not implemented 5 | D1D13A09AC92427F 6 |
Host
7 | oNZgzTTmWiovwGGwHXAzz+1vRmAJVAplS9TF7B0cuOGfEwoi7DYSTa/1Qhv90CfW 8 |
9 | 10 | access_denied: > 11 | 12 | AccessDenied 13 | Access Denied 14 | F99F6D58B96C98E0 15 | XwCF7k3llrcEwtoHR7MusZ6ilCdF5DKDmwYpglvjKNjvwo24INCeXlEpo1M03Wxm 16 | 17 | 18 | internal_error: > 19 | 20 | InternalError 21 | Internal Error 22 | F99F6D223B96C98E0 23 | XwCF7k3llrcEwtoHR7MusZ6ilCdF5DKDmwYpglvjKNjvwo24INCeXlEpo1M03Wxm 24 | 25 | 26 | error_with_no_message: > 27 | 28 | InvalidArgument 29 | 30 | READ 31 | 74A377B1C0FA2BCF 32 | cP4rqsAEtHpN6Ckv08Hr3LXjLzx15/YgyoSqzs779vMR8MrAFSodxZp96wtuMQuI 33 | x-amz-acl 34 | -------------------------------------------------------------------------------- /test/fixtures/headers.yml: -------------------------------------------------------------------------------- 1 | headers_including_one_piece_of_metadata: 2 | x-amz-meta-test: foo 3 | content_type: text/plain -------------------------------------------------------------------------------- /test/fixtures/logging.yml: -------------------------------------------------------------------------------- 1 | logging_enabled: > 2 | 3 | 4 | mylogs 5 | access_log- 6 | 7 | 8 | 9 | logging_disabled: > 10 | 11 | 15 | -------------------------------------------------------------------------------- /test/fixtures/loglines.yml: -------------------------------------------------------------------------------- 1 | bucket_get: 2 | "bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 marcel [14/Nov/2006:06:36:48 +0000] 67.165.183.125 bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 8B5297D428A05432 REST.GET.BUCKET - \"GET /marcel HTTP/1.1\" 200 - 4534 - 398 395 \"-\" \"-\"\n" 3 | 4 | browser_get: 5 | "bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 marcel [25/Nov/2006:06:26:23 +0000] 67.165.183.125 65a011a29cdf8ec533ec3d1ccaae921c 41521D07CA012312 REST.GET.OBJECT kiss.jpg \"GET /marcel/kiss.jpg HTTP/1.1\" 200 - 67748 67748 259 104 \"-\" \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1) Gecko/20061010 Firefox/2.0\"\n" -------------------------------------------------------------------------------- /test/fixtures/logs.yml: -------------------------------------------------------------------------------- 1 | simple_log: 2 | - "bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 marcel [14/Nov/2006:06:36:48 +0000] 67.165.183.125 bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 8B5297D428A05432 REST.GET.BUCKET - \"GET /marcel HTTP/1.1\" 200 - 4534 - 398 395 \"-\" \"-\"\n" 3 | - "bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 marcel [14/Nov/2006:06:38:58 +0000] 67.165.183.125 bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 8F6F3C4027849420 REST.GET.BUCKET - \"GET /marcel HTTP/1.1\" 200 - 4534 - 458 456 \"-\" \"-\"\n" 4 | 5 | requests_from_a_browser: 6 | - "bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 marcel [25/Nov/2006:06:26:23 +0000] 67.165.183.125 65a011a29cdf8ec533ec3d1ccaae921c 41521D07CA012312 REST.GET.OBJECT kiss.jpg \"GET /marcel/kiss.jpg HTTP/1.1\" 200 - 67748 67748 259 104 \"-\" \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1) Gecko/20061010 Firefox/2.0\"\n" 7 | - "bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 marcel [25/Nov/2006:06:26:27 +0000] 67.165.183.125 65a011a29cdf8ec533ec3d1ccaae921c 88629578AFDDD9B5 REST.GET.TORRENT kiss.jpg \"GET /marcel/kiss.jpg?torrent HTTP/1.1\" 200 - 215 - 379 - \"-\" \"Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1) Gecko/20061010 Firefox/2.0\"\n" -------------------------------------------------------------------------------- /test/fixtures/policies.yml: -------------------------------------------------------------------------------- 1 | policy_with_one_grant: > 2 | 3 | 4 | bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 5 | mmolina@onramp.net 6 | 7 | 8 | 9 | 10 | bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1 11 | mmolina@onramp.net 12 | 13 | FULL_CONTROL 14 | 15 | 16 | -------------------------------------------------------------------------------- /test/logging_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/test_helper' 2 | 3 | class LoggingStatusReadingTest < Test::Unit::TestCase 4 | 5 | def setup 6 | @disabled = logging_status(:logging_disabled) 7 | @enabled = logging_status(:logging_enabled) 8 | @new_status = Logging::Status.new('target_bucket' => 'foo', 'target_prefix' => 'access-log-') 9 | end 10 | 11 | def test_logging_enabled? 12 | assert !@disabled.logging_enabled? 13 | assert !@new_status.logging_enabled? 14 | assert @enabled.logging_enabled? 15 | end 16 | 17 | def test_passing_in_prefix_and_bucket 18 | assert_equal 'foo', @new_status.target_bucket 19 | assert_equal 'access-log-', @new_status.target_prefix 20 | assert !@new_status.logging_enabled? 21 | end 22 | 23 | private 24 | def logging_status(fixture) 25 | Logging::Status.new(Parsing::XmlParser.new(Fixtures::Logging[fixture.to_s])) 26 | end 27 | end 28 | 29 | class LoggingStatusWritingTest < LoggingStatusReadingTest 30 | def setup 31 | super 32 | @disabled = Logging::Status.new(Parsing::XmlParser.new(@disabled.to_xml)) 33 | @enabled = Logging::Status.new(Parsing::XmlParser.new(@enabled.to_xml)) 34 | end 35 | end 36 | 37 | class LogTest < Test::Unit::TestCase 38 | def test_value_converted_to_log_lines 39 | log_object = S3Object.new 40 | log_object.value = Fixtures::Logs.simple_log.join 41 | log = Logging::Log.new(log_object) 42 | assert_nothing_raised do 43 | log.lines 44 | end 45 | 46 | assert_equal 2, log.lines.size 47 | assert_kind_of Logging::Log::Line, log.lines.first 48 | assert_equal 'marcel', log.lines.first.bucket 49 | end 50 | end 51 | 52 | class LogLineTest < Test::Unit::TestCase 53 | def setup 54 | @line = Logging::Log::Line.new(Fixtures::Loglines.bucket_get) 55 | end 56 | 57 | def test_field_accessors 58 | expected_results = { 59 | :owner => Owner.new('id' => 'bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1'), 60 | :bucket => 'marcel', 61 | :time => Time.parse('Nov 14 2006 06:36:48 +0000'), 62 | :remote_ip => '67.165.183.125', 63 | :request_id => '8B5297D428A05432', 64 | :requestor => Owner.new('id' => 'bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1'), 65 | :operation => 'REST.GET.BUCKET', 66 | :key => nil, 67 | :request_uri => 'GET /marcel HTTP/1.1', 68 | :http_status => 200, 69 | :error_code => nil, 70 | :bytes_sent => 4534, 71 | :object_size => nil, 72 | :total_time => 398, 73 | :turn_around_time => 395, 74 | :referrer => nil, 75 | :user_agent => nil 76 | } 77 | 78 | expected_results.each do |field, expected| 79 | assert_equal expected, @line.send(field) 80 | end 81 | 82 | assert_equal expected_results, @line.attributes 83 | end 84 | 85 | def test_user_agent 86 | line = Logging::Log::Line.new(Fixtures::Loglines.browser_get) 87 | assert_equal 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1) Gecko/20061010 Firefox/2.0', line.user_agent 88 | end 89 | end -------------------------------------------------------------------------------- /test/mocks/fake_response.rb: -------------------------------------------------------------------------------- 1 | module AWS 2 | module S3 3 | class FakeResponse 4 | attr_reader :code, :body, :headers 5 | def initialize(options = {}) 6 | @code = options.delete(:code) || 200 7 | @body = options.delete(:body) || '' 8 | @headers = {'content-type' => 'application/xml'}.merge(options.delete(:headers) || {}) 9 | end 10 | 11 | # For ErrorResponse 12 | def response 13 | body 14 | end 15 | 16 | def [](header) 17 | headers[header] 18 | end 19 | 20 | def each(&block) 21 | headers.each(&block) 22 | end 23 | alias_method :each_header, :each 24 | end 25 | end 26 | end -------------------------------------------------------------------------------- /test/object_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/test_helper' 2 | 3 | class ObjectTest < Test::Unit::TestCase 4 | def setup 5 | bucket = Bucket.new(Parsing::XmlParser.new(Fixtures::Buckets.bucket_with_one_key)) 6 | @object = bucket.objects.first 7 | end 8 | 9 | def test_header_settings_reader_and_writer 10 | headers = {'content-type' => 'text/plain'} 11 | mock_connection_for(S3Object, :returns => {:headers => headers}) 12 | 13 | assert_nothing_raised do 14 | @object.content_type 15 | end 16 | 17 | assert_equal 'text/plain', @object.content_type 18 | 19 | assert_nothing_raised do 20 | @object.content_type = 'image/jpg' 21 | end 22 | 23 | assert_equal 'image/jpg', @object.content_type 24 | 25 | assert_raises(NoMethodError) do 26 | @object.non_existant_header_setting 27 | end 28 | end 29 | 30 | def test_key_name_validation 31 | assert_raises(InvalidKeyName) do 32 | S3Object.create(nil, '', 'marcel') 33 | end 34 | 35 | assert_raises(InvalidKeyName) do 36 | huge_name = 'a' * 1500 37 | S3Object.create(huge_name, '', 'marcel') 38 | end 39 | end 40 | 41 | def test_content_type_inference 42 | [ 43 | ['foo.jpg', {}, 'image/jpeg'], 44 | ['foo.txt', {}, 'text/plain'], 45 | ['foo', {}, nil], 46 | ['foo.asdf', {}, nil], 47 | ['foo.jpg', {:content_type => nil}, nil], 48 | ['foo', {:content_type => 'image/jpg'}, 'image/jpg'], 49 | ['foo.jpg', {:content_type => 'image/png'}, 'image/png'], 50 | ['foo.asdf', {:content_type => 'image/jpg'}, 'image/jpg'] 51 | ].each do |key, options, content_type| 52 | S3Object.send(:infer_content_type!, key, options) 53 | assert_equal content_type, options[:content_type] 54 | end 55 | end 56 | 57 | def test_object_has_owner 58 | assert_kind_of Owner, @object.owner 59 | end 60 | 61 | def test_url_is_authenticated 62 | conn = Connection.new :access_key_id => '123', :secret_access_key => 'abc' 63 | 64 | begin 65 | AWS::S3::Base.connections['AWS::S3::Base'] = conn 66 | authenticated = lambda {|url| url['?AWSAccessKeyId']} 67 | assert authenticated[@object.url] 68 | ensure 69 | AWS::S3::Base.connections.clear 70 | end 71 | end 72 | 73 | def test_url_with_custom_query 74 | conn = Connection.new :access_key_id => '123', :secret_access_key => 'abc' 75 | 76 | begin 77 | AWS::S3::Base.connections['AWS::S3::Base'] = conn 78 | assert_match 'response-content-disposition=attachment%3B%20filename%3Dfoo.txt', 79 | @object.url(:query => { 80 | 'response-content-disposition' => 'attachment; filename=foo.txt'}) 81 | ensure 82 | AWS::S3::Base.connections.clear 83 | end 84 | end 85 | 86 | def test_owner_attributes_are_accessible 87 | owner = @object.owner 88 | assert owner.id 89 | assert owner.display_name 90 | assert_equal 'bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1', owner.id 91 | assert_equal 'mmolina@onramp.net', owner.display_name 92 | end 93 | 94 | def test_only_valid_attributes_accessible 95 | assert_raises(NoMethodError) do 96 | @object.owner.foo 97 | end 98 | end 99 | 100 | def test_fetching_object_value_generates_value_object 101 | mock_connection_for(S3Object, :returns => {:body => 'hello!'}) 102 | value = S3Object.value('foo', 'bar') 103 | assert_kind_of S3Object::Value, value 104 | assert_equal 'hello!', value 105 | end 106 | 107 | def test_fetching_file_by_name_raises_when_heuristic_fails 108 | mock_connection_for(Bucket, :returns => {:body => Fixtures::Buckets.bucket_with_one_key}) 109 | assert_raises(NoSuchKey) do 110 | S3Object.find('not_tongue_overload.jpg', 'marcel_molina') 111 | end 112 | 113 | object = nil # Block scoping 114 | assert_nothing_raised do 115 | object = S3Object.find('tongue_overload.jpg', 'marcel_molina') 116 | end 117 | assert_kind_of S3Object, object 118 | assert_equal 'tongue_overload.jpg', object.key 119 | end 120 | 121 | def test_about 122 | headers = {'content-size' => '12345', 'date' => Time.now.httpdate, 'content-type' => 'application/xml'} 123 | mock_connection_for(S3Object, :returns => [ 124 | {:headers => headers}, 125 | {:code => 404} 126 | ] 127 | ) 128 | about = S3Object.about('foo', 'bar') 129 | assert_kind_of S3Object::About, about 130 | assert_equal headers, about 131 | 132 | assert_raises(NoSuchKey) do 133 | S3Object.about('foo', 'bar') 134 | end 135 | end 136 | 137 | def test_can_tell_that_an_s3object_does_not_exist 138 | mock_connection_for(S3Object, :returns => {:code => 404}) 139 | assert_equal false, S3Object.exists?('foo', 'bar') 140 | end 141 | 142 | def test_can_tell_that_an_s3object_exists 143 | mock_connection_for(S3Object, :returns => {:code => 200}) 144 | assert_equal true, S3Object.exists?('foo', 'bar') 145 | end 146 | 147 | def test_s3object_equality 148 | mock_connection_for(Bucket, :returns => {:body => Fixtures::Buckets.bucket_with_more_than_one_key}) 149 | file1, file2 = Bucket.objects('does not matter') 150 | assert file1 == file1 151 | assert file2 == file2 152 | assert !(file1 == file2) # /!\ Parens required /!\ 153 | end 154 | 155 | def test_inspect 156 | mock_connection_for(Bucket, :returns => {:body => Fixtures::Buckets.bucket_with_one_key}) 157 | object = S3Object.find('tongue_overload.jpg', 'bucket does not matter') 158 | assert object.path 159 | assert_nothing_raised { object.inspect } 160 | assert object.inspect[object.path] 161 | end 162 | 163 | def test_etag 164 | mock_connection_for(Bucket, :returns => {:body => Fixtures::Buckets.bucket_with_one_key}) 165 | file = S3Object.find('tongue_overload.jpg', 'bucket does not matter') 166 | assert file.etag 167 | assert_equal 'f21f7c4e8ea6e34b268887b07d6da745', file.etag 168 | end 169 | 170 | def test_fetching_information_about_an_object_that_does_not_exist_raises_no_such_key 171 | mock_connection_for(S3Object, :returns => {:body => '', :code => 404}) 172 | assert_raises(NoSuchKey) do 173 | S3Object.about('asdfasdfasdfas-this-does-not-exist', 'bucket does not matter') 174 | end 175 | end 176 | def test_copy_options_are_used 177 | options = {'x-amz-storage-class' => 'REDUCED_REDUNDANCY'} 178 | resp = FakeResponse.new 179 | 180 | connection = flexmock('Mock connection') do |mock| 181 | mock.should_receive(:request). 182 | # The storage-class key must be passed to connection.request(:put, ...) 183 | with(:put, '/some-bucket/new', hsh(options), any, any). 184 | and_return(resp) 185 | end 186 | flexmock(S3Object).should_receive(:connection).and_return(connection) 187 | 188 | result = S3Object.copy('old', 'new', 'some-bucket', options) 189 | assert_equal resp.code, result.code 190 | end 191 | end 192 | 193 | class MetadataTest < Test::Unit::TestCase 194 | def setup 195 | @metadata = S3Object::Metadata.new(Fixtures::Headers.headers_including_one_piece_of_metadata) 196 | end 197 | 198 | def test_only_metadata_is_extracted 199 | assert @metadata.to_headers.size == 1 200 | assert @metadata.to_headers['x-amz-meta-test'] 201 | assert_equal 'foo', @metadata.to_headers['x-amz-meta-test'] 202 | end 203 | 204 | def test_setting_new_metadata_normalizes_name 205 | @metadata[:bar] = 'baz' 206 | assert @metadata.to_headers.include?('x-amz-meta-bar') 207 | @metadata['baz'] = 'quux' 208 | assert @metadata.to_headers.include?('x-amz-meta-baz') 209 | @metadata['x-amz-meta-quux'] = 'whatever' 210 | assert @metadata.to_headers.include?('x-amz-meta-quux') 211 | end 212 | 213 | def test_clobbering_existing_header 214 | @metadata[:bar] = 'baz' 215 | assert_equal 'baz', @metadata.to_headers['x-amz-meta-bar'] 216 | @metadata[:bar] = 'quux' 217 | assert_equal 'quux', @metadata.to_headers['x-amz-meta-bar'] 218 | @metadata['bar'] = 'foo' 219 | assert_equal 'foo', @metadata.to_headers['x-amz-meta-bar'] 220 | @metadata['x-amz-meta-bar'] = 'bar' 221 | assert_equal 'bar', @metadata.to_headers['x-amz-meta-bar'] 222 | end 223 | 224 | def test_invalid_metadata 225 | @metadata[:invalid_header] = ' ' * (S3Object::Metadata::SIZE_LIMIT + 1) 226 | assert_raises InvalidMetadataValue do 227 | @metadata.to_headers 228 | end 229 | end 230 | end 231 | 232 | class ValueTest < Test::Unit::TestCase 233 | def setup 234 | @response = FakeResponse.new(:body => 'hello there') 235 | @value = S3Object::Value.new(@response) 236 | end 237 | 238 | def test_value_is_set_to_response_body 239 | assert_equal @response.body, @value 240 | end 241 | 242 | def test_response_is_accessible_from_value_object 243 | assert_equal @response, @value.response 244 | end 245 | end 246 | -------------------------------------------------------------------------------- /test/parsing_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/test_helper' 2 | 3 | class TypecastingTest < Test::Unit::TestCase 4 | # Make it easier to call methods in tests 5 | Parsing::Typecasting.public_instance_methods.each do |method| 6 | Parsing::Typecasting.send(:module_function, method) 7 | end 8 | 9 | def test_array_with_one_element_that_is_a_hash 10 | value = [{'Available' => 'true'}] 11 | assert_equal [{'available' => true}], Parsing::Typecasting.typecast(value) 12 | end 13 | 14 | def test_hash_with_one_key_whose_value_is_an_array 15 | value = { 16 | 'Bucket' => 17 | [ 18 | {'Available' => 'true'} 19 | ] 20 | } 21 | 22 | expected = { 23 | 'bucket' => 24 | [ 25 | {'available' => true} 26 | ] 27 | } 28 | assert_equal expected, Parsing::Typecasting.typecast(value) 29 | end 30 | 31 | end 32 | 33 | class XmlParserTest < Test::Unit::TestCase 34 | def test_bucket_is_always_forced_to_be_an_array_unless_empty 35 | one_bucket = Parsing::XmlParser.new(Fixtures::Buckets.bucket_list_with_one_bucket) 36 | more_than_one = Parsing::XmlParser.new(Fixtures::Buckets.bucket_list_with_more_than_one_bucket) 37 | 38 | [one_bucket, more_than_one].each do |bucket_list| 39 | assert_kind_of Array, bucket_list['buckets']['bucket'] 40 | end 41 | 42 | no_buckets = Parsing::XmlParser.new(Fixtures::Buckets.empty_bucket_list) 43 | assert no_buckets.has_key?('buckets') 44 | assert_nil no_buckets['buckets'] 45 | end 46 | 47 | def test_bucket_contents_are_forced_to_be_an_array_unless_empty 48 | one_key = Parsing::XmlParser.new(Fixtures::Buckets.bucket_with_one_key) 49 | more_than_one = Parsing::XmlParser.new(Fixtures::Buckets.bucket_with_more_than_one_key) 50 | [one_key, more_than_one].each do |bucket_with_contents| 51 | assert_kind_of Array, bucket_with_contents['contents'] 52 | end 53 | 54 | no_keys = Parsing::XmlParser.new(Fixtures::Buckets.empty_bucket) 55 | assert !no_keys.has_key?('contents') 56 | end 57 | 58 | def test_policy_grants_are_always_an_array 59 | policy = Parsing::XmlParser.new(Fixtures::Policies.policy_with_one_grant) 60 | assert_kind_of Array, policy['access_control_list']['grant'] 61 | end 62 | 63 | def test_empty_xml_response_is_not_parsed 64 | assert_equal({}, Parsing::XmlParser.new('')) 65 | end 66 | end -------------------------------------------------------------------------------- /test/remote/acl_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/test_helper' 2 | 3 | class RemoteACLTest < Test::Unit::TestCase 4 | 5 | def setup 6 | establish_real_connection 7 | end 8 | 9 | def teardown 10 | disconnect! 11 | end 12 | 13 | def test_acl 14 | Bucket.create(TEST_BUCKET) # Wipe out the existing bucket's ACL 15 | 16 | bucket_policy = Bucket.acl(TEST_BUCKET) 17 | assert_equal 1, bucket_policy.grants.size 18 | assert !bucket_policy.grants.include?(:public_read_acp) 19 | 20 | bucket_policy.grants << ACL::Grant.grant(:public_read_acp) 21 | 22 | assert_nothing_raised do 23 | Bucket.acl(TEST_BUCKET, bucket_policy) 24 | end 25 | 26 | bucket = Bucket.find(TEST_BUCKET) 27 | assert bucket.acl.grants.include?(:public_read_acp) 28 | 29 | bucket.acl.grants.pop # Get rid of the newly added grant 30 | 31 | assert !bucket.acl.grants.include?(:public_read_acp) 32 | bucket.acl(bucket.acl) # Update its acl 33 | assert Service.response.success? 34 | 35 | bucket_policy = Bucket.acl(TEST_BUCKET) 36 | assert_equal 1, bucket_policy.grants.size 37 | assert !bucket_policy.grants.include?(:public_read_acp) 38 | 39 | S3Object.store('testing-acls', 'the test data', TEST_BUCKET, :content_type => 'text/plain') 40 | acl = S3Object.acl('testing-acls', TEST_BUCKET) 41 | 42 | # Confirm object has the default policy 43 | 44 | assert !acl.grants.empty? 45 | assert_equal 1, acl.grants.size 46 | grant = acl.grants.first 47 | 48 | assert_equal 'FULL_CONTROL', grant.permission 49 | 50 | grantee = grant.grantee 51 | 52 | assert acl.owner.id 53 | assert acl.owner.display_name 54 | assert grantee.id 55 | assert grantee.display_name 56 | 57 | assert_equal acl.owner.id, grantee.id 58 | assert_equal acl.owner.display_name, grantee.display_name 59 | 60 | assert_equal Owner.current, acl.owner 61 | 62 | 63 | # Manually add read access to an Amazon customer by email address 64 | 65 | new_grant = ACL::Grant.new 66 | new_grant.permission = 'READ' 67 | new_grant_grantee = ACL::Grantee.new 68 | new_grant_grantee.email_address = [['amazon', 'marcelmolina'].join('@'), 'com'].join('.') 69 | new_grant.grantee = new_grant_grantee 70 | acl.grants << new_grant 71 | 72 | assert_nothing_raised do 73 | S3Object.acl('testing-acls', TEST_BUCKET, acl) 74 | end 75 | 76 | # Confirm the acl was updated successfully 77 | 78 | assert Service.response.success? 79 | 80 | acl = S3Object.acl('testing-acls', TEST_BUCKET) 81 | assert !acl.grants.empty? 82 | assert_equal 2, acl.grants.size 83 | new_grant = acl.grants.last 84 | assert_equal 'READ', new_grant.permission 85 | 86 | # Confirm instance method has same result 87 | 88 | assert_equal acl.grants, S3Object.find('testing-acls', TEST_BUCKET).acl.grants 89 | 90 | # Get rid of the grant we just added 91 | 92 | acl.grants.pop 93 | 94 | # Confirm acl class method sees that the bucket option is being used to put a new acl 95 | 96 | assert_nothing_raised do 97 | TestS3Object.acl('testing-acls', acl) 98 | end 99 | 100 | assert Service.response.success? 101 | 102 | acl = TestS3Object.acl('testing-acls') 103 | 104 | # Confirm added grant was removed from the policy 105 | 106 | assert !acl.grants.empty? 107 | assert_equal 1, acl.grants.size 108 | grant = acl.grants.first 109 | assert_equal 'FULL_CONTROL', grant.permission 110 | 111 | assert_nothing_raised do 112 | S3Object.delete('testing-acls', TEST_BUCKET) 113 | end 114 | 115 | assert Service.response.success? 116 | end 117 | end -------------------------------------------------------------------------------- /test/remote/bittorrent_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/test_helper' 2 | 3 | class RemoteBittorrentTest < Test::Unit::TestCase 4 | def setup 5 | establish_real_connection 6 | end 7 | 8 | def teardown 9 | disconnect! 10 | end 11 | 12 | def test_bittorrent 13 | bt_test_key = 'testing-bittorrent' 14 | S3Object.create(bt_test_key, 'foo', TEST_BUCKET) 15 | 16 | # Confirm we can fetch a bittorrent file for this object 17 | 18 | torrent_file = nil 19 | assert_nothing_raised do 20 | torrent_file = S3Object.torrent_for(bt_test_key, TEST_BUCKET) 21 | end 22 | assert torrent_file 23 | assert torrent_file['tracker'] 24 | 25 | # Make object accessible to the public via a torrent 26 | 27 | policy = S3Object.acl(bt_test_key, TEST_BUCKET) 28 | 29 | assert !policy.grants.include?(:public_read) 30 | 31 | assert_nothing_raised do 32 | S3Object.grant_torrent_access_to(bt_test_key, TEST_BUCKET) 33 | end 34 | 35 | policy = S3Object.acl(bt_test_key, TEST_BUCKET) 36 | 37 | assert policy.grants.include?(:public_read) 38 | 39 | # Confirm instance method wraps class method 40 | 41 | assert_equal torrent_file, S3Object.find(bt_test_key, TEST_BUCKET).torrent 42 | 43 | S3Object.delete(bt_test_key, TEST_BUCKET) 44 | end 45 | end -------------------------------------------------------------------------------- /test/remote/bucket_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/test_helper' 2 | 3 | class RemoteBucketTest < Test::Unit::TestCase 4 | 5 | def setup 6 | establish_real_connection 7 | assert Bucket.find(TEST_BUCKET).delete_all 8 | end 9 | 10 | def teardown 11 | disconnect! 12 | end 13 | 14 | def test_bucket 15 | # Fetch the testing bucket 16 | 17 | bucket = nil 18 | assert_nothing_raised do 19 | bucket = Bucket.find(TEST_BUCKET) 20 | end 21 | 22 | assert bucket 23 | 24 | # Confirm we can fetch the bucket implicitly 25 | 26 | bucket = nil 27 | assert_nothing_raised do 28 | bucket = TestBucket.find 29 | end 30 | 31 | assert bucket 32 | 33 | # Confirm the bucket has the right name 34 | 35 | assert_equal TEST_BUCKET, bucket.name 36 | 37 | assert bucket.empty? 38 | assert_equal 0, bucket.size 39 | 40 | # Add some files to the bucket 41 | 42 | assert_nothing_raised do 43 | %w(a m z).each do |file_name| 44 | S3Object.create(file_name, file_name, bucket.name, :content_type => 'text/plain') 45 | end 46 | end 47 | 48 | # Confirm that we can reload the objects 49 | 50 | assert_nothing_raised do 51 | bucket.objects(:reload) 52 | end 53 | 54 | assert !bucket.empty? 55 | assert_equal 3, bucket.size 56 | 57 | assert_nothing_raised do 58 | bucket.objects(:marker => 'm') 59 | end 60 | 61 | assert_equal 1, bucket.size 62 | assert bucket['z'] 63 | 64 | assert_equal 1, Bucket.find(TEST_BUCKET, :max_keys => 1).size 65 | 66 | assert_nothing_raised do 67 | bucket.objects(:reload) 68 | end 69 | 70 | assert_equal 3, bucket.size 71 | 72 | # Ensure the reloaded buckets have been repatriated 73 | 74 | assert_equal bucket, bucket.objects.first.bucket 75 | 76 | # Confirm that we can delete one of the objects and it will be removed 77 | 78 | object_to_be_deleted = bucket.objects.last 79 | assert_nothing_raised do 80 | object_to_be_deleted.delete 81 | end 82 | 83 | assert !bucket.objects.include?(object_to_be_deleted) 84 | 85 | # Confirm that we can add an object 86 | 87 | object = bucket.new_object(:value => 'hello') 88 | 89 | assert_raises(NoKeySpecified) do 90 | object.store 91 | end 92 | 93 | object.key = 'abc' 94 | assert_nothing_raised do 95 | object.store 96 | end 97 | 98 | assert bucket.objects.include?(object) 99 | 100 | # Confirm that the object is still there after reloading its objects 101 | 102 | assert_nothing_raised do 103 | bucket.objects(:reload) 104 | end 105 | assert bucket.objects.include?(object) 106 | 107 | # Check that TestBucket has the same objects fetched implicitly 108 | 109 | assert_equal bucket.objects, TestBucket.objects 110 | 111 | # Empty out bucket 112 | 113 | assert_nothing_raised do 114 | bucket.delete_all 115 | end 116 | 117 | assert bucket.empty? 118 | 119 | bucket = nil 120 | assert_nothing_raised do 121 | bucket = Bucket.find(TEST_BUCKET) 122 | end 123 | 124 | assert bucket.empty? 125 | end 126 | 127 | def test_bucket_name_is_switched_with_options_when_bucket_is_implicit_and_options_are_passed 128 | Object.const_set(:ImplicitlyNamedBucket, Class.new(Bucket)) 129 | ImplicitlyNamedBucket.current_bucket = TEST_BUCKET 130 | assert ImplicitlyNamedBucket.objects.empty? 131 | 132 | %w(a b c).each {|key| S3Object.store(key, 'value does not matter', TEST_BUCKET)} 133 | 134 | assert_equal 3, ImplicitlyNamedBucket.objects.size 135 | 136 | objects = nil 137 | assert_nothing_raised do 138 | objects = ImplicitlyNamedBucket.objects(:max_keys => 1) 139 | end 140 | 141 | assert objects 142 | assert_equal 1, objects.size 143 | ensure 144 | %w(a b c).each {|key| S3Object.delete(key, TEST_BUCKET)} 145 | end 146 | end -------------------------------------------------------------------------------- /test/remote/logging_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/test_helper' 2 | 3 | class RemoteLoggingTest < Test::Unit::TestCase 4 | def setup 5 | establish_real_connection 6 | end 7 | 8 | def teardown 9 | disconnect! 10 | end 11 | 12 | def test_logging 13 | Bucket.create(TEST_BUCKET) # Clear out any custom grants 14 | 15 | # Confirm that logging is not enabled on the test bucket 16 | 17 | assert !Bucket.logging_enabled_for?(TEST_BUCKET) 18 | assert !Bucket.find(TEST_BUCKET).logging_enabled? 19 | 20 | assert_equal [], Bucket.logs_for(TEST_BUCKET) 21 | 22 | # Confirm the current bucket doesn't have logging grants 23 | 24 | policy = Bucket.acl(TEST_BUCKET) 25 | assert !policy.grants.include?(:logging_read_acp) 26 | assert !policy.grants.include?(:logging_write) 27 | 28 | # Confirm that we can enable logging 29 | 30 | assert_nothing_raised do 31 | Bucket.enable_logging_for TEST_BUCKET 32 | end 33 | 34 | # Confirm enabling logging worked 35 | 36 | assert Service.response.success? 37 | 38 | assert Bucket.logging_enabled_for?(TEST_BUCKET) 39 | assert Bucket.find(TEST_BUCKET).logging_enabled? 40 | 41 | # Confirm the appropriate grants were added 42 | 43 | policy = Bucket.acl(TEST_BUCKET) 44 | assert policy.grants.include?(:logging_read_acp) 45 | assert policy.grants.include?(:logging_write) 46 | 47 | # Confirm logging status used defaults 48 | 49 | logging_status = Bucket.logging_status_for TEST_BUCKET 50 | assert_equal TEST_BUCKET, logging_status.target_bucket 51 | assert_equal 'log-', logging_status.target_prefix 52 | 53 | # Confirm we can update the logging status 54 | 55 | logging_status.target_prefix = 'access-log-' 56 | 57 | assert_nothing_raised do 58 | Bucket.logging_status_for TEST_BUCKET, logging_status 59 | end 60 | 61 | assert Service.response.success? 62 | 63 | logging_status = Bucket.logging_status_for TEST_BUCKET 64 | assert_equal 'access-log-', logging_status.target_prefix 65 | 66 | # Confirm we can make a request for the bucket's logs 67 | 68 | assert_nothing_raised do 69 | Bucket.logs_for TEST_BUCKET 70 | end 71 | 72 | # Confirm we can disable logging 73 | 74 | assert_nothing_raised do 75 | Bucket.disable_logging_for(TEST_BUCKET) 76 | end 77 | 78 | assert Service.response.success? 79 | 80 | assert !Bucket.logging_enabled_for?(TEST_BUCKET) 81 | end 82 | end -------------------------------------------------------------------------------- /test/remote/object_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/test_helper' 2 | 3 | class RemoteS3ObjectTest < Test::Unit::TestCase 4 | def setup 5 | establish_real_connection 6 | end 7 | 8 | def teardown 9 | disconnect! 10 | end 11 | 12 | def test_object 13 | key = 'testing_s3objects' 14 | value = 'testing' 15 | content_type = 'text/plain' 16 | unauthenticated_url = ['http:/', Base.connection.http.address, TEST_BUCKET, key].join('/') 17 | 18 | # Create an object 19 | 20 | response = nil 21 | assert_nothing_raised do 22 | response = S3Object.create(key, value, TEST_BUCKET, :access => :public_read, :content_type => content_type) 23 | end 24 | 25 | # Check response 26 | 27 | assert response.success? 28 | 29 | # Extract the object's etag 30 | 31 | etag = nil 32 | assert_nothing_raised do 33 | etag = response.etag 34 | end 35 | 36 | assert etag 37 | 38 | # Confirm we can't create an object unless the bucket is set 39 | 40 | assert_raises(NoBucketSpecified) do 41 | object = S3Object.new 42 | object.key = 'hello' 43 | object.store 44 | end 45 | 46 | # Fetch newly created object to show it was actually created 47 | 48 | object = nil 49 | assert_nothing_raised do 50 | object = S3Object.find(key, TEST_BUCKET) 51 | end 52 | 53 | assert object 54 | 55 | # Confirm it has the right etag 56 | 57 | assert_equal etag, object.etag 58 | 59 | # Check if its owner is properly set 60 | 61 | assert_nothing_raised do 62 | object.owner.display_name 63 | end 64 | 65 | # Confirm we can get the object's key 66 | 67 | assert_equal key, object.key 68 | 69 | # Confirm its value was properly set 70 | 71 | assert_equal value, object.value 72 | assert_equal value, S3Object.value(key, TEST_BUCKET) 73 | streamed_value = '' 74 | assert_nothing_raised do 75 | S3Object.stream(key, TEST_BUCKET) do |segment| 76 | streamed_value << segment 77 | end 78 | end 79 | 80 | assert_equal value, streamed_value 81 | 82 | # Change its value 83 | 84 | new_value = "" 85 | assert_nothing_raised do 86 | object.value = new_value 87 | end 88 | assert_equal new_value, object.value 89 | 90 | # Confirm content type was properly set 91 | 92 | assert_equal content_type, object.content_type 93 | 94 | # Change its content type 95 | 96 | new_content_type = 'text/javascript' 97 | assert_nothing_raised do 98 | object.content_type = new_content_type 99 | end 100 | 101 | assert_equal new_content_type, object.content_type 102 | 103 | # Test that it is publicly readable 104 | 105 | response = fetch_object_at(unauthenticated_url) 106 | assert (200..299).include?(response.code.to_i) 107 | 108 | # Confirm that it has no meta data 109 | 110 | assert object.metadata.empty? 111 | 112 | # Set some meta data 113 | 114 | metadata_key = :secret_sauce 115 | metadata_value = "it's a secret" 116 | object.metadata[metadata_key] = metadata_value 117 | 118 | # Persist all changes 119 | 120 | assert_nothing_raised do 121 | object.store 122 | end 123 | 124 | # Refetch the object 125 | 126 | key = object.key 127 | object = nil 128 | assert_nothing_raised do 129 | object = S3Object.find(key, TEST_BUCKET) 130 | end 131 | 132 | # Confirm all changes were persisted 133 | 134 | assert object 135 | assert_equal key, object.key 136 | 137 | assert_equal new_content_type, object.content_type 138 | 139 | assert_equal new_value, object.value 140 | assert_equal new_value, object.value(:reload) 141 | 142 | assert !object.metadata.empty? 143 | assert_equal metadata_value, object.metadata[metadata_key] 144 | 145 | # Change acl 146 | 147 | assert_nothing_raised do 148 | S3Object.create(object.key, object.value, TEST_BUCKET, :access => :private, :content_type => object.content_type) 149 | end 150 | 151 | # Confirm object is no longer publicly readable 152 | 153 | response = fetch_object_at(unauthenticated_url) 154 | assert (400..499).include?(response.code.to_i) 155 | 156 | # Confirm object is accessible from its authenticated url 157 | 158 | response = fetch_object_at(object.url) 159 | assert (200..299).include?(response.code.to_i) 160 | 161 | # Copy the object 162 | 163 | assert_nothing_raised do 164 | object.copy('testing_s3objects-copy') 165 | end 166 | 167 | # Confirm the object is identical 168 | 169 | copy = nil 170 | assert_nothing_raised do 171 | copy = S3Object.find('testing_s3objects-copy', TEST_BUCKET) 172 | end 173 | 174 | assert copy 175 | 176 | assert_equal object.value, copy.value 177 | assert_equal object.content_type, copy.content_type 178 | 179 | # Delete object 180 | 181 | assert_nothing_raised do 182 | object.delete 183 | end 184 | 185 | # Confirm we can rename objects 186 | 187 | renamed_to = copy.key + '-renamed' 188 | renamed_value = copy.value 189 | assert_nothing_raised do 190 | S3Object.rename(copy.key, renamed_to, TEST_BUCKET) 191 | end 192 | 193 | # Confirm renamed copy exists 194 | 195 | renamed = nil 196 | assert_nothing_raised do 197 | renamed = S3Object.find(renamed_to, TEST_BUCKET) 198 | end 199 | 200 | assert renamed 201 | assert_equal renamed_value, renamed.value 202 | 203 | # Confirm copy is deleted 204 | 205 | assert_raises(NoSuchKey) do 206 | S3Object.find(copy.key, TEST_BUCKET) 207 | end 208 | 209 | # Confirm that you can not store an object once it is deleted 210 | 211 | assert_raises(DeletedObject) do 212 | object.store 213 | end 214 | 215 | assert_raises(NoSuchKey) do 216 | S3Object.find(key, TEST_BUCKET) 217 | end 218 | 219 | # Confirm we can pass in an IO stream and have the uploading sent in chunks 220 | 221 | response = nil 222 | test_file_key = File.basename(TEST_FILE) 223 | assert_nothing_raised do 224 | response = S3Object.store(test_file_key, open(TEST_FILE), TEST_BUCKET) 225 | end 226 | assert response.success? 227 | 228 | assert_equal File.size(TEST_FILE), Integer(S3Object.about(test_file_key, TEST_BUCKET)['content-length']) 229 | 230 | result = nil 231 | assert_nothing_raised do 232 | result = S3Object.delete(test_file_key, TEST_BUCKET) 233 | end 234 | 235 | assert result 236 | end 237 | 238 | def test_content_type_inference 239 | # Confirm appropriate content type is inferred when not specified 240 | 241 | content_type_objects = {'foo.jpg' => 'image/jpeg', 'no-extension-specified' => 'binary/octet-stream', 'foo.txt' => 'text/plain'} 242 | content_type_objects.each_key do |key| 243 | S3Object.store(key, 'fake data', TEST_BUCKET) # No content type explicitly set 244 | end 245 | 246 | content_type_objects.each do |key, content_type| 247 | assert_equal content_type, S3Object.about(key, TEST_BUCKET)['content-type'] 248 | end 249 | 250 | # Confirm we can update the content type 251 | 252 | assert_nothing_raised do 253 | object = S3Object.find('no-extension-specified', TEST_BUCKET) 254 | object.content_type = 'application/pdf' 255 | object.store 256 | end 257 | 258 | assert_equal 'application/pdf', S3Object.about('no-extension-specified', TEST_BUCKET)['content-type'] 259 | 260 | ensure 261 | # Get rid of objects we just created 262 | content_type_objects.each_key {|key| S3Object.delete(key, TEST_BUCKET) } 263 | end 264 | 265 | def test_body_can_be_more_than_just_string_or_io 266 | require 'stringio' 267 | key = 'testing-body-as-string-io' 268 | io = StringIO.new('hello there') 269 | S3Object.store(key, io, TEST_BUCKET) 270 | assert_equal 'hello there', S3Object.value(key, TEST_BUCKET) 271 | ensure 272 | S3Object.delete(key, TEST_BUCKET) 273 | end 274 | 275 | def test_fetching_information_about_an_object_that_does_not_exist_raises_no_such_key 276 | assert_raises(NoSuchKey) do 277 | S3Object.about('asdfasdfasdfas-this-does-not-exist', TEST_BUCKET) 278 | end 279 | end 280 | 281 | # Regression test for http://developer.amazonwebservices.com/connect/thread.jspa?messageID=49152&tstart=0#49152 282 | def test_finding_an_object_with_slashes_in_its_name_does_not_escape_the_slash 283 | S3Object.store('rails/1', 'value does not matter', TEST_BUCKET) 284 | S3Object.store('rails/1.html', 'value does not matter', TEST_BUCKET) 285 | 286 | object = nil 287 | assert_nothing_raised do 288 | object = S3Object.find('rails/1.html', TEST_BUCKET) 289 | end 290 | 291 | assert_equal 'rails/1.html', object.key 292 | ensure 293 | %w(rails/1 rails/1.html).each {|key| S3Object.delete(key, TEST_BUCKET)} 294 | end 295 | 296 | def test_finding_an_object_with_spaces_in_its_name 297 | assert_nothing_raised do 298 | S3Object.store('name with spaces', 'value does not matter', TEST_BUCKET) 299 | end 300 | 301 | object = nil 302 | assert_nothing_raised do 303 | object = S3Object.find('name with spaces', TEST_BUCKET) 304 | end 305 | 306 | assert object 307 | assert_equal 'name with spaces', object.key 308 | 309 | # Confirm authenticated url is generated correctly despite space in file name 310 | 311 | response = fetch_object_at(object.url) 312 | assert (200..299).include?(response.code.to_i) 313 | 314 | ensure 315 | S3Object.delete('name with spaces', TEST_BUCKET) 316 | end 317 | 318 | def test_copying_an_object_should_copy_over_its_acl_also_if_requested 319 | key = 'copied-objects-inherit-acl' 320 | copy_key = key + '2' 321 | S3Object.store(key, 'value does not matter', TEST_BUCKET) 322 | original_object = S3Object.find(key, TEST_BUCKET) 323 | original_object.acl.grants << ACL::Grant.grant(:public_read) 324 | original_object.acl.grants << ACL::Grant.grant(:public_read_acp) 325 | 326 | S3Object.acl(key, TEST_BUCKET, original_object.acl) 327 | 328 | acl = S3Object.acl(key, TEST_BUCKET) 329 | assert_equal 3, acl.grants.size 330 | 331 | S3Object.copy(key, copy_key, TEST_BUCKET, :copy_acl => true) 332 | copied_object = S3Object.find(copy_key, TEST_BUCKET) 333 | assert_equal acl.grants, copied_object.acl.grants 334 | ensure 335 | S3Object.delete(key, TEST_BUCKET) 336 | S3Object.delete(copy_key, TEST_BUCKET) 337 | end 338 | 339 | def test_handling_a_path_that_is_not_valid_utf8 340 | key = "318597/620065/GTL_75\24300_A600_A610.zip" 341 | assert_nothing_raised do 342 | S3Object.store(key, 'value does not matter', TEST_BUCKET) 343 | end 344 | 345 | object = nil 346 | assert_nothing_raised do 347 | object = S3Object.find(key, TEST_BUCKET) 348 | end 349 | 350 | assert object 351 | 352 | url = nil 353 | assert_nothing_raised do 354 | url = S3Object.url_for(key, TEST_BUCKET) 355 | end 356 | 357 | assert url 358 | 359 | assert_equal object.value, fetch_object_at(url).body 360 | ensure 361 | assert_nothing_raised do 362 | S3Object.delete(key, TEST_BUCKET) 363 | end 364 | end 365 | 366 | private 367 | def fetch_object_at(url) 368 | Net::HTTP.get_response(URI.parse(url)) 369 | end 370 | 371 | end -------------------------------------------------------------------------------- /test/remote/test_file.data: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcel/aws-s3/c4a99b34618ecc2990305fb52c685a9b0b7b8389/test/remote/test_file.data -------------------------------------------------------------------------------- /test/remote/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | require 'uri' 3 | $:.unshift File.dirname(__FILE__) + '/../../lib' 4 | require 'aws/s3' 5 | begin 6 | require_library_or_gem 'breakpoint' 7 | rescue LoadError 8 | end 9 | 10 | TEST_BUCKET = 'aws-s3-tests' 11 | TEST_FILE = File.dirname(__FILE__) + '/test_file.data' 12 | 13 | class Test::Unit::TestCase 14 | include AWS::S3 15 | def establish_real_connection 16 | Base.establish_connection!( 17 | :access_key_id => ENV['AMAZON_ACCESS_KEY_ID'], 18 | :secret_access_key => ENV['AMAZON_SECRET_ACCESS_KEY'] 19 | ) 20 | end 21 | 22 | def disconnect! 23 | Base.disconnect 24 | end 25 | 26 | class TestBucket < Bucket 27 | set_current_bucket_to TEST_BUCKET 28 | end 29 | 30 | class TestS3Object < S3Object 31 | set_current_bucket_to TEST_BUCKET 32 | end 33 | end -------------------------------------------------------------------------------- /test/response_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/test_helper' 2 | class BaseResponseTest < Test::Unit::TestCase 3 | def setup 4 | @headers = {'content-type' => 'text/plain', 'date' => Time.now} 5 | @response = FakeResponse.new() 6 | @base_response = Base::Response.new(@response) 7 | end 8 | 9 | def test_status_predicates 10 | response = Proc.new {|code| Base::Response.new(FakeResponse.new(:code => code))} 11 | assert response[200].success? 12 | assert response[300].redirect? 13 | assert response[400].client_error? 14 | assert response[500].server_error? 15 | end 16 | 17 | def test_headers_passed_along_from_original_response 18 | assert_equal @response.headers, @base_response.headers 19 | assert_equal @response['date'], @base_response['date'] 20 | original_headers, new_headers = {}, {} 21 | @response.headers.each {|k,v| original_headers[k] = v} 22 | @base_response.each {|k,v| new_headers[k] = v} 23 | assert_equal original_headers, new_headers 24 | end 25 | end 26 | 27 | class ErrorResponseTest < Test::Unit::TestCase 28 | def test_error_responses_are_always_in_error 29 | assert Error::Response.new(FakeResponse.new).error? 30 | assert Error::Response.new(FakeResponse.new(:code => 200)).error? 31 | assert Error::Response.new(FakeResponse.new(:headers => {'content-type' => 'text/plain'})).error? 32 | end 33 | end 34 | 35 | class S3ObjectResponseTest < Test::Unit::TestCase 36 | def test_etag_extracted 37 | mock_connection_for(S3Object, :returns => {:headers => {"etag" => %("acbd18db4cc2f85cedef654fccc4a4d8")}}).once 38 | object_response = S3Object.create('name_does_not_matter', 'data does not matter', 'bucket does not matter') 39 | assert_equal "acbd18db4cc2f85cedef654fccc4a4d8", object_response.etag 40 | end 41 | end 42 | 43 | class ResponseClassFinderTest < Test::Unit::TestCase 44 | class CampfireBucket < Bucket 45 | end 46 | 47 | class BabyBase < Base 48 | end 49 | 50 | def test_on_base 51 | assert_equal Base::Response, FindResponseClass.for(Base) 52 | assert_equal Base::Response, FindResponseClass.for(AWS::S3::Base) 53 | 54 | end 55 | 56 | def test_on_subclass_with_corresponding_response_class 57 | assert_equal Bucket::Response, FindResponseClass.for(Bucket) 58 | assert_equal Bucket::Response, FindResponseClass.for(AWS::S3::Bucket) 59 | end 60 | 61 | def test_on_subclass_with_intermediary_parent_that_has_corresponding_response_class 62 | assert_equal Bucket::Response, FindResponseClass.for(CampfireBucket) 63 | end 64 | 65 | def test_on_subclass_with_no_corresponding_response_class_and_no_intermediary_parent 66 | assert_equal Base::Response, FindResponseClass.for(BabyBase) 67 | end 68 | end -------------------------------------------------------------------------------- /test/service_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/test_helper' 2 | 3 | class ServiceTest < Test::Unit::TestCase 4 | def test_bucket_list_with_empty_bucket_list 5 | mock_connection_for(Service, :returns => {:body => Fixtures::Buckets.empty_bucket_list, :code => 200}) 6 | list = Service.buckets(:reload) 7 | assert_equal [], list 8 | end 9 | 10 | def test_bucket_list_with_bucket_list_containing_one_bucket 11 | mock_connection_for(Service, :returns => {:body => Fixtures::Buckets.bucket_list_with_one_bucket, :code => 200}) 12 | list = Service.buckets(:reload) 13 | assert_equal 1, list.size 14 | assert_equal 'marcel_molina', list.first.name 15 | end 16 | 17 | def test_bucket_list_with_bucket_list_containing_more_than_one_bucket 18 | mock_connection_for(Service, :returns => {:body => Fixtures::Buckets.bucket_list_with_more_than_one_bucket, :code => 200}) 19 | list = Service.buckets(:reload) 20 | assert_equal 2, list.size 21 | assert_equal %w(marcel_molina marcel_molina_jr), list.map {|bucket| bucket.name}.sort 22 | end 23 | end -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | $:.unshift File.dirname(__FILE__) + '/../lib' 3 | require 'aws/s3' 4 | require File.dirname(__FILE__) + '/mocks/fake_response' 5 | require File.dirname(__FILE__) + '/fixtures' 6 | begin 7 | require_library_or_gem 'ruby-debug' 8 | rescue LoadError 9 | end 10 | require_library_or_gem 'flexmock' 11 | require_library_or_gem 'flexmock/test_unit' 12 | 13 | 14 | # Data copied from http://docs.amazonwebservices.com/AmazonS3/2006-03-01/RESTAuthentication.html 15 | module AmazonDocExampleData 16 | module Example1 17 | module_function 18 | 19 | def request 20 | request = Net::HTTP::Put.new('/quotes/nelson') 21 | request['Content-Md5'] = 'c8fdb181845a4ca6b8fec737b3581d76' 22 | request['Content-Type'] = 'text/html' 23 | request['Date'] = 'Thu, 17 Nov 2005 18:49:58 GMT' 24 | request['X-Amz-Meta-Author'] = 'foo@bar.com' 25 | request['X-Amz-Magic'] = 'abracadabra' 26 | request 27 | end 28 | 29 | def canonical_string 30 | "PUT\nc8fdb181845a4ca6b8fec737b3581d76\ntext/html\nThu, 17 Nov 2005 18:49:58 GMT\nx-amz-magic:abracadabra\nx-amz-meta-author:foo@bar.com\n/quotes/nelson" 31 | end 32 | 33 | def access_key_id 34 | '44CF9590006BF252F707' 35 | end 36 | 37 | def secret_access_key 38 | 'OtxrzxIsfpFjA7SwPzILwy8Bw21TLhquhboDYROV' 39 | end 40 | 41 | def signature 42 | 'jZNOcbfWmD/A/f3hSvVzXZjM2HU=' 43 | end 44 | 45 | def authorization_header 46 | 'AWS 44CF9590006BF252F707:jZNOcbfWmD/A/f3hSvVzXZjM2HU=' 47 | end 48 | end 49 | 50 | module Example3 51 | module_function 52 | 53 | def request 54 | request = Net::HTTP::Get.new('/quotes/nelson') 55 | request['Date'] = date 56 | request 57 | end 58 | 59 | def date 60 | 'Thu Mar 9 01:24:20 CST 2006' 61 | end 62 | 63 | def access_key_id 64 | Example1.access_key_id 65 | end 66 | 67 | def secret_access_key 68 | Example1.secret_access_key 69 | end 70 | 71 | def expires 72 | 1141889120 73 | end 74 | 75 | def query_string 76 | 'AWSAccessKeyId=44CF9590006BF252F707&Expires=1141889120&Signature=vjbyPxybdZaNmGa%2ByT272YEAiv4%3D' 77 | end 78 | 79 | def canonical_string 80 | "GET\n\n\n1141889120\n/quotes/nelson" 81 | end 82 | 83 | end 84 | end 85 | 86 | class Test::Unit::TestCase 87 | include AWS::S3 88 | 89 | def sample_proxy_settings 90 | {:host => 'http://google.com', :port => 8080, :user => 'marcel', :password => 'secret'} 91 | end 92 | 93 | def mock_connection_for(klass, options = {}) 94 | data = options[:returns] 95 | return_values = case data 96 | when Hash 97 | FakeResponse.new(data) 98 | when Array 99 | data.map {|hash| FakeResponse.new(hash)} 100 | else 101 | abort "Response data for mock connection must be a Hash or an Array. Was #{data.inspect}." 102 | end 103 | 104 | connection = flexmock('Mock connection') do |mock| 105 | mock.should_receive(:request).and_return(*return_values).at_least.once 106 | end 107 | 108 | flexmock(klass).should_receive(:connection).and_return(connection) 109 | end 110 | end --------------------------------------------------------------------------------