s3 0.2.0
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.
- 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
|