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.
- data/.document +5 -0
- data/.gitignore +6 -0
- data/LICENSE +20 -0
- data/README.rdoc +7 -0
- data/Rakefile +58 -0
- data/VERSION +1 -0
- data/bin/s3cmd.rb +188 -0
- data/lib/s3.rb +23 -0
- data/lib/s3/bucket.rb +154 -0
- data/lib/s3/connection.rb +152 -0
- data/lib/s3/exceptions.rb +94 -0
- data/lib/s3/object.rb +153 -0
- data/lib/s3/roxy/moxie.rb +58 -0
- data/lib/s3/roxy/proxy.rb +72 -0
- data/lib/s3/service.rb +106 -0
- data/lib/s3/signature.rb +120 -0
- data/s3.gemspec +65 -0
- data/test/bucket_test.rb +22 -0
- data/test/connection_test.rb +164 -0
- data/test/s3_test.rb +5 -0
- data/test/service_test.rb +133 -0
- data/test/signature_test.rb +143 -0
- data/test/test_helper.rb +11 -0
- metadata +82 -0
@@ -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
|