fakes3 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile.lock +22 -9
- data/README.md +10 -7
- data/Rakefile +1 -0
- data/lib/fakes3/bucket.rb +46 -2
- data/lib/fakes3/bucket_query.rb +11 -0
- data/lib/fakes3/file_store.rb +20 -2
- data/lib/fakes3/red_black_tree.rb +395 -0
- data/lib/fakes3/s3_object.rb +6 -0
- data/lib/fakes3/server.rb +67 -8
- data/lib/fakes3/unsupported_operation.rb +4 -0
- data/lib/fakes3/version.rb +1 -1
- data/lib/fakes3/xml_adapter.rb +51 -2
- data/test/right_aws_commands_test.rb +1 -1
- data/test/s3_commands_test.rb +68 -10
- metadata +6 -3
data/Gemfile.lock
CHANGED
@@ -8,25 +8,38 @@ PATH
|
|
8
8
|
GEM
|
9
9
|
remote: http://rubygems.org/
|
10
10
|
specs:
|
11
|
+
archive-tar-minitar (0.5.2)
|
11
12
|
aws-s3 (0.6.2)
|
12
13
|
builder
|
13
14
|
mime-types
|
14
15
|
xml-simple
|
15
|
-
builder (
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
16
|
+
builder (3.0.0)
|
17
|
+
columnize (0.3.6)
|
18
|
+
linecache19 (0.5.12)
|
19
|
+
ruby_core_source (>= 0.1.4)
|
20
|
+
mime-types (1.18)
|
21
|
+
right_aws (3.0.4)
|
22
|
+
right_http_connection (>= 1.2.5)
|
23
|
+
right_http_connection (1.3.0)
|
24
|
+
ruby-debug-base19 (0.11.25)
|
25
|
+
columnize (>= 0.3.1)
|
26
|
+
linecache19 (>= 0.5.11)
|
27
|
+
ruby_core_source (>= 0.1.4)
|
28
|
+
ruby-debug19 (0.11.6)
|
29
|
+
columnize (>= 0.3.1)
|
30
|
+
linecache19 (>= 0.5.11)
|
31
|
+
ruby-debug-base19 (>= 0.11.19)
|
32
|
+
ruby_core_source (0.1.5)
|
33
|
+
archive-tar-minitar (>= 0.5.2)
|
34
|
+
thor (0.14.6)
|
35
|
+
xml-simple (1.1.1)
|
22
36
|
|
23
37
|
PLATFORMS
|
24
38
|
ruby
|
25
39
|
|
26
40
|
DEPENDENCIES
|
27
41
|
aws-s3
|
28
|
-
builder
|
29
42
|
bundler (>= 1.0.0)
|
30
43
|
fakes3!
|
31
44
|
right_aws
|
32
|
-
|
45
|
+
ruby-debug19
|
data/README.md
CHANGED
@@ -3,7 +3,10 @@ FakeS3 is a lightweight server that responds to the same calls Amazon S3 respond
|
|
3
3
|
It is extremely useful for testing of S3 in a sandbox environment without actually
|
4
4
|
making calls to Amazon, which not only require network, but also cost you precious dollars.
|
5
5
|
|
6
|
-
|
6
|
+
The goal of Fake S3 is to minimize runtime dependencies and be more of a
|
7
|
+
development tool to test S3 calls in your code rather than a production server
|
8
|
+
looking to duplicate S3 functionality. Trying RiakCS, ParkPlace/Boardwalk, or
|
9
|
+
Ceph might be a place to start if that is your goal.
|
7
10
|
|
8
11
|
FakeS3 doesn't support all of the S3 command set, but the basic ones like put, get,
|
9
12
|
list, copy, and make bucket are supported. More coming soon.
|
@@ -20,20 +23,20 @@ To run a fakes3 server, you just specify a root and a port.
|
|
20
23
|
|
21
24
|
Take a look at the test cases to see client example usage. For now, FakeS3 is
|
22
25
|
mainly tested with s3cmd, aws-s3 gem, and right_aws. There are plenty more
|
23
|
-
libraries out there, and please do mention other clients.
|
26
|
+
libraries out there, and please do mention if other clients work or not.
|
24
27
|
|
25
28
|
## Running Tests
|
26
|
-
In order to run the tests add the following line to your /etc/hosts:
|
27
29
|
|
28
|
-
|
29
|
-
|
30
|
-
Then start the test server using
|
30
|
+
Start the test server using
|
31
31
|
|
32
32
|
rake test_server
|
33
33
|
|
34
|
-
|
35
34
|
Then in another terminal window run
|
36
35
|
|
37
36
|
rake test
|
38
37
|
|
39
38
|
It is a TODO to get this to be just one command
|
39
|
+
|
40
|
+
## More Information
|
41
|
+
|
42
|
+
Check out the wiki https://github.com/jubos/fake-s3/wiki
|
data/Rakefile
CHANGED
data/lib/fakes3/bucket.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
require 'builder'
|
2
|
+
require 'thread'
|
2
3
|
require 'fakes3/s3_object'
|
4
|
+
require 'fakes3/red_black_tree'
|
3
5
|
|
4
6
|
module FakeS3
|
5
7
|
class Bucket
|
@@ -8,10 +10,52 @@ module FakeS3
|
|
8
10
|
def initialize(name,creation_date,objects)
|
9
11
|
@name = name
|
10
12
|
@creation_date = creation_date
|
11
|
-
@objects =
|
13
|
+
@objects = SortedSet.new
|
14
|
+
@objects = RedBlackTree.new
|
12
15
|
objects.each do |obj|
|
13
|
-
@objects
|
16
|
+
@objects.add(obj)
|
14
17
|
end
|
18
|
+
@mutex = Mutex.new
|
19
|
+
end
|
20
|
+
|
21
|
+
def <<(object)
|
22
|
+
# Unfortunately have to synchronize here since the RB Tree is not thread
|
23
|
+
# safe. Probably can get finer granularity if performance is important
|
24
|
+
@mutex.synchronize do
|
25
|
+
exists = @objects.search(object.name)
|
26
|
+
if exists.nil?
|
27
|
+
@objects.add(object.name,object)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def delete(object_name)
|
33
|
+
@objects.delete_via_key(object_name)
|
34
|
+
end
|
35
|
+
|
36
|
+
def query_for_range(options)
|
37
|
+
marker = options[:marker]
|
38
|
+
prefix = options[:prefix]
|
39
|
+
max_keys = options[:max_keys] || 1000
|
40
|
+
delimiter = options[:delimiter]
|
41
|
+
|
42
|
+
matches = []
|
43
|
+
is_truncated = false
|
44
|
+
|
45
|
+
@mutex.synchronize do
|
46
|
+
matches, is_truncated = @objects.search_for_range(
|
47
|
+
marker,prefix,max_keys,delimiter)
|
48
|
+
end
|
49
|
+
|
50
|
+
bq = BucketQuery.new
|
51
|
+
bq.bucket = self
|
52
|
+
bq.marker = marker
|
53
|
+
bq.prefix = prefix
|
54
|
+
bq.max_keys = max_keys
|
55
|
+
bq.delimiter = delimiter
|
56
|
+
bq.matches = matches
|
57
|
+
bq.is_truncated = is_truncated
|
58
|
+
return bq
|
15
59
|
end
|
16
60
|
|
17
61
|
end
|
data/lib/fakes3/file_store.rb
CHANGED
@@ -112,9 +112,9 @@ module FakeS3
|
|
112
112
|
return obj
|
113
113
|
end
|
114
114
|
|
115
|
-
def store_object(bucket,
|
115
|
+
def store_object(bucket,object_name,request)
|
116
116
|
begin
|
117
|
-
filename = File.join(@root,bucket,
|
117
|
+
filename = File.join(@root,bucket.name,object_name)
|
118
118
|
FileUtils.mkdir_p(filename)
|
119
119
|
|
120
120
|
metadata_dir = File.join(filename,SHUCK_METADATA_DIR)
|
@@ -124,6 +124,7 @@ module FakeS3
|
|
124
124
|
metadata = File.join(filename,SHUCK_METADATA_DIR,"metadata")
|
125
125
|
|
126
126
|
md5 = Digest::MD5.new
|
127
|
+
# TODO put a tmpfile here first and mv it over at the end
|
127
128
|
|
128
129
|
File.open(content,'wb') do |f|
|
129
130
|
request.body do |chunk|
|
@@ -139,9 +140,14 @@ module FakeS3
|
|
139
140
|
File.open(metadata,'w') do |f|
|
140
141
|
f << YAML::dump(metadata_struct)
|
141
142
|
end
|
143
|
+
|
142
144
|
obj = S3Object.new
|
145
|
+
obj.name = object_name
|
143
146
|
obj.md5 = metadata_struct[:md5]
|
144
147
|
obj.content_type = metadata_struct[:content_type]
|
148
|
+
|
149
|
+
# TODO need a semaphore here since bucket is not probably not thread safe
|
150
|
+
bucket << obj
|
145
151
|
return obj
|
146
152
|
rescue
|
147
153
|
puts $!
|
@@ -149,5 +155,17 @@ module FakeS3
|
|
149
155
|
return nil
|
150
156
|
end
|
151
157
|
end
|
158
|
+
|
159
|
+
def delete_object(bucket,object_name,request)
|
160
|
+
begin
|
161
|
+
filename = File.join(@root,bucket.name,object_name)
|
162
|
+
FileUtils.rm_rf(filename)
|
163
|
+
bucket.delete(object_name)
|
164
|
+
rescue
|
165
|
+
puts $!
|
166
|
+
$!.backtrace.each { |line| puts line }
|
167
|
+
return nil
|
168
|
+
end
|
169
|
+
end
|
152
170
|
end
|
153
171
|
end
|
@@ -0,0 +1,395 @@
|
|
1
|
+
# Algorithm based on "Introduction to Algorithms" by Cormen and others
|
2
|
+
# Retrieved from
|
3
|
+
# https://github.com/headius/redblack/blob/master/red_black_tree.rb
|
4
|
+
#
|
5
|
+
# Modified by Curtis Spencer
|
6
|
+
class RedBlackTree
|
7
|
+
class Node
|
8
|
+
attr_accessor :color
|
9
|
+
attr_accessor :key
|
10
|
+
attr_accessor :left
|
11
|
+
attr_accessor :right
|
12
|
+
attr_accessor :parent
|
13
|
+
attr_accessor :value
|
14
|
+
|
15
|
+
RED = :red
|
16
|
+
BLACK = :black
|
17
|
+
COLORS = [RED, BLACK].freeze
|
18
|
+
|
19
|
+
def initialize(key, value, color = RED)
|
20
|
+
raise ArgumentError, "Bad value for color parameter" unless COLORS.include?(color)
|
21
|
+
@color = color
|
22
|
+
@key = key
|
23
|
+
@value = value
|
24
|
+
@left = @right = @parent = NilNode.instance
|
25
|
+
end
|
26
|
+
|
27
|
+
def black?
|
28
|
+
return color == BLACK
|
29
|
+
end
|
30
|
+
|
31
|
+
def red?
|
32
|
+
return color == RED
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class NilNode < Node
|
37
|
+
class << self
|
38
|
+
private :new
|
39
|
+
@instance = nil
|
40
|
+
|
41
|
+
# it's not thread safe
|
42
|
+
def instance
|
43
|
+
if @instance.nil?
|
44
|
+
@instance = new
|
45
|
+
|
46
|
+
def instance
|
47
|
+
return @instance
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
return @instance
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def initialize
|
56
|
+
self.color = BLACK
|
57
|
+
self.key = 0
|
58
|
+
self.left = nil
|
59
|
+
self.right = nil
|
60
|
+
self.parent = nil
|
61
|
+
end
|
62
|
+
|
63
|
+
def nil?
|
64
|
+
return true
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
include Enumerable
|
69
|
+
|
70
|
+
attr_accessor :root
|
71
|
+
attr_accessor :size
|
72
|
+
|
73
|
+
def initialize
|
74
|
+
self.root = NilNode.instance
|
75
|
+
self.size = 0
|
76
|
+
end
|
77
|
+
|
78
|
+
def add(key,value)
|
79
|
+
insert(Node.new(key,value))
|
80
|
+
end
|
81
|
+
|
82
|
+
def insert(x)
|
83
|
+
insert_helper(x)
|
84
|
+
|
85
|
+
x.color = Node::RED
|
86
|
+
while x != root && x.parent.color == Node::RED
|
87
|
+
if x.parent == x.parent.parent.left
|
88
|
+
y = x.parent.parent.right
|
89
|
+
if !y.nil? && y.color == Node::RED
|
90
|
+
x.parent.color = Node::BLACK
|
91
|
+
y.color = Node::BLACK
|
92
|
+
x.parent.parent.color = Node::RED
|
93
|
+
x = x.parent.parent
|
94
|
+
else
|
95
|
+
if x == x.parent.right
|
96
|
+
x = x.parent
|
97
|
+
left_rotate(x)
|
98
|
+
end
|
99
|
+
x.parent.color = Node::BLACK
|
100
|
+
x.parent.parent.color = Node::RED
|
101
|
+
right_rotate(x.parent.parent)
|
102
|
+
end
|
103
|
+
else
|
104
|
+
y = x.parent.parent.left
|
105
|
+
if !y.nil? && y.color == Node::RED
|
106
|
+
x.parent.color = Node::BLACK
|
107
|
+
y.color = Node::BLACK
|
108
|
+
x.parent.parent.color = Node::RED
|
109
|
+
x = x.parent.parent
|
110
|
+
else
|
111
|
+
if x == x.parent.left
|
112
|
+
x = x.parent
|
113
|
+
right_rotate(x)
|
114
|
+
end
|
115
|
+
x.parent.color = Node::BLACK
|
116
|
+
x.parent.parent.color = Node::RED
|
117
|
+
left_rotate(x.parent.parent)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
root.color = Node::BLACK
|
122
|
+
end
|
123
|
+
|
124
|
+
alias << insert
|
125
|
+
|
126
|
+
def delete(z)
|
127
|
+
y = (z.left.nil? || z.right.nil?) ? z : successor(z)
|
128
|
+
x = y.left.nil? ? y.right : y.left
|
129
|
+
x.parent = y.parent
|
130
|
+
|
131
|
+
if y.parent.nil?
|
132
|
+
self.root = x
|
133
|
+
else
|
134
|
+
if y == y.parent.left
|
135
|
+
y.parent.left = x
|
136
|
+
else
|
137
|
+
y.parent.right = x
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
z.key = y.key if y != z
|
142
|
+
|
143
|
+
if y.color == Node::BLACK
|
144
|
+
delete_fixup(x)
|
145
|
+
end
|
146
|
+
|
147
|
+
self.size -= 1
|
148
|
+
return y
|
149
|
+
end
|
150
|
+
|
151
|
+
def minimum(x = root)
|
152
|
+
while !x.left.nil?
|
153
|
+
x = x.left
|
154
|
+
end
|
155
|
+
return x
|
156
|
+
end
|
157
|
+
|
158
|
+
def maximum(x = root)
|
159
|
+
while !x.right.nil?
|
160
|
+
x = x.right
|
161
|
+
end
|
162
|
+
return x
|
163
|
+
end
|
164
|
+
|
165
|
+
def successor(x)
|
166
|
+
if !x.right.nil?
|
167
|
+
return minimum(x.right)
|
168
|
+
end
|
169
|
+
y = x.parent
|
170
|
+
while !y.nil? && x == y.right
|
171
|
+
x = y
|
172
|
+
y = y.parent
|
173
|
+
end
|
174
|
+
return y
|
175
|
+
end
|
176
|
+
|
177
|
+
def predecessor(x)
|
178
|
+
if !x.left.nil?
|
179
|
+
return maximum(x.left)
|
180
|
+
end
|
181
|
+
y = x.parent
|
182
|
+
while !y.nil? && x == y.left
|
183
|
+
x = y
|
184
|
+
y = y.parent
|
185
|
+
end
|
186
|
+
return y
|
187
|
+
end
|
188
|
+
|
189
|
+
def inorder_walk(x = root)
|
190
|
+
x = self.minimum
|
191
|
+
while !x.nil?
|
192
|
+
yield x.key
|
193
|
+
x = successor(x)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
alias each inorder_walk
|
198
|
+
|
199
|
+
def reverse_inorder_walk(x = root)
|
200
|
+
x = self.maximum
|
201
|
+
while !x.nil?
|
202
|
+
yield x.key
|
203
|
+
x = predecessor(x)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
alias reverse_each reverse_inorder_walk
|
208
|
+
|
209
|
+
def search(key, x = root)
|
210
|
+
while !x.nil? && x.key != key
|
211
|
+
key < x.key ? x = x.left : x = x.right
|
212
|
+
end
|
213
|
+
return x
|
214
|
+
end
|
215
|
+
|
216
|
+
# Search for a successor even if the key doesn't exist
|
217
|
+
def search_for_possible_successor(key, x = root)
|
218
|
+
node = search(key)
|
219
|
+
pseudo = false
|
220
|
+
if node.class == NilNode
|
221
|
+
insert(Node.new(key,nil))
|
222
|
+
node = search(key)
|
223
|
+
pseudo = true
|
224
|
+
end
|
225
|
+
|
226
|
+
succ = successor(node)
|
227
|
+
delete(node) if pseudo
|
228
|
+
return nil if !succ
|
229
|
+
return succ
|
230
|
+
end
|
231
|
+
|
232
|
+
def delete_via_key(key)
|
233
|
+
node = search(key)
|
234
|
+
return if node.class == NilNode
|
235
|
+
delete(node)
|
236
|
+
end
|
237
|
+
|
238
|
+
# Return array of matches and a boolean to denote whether truncation occurred
|
239
|
+
def search_for_range(marker,prefix,max_keys,delimiter)
|
240
|
+
succ = nil
|
241
|
+
if marker
|
242
|
+
succ = search_for_possible_successor(marker)
|
243
|
+
else
|
244
|
+
succ = self.minimum
|
245
|
+
end
|
246
|
+
|
247
|
+
return [],false if succ.class == NilNode
|
248
|
+
|
249
|
+
keys_count = 0
|
250
|
+
keys = []
|
251
|
+
is_truncated = false
|
252
|
+
|
253
|
+
loop do
|
254
|
+
if !prefix or succ.key.index(prefix) == 0
|
255
|
+
keys_count += 1
|
256
|
+
if keys_count <= max_keys
|
257
|
+
keys << succ.value
|
258
|
+
else
|
259
|
+
is_truncated = true
|
260
|
+
break
|
261
|
+
end
|
262
|
+
end
|
263
|
+
succ = successor(succ)
|
264
|
+
if succ.class == NilNode
|
265
|
+
break
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
return keys,is_truncated
|
270
|
+
end
|
271
|
+
|
272
|
+
def empty?
|
273
|
+
return self.root.nil?
|
274
|
+
end
|
275
|
+
|
276
|
+
def black_height(x = root)
|
277
|
+
height = 0
|
278
|
+
while !x.nil?
|
279
|
+
x = x.left
|
280
|
+
height +=1 if x.nil? || x.black?
|
281
|
+
end
|
282
|
+
return height
|
283
|
+
end
|
284
|
+
|
285
|
+
private
|
286
|
+
|
287
|
+
def left_rotate(x)
|
288
|
+
raise "x.right is nil!" if x.right.nil?
|
289
|
+
y = x.right
|
290
|
+
x.right = y.left
|
291
|
+
y.left.parent = x if !y.left.nil?
|
292
|
+
y.parent = x.parent
|
293
|
+
if x.parent.nil?
|
294
|
+
self.root = y
|
295
|
+
else
|
296
|
+
if x == x.parent.left
|
297
|
+
x.parent.left = y
|
298
|
+
else
|
299
|
+
x.parent.right = y
|
300
|
+
end
|
301
|
+
end
|
302
|
+
y.left = x
|
303
|
+
x.parent = y
|
304
|
+
end
|
305
|
+
|
306
|
+
def right_rotate(x)
|
307
|
+
raise "x.left is nil!" if x.left.nil?
|
308
|
+
y = x.left
|
309
|
+
x.left = y.right
|
310
|
+
y.right.parent = x if !y.right.nil?
|
311
|
+
y.parent = x.parent
|
312
|
+
if x.parent.nil?
|
313
|
+
self.root = y
|
314
|
+
else
|
315
|
+
if x == x.parent.left
|
316
|
+
x.parent.left = y
|
317
|
+
else
|
318
|
+
x.parent.right = y
|
319
|
+
end
|
320
|
+
end
|
321
|
+
y.right = x
|
322
|
+
x.parent = y
|
323
|
+
end
|
324
|
+
|
325
|
+
def insert_helper(z)
|
326
|
+
y = NilNode.instance
|
327
|
+
x = root
|
328
|
+
while !x.nil?
|
329
|
+
y = x
|
330
|
+
z.key < x.key ? x = x.left : x = x.right
|
331
|
+
end
|
332
|
+
z.parent = y
|
333
|
+
if y.nil?
|
334
|
+
self.root = z
|
335
|
+
else
|
336
|
+
z.key < y.key ? y.left = z : y.right = z
|
337
|
+
end
|
338
|
+
self.size += 1
|
339
|
+
end
|
340
|
+
|
341
|
+
def delete_fixup(x)
|
342
|
+
while x != root && x.color == Node::BLACK
|
343
|
+
if x == x.parent.left
|
344
|
+
w = x.parent.right
|
345
|
+
if w.color == Node::RED
|
346
|
+
w.color = Node::BLACK
|
347
|
+
x.parent.color = Node::RED
|
348
|
+
left_rotate(x.parent)
|
349
|
+
w = x.parent.right
|
350
|
+
end
|
351
|
+
if w.left.color == Node::BLACK && w.right.color == Node::BLACK
|
352
|
+
w.color = Node::RED
|
353
|
+
x = x.parent
|
354
|
+
else
|
355
|
+
if w.right.color == Node::BLACK
|
356
|
+
w.left.color = Node::BLACK
|
357
|
+
w.color = Node::RED
|
358
|
+
right_rotate(w)
|
359
|
+
w = x.parent.right
|
360
|
+
end
|
361
|
+
w.color = x.parent.color
|
362
|
+
x.parent.color = Node::BLACK
|
363
|
+
w.right.color = Node::BLACK
|
364
|
+
left_rotate(x.parent)
|
365
|
+
x = root
|
366
|
+
end
|
367
|
+
else
|
368
|
+
w = x.parent.left
|
369
|
+
if w.color == Node::RED
|
370
|
+
w.color = Node::BLACK
|
371
|
+
x.parent.color = Node::RED
|
372
|
+
right_rotate(x.parent)
|
373
|
+
w = x.parent.left
|
374
|
+
end
|
375
|
+
if w.right.color == Node::BLACK && w.left.color == Node::BLACK
|
376
|
+
w.color = Node::RED
|
377
|
+
x = x.parent
|
378
|
+
else
|
379
|
+
if w.left.color == Node::BLACK
|
380
|
+
w.right.color = Node::BLACK
|
381
|
+
w.color = Node::RED
|
382
|
+
left_rotate(w)
|
383
|
+
w = x.parent.left
|
384
|
+
end
|
385
|
+
w.color = x.parent.color
|
386
|
+
x.parent.color = Node::BLACK
|
387
|
+
w.left.color = Node::BLACK
|
388
|
+
right_rotate(x.parent)
|
389
|
+
x = root
|
390
|
+
end
|
391
|
+
end
|
392
|
+
end
|
393
|
+
x.color = Node::BLACK
|
394
|
+
end
|
395
|
+
end
|
data/lib/fakes3/s3_object.rb
CHANGED
data/lib/fakes3/server.rb
CHANGED
@@ -1,21 +1,27 @@
|
|
1
1
|
require 'webrick'
|
2
2
|
require 'fakes3/file_store'
|
3
3
|
require 'fakes3/xml_adapter'
|
4
|
+
require 'fakes3/bucket_query'
|
5
|
+
require 'fakes3/unsupported_operation'
|
4
6
|
|
5
7
|
module FakeS3
|
6
8
|
class Request
|
7
9
|
CREATE_BUCKET = "CREATE_BUCKET"
|
8
10
|
LIST_BUCKETS = "LIST_BUCKETS"
|
9
11
|
LS_BUCKET = "LS_BUCKET"
|
12
|
+
HEAD = "HEAD"
|
10
13
|
STORE = "STORE"
|
11
14
|
COPY = "COPY"
|
12
15
|
GET = "GET"
|
13
16
|
GET_ACL = "GET_ACL"
|
14
17
|
SET_ACL = "SET_ACL"
|
15
18
|
MOVE = "MOVE"
|
16
|
-
|
19
|
+
DELETE_OBJECT = "DELETE_OBJECT"
|
20
|
+
DELETE_BUCKET = "DELETE_BUCKET"
|
17
21
|
|
18
|
-
attr_accessor :bucket,:object,:type,:src_bucket
|
22
|
+
attr_accessor :bucket,:object,:type,:src_bucket,
|
23
|
+
:src_object,:method,:webrick_request,
|
24
|
+
:path,:is_path_style,:query,:http_verb
|
19
25
|
|
20
26
|
def inspect
|
21
27
|
puts "-----Inspect FakeS3 Request"
|
@@ -26,6 +32,7 @@ module FakeS3
|
|
26
32
|
puts "Object: #{@object}"
|
27
33
|
puts "Src Bucket: #{@src_bucket}"
|
28
34
|
puts "Src Object: #{@src_object}"
|
35
|
+
puts "Query: #{@query}"
|
29
36
|
puts "-----Done"
|
30
37
|
end
|
31
38
|
end
|
@@ -51,8 +58,15 @@ module FakeS3
|
|
51
58
|
bucket_obj = @store.get_bucket(s_req.bucket)
|
52
59
|
if bucket_obj
|
53
60
|
response.status = 200
|
54
|
-
response.body = XmlAdapter.bucket(bucket_obj)
|
55
61
|
response['Content-Type'] = "application/xml"
|
62
|
+
query = {
|
63
|
+
:marker => s_req.query["marker"] ? s_req.query["marker"].to_s : nil,
|
64
|
+
:prefix => s_req.query["prefix"] ? s_req.query["prefix"].to_s : nil,
|
65
|
+
:max_keys => s_req.query["max_keys"] ? s_req.query["max_keys"].to_s : nil,
|
66
|
+
:delimiter => s_req.query["delimiter"] ? s_req.query["delimiter"].to_s : nil
|
67
|
+
}
|
68
|
+
bq = bucket_obj.query_for_range(query)
|
69
|
+
response.body = XmlAdapter.bucket_query(bq)
|
56
70
|
else
|
57
71
|
response.status = 404
|
58
72
|
response.body = XmlAdapter.error_no_such_bucket(s_req.bucket)
|
@@ -98,19 +112,23 @@ module FakeS3
|
|
98
112
|
end
|
99
113
|
end
|
100
114
|
response['Content-Length'] = File::Stat.new(real_obj.io.path).size
|
101
|
-
|
115
|
+
if s_req.http_verb == 'HEAD'
|
116
|
+
response.body = ""
|
117
|
+
else
|
118
|
+
response.body = real_obj.io
|
119
|
+
end
|
102
120
|
end
|
103
121
|
end
|
104
122
|
|
105
123
|
def do_PUT(request,response)
|
106
124
|
s_req = normalize_request(request)
|
107
125
|
|
108
|
-
|
109
126
|
case s_req.type
|
110
127
|
when Request::COPY
|
111
128
|
@store.copy_object(s_req.src_bucket,s_req.src_object,s_req.bucket,s_req.object)
|
112
129
|
when Request::STORE
|
113
|
-
|
130
|
+
bucket_obj = @store.get_bucket(s_req.bucket)
|
131
|
+
real_obj = @store.store_object(bucket_obj,s_req.object,s_req.webrick_request)
|
114
132
|
response['Etag'] = real_obj.md5
|
115
133
|
when Request::CREATE_BUCKET
|
116
134
|
@store.create_bucket(s_req.bucket)
|
@@ -126,11 +144,47 @@ module FakeS3
|
|
126
144
|
end
|
127
145
|
|
128
146
|
def do_DELETE(request,response)
|
129
|
-
|
147
|
+
s_req = normalize_request(request)
|
148
|
+
|
149
|
+
case s_req.type
|
150
|
+
when Request::DELETE_OBJECT
|
151
|
+
bucket_obj = @store.get_bucket(s_req.bucket)
|
152
|
+
@store.delete_object(bucket_obj,s_req.object,s_req.webrick_request)
|
153
|
+
end
|
154
|
+
|
155
|
+
response.status = 204
|
156
|
+
response.body = ""
|
130
157
|
end
|
131
158
|
|
132
159
|
private
|
133
160
|
|
161
|
+
def normalize_delete(webrick_req,s_req)
|
162
|
+
path = webrick_req.path
|
163
|
+
path_len = path.size
|
164
|
+
query = webrick_req.query
|
165
|
+
if path == "/" and s_req.is_path_style
|
166
|
+
# Probably do a 404 here
|
167
|
+
else
|
168
|
+
if s_req.is_path_style
|
169
|
+
elems = path[1,path_len].split("/")
|
170
|
+
s_req.bucket = elems[0]
|
171
|
+
else
|
172
|
+
elems = path.split("/")
|
173
|
+
end
|
174
|
+
|
175
|
+
if elems.size == 0
|
176
|
+
raise UnsupportedOperation
|
177
|
+
elsif elems.size == 1
|
178
|
+
s_req.type = Request::DELETE_BUCKET
|
179
|
+
s_req.query = query
|
180
|
+
else
|
181
|
+
s_req.type = Request::DELETE_OBJECT
|
182
|
+
object = elems[1,elems.size].join('/')
|
183
|
+
s_req.object = object
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
134
188
|
def normalize_get(webrick_req,s_req)
|
135
189
|
path = webrick_req.path
|
136
190
|
path_len = path.size
|
@@ -150,6 +204,7 @@ module FakeS3
|
|
150
204
|
s_req.type = Request::LIST_BUCKETS
|
151
205
|
elsif elems.size == 1
|
152
206
|
s_req.type = Request::LS_BUCKET
|
207
|
+
s_req.query = query
|
153
208
|
else
|
154
209
|
if query["acl"] == ""
|
155
210
|
s_req.type = Request::GET_ACL
|
@@ -219,11 +274,15 @@ module FakeS3
|
|
219
274
|
s_req.is_path_style = false
|
220
275
|
end
|
221
276
|
|
277
|
+
s_req.http_verb = webrick_req.request_method
|
278
|
+
|
222
279
|
case webrick_req.request_method
|
223
280
|
when 'PUT'
|
224
281
|
normalize_put(webrick_req,s_req)
|
225
|
-
when 'GET'
|
282
|
+
when 'GET','HEAD'
|
226
283
|
normalize_get(webrick_req,s_req)
|
284
|
+
when 'DELETE'
|
285
|
+
normalize_delete(webrick_req,s_req)
|
227
286
|
else
|
228
287
|
raise "Unknown Request"
|
229
288
|
end
|
data/lib/fakes3/version.rb
CHANGED
data/lib/fakes3/xml_adapter.rb
CHANGED
@@ -28,9 +28,10 @@ module FakeS3
|
|
28
28
|
#<Error>
|
29
29
|
# <Code>NoSuchKey</Code>
|
30
30
|
# <Message>The resource you requested does not exist</Message>
|
31
|
-
# <Resource>/mybucket/myfoto.jpg</Resource>
|
31
|
+
# <Resource>/mybucket/myfoto.jpg</Resource>
|
32
32
|
# <RequestId>4442587FB7D0A2F9</RequestId>
|
33
|
-
#</Error>
|
33
|
+
#</Error>
|
34
|
+
#
|
34
35
|
def self.error_no_such_bucket(name)
|
35
36
|
output = ""
|
36
37
|
xml = Builder::XmlMarkup.new(:target => output)
|
@@ -72,6 +73,54 @@ module FakeS3
|
|
72
73
|
output
|
73
74
|
end
|
74
75
|
|
76
|
+
# A bucket query gives back the bucket along with contents
|
77
|
+
# <Contents>
|
78
|
+
#<Key>Nelson</Key>
|
79
|
+
# <LastModified>2006-01-01T12:00:00.000Z</LastModified>
|
80
|
+
# <ETag>"828ef3fdfa96f00ad9f27c383fc9ac7f"</ETag>
|
81
|
+
# <Size>5</Size>
|
82
|
+
# <StorageClass>STANDARD</StorageClass>
|
83
|
+
# <Owner>
|
84
|
+
# <ID>bcaf161ca5fb16fd081034f</ID>
|
85
|
+
# <DisplayName>webfile</DisplayName>
|
86
|
+
# </Owner>
|
87
|
+
# </Contents>
|
88
|
+
|
89
|
+
def self.append_objects_to_list_bucket_result(lbr,objects)
|
90
|
+
return if objects.nil? or objects.size == 0
|
91
|
+
|
92
|
+
objects.each do |s3_object|
|
93
|
+
lbr.Contents { |contents|
|
94
|
+
contents.Key(s3_object.name)
|
95
|
+
contents.LastModifed(s3_object.creation_date)
|
96
|
+
contents.ETag("\"#{s3_object.md5}\"")
|
97
|
+
contents.Size(s3_object.size)
|
98
|
+
contents.StorageClass("STANDARD")
|
99
|
+
|
100
|
+
contents.Owner { |owner|
|
101
|
+
owner.ID("abc")
|
102
|
+
owner.DisplayName("You")
|
103
|
+
}
|
104
|
+
}
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def self.bucket_query(bucket_query)
|
109
|
+
output = ""
|
110
|
+
bucket = bucket_query.bucket
|
111
|
+
xml = Builder::XmlMarkup.new(:target => output)
|
112
|
+
xml.instruct! :xml, :version=>"1.0", :encoding=>"UTF-8"
|
113
|
+
xml.ListBucketResult(:xmlns => "http://s3.amazonaws.com/doc/2006-03-01/") { |lbr|
|
114
|
+
lbr.Name(bucket.name)
|
115
|
+
lbr.Prefix(bucket_query.prefix)
|
116
|
+
lbr.Marker(bucket_query.marker)
|
117
|
+
lbr.MaxKeys(bucket_query.max_keys)
|
118
|
+
lbr.IsTruncated(bucket_query.is_truncated?)
|
119
|
+
append_objects_to_list_bucket_result(lbr,bucket_query.matches)
|
120
|
+
}
|
121
|
+
output
|
122
|
+
end
|
123
|
+
|
75
124
|
# ACL xml
|
76
125
|
def self.acl(object = nil)
|
77
126
|
output = ""
|
@@ -8,7 +8,7 @@ class RightAWSCommandsTest < Test::Unit::TestCase
|
|
8
8
|
def setup
|
9
9
|
@s3 = RightAws::S3Interface.new('1E3GDYEOGFJPIT7XXXXXX','hgTHt68JY07JKUY08ftHYtERkjgtfERn57XXXXXX',
|
10
10
|
{:multi_thread => false, :server => 'localhost',
|
11
|
-
:port => 10453, :protocol => 'http',:logger => Logger.new("/dev/null") })
|
11
|
+
:port => 10453, :protocol => 'http',:logger => Logger.new("/dev/null"),:no_subdomains => true })
|
12
12
|
end
|
13
13
|
|
14
14
|
def teardown
|
data/test/s3_commands_test.rb
CHANGED
@@ -15,44 +15,44 @@ class S3CommandsTest < Test::Unit::TestCase
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def test_create_bucket
|
18
|
-
bucket = Bucket.create("
|
18
|
+
bucket = Bucket.create("ruby_aws_s3")
|
19
19
|
assert_not_nil bucket
|
20
20
|
end
|
21
21
|
|
22
22
|
def test_store
|
23
|
-
bucket = Bucket.create("
|
24
|
-
S3Object.store("hello","world","
|
23
|
+
bucket = Bucket.create("ruby_aws_s3")
|
24
|
+
S3Object.store("hello","world","ruby_aws_s3")
|
25
25
|
|
26
26
|
output = ""
|
27
|
-
obj = S3Object.stream("hello","
|
27
|
+
obj = S3Object.stream("hello","ruby_aws_s3") do |chunk|
|
28
28
|
output << chunk
|
29
29
|
end
|
30
30
|
assert_equal "world", output
|
31
31
|
end
|
32
32
|
|
33
33
|
def test_large_store
|
34
|
-
bucket = Bucket.create("
|
34
|
+
bucket = Bucket.create("ruby_aws_s3")
|
35
35
|
buffer = ""
|
36
36
|
500000.times do
|
37
37
|
buffer << "#{(rand * 100).to_i}"
|
38
38
|
end
|
39
39
|
|
40
40
|
buf_len = buffer.length
|
41
|
-
S3Object.store("big",buffer,"
|
41
|
+
S3Object.store("big",buffer,"ruby_aws_s3")
|
42
42
|
|
43
43
|
output = ""
|
44
|
-
S3Object.stream("big","
|
44
|
+
S3Object.stream("big","ruby_aws_s3") do |chunk|
|
45
45
|
output << chunk
|
46
46
|
end
|
47
47
|
assert_equal buf_len,output.size
|
48
48
|
end
|
49
49
|
|
50
50
|
def test_multi_directory
|
51
|
-
bucket = Bucket.create("
|
52
|
-
S3Object.store("dir/myfile/123.txt","recursive","
|
51
|
+
bucket = Bucket.create("ruby_aws_s3")
|
52
|
+
S3Object.store("dir/myfile/123.txt","recursive","ruby_aws_s3")
|
53
53
|
|
54
54
|
output = ""
|
55
|
-
obj = S3Object.stream("dir/myfile/123.txt","
|
55
|
+
obj = S3Object.stream("dir/myfile/123.txt","ruby_aws_s3") do |chunk|
|
56
56
|
output << chunk
|
57
57
|
end
|
58
58
|
assert_equal "recursive", output
|
@@ -66,4 +66,62 @@ class S3CommandsTest < Test::Unit::TestCase
|
|
66
66
|
assert_equal AWS::S3::NoSuchBucket,$!.class
|
67
67
|
end
|
68
68
|
end
|
69
|
+
|
70
|
+
def test_find_object
|
71
|
+
bucket = Bucket.create('find_bucket')
|
72
|
+
obj_name = 'short'
|
73
|
+
S3Object.store(obj_name,'short_text','find_bucket')
|
74
|
+
short = S3Object.find(obj_name,"find_bucket")
|
75
|
+
assert_not_nil(short)
|
76
|
+
assert_equal(short.value,'short_text')
|
77
|
+
end
|
78
|
+
|
79
|
+
def test_find_non_existent_object
|
80
|
+
bucket = Bucket.create('find_bucket')
|
81
|
+
obj_name = 'doesnotexist'
|
82
|
+
assert_raise AWS::S3::NoSuchKey do
|
83
|
+
should_throw = S3Object.find(obj_name,"find_bucket")
|
84
|
+
end
|
85
|
+
|
86
|
+
# Try something higher in the alphabet
|
87
|
+
assert_raise AWS::S3::NoSuchKey do
|
88
|
+
should_throw = S3Object.find("zzz","find_bucket")
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def test_exists?
|
93
|
+
bucket = Bucket.create('ruby_aws_s3')
|
94
|
+
obj_name = 'dir/myfile/exists.txt'
|
95
|
+
S3Object.store(obj_name,'exists','ruby_aws_s3')
|
96
|
+
assert S3Object.exists?(obj_name, 'ruby_aws_s3')
|
97
|
+
assert !S3Object.exists?('dir/myfile/doesnotexist.txt','ruby_aws_s3')
|
98
|
+
end
|
99
|
+
|
100
|
+
def test_delete
|
101
|
+
bucket = Bucket.create("ruby_aws_s3")
|
102
|
+
S3Object.store("something_to_delete","asdf","ruby_aws_s3")
|
103
|
+
something = S3Object.find("something_to_delete","ruby_aws_s3")
|
104
|
+
S3Object.delete("something_to_delete","ruby_aws_s3")
|
105
|
+
end
|
106
|
+
|
107
|
+
def test_larger_lists
|
108
|
+
Bucket.create("ruby_aws_s3_many")
|
109
|
+
(0..100).each do |i|
|
110
|
+
('a'..'z').each do |letter|
|
111
|
+
name = "#{letter}#{i}"
|
112
|
+
S3Object.store(name,"asdf","ruby_aws_s3_many")
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
bucket = Bucket.find("ruby_aws_s3_many")
|
117
|
+
assert_equal(bucket.objects.first.key,"a0")
|
118
|
+
assert_equal(bucket.size,1000)
|
119
|
+
end
|
120
|
+
|
121
|
+
# Copying an object
|
122
|
+
#S3Object.copy 'headshot.jpg', 'headshot2.jpg', 'photos'
|
123
|
+
|
124
|
+
# Renaming an object
|
125
|
+
#S3Object.rename 'headshot.jpg', 'portrait.jpg', 'photos'
|
126
|
+
|
69
127
|
end
|
metadata
CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
|
|
5
5
|
segments:
|
6
6
|
- 0
|
7
7
|
- 1
|
8
|
-
-
|
9
|
-
version: 0.1.
|
8
|
+
- 1
|
9
|
+
version: 0.1.1
|
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-04-
|
17
|
+
date: 2012-04-16 00:00:00 -06:00
|
18
18
|
default_executable:
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
@@ -103,11 +103,14 @@ files:
|
|
103
103
|
- fakes3.gemspec
|
104
104
|
- lib/fakes3.rb
|
105
105
|
- lib/fakes3/bucket.rb
|
106
|
+
- lib/fakes3/bucket_query.rb
|
106
107
|
- lib/fakes3/cli.rb
|
107
108
|
- lib/fakes3/file_store.rb
|
108
109
|
- lib/fakes3/rate_limitable_file.rb
|
110
|
+
- lib/fakes3/red_black_tree.rb
|
109
111
|
- lib/fakes3/s3_object.rb
|
110
112
|
- lib/fakes3/server.rb
|
113
|
+
- lib/fakes3/unsupported_operation.rb
|
111
114
|
- lib/fakes3/version.rb
|
112
115
|
- lib/fakes3/xml_adapter.rb
|
113
116
|
- test/local_s3_cfg
|