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 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