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