s4 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (6) hide show
  1. data/Rakefile +1 -1
  2. data/lib/s4.rb +120 -39
  3. data/s4.gemspec +7 -7
  4. data/test/s4_test.rb +151 -0
  5. metadata +4 -6
  6. data/test/s3_test.rb +0 -108
data/Rakefile CHANGED
@@ -1,5 +1,5 @@
1
1
  task :default => :test
2
2
 
3
3
  task :test do
4
- system "cutest test/*.rb"
4
+ system "ruby test/*.rb"
5
5
  end
data/lib/s4.rb CHANGED
@@ -5,7 +5,7 @@ require "time"
5
5
 
6
6
  # Simpler AWS S3 library
7
7
  class S4
8
- VERSION = "0.0.1"
8
+ VERSION = "0.0.2"
9
9
 
10
10
  # sub-resource names which may appear in the query string and also must be
11
11
  # signed against.
@@ -16,8 +16,42 @@ class S4
16
16
  HeaderValues = %w( response-content-type response-content-language response-expires reponse-cache-control response-content-disposition response-content-encoding )
17
17
 
18
18
  attr_reader :connection, :access_key_id, :secret_access_key, :bucket, :host
19
-
20
- def initialize(s3_url = ENV["S3_URL"])
19
+
20
+ class << self
21
+ # Connect to an S3 bucket.
22
+ #
23
+ # Pass your S3 connection parameters as URL, or read from ENV["S3_URL"] if
24
+ # none is passed.
25
+ #
26
+ # S3_URL format is s3://<access key id>:<secret access key>@s3.amazonaws.com/<bucket>
27
+ #
28
+ # i.e.
29
+ # bucket = S4.connect #=> Connects to ENV["S3_URL"]
30
+ # bucket = S4.connect("s3://0PN5J17HBGZHT7JJ3X82:k3nL7gH3+PadhTEVn5EXAMPLE@s3.amazonaws.com/bucket")
31
+ def connect(s3_url=ENV["S3_URL"])
32
+ new(s3_url).tap do |s4|
33
+ s4.connect
34
+ end
35
+ end
36
+
37
+ # Create an S3 bucket.
38
+ #
39
+ # See #connect for S3_URL parameters.
40
+ #
41
+ # Will create the bucket on S3 and connect to it, or just connect if the
42
+ # bucket already exists and is owned by you.
43
+ #
44
+ # i.e.
45
+ # bucket = S4.create
46
+ def create(s3_url=ENV["S3_URL"])
47
+ new(s3_url).tap do |s4|
48
+ s4.create
49
+ end
50
+ end
51
+ end
52
+
53
+ # Initialize a new S3 bucket connection.
54
+ def initialize(s3_url=ENV["S3_URL"])
21
55
  raise ArgumentError, "No S3 URL provided. You can set ENV['S3_URL'], too." if s3_url.nil? || s3_url.empty?
22
56
 
23
57
  begin
@@ -27,23 +61,38 @@ class S4
27
61
  raise e
28
62
  end
29
63
 
30
- @access_key_id = url.user
64
+ @access_key_id = url.user
31
65
  @secret_access_key = URI.unescape(url.password || "")
32
- @host = url.host
33
- @bucket = url.path[1..-1]
66
+ @host = url.host
67
+ @bucket = url.path[1..-1]
34
68
  end
35
-
36
- def connection
37
- @connection ||= Net::HTTP::Persistent.new("aws-s3/#{bucket}")
69
+
70
+ # Connect to the S3 bucket.
71
+ #
72
+ # Since S3 doesn't really require a persistent connection this really just
73
+ # makes sure that it *can* connect (i.e. the bucket exists and you own it).
74
+ def connect
75
+ raise NoSuchBucket.new(bucket) if request(uri("/", query: "location")).nil?
76
+ end
77
+
78
+ # Create the S3 bucket.
79
+ def create
80
+ uri = URI::HTTP.build(host: host, path: "/#{bucket}")
81
+ request uri, Net::HTTP::Put.new(uri.request_uri)
38
82
  end
39
83
 
40
84
  # Lower level object get which just yields the successful S3 response to the
41
85
  # block. See #download if you want to simply copy a file from S3 to local.
42
86
  def get(name, &block)
43
87
  request(uri(name), &block)
88
+ rescue S4::Error => e
89
+ raise e if e.status != "404"
44
90
  end
45
91
 
46
92
  # Download the file with the given filename to the given destination.
93
+ #
94
+ # i.e.
95
+ # bucket.download("images/palm_trees.jpg", "./palm_trees.jpg")
47
96
  def download(name, destination=nil)
