awsraw 0.0.1
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/.gitignore +4 -0
- data/Gemfile +4 -0
- data/Rakefile +1 -0
- data/awsraw.gemspec +25 -0
- data/lib/awsraw/s3/client.rb +78 -0
- data/lib/awsraw/s3/request.rb +58 -0
- data/lib/awsraw/s3/response.rb +26 -0
- data/lib/awsraw/s3/signer.rb +60 -0
- data/lib/awsraw/version.rb +3 -0
- data/lib/awsraw.rb +5 -0
- data/spec/s3/client_spec.rb +28 -0
- data/spec/s3/signer_spec.rb +49 -0
- metadata +107 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/awsraw.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "awsraw/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "awsraw"
|
7
|
+
s.version = Awsraw::VERSION
|
8
|
+
s.authors = ["Pete Yandell"]
|
9
|
+
s.email = ["pete@notahat.com"]
|
10
|
+
s.homepage = "http://github.com/envato/flickraw"
|
11
|
+
s.summary = %q{Minimal AWS client}
|
12
|
+
s.description = %q{A client for Amazon Web Services in the style of FlickRaw}
|
13
|
+
|
14
|
+
s.rubyforge_project = "awsraw"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
# specify any dependencies here; for example:
|
22
|
+
s.add_development_dependency "rake"
|
23
|
+
s.add_development_dependency "rspec"
|
24
|
+
# s.add_runtime_dependency "rest-client"
|
25
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'awsraw/s3/request'
|
3
|
+
require 'awsraw/s3/response'
|
4
|
+
require 'awsraw/s3/signer'
|
5
|
+
|
6
|
+
module AWSRaw
|
7
|
+
module S3
|
8
|
+
|
9
|
+
# A client for the AWS S3 rest API.
|
10
|
+
#
|
11
|
+
# http://docs.amazonwebservices.com/AmazonS3/latest/API/APIRest.html
|
12
|
+
class Client
|
13
|
+
|
14
|
+
def initialize(access_key_id, secret_access_key)
|
15
|
+
@access_key_id = access_key_id
|
16
|
+
@secret_access_key = secret_access_key
|
17
|
+
end
|
18
|
+
|
19
|
+
def request(params = {})
|
20
|
+
request = Request.new(params, signer)
|
21
|
+
|
22
|
+
http_request = construct_http_request(request)
|
23
|
+
|
24
|
+
http_response = Net::HTTP.start(request.uri.host, request.uri.port) do |http|
|
25
|
+
http.request(http_request)
|
26
|
+
end
|
27
|
+
|
28
|
+
construct_response(http_response)
|
29
|
+
end
|
30
|
+
|
31
|
+
def request!(params = {})
|
32
|
+
response = request(params)
|
33
|
+
raise "Uh oh! Failure from S3." if response.failure?
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def construct_http_request(request)
|
39
|
+
klass = http_request_class(request)
|
40
|
+
path = request.uri.request_uri
|
41
|
+
|
42
|
+
klass.new(path).tap do |http_request|
|
43
|
+
request.headers.each do |name, value|
|
44
|
+
http_request[name] = value
|
45
|
+
end
|
46
|
+
http_request.body = request.content
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def http_request_class(request)
|
51
|
+
case request.method
|
52
|
+
when "GET"
|
53
|
+
Net::HTTP::Get
|
54
|
+
when "HEAD"
|
55
|
+
Net::HTTP::Head
|
56
|
+
when "PUT"
|
57
|
+
Net::HTTP::Put
|
58
|
+
else
|
59
|
+
raise "Invalid HTTP method!"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def construct_response(http_response)
|
64
|
+
Response.new(
|
65
|
+
:code => http_response.code,
|
66
|
+
:headers => http_response.to_hash,
|
67
|
+
:content => http_response.body
|
68
|
+
)
|
69
|
+
end
|
70
|
+
|
71
|
+
def signer
|
72
|
+
@signer ||= Signer.new(@access_key_id, @secret_access_key)
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
require 'time'
|
3
|
+
require 'uri'
|
4
|
+
|
5
|
+
module AWSRaw
|
6
|
+
module S3
|
7
|
+
|
8
|
+
# Note that we use path style (rather than virtual hosted style) requests.
|
9
|
+
# This is because virtual hosted requests only support lower case bucket
|
10
|
+
# names.
|
11
|
+
#
|
12
|
+
# See http://docs.amazonwebservices.com/AmazonS3/latest/dev/VirtualHosting.html
|
13
|
+
class Request
|
14
|
+
def initialize(params, signer)
|
15
|
+
@method = params[:method]
|
16
|
+
@bucket = params[:bucket]
|
17
|
+
@key = params[:key]
|
18
|
+
@query = params[:query]
|
19
|
+
@headers = params[:headers] || {}
|
20
|
+
@content = params[:content]
|
21
|
+
|
22
|
+
raise "Content without Content-Type" if !@content.nil? && @headers["Content-Type"].nil?
|
23
|
+
|
24
|
+
headers["Content-MD5"] = content_md5 unless content.nil?
|
25
|
+
headers["Date"] = Time.now.rfc2822
|
26
|
+
headers["Authorization"] = signer.signature(self)
|
27
|
+
end
|
28
|
+
|
29
|
+
attr_reader :method
|
30
|
+
attr_reader :bucket
|
31
|
+
attr_reader :key
|
32
|
+
attr_reader :query
|
33
|
+
attr_reader :headers
|
34
|
+
attr_reader :content
|
35
|
+
|
36
|
+
def host
|
37
|
+
"s3.amazonaws.com"
|
38
|
+
end
|
39
|
+
|
40
|
+
def path
|
41
|
+
@path ||= URI.escape("/#{bucket}#{key}")
|
42
|
+
end
|
43
|
+
|
44
|
+
def uri
|
45
|
+
@uri ||= URI::HTTP.build(
|
46
|
+
:host => host,
|
47
|
+
:path => path,
|
48
|
+
:query => query
|
49
|
+
)
|
50
|
+
end
|
51
|
+
|
52
|
+
def content_md5
|
53
|
+
@content_md5 ||= Base64.encode64(Digest::MD5.digest(content)).strip
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module AWSRaw
|
2
|
+
module S3
|
3
|
+
|
4
|
+
class Response
|
5
|
+
def initialize(params = {})
|
6
|
+
@code = params[:code]
|
7
|
+
@headers = params[:headers]
|
8
|
+
@content = params[:content]
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_accessor :code
|
12
|
+
attr_accessor :headers
|
13
|
+
attr_accessor :content
|
14
|
+
|
15
|
+
def success?
|
16
|
+
code =~ /^2\d\d$/
|
17
|
+
end
|
18
|
+
|
19
|
+
def failure?
|
20
|
+
!success?
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'digest/sha1'
|
2
|
+
require 'openssl'
|
3
|
+
require 'uri'
|
4
|
+
require 'base64'
|
5
|
+
|
6
|
+
module AWSRaw
|
7
|
+
module S3
|
8
|
+
|
9
|
+
# Generates the Authorization header for a REST request to S3.
|
10
|
+
#
|
11
|
+
# See http://docs.amazonwebservices.com/AmazonS3/latest/dev/RESTAuthentication.html
|
12
|
+
class Signer
|
13
|
+
|
14
|
+
def initialize(access_key_id, secret_access_key)
|
15
|
+
@access_key_id = access_key_id
|
16
|
+
@secret_access_key = secret_access_key
|
17
|
+
end
|
18
|
+
|
19
|
+
def signature(request)
|
20
|
+
string_to_sign = string_to_sign(request)
|
21
|
+
|
22
|
+
digest = OpenSSL::Digest::Digest.new("sha1")
|
23
|
+
sha = OpenSSL::HMAC.digest(digest, @secret_access_key, string_to_sign)
|
24
|
+
signature = Base64.encode64(sha).strip
|
25
|
+
|
26
|
+
"AWS #{@access_key_id}:#{signature}"
|
27
|
+
end
|
28
|
+
|
29
|
+
def string_to_sign(request)
|
30
|
+
[
|
31
|
+
request.method,
|
32
|
+
request.headers["Content-MD5"] || "",
|
33
|
+
request.headers["Content-Type"] || "",
|
34
|
+
request.headers["Date"],
|
35
|
+
canonicalized_amz_headers(request.headers),
|
36
|
+
canonicalized_resource(request)
|
37
|
+
].flatten.join("\n")
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def canonicalized_amz_headers(headers)
|
43
|
+
header_names = headers.keys.
|
44
|
+
select {|name| name =~ /^x-amz-/i }.
|
45
|
+
sort_by {|name| name.downcase }
|
46
|
+
|
47
|
+
header_names.map do |name|
|
48
|
+
"#{name.downcase}:#{headers[name]}"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def canonicalized_resource(request)
|
53
|
+
# TODO: Should also append the sub-resource.
|
54
|
+
request.path
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
data/lib/awsraw.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'awsraw/s3/client'
|
2
|
+
|
3
|
+
describe AWSRaw::S3::Client do
|
4
|
+
|
5
|
+
subject { AWSRaw::S3::Client.new("dummy_access_key_id", "dummy_secret_access_key") }
|
6
|
+
|
7
|
+
context "#request!" do
|
8
|
+
it "returns if the response indicates success" do
|
9
|
+
response = stub(:failure? => false)
|
10
|
+
subject.stub(:request => response)
|
11
|
+
|
12
|
+
expect {
|
13
|
+
subject.request!(:method => "PUT")
|
14
|
+
}.should_not raise_error
|
15
|
+
end
|
16
|
+
|
17
|
+
it "raises an error if the response indicates failure" do
|
18
|
+
response = stub(:failure? => true)
|
19
|
+
subject.stub(:request => response)
|
20
|
+
|
21
|
+
expect {
|
22
|
+
subject.request!(:method => "PUT")
|
23
|
+
}.should raise_error("Uh oh! Failure from S3.")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'awsraw/s3/signer'
|
2
|
+
|
3
|
+
describe AWSRaw::S3::Signer do
|
4
|
+
let(:access_key_id) { "0PN5J17HBGZHT7JJ3X82" }
|
5
|
+
let(:secret_access_key) { "uV3F3YluFJax1cknvbcGwgjvx4QpvB+leU8dUj2o" }
|
6
|
+
|
7
|
+
subject { AWSRaw::S3::Signer.new(access_key_id, secret_access_key) }
|
8
|
+
|
9
|
+
context "examples from Amazon docs" do
|
10
|
+
it "signs a get request correctly" do
|
11
|
+
request = stub(
|
12
|
+
:method => "GET",
|
13
|
+
:path => "/johnsmith/photos/puppy.jpg",
|
14
|
+
:headers => { "Date" => "Tue, 27 Mar 2007 19:36:42 +0000" }
|
15
|
+
)
|
16
|
+
|
17
|
+
subject.string_to_sign(request).should ==
|
18
|
+
"GET\n\n\nTue, 27 Mar 2007 19:36:42 +0000\n/johnsmith/photos/puppy.jpg"
|
19
|
+
|
20
|
+
subject.signature(request).should == "AWS #{access_key_id}:xXjDGYUmKxnwqr5KXNPGldn5LbA="
|
21
|
+
end
|
22
|
+
|
23
|
+
it "signs an upload correctly" do
|
24
|
+
request = stub(
|
25
|
+
:method => "PUT",
|
26
|
+
:path => "/static.johnsmith.net/db-backup.dat.gz",
|
27
|
+
:headers => {
|
28
|
+
"User-Agent" => "curl/7.15.5",
|
29
|
+
"Date" => "Tue, 27 Mar 2007 21:06:08 +0000",
|
30
|
+
"x-amz-acl" => "public-read",
|
31
|
+
"Content-Type" => "application/x-download",
|
32
|
+
"Content-MD5" => "4gJE4saaMU4BqNR0kLY+lw==",
|
33
|
+
"X-Amz-Meta-ReviewedBy" => "joe@johnsmith.net,jane@johnsmith.net",
|
34
|
+
"X-Amz-Meta-FileChecksum" => "0x02661779",
|
35
|
+
"X-Amz-Meta-ChecksumAlgorithm" => "crc32",
|
36
|
+
"Content-Disposition" => "attachment; filename=database.dat",
|
37
|
+
"Content-Encoding" => "gzip",
|
38
|
+
"Content-Length" => "5913339"
|
39
|
+
}
|
40
|
+
)
|
41
|
+
|
42
|
+
subject.string_to_sign(request).should ==
|
43
|
+
"PUT\n4gJE4saaMU4BqNR0kLY+lw==\napplication/x-download\nTue, 27 Mar 2007 21:06:08 +0000\nx-amz-acl:public-read\nx-amz-meta-checksumalgorithm:crc32\nx-amz-meta-filechecksum:0x02661779\nx-amz-meta-reviewedby:joe@johnsmith.net,jane@johnsmith.net\n/static.johnsmith.net/db-backup.dat.gz"
|
44
|
+
|
45
|
+
subject.signature(request).should == "AWS #{access_key_id}:C0FlOtU8Ylb9KDTpZqYkZPX91iI="
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
metadata
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: awsraw
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Pete Yandell
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2012-02-12 00:00:00 +11:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: rake
|
23
|
+
type: :development
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: &id001 !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - ">="
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
hash: 3
|
31
|
+
segments:
|
32
|
+
- 0
|
33
|
+
version: "0"
|
34
|
+
requirement: *id001
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: rspec
|
37
|
+
type: :development
|
38
|
+
prerelease: false
|
39
|
+
version_requirements: &id002 !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ">="
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
hash: 3
|
45
|
+
segments:
|
46
|
+
- 0
|
47
|
+
version: "0"
|
48
|
+
requirement: *id002
|
49
|
+
description: A client for Amazon Web Services in the style of FlickRaw
|
50
|
+
email:
|
51
|
+
- pete@notahat.com
|
52
|
+
executables: []
|
53
|
+
|
54
|
+
extensions: []
|
55
|
+
|
56
|
+
extra_rdoc_files: []
|
57
|
+
|
58
|
+
files:
|
59
|
+
- .gitignore
|
60
|
+
- Gemfile
|
61
|
+
- Rakefile
|
62
|
+
- awsraw.gemspec
|
63
|
+
- lib/awsraw.rb
|
64
|
+
- lib/awsraw/s3/client.rb
|
65
|
+
- lib/awsraw/s3/request.rb
|
66
|
+
- lib/awsraw/s3/response.rb
|
67
|
+
- lib/awsraw/s3/signer.rb
|
68
|
+
- lib/awsraw/version.rb
|
69
|
+
- spec/s3/client_spec.rb
|
70
|
+
- spec/s3/signer_spec.rb
|
71
|
+
has_rdoc: true
|
72
|
+
homepage: http://github.com/envato/flickraw
|
73
|
+
licenses: []
|
74
|
+
|
75
|
+
post_install_message:
|
76
|
+
rdoc_options: []
|
77
|
+
|
78
|
+
require_paths:
|
79
|
+
- lib
|
80
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ">="
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
hash: 3
|
86
|
+
segments:
|
87
|
+
- 0
|
88
|
+
version: "0"
|
89
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
90
|
+
none: false
|
91
|
+
requirements:
|
92
|
+
- - ">="
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
hash: 3
|
95
|
+
segments:
|
96
|
+
- 0
|
97
|
+
version: "0"
|
98
|
+
requirements: []
|
99
|
+
|
100
|
+
rubyforge_project: awsraw
|
101
|
+
rubygems_version: 1.6.2
|
102
|
+
signing_key:
|
103
|
+
specification_version: 3
|
104
|
+
summary: Minimal AWS client
|
105
|
+
test_files:
|
106
|
+
- spec/s3/client_spec.rb
|
107
|
+
- spec/s3/signer_spec.rb
|