fakes3 0.2.0 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +7 -1
- data/lib/fakes3/file_store.rb +61 -23
- data/lib/fakes3/server.rb +138 -37
- data/lib/fakes3/version.rb +1 -1
- data/lib/fakes3/xml_adapter.rb +19 -0
- data/test/aws_sdk_commands_test.rb +8 -0
- metadata +2 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 35ac3fdceddeef49ddeb95137f2dbfc8a719a4e2
|
4
|
+
data.tar.gz: c88fa58f1a7b52b9fcbb1dae974175bed5c1c178
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d9a5a1c68ee5187593a2bb3aedec7e888b6f400b6c0882a1ebb8cb4323d81ed7d29c53657833da2564fea09bb3e9732eb34f6f78cfa63e2274910ae2f128c8d8
|
7
|
+
data.tar.gz: 21cd19d83eed82e5b89511ad0a7b60e934c516f97295ce54b09135240f36c0523dd2927bf68765cb6ba219233c59595c0df17dbb425db84f562c215a8c3bf913
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
fakes3 (0.2.
|
4
|
+
fakes3 (0.2.1)
|
5
5
|
builder
|
6
6
|
thor
|
7
7
|
|
@@ -16,12 +16,17 @@ GEM
|
|
16
16
|
json (~> 1.4)
|
17
17
|
nokogiri (>= 1.4.4)
|
18
18
|
builder (3.2.2)
|
19
|
+
byebug (4.0.1)
|
20
|
+
columnize (= 0.9.0)
|
21
|
+
rb-readline (= 0.5.2)
|
22
|
+
columnize (0.9.0)
|
19
23
|
json (1.8.1)
|
20
24
|
mime-types (1.25)
|
21
25
|
mini_portile (0.6.1)
|
22
26
|
nokogiri (1.6.4.1)
|
23
27
|
mini_portile (~> 0.6.0)
|
24
28
|
rake (10.1.0)
|
29
|
+
rb-readline (0.5.2)
|
25
30
|
rest-client (1.6.7)
|
26
31
|
mime-types (>= 1.16)
|
27
32
|
right_aws (3.1.0)
|
@@ -37,6 +42,7 @@ DEPENDENCIES
|
|
37
42
|
aws-s3
|
38
43
|
aws-sdk-v1
|
39
44
|
bundler (>= 1.0.0)
|
45
|
+
byebug
|
40
46
|
fakes3!
|
41
47
|
rake
|
42
48
|
rest-client
|
data/lib/fakes3/file_store.rb
CHANGED
@@ -103,7 +103,7 @@ module FakeS3
|
|
103
103
|
def object_metadata(bucket,object)
|
104
104
|
end
|
105
105
|
|
106
|
-
def copy_object(src_bucket_name,src_name,dst_bucket_name,dst_name,request)
|
106
|
+
def copy_object(src_bucket_name, src_name, dst_bucket_name, dst_name, request)
|
107
107
|
src_root = File.join(@root,src_bucket_name,src_name,SHUCK_METADATA_DIR)
|
108
108
|
src_metadata_filename = File.join(src_root,"metadata")
|
109
109
|
src_metadata = YAML.load(File.open(src_metadata_filename,'rb').read)
|
@@ -140,8 +140,8 @@ module FakeS3
|
|
140
140
|
end
|
141
141
|
end
|
142
142
|
|
143
|
-
src_bucket =
|
144
|
-
dst_bucket =
|
143
|
+
src_bucket = get_bucket(src_bucket_name) || create_bucket(src_bucket_name)
|
144
|
+
dst_bucket = get_bucket(dst_bucket_name) || create_bucket(dst_bucket_name)
|
145
145
|
|
146
146
|
obj = S3Object.new
|
147
147
|
obj.name = dst_name
|
@@ -155,7 +155,31 @@ module FakeS3
|
|
155
155
|
return obj
|
156
156
|
end
|
157
157
|
|
158
|
-
def store_object(bucket,object_name,request)
|
158
|
+
def store_object(bucket, object_name, request)
|
159
|
+
filedata = ""
|
160
|
+
|
161
|
+
# TODO put a tmpfile here first and mv it over at the end
|
162
|
+
content_type = request.content_type || ""
|
163
|
+
|
164
|
+
match = content_type.match(/^multipart\/form-data; boundary=(.+)/)
|
165
|
+
boundary = match[1] if match
|
166
|
+
if boundary
|
167
|
+
boundary = WEBrick::HTTPUtils::dequote(boundary)
|
168
|
+
form_data = WEBrick::HTTPUtils::parse_form_data(request.body, boundary)
|
169
|
+
|
170
|
+
if form_data['file'] == nil or form_data['file'] == ""
|
171
|
+
raise WEBrick::HTTPStatus::BadRequest
|
172
|
+
end
|
173
|
+
|
174
|
+
filedata = form_data['file']
|
175
|
+
else
|
176
|
+
request.body { |chunk| filedata << chunk }
|
177
|
+
end
|
178
|
+
|
179
|
+
do_store_object(bucket, object_name, filedata, request)
|
180
|
+
end
|
181
|
+
|
182
|
+
def do_store_object(bucket, object_name, filedata, request)
|
159
183
|
begin
|
160
184
|
filename = File.join(@root,bucket.name,object_name)
|
161
185
|
FileUtils.mkdir_p(filename)
|
@@ -163,29 +187,12 @@ module FakeS3
|
|
163
187
|
metadata_dir = File.join(filename,SHUCK_METADATA_DIR)
|
164
188
|
FileUtils.mkdir_p(metadata_dir)
|
165
189
|
|
166
|
-
content
|
190
|
+
content = File.join(filename,SHUCK_METADATA_DIR,"content")
|
167
191
|
metadata = File.join(filename,SHUCK_METADATA_DIR,"metadata")
|
168
192
|
|
169
|
-
|
193
|
+
File.open(content,'wb') { |f| f << filedata }
|
170
194
|
|
171
|
-
match=request.content_type.match(/^multipart\/form-data; boundary=(.+)/)
|
172
|
-
boundary = match[1] if match
|
173
|
-
if boundary
|
174
|
-
boundary = WEBrick::HTTPUtils::dequote(boundary)
|
175
|
-
filedata = WEBrick::HTTPUtils::parse_form_data(request.body, boundary)
|
176
|
-
raise HTTPStatus::BadRequest if filedata['file'].empty?
|
177
|
-
File.open(content, 'wb') do |f|
|
178
|
-
f << filedata['file']
|
179
|
-
end
|
180
|
-
else
|
181
|
-
File.open(content,'wb') do |f|
|
182
|
-
request.body do |chunk|
|
183
|
-
f << chunk
|
184
|
-
end
|
185
|
-
end
|
186
|
-
end
|
187
195
|
metadata_struct = create_metadata(content,request)
|
188
|
-
|
189
196
|
File.open(metadata,'w') do |f|
|
190
197
|
f << YAML::dump(metadata_struct)
|
191
198
|
end
|
@@ -206,6 +213,36 @@ module FakeS3
|
|
206
213
|
end
|
207
214
|
end
|
208
215
|
|
216
|
+
def combine_object_parts(bucket, upload_id, object_name, parts, request)
|
217
|
+
upload_path = File.join(@root, bucket.name)
|
218
|
+
base_path = File.join(upload_path, "#{upload_id}_#{object_name}")
|
219
|
+
|
220
|
+
complete_file = ""
|
221
|
+
chunk = ""
|
222
|
+
part_paths = []
|
223
|
+
|
224
|
+
parts.sort_by { |part| part[:number] }.each do |part|
|
225
|
+
part_path = "#{base_path}_part#{part[:number]}"
|
226
|
+
content_path = File.join(part_path, SHUCK_METADATA_DIR, 'content')
|
227
|
+
|
228
|
+
File.open(content_path, 'rb') { |f| chunk = f.read }
|
229
|
+
etag = Digest::MD5.hexdigest(chunk)
|
230
|
+
|
231
|
+
raise new Error "invalid file chunk" unless part[:etag] == etag
|
232
|
+
complete_file << chunk
|
233
|
+
part_paths << part_path
|
234
|
+
end
|
235
|
+
|
236
|
+
object = do_store_object(bucket, object_name, complete_file, request)
|
237
|
+
|
238
|
+
# clean up parts
|
239
|
+
part_paths.each do |path|
|
240
|
+
FileUtils.remove_dir(path)
|
241
|
+
end
|
242
|
+
|
243
|
+
object
|
244
|
+
end
|
245
|
+
|
209
246
|
def delete_object(bucket,object_name,request)
|
210
247
|
begin
|
211
248
|
filename = File.join(@root,bucket.name,object_name)
|
@@ -219,6 +256,7 @@ module FakeS3
|
|
219
256
|
end
|
220
257
|
end
|
221
258
|
|
259
|
+
# TODO: abstract getting meta data from request.
|
222
260
|
def create_metadata(content,request)
|
223
261
|
metadata = {}
|
224
262
|
metadata[:md5] = Digest::MD5.file(content).hexdigest
|
data/lib/fakes3/server.rb
CHANGED
@@ -2,6 +2,8 @@ require 'time'
|
|
2
2
|
require 'webrick'
|
3
3
|
require 'webrick/https'
|
4
4
|
require 'openssl'
|
5
|
+
require 'securerandom'
|
6
|
+
require 'cgi'
|
5
7
|
require 'fakes3/file_store'
|
6
8
|
require 'fakes3/xml_adapter'
|
7
9
|
require 'fakes3/bucket_query'
|
@@ -109,7 +111,7 @@ module FakeS3
|
|
109
111
|
if time >= Time.iso8601(real_obj.modified_date)
|
110
112
|
response.status = 304
|
111
113
|
return
|
112
|
-
end
|
114
|
+
end
|
113
115
|
end
|
114
116
|
|
115
117
|
response.status = 200
|
@@ -160,6 +162,9 @@ module FakeS3
|
|
160
162
|
|
161
163
|
def do_PUT(request,response)
|
162
164
|
s_req = normalize_request(request)
|
165
|
+
query = CGI::parse(request.request_uri.query || "")
|
166
|
+
|
167
|
+
return do_multipartPUT(request, response) if query['uploadId'].first
|
163
168
|
|
164
169
|
response.status = 200
|
165
170
|
response.body = ""
|
@@ -184,44 +189,114 @@ module FakeS3
|
|
184
189
|
end
|
185
190
|
end
|
186
191
|
|
187
|
-
def
|
188
|
-
|
189
|
-
|
190
|
-
|
192
|
+
def do_multipartPUT(request, response)
|
193
|
+
s_req = normalize_request(request)
|
194
|
+
query = CGI::parse(request.request_uri.query)
|
195
|
+
|
196
|
+
part_number = query['partNumber'].first
|
197
|
+
upload_id = query['uploadId'].first
|
198
|
+
part_name = "#{upload_id}_#{s_req.object}_part#{part_number}"
|
199
|
+
|
200
|
+
# store the part
|
201
|
+
if s_req.type == Request::COPY
|
202
|
+
real_obj = @store.copy_object(
|
203
|
+
s_req.src_bucket, s_req.src_object,
|
204
|
+
s_req.bucket , part_name,
|
205
|
+
request
|
206
|
+
)
|
207
|
+
|
208
|
+
response['Content-Type'] = "text/xml"
|
209
|
+
response.body = XmlAdapter.copy_object_result real_obj
|
210
|
+
else
|
211
|
+
bucket_obj = @store.get_bucket(s_req.bucket)
|
212
|
+
if !bucket_obj
|
213
|
+
bucket_obj = @store.create_bucket(s_req.bucket)
|
214
|
+
end
|
215
|
+
real_obj = @store.store_object(
|
216
|
+
bucket_obj, part_name,
|
217
|
+
request
|
218
|
+
)
|
219
|
+
|
220
|
+
response.body = ""
|
221
|
+
response.header['ETag'] = "\"#{real_obj.md5}\""
|
191
222
|
end
|
223
|
+
|
224
|
+
response['Access-Control-Allow-Origin'] = '*'
|
225
|
+
response['Access-Control-Allow-Headers'] = 'Authorization, Content-Length'
|
226
|
+
response['Access-Control-Expose-Headers'] = 'ETag'
|
227
|
+
|
228
|
+
response.status = 200
|
229
|
+
end
|
230
|
+
|
231
|
+
def do_POST(request,response)
|
192
232
|
s_req = normalize_request(request)
|
193
|
-
key=request.query['key']
|
194
|
-
|
195
|
-
|
233
|
+
key = request.query['key']
|
234
|
+
query = CGI::parse(request.request_uri.query || "")
|
235
|
+
|
236
|
+
if query.has_key?('uploads')
|
237
|
+
upload_id = SecureRandom.hex
|
238
|
+
|
239
|
+
response.body = <<-eos.strip
|
240
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
241
|
+
<InitiateMultipartUploadResult>
|
242
|
+
<Bucket>#{ s_req.bucket }</Bucket>
|
243
|
+
<Key>#{ key }</Key>
|
244
|
+
<UploadId>#{ upload_id }</UploadId>
|
245
|
+
</InitiateMultipartUploadResult>
|
246
|
+
eos
|
247
|
+
elsif query.has_key?('uploadId')
|
248
|
+
upload_id = query['uploadId'].first
|
249
|
+
bucket_obj = @store.get_bucket(s_req.bucket)
|
250
|
+
real_obj = @store.combine_object_parts(
|
251
|
+
bucket_obj,
|
252
|
+
upload_id,
|
253
|
+
s_req.object,
|
254
|
+
parse_complete_multipart_upload(request),
|
255
|
+
request
|
256
|
+
)
|
196
257
|
|
197
|
-
|
198
|
-
|
199
|
-
|
258
|
+
response.body = XmlAdapter.complete_multipart_result real_obj
|
259
|
+
elsif request.content_type =~ /^multipart\/form-data; boundary=(.+)/
|
260
|
+
key=request.query['key']
|
200
261
|
|
201
|
-
|
202
|
-
|
262
|
+
success_action_redirect = request.query['success_action_redirect']
|
263
|
+
success_action_status = request.query['success_action_status']
|
203
264
|
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
265
|
+
filename = 'default'
|
266
|
+
filename = $1 if request.body =~ /filename="(.*)"/
|
267
|
+
key = key.gsub('${filename}', filename)
|
268
|
+
|
269
|
+
bucket_obj = @store.get_bucket(s_req.bucket) || @store.create_bucket(s_req.bucket)
|
270
|
+
real_obj = @store.store_object(bucket_obj, key, s_req.webrick_request)
|
271
|
+
|
272
|
+
response['Etag'] = "\"#{real_obj.md5}\""
|
273
|
+
|
274
|
+
if success_action_redirect
|
275
|
+
response.status = 307
|
276
|
+
response.body = ""
|
277
|
+
response['Location'] = success_action_redirect
|
278
|
+
else
|
279
|
+
response.status = success_action_status || 204
|
280
|
+
if response.status == "201"
|
281
|
+
response.body = <<-eos.strip
|
282
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
283
|
+
<PostResponse>
|
284
|
+
<Location>http://#{s_req.bucket}.localhost:#{@port}/#{key}</Location>
|
285
|
+
<Bucket>#{s_req.bucket}</Bucket>
|
286
|
+
<Key>#{key}</Key>
|
287
|
+
<ETag>#{response['Etag']}</ETag>
|
288
|
+
</PostResponse>
|
289
|
+
eos
|
290
|
+
end
|
221
291
|
end
|
292
|
+
else
|
293
|
+
raise WEBrick::HTTPStatus::BadRequest
|
222
294
|
end
|
223
|
-
|
224
|
-
response['
|
295
|
+
|
296
|
+
response['Content-Type'] = 'text/xml'
|
297
|
+
response['Access-Control-Allow-Origin'] = '*'
|
298
|
+
response['Access-Control-Allow-Headers'] = 'Authorization, Content-Length'
|
299
|
+
response['Access-Control-Expose-Headers'] = 'ETag'
|
225
300
|
end
|
226
301
|
|
227
302
|
def do_DELETE(request,response)
|
@@ -241,10 +316,11 @@ module FakeS3
|
|
241
316
|
|
242
317
|
def do_OPTIONS(request, response)
|
243
318
|
super
|
244
|
-
|
245
|
-
response[
|
246
|
-
response[
|
247
|
-
response[
|
319
|
+
|
320
|
+
response['Access-Control-Allow-Origin'] = '*'
|
321
|
+
response['Access-Control-Allow-Methods'] = 'PUT, POST, HEAD, GET, OPTIONS'
|
322
|
+
response['Access-Control-Allow-Headers'] = 'Accept, Content-Type, Authorization, Content-Length, ETag'
|
323
|
+
response['Access-Control-Expose-Headers'] = 'ETag'
|
248
324
|
end
|
249
325
|
|
250
326
|
private
|
@@ -336,9 +412,11 @@ module FakeS3
|
|
336
412
|
end
|
337
413
|
end
|
338
414
|
|
415
|
+
# TODO: also parse the x-amz-copy-source-range:bytes=first-last header
|
416
|
+
# for multipart copy
|
339
417
|
copy_source = webrick_req.header["x-amz-copy-source"]
|
340
418
|
if copy_source and copy_source.size == 1
|
341
|
-
src_elems
|
419
|
+
src_elems = copy_source.first.split("/")
|
342
420
|
root_offset = src_elems[0] == "" ? 1 : 0
|
343
421
|
s_req.src_bucket = src_elems[root_offset]
|
344
422
|
s_req.src_object = src_elems[1 + root_offset,src_elems.size].join("/")
|
@@ -355,6 +433,14 @@ module FakeS3
|
|
355
433
|
s_req.path = webrick_req.query['key']
|
356
434
|
|
357
435
|
s_req.webrick_request = webrick_req
|
436
|
+
|
437
|
+
if s_req.is_path_style
|
438
|
+
elems = path[1,path_len].split("/")
|
439
|
+
s_req.bucket = elems[0]
|
440
|
+
s_req.object = elems[1..-1].join('/') if elems.size >= 2
|
441
|
+
else
|
442
|
+
s_req.object = path[1..-1]
|
443
|
+
end
|
358
444
|
end
|
359
445
|
|
360
446
|
# This method takes a webrick request and generates a normalized FakeS3 request
|
@@ -391,6 +477,21 @@ module FakeS3
|
|
391
477
|
return s_req
|
392
478
|
end
|
393
479
|
|
480
|
+
def parse_complete_multipart_upload request
|
481
|
+
parts_xml = ""
|
482
|
+
request.body { |chunk| parts_xml << chunk }
|
483
|
+
|
484
|
+
# TODO: I suck at parsing xml
|
485
|
+
parts_xml = parts_xml.scan /\<Part\>.*?<\/Part\>/m
|
486
|
+
|
487
|
+
parts_xml.collect do |xml|
|
488
|
+
{
|
489
|
+
number: xml[/\<PartNumber\>(\d+)\<\/PartNumber\>/, 1].to_i,
|
490
|
+
etag: xml[/\<ETag\>\"(.+)\"\<\/ETag\>/, 1]
|
491
|
+
}
|
492
|
+
end
|
493
|
+
end
|
494
|
+
|
394
495
|
def dump_request(request)
|
395
496
|
puts "----------Dump Request-------------"
|
396
497
|
puts request.request_method
|
data/lib/fakes3/version.rb
CHANGED
data/lib/fakes3/xml_adapter.rb
CHANGED
@@ -199,5 +199,24 @@ module FakeS3
|
|
199
199
|
}
|
200
200
|
output
|
201
201
|
end
|
202
|
+
|
203
|
+
# <CompleteMultipartUploadResult>
|
204
|
+
# <Location>http://Example-Bucket.s3.amazonaws.com/Example-Object</Location>
|
205
|
+
# <Bucket>Example-Bucket</Bucket>
|
206
|
+
# <Key>Example-Object</Key>
|
207
|
+
# <ETag>"3858f62230ac3c915f300c664312c11f-9"</ETag>
|
208
|
+
# </CompleteMultipartUploadResult>
|
209
|
+
def self.complete_multipart_result(object)
|
210
|
+
output = ""
|
211
|
+
xml = Builder::XmlMarkup.new(:target => output)
|
212
|
+
xml.instruct! :xml, :version=>"1.0", :encoding=>"UTF-8"
|
213
|
+
xml.CompleteMultipartUploadResult { |result|
|
214
|
+
result.Location("TODO: implement")
|
215
|
+
result.Bucket("TODO: implement")
|
216
|
+
result.Key(object.name)
|
217
|
+
result.ETag("\"#{object.md5}\"")
|
218
|
+
}
|
219
|
+
output
|
220
|
+
end
|
202
221
|
end
|
203
222
|
end
|
@@ -20,4 +20,12 @@ class AwsSdkCommandsTest < Test::Unit::TestCase
|
|
20
20
|
|
21
21
|
assert_equal 2, bucket.objects.count
|
22
22
|
end
|
23
|
+
|
24
|
+
def test_multipart_upload
|
25
|
+
bucket = @s3.buckets["test_multipart_upload"]
|
26
|
+
object = bucket.objects["key1"]
|
27
|
+
object.write("thisisaverybigfile", :multipart_threshold => 5)
|
28
|
+
assert object.exists?
|
29
|
+
assert_equal "thisisaverybigfile", object.read
|
30
|
+
end
|
23
31
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: fakes3
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Curtis Spencer
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-03-
|
11
|
+
date: 2015-03-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -196,4 +196,3 @@ test_files:
|
|
196
196
|
- test/s3_commands_test.rb
|
197
197
|
- test/s3cmd_test.rb
|
198
198
|
- test/test_helper.rb
|
199
|
-
has_rdoc:
|