48
97
  get(name) do |response|
49
98
  File.open(destination || File.join(Dir.pwd, File.basename(name)), "wb") do |io|
@@ -59,13 +108,18 @@ class S4
59
108
  request(uri = uri(name), Net::HTTP::Delete.new(uri.request_uri))
60
109
  end
61
110
 
62
- # Upload the file with the given filename to the given destination in your
63
- # S3 bucket. If no destination is given then uploads it with the same
64
- # filename to the root of your bucket.
111
+ # Upload the file with the given filename to the given destination in your S3
112
+ # bucket.
113
+ #
114
+ # If no destination is given then uploads it with the same filename to the
115
+ # root of your bucket.
116
+ #
117
+ # i.e.
118
+ # bucket.upload("./images/1996_animated_explosion.gif", "website_background.gif")
65
119
  def upload(name, destination=nil)
66
120
  put File.open(name, "rb"), destination || File.basename(name)
67
121
  end
68
-
122
+
69
123
  def put(io, name)
70
124
  uri = uri(name)
71
125
  req = Net::HTTP::Put.new(uri.request_uri)
@@ -77,38 +131,40 @@ class S4
77
131
  request(URI::HTTP.build(host: host, path: "/#{bucket}/#{name}"), req)
78
132
  end
79
133
 
80
- # List bucket contents
134
+ # List bucket contents.
135
+ #
136
+ # Optionally pass a prefix to list from (useful for paths).
137
+ #
138
+ # i.e.
139
+ # bucket.list("images/") #=> [ "birds.jpg", "bees.jpg" ]
81
140
  def list(prefix = "")
82
141
  REXML::Document.new(request(uri("", query: "prefix=#{prefix}"))).elements.collect("//Key", &:text)
83
142
  end
84
-
143
+
85
144
  private
86
-
145
+
146
+ def connection
147
+ @connection ||= Net::HTTP::Persistent.new("aws-s3/#{bucket}")
148
+ end
149
+
87
150
  def uri(path, options={})
88
- URI::HTTP.build(options.merge(host: host, path: "/#{bucket}/#{CGI.escape(path.sub(/^\//, ""))}"))
151
+ URI::HTTP.build(options.merge(host: host, path: "/#{bucket}/#{URI.escape(path.sub(/^\//, ""))}"))
89
152
  end
90
153
 
91
154
  # Makes a request to the S3 API.
92
- def request(uri, request = nil)
93
- # TODO: Possibly use SAX parsing for large request bodies (?)
94
-
155
+ def request(uri, request=nil)
95
156
  request ||= Net::HTTP::Get.new(uri.request_uri)
96
-
157
+
97
158
  connection.request(uri, sign(uri, request)) do |response|
98
159
  case response
99
- when Net::HTTPNotFound
100
- return nil
101
-
102
- when Net::HTTPSuccess
103
- if block_given?
104
- yield(response)
105
- else
106
- return response.body
107
- end
108
-
160
+ when Net::HTTPSuccess
161
+ if block_given?
162
+ yield(response)
109
163
  else
110
- raise Error.from_xml(response.body)
111
-
164
+ return response.body
165
+ end
166
+ else
167
+ raise Error.from_response(response)
112
168
  end
113
169
  end
114
170
  end
@@ -123,22 +179,47 @@ class S4
123
179
  end
124
180
 
125
181
  def signature(uri, request)
182
+ query = signed_params(uri.query) if uri.query
183
+
126
184
  Base64.encode64(
127
185
  OpenSSL::HMAC.digest(
128
186
  OpenSSL::Digest::Digest.new("sha1"),
129
187
  secret_access_key,
130
- "#{request.class::METHOD}\n\n#{request["Content-Type"]}\n#{request.fetch("Date")}\n#{uri.path}"
188
+ "#{request.class::METHOD}\n\n#{request["Content-Type"]}\n#{request.fetch("Date")}\n" + uri.path + (query ? "?#{query}" : "")
131
189
  )
132
190
  ).chomp
133
191
  end
134
-
192
+
193
+ # Returns the given query string consisting only of query parameters which
194
+ # need to be signed against, or nil if there are none in the query string.
195
+ def signed_params(query)
196
+ signed = query.
197
+ split("&").
198
+ collect{ |param| param.split("=") }.
199
+ reject{ |pair| !SubResources.include?(pair[0]) }.
200
+ collect{ |pair| pair.join("=") }.
201
+ join("&")
202
+
203
+ signed unless signed.empty?
204
+ end
205
+
135
206
  # Base class of all S3 Errors
136
207
  class Error < ::RuntimeError
