shuck 0.0.8 → 0.0.9
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/shuck/cli.rb +5 -1
- data/lib/shuck/file_store.rb +4 -2
- data/lib/shuck/server.rb +139 -103
- data/lib/shuck/version.rb +1 -1
- data/lib/shuck/xml_adapter.rb +36 -0
- data/test/local_s3_cfg +2 -2
- data/test/s3cmd_test.rb +26 -1
- metadata +3 -3
data/lib/shuck/cli.rb
CHANGED
@@ -8,7 +8,7 @@ module Shuck
|
|
8
8
|
desc "server", "Run a server on a particular hostname"
|
9
9
|
method_option :root, :type => :string, :aliases => '-r', :required => true
|
10
10
|
method_option :port, :type => :numeric, :aliases => '-p', :required => true
|
11
|
-
method_option :hostname, :type => :string, :aliases => '-h', :desc => "The root name of the host. Defaults to
|
11
|
+
method_option :hostname, :type => :string, :aliases => '-h', :desc => "The root name of the host. Defaults to s3.amazonaws.com."
|
12
12
|
method_option :limit, :aliases => '-l', :type => :string, :desc => 'Rate limit for serving (ie. 50K, 1.0M)'
|
13
13
|
def server
|
14
14
|
store = nil
|
@@ -25,6 +25,10 @@ module Shuck
|
|
25
25
|
hostname = 's3.amazonaws.com'
|
26
26
|
if options[:hostname]
|
27
27
|
hostname = options[:hostname]
|
28
|
+
# In case the user has put a port on the hostname
|
29
|
+
if hostname =~ /:(\d+)/
|
30
|
+
hostname = hostname.split(":")[0]
|
31
|
+
end
|
28
32
|
end
|
29
33
|
|
30
34
|
if options[:limit]
|
data/lib/shuck/file_store.rb
CHANGED
@@ -52,8 +52,10 @@ module Shuck
|
|
52
52
|
def create_bucket(bucket)
|
53
53
|
FileUtils.mkdir_p(File.join(@root,bucket))
|
54
54
|
bucket_obj = Bucket.new(bucket,Time.now,[])
|
55
|
-
|
56
|
-
|
55
|
+
if !@bucket_hash[bucket]
|
56
|
+
@buckets << bucket_obj
|
57
|
+
@bucket_hash[bucket] = bucket_obj
|
58
|
+
end
|
57
59
|
end
|
58
60
|
|
59
61
|
def get_object(bucket,object, request)
|
data/lib/shuck/server.rb
CHANGED
@@ -5,17 +5,22 @@ require 'shuck/xml_adapter'
|
|
5
5
|
module Shuck
|
6
6
|
class Request
|
7
7
|
CREATE_BUCKET = "CREATE_BUCKET"
|
8
|
+
LIST_BUCKETS = "LIST_BUCKETS"
|
9
|
+
LS_BUCKET = "LS_BUCKET"
|
8
10
|
STORE = "STORE"
|
9
11
|
COPY = "COPY"
|
10
12
|
GET = "GET"
|
13
|
+
GET_ACL = "GET_ACL"
|
14
|
+
SET_ACL = "SET_ACL"
|
11
15
|
MOVE = "MOVE"
|
12
16
|
DELETE = "DELETE"
|
13
17
|
|
14
|
-
attr_accessor :bucket,:object,:type,:src_bucket,:src_object,:method,:webrick_request,:path
|
18
|
+
attr_accessor :bucket,:object,:type,:src_bucket,:src_object,:method,:webrick_request,:path,:is_path_style
|
15
19
|
|
16
20
|
def inspect
|
17
21
|
puts "-----Inspect Shuck Request"
|
18
22
|
puts "Type: #{@type}"
|
23
|
+
puts "Is Path Style: #{@is_path_style}"
|
19
24
|
puts "Request Method: #{@method}"
|
20
25
|
puts "Bucket: #{@bucket}"
|
21
26
|
puts "Object: #{@object}"
|
@@ -30,94 +35,77 @@ module Shuck
|
|
30
35
|
super(server)
|
31
36
|
@store = store
|
32
37
|
@hostname = hostname
|
38
|
+
@root_hostnames = [hostname,'localhost','s3.amazonaws.com','s3.localhost']
|
33
39
|
end
|
34
40
|
|
35
41
|
def do_GET(request, response)
|
36
|
-
|
37
|
-
path_len = path.size
|
38
|
-
host = request["HOST"]
|
39
|
-
path_style_request = true
|
40
|
-
bucket = nil
|
41
|
-
if host != @hostname
|
42
|
-
bucket = host.split(".")[0]
|
43
|
-
path_style_request = false
|
44
|
-
end
|
42
|
+
s_req = normalize_request(request)
|
45
43
|
|
46
|
-
|
44
|
+
case s_req.type
|
45
|
+
when 'LIST_BUCKETS'
|
47
46
|
response.status = 200
|
48
|
-
response['Content-Type'] = '
|
47
|
+
response['Content-Type'] = 'application/xml'
|
49
48
|
buckets = @store.buckets
|
50
49
|
response.body = XmlAdapter.buckets(buckets)
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
bucket = elems[0]
|
55
|
-
else
|
56
|
-
elems = path.split("/")
|
57
|
-
end
|
58
|
-
|
59
|
-
if elems.size == 0
|
60
|
-
# List buckets
|
61
|
-
buckets = @store.buckets
|
50
|
+
when 'LS_BUCKET'
|
51
|
+
bucket_obj = @store.get_bucket(s_req.bucket)
|
52
|
+
if bucket_obj
|
62
53
|
response.status = 200
|
63
|
-
response.body = XmlAdapter.
|
54
|
+
response.body = XmlAdapter.bucket(bucket_obj)
|
64
55
|
response['Content-Type'] = "application/xml"
|
65
|
-
elsif elems.size == 1
|
66
|
-
bucket_obj = @store.get_bucket(bucket)
|
67
|
-
if bucket_obj
|
68
|
-
response.status = 200
|
69
|
-
response.body = XmlAdapter.bucket(bucket_obj)
|
70
|
-
response['Content-Type'] = "application/xml"
|
71
|
-
else
|
72
|
-
response.status = 404
|
73
|
-
response.body = XmlAdapter.error_no_such_bucket(bucket)
|
74
|
-
response['Content-Type'] = "application/xml"
|
75
|
-
end
|
76
56
|
else
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
57
|
+
response.status = 404
|
58
|
+
response.body = XmlAdapter.error_no_such_bucket(s_req.bucket)
|
59
|
+
response['Content-Type'] = "application/xml"
|
60
|
+
end
|
61
|
+
when 'GET_ACL'
|
62
|
+
response.status = 200
|
63
|
+
response.body = XmlAdapter.acl()
|
64
|
+
response['Content-Type'] = 'application/xml'
|
65
|
+
when 'GET'
|
66
|
+
real_obj = @store.get_object(s_req.bucket,s_req.object,request)
|
67
|
+
if !real_obj
|
68
|
+
response.status = 404
|
69
|
+
response.body = ""
|
70
|
+
return
|
71
|
+
end
|
72
|
+
|
73
|
+
response.status = 200
|
74
|
+
response['Content-Type'] = real_obj.content_type
|
75
|
+
content_length = File::Stat.new(real_obj.io.path).size
|
76
|
+
response['Etag'] = real_obj.md5
|
77
|
+
response['Accept-Ranges'] = "bytes"
|
78
|
+
|
79
|
+
# Added Range Query support
|
80
|
+
if range = request.header["range"].first
|
81
|
+
response.status = 206
|
82
|
+
if range =~ /bytes=(\d*)-(\d*)/
|
83
|
+
start = $1.to_i
|
84
|
+
finish = $2.to_i
|
85
|
+
finish_str = ""
|
86
|
+
if finish == 0
|
87
|
+
finish = content_length - 1
|
88
|
+
finish_str = "#{finish}"
|
89
|
+
else
|
90
|
+
finish_str = finish.to_s
|
107
91
|
end
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
response.body =
|
92
|
+
|
93
|
+
bytes_to_read = finish - start + 1
|
94
|
+
response['Content-Range'] = "bytes #{start}-#{finish_str}/#{content_length}"
|
95
|
+
real_obj.io.pos = start
|
96
|
+
response.body = real_obj.io.read(bytes_to_read)
|
97
|
+
return
|
113
98
|
end
|
114
99
|
end
|
100
|
+
response['Content-Length'] = File::Stat.new(real_obj.io.path).size
|
101
|
+
response.body = real_obj.io
|
115
102
|
end
|
116
103
|
end
|
117
104
|
|
118
105
|
def do_PUT(request,response)
|
119
106
|
s_req = normalize_request(request)
|
120
107
|
|
108
|
+
|
121
109
|
case s_req.type
|
122
110
|
when Request::COPY
|
123
111
|
@store.copy_object(s_req.src_bucket,s_req.src_object,s_req.bucket,s_req.object)
|
@@ -142,57 +130,105 @@ module Shuck
|
|
142
130
|
end
|
143
131
|
|
144
132
|
private
|
145
|
-
|
146
|
-
def
|
147
|
-
path =
|
133
|
+
|
134
|
+
def normalize_get(webrick_req,s_req)
|
135
|
+
path = webrick_req.path
|
148
136
|
path_len = path.size
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
137
|
+
query = webrick_req.query
|
138
|
+
if path == "/" and s_req.is_path_style
|
139
|
+
s_req.type = Request::LIST_BUCKETS
|
140
|
+
else
|
141
|
+
if s_req.is_path_style
|
142
|
+
elems = path[1,path_len].split("/")
|
143
|
+
s_req.bucket = elems[0]
|
144
|
+
else
|
145
|
+
elems = path.split("/")
|
146
|
+
end
|
159
147
|
|
160
|
-
|
161
|
-
|
148
|
+
if elems.size == 0
|
149
|
+
# List buckets
|
150
|
+
s_req.type = Request::LIST_BUCKETS
|
151
|
+
elsif elems.size == 1
|
152
|
+
s_req.type = Request::LS_BUCKET
|
153
|
+
else
|
154
|
+
if query["acl"] == ""
|
155
|
+
s_req.type = Request::GET_ACL
|
156
|
+
else
|
157
|
+
s_req.type = Request::GET
|
158
|
+
end
|
159
|
+
object = elems[1,elems.size].join('/')
|
160
|
+
s_req.object = object
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
162
164
|
|
165
|
+
def normalize_put(webrick_req,s_req)
|
166
|
+
path = webrick_req.path
|
167
|
+
path_len = path.size
|
163
168
|
if path == "/"
|
164
|
-
if bucket
|
165
|
-
|
169
|
+
if s_req.bucket
|
170
|
+
s_req.type = Request::CREATE_BUCKET
|
166
171
|
end
|
167
172
|
else
|
168
|
-
if
|
173
|
+
if s_req.is_path_style
|
169
174
|
elems = path[1,path_len].split("/")
|
170
|
-
bucket = elems[0]
|
175
|
+
s_req.bucket = elems[0]
|
171
176
|
if elems.size == 1
|
172
|
-
|
177
|
+
s_req.type = Request::CREATE_BUCKET
|
173
178
|
else
|
174
|
-
|
175
|
-
|
179
|
+
if webrick_req.request_line =~ /\?acl/
|
180
|
+
s_req.type = Request::SET_ACL
|
181
|
+
else
|
182
|
+
s_req.type = Request::STORE
|
183
|
+
end
|
184
|
+
s_req.object = elems[1,elems.size].join('/')
|
176
185
|
end
|
177
186
|
else
|
178
|
-
|
179
|
-
|
187
|
+
if webrick_req.request_line =~ /\?acl/
|
188
|
+
s_req.type = Request::SET_ACL
|
189
|
+
else
|
190
|
+
s_req.type = Request::STORE
|
191
|
+
end
|
192
|
+
s_req.object = webrick_req.path
|
180
193
|
end
|
181
194
|
end
|
182
195
|
|
183
|
-
copy_source =
|
196
|
+
copy_source = webrick_req.header["x-amz-copy-source"]
|
184
197
|
if copy_source and copy_source.size == 1
|
185
198
|
src_elems = copy_source.first.split("/")
|
186
199
|
root_offset = src_elems[0] == "" ? 1 : 0
|
187
|
-
|
188
|
-
|
189
|
-
|
200
|
+
s_req.src_bucket = src_elems[root_offset]
|
201
|
+
s_req.src_object = src_elems[1 + root_offset,src_elems.size].join("/")
|
202
|
+
s_req.type = Request::COPY
|
203
|
+
end
|
204
|
+
|
205
|
+
s_req.webrick_request = webrick_req
|
206
|
+
end
|
207
|
+
|
208
|
+
# This method takes a webrick request and generates a normalized Shuck request
|
209
|
+
def normalize_request(webrick_req)
|
210
|
+
host_header= webrick_req["Host"]
|
211
|
+
host = host_header.split(':')[0]
|
212
|
+
|
213
|
+
s_req = Request.new
|
214
|
+
s_req.path = webrick_req.path
|
215
|
+
s_req.is_path_style = true
|
216
|
+
|
217
|
+
if !@root_hostnames.include?(host)
|
218
|
+
s_req.bucket = host.split(".")[0]
|
219
|
+
s_req.is_path_style = false
|
220
|
+
end
|
221
|
+
|
222
|
+
case webrick_req.request_method
|
223
|
+
when 'PUT'
|
224
|
+
normalize_put(webrick_req,s_req)
|
225
|
+
when 'GET'
|
226
|
+
normalize_get(webrick_req,s_req)
|
227
|
+
else
|
228
|
+
raise "Unknown Request"
|
190
229
|
end
|
191
230
|
|
192
|
-
|
193
|
-
req.object = object
|
194
|
-
req.webrick_request = webrick_r
|
195
|
-
return req
|
231
|
+
return s_req
|
196
232
|
end
|
197
233
|
|
198
234
|
def dump_request(request)
|
@@ -208,7 +244,7 @@ module Shuck
|
|
208
244
|
|
209
245
|
|
210
246
|
class Server
|
211
|
-
def initialize(port,store,hostname
|
247
|
+
def initialize(port,store,hostname)
|
212
248
|
@port = port
|
213
249
|
@store = store
|
214
250
|
@hostname = hostname
|
data/lib/shuck/version.rb
CHANGED
data/lib/shuck/xml_adapter.rb
CHANGED
@@ -44,6 +44,20 @@ module Shuck
|
|
44
44
|
output
|
45
45
|
end
|
46
46
|
|
47
|
+
def self.error_no_such_key(name)
|
48
|
+
output = ""
|
49
|
+
xml = Builder::XmlMarkup.new(:target => output)
|
50
|
+
xml.instruct! :xml, :version=>"1.0", :encoding=>"UTF-8"
|
51
|
+
xml.Error { |err|
|
52
|
+
err.Code("NoSuchKey")
|
53
|
+
err.Message("The specified key does not exist")
|
54
|
+
err.Key(name)
|
55
|
+
err.RequestId(1)
|
56
|
+
err.HostId(2)
|
57
|
+
}
|
58
|
+
output
|
59
|
+
end
|
60
|
+
|
47
61
|
def self.bucket(bucket)
|
48
62
|
output = ""
|
49
63
|
xml = Builder::XmlMarkup.new(:target => output)
|
@@ -58,5 +72,27 @@ module Shuck
|
|
58
72
|
output
|
59
73
|
end
|
60
74
|
|
75
|
+
# ACL xml
|
76
|
+
def self.acl(object = nil)
|
77
|
+
output = ""
|
78
|
+
xml = Builder::XmlMarkup.new(:target => output)
|
79
|
+
xml.instruct! :xml, :version=>"1.0", :encoding=>"UTF-8"
|
80
|
+
xml.AccessControlPolicy(:xmlns => "http://s3.amazonaws.com/doc/2006-03-01/") { |acp|
|
81
|
+
acp.Owner do |owner|
|
82
|
+
owner.ID("abc")
|
83
|
+
owner.DisplayName("You")
|
84
|
+
end
|
85
|
+
acp.AccessControlList do |acl|
|
86
|
+
acl.Grant do |grant|
|
87
|
+
grant.Grantee("xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance", "xsi:type" => "CanonicalUser") do |grantee|
|
88
|
+
grantee.ID("abc")
|
89
|
+
grantee.DisplayName("You")
|
90
|
+
end
|
91
|
+
grant.Permission("FULL_CONTROL")
|
92
|
+
end
|
93
|
+
end
|
94
|
+
}
|
95
|
+
output
|
96
|
+
end
|
61
97
|
end
|
62
98
|
end
|
data/test/local_s3_cfg
CHANGED
@@ -16,8 +16,8 @@ gpg_decrypt = %(gpg_command)s -d --verbose --no-use-agent --batch --yes --passph
|
|
16
16
|
gpg_encrypt = %(gpg_command)s -c --verbose --no-use-agent --batch --yes --passphrase-fd %(passphrase_fd)s -o %(output_file)s %(input_file)s
|
17
17
|
gpg_passphrase =
|
18
18
|
guess_mime_type = True
|
19
|
-
host_base =
|
20
|
-
host_bucket = %(bucket)s.
|
19
|
+
host_base = localhost:10453
|
20
|
+
host_bucket = %(bucket)s.localhost:10453
|
21
21
|
human_readable_sizes = False
|
22
22
|
list_md5 = False
|
23
23
|
preserve_attrs = True
|
data/test/s3cmd_test.rb
CHANGED
@@ -6,16 +6,41 @@ require 'shuck/server'
|
|
6
6
|
class S3CmdTest < Test::Unit::TestCase
|
7
7
|
|
8
8
|
def setup
|
9
|
-
|
9
|
+
config = File.expand_path(File.join(File.dirname(__FILE__),'local_s3_cfg'))
|
10
|
+
@s3cmd = "s3cmd --config #{config}"
|
10
11
|
end
|
11
12
|
|
12
13
|
def teardown
|
13
14
|
end
|
14
15
|
|
15
16
|
def test_create_bucket
|
17
|
+
`#{@s3cmd} mb s3://s3cmd_bucket`
|
18
|
+
output = `#{@s3cmd} ls`
|
19
|
+
assert_match(/s3cmd_bucket/,output)
|
16
20
|
end
|
17
21
|
|
18
22
|
def test_store
|
23
|
+
File.open(__FILE__,'rb') do |input|
|
24
|
+
File.open("/tmp/shuck_upload",'wb') do |output|
|
25
|
+
output << input.read
|
26
|
+
end
|
27
|
+
end
|
28
|
+
output = `#{@s3cmd} put /tmp/shuck_upload s3://s3cmd_bucket/upload`
|
29
|
+
assert_match(/stored/,output)
|
30
|
+
|
31
|
+
FileUtils.rm("/tmp/shuck_upload")
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_acl
|
35
|
+
File.open(__FILE__,'rb') do |input|
|
36
|
+
File.open("/tmp/shuck_acl_upload",'wb') do |output|
|
37
|
+
output << input.read
|
38
|
+
end
|
39
|
+
end
|
40
|
+
output = `#{@s3cmd} put /tmp/shuck_acl_upload s3://s3cmd_bucket/acl_upload`
|
41
|
+
assert_match(/stored/,output)
|
42
|
+
|
43
|
+
output = `#{@s3cmd} --force setacl -P s3://s3cmd_bucket/acl_upload`
|
19
44
|
end
|
20
45
|
|
21
46
|
def test_large_store
|
metadata
CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
|
|
5
5
|
segments:
|
6
6
|
- 0
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
version: 0.0.
|
8
|
+
- 9
|
9
|
+
version: 0.0.9
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Curtis Spencer
|
@@ -14,7 +14,7 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2012-02-
|
17
|
+
date: 2012-02-23 00:00:00 -08:00
|
18
18
|
default_executable:
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|