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,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 S3
24
+ module Roxy
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
data/lib/s3/service.rb ADDED
@@ -0,0 +1,106 @@
1
+ module S3
2
+ class Service
3
+ extend Roxy::Moxie
4
+
5
+ attr_reader :access_key_id, :secret_access_key, :use_ssl
6
+
7
+ def ==(other)
8
+ self.access_key_id == other.access_key_id and self.secret_access_key == other.secret_access_key
9
+ end
10
+
11
+ def initialize(options)
12
+ @access_key_id = options[:access_key_id] or raise ArgumentError.new("No access key id given")
13
+ @secret_access_key = options[:secret_access_key] or raise ArgumentError.new("No secret access key given")
14
+ @use_ssl = options[:use_ssl]
15
+ @timeout = options[:timeout]
16
+ @debug = options[:debug]
17
+ end
18
+
19
+ def buckets(reload = false)
20
+ if reload or @buckets.nil?
21
+ response = service_request(:get)
22
+ @buckets = parse_buckets(response.body)
23
+ else
24
+ @buckets
25
+ end
26
+ end
27
+
28
+ def protocol
29
+ use_ssl ? "https://" : "http://"
30
+ end
31
+
32
+ def port
33
+ use_ssl ? 443 : 80
34
+ end
35
+
36
+ proxy :buckets do
37
+ def build(name)
38
+ Bucket.new(proxy_owner, name)
39
+ end
40
+
41
+ def find_first(name)
42
+ bucket = build(name)
43
+ bucket.retrieve
44
+ end
45
+ alias :find :find_first
46
+
47
+ def find_all
48
+ proxy_target
49
+ end
50
+
51
+ def reload
52
+ proxy_owner.buckets(true)
53
+ end
54
+
55
+ def destroy_all(force = false)
56
+ proxy_target.each do |bucket|
57
+ begin
58
+ bucket.destroy
59
+ rescue Error::BucketNotEmpty
60
+ if force
61
+ bucket.objects.destroy_all
62
+ retry
63
+ else
64
+ raise
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ def inspect
72
+ "#<#{self.class}:#@access_key_id>"
73
+ end
74
+
75
+ private
76
+
77
+ def service_request(method, options = {})
78
+ connection.request(method, options.merge(:path => "/#{options[:path]}"))
79
+ end
80
+
81
+ def connection
82
+ if @connection.nil?
83
+ @connection = Connection.new
84
+ @connection.access_key_id = @access_key_id
85
+ @connection.secret_access_key = @secret_access_key
86
+ @connection.use_ssl = @use_ssl
87
+ @connection.timeout = @timeout
88
+ @connection.debug = @debug
89
+ end
90
+ @connection
91
+ end
92
+
93
+ def parse_buckets(xml_body)
94
+ xml = XmlSimple.xml_in(xml_body)
95
+ buckets = xml["Buckets"].first["Bucket"]
96
+ if buckets
97
+ buckets_names = buckets.map { |bucket| bucket["Name"].first }
98
+ buckets_names.map do |bucket_name|
99
+ Bucket.new(self, bucket_name)
100
+ end
101
+ else
102
+ []
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,120 @@
1
+ module S3
2
+ class Signature
3
+
4
+ # Required options: host, request, access_key_id, secret_access_key
5
+ def self.generate(options)
6
+ request = options[:request]
7
+ host = options[:host]
8
+ access_key_id = options[:access_key_id]
9
+ secret_access_key = options[:secret_access_key]
10
+
11
+ http_verb = request.method
12
+ content_md5 = request["content-md5"] || ""
13
+ content_type = request["content-type"] || ""
14
+ date = request["x-amz-date"].nil? ? request["date"] : ""
15
+ canonicalized_resource = canonicalized_resource(host, request)
16
+ canonicalized_amz_headers = canonicalized_amz_headers(request)
17
+
18
+ string_to_sign = ""
19
+ string_to_sign << http_verb
20
+ string_to_sign << "\n"
21
+ string_to_sign << content_md5
22
+ string_to_sign << "\n"
23
+ string_to_sign << content_type
24
+ string_to_sign << "\n"
25
+ string_to_sign << date
26
+ string_to_sign << "\n"
27
+ string_to_sign << canonicalized_amz_headers
28
+ string_to_sign << canonicalized_resource
29
+
30
+ digest = OpenSSL::Digest::Digest.new('sha1')
31
+ hmac = OpenSSL::HMAC.digest(digest, secret_access_key, string_to_sign)
32
+ base64 = Base64.encode64(hmac)
33
+ signature = base64.chomp
34
+
35
+ "AWS #{access_key_id}:#{signature}"
36
+ end
37
+
38
+ private
39
+
40
+ def self.canonicalized_amz_headers(request)
41
+ headers = []
42
+
43
+ # 1. Convert each HTTP header name to lower-case. For example,
44
+ # 'X-Amz-Date' becomes 'x-amz-date'.
45
+ request.each { |key, value| headers << [key.downcase, value] if key =~ /\Ax-amz-/io }
46
+ #=> [["c", 0], ["a", 1], ["a", 2], ["b", 3]]
47
+
48
+ # 2. Sort the collection of headers lexicographically by header
49
+ # name.
50
+ headers.sort!
51
+ #=> [["a", 1], ["a", 2], ["b", 3], ["c", 0]]
52
+
53
+ # 3. Combine header fields with the same name into one
54
+ # "header-name:comma-separated-value-list" pair as prescribed by
55
+ # RFC 2616, section 4.2, without any white-space between
56
+ # values. For example, the two metadata headers
57
+ # 'x-amz-meta-username: fred' and 'x-amz-meta-username: barney'
58
+ # would be combined into the single header 'x-amz-meta-username:
59
+ # fred,barney'.
60
+ groupped_headers = headers.group_by { |i| i.first }
61
+ #=> {"a"=>[["a", 1], ["a", 2]], "b"=>[["b", 3]], "c"=>[["c", 0]]}
62
+ combined_headers = groupped_headers.map do |key, value|
63
+ values = value.map { |e| e.last }
64
+ [key, values.join(",")]
65
+ end
66
+ #=> [["a", "1,2"], ["b", "3"], ["c", "0"]]
67
+
68
+ # 4. "Un-fold" long headers that span multiple lines (as allowed
69
+ # by RFC 2616, section 4.2) by replacing the folding white-space
70
+ # (including new-line) by a single space.
71
+ unfolded_headers = combined_headers.map do |header|
72
+ key = header.first
73
+ value = header.last
74
+ value.gsub!(/\s+/, " ")
75
+ [key, value]
76
+ end
77
+
78
+ # 5. Trim any white-space around the colon in the header. For
79
+ # example, the header 'x-amz-meta-username: fred,barney' would
80
+ # become 'x-amz-meta-username:fred,barney'
81
+ joined_headers = unfolded_headers.map do |header|
82
+ key = header.first.strip
83
+ value = header.last.strip
84
+ "#{key}:#{value}"
85
+ end
86
+
87
+ # 6. Finally, append a new-line (U+000A) to each canonicalized
88
+ # header in the resulting list. Construct the
89
+ # CanonicalizedResource element by concatenating all headers in
90
+ # this list into a single string.
91
+ joined_headers << "" unless joined_headers.empty?
92
+ joined_headers.join("\n")
93
+ end
94
+
95
+ def self.canonicalized_resource(host, request)
96
+ # 1. Start with the empty string ("").
97
+ string = ""
98
+
99
+ # 2. If the request specifies a bucket using the HTTP Host
100
+ # header (virtual hosted-style), append the bucket name preceded
101
+ # by a "/" (e.g., "/bucketname"). For path-style requests and
102
+ # requests that don't address a bucket, do nothing. For more
103
+ # information on virtual hosted-style requests, see Virtual
104
+ # Hosting of Buckets.
105
+ bucket_name = host.sub(/\.?s3\.amazonaws\.com\Z/, "")
106
+ string << "/#{bucket_name}" unless bucket_name.empty?
107
+
108
+ # 3. Append the path part of the un-decoded HTTP Request-URI,
109
+ # up-to but not including the query string.
110
+ uri = URI.parse(request.path)
111
+ string << uri.path
112
+
113
+ # 4. If the request addresses a sub-resource, like ?location,
114
+ # ?acl, or ?torrent, append the sub-resource including question
115
+ # mark.
116
+ string << "?#{$1}" if uri.query =~ /&?(acl|torrent|logging|location)(?:&|=|\Z)/
117
+ string
118
+ end
119
+ end
120
+ end
data/s3.gemspec ADDED
@@ -0,0 +1,65 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{s3}
5
+ s.version = "0.0.3"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Jakub Kuźma", "Mirosław Boruta"]
9
+ s.date = %q{2009-07-03}
10
+ s.default_executable = %q{s3cmd.rb}
11
+ s.email = %q{qoobaa@gmail.com}
12
+ s.executables = ["s3cmd.rb"]
13
+ s.extra_rdoc_files = [
14
+ "LICENSE",
15
+ "README.rdoc"
16
+ ]
17
+ s.files = [
18
+ ".document",
19
+ ".gitignore",
20
+ "LICENSE",
21
+ "README.rdoc",
22
+ "Rakefile",
23
+ "VERSION",
24
+ "bin/s3cmd.rb",
25
+ "lib/s3.rb",
26
+ "lib/s3/bucket.rb",
27
+ "lib/s3/connection.rb",
28
+ "lib/s3/exceptions.rb",
29
+ "lib/s3/object.rb",
30
+ "lib/s3/roxy/moxie.rb",
31
+ "lib/s3/roxy/proxy.rb",
32
+ "lib/s3/service.rb",
33
+ "lib/s3/signature.rb",
34
+ "s3.gemspec",
35
+ "test/bucket_test.rb",
36
+ "test/connection_test.rb",
37
+ "test/s3_test.rb",
38
+ "test/service_test.rb",
39
+ "test/signature_test.rb",
40
+ "test/test_helper.rb"
41
+ ]
42
+ s.homepage = %q{http://github.com/qoobaa/s3}
43
+ s.rdoc_options = ["--charset=UTF-8"]
44
+ s.require_paths = ["lib"]
45
+ s.rubygems_version = %q{1.3.4}
46
+ s.summary = %q{Library for accessing S3 objects and buckets, with command line tool}
47
+ s.test_files = [
48
+ "test/s3_test.rb",
49
+ "test/bucket_test.rb",
50
+ "test/service_test.rb",
51
+ "test/signature_test.rb",
52
+ "test/connection_test.rb",
53
+ "test/test_helper.rb"
54
+ ]
55
+
56
+ if s.respond_to? :specification_version then
57
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
58
+ s.specification_version = 3
59
+
60
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
61
+ else
62
+ end
63
+ else
64
+ end
65
+ end
@@ -0,0 +1,22 @@
1
+ require 'test_helper'
2
+
3
+ class BucketTest < Test::Unit::TestCase
4
+ # TODO fix or remove
5
+ # def test_parse_name_without_bucket
6
+ # host, prefix = S3::Bucket.parse_name("", "s3.amazonaws.com")
7
+ # assert_equal "s3.amazonaws.com", host
8
+ # assert_equal "", prefix
9
+ # end
10
+ #
11
+ # def test_parse_name_with_vhost_name
12
+ # host, prefix = S3::Bucket.parse_name("data.synergypeople.net", "s3.amazonaws.com")
13
+ # assert_equal "data.synergypeople.net.s3.amazonaws.com", host
14
+ # assert_equal "", prefix
15
+ # end
16
+ #
17
+ # def test_parse_name_with_path_based_name
18
+ # host, prefix = S3::Bucket.parse_name("synergypeople_net", "s3.amazonaws.com")
19
+ # assert_equal "s3.amazonaws.com", host
20
+ # assert_equal "/synergypeople_net", prefix
21
+ # end
22
+ end
@@ -0,0 +1,164 @@
1
+ require 'test_helper'
2
+
3
+ class ConnectionTest < Test::Unit::TestCase
4
+ def setup
5
+ @connection = S3::Connection.new(
6
+ :access_key_id => "12345678901234567890",
7
+ :secret_access_key => "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDF"
8
+ )
9
+ @http_request = Net::HTTP.new("")
10
+ @response_ok = Net::HTTPOK.new("1.1", "200", "OK")
11
+ @response_not_found = Net::HTTPNotFound.new("1.1", "404", "Not Found")
12
+ stub(@connection).http { @http_request }
13
+ stub(@http_request).start { @response_ok }
14
+ end
15
+
16
+ def test_handle_response_not_modify_response_when_ok
17
+ assert_nothing_raised do
18
+ response = @connection.request(
19
+ :get,
20
+ :host => "s3.amazonaws.com",
21
+ :path => "/"
22
+ )
23
+ assert_equal @response_ok, response
24
+ end
25
+ end
26
+
27
+ def test_handle_response_throws_exception_when_not_ok
28
+ response_body = <<-EOFakeBody
29
+ <?xml version=\"1.0\" encoding=\"UTF-8\"?>
30
+ <SomeResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">
31
+ <Code>NoSuchBucket</Code>
32
+ <Message>The specified bucket does not exist</Message>
33
+ </SomeResult>
34
+ EOFakeBody
35
+ stub(@http_request).start { @response_not_found }
36
+ stub(@response_not_found).body { response_body }
37
+
38
+ assert_raise S3::Error::NoSuchBucket do
39
+ response = @connection.request(
40
+ :get,
41
+ :host => "data.example.com.s3.amazonaws.com",
42
+ :path => "/"
43
+ )
44
+ end
45
+ end
46
+
47
+ def test_handle_response_throws_standard_exception_when_not_ok
48
+ stub(@http_request).start { @response_not_found }
49
+ stub(@response_not_found).body { nil }
50
+ assert_raise S3::Error::ResponseError do
51
+ response = @connection.request(
52
+ :get,
53
+ :host => "data.example.com.s3.amazonaws.com",
54
+ :path => "/"
55
+ )
56
+ end
57
+
58
+ stub(@response_not_found).body { "" }
59
+ assert_raise S3::Error::ResponseError do
60
+ response = @connection.request(
61
+ :get,
62
+ :host => "data.example.com.s3.amazonaws.com",
63
+ :path => "/"
64
+ )
65
+ end
66
+ end
67
+
68
+ def test_parse_params_empty
69
+ expected = ""
70
+ actual = S3::Connection.parse_params({})
71
+ assert_equal expected, actual
72
+ end
73
+
74
+ def test_parse_params_only_interesting_params
75
+ expected = ""
76
+ actual = S3::Connection.parse_params(:param1 => "1", :maxkeys => "2")
77
+ assert_equal expected, actual
78
+ end
79
+
80
+ def test_parse_params_remove_underscore
81
+ expected = "max-keys=100"
82
+ actual = S3::Connection.parse_params(:max_keys => 100)
83
+ assert_equal expected, actual
84
+ end
85
+
86
+ def test_parse_params_with_and_without_values
87
+ expected = "max-keys=100&prefix"
88
+ actual = S3::Connection.parse_params(:max_keys => 100, :prefix => nil)
89
+ assert_equal expected, actual
90
+ end
91
+
92
+ def test_headers_headers_empty
93
+ expected = {}
94
+ actual = S3::Connection.parse_headers({})
95
+ assert_equal expected, actual
96
+ end
97
+
98
+ def test_parse_headers_only_interesting_headers
99
+ expected = {}
100
+ actual = S3::Connection.parse_headers(
101
+ :accept => "text/*, text/html, text/html;level=1, */*",
102
+ :accept_charset => "iso-8859-2, unicode-1-1;q=0.8"
103
+ )
104
+ assert_equal expected, actual
105
+ end
106
+
107
+ def test_parse_headers_remove_underscore
108
+ expected = {
109
+ "content-type" => nil,
110
+ "x-amz-acl" => nil,
111
+ "if-modified-since" => nil,
112
+ "if-unmodified-since" => nil,
113
+ "if-match" => nil,
114
+ "if-none-match" => nil,
115
+ "content-disposition" => nil,
116
+ "content-encoding" => nil
117
+ }
118
+ actual = S3::Connection.parse_headers(
119
+ :content_type => nil,
120
+ :x_amz_acl => nil,
121
+ :if_modified_since => nil,
122
+ :if_unmodified_since => nil,
123
+ :if_match => nil,
124
+ :if_none_match => nil,
125
+ :content_disposition => nil,
126
+ :content_encoding => nil
127
+ )
128
+ assert_equal expected, actual
129
+ end
130
+
131
+ def test_parse_headers_with_values
132
+ expected = {
133
+ "content-type" => "text/html",
134
+ "x-amz-acl" => "public-read",
135
+ "if-modified-since" => "today",
136
+ "if-unmodified-since" => "tomorrow",
137
+ "if-match" => "1234",
138
+ "if-none-match" => "1243",
139
+ "content-disposition" => "inline",
140
+ "content-encoding" => "gzip"
141
+ }
142
+ actual = S3::Connection.parse_headers(
143
+ :content_type => "text/html",
144
+ :x_amz_acl => "public-read",
145
+ :if_modified_since => "today",
146
+ :if_unmodified_since => "tomorrow",
147
+ :if_match => "1234",
148
+ :if_none_match => "1243",
149
+ :content_disposition => "inline",
150
+ :content_encoding => "gzip"
151
+ )
152
+ assert_equal expected, actual
153
+ end
154
+
155
+ def test_parse_headers_with_range
156
+ expected = {
157
+ "range" => "bytes=0-100"
158
+ }
159
+ actual = S3::Connection.parse_headers(
160
+ :range => 0..100
161
+ )
162
+ assert_equal expected, actual
163
+ end
164
+ end