lib/fakes3/server.rb in fakes3-0.2.0 vs lib/fakes3/server.rb in fakes3-0.2.1
- old
+ new
@@ -1,9 +1,11 @@
require 'time'
require 'webrick'
require 'webrick/https'
require 'openssl'
+require 'securerandom'
+require 'cgi'
require 'fakes3/file_store'
require 'fakes3/xml_adapter'
require 'fakes3/bucket_query'
require 'fakes3/unsupported_operation'
require 'fakes3/errors'
@@ -107,11 +109,11 @@
if if_modified_since
time = Time.httpdate(if_modified_since)
if time >= Time.iso8601(real_obj.modified_date)
response.status = 304
return
- end
+ end
end
response.status = 200
response['Content-Type'] = real_obj.content_type
stat = File::Stat.new(real_obj.io.path)
@@ -158,11 +160,14 @@
end
end
def do_PUT(request,response)
s_req = normalize_request(request)
+ query = CGI::parse(request.request_uri.query || "")
+ return do_multipartPUT(request, response) if query['uploadId'].first
+
response.status = 200
response.body = ""
response['Content-Type'] = "text/xml"
response['Access-Control-Allow-Origin'] = '*'
@@ -182,50 +187,120 @@
when Request::CREATE_BUCKET
@store.create_bucket(s_req.bucket)
end
end
- def do_POST(request,response)
- # check that we've received file data
- unless request.content_type =~ /^multipart\/form-data; boundary=(.+)/
- raise WEBrick::HTTPStatus::BadRequest
- end
+ def do_multipartPUT(request, response)
s_req = normalize_request(request)
- key=request.query['key']
- success_action_redirect=request.query['success_action_redirect']
- success_action_status=request.query['success_action_status']
+ query = CGI::parse(request.request_uri.query)
- filename = 'default'
- filename = $1 if request.body =~ /filename="(.*)"/
- key=key.gsub('${filename}', filename)
+ part_number = query['partNumber'].first
+ upload_id = query['uploadId'].first
+ part_name = "#{upload_id}_#{s_req.object}_part#{part_number}"
- bucket_obj = @store.get_bucket(s_req.bucket) || @store.create_bucket(s_req.bucket)
- real_obj=@store.store_object(bucket_obj, key, s_req.webrick_request)
+ # store the part
+ if s_req.type == Request::COPY
+ real_obj = @store.copy_object(
+ s_req.src_bucket, s_req.src_object,
+ s_req.bucket , part_name,
+ request
+ )
- response['Etag'] = "\"#{real_obj.md5}\""
- response.body = ""
- if success_action_redirect
- response.status = 307
- response['Location']=success_action_redirect
+ response['Content-Type'] = "text/xml"
+ response.body = XmlAdapter.copy_object_result real_obj
else
- response.status = success_action_status || 204
- if response.status=="201"
- response.body= <<-eos.strip
- <?xml version="1.0" encoding="UTF-8"?>
- <PostResponse>
- <Location>http://#{s_req.bucket}.localhost:#{@port}/#{key}</Location>
- <Bucket>#{s_req.bucket}</Bucket>
- <Key>#{key}</Key>
- <ETag>#{response['Etag']}</ETag>
- </PostResponse>
- eos
+ bucket_obj = @store.get_bucket(s_req.bucket)
+ if !bucket_obj
+ bucket_obj = @store.create_bucket(s_req.bucket)
end
+ real_obj = @store.store_object(
+ bucket_obj, part_name,
+ request
+ )
+
+ response.body = ""
+ response.header['ETag'] = "\"#{real_obj.md5}\""
end
- response['Content-Type'] = 'text/xml'
- response['Access-Control-Allow-Origin'] = '*'
+
+ response['Access-Control-Allow-Origin'] = '*'
+ response['Access-Control-Allow-Headers'] = 'Authorization, Content-Length'
+ response['Access-Control-Expose-Headers'] = 'ETag'
+
+ response.status = 200
end
+ def do_POST(request,response)
+ s_req = normalize_request(request)
+ key = request.query['key']
+ query = CGI::parse(request.request_uri.query || "")
+
+ if query.has_key?('uploads')
+ upload_id = SecureRandom.hex
+
+ response.body = <<-eos.strip
+ <?xml version="1.0" encoding="UTF-8"?>
+ <InitiateMultipartUploadResult>
+ <Bucket>#{ s_req.bucket }</Bucket>
+ <Key>#{ key }</Key>
+ <UploadId>#{ upload_id }</UploadId>
+ </InitiateMultipartUploadResult>
+ eos
+ elsif query.has_key?('uploadId')
+ upload_id = query['uploadId'].first
+ bucket_obj = @store.get_bucket(s_req.bucket)
+ real_obj = @store.combine_object_parts(
+ bucket_obj,
+ upload_id,
+ s_req.object,
+ parse_complete_multipart_upload(request),
+ request
+ )
+
+ response.body = XmlAdapter.complete_multipart_result real_obj
+ elsif request.content_type =~ /^multipart\/form-data; boundary=(.+)/
+ key=request.query['key']
+
+ success_action_redirect = request.query['success_action_redirect']
+ success_action_status = request.query['success_action_status']
+
+ filename = 'default'
+ filename = $1 if request.body =~ /filename="(.*)"/
+ key = key.gsub('${filename}', filename)
+
+ bucket_obj = @store.get_bucket(s_req.bucket) || @store.create_bucket(s_req.bucket)
+ real_obj = @store.store_object(bucket_obj, key, s_req.webrick_request)
+
+ response['Etag'] = "\"#{real_obj.md5}\""
+
+ if success_action_redirect
+ response.status = 307
+ response.body = ""
+ response['Location'] = success_action_redirect
+ else
+ response.status = success_action_status || 204
+ if response.status == "201"
+ response.body = <<-eos.strip
+ <?xml version="1.0" encoding="UTF-8"?>
+ <PostResponse>
+ <Location>http://#{s_req.bucket}.localhost:#{@port}/#{key}</Location>
+ <Bucket>#{s_req.bucket}</Bucket>
+ <Key>#{key}</Key>
+ <ETag>#{response['Etag']}</ETag>
+ </PostResponse>
+ eos
+ end
+ end
+ else
+ raise WEBrick::HTTPStatus::BadRequest
+ end
+
+ response['Content-Type'] = 'text/xml'
+ response['Access-Control-Allow-Origin'] = '*'
+ response['Access-Control-Allow-Headers'] = 'Authorization, Content-Length'
+ response['Access-Control-Expose-Headers'] = 'ETag'
+ end
+
def do_DELETE(request,response)
s_req = normalize_request(request)
case s_req.type
when Request::DELETE_OBJECT
@@ -239,14 +314,15 @@
response.body = ""
end
def do_OPTIONS(request, response)
super
- response["Access-Control-Allow-Origin"] = "*"
- response["Access-Control-Allow-Methods"] = "HEAD, GET, PUT, POST"
- response["Access-Control-Allow-Headers"] = "accept, content-type"
- response["Access-Control-Expose-Headers"] = "ETag, x-amz-meta-custom-header"
+
+ response['Access-Control-Allow-Origin'] = '*'
+ response['Access-Control-Allow-Methods'] = 'PUT, POST, HEAD, GET, OPTIONS'
+ response['Access-Control-Allow-Headers'] = 'Accept, Content-Type, Authorization, Content-Length, ETag'
+ response['Access-Control-Expose-Headers'] = 'ETag'
end
private
def normalize_delete(webrick_req,s_req)
@@ -334,13 +410,15 @@
end
s_req.object = webrick_req.path[1..-1]
end
end
+ # TODO: also parse the x-amz-copy-source-range:bytes=first-last header
+ # for multipart copy
copy_source = webrick_req.header["x-amz-copy-source"]
if copy_source and copy_source.size == 1
- src_elems = copy_source.first.split("/")
+ src_elems = copy_source.first.split("/")
root_offset = src_elems[0] == "" ? 1 : 0
s_req.src_bucket = src_elems[root_offset]
s_req.src_object = src_elems[1 + root_offset,src_elems.size].join("/")
s_req.type = Request::COPY
end
@@ -353,10 +431,18 @@
path_len = path.size
s_req.path = webrick_req.query['key']
s_req.webrick_request = webrick_req
+
+ if s_req.is_path_style
+ elems = path[1,path_len].split("/")
+ s_req.bucket = elems[0]
+ s_req.object = elems[1..-1].join('/') if elems.size >= 2
+ else
+ s_req.object = path[1..-1]
+ end
end
# This method takes a webrick request and generates a normalized FakeS3 request
def normalize_request(webrick_req)
host_header= webrick_req["Host"]
@@ -387,9 +473,24 @@
end
validate_request(s_req)
return s_req
+ end
+
+ def parse_complete_multipart_upload request
+ parts_xml = ""
+ request.body { |chunk| parts_xml << chunk }
+
+ # TODO: I suck at parsing xml
+ parts_xml = parts_xml.scan /\<Part\>.*?<\/Part\>/m
+
+ parts_xml.collect do |xml|
+ {
+ number: xml[/\<PartNumber\>(\d+)\<\/PartNumber\>/, 1].to_i,
+ etag: xml[/\<ETag\>\"(.+)\"\<\/ETag\>/, 1]
+ }
+ end
end
def dump_request(request)
puts "----------Dump Request-------------"
puts request.request_method