qoobaa-s3 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,152 @@
1
+ module S3
2
+ class Connection
3
+ attr_accessor :access_key_id, :secret_access_key, :use_ssl, :timeout, :debug
4
+ alias :use_ssl? :use_ssl
5
+
6
+ def initialize(options = {})
7
+ @access_key_id = options[:access_key_id]
8
+ @secret_access_key = options[:secret_access_key]
9
+ @use_ssl = options[:use_ssl] || false
10
+ @debug = options[:debug]
11
+ @timeout = options[:timeout]
12
+ end
13
+
14
+ def request(method, options)
15
+ host = options[:host] || HOST
16
+ path = options[:path] or raise ArgumentError.new("no path given")
17
+ body = options[:body]
18
+ params = options[:params]
19
+ headers = options[:headers]
20
+
21
+ if params
22
+ params = params.is_a?(String) ? params : self.class.parse_params(params)
23
+ path << "?#{params}"
24
+ end
25
+
26
+ path = URI.escape(path)
27
+ request = request_class(method).new(path)
28
+
29
+ headers = self.class.parse_headers(headers)
30
+ headers.each do |key, value|
31
+ request[key] = value
32
+ end
33
+
34
+ request.body = body
35
+
36
+ send_request(host, request)
37
+ end
38
+
39
+ def self.parse_params(params)
40
+ interesting_keys = [:max_keys, :prefix, :marker, :delimiter, :location]
41
+
42
+ result = []
43
+ params.each do |key, value|
44
+ if interesting_keys.include?(key)
45
+ parsed_key = key.to_s.gsub("_", "-")
46
+ case value
47
+ when nil
48
+ result << parsed_key
49
+ else
50
+ result << "#{parsed_key}=#{value}"
51
+ end
52
+ end
53
+ end
54
+ result.join("&")
55
+ end
56
+
57
+ def self.parse_headers(headers)
58
+ interesting_keys = [:content_type, :x_amz_acl, :range,
59
+ :if_modified_since, :if_unmodified_since,
60
+ :if_match, :if_none_match,
61
+ :content_disposition, :content_encoding,
62
+ :x_amz_copy_source, :x_amz_metadata_directive,
63
+ :x_amz_copy_source_if_match,
64
+ :x_amz_copy_source_if_none_match,
65
+ :x_amz_copy_source_if_unmodified_since,
66
+ :x_amz_copy_source_if_modified_since]
67
+
68
+ parsed_headers = {}
69
+ if headers
70
+ headers.each do |key, value|
71
+ if interesting_keys.include?(key)
72
+ parsed_key = key.to_s.gsub("_", "-")
73
+ parsed_value = value
74
+ case value
75
+ when Range
76
+ parsed_value = "bytes=#{value.first}-#{value.last}"
77
+ end
78
+ parsed_headers[parsed_key] = parsed_value
79
+ end
80
+ end
81
+ end
82
+ parsed_headers
83
+ end
84
+
85
+ private
86
+
87
+ def request_class(method)
88
+ case method
89
+ when :get
90
+ request_class = Net::HTTP::Get
91
+ when :put
92
+ request_class = Net::HTTP::Put
93
+ when :delete
94
+ request_class = Net::HTTP::Delete
95
+ end
96
+ end
97
+
98
+ def port
99
+ use_ssl ? 443 : 80
100
+ end
101
+
102
+ def http(host)
103
+ http = Net::HTTP.new(host, port)
104
+ http.set_debug_output(STDOUT) if @debug
105
+ http.use_ssl = @use_ssl
106
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE if @use_ssl
107
+ http.read_timeout = @timeout if @timeout
108
+ http
109
+ end
110
+
111
+ def send_request(host, request)
112
+ response = http(host).start do |http|
113
+ host = http.address
114
+
115
+ request['Date'] ||= Time.now.httpdate
116
+
117
+ if request.body
118
+ request["Content-Type"] ||= "application/octet-stream"
119
+ request["Content-MD5"] = Base64.encode64(Digest::MD5.digest(request.body)).chomp
120
+ end
121
+
122
+ request["Authorization"] = S3::Signature.generate(:host => host,
123
+ :request => request,
124
+ :access_key_id => access_key_id,
125
+ :secret_access_key => secret_access_key)
126
+ http.request(request)
127
+ end
128
+
129
+ handle_response(response)
130
+ end
131
+
132
+ def handle_response(response)
133
+ case response.code.to_i
134
+ when 200...300
135
+ response
136
+ when 300...600
137
+ if response.body.nil? || response.body.empty?
138
+ raise S3::Error::ResponseError.new(nil, response)
139
+ else
140
+ xml = XmlSimple.xml_in(response.body)
141
+ message = xml["Message"].first
142
+ code = xml["Code"].first
143
+ raise S3::Error::ResponseError.exception(code).new(message, response)
144
+ end
145
+ else
146
+ raise(ConnectionError.new(response, "Unknown response code: #{response.code}"))
147
+ end
148
+ response
149
+ end
150
+
151
+ end
152
+ end
@@ -0,0 +1,94 @@
1
+ module S3
2
+ module Error
3
+ class Exception < StandardError
4
+ end
5
+
6
+ # All responses with a code between 300 and 599 that contain an <Error></Error> body are wrapped in an
7
+ # ErrorResponse which contains an Error object. This Error class generates a custom exception with the name
8
+ # of the xml Error and its message. All such runtime generated exception classes descend from ResponseError
9
+ # and contain the ErrorResponse object so that all code that makes a request can rescue ResponseError and get
10
+ # access to the ErrorResponse.
11
+ class ResponseError < Exception
12
+ attr_reader :response
13
+ def initialize(message, response)
14
+ @response = response
15
+ super(message)
16
+ end
17
+
18
+ def self.exception(code)
19
+ S3::Error.const_get(code)
20
+ rescue NameError
21
+ ResponseError
22
+ end
23
+ end
24
+
25
+ #:stopdoc:
26
+
27
+ class AccessDenied < ResponseError; end
28
+ class AccountProblem < ResponseError; end
29
+ class AmbiguousGrantByEmailAddress < ResponseError; end
30
+ class BadDigest < ResponseError; end
31
+ class BucketAlreadyExists < ResponseError; end
32
+ class BucketAlreadyOwnedByYou < ResponseError; end
33
+ class BucketNotEmpty < ResponseError; end
34
+ class CredentialsNotSupported < ResponseError; end
35
+ class CrossLocationLoggingProhibited < ResponseError; end
36
+ class EntityTooSmall < ResponseError; end
37
+ class EntityTooLarge < ResponseError; end
38
+ class ExpiredToken < ResponseError; end
39
+ class IncompleteBody < ResponseError; end
40
+ class IncorrectNumberOfFilesInPostRequestPOST < ResponseError; end
41
+ class InlineDataTooLarge < ResponseError; end
42
+ class InternalError < ResponseError; end
43
+ class InvalidAccessKeyId < ResponseError; end
44
+ class InvalidAddressingHeader < ResponseError; end
45
+ class InvalidArgument < ResponseError; end
46
+ class InvalidBucketName < ResponseError; end
47
+ class InvalidDigest < ResponseError; end
48
+ class InvalidLocationConstraint < ResponseError; end
49
+ class InvalidPayer < ResponseError; end
50
+ class InvalidPolicyDocument < ResponseError; end
51
+ class InvalidRange < ResponseError; end
52
+ class InvalidSecurity < ResponseError; end
53
+ class InvalidSOAPRequest < ResponseError; end
54
+ class InvalidStorageClass < ResponseError; end
55
+ class InvalidTargetBucketForLogging < ResponseError; end
56
+ class InvalidToken < ResponseError; end
57
+ class InvalidURI < ResponseError; end
58
+ class KeyTooLong < ResponseError; end
59
+ class MalformedACLError < ResponseError; end
60
+ class MalformedACLError < ResponseError; end
61
+ class MalformedPOSTRequest < ResponseError; end
62
+ class MalformedXML < ResponseError; end
63
+ class MaxMessageLengthExceeded < ResponseError; end
64
+ class MaxPostPreDataLengthExceededErrorYour < ResponseError; end
65
+ class MetadataTooLarge < ResponseError; end
66
+ class MethodNotAllowed < ResponseError; end
67
+ class MissingAttachment < ResponseError; end
68
+ class MissingContentLength < ResponseError; end
69
+ class MissingRequestBodyError < ResponseError; end
70
+ class MissingSecurityElement < ResponseError; end
71
+ class MissingSecurityHeader < ResponseError; end
72
+ class NoLoggingStatusForKey < ResponseError; end
73
+ class NoSuchBucket < ResponseError; end
74
+ class NoSuchKey < ResponseError; end
75
+ class NotImplemented < ResponseError; end
76
+ class NotSignedUp < ResponseError; end
77
+ class OperationAborted < ResponseError; end
78
+ class PermanentRedirect < ResponseError; end
79
+ class PreconditionFailed < ResponseError; end
80
+ class Redirect < ResponseError; end
81
+ class RequestIsNotMultiPartContent < ResponseError; end
82
+ class RequestTimeout < ResponseError; end
83
+ class RequestTimeTooSkewed < ResponseError; end
84
+ class RequestTorrentOfBucketError < ResponseError; end
85
+ class SignatureDoesNotMatch < ResponseError; end
86
+ class SlowDown < ResponseError; end
87
+ class TemporaryRedirect < ResponseError; end
88
+ class TokenRefreshRequired < ResponseError; end
89
+ class TooManyBuckets < ResponseError; end
90
+ class UnexpectedContent < ResponseError; end
91
+ class UnresolvableGrantByEmailAddress < ResponseError; end
92
+ class UserKeyMustBeSpecified < ResponseError; end
93
+ end
94
+ end
data/lib/s3/object.rb ADDED
@@ -0,0 +1,153 @@
1
+ module S3
2
+ class Object
3
+ extend Forwardable
4
+
5
+ attr_accessor :content_type, :content_disposition, :content_encoding
6
+ attr_reader :last_modified, :etag, :size, :bucket, :key, :acl
7
+ attr_writer :content
8
+
9
+ def_instance_delegators :bucket, :name, :service, :bucket_request, :vhost?, :host, :path_prefix
10
+ def_instance_delegators :service, :protocol, :port
11
+
12
+ def full_key
13
+ [name, key].join("/")
14
+ end
15
+
16
+ def key=(key)
17
+ raise ArgumentError.new("Invalid key name: #{key}") unless key_valid?(key)
18
+ @key ||= key
19
+ end
20
+
21
+ def acl=(acl)
22
+ @acl = acl.to_s.gsub("_", "-")
23
+ end
24
+
25
+ def retrieve
26
+ response = object_request(:get, :headers => { :range => 0..0 })
27
+ parse_headers(response)
28
+ self
29
+ end
30
+
31
+ def exists?
32
+ retrieve
33
+ true
34
+ rescue Error::NoSuchKey
35
+ false
36
+ end
37
+
38
+ def content(reload = false)
39
+ if reload or @content.nil?
40
+ response = object_request(:get)
41
+ parse_headers(response)
42
+ self.content = response.body
43
+ end
44
+ @content
45
+ end
46
+
47
+ def save
48
+ body = content.is_a?(IO) ? content.read : content
49
+ response = object_request(:put, :body => body, :headers => dump_headers)
50
+ parse_headers(response)
51
+ true
52
+ end
53
+
54
+ def copy(options = {})
55
+ key = options[:key] || self.key
56
+ bucket = options[:bucket] || self.bucket
57
+
58
+ headers = {}
59
+ headers[:x_amz_acl] = options[:acl] || acl || "public-read"
60
+ headers[:content_type] = options[:content_type] || content_type || "application/octet-stream"
61
+ headers[:content_encoding] = options[:content_encoding] if options[:content_encoding]
62
+ headers[:content_disposition] = options[:content_disposition] if options[:content_disposition]
63
+ headers[:x_amz_copy_source] = full_key
64
+ headers[:x_amz_metadata_directive] = "REPLACE"
65
+ headers[:x_amz_copy_source_if_match] = options[:if_match] if options[:if_match]
66
+ headers[:x_amz_copy_source_if_none_match] = options[:if_none_match] if options[:if_none_match]
67
+ headers[:x_amz_copy_source_if_unmodified_since] = options[:if_modified_since] if options[:if_modified_since]
68
+ headers[:x_amz_copy_source_if_modified_since] = options[:if_unmodified_since] if options[:if_unmodified_since]
69
+
70
+ response = bucket.send(:bucket_request, :put, :path => key, :headers => headers)
71
+ self.class.parse_copied(:object => self, :bucket => bucket, :key => key, :body => response.body, :headers => headers)
72
+ end
73
+
74
+ def destroy
75
+ object_request(:delete)
76
+ true
77
+ end
78
+
79
+ def url
80
+ "#{protocol}#{host}/#{path_prefix}#{key}"
81
+ end
82
+
83
+ def cname_url
84
+ "#{protocol}#{name}/#{key}" if bucket.vhost?
85
+ end
86
+
87
+ def inspect
88
+ "#<#{self.class}:/#{name}/#{key}>"
89
+ end
90
+
91
+ def initialize(bucket, key, options = {})
92
+ self.bucket = bucket
93
+ self.key = key
94
+ self.last_modified = options[:last_modified]
95
+ self.etag = options[:etag]
96
+ self.size = options[:size]
97
+ end
98
+
99
+ private
100
+
101
+ attr_writer :last_modified, :etag, :size, :original_key, :bucket
102
+
103
+ def object_request(method, options = {})
104
+ bucket_request(method, options.merge(:path => key))
105
+ end
106
+
107
+ def last_modified=(last_modified)
108
+ @last_modified = Time.parse(last_modified) if last_modified
109
+ end
110
+
111
+ def etag=(etag)
112
+ @etag = etag[1..-2] if etag
113
+ end
114
+
115
+ def dump_headers
116
+ headers = {}
117
+ headers[:x_amz_acl] = @acl || "public-read"
118
+ headers[:content_type] = @content_type || "application/octet-stream"
119
+ headers[:content_encoding] = @content_encoding if @content_encoding
120
+ headers[:content_disposition] = @content_disposition if @content_disposition
121
+ headers
122
+ end
123
+
124
+ def key_valid?(key)
125
+ key !~ /\/\//
126
+ end
127
+
128
+ def parse_headers(response)
129
+ self.etag = response["etag"]
130
+ self.content_type = response["content-type"]
131
+ self.content_disposition = response["content-disposition"]
132
+ self.content_encoding = response["content-encoding"]
133
+ self.last_modified = response["last-modified"]
134
+ self.size = response["content-length"]
135
+ if response["content-range"]
136
+ self.size = response["content-range"].sub(/[^\/]+\//, "").to_i
137
+ end
138
+ end
139
+
140
+ def self.parse_copied(options)
141
+ xml = XmlSimple.xml_in(options[:body])
142
+ etag = xml["ETag"].first
143
+ last_modified = xml["LastModified"].first
144
+ size = options[:object].size
145
+ object = Object.new(options[:bucket], options[:key], :etag => etag, :last_modified => last_modified, :size => size)
146
+ object.acl = options[:headers][:x_amz_acl]
147
+ object.content_type = options[:headers][:content_type]
148
+ object.content_encoding = options[:headers][:content_encoding]
149
+ object.content_disposition = options[:headers][:content_disposition]
150
+ object
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,58 @@
1
+ # Copyright (c) 2008 Ryan Daigle
2
+
3
+ # Permission is hereby granted, free of charge, to any person
4
+ # obtaining a copy of this software and associated documentation files
5
+ # (the "Software"), to deal in the Software without restriction,
6
+ # including without limitation the rights to use, copy, modify, merge,
7
+ # publish, distribute, sublicense, and/or sell copies of the Software,
8
+ # 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
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
18
+ # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
19
+ # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ module S3
24
+ module Roxy
25
+ module Moxie
26
+ # Set up this class to proxy on the given name
27
+ def proxy(name, options = {}, &block)
28
+
29
+ # Make sure args are OK
30
+ original_method = method_defined?(name) ? instance_method(name) : nil
31
+ raise "Cannot proxy an existing method, \"#{name}\", and also have a :to option. Please use one or the other." if
32
+ original_method and options[:to]
33
+
34
+ # If we're proxying an existing method, we need to store
35
+ # the original method and move it out of the way so
36
+ # we can take over
37
+ if original_method
38
+ new_method = "proxied_#{name}"
39
+ alias_method new_method, "#{name}"
40
+ options[:to] = original_method
41
+ end
42
+
43
+ # Thanks to Jerry for this simplification of my original class_eval approach
44
+ # http://ryandaigle.com/articles/2008/11/10/implement-ruby-proxy-objects-with-roxy/comments/8059#comment-8059
45
+ if !original_method or original_method.arity == 0
46
+ define_method name do
47
+ @proxy_for ||= {}
48
+ @proxy_for[name] ||= Proxy.new(self, options, nil, &block)
49
+ end
50
+ else
51
+ define_method name do |*args|
52
+ Proxy.new(self, options, args, &block)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end