s3 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +6 -0
- data/LICENSE +20 -0
- data/README.rdoc +48 -0
- data/Rakefile +58 -0
- data/VERSION +1 -0
- data/bin/stree +187 -0
- data/extra/stree_backend.rb +159 -0
- data/lib/stree/bucket.rb +186 -0
- data/lib/stree/connection.rb +199 -0
- data/lib/stree/exceptions.rb +108 -0
- data/lib/stree/object.rb +210 -0
- data/lib/stree/parser.rb +48 -0
- data/lib/stree/roxy/moxie.rb +58 -0
- data/lib/stree/roxy/proxy.rb +72 -0
- data/lib/stree/service.rb +110 -0
- data/lib/stree/signature.rb +157 -0
- data/lib/stree.rb +24 -0
- data/test/bucket_test.rb +231 -0
- data/test/connection_test.rb +164 -0
- data/test/object_test.rb +164 -0
- data/test/service_test.rb +128 -0
- data/test/signature_test.rb +143 -0
- data/test/test_helper.rb +11 -0
- metadata +94 -0
@@ -0,0 +1,199 @@
|
|
1
|
+
module Stree
|
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
|
8
|
+
alias :use_ssl? :use_ssl
|
9
|
+
|
10
|
+
# ==== Parameters:
|
11
|
+
# +options+:: Hash of options
|
12
|
+
#
|
13
|
+
# ==== Options:
|
14
|
+
# +access_key_id+:: access key id
|
15
|
+
# +secret_access_key+:: secret access key
|
16
|
+
# +use_ssl+:: optional, defaults to false
|
17
|
+
# +debug+:: optional, defaults to false
|
18
|
+
# +timeout+:: optional, for Net::HTTP
|
19
|
+
def initialize(options = {})
|
20
|
+
@access_key_id = options[:access_key_id]
|
21
|
+
@secret_access_key = options[:secret_access_key]
|
22
|
+
@use_ssl = options[:use_ssl] || false
|
23
|
+
@debug = options[:debug]
|
24
|
+
@timeout = options[:timeout]
|
25
|
+
end
|
26
|
+
|
27
|
+
# Makes request with given HTTP method, sets missing parameters,
|
28
|
+
# adds signature to request header and returns response object
|
29
|
+
# (Net::HTTPResponse)
|
30
|
+
#
|
31
|
+
# ==== Parameters:
|
32
|
+
# +method+:: HTTP Method symbol, can be :get, :put, :delete
|
33
|
+
# +options+:: hash of options
|
34
|
+
#
|
35
|
+
# ==== Options:
|
36
|
+
# +host+:: hostname to connecto to, optional, defaults to s3.amazonaws.com[s3.amazonaws.com]
|
37
|
+
# +path+:: path to send request to, required, throws ArgumentError if not given
|
38
|
+
# +body+:: request body, only meaningful for :put request
|
39
|
+
# +params+:: parameters to add to query string for request, can be String or Hash
|
40
|
+
# +headers+:: Hash of headers fields to add to request header
|
41
|
+
#
|
42
|
+
# ==== Returns:
|
43
|
+
# Net::HTTPResponse object -- response from remote server
|
44
|
+
def request(method, options)
|
45
|
+
host = options[:host] || HOST
|
46
|
+
path = options[:path] or raise ArgumentError.new("no path given")
|
47
|
+
body = options[:body]
|
48
|
+
params = options[:params]
|
49
|
+
headers = options[:headers]
|
50
|
+
|
51
|
+
if params
|
52
|
+
params = params.is_a?(String) ? params : self.class.parse_params(params)
|
53
|
+
path << "?#{params}"
|
54
|
+
end
|
55
|
+
|
56
|
+
path = URI.escape(path)
|
57
|
+
request = request_class(method).new(path)
|
58
|
+
|
59
|
+
headers = self.class.parse_headers(headers)
|
60
|
+
headers.each do |key, value|
|
61
|
+
request[key] = value
|
62
|
+
end
|
63
|
+
|
64
|
+
request.body = body
|
65
|
+
|
66
|
+
send_request(host, request)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Helper function to parser parameters and create single string of params
|
70
|
+
# added to questy string
|
71
|
+
#
|
72
|
+
# ==== Parameters:
|
73
|
+
# +params+: Hash of parameters if form <tt>key => value|nil</tt>
|
74
|
+
#
|
75
|
+
# ==== Returns:
|
76
|
+
# String -- containing all parameters joined in one params string,
|
77
|
+
# i.e. <tt>param1=val¶m2¶m3=0</tt>
|
78
|
+
def self.parse_params(params)
|
79
|
+
interesting_keys = [:max_keys, :prefix, :marker, :delimiter, :location]
|
80
|
+
|
81
|
+
result = []
|
82
|
+
params.each do |key, value|
|
83
|
+
if interesting_keys.include?(key)
|
84
|
+
parsed_key = key.to_s.gsub("_", "-")
|
85
|
+
case value
|
86
|
+
when nil
|
87
|
+
result << parsed_key
|
88
|
+
else
|
89
|
+
result << "#{parsed_key}=#{value}"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
result.join("&")
|
94
|
+
end
|
95
|
+
|
96
|
+
# Helper function to change headers from symbols, to in correct
|
97
|
+
# form (i.e. with '-' instead of '_')
|
98
|
+
#
|
99
|
+
# ==== Parameters:
|
100
|
+
# +headers+:: Hash of pairs <tt>headername => value</tt>,
|
101
|
+
# where value can be Range (for Range header) or any other
|
102
|
+
# value which can be translated to string
|
103
|
+
#
|
104
|
+
# ==== Returns:
|
105
|
+
# Hash of headers translated from symbol to string,
|
106
|
+
# containing only interesting headers
|
107
|
+
def self.parse_headers(headers)
|
108
|
+
interesting_keys = [:content_type, :x_amz_acl, :range,
|
109
|
+
:if_modified_since, :if_unmodified_since,
|
110
|
+
:if_match, :if_none_match,
|
111
|
+
:content_disposition, :content_encoding,
|
112
|
+
:x_amz_copy_source, :x_amz_metadata_directive,
|
113
|
+
:x_amz_copy_source_if_match,
|
114
|
+
:x_amz_copy_source_if_none_match,
|
115
|
+
:x_amz_copy_source_if_unmodified_since,
|
116
|
+
:x_amz_copy_source_if_modified_since]
|
117
|
+
|
118
|
+
parsed_headers = {}
|
119
|
+
if headers
|
120
|
+
headers.each do |key, value|
|
121
|
+
if interesting_keys.include?(key)
|
122
|
+
parsed_key = key.to_s.gsub("_", "-")
|
123
|
+
parsed_value = value
|
124
|
+
case value
|
125
|
+
when Range
|
126
|
+
parsed_value = "bytes=#{value.first}-#{value.last}"
|
127
|
+
end
|
128
|
+
parsed_headers[parsed_key] = parsed_value
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
parsed_headers
|
133
|
+
end
|
134
|
+
|
135
|
+
private
|
136
|
+
|
137
|
+
def request_class(method)
|
138
|
+
case method
|
139
|
+
when :get
|
140
|
+
request_class = Net::HTTP::Get
|
141
|
+
when :put
|
142
|
+
request_class = Net::HTTP::Put
|
143
|
+
when :delete
|
144
|
+
request_class = Net::HTTP::Delete
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def port
|
149
|
+
use_ssl ? 443 : 80
|
150
|
+
end
|
151
|
+
|
152
|
+
def http(host)
|
153
|
+
http = Net::HTTP.new(host, port)
|
154
|
+
http.set_debug_output(STDOUT) if @debug
|
155
|
+
http.use_ssl = @use_ssl
|
156
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE if @use_ssl
|
157
|
+
http.read_timeout = @timeout if @timeout
|
158
|
+
http
|
159
|
+
end
|
160
|
+
|
161
|
+
def send_request(host, request)
|
162
|
+
response = http(host).start do |http|
|
163
|
+
host = http.address
|
164
|
+
|
165
|
+
request['Date'] ||= Time.now.httpdate
|
166
|
+
|
167
|
+
if request.body
|
168
|
+
request["Content-Type"] ||= "application/octet-stream"
|
169
|
+
request["Content-MD5"] = Base64.encode64(Digest::MD5.digest(request.body)).chomp
|
170
|
+
end
|
171
|
+
|
172
|
+
request["Authorization"] = Signature.generate(:host => host,
|
173
|
+
:request => request,
|
174
|
+
:access_key_id => access_key_id,
|
175
|
+
:secret_access_key => secret_access_key)
|
176
|
+
http.request(request)
|
177
|
+
end
|
178
|
+
|
179
|
+
handle_response(response)
|
180
|
+
end
|
181
|
+
|
182
|
+
def handle_response(response)
|
183
|
+
case response.code.to_i
|
184
|
+
when 200...300
|
185
|
+
response
|
186
|
+
when 300...600
|
187
|
+
if response.body.nil? || response.body.empty?
|
188
|
+
raise Error::ResponseError.new(nil, response)
|
189
|
+
else
|
190
|
+
code, message = parse_error(response.body)
|
191
|
+
raise Error::ResponseError.exception(code).new(message, response)
|
192
|
+
end
|
193
|
+
else
|
194
|
+
raise(ConnectionError.new(response, "Unknown response code: #{response.code}"))
|
195
|
+
end
|
196
|
+
response
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
module Stree
|
2
|
+
module Error
|
3
|
+
|
4
|
+
# All responses with a code between 300 and 599 that contain an
|
5
|
+
# <Error></Error> body are wrapped in an ErrorResponse which
|
6
|
+
# contains an Error object. This Error class generates a custom
|
7
|
+
# exception with the name of the xml Error and its message. All
|
8
|
+
# such runtime generated exception classes descend from
|
9
|
+
# ResponseError and contain the ErrorResponse object so that all
|
10
|
+
# code that makes a request can rescue ResponseError and get
|
11
|
+
# access to the ErrorResponse.
|
12
|
+
class ResponseError < StandardError
|
13
|
+
attr_reader :response
|
14
|
+
|
15
|
+
# ==== Parameters:
|
16
|
+
# +message+:: what went wrong
|
17
|
+
# +response+:: Net::HTTPResponse object or nil
|
18
|
+
def initialize(message, response)
|
19
|
+
@response = response
|
20
|
+
super(message)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Factory for all other Exception classes in module, each for every
|
24
|
+
# error response available from AmazonAWS
|
25
|
+
#
|
26
|
+
# ==== Parameters:
|
27
|
+
# +code+:: code name of exception
|
28
|
+
#
|
29
|
+
# ==== Returns:
|
30
|
+
# Descendant of ResponseError suitable for that exception code or ResponseError class
|
31
|
+
# if no class found
|
32
|
+
def self.exception(code)
|
33
|
+
Stree::Error.const_get(code)
|
34
|
+
rescue NameError
|
35
|
+
ResponseError
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
#:stopdoc:
|
40
|
+
|
41
|
+
class AccessDenied < ResponseError; end
|
42
|
+
class AccountProblem < ResponseError; end
|
43
|
+
class AmbiguousGrantByEmailAddress < ResponseError; end
|
44
|
+
class BadDigest < ResponseError; end
|
45
|
+
class BucketAlreadyExists < ResponseError; end
|
46
|
+
class BucketAlreadyOwnedByYou < ResponseError; end
|
47
|
+
class BucketNotEmpty < ResponseError; end
|
48
|
+
class CredentialsNotSupported < ResponseError; end
|
49
|
+
class CrossLocationLoggingProhibited < ResponseError; end
|
50
|
+
class EntityTooSmall < ResponseError; end
|
51
|
+
class EntityTooLarge < ResponseError; end
|
52
|
+
class ExpiredToken < ResponseError; end
|
53
|
+
class IncompleteBody < ResponseError; end
|
54
|
+
class IncorrectNumberOfFilesInPostRequestPOST < ResponseError; end
|
55
|
+
class InlineDataTooLarge < ResponseError; end
|
56
|
+
class InternalError < ResponseError; end
|
57
|
+
class InvalidAccessKeyId < ResponseError; end
|
58
|
+
class InvalidAddressingHeader < ResponseError; end
|
59
|
+
class InvalidArgument < ResponseError; end
|
60
|
+
class InvalidBucketName < ResponseError; end
|
61
|
+
class InvalidDigest < ResponseError; end
|
62
|
+
class InvalidLocationConstraint < ResponseError; end
|
63
|
+
class InvalidPayer < ResponseError; end
|
64
|
+
class InvalidPolicyDocument < ResponseError; end
|
65
|
+
class InvalidRange < ResponseError; end
|
66
|
+
class InvalidSecurity < ResponseError; end
|
67
|
+
class InvalidSOAPRequest < ResponseError; end
|
68
|
+
class InvalidStorageClass < ResponseError; end
|
69
|
+
class InvalidTargetBucketForLogging < ResponseError; end
|
70
|
+
class InvalidToken < ResponseError; end
|
71
|
+
class InvalidURI < ResponseError; end
|
72
|
+
class KeyTooLong < ResponseError; end
|
73
|
+
class MalformedACLError < ResponseError; end
|
74
|
+
class MalformedACLError < ResponseError; end
|
75
|
+
class MalformedPOSTRequest < ResponseError; end
|
76
|
+
class MalformedXML < ResponseError; end
|
77
|
+
class MaxMessageLengthExceeded < ResponseError; end
|
78
|
+
class MaxPostPreDataLengthExceededErrorYour < ResponseError; end
|
79
|
+
class MetadataTooLarge < ResponseError; end
|
80
|
+
class MethodNotAllowed < ResponseError; end
|
81
|
+
class MissingAttachment < ResponseError; end
|
82
|
+
class MissingContentLength < ResponseError; end
|
83
|
+
class MissingRequestBodyError < ResponseError; end
|
84
|
+
class MissingSecurityElement < ResponseError; end
|
85
|
+
class MissingSecurityHeader < ResponseError; end
|
86
|
+
class NoLoggingStatusForKey < ResponseError; end
|
87
|
+
class NoSuchBucket < ResponseError; end
|
88
|
+
class NoSuchKey < ResponseError; end
|
89
|
+
class NotImplemented < ResponseError; end
|
90
|
+
class NotSignedUp < ResponseError; end
|
91
|
+
class OperationAborted < ResponseError; end
|
92
|
+
class PermanentRedirect < ResponseError; end
|
93
|
+
class PreconditionFailed < ResponseError; end
|
94
|
+
class Redirect < ResponseError; end
|
95
|
+
class RequestIsNotMultiPartContent < ResponseError; end
|
96
|
+
class RequestTimeout < ResponseError; end
|
97
|
+
class RequestTimeTooSkewed < ResponseError; end
|
98
|
+
class RequestTorrentOfBucketError < ResponseError; end
|
99
|
+
class SignatureDoesNotMatch < ResponseError; end
|
100
|
+
class SlowDown < ResponseError; end
|
101
|
+
class TemporaryRedirect < ResponseError; end
|
102
|
+
class TokenRefreshRequired < ResponseError; end
|
103
|
+
class TooManyBuckets < ResponseError; end
|
104
|
+
class UnexpectedContent < ResponseError; end
|
105
|
+
class UnresolvableGrantByEmailAddress < ResponseError; end
|
106
|
+
class UserKeyMustBeSpecified < ResponseError; end
|
107
|
+
end
|
108
|
+
end
|
data/lib/stree/object.rb
ADDED
@@ -0,0 +1,210 @@
|
|
1
|
+
module Stree
|
2
|
+
|
3
|
+
# Class responsible for handling objects stored in S3 buckets
|
4
|
+
class Object
|
5
|
+
include Parser
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
attr_accessor :content_type, :content_disposition, :content_encoding
|
9
|
+
attr_reader :last_modified, :etag, :size, :bucket, :key, :acl
|
10
|
+
attr_writer :content
|
11
|
+
|
12
|
+
def_instance_delegators :bucket, :name, :service, :bucket_request, :vhost?, :host, :path_prefix
|
13
|
+
def_instance_delegators :service, :protocol, :port
|
14
|
+
private_class_method :new
|
15
|
+
|
16
|
+
# Compares the object with other object. Returns true if the key
|
17
|
+
# of the objects are the same, and both have the same buckets (see
|
18
|
+
# bucket equality)
|
19
|
+
def ==(other)
|
20
|
+
self.key == other.key and self.bucket == other.bucket
|
21
|
+
end
|
22
|
+
|
23
|
+
# Returns full key of the object: e.g. +bucket-name/object/key.ext+
|
24
|
+
def full_key
|
25
|
+
[name, key].join("/")
|
26
|
+
end
|
27
|
+
|
28
|
+
# Assigns a new +key+ to the object, raises ArgumentError if given
|
29
|
+
# key is not valid key name
|
30
|
+
def key=(key)
|
31
|
+
raise ArgumentError.new("Invalid key name: #{key}") unless key_valid?(key)
|
32
|
+
@key ||= key
|
33
|
+
end
|
34
|
+
|
35
|
+
# Assigns a new ACL to the object. Please note that ACL is not
|
36
|
+
# retrieved from the server and set to "public-read" by default.
|
37
|
+
# ==== Example
|
38
|
+
# object.acl = :public_read
|
39
|
+
def acl=(acl)
|
40
|
+
@acl = acl.to_s.gsub("_", "-") if acl
|
41
|
+
end
|
42
|
+
|
43
|
+
# Retrieves the object from the server. Method is used to download
|
44
|
+
# object information only (content-type, size and so on). It does
|
45
|
+
# NOT download the content of the object (use the content method
|
46
|
+
# to do it).
|
47
|
+
def retrieve
|
48
|
+
get_object(:headers => { :range => 0..0 })
|
49
|
+
self
|
50
|
+
end
|
51
|
+
|
52
|
+
# Retrieves the object from the server, returns true if the
|
53
|
+
# object exists or false otherwise. Uses retrieve method, but
|
54
|
+
# catches NoSuchKey exception and returns false when it happens
|
55
|
+
def exists?
|
56
|
+
retrieve
|
57
|
+
true
|
58
|
+
rescue Error::NoSuchKey
|
59
|
+
false
|
60
|
+
end
|
61
|
+
|
62
|
+
# Download the content of the object, and caches it. Pass true
|
63
|
+
# to clear the cache and download the object again.
|
64
|
+
def content(reload = false)
|
65
|
+
if reload or @content.nil?
|
66
|
+
get_object
|
67
|
+
end
|
68
|
+
@content
|
69
|
+
end
|
70
|
+
|
71
|
+
# Saves the object, returns true if successfull.
|
72
|
+
def save
|
73
|
+
put_object
|
74
|
+
true
|
75
|
+
end
|
76
|
+
|
77
|
+
# Copies the file to another key and/or bucket.
|
78
|
+
# ==== Options:
|
79
|
+
# +key+:: new key to store object in
|
80
|
+
# +bucket+:: new bucket to store object in (instance of Stree::Bucket)
|
81
|
+
# +acl+:: acl of the copied object (default: "public-read")
|
82
|
+
# +content_type+:: content type of the copied object (default: "application/octet-stream")
|
83
|
+
def copy(options = {})
|
84
|
+
copy_object(options)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Destroys the file on the server
|
88
|
+
def destroy
|
89
|
+
delete_object
|
90
|
+
true
|
91
|
+
end
|
92
|
+
|
93
|
+
# Returns Object's URL using protocol specified in Service,
|
94
|
+
# e.g. http://domain.com.s3.amazonaws.com/key/with/path.extension
|
95
|
+
def url
|
96
|
+
URI.escape("#{protocol}#{host}/#{path_prefix}#{key}")
|
97
|
+
end
|
98
|
+
|
99
|
+
# Returns Object's CNAME URL (without s3.amazonaws.com suffix)
|
100
|
+
# using protocol specified in Service,
|
101
|
+
# e.g. http://domain.com/key/with/path.extension. (you have to set
|
102
|
+
# the CNAME in your DNS before you use the CNAME URL schema).
|
103
|
+
def cname_url
|
104
|
+
URI.escape("#{protocol}#{name}/#{key}") if bucket.vhost?
|
105
|
+
end
|
106
|
+
|
107
|
+
def inspect #:nodoc:
|
108
|
+
"#<#{self.class}:/#{name}/#{key}>"
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
attr_writer :last_modified, :etag, :size, :original_key, :bucket
|
114
|
+
|
115
|
+
def copy_object(options = {})
|
116
|
+
key = options[:key] or raise ArgumentError, "No key given"
|
117
|
+
raise ArgumentError.new("Invalid key name: #{key}") unless key_valid?(key)
|
118
|
+
bucket = options[:bucket] || self.bucket
|
119
|
+
|
120
|
+
headers = {}
|
121
|
+
|
122
|
+
headers[:x_amz_acl] = options[:acl] || acl || "public-read"
|
123
|
+
headers[:content_type] = options[:content_type] || content_type || "application/octet-stream"
|
124
|
+
headers[:content_encoding] = options[:content_encoding] if options[:content_encoding]
|
125
|
+
headers[:content_disposition] = options[:content_disposition] if options[:content_disposition]
|
126
|
+
headers[:x_amz_copy_source] = full_key
|
127
|
+
headers[:x_amz_metadata_directive] = "REPLACE"
|
128
|
+
headers[:x_amz_copy_source_if_match] = options[:if_match] if options[:if_match]
|
129
|
+
headers[:x_amz_copy_source_if_none_match] = options[:if_none_match] if options[:if_none_match]
|
130
|
+
headers[:x_amz_copy_source_if_unmodified_since] = options[:if_modified_since] if options[:if_modified_since]
|
131
|
+
headers[:x_amz_copy_source_if_modified_since] = options[:if_unmodified_since] if options[:if_unmodified_since]
|
132
|
+
|
133
|
+
response = bucket.send(:bucket_request, :put, :path => key, :headers => headers)
|
134
|
+
object_attributes = parse_copy_object_result(response.body)
|
135
|
+
|
136
|
+
object = Object.send(:new, bucket, object_attributes.merge(:key => key, :size => size))
|
137
|
+
object.acl = response[:x_amz_acl]
|
138
|
+
object.content_type = response[:content_type]
|
139
|
+
object.content_encoding = response[:content_encoding]
|
140
|
+
object.content_disposition = response[:content_disposition]
|
141
|
+
object
|
142
|
+
end
|
143
|
+
|
144
|
+
def get_object(options = {})
|
145
|
+
response = object_request(:get, options)
|
146
|
+
parse_headers(response)
|
147
|
+
end
|
148
|
+
|
149
|
+
def put_object
|
150
|
+
body = content.is_a?(IO) ? content.read : content
|
151
|
+
response = object_request(:put, :body => body, :headers => dump_headers)
|
152
|
+
parse_headers(response)
|
153
|
+
end
|
154
|
+
|
155
|
+
def delete_object(options = {})
|
156
|
+
object_request(:delete)
|
157
|
+
end
|
158
|
+
|
159
|
+
def initialize(bucket, options = {})
|
160
|
+
self.bucket = bucket
|
161
|
+
self.key = options[:key]
|
162
|
+
self.last_modified = options[:last_modified]
|
163
|
+
self.etag = options[:etag]
|
164
|
+
self.size = options[:size]
|
165
|
+
end
|
166
|
+
|
167
|
+
def object_request(method, options = {})
|
168
|
+
bucket_request(method, options.merge(:path => key))
|
169
|
+
end
|
170
|
+
|
171
|
+
def last_modified=(last_modified)
|
172
|
+
@last_modified = Time.parse(last_modified) if last_modified
|
173
|
+
end
|
174
|
+
|
175
|
+
def etag=(etag)
|
176
|
+
@etag = etag[1..-2] if etag
|
177
|
+
end
|
178
|
+
|
179
|
+
def key_valid?(key)
|
180
|
+
if (key.nil? or key.empty? or key =~ %r#//#)
|
181
|
+
false
|
182
|
+
else
|
183
|
+
true
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def dump_headers
|
188
|
+
headers = {}
|
189
|
+
headers[:x_amz_acl] = @acl || "public-read"
|
190
|
+
headers[:content_type] = @content_type || "application/octet-stream"
|
191
|
+
headers[:content_encoding] = @content_encoding if @content_encoding
|
192
|
+
headers[:content_disposition] = @content_disposition if @content_disposition
|
193
|
+
headers
|
194
|
+
end
|
195
|
+
|
196
|
+
def parse_headers(response)
|
197
|
+
self.etag = response["etag"]
|
198
|
+
self.content_type = response["content-type"]
|
199
|
+
self.content_disposition = response["content-disposition"]
|
200
|
+
self.content_encoding = response["content-encoding"]
|
201
|
+
self.last_modified = response["last-modified"]
|
202
|
+
if response["content-range"]
|
203
|
+
self.size = response["content-range"].sub(/[^\/]+\//, "").to_i
|
204
|
+
else
|
205
|
+
self.size = response["content-length"]
|
206
|
+
self.content = response.body
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
data/lib/stree/parser.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
module Stree
|
2
|
+
module Parser
|
3
|
+
include REXML
|
4
|
+
|
5
|
+
def rexml_document(xml)
|
6
|
+
xml.force_encoding(Encoding::UTF_8) if xml.respond_to? :force_encoding
|
7
|
+
Document.new(xml)
|
8
|
+
end
|
9
|
+
|
10
|
+
def parse_list_all_my_buckets_result(xml)
|
11
|
+
names = []
|
12
|
+
rexml_document(xml).elements.each("ListAllMyBucketsResult/Buckets/Bucket/Name") { |e| names << e.text }
|
13
|
+
names
|
14
|
+
end
|
15
|
+
|
16
|
+
def parse_location_constraint(xml)
|
17
|
+
rexml_document(xml).elements["LocationConstraint"].text
|
18
|
+
end
|
19
|
+
|
20
|
+
def parse_list_bucket_result(xml)
|
21
|
+
objects_attributes = []
|
22
|
+
rexml_document(xml).elements.each("ListBucketResult/Contents") do |e|
|
23
|
+
object_attributes = {}
|
24
|
+
object_attributes[:key] = e.elements["Key"].text
|
25
|
+
object_attributes[:etag] = e.elements["ETag"].text
|
26
|
+
object_attributes[:last_modified] = e.elements["LastModified"].text
|
27
|
+
object_attributes[:size] = e.elements["Size"].text
|
28
|
+
objects_attributes << object_attributes
|
29
|
+
end
|
30
|
+
objects_attributes
|
31
|
+
end
|
32
|
+
|
33
|
+
def parse_copy_object_result(xml)
|
34
|
+
object_attributes = {}
|
35
|
+
document = rexml_document(xml)
|
36
|
+
object_attributes[:etag] = document.elements["CopyObjectResult/ETag"].text
|
37
|
+
object_attributes[:last_modified] = document.elements["CopyObjectResult/LastModified"].text
|
38
|
+
object_attributes
|
39
|
+
end
|
40
|
+
|
41
|
+
def parse_error(xml)
|
42
|
+
document = rexml_document(xml)
|
43
|
+
code = document.elements["Error/Code"].text
|
44
|
+
message = document.elements["Error/Message"].text
|
45
|
+
[code, message]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
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 Stree
|
24
|
+
module Roxy # :nodoc:all
|
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
|
@@ -0,0 +1,72 @@
|
|
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 Stree
|
24
|
+
module Roxy # :nodoc:all
|
25
|
+
# The very simple proxy class that provides a basic pass-through
|
26
|
+
# mechanism between the proxy owner and the proxy target.
|
27
|
+
class Proxy
|
28
|
+
|
29
|
+
alias :proxy_instance_eval :instance_eval
|
30
|
+
alias :proxy_extend :extend
|
31
|
+
|
32
|
+
# Make sure the proxy is as dumb as it can be.
|
33
|
+
# Blatanly taken from Jim Wierich's BlankSlate post:
|
34
|
+
# http://onestepback.org/index.cgi/Tech/Ruby/BlankSlate.rdoc
|
35
|
+
instance_methods.each { |m| undef_method m unless m =~ /(^__|^proxy_|^object_id)/ }
|
36
|
+
|
37
|
+
def initialize(owner, options, args, &block)
|
38
|
+
@owner = owner
|
39
|
+
@target = options[:to]
|
40
|
+
@args = args
|
41
|
+
|
42
|
+
# Adorn with user-provided proxy methods
|
43
|
+
[options[:extend]].flatten.each { |ext| proxy_extend(ext) } if options[:extend]
|
44
|
+
proxy_instance_eval &block if block_given?
|
45
|
+
end
|
46
|
+
|
47
|
+
def proxy_owner
|
48
|
+
@owner
|
49
|
+
end
|
50
|
+
|
51
|
+
def proxy_target
|
52
|
+
if @target.is_a?(Proc)
|
53
|
+
@target.call(@owner)
|
54
|
+
elsif @target.is_a?(UnboundMethod)
|
55
|
+
bound_method = @target.bind(proxy_owner)
|
56
|
+
bound_method.arity == 0 ? bound_method.call : bound_method.call(*@args)
|
57
|
+
else
|
58
|
+
@target
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# def inspect
|
63
|
+
# "#<S3::Roxy::Proxy:0x#{object_id.to_s(16)}>"
|
64
|
+
# end
|
65
|
+
|
66
|
+
# Delegate all method calls we don't know about to target object
|
67
|
+
def method_missing(sym, *args, &block)
|
68
|
+
proxy_target.__send__(sym, *args, &block)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|