seanwalbran-scashin133-s3 0.3.11

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
6
+ *.gem
7
+ .bundle
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "http://rubygems.org"
2
+ gemspec
@@ -0,0 +1,23 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ scashin133-s3 (0.3.11)
5
+ proxies (~> 0.2.0)
6
+
7
+ GEM
8
+ remote: http://rubygems.org/
9
+ specs:
10
+ mocha (0.9.8)
11
+ rake
12
+ proxies (0.2.1)
13
+ rake (0.8.7)
14
+ test-unit (2.1.1)
15
+
16
+ PLATFORMS
17
+ ruby
18
+
19
+ DEPENDENCIES
20
+ bundler (>= 1.0.0)
21
+ mocha
22
+ scashin133-s3!
23
+ test-unit (>= 2.0)
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Jakub Kuźma, Mirosław Boruta
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,79 @@
1
+ = S3
2
+
3
+ S3 library provides access to {Amazon's Simple Storage Service}[http://aws.amazon.com/s3/].
4
+
5
+ It supports both: European and US buckets through the {REST API}[http://docs.amazonwebservices.com/AmazonS3/latest/API/APIRest.html].
6
+
7
+ == Installation
8
+
9
+ gem install s3
10
+
11
+ == Usage
12
+
13
+ === Initialize the service
14
+
15
+ require "s3"
16
+ service = S3::Service.new(:access_key_id => "...",
17
+ :secret_access_key => "...")
18
+ #=> #<S3::Service:...>
19
+
20
+ === List buckets
21
+
22
+ service.buckets
23
+ #=> [#<S3::Bucket:first-bucket>,
24
+ # #<S3::Bucket:second-bucket>]
25
+
26
+ === Find bucket
27
+
28
+ first_bucket = service.buckets.find("first-bucket")
29
+ #=> #<S3::Bucket:first-bucket>
30
+
31
+ === List objects in a bucket
32
+
33
+ first_bucket.objects
34
+ #=> [#<S3::Object:/first-bucket/lenna.png>,
35
+ # #<S3::Object:/first-bucket/lenna_mini.png>]
36
+
37
+ === Find object in a bucket
38
+
39
+ object = first_bucket.objects.find("lenna.png")
40
+ #=> #<S3::Object:/first-bucket/lenna.png>
41
+
42
+ === Access object metadata (cached from find)
43
+
44
+ object.content_type
45
+ #=> "image/png"
46
+
47
+ === Access object content (downloads the object)
48
+
49
+ object.content
50
+ #=> "\x89PNG\r\n\x1A\n\x00\x00\x00\rIHDR\x00..."
51
+
52
+ === Delete an object
53
+
54
+ object.destroy
55
+ #=> true
56
+
57
+ === Create an object
58
+
59
+ new_object = bucket.objects.build("bender.png")
60
+ #=> #<S3::Object:/synergy-staging/bender.png>
61
+
62
+ new_object.content = open("bender.png")
63
+
64
+ new_object.save
65
+ #=> true
66
+
67
+ Please note that new objects are created with "public-read" ACL by
68
+ default.
69
+
70
+ == See also
71
+
72
+ * rubygems[http://rubygems.org/gems/s3]
73
+ * repository[http://github.com/qoobaa/s3]
74
+ * {issue tracker}[http://github.com/qoobaa/s3/issues]
75
+ * documentation[http://rubydoc.info/github/qoobaa/s3/master/frames]
76
+
77
+ == Copyright
78
+
79
+ Copyright (c) 2009 Jakub Kuźma, Mirosław Boruta. See LICENSE[http://github.com/qoobaa/s3/raw/master/LICENSE] for details.
@@ -0,0 +1,21 @@
1
+ require "bundler"
2
+ Bundler::GemHelper.install_tasks
3
+ Bundler.setup
4
+
5
+ require "rake/testtask"
6
+ require "rake/rdoctask"
7
+
8
+ Rake::TestTask.new(:test) do |test|
9
+ test.libs << "lib" << "test"
10
+ test.pattern = "test/**/*_test.rb"
11
+ test.verbose = true
12
+ end
13
+
14
+ Rake::RDocTask.new do |rdoc|
15
+ rdoc.rdoc_dir = "rdoc"
16
+ rdoc.title = "s3 #{S3::VERSION}"
17
+ rdoc.rdoc_files.include("README.rdoc")
18
+ rdoc.rdoc_files.include("lib/**/*.rb")
19
+ end
20
+
21
+ task :default => :test
@@ -0,0 +1,27 @@
1
+ require "base64"
2
+ require "cgi"
3
+ require "digest/md5"
4
+ require "forwardable"
5
+ require "net/http"
6
+ require "net/https"
7
+ require "openssl"
8
+ require "rexml/document"
9
+ require "time"
10
+
11
+ require "proxies"
12
+ require "s3/objects_extension"
13
+ require "s3/buckets_extension"
14
+ require "s3/parser"
15
+ require "s3/bucket"
16
+ require "s3/connection"
17
+ require "s3/exceptions"
18
+ require "s3/object"
19
+ require "s3/request"
20
+ require "s3/service"
21
+ require "s3/signature"
22
+ require "s3/version"
23
+
24
+ module S3
25
+ # Default (and only) host serving S3 stuff
26
+ HOST = "s3.amazonaws.com"
27
+ end
@@ -0,0 +1,182 @@
1
+ module S3
2
+ class Bucket
3
+ include Parser
4
+ include Proxies
5
+ extend Forwardable
6
+
7
+ attr_reader :name, :service
8
+
9
+ def_instance_delegators :service, :service_request
10
+ private_class_method :new
11
+
12
+ # Retrieves the bucket information from the server. Raises an
13
+ # S3::Error exception if the bucket doesn't exist or you don't
14
+ # have access to it, etc.
15
+ def retrieve
16
+ bucket_headers
17
+ self
18
+ end
19
+
20
+ # Returns location of the bucket, e.g. "EU"
21
+ def location(reload = false)
22
+ return @location if defined?(@location) and not reload
23
+ @location = location_constraint
24
+ end
25
+
26
+ # Compares the bucket with other bucket. Returns true if the names
27
+ # of the buckets are the same, and both have the same services
28
+ # (see Service equality)
29
+ def ==(other)
30
+ self.name == other.name and self.service == other.service
31
+ end
32
+
33
+ # Similar to retrieve, but catches S3::Error::NoSuchBucket
34
+ # exceptions and returns false instead.
35
+ def exists?
36
+ retrieve
37
+ true
38
+ rescue Error::NoSuchBucket
39
+ false
40
+ end
41
+
42
+ # Destroys given bucket. Raises an S3::Error::BucketNotEmpty
43
+ # exception if the bucket is not empty. You can destroy non-empty
44
+ # bucket passing true (to force destroy)
45
+ def destroy(force = false)
46
+ delete_bucket
47
+ true
48
+ rescue Error::BucketNotEmpty
49
+ if force
50
+ objects.destroy_all
51
+ retry
52
+ else
53
+ raise
54
+ end
55
+ end
56
+
57
+ # Saves the newly built bucket.
58
+ #
59
+ # ==== Options
60
+ # * <tt>:location</tt> - location of the bucket
61
+ # (<tt>:eu</tt> or <tt>us</tt>)
62
+ # * Any other options are passed through to
63
+ # Connection#request
64
+ def save(options = {})
65
+ options = {:location => options} unless options.is_a?(Hash)
66
+ create_bucket_configuration(options)
67
+ true
68
+ end
69
+
70
+ # Returns true if the name of the bucket can be used like +VHOST+
71
+ # name. If the bucket contains characters like underscore it can't
72
+ # be used as +VHOST+ (e.g. <tt>bucket_name.s3.amazonaws.com</tt>)
73
+ def vhost?
74
+ self.class.vhost?(@name)
75
+ end
76
+
77
+ # Returns host name of the bucket according (see #vhost? method)
78
+ def host
79
+ vhost? ? "#@name.#{HOST}" : "#{HOST}"
80
+ end
81
+
82
+ # Returns path prefix for non +VHOST+ bucket. Path prefix is used
83
+ # instead of +VHOST+ name, e.g. "bucket_name/"
84
+ def path_prefix
85
+ vhost? ? "" : "#@name/"
86
+ end
87
+
88
+ # Returns the objects in the bucket and caches the result
89
+ def objects
90
+ Proxy.new(lambda { list_bucket }, :owner => self, :extend => ObjectsExtension)
91
+ end
92
+
93
+ # Returns the object with the given key. Does not check whether the
94
+ # object exists. But also does not issue any HTTP requests, so it's
95
+ # much faster than objects.find
96
+ def object(key)
97
+ Object.send(:new, self, :key => key)
98
+ end
99
+
100
+ def inspect #:nodoc:
101
+ "#<#{self.class}:#{name}>"
102
+ end
103
+
104
+ def self.vhost?(name)
105
+ "#{name}.#{HOST}" =~ /\A#{URI::REGEXP::PATTERN::HOSTNAME}\Z/
106
+ end
107
+
108
+ private
109
+
110
+ attr_writer :service
111
+
112
+ def location_constraint
113
+ response = bucket_request(:get, :params => {:location => nil})
114
+ parse_location_constraint(response.body)
115
+ end
116
+
117
+ def list_bucket(options = {})
118
+ response = bucket_request(:get, :params => options)
119
+ max_keys = options[:max_keys]
120
+ objects_attributes = parse_list_bucket_result(response.body)
121
+
122
+ # If there are more than 1000 objects S3 truncates listing and
123
+ # we need to request another listing for the remaining objects.
124
+ while parse_is_truncated(response.body)
125
+ next_request_options = {:marker => objects_attributes.last[:key]}
126
+
127
+ if max_keys
128
+ break if objects_attributes.length >= max_keys
129
+ next_request_options[:max_keys] = max_keys - objects_attributes.length
130
+ end
131
+
132
+ response = bucket_request(:get, :params => options.merge(next_request_options))
133
+ objects_attributes += parse_list_bucket_result(response.body)
134
+ end
135
+
136
+ objects_attributes.map { |object_attributes| Object.send(:new, self, object_attributes) }
137
+ end
138
+
139
+ def bucket_headers(options = {})
140
+ response = bucket_request(:head, :params => options)
141
+ rescue Error::ResponseError => e
142
+ if e.response.code.to_i == 404
143
+ raise Error::ResponseError.exception("NoSuchBucket").new("The specified bucket does not exist.", nil)
144
+ else
145
+ raise e
146
+ end
147
+ end
148
+
149
+ def create_bucket_configuration(options = {})
150
+ location = options[:location].to_s.upcase if options[:location]
151
+ options[:headers] ||= {}
152
+ if location and location != "US"
153
+ options[:body] = "<CreateBucketConfiguration><LocationConstraint>#{location}</LocationConstraint></CreateBucketConfiguration>"
154
+ options[:headers][:content_type] = "application/xml"
155
+ end
156
+ bucket_request(:put, options)
157
+ end
158
+
159
+ def delete_bucket
160
+ bucket_request(:delete)
161
+ end
162
+
163
+ def initialize(service, name) #:nodoc:
164
+ self.service = service
165
+ self.name = name
166
+ end
167
+
168
+ def name=(name)
169
+ raise ArgumentError.new("Invalid bucket name: #{name}") unless name_valid?(name)
170
+ @name = name
171
+ end
172
+
173
+ def bucket_request(method, options = {})
174
+ path = "#{path_prefix}#{options[:path]}"
175
+ service_request(method, options.merge(:host => host, :path => path))
176
+ end
177
+
178
+ def name_valid?(name)
179
+ name =~ /\A[a-z0-9][a-z0-9\._-]{2,254}\Z/i and name !~ /\A#{URI::REGEXP::PATTERN::IPV4ADDR}\Z/
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,26 @@
1
+ module S3
2
+ module BucketsExtension
3
+ # Builds new bucket with given name
4
+ def build(name)
5
+ Bucket.send(:new, proxy_owner, name)
6
+ end
7
+
8
+ # Finds the bucket with given name
9
+ def find_first(name)
10
+ bucket = build(name)
11
+ bucket.retrieve
12
+ end
13
+ alias :find :find_first
14
+
15
+ # Finds all buckets in the service
16
+ def find_all
17
+ proxy_target
18
+ end
19
+
20
+ # Destroys all buckets in the service. Doesn't destroy non-empty
21
+ # buckets by default, pass true to force destroy (USE WITH CARE!).
22
+ def destroy_all(force = false)
23
+ proxy_target.each { |bucket| bucket.destroy(force) }
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,225 @@
1
+ module S3
2
+
3
+ # Class responsible for handling connections to amazon hosts
4
+ class Connection
5
+ include Parser
6
+
7
+ attr_accessor :access_key_id, :secret_access_key, :use_ssl, :timeout, :debug, :proxy
8
+ alias :use_ssl? :use_ssl
9
+
10
+ # Creates new connection object.
11
+ #
12
+ # ==== Options
13
+ # * <tt>:access_key_id</tt> - Access key id (REQUIRED)
14
+ # * <tt>:secret_access_key</tt> - Secret access key (REQUIRED)
15
+ # * <tt>:use_ssl</tt> - Use https or http protocol (false by
16
+ # default)
17
+ # * <tt>:debug</tt> - Display debug information on the STDOUT
18
+ # (false by default)
19
+ # * <tt>:timeout</tt> - Timeout to use by the Net::HTTP object
20
+ # (60 by default)
21
+ # * <tt>:proxy</tt> - Hash for Net::HTTP Proxy settings
22
+ # { :host => "proxy.mydomain.com", :port => "80, :user => "user_a", :password => "secret" }
23
+ # * <tt>:proxy</tt> - Hash for Net::HTTP Proxy settings
24
+ # * <tt>:chunk_size</tt> - Size of a chunk when streaming
25
+ # (1048576 (1 MiB) by default)
26
+ def initialize(options = {})
27
+ @access_key_id = options.fetch(:access_key_id)
28
+ @secret_access_key = options.fetch(:secret_access_key)
29
+ @use_ssl = options.fetch(:use_ssl, false)
30
+ @debug = options.fetch(:debug, false)
31
+ @timeout = options.fetch(:timeout, 60)
32
+ @proxy = options.fetch(:proxy, nil)
33
+ @chunk_size = options.fetch(:chunk_size, 1048576)
34
+ end
35
+
36
+ # Makes request with given HTTP method, sets missing parameters,
37
+ # adds signature to request header and returns response object
38
+ # (Net::HTTPResponse)
39
+ #
40
+ # ==== Parameters
41
+ # * <tt>method</tt> - HTTP Method symbol, can be <tt>:get</tt>,
42
+ # <tt>:put</tt>, <tt>:delete</tt>
43
+ #
44
+ # ==== Options:
45
+ # * <tt>:host</tt> - Hostname to connecto to, defaults
46
+ # to <tt>s3.amazonaws.com</tt>
47
+ # * <tt>:path</tt> - path to send request to (REQUIRED)
48
+ # * <tt>:body</tt> - Request body, only meaningful for
49
+ # <tt>:put</tt> request
50
+ # * <tt>:params</tt> - Parameters to add to query string for
51
+ # request, can be String or Hash
52
+ # * <tt>:headers</tt> - Hash of headers fields to add to request
53
+ # header
54
+ #
55
+ # ==== Returns
56
+ # Net::HTTPResponse object -- response from the server
57
+ def request(method, options)
58
+ host = options.fetch(:host, HOST)
59
+ path = options.fetch(:path)
60
+ body = options.fetch(:body, nil)
61
+ params = options.fetch(:params, {})
62
+ headers = options.fetch(:headers, {})
63
+
64
+ # Must be done before adding params
65
+ # Encodes all characters except forward-slash (/) and explicitly legal URL characters
66
+ path = URI.escape(path, /[^#{URI::REGEXP::PATTERN::UNRESERVED}\/]/)
67
+
68
+ if params
69
+ params = params.is_a?(String) ? params : self.class.parse_params(params)
70
+ path << "?#{params}"
71
+ end
72
+
73
+ request = Request.new(@chunk_size, method.to_s.upcase, !!body, method.to_s.upcase != "HEAD", path)
74
+
75
+ headers = self.class.parse_headers(headers)
76
+ headers.each do |key, value|
77
+ request[key] = value
78
+ end
79
+
80
+ if body
81
+ if body.respond_to?(:read)
82
+ request.body_stream = body
83
+ else
84
+ request.body = body
85
+ end
86
+ request.content_length = body.respond_to?(:lstat) ? body.stat.size : body.size
87
+ end
88
+
89
+ send_request(host, request)
90
+ end
91
+
92
+ # Helper function to parser parameters and create single string of
93
+ # params added to questy string
94
+ #
95
+ # ==== Parameters
96
+ # * <tt>params</tt> - Hash of parameters
97
+ #
98
+ # ==== Returns
99
+ # String -- containing all parameters joined in one params string,
100
+ # i.e. <tt>param1=val&param2&param3=0</tt>
101
+ def self.parse_params(params)
102
+ interesting_keys = [:max_keys, :prefix, :marker, :delimiter, :location]
103
+
104
+ result = []
105
+ params.each do |key, value|
106
+ if interesting_keys.include?(key)
107
+ parsed_key = key.to_s.gsub("_", "-")
108
+ case value
109
+ when nil
110
+ result << parsed_key
111
+ else
112
+ result << "#{parsed_key}=#{value}"
113
+ end
114
+ end
115
+ end
116
+ result.join("&")
117
+ end
118
+
119
+ # Helper function to change headers from symbols, to in correct
120
+ # form (i.e. with '-' instead of '_')
121
+ #
122
+ # ==== Parameters
123
+ # * <tt>headers</tt> - Hash of pairs <tt>headername => value</tt>,
124
+ # where value can be Range (for Range header) or any other value
125
+ # which can be translated to string
126
+ #
127
+ # ==== Returns
128
+ # Hash of headers translated from symbol to string, containing
129
+ # only interesting headers
130
+ def self.parse_headers(headers)
131
+ interesting_keys = [:content_type, :cache_control, :x_amz_acl, :x_amz_storage_class, :range,
132
+ :if_modified_since, :if_unmodified_since,
133
+ :if_match, :if_none_match,
134
+ :content_disposition, :content_encoding,
135
+ :x_amz_copy_source, :x_amz_metadata_directive,
136
+ :x_amz_copy_source_if_match,
137
+ :x_amz_copy_source_if_none_match,
138
+ :x_amz_copy_source_if_unmodified_since,
139
+ :x_amz_copy_source_if_modified_since]
140
+
141
+ parsed_headers = {}
142
+ if headers
143
+ headers.each do |key, value|
144
+ if interesting_keys.include?(key)
145
+ parsed_key = key.to_s.gsub("_", "-")
146
+ parsed_value = value
147
+ case value
148
+ when Range
149
+ parsed_value = "bytes=#{value.first}-#{value.last}"
150
+ end
151
+ parsed_headers[parsed_key] = parsed_value
152
+ end
153
+ end
154
+ end
155
+ parsed_headers
156
+ end
157
+
158
+ private
159
+
160
+ def port
161
+ use_ssl ? 443 : 80
162
+ end
163
+
164
+ def proxy_settings
165
+ @proxy.values_at(:host, :port, :user, :password) unless @proxy.nil? || @proxy.empty?
166
+ end
167
+
168
+ def http(host)
169
+ http = Net::HTTP.new(host, port, *proxy_settings)
170
+ http.set_debug_output(STDOUT) if @debug
171
+ http.use_ssl = @use_ssl
172
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE if @use_ssl
173
+ http.read_timeout = @timeout if @timeout
174
+ http
175
+ end
176
+
177
+ def send_request(host, request, skip_authorization = false)
178
+ response = http(host).start do |http|
179
+ host = http.address
180
+
181
+ request["Date"] ||= Time.now.httpdate
182
+
183
+ if request.body
184
+ request["Content-Type"] ||= "application/octet-stream"
185
+ request["Content-MD5"] = Base64.encode64(Digest::MD5.digest(request.body)).chomp unless request.body.empty?
186
+ end
187
+
188
+ unless skip_authorization
189
+ request["Authorization"] = Signature.generate(:host => host,
190
+ :request => request,
191
+ :access_key_id => access_key_id,
192
+ :secret_access_key => secret_access_key)
193
+ end
194
+
195
+ http.request(request)
196
+ end
197
+
198
+ if response.code.to_i == 307
199
+ if response.body
200
+ doc = Document.new response.body
201
+ send_request(doc.elements["Error"].elements["Endpoint"].text, request, true)
202
+ end
203
+ else
204
+ handle_response(response)
205
+ end
206
+ end
207
+
208
+ def handle_response(response)
209
+ case response.code.to_i
210
+ when 200...300
211
+ response
212
+ when 300...600
213
+ if response.body.nil? || response.body.empty?
214
+ raise Error::ResponseError.new(nil, response)
215
+ else
216
+ code, message = parse_error(response.body)
217
+ raise Error::ResponseError.exception(code).new(message, response)
218
+ end
219
+ else
220
+ raise(ConnectionError.new(response, "Unknown response code: #{response.code}"))
221
+ end
222
+ response
223
+ end
224
+ end
225
+ end