137
- def self.from_xml(xml)
138
- doc = REXML::Document.new(xml).elements["//Error"]
208
+ attr_reader :code, :status
209
+
210
+ def self.from_response(response)
211
+ doc = REXML::Document.new(response.body).elements["//Error"]
139
212
  code = doc.elements["Code"].text
140
213
  message = doc.elements["Message"].text
141
- new("#{code}: #{message}")
214
+
215
+ new response.code, code, message
142
216
  end
143
- end
217
+
218
+ def initialize(status, code, message)
219
+ @status = status
220
+ @code = code
221
+
222
+ super "#{@code}: #{message}"
223
+ end
224
+ end
144
225
  end
data/s4.gemspec CHANGED
@@ -1,13 +1,13 @@
1
1
  require "./lib/s4"
2
2
 
3
3
  Gem::Specification.new do |s|
4
- s.name = "s4"
5
- s.version = S4::VERSION
6
- s.summary = "Simple API for AWS S3"
7
- s.description = "Simple API for AWS S3"
8
- s.authors = ["Ben Alavi"]
9
- s.email = ["ben.alavi@citrusbyte.com"]
10
- s.homepage = "http://github.com/benalavi/s4"
4
+ s.name = "s4"
5
+ s.version = S4::VERSION
6
+ s.summary = "Simple API for AWS S3"
7
+ s.description = "Simple API for AWS S3"
8
+ s.authors = ["Ben Alavi"]
9
+ s.email = ["ben.alavi@citrusbyte.com"]
10
+ s.homepage = "http://github.com/benalavi/s4"
11
11
 
