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.
- data/Rakefile +5 -0
- data/lib/s4.rb +144 -0
- data/s4.gemspec +25 -0
- data/test/s3_test.rb +108 -0
- metadata +92 -0
data/Rakefile
ADDED
data/lib/s4.rb
ADDED
@@ -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
|
data/s4.gemspec
ADDED
@@ -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
|
data/test/s3_test.rb
ADDED
@@ -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
|
+
|