fakes3 0.1.0 → 0.1.1
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/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
|