s4 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.
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
+