s4 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. data/Rakefile +5 -0
  2. data/lib/s4.rb +144 -0
  3. data/s4.gemspec +25 -0
  4. data/test/s3_test.rb +108 -0
  5. metadata +92 -0
@@ -0,0 +1,5 @@
1
+ task :default => :test
2
+
3
+ task :test do
4
+ system "cutest test/*.rb"
5
+ end
@@ -0,0 +1,144 @@
1
+ require "net/http/persistent"
2
+ require "rexml/document"
3
+ require "base64"
4
+ require "time"
5
+
6
+ # Simpler AWS S3 library
7
+ class S4
8
+ VERSION = "0.0.1"
9
+
10
+ # sub-resource names which may appear in the query string and also must be
11
+ # signed against.
12
+ SubResources = %w( acl location logging notification partNumber policy requestPayment torrent uploadId uploads versionId versioning versions website )
13
+
14
+ # Header over-rides which may appear in the query string and also must be
15
+ # signed against.
16
+ HeaderValues = %w( response-content-type response-content-language response-expires reponse-cache-control response-content-disposition response-content-encoding )
17
+
18
+ attr_reader :connection, :access_key_id, :secret_access_key, :bucket, :host
19
+
20
+ def initialize(s3_url = ENV["S3_URL"])
21
+ raise ArgumentError, "No S3 URL provided. You can set ENV['S3_URL'], too." if s3_url.nil? || s3_url.empty?
22
+
23
+ begin
24
+ url = URI(s3_url)
25
+ rescue URI::InvalidURIError => e
26
+ e.message << " The format is s3://access_key_id:secret_access_key@s3.amazonaws.com/bucket"
27
+ raise e
28
+ end
29
+
30
+ @access_key_id = url.user
31
+ @secret_access_key = URI.unescape(url.password || "")
32
+ @host = url.host
33
+ @bucket = url.path[1..-1]
34
+ end
35
+
36
+ def connection
37
+ @connection ||= Net::HTTP::Persistent.new("aws-s3/#{bucket}")
38
+ end
39
+
40
+ # Lower level object get which just yields the successful S3 response to the
41
+ # block. See #download if you want to simply copy a file from S3 to local.
42
+ def get(name, &block)
43
+ request(uri(name), &block)
44
+ end
45
+
46
+ # Download the file with the given filename to the given destination.
47
+ def download(name, destination=nil)
48
+ get(name) do |response|
49
+ File.open(destination || File.join(Dir.pwd, File.basename(name)), "wb") do |io|
50
+ response.read_body do |chunk|
51
+ io.write(chunk)
52
+ end
53
+ end
54
+ end
55
+ end
56
+
57
+ # Delete the object with the given name.
58
+ def delete(name)
59
+ request(uri = uri(name), Net::HTTP::Delete.new(uri.request_uri))
60
+ end
61
+
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.
65
+ def upload(name, destination=nil)
66
+ put File.open(name, "rb"), destination || File.basename(name)
67
+ end
68
+
69
+ def put(io, name)
70
+ uri = uri(name)
71
+ req = Net::HTTP::Put.new(uri.request_uri)
72
+
73
+ req.body_stream = io
74
+ req.add_field "Content-Length", io.size
75
+ req.add_field "Content-Type", "application/x-www-form-urlencoded"
76
+
77
+ request(URI::HTTP.build(host: host, path: "/#{bucket}/#{name}"), req)
78
+ end
79
+
80
+ # List bucket contents
81
+ def list(prefix = "")
82
+ REXML::Document.new(request(uri("", query: "prefix=#{prefix}"))).elements.collect("//Key", &:text)
83
+ end
84
+
85
+ private
86
+
87
+ def uri(path, options={})
88
+ URI::HTTP.build(options.merge(host: host, path: "/#{bucket}/#{CGI.escape(path.sub(/^\//, ""))}"))
89
+ end
90
+
91
+ # Makes a request to the S3 API.
92
+ def request(uri, request = nil)
93
+ # TODO: Possibly use SAX parsing for large request bodies (?)
94
+
95
+ request ||= Net::HTTP::Get.new(uri.request_uri)
96
+
97
+ connection.request(uri, sign(uri, request)) do |response|
98
+ 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
+
109
+ else
110
+ raise Error.from_xml(response.body)
111
+
112
+ end
113
+ end
114
+ end
115
+
116
+ def sign(uri, request)
117
+ date = Time.now.utc.rfc822
118
+
119
+ request.add_field "Date", date
120
+ request.add_field "Authorization", "AWS #{access_key_id}:#{signature(uri, request)}"
121
+
122
+ request
123
+ end
124
+
125
+ def signature(uri, request)
126
+ Base64.encode64(
127
+ OpenSSL::HMAC.digest(
128
+ OpenSSL::Digest::Digest.new("sha1"),
129
+ secret_access_key,
130
+ "#{request.class::METHOD}\n\n#{request["Content-Type"]}\n#{request.fetch("Date")}\n#{uri.path}"
131
+ )
132
+ ).chomp
133
+ end
134
+
135
+ # Base class of all S3 Errors
136
+ class Error < ::RuntimeError
137
+ def self.from_xml(xml)
138
+ doc = REXML::Document.new(xml).elements["//Error"]
139
+ code = doc.elements["Code"].text
140
+ message = doc.elements["Message"].text
141
+ new("#{code}: #{message}")
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,25 @@
1
+ require "./lib/s4"
2
+
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"
11
+
12
+ s.files = Dir[
13
+ "LICENSE",
14
+ "README.markdown",
15
+ "Rakefile",
16
+ "lib/**/*.rb",
17
+ "*.gemspec",
18
+ "test/**/*.rb"
19
+ ]
20
+
21
+ s.add_dependency "net-http-persistent", "~> 1.7"
22
+
23
+ s.add_development_dependency "cutest"
24
+ s.add_development_dependency "timecop", "~> 0.3"
25
+ end
@@ -0,0 +1,108 @@
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
metadata ADDED
@@ -0,0 +1,92 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: s4
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.0.1
6
+ platform: ruby
7
+ authors:
8
+ - Ben Alavi
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2011-05-20 00:00:00 -03:00
14
+ default_executable:
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: net-http-persistent
18
+ prerelease: false
19
+ requirement: &id001 !ruby/object:Gem::Requirement
20
+ none: false
21
+ requirements:
22
+ - - ~>
23
+ - !ruby/object:Gem::Version
24
+ version: "1.7"
25
+ type: :runtime
26
+ version_requirements: *id001
27
+ - !ruby/object:Gem::Dependency
28
+ name: cutest
29
+ prerelease: false
30
+ requirement: &id002 !ruby/object:Gem::Requirement
31
+ none: false
32
+ requirements:
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: "0"
36
+ type: :development
37
+ version_requirements: *id002
38
+ - !ruby/object:Gem::Dependency
39
+ name: timecop
40
+ prerelease: false
41
+ requirement: &id003 !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ~>
45
+ - !ruby/object:Gem::Version
46
+ version: "0.3"
47
+ type: :development
48
+ version_requirements: *id003
49
+ description: Simple API for AWS S3
50
+ email:
51
+ - ben.alavi@citrusbyte.com
52
+ executables: []
53
+
54
+ extensions: []
55
+
56
+ extra_rdoc_files: []
57
+
58
+ files:
59
+ - Rakefile
60
+ - lib/s4.rb
61
+ - s4.gemspec
62
+ - test/s3_test.rb
63
+ has_rdoc: true
64
+ homepage: http://github.com/benalavi/s4
65
+ licenses: []
66
+
67
+ post_install_message:
68
+ rdoc_options: []
69
+
70
+ require_paths:
71
+ - lib
72
+ required_ruby_version: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: "0"
78
+ required_rubygems_version: !ruby/object:Gem::Requirement
79
+ none: false
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: "0"
84
+ requirements: []
85
+
86
+ rubyforge_project:
87
+ rubygems_version: 1.6.2
88
+ signing_key:
89
+ specification_version: 3
90
+ summary: Simple API for AWS S3
91
+ test_files: []
92
+