shuck 0.0.8 → 0.0.9
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/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
|