12
12
  s.files = Dir[
13
13
  "LICENSE",
data/test/s4_test.rb ADDED
@@ -0,0 +1,151 @@
1
+ require "contest"
2
+ require "timecop"
3
+ require "fileutils"
4
+
5
+ begin
6
+ require "ruby-debug"
7
+ rescue LoadError
8
+ end
9
+
10
+ require File.expand_path("../lib/s4", File.dirname(__FILE__))
11
+
12
+ def fixture(filename="")
13
+ File.join(File.dirname(__FILE__), "fixtures", filename)
14
+ end
15
+
16
+ def output(filename="")
17
+ File.join(File.dirname(__FILE__), "output", filename)
18
+ end
19
+
20
+ NewBucket = "s4-bucketthatdoesntexist"
21
+ TestBucket = "s4-test-bucket"
22
+
23
+ class S4Test < Test::Unit::TestCase
24
+ setup do
25
+ FileUtils.rm_rf(output)
26
+ FileUtils.mkdir_p(output)
27
+ end
28
+
29
+ context "connecting to S3" do
30
+ should "return connected bucket if can connect" do
31
+ s4 = S4.connect
32
+ end
33
+
34
+ should "raise error if cannot connect" do
35
+ `s3cmd rb 's3://#{NewBucket}' 2>&1`
36
+
37
+ assert_raise(S4::Error) do
38
+ S4.connect ENV["S3_URL"].sub(TestBucket, NewBucket)
39
+ end
40
+ end
41
+ end
42
+
43
+ context "when S3 errors occur" do
44
+ # foo is taken bucket, will cause 409 Conflict on create
45
+
46
+ should "raise on S3 errors" do
47
+ assert_raise(S4::Error) do
48
+ S4.create(ENV["S3_URL"].sub(TestBucket, "foo"))
49
+ end
50
+ end
51
+
52
+ should "capture code of S3 error" do
53
+ begin
54
+ S4.create(ENV["S3_URL"].sub(TestBucket, "foo"))
55
+ rescue S4::Error => e
56
+ assert_equal "409", e.status
57
+ end
58
+ end
59
+ end
60
+
61
+ context "creating a bucket" do
62
+ setup do
63
+ `s3cmd rb 's3://#{NewBucket}' 2>&1`
64
+ end
65
+
66
+ should "create a bucket" do
67
+ assert_equal "ERROR: Bucket '#{NewBucket}' does not exist", `s3cmd ls 's3://#{NewBucket}' 2>&1`.chomp
68
+ s4 = S4.create ENV["S3_URL"].sub(TestBucket, NewBucket)
69
+ assert_equal "", `s3cmd ls 's3://#{NewBucket}' 2>&1`.chomp
70
+ end
71
+ end
72
+
73
+ context "when connected" do
74
+ setup do
75
+ @s4 = S4.connect
76
+ end
77
+
78
+ should "download foo.txt" do
79
+ `s3cmd put #{fixture("foo.txt")} s3://#{@s4.bucket}/foo.txt`
80
+ @s4.download("foo.txt", output("foo.txt"))
81
+
82
+ assert_equal "abc123", File.read(output("foo.txt"))
83
+ end
84
+
85
+ should "not download non-existent files" do
86
+ `s3cmd del 's3://#{@s4.bucket}/foo.txt'`
87
+ @s4.download("foo.txt", output("foo.txt"))
88
+
89
+ assert !File.exists?(output("foo.txt"))
90
+ end
91
+
92
+ should "return false when downloading non-existent files" do
93
+ `s3cmd del 's3://#{@s4.bucket}/foo.txt'`
94
+ assert_equal nil, @s4.download("foo.txt", output("foo.txt"))
95
+ end
96
+
97
+ should "yield raw response from get of foo.txt" do
98
+ `s3cmd put #{fixture("foo.txt")} s3://#{@s4.bucket}/foo.txt`
99
+
100
+ @s4.get("foo.txt") do |response|
101
+ assert_equal "abc123", response.body
102
+ end
103
+ end
104
+
105
+ should "delete object" do
106
+ `s3cmd put #{fixture("foo.txt")} s3://#{@s4.bucket}/foo.txt`
107
+ @s4.get("foo.txt") { |response| assert_equal "abc123", response.body }
108
+ @s4.delete("foo.txt")
109
+
110
+ assert_equal nil, @s4.get("foo.txt")
111
+ end
112
+
113
+ should "return list of items in bucket" do
114
+ `s3cmd del 's3://#{@s4.bucket}/abc/*'`
115
+ `s3cmd del 's3://#{@s4.bucket}/boom.txt'`
116
+ `s3cmd del 's3://#{@s4.bucket}/foo\ bar+baz.txt'`
117
+ `s3cmd put #{fixture("foo.txt")} s3://#{@s4.bucket}/foo.txt`
118
+ `s3cmd put #{fixture("foo.txt")} s3://#{@s4.bucket}/bar.txt`
119
+ `s3cmd put #{fixture("foo.txt")} s3://#{@s4.bucket}/baz.txt`
120
+
121
+ assert_equal %w( bar.txt baz.txt foo.txt ), @s4.list
122
+ end
123
+
124
+ should "return list of keys starting with prefix" do
125
+ `s3cmd put #{fixture("foo.txt")} s3://#{@s4.bucket}/abc/bing.txt`
126
+ `s3cmd put #{fixture("foo.txt")} s3://#{@s4.bucket}/abc/bang.txt`
127
+ `s3cmd put #{fixture("foo.txt")} s3://#{@s4.bucket}/boom.txt`
128
+
129
+ assert_equal %w( abc/bang.txt abc/bing.txt ), @s4.list("abc/")
130
+ end
131
+
132
+ should "get content with special chars in it" do
133
+ `s3cmd put #{fixture("foo.txt")} 's3://#{@s4.bucket}/foo bar+baz.txt'`
134
+
135
+ @s4.get("foo bar+baz.txt") { |response| assert_equal "abc123", response.body }
136
+ end
137
+
138
+ should "upload foo.txt" do
139
+ `s3cmd del 's3://#{@s4.bucket}/foo.txt'`
140
+ @s4.upload(fixture("foo.txt"))
141
+ @s4.get("foo.txt") { |response| assert_equal "abc123", response.body }
142
+ end
143
+
144
+ should "bark when no URL is provided" do
145
+ assert_raise(ArgumentError) { S4.connect("") }
146
+ assert_raise(ArgumentError) { S4.connect(nil) }
147
+
148
+ assert_raise(URI::InvalidURIError) { S4.connect("s3://foo:bar/baz") }
149
+ end
150
+ end
151
+ end
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: s4
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 0.0.1
5
+ version: 0.0.2
6
6
  platform: ruby
7
7
  authors:
8
8
  - Ben Alavi
@@ -10,8 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2011-05-20 00:00:00 -03:00
14
- default_executable:
13
+ date: 2011-09-01 00:00:00 Z
15
14
  dependencies:
16
15
  - !ruby/object:Gem::Dependency
17
16
  name: net-http-persistent
@@ -59,8 +58,7 @@ files:
59
58
  - Rakefile
60
59
  - lib/s4.rb
61
60
  - s4.gemspec
62
- - test/s3_test.rb
63
- has_rdoc: true
61
+ - test/s4_test.rb
64
62
  homepage: http://github.com/benalavi/s4
65
63
  licenses: []
66
64
 
@@ -84,7 +82,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
84
82
  requirements: []
85
83
 
86
84
  rubyforge_project:
87
- rubygems_version: 1.6.2
85
+ rubygems_version: 1.8.10
88
86
  signing_key:
89
87
  specification_version: 3
90
88
  summary: Simple API for AWS S3
data/test/s3_test.rb DELETED
@@ -1,108 +0,0 @@
1
- require "cutest"
2
- require "timecop"
3
- require "fileutils"
4
-
5
- begin
6
- require "ruby-debug"
7
- rescue LoadError
8
- end
9
-
10
- require File.expand_path("../lib/s3", File.dirname(__FILE__))
11
-
12
- def fixture(filename="")
13
- File.join(File.dirname(__FILE__), "fixtures", filename)
14
- end
15
-
16
- def output(filename="")
17
- File.join(File.dirname(__FILE__), "output", filename)
18
- end
19
-
20
- Bucket = URI(ENV["S3_URL"]).path[1..-1]
21
-
22
- scope do
23
- setup do
24
- FileUtils.rm_rf(output)
25
- FileUtils.mkdir_p(output)
26
- @s3 = S3.new
27
- end
28
-
29
- test "should download foo.txt" do
30
- `s3cmd put #{fixture("foo.txt")} s3://#{Bucket}/foo.txt`
31
- @s3.download("foo.txt", output("foo.txt"))
32
-
33
- assert_equal "abc123", File.read(output("foo.txt"))
34
- end
35
-
36
- test "should not download non-existent files" do
37
- `s3cmd del 's3://#{Bucket}/foo.txt'`
38
- @s3.download("foo.txt", output("foo.txt"))
39
-
40
- assert !File.exists?(output("foo.txt"))
41
- end
42
-
43
- test "should yield raw response from get of foo.txt" do
44
- `s3cmd put #{fixture("foo.txt")} s3://#{Bucket}/foo.txt`
45
-
46
- @s3.get("foo.txt") do |response|
47
- assert_equal "abc123", response.body
48
- end
49
- end
50
-
51
- test "should delete object" do
52
- `s3cmd put #{fixture("foo.txt")} s3://#{Bucket}/foo.txt`
53
- @s3.get("foo.txt") { |response| assert_equal "abc123", response.body }
54
- @s3.delete("foo.txt")
55
-
56
- assert_equal nil, @s3.get("foo.txt")
57
- end
58
-
59
- test "should return list of items in bucket" do
60
- `s3cmd del 's3://#{Bucket}/abc/*'`
61
- `s3cmd del 's3://#{Bucket}/boom.txt'`
62
- `s3cmd del 's3://#{Bucket}/foo\ bar+baz.txt'`
63
- `s3cmd put #{fixture("foo.txt")} s3://#{Bucket}/foo.txt`
64
- `s3cmd put #{fixture("foo.txt")} s3://#{Bucket}/bar.txt`
65
- `s3cmd put #{fixture("foo.txt")} s3://#{Bucket}/baz.txt`
66
-
67
- assert_equal %w( bar.txt baz.txt foo.txt ), @s3.list
68
- end
69
-
70
- test "should return list of keys starting with prefix" do
71
- `s3cmd put #{fixture("foo.txt")} s3://#{Bucket}/abc/bing.txt`
72
- `s3cmd put #{fixture("foo.txt")} s3://#{Bucket}/abc/bang.txt`
73
- `s3cmd put #{fixture("foo.txt")} s3://#{Bucket}/boom.txt`
74
-
75
- assert_equal %w( abc/bang.txt abc/bing.txt ), @s3.list("abc/")
76
- end
77
-
78
- test "should get content with special chars in it" do
79
- `s3cmd put #{fixture("foo.txt")} 's3://#{Bucket}/foo bar+baz.txt'`
80
-
81
- @s3.get("foo bar+baz.txt") { |response| assert_equal "abc123", response.body }
82
- end
83
-
84
- test "should upload foo.txt" do
85
- `s3cmd del 's3://#{Bucket}/foo.txt'`
86
-
87
- s3 = S3.new
88
-
89
- s3.upload(fixture("foo.txt"))
90
-
91
- s3.get("foo.txt") { |response| assert_equal "abc123", response.body }
92
- end
93
-
94
- test "barks when no URL is provided" do
95
- assert_raise(ArgumentError) { S3.new("") }
96
- assert_raise(ArgumentError) { S3.new(nil) }
97
-
98
- assert_raise(URI::InvalidURIError) { S3.new("s3://foo:bar/baz") }
99
- end
100
-
101
- test "raises on S3 errors" do
102
- s3 = S3.new(ENV["S3_URL"].sub(/.@/, "@")) # Break the password
103
-
104
- assert_raise(S3::Error) do
105
- s3.upload(fixture("foo.txt"))
106
- end
107
- end
108
- end