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 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 localhost."
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]
@@ -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
- @buckets << bucket_obj
56
- @bucket_hash[bucket] = bucket_obj
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
- path = request.path
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
- if path == "/" and path_style_request
44
+ case s_req.type
45
+ when 'LIST_BUCKETS'
47
46
  response.status = 200
48
- response['Content-Type'] = 'text/xml'
47
+ response['Content-Type'] = 'application/xml'
49
48
  buckets = @store.buckets
50
49
  response.body = XmlAdapter.buckets(buckets)
51
- else
52
- if path_style_request
53
- elems = path[1,path_len].split("/")
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.buckets(buckets)
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
- object = elems[1,elems.size].join('/')
78
- real_obj = @store.get_object(bucket,object, request)
79
-
80
- if real_obj
81
- response.status = 200
82
- response['Content-Type'] = real_obj.content_type
83
- content_length = File::Stat.new(real_obj.io.path).size
84
- response['Etag'] = real_obj.md5
85
- response['Accept-Ranges'] = "bytes"
86
-
87
- # Added Range Query support
88
- if range = request.header["range"].first
89
- response.status = 206
90
- if range =~ /bytes=(\d*)-(\d*)/
91
- start = $1.to_i
92
- finish = $2.to_i
93
- finish_str = ""
94
- if finish == 0
95
- finish = content_length - 1
96
- finish_str = "#{finish}"
97
- else
98
- finish_str = finish.to_s
99
- end
100
-
101
- bytes_to_read = finish - start + 1
102
- response['Content-Range'] = "bytes #{start}-#{finish_str}/#{content_length}"
103
- real_obj.io.pos = start
104
- response.body = real_obj.io.read(bytes_to_read)
105
- return
106
- end
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
- response['Content-Length'] = File::Stat.new(real_obj.io.path).size
109
- response.body = real_obj.io
110
- else
111
- response.status = 404
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
- # This method takes a webrick request and generates a normalized Shuck request
146
- def normalize_request(webrick_r)
147
- path = webrick_r.path
133
+
134
+ def normalize_get(webrick_req,s_req)
135
+ path = webrick_req.path
148
136
  path_len = path.size
149
- path_style_request = true
150
- host = webrick_r["Host"]
151
- bucket = nil
152
- object = nil
153
- path_style_request = true
154
-
155
- if host != @hostname
156
- bucket = host.split(".")[0]
157
- path_style_request = false
158
- end
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
- req = Request.new
161
- req.path = webrick_r.path
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
- req.type = Request::CREATE_BUCKET
169
+ if s_req.bucket
170
+ s_req.type = Request::CREATE_BUCKET
166
171
  end
167
172
  else
168
- if path_style_request
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
- req.type = Request::CREATE_BUCKET
177
+ s_req.type = Request::CREATE_BUCKET
173
178
  else
174
- req.type = Request::STORE
175
- object = elems[1,elems.size].join('/')
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
- req.type = Request::STORE
179
- object = webrick_r.path
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 = webrick_r.header["x-amz-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
- req.src_bucket = src_elems[root_offset]
188
- req.src_object = src_elems[1 + root_offset,src_elems.size].join("/")
189
- req.type = Request::COPY
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
- req.bucket = bucket
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 = "localhost")
247
+ def initialize(port,store,hostname)
212
248
  @port = port
213
249
  @store = store
214
250
  @hostname = hostname
data/lib/shuck/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Shuck
2
- VERSION = "0.0.8"
2
+ VERSION = "0.0.9"
3
3
  end
@@ -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 = s3.localhost:10453
20
- host_bucket = %(bucket)s.s3.localhost:10453
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
- @s3cmd = "s3cmd --config"
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
- - 8
9
- version: 0.0.8
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-22 00:00:00 -08:00
17
+ date: 2012-02-23 00:00:00 -08:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency