fakes3testing2 1.2.0

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.
@@ -0,0 +1,320 @@
1
+ require 'fileutils'
2
+ require 'time'
3
+ require 'fakes3/s3_object'
4
+ require 'fakes3/bucket'
5
+ require 'fakes3/rate_limitable_file'
6
+ require 'digest/md5'
7
+ require 'yaml'
8
+
9
+ module FakeS3
10
+ class FileStore
11
+ FAKE_S3_METADATA_DIR = ".fakes3_metadataFFF"
12
+
13
+ # S3 clients with overly strict date parsing fails to parse ISO 8601 dates
14
+ # without any sub second precision (e.g. jets3t v0.7.2), and the examples
15
+ # given in the official AWS S3 documentation specify three (3) decimals for
16
+ # sub second precision.
17
+ SUBSECOND_PRECISION = 3
18
+
19
+ def initialize(root, quiet_mode)
20
+ @root = root
21
+ @buckets = []
22
+ @bucket_hash = {}
23
+ @quiet_mode = quiet_mode
24
+ Dir[File.join(root,"*")].each do |bucket|
25
+ bucket_name = File.basename(bucket)
26
+ bucket_obj = Bucket.new(bucket_name,Time.now,[])
27
+ @buckets << bucket_obj
28
+ @bucket_hash[bucket_name] = bucket_obj
29
+ end
30
+ end
31
+
32
+ # Pass a rate limit in bytes per second
33
+ def rate_limit=(rate_limit)
34
+ if rate_limit.is_a?(String)
35
+ if rate_limit =~ /^(\d+)$/
36
+ RateLimitableFile.rate_limit = rate_limit.to_i
37
+ elsif rate_limit =~ /^(.*)K$/
38
+ RateLimitableFile.rate_limit = $1.to_f * 1000
39
+ elsif rate_limit =~ /^(.*)M$/
40
+ RateLimitableFile.rate_limit = $1.to_f * 1000000
41
+ elsif rate_limit =~ /^(.*)G$/
42
+ RateLimitableFile.rate_limit = $1.to_f * 1000000000
43
+ else
44
+ raise "Invalid Rate Limit Format: Valid values include (1000,10K,1.1M)"
45
+ end
46
+ else
47
+ RateLimitableFile.rate_limit = nil
48
+ end
49
+ end
50
+
51
+ def buckets
52
+ @buckets
53
+ end
54
+
55
+ def get_bucket_folder(bucket)
56
+ File.join(@root, bucket.name)
57
+ end
58
+
59
+ def get_bucket(bucket)
60
+ @bucket_hash[bucket]
61
+ end
62
+
63
+ def create_bucket(bucket)
64
+ FileUtils.mkdir_p(File.join(@root, bucket))
65
+ bucket_obj = Bucket.new(bucket, Time.now, [])
66
+ if !@bucket_hash[bucket]
67
+ @buckets << bucket_obj
68
+ @bucket_hash[bucket] = bucket_obj
69
+ end
70
+ bucket_obj
71
+ end
72
+
73
+ def delete_bucket(bucket_name)
74
+ bucket = get_bucket(bucket_name)
75
+ raise NoSuchBucket if !bucket
76
+ raise BucketNotEmpty if bucket.objects.count > 0
77
+ FileUtils.rm_r(get_bucket_folder(bucket))
78
+ @bucket_hash.delete(bucket_name)
79
+ end
80
+
81
+ def get_object(bucket, object_name, request)
82
+ begin
83
+ real_obj = S3Object.new
84
+ obj_root = File.join(@root,bucket,object_name,FAKE_S3_METADATA_DIR)
85
+ metadata = File.open(File.join(obj_root, "metadata")) { |file| YAML::load(file) }
86
+ real_obj.name = object_name
87
+ real_obj.md5 = metadata[:md5]
88
+ real_obj.content_type = request.query['response-content-type'] ||
89
+ metadata.fetch(:content_type) { "application/octet-stream" }
90
+ real_obj.content_disposition = request.query['response-content-disposition'] ||
91
+ metadata[:content_disposition]
92
+ real_obj.content_encoding = metadata.fetch(:content_encoding) # if metadata.fetch(:content_encoding)
93
+ real_obj.io = RateLimitableFile.open(File.join(obj_root, "content"), 'rb')
94
+ real_obj.size = metadata.fetch(:size) { 0 }
95
+ real_obj.creation_date = File.ctime(obj_root).utc.iso8601(SUBSECOND_PRECISION)
96
+ real_obj.modified_date = metadata.fetch(:modified_date) do
97
+ File.mtime(File.join(obj_root, "content")).utc.iso8601(SUBSECOND_PRECISION)
98
+ end
99
+ real_obj.custom_metadata = metadata.fetch(:custom_metadata) { {} }
100
+ return real_obj
101
+ rescue
102
+ unless @quiet_mode
103
+ puts $!
104
+ $!.backtrace.each { |line| puts line }
105
+ end
106
+ return nil
107
+ end
108
+ end
109
+
110
+ def object_metadata(bucket, object)
111
+ end
112
+
113
+ def copy_object(src_bucket_name, src_name, dst_bucket_name, dst_name, request)
114
+ src_root = File.join(@root,src_bucket_name,src_name,FAKE_S3_METADATA_DIR)
115
+ src_metadata_filename = File.join(src_root, "metadata")
116
+ src_metadata = YAML.load(File.open(src_metadata_filename, 'rb').read)
117
+ src_content_filename = File.join(src_root, "content")
118
+
119
+ dst_filename= File.join(@root,dst_bucket_name,dst_name)
120
+ FileUtils.mkdir_p(dst_filename)
121
+
122
+ metadata_dir = File.join(dst_filename,FAKE_S3_METADATA_DIR)
123
+ FileUtils.mkdir_p(metadata_dir)
124
+
125
+ content = File.join(metadata_dir, "content")
126
+ metadata = File.join(metadata_dir, "metadata")
127
+
128
+ if src_bucket_name != dst_bucket_name || src_name != dst_name
129
+ File.open(content, 'wb') do |f|
130
+ File.open(src_content_filename, 'rb') do |input|
131
+ f << input.read
132
+ end
133
+ end
134
+
135
+ File.open(metadata,'w') do |f|
136
+ File.open(src_metadata_filename,'r') do |input|
137
+ f << input.read
138
+ end
139
+ end
140
+ end
141
+
142
+ metadata_directive = request.header["x-amz-metadata-directive"].first
143
+ if metadata_directive == "REPLACE"
144
+ metadata_struct = create_metadata(content, request)
145
+ File.open(metadata,'w') do |f|
146
+ f << YAML::dump(metadata_struct)
147
+ end
148
+ end
149
+
150
+ src_bucket = get_bucket(src_bucket_name) || create_bucket(src_bucket_name)
151
+ dst_bucket = get_bucket(dst_bucket_name) || create_bucket(dst_bucket_name)
152
+
153
+ obj = S3Object.new
154
+ obj.name = dst_name
155
+ obj.md5 = src_metadata[:md5]
156
+ obj.content_type = src_metadata[:content_type]
157
+ obj.content_disposition = src_metadata[:content_disposition]
158
+ obj.content_encoding = src_metadata[:content_encoding] # if src_metadata[:content_encoding]
159
+ obj.size = src_metadata[:size]
160
+ obj.modified_date = src_metadata[:modified_date]
161
+
162
+ src_bucket.find(src_name)
163
+ dst_bucket.add(obj)
164
+ return obj
165
+ end
166
+
167
+ def store_object(bucket, object_name, request)
168
+ filedata = ""
169
+
170
+ # TODO put a tmpfile here first and mv it over at the end
171
+ content_type = request.content_type || ""
172
+
173
+ match = content_type.match(/^multipart\/form-data; boundary=(.+)/)
174
+ boundary = match[1] if match
175
+ if boundary
176
+ boundary = WEBrick::HTTPUtils::dequote(boundary)
177
+ form_data = WEBrick::HTTPUtils::parse_form_data(request.body, boundary)
178
+
179
+ if form_data['file'] == nil || form_data['file'] == ""
180
+ raise WEBrick::HTTPStatus::BadRequest
181
+ end
182
+
183
+ filedata = form_data['file']
184
+ else
185
+ request.body { |chunk| filedata << chunk }
186
+ end
187
+
188
+ do_store_object(bucket, object_name, filedata, request)
189
+ end
190
+
191
+ def do_store_object(bucket, object_name, filedata, request)
192
+ begin
193
+ filename = File.join(@root, bucket.name, object_name)
194
+ FileUtils.mkdir_p(filename)
195
+
196
+ metadata_dir = File.join(filename, FAKE_S3_METADATA_DIR)
197
+ FileUtils.mkdir_p(metadata_dir)
198
+
199
+ content = File.join(filename, FAKE_S3_METADATA_DIR, "content")
200
+ metadata = File.join(filename, FAKE_S3_METADATA_DIR, "metadata")
201
+
202
+ File.open(content,'wb') { |f| f << filedata }
203
+
204
+ metadata_struct = create_metadata(content, request)
205
+ File.open(metadata,'w') do |f|
206
+ f << YAML::dump(metadata_struct)
207
+ end
208
+
209
+ obj = S3Object.new
210
+ obj.name = object_name
211
+ obj.md5 = metadata_struct[:md5]
212
+ obj.content_type = metadata_struct[:content_type]
213
+ obj.content_disposition = metadata_struct[:content_disposition]
214
+ obj.content_encoding = metadata_struct[:content_encoding] # if metadata_struct[:content_encoding]
215
+ obj.size = metadata_struct[:size]
216
+ obj.modified_date = metadata_struct[:modified_date]
217
+
218
+ bucket.add(obj)
219
+ return obj
220
+ rescue
221
+ unless @quiet_mode
222
+ puts $!
223
+ $!.backtrace.each { |line| puts line }
224
+ end
225
+ return nil
226
+ end
227
+ end
228
+
229
+ def combine_object_parts(bucket, upload_id, object_name, parts, request)
230
+ upload_path = File.join(@root, bucket.name)
231
+ base_path = File.join(upload_path, "#{upload_id}_#{object_name}")
232
+
233
+ complete_file = ""
234
+ chunk = ""
235
+ part_paths = []
236
+
237
+ parts.sort_by { |part| part[:number] }.each do |part|
238
+ part_path = "#{base_path}_part#{part[:number]}"
239
+ content_path = File.join(part_path, FAKE_S3_METADATA_DIR, 'content')
240
+
241
+ File.open(content_path, 'rb') { |f| chunk = f.read }
242
+ etag = Digest::MD5.hexdigest(chunk)
243
+
244
+ raise new Error "invalid file chunk" unless part[:etag] == etag
245
+ complete_file << chunk
246
+ part_paths << part_path
247
+ end
248
+
249
+ object = do_store_object(bucket, object_name, complete_file, request)
250
+
251
+ # clean up parts
252
+ part_paths.each do |path|
253
+ FileUtils.remove_dir(path)
254
+ end
255
+
256
+ object
257
+ end
258
+
259
+ def delete_object(bucket,object_name,request)
260
+ begin
261
+ filename = File.join(@root,bucket.name,object_name)
262
+ FileUtils.rm_rf(filename)
263
+ object = bucket.find(object_name)
264
+ bucket.remove(object)
265
+ rescue
266
+ puts $!
267
+ $!.backtrace.each { |line| puts line }
268
+ return nil
269
+ end
270
+ end
271
+
272
+ def delete_objects(bucket, objects, request)
273
+ begin
274
+ filenames = []
275
+ objects.each do |object_name|
276
+ filenames << File.join(@root,bucket.name,object_name)
277
+ object = bucket.find(object_name)
278
+ bucket.remove(object)
279
+ end
280
+
281
+ FileUtils.rm_rf(filenames)
282
+ rescue
283
+ puts $!
284
+ $!.backtrace.each { |line| puts line }
285
+ return nil
286
+ end
287
+ end
288
+
289
+ # TODO: abstract getting meta data from request.
290
+ def create_metadata(content, request)
291
+ metadata = {}
292
+ metadata[:md5] = Digest::MD5.file(content).hexdigest
293
+ metadata[:content_type] = request.header["content-type"].first
294
+ if request.header['content-disposition']
295
+ metadata[:content_disposition] = request.header['content-disposition'].first
296
+ end
297
+ content_encoding = request.header["content-encoding"].first
298
+ metadata[:content_encoding] = content_encoding
299
+ #if content_encoding
300
+ # metadata[:content_encoding] = content_encoding
301
+ #end
302
+ metadata[:size] = File.size(content)
303
+ metadata[:modified_date] = File.mtime(content).utc.iso8601(SUBSECOND_PRECISION)
304
+ metadata[:amazon_metadata] = {}
305
+ metadata[:custom_metadata] = {}
306
+
307
+ # Add custom metadata from the request header
308
+ request.header.each do |key, value|
309
+ match = /^x-amz-([^-]+)-(.*)$/.match(key)
310
+ next unless match
311
+ if match[1].eql?('meta') && (match_key = match[2])
312
+ metadata[:custom_metadata][match_key] = value.join(', ')
313
+ next
314
+ end
315
+ metadata[:amazon_metadata][key.gsub(/^x-amz-/, '')] = value.join(', ')
316
+ end
317
+ return metadata
318
+ end
319
+ end
320
+ end
@@ -0,0 +1,21 @@
1
+ module FakeS3
2
+ class RateLimitableFile < File
3
+ @@rate_limit = nil
4
+ # Specify a rate limit in bytes per second
5
+ def self.rate_limit
6
+ @@rate_limit
7
+ end
8
+
9
+ def self.rate_limit=(rate_limit)
10
+ @@rate_limit = rate_limit
11
+ end
12
+
13
+ def read(args)
14
+ if @@rate_limit
15
+ time_to_sleep = args / @@rate_limit
16
+ sleep(time_to_sleep)
17
+ end
18
+ return super(args)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,19 @@
1
+ module FakeS3
2
+ class S3Object
3
+ include Comparable
4
+ attr_accessor :name,:size,:creation_date,:modified_date,:md5,:io,:content_type,:content_disposition,:content_encoding,:custom_metadata
5
+
6
+ def hash
7
+ @name.hash
8
+ end
9
+
10
+ def eql?(object)
11
+ object.is_a?(self.class) ? (@name == object.name) : false
12
+ end
13
+
14
+ # Sort by the object's name
15
+ def <=>(object)
16
+ object.is_a?(self.class) ? (@name <=> object.name) : nil
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,597 @@
1
+ require 'time'
2
+ require 'webrick'
3
+ require 'webrick/https'
4
+ require 'openssl'
5
+ require 'securerandom'
6
+ require 'cgi'
7
+ require 'fakes3/util'
8
+ require 'fakes3/file_store'
9
+ require 'fakes3/xml_adapter'
10
+ require 'fakes3/xml_parser'
11
+ require 'fakes3/bucket_query'
12
+ require 'fakes3/unsupported_operation'
13
+ require 'fakes3/errors'
14
+ require 'ipaddr'
15
+
16
+ module FakeS3
17
+ class Request
18
+ CREATE_BUCKET = "CREATE_BUCKET"
19
+ LIST_BUCKETS = "LIST_BUCKETS"
20
+ LS_BUCKET = "LS_BUCKET"
21
+ HEAD = "HEAD"
22
+ STORE = "STORE"
23
+ COPY = "COPY"
24
+ GET = "GET"
25
+ GET_ACL = "GET_ACL"
26
+ SET_ACL = "SET_ACL"
27
+ MOVE = "MOVE"
28
+ DELETE_OBJECT = "DELETE_OBJECT"
29
+ DELETE_BUCKET = "DELETE_BUCKET"
30
+ DELETE_OBJECTS = "DELETE_OBJECTS"
31
+
32
+ attr_accessor :bucket, :object, :type, :src_bucket,
33
+ :src_object, :method, :webrick_request,
34
+ :path, :is_path_style, :query, :http_verb
35
+
36
+ def inspect
37
+ puts "-----Inspect FakeS3 Request"
38
+ puts "Type: #{@type}"
39
+ puts "Is Path Style: #{@is_path_style}"
40
+ puts "Request Method: #{@method}"
41
+ puts "Bucket: #{@bucket}"
42
+ puts "Object: #{@object}"
43
+ puts "Src Bucket: #{@src_bucket}"
44
+ puts "Src Object: #{@src_object}"
45
+ puts "Query: #{@query}"
46
+ puts "-----Done"
47
+ end
48
+ end
49
+
50
+ def self.logger
51
+ @@logger ||= defined?(Rails) ? Rails.logger : Logger.new(STDOUT)
52
+ end
53
+
54
+ def self.logger=(logger)
55
+ @@logger = logger
56
+ end
57
+
58
+ class Servlet < WEBrick::HTTPServlet::AbstractServlet
59
+ def initialize(server,store,hostname)
60
+ super(server)
61
+ @store = store
62
+ @hostname = hostname
63
+ @port = server.config[:Port]
64
+ @root_hostnames = [hostname,'localhost','s3.amazonaws.com','s3.localhost']
65
+ end
66
+
67
+ def validate_request(request)
68
+ req = request.webrick_request
69
+ return if req.nil?
70
+ return if not req.header.has_key?('expect')
71
+ req.continue if req.header['expect'].first=='100-continue'
72
+ end
73
+
74
+ def do_GET(request, response)
75
+ s_req = normalize_request(request)
76
+
77
+ case s_req.type
78
+ when 'LIST_BUCKETS'
79
+ response.status = 200
80
+ response['Content-Type'] = 'application/xml'
81
+ buckets = @store.buckets
82
+ response.body = XmlAdapter.buckets(buckets)
83
+ when 'LS_BUCKET'
84
+ bucket_obj = @store.get_bucket(s_req.bucket)
85
+ if bucket_obj
86
+ response.status = 200
87
+ response['Content-Type'] = "application/xml"
88
+ query = {
89
+ :marker => s_req.query["marker"] ? s_req.query["marker"].to_s : nil,
90
+ :prefix => s_req.query["prefix"] ? s_req.query["prefix"].to_s : nil,
91
+ :max_keys => s_req.query["max-keys"] ? s_req.query["max-keys"].to_i : nil,
92
+ :delimiter => s_req.query["delimiter"] ? s_req.query["delimiter"].to_s : nil
93
+ }
94
+ bq = bucket_obj.query_for_range(query)
95
+ response.body = XmlAdapter.bucket_query(bq)
96
+ else
97
+ response.status = 404
98
+ response.body = XmlAdapter.error_no_such_bucket(s_req.bucket)
99
+ response['Content-Type'] = "application/xml"
100
+ end
101
+ when 'GET_ACL'
102
+ response.status = 200
103
+ response.body = XmlAdapter.acl
104
+ response['Content-Type'] = 'application/xml'
105
+ when 'GET'
106
+ real_obj = @store.get_object(s_req.bucket, s_req.object, request)
107
+ if !real_obj
108
+ response.status = 404
109
+ response.body = XmlAdapter.error_no_such_key(s_req.object)
110
+ response['Content-Type'] = "application/xml"
111
+ return
112
+ end
113
+
114
+ if_none_match = request["If-None-Match"]
115
+ if if_none_match == "\"#{real_obj.md5}\"" or if_none_match == "*"
116
+ response.status = 304
117
+ return
118
+ end
119
+
120
+ if_modified_since = request["If-Modified-Since"]
121
+ if if_modified_since
122
+ time = Time.httpdate(if_modified_since)
123
+ if time >= Time.iso8601(real_obj.modified_date)
124
+ response.status = 304
125
+ return
126
+ end
127
+ end
128
+
129
+ response.status = 200
130
+ response['Content-Type'] = real_obj.content_type
131
+
132
+ if real_obj.content_encoding
133
+ response.header['X-Content-Encoding'] = real_obj.content_encoding
134
+ response.header['Content-Encoding'] = real_obj.content_encoding
135
+ end
136
+
137
+ response['Content-Disposition'] = real_obj.content_disposition if real_obj.content_disposition
138
+ stat = File::Stat.new(real_obj.io.path)
139
+
140
+ response['Last-Modified'] = Time.iso8601(real_obj.modified_date).httpdate
141
+ response.header['ETag'] = "\"#{real_obj.md5}\""
142
+ response['Accept-Ranges'] = "bytes"
143
+ response['Last-Ranges'] = "bytes"
144
+ response['Access-Control-Allow-Origin'] = '*'
145
+
146
+ real_obj.custom_metadata.each do |header, value|
147
+ response.header['x-amz-meta-' + header] = value
148
+ end
149
+
150
+ content_length = stat.size
151
+
152
+ # Added Range Query support
153
+ range = request.header["range"].first
154
+ if range
155
+ response.status = 206
156
+ if range =~ /bytes=(\d*)-(\d*)/
157
+ start = $1.to_i
158
+ finish = $2.to_i
159
+ finish_str = ""
160
+ if finish == 0
161
+ finish = content_length - 1
162
+ finish_str = "#{finish}"
163
+ else
164
+ finish_str = finish.to_s
165
+ end
166
+
167
+ bytes_to_read = finish - start + 1
168
+ response['Content-Range'] = "bytes #{start}-#{finish_str}/#{content_length}"
169
+ real_obj.io.pos = start
170
+ response.body = real_obj.io.read(bytes_to_read)
171
+ return
172
+ end
173
+ end
174
+ response['Content-Length'] = File::Stat.new(real_obj.io.path).size
175
+ response['Content-Disposition'] = 'attachment'
176
+ if s_req.http_verb == 'HEAD'
177
+ response.body = ""
178
+ real_obj.io.close
179
+ else
180
+ response.body = real_obj.io
181
+ end
182
+ end
183
+ end
184
+
185
+ def do_PUT(request, response)
186
+ s_req = normalize_request(request)
187
+ query = CGI::parse(request.request_uri.query || "")
188
+
189
+ return do_multipartPUT(request, response) if query['uploadId'].first
190
+
191
+ response.status = 200
192
+ response.body = ""
193
+ response['Content-Type'] = "text/xml"
194
+ response['Access-Control-Allow-Origin'] = '*'
195
+
196
+ case s_req.type
197
+ when Request::COPY
198
+ object = @store.copy_object(s_req.src_bucket, s_req.src_object, s_req.bucket, s_req.object, request)
199
+ response.body = XmlAdapter.copy_object_result(object)
200
+ when Request::STORE
201
+ bucket_obj = @store.get_bucket(s_req.bucket)
202
+ if !bucket_obj
203
+ # Lazily create a bucket. TODO fix this to return the proper error
204
+ bucket_obj = @store.create_bucket(s_req.bucket)
205
+ end
206
+
207
+ real_obj = @store.store_object(bucket_obj, s_req.object, s_req.webrick_request)
208
+ response.header['ETag'] = "\"#{real_obj.md5}\""
209
+ when Request::CREATE_BUCKET
210
+ @store.create_bucket(s_req.bucket)
211
+ end
212
+ end
213
+
214
+ def do_multipartPUT(request, response)
215
+ s_req = normalize_request(request)
216
+ query = CGI::parse(request.request_uri.query)
217
+
218
+ part_number = query['partNumber'].first
219
+ upload_id = query['uploadId'].first
220
+ part_name = "#{upload_id}_#{s_req.object}_part#{part_number}"
221
+
222
+ # store the part
223
+ if s_req.type == Request::COPY
224
+ real_obj = @store.copy_object(
225
+ s_req.src_bucket, s_req.src_object,
226
+ s_req.bucket , part_name,
227
+ request
228
+ )
229
+
230
+ response['Content-Type'] = "text/xml"
231
+ response.body = XmlAdapter.copy_object_result real_obj
232
+ else
233
+ bucket_obj = @store.get_bucket(s_req.bucket)
234
+ if !bucket_obj
235
+ bucket_obj = @store.create_bucket(s_req.bucket)
236
+ end
237
+ real_obj = @store.store_object(
238
+ bucket_obj, part_name,
239
+ request
240
+ )
241
+
242
+ response.body = ""
243
+ response.header['ETag'] = "\"#{real_obj.md5}\""
244
+ end
245
+
246
+ response['Access-Control-Allow-Origin'] = '*'
247
+ response['Access-Control-Allow-Headers'] = 'Authorization, Content-Length'
248
+ response['Access-Control-Expose-Headers'] = 'ETag'
249
+
250
+ response.status = 200
251
+ end
252
+
253
+ def do_POST(request,response)
254
+ if request.query_string === 'delete'
255
+ return do_DELETE(request, response)
256
+ end
257
+
258
+ s_req = normalize_request(request)
259
+ key = request.query['key']
260
+ query = CGI::parse(request.request_uri.query || "")
261
+
262
+ if query.has_key?('uploads')
263
+ upload_id = SecureRandom.hex
264
+
265
+ response.body = <<-eos.strip
266
+ <?xml version="1.0" encoding="UTF-8"?>
267
+ <InitiateMultipartUploadResult>
268
+ <Bucket>#{ s_req.bucket }</Bucket>
269
+ <Key>#{ key }</Key>
270
+ <UploadId>#{ upload_id }</UploadId>
271
+ </InitiateMultipartUploadResult>
272
+ eos
273
+ elsif query.has_key?('uploadId')
274
+ upload_id = query['uploadId'].first
275
+ bucket_obj = @store.get_bucket(s_req.bucket)
276
+ real_obj = @store.combine_object_parts(
277
+ bucket_obj,
278
+ upload_id,
279
+ s_req.object,
280
+ parse_complete_multipart_upload(request),
281
+ request
282
+ )
283
+
284
+ response.body = XmlAdapter.complete_multipart_result real_obj
285
+ elsif request.content_type =~ /^multipart\/form-data; boundary=(.+)/
286
+ key = request.query['key']
287
+
288
+ success_action_redirect = request.query['success_action_redirect']
289
+ success_action_status = request.query['success_action_status']
290
+
291
+ filename = 'default'
292
+ filename = $1 if request.body =~ /filename="(.*)"/
293
+ key = key.gsub('${filename}', filename)
294
+
295
+ bucket_obj = @store.get_bucket(s_req.bucket) || @store.create_bucket(s_req.bucket)
296
+ real_obj = @store.store_object(bucket_obj, key, s_req.webrick_request)
297
+
298
+ response['Etag'] = "\"#{real_obj.md5}\""
299
+
300
+ if success_action_redirect
301
+ object_params = [ [ :bucket, s_req.bucket ], [ :key, key ] ]
302
+ location_uri = URI.parse(success_action_redirect)
303
+ original_location_params = URI.decode_www_form(String(location_uri.query))
304
+ location_uri.query = URI.encode_www_form(original_location_params + object_params)
305
+
306
+ response.status = 303
307
+ response.body = ""
308
+ response['Location'] = location_uri.to_s
309
+ else
310
+ response.status = success_action_status || 204
311
+ if response.status == "201"
312
+
313
+ FakeS3.logger.debug '##### here is the request.body: '
314
+ FakeS3.logger.debug request.body
315
+ FakeS3.logger.debug '##### request.body ^^^'
316
+ str = request.inspect
317
+ port = str[str.index('@port')+6..str[str.index('@port')..str.length].index(',')+str.index('@port')-1]
318
+ host = str[str.index('@host')+7..str[str.index('@host')..str.length].index(',')+str.index('@host')-2]
319
+ response.body = <<-eos.strip
320
+ <?xml version="1.0" encoding="UTF-8"?>
321
+ <PostResponse>
322
+ <Location>http://#{host}:#{port}/#{s_req.bucket}/#{key}</Location>
323
+ <Bucket>#{s_req.bucket}</Bucket>
324
+ <Key>#{key}</Key>
325
+ <ETag>#{response['Etag']}</ETag>
326
+ </PostResponse>
327
+ eos
328
+ end
329
+
330
+ end
331
+ else
332
+ raise WEBrick::HTTPStatus::BadRequest
333
+ end
334
+
335
+ response['Content-Type'] = 'text/xml'
336
+ response['Access-Control-Allow-Origin'] = '*'
337
+ response['Access-Control-Allow-Headers'] = 'Authorization, Content-Length'
338
+ response['Access-Control-Expose-Headers'] = 'ETag'
339
+ end
340
+
341
+ def do_DELETE(request, response)
342
+ s_req = normalize_request(request)
343
+
344
+ case s_req.type
345
+ when Request::DELETE_OBJECTS
346
+ bucket_obj = @store.get_bucket(s_req.bucket)
347
+ keys = XmlParser.delete_objects(s_req.webrick_request)
348
+ @store.delete_objects(bucket_obj,keys,s_req.webrick_request)
349
+ when Request::DELETE_OBJECT
350
+ bucket_obj = @store.get_bucket(s_req.bucket)
351
+ @store.delete_object(bucket_obj,s_req.object,s_req.webrick_request)
352
+ when Request::DELETE_BUCKET
353
+ @store.delete_bucket(s_req.bucket)
354
+ end
355
+
356
+ response.status = 204
357
+ response.body = ""
358
+ end
359
+
360
+ def do_OPTIONS(request, response)
361
+ super
362
+ response['Access-Control-Allow-Origin'] = '*'
363
+ response['Access-Control-Allow-Methods'] = 'PUT, POST, HEAD, GET, OPTIONS'
364
+ response['Access-Control-Allow-Headers'] = 'Accept, Content-Type, Authorization, Content-Length, ETag, X-CSRF-Token, Content-Disposition'
365
+ response['Access-Control-Expose-Headers'] = 'ETag'
366
+ end
367
+
368
+ private
369
+
370
+ def normalize_delete(webrick_req, s_req)
371
+ path = webrick_req.path
372
+ path_len = path.size
373
+ query = webrick_req.query
374
+ if path == "/" and s_req.is_path_style
375
+ # Probably do a 404 here
376
+ else
377
+ if s_req.is_path_style
378
+ elems = path[1,path_len].split("/")
379
+ s_req.bucket = elems[0]
380
+ else
381
+ elems = path.split("/")
382
+ end
383
+
384
+ if elems.size == 0
385
+ raise UnsupportedOperation
386
+ elsif elems.size == 1
387
+ s_req.type = webrick_req.query_string == 'delete' ? Request::DELETE_OBJECTS : Request::DELETE_BUCKET
388
+ s_req.query = query
389
+ s_req.webrick_request = webrick_req
390
+ else
391
+ s_req.type = Request::DELETE_OBJECT
392
+ object = elems[1,elems.size].join('/')
393
+ s_req.object = object
394
+ end
395
+ end
396
+ end
397
+
398
+ def normalize_get(webrick_req, s_req)
399
+ path = webrick_req.path
400
+ path_len = path.size
401
+ query = webrick_req.query
402
+ if path == "/" and s_req.is_path_style
403
+ s_req.type = Request::LIST_BUCKETS
404
+ else
405
+ if s_req.is_path_style
406
+ elems = path[1,path_len].split("/")
407
+ s_req.bucket = elems[0]
408
+ else
409
+ elems = path.split("/")
410
+ end
411
+
412
+ if elems.size < 2
413
+ s_req.type = Request::LS_BUCKET
414
+ s_req.query = query
415
+ else
416
+ if query["acl"] == ""
417
+ s_req.type = Request::GET_ACL
418
+ else
419
+ s_req.type = Request::GET
420
+ end
421
+ object = elems[1,elems.size].join('/')
422
+ s_req.object = object
423
+ end
424
+ end
425
+ end
426
+
427
+ def normalize_put(webrick_req, s_req)
428
+ path = webrick_req.path
429
+ path_len = path.size
430
+ if path == "/"
431
+ if s_req.bucket
432
+ s_req.type = Request::CREATE_BUCKET
433
+ end
434
+ else
435
+ if s_req.is_path_style
436
+ elems = path[1,path_len].split("/")
437
+ s_req.bucket = elems[0]
438
+ if elems.size == 1
439
+ s_req.type = Request::CREATE_BUCKET
440
+ else
441
+ if webrick_req.request_line =~ /\?acl/
442
+ s_req.type = Request::SET_ACL
443
+ else
444
+ s_req.type = Request::STORE
445
+ end
446
+ s_req.object = elems[1,elems.size].join('/')
447
+ end
448
+ else
449
+ if webrick_req.request_line =~ /\?acl/
450
+ s_req.type = Request::SET_ACL
451
+ else
452
+ s_req.type = Request::STORE
453
+ end
454
+ s_req.object = webrick_req.path[1..-1]
455
+ end
456
+ end
457
+
458
+ # TODO: also parse the x-amz-copy-source-range:bytes=first-last header
459
+ # for multipart copy
460
+ copy_source = webrick_req.header["x-amz-copy-source"]
461
+ if copy_source and copy_source.size == 1
462
+ src_elems = copy_source.first.split("/")
463
+ root_offset = src_elems[0] == "" ? 1 : 0
464
+ s_req.src_bucket = src_elems[root_offset]
465
+ s_req.src_object = src_elems[1 + root_offset,src_elems.size].join("/")
466
+ s_req.type = Request::COPY
467
+ end
468
+
469
+ s_req.webrick_request = webrick_req
470
+ end
471
+
472
+ def normalize_post(webrick_req,s_req)
473
+ path = webrick_req.path
474
+ path_len = path.size
475
+
476
+ s_req.path = webrick_req.query['key']
477
+ s_req.webrick_request = webrick_req
478
+
479
+ if s_req.is_path_style
480
+ elems = path[1, path_len].split("/")
481
+ s_req.bucket = elems[0]
482
+ s_req.object = elems[1..-1].join('/') if elems.size >= 2
483
+ else
484
+ s_req.object = path[1..-1]
485
+ end
486
+ end
487
+
488
+ # This method takes a webrick request and generates a normalized FakeS3 request
489
+ def normalize_request(webrick_req)
490
+ host_header= webrick_req["Host"]
491
+ host = host_header.split(':')[0]
492
+
493
+ s_req = Request.new
494
+ s_req.path = webrick_req.path
495
+ s_req.is_path_style = true
496
+
497
+ if !@root_hostnames.include?(host) && !(IPAddr.new(host) rescue nil)
498
+ s_req.bucket = host.split(".")[0]
499
+ s_req.is_path_style = false
500
+ end
501
+
502
+ s_req.http_verb = webrick_req.request_method
503
+
504
+ case webrick_req.request_method
505
+ when 'PUT'
506
+ normalize_put(webrick_req,s_req)
507
+ when 'GET','HEAD'
508
+ normalize_get(webrick_req,s_req)
509
+ when 'DELETE'
510
+ normalize_delete(webrick_req,s_req)
511
+ when 'POST'
512
+ if webrick_req.query_string != 'delete'
513
+ normalize_post(webrick_req,s_req)
514
+ else
515
+ normalize_delete(webrick_req,s_req)
516
+ end
517
+ else
518
+ raise "Unknown Request"
519
+ end
520
+
521
+ validate_request(s_req)
522
+
523
+ return s_req
524
+ end
525
+
526
+ def parse_complete_multipart_upload(request)
527
+ parts_xml = ""
528
+ request.body { |chunk| parts_xml << chunk }
529
+
530
+ # TODO: improve parsing xml
531
+ parts_xml = parts_xml.scan(/<Part>.*?<\/Part>/m)
532
+
533
+ parts_xml.collect do |xml|
534
+ {
535
+ number: xml[/<PartNumber>(\d+)<\/PartNumber>/, 1].to_i,
536
+ etag: FakeS3::Util.strip_before_and_after(xml[/\<ETag\>(.+)<\/ETag>/, 1], '"')
537
+ }
538
+ end
539
+ end
540
+
541
+ def dump_request(request)
542
+ puts "----------Dump Request-------------"
543
+ puts request.request_method
544
+ puts request.path
545
+ request.each do |k,v|
546
+ puts "#{k}:#{v}"
547
+ end
548
+ puts "----------End Dump -------------"
549
+ end
550
+ end
551
+
552
+
553
+ class Server
554
+ def initialize(address, port, store, hostname, ssl_cert_path, ssl_key_path, extra_options={})
555
+ @address = address
556
+ @port = port
557
+ @store = store
558
+ @hostname = hostname
559
+ @ssl_cert_path = ssl_cert_path
560
+ @ssl_key_path = ssl_key_path
561
+ webrick_config = {
562
+ :BindAddress => @address,
563
+ :Port => @port
564
+ }
565
+ if !@ssl_cert_path.to_s.empty?
566
+ webrick_config.merge!(
567
+ {
568
+ :SSLEnable => true,
569
+ :SSLCertificate => OpenSSL::X509::Certificate.new(File.read(@ssl_cert_path)),
570
+ :SSLPrivateKey => OpenSSL::PKey::RSA.new(File.read(@ssl_key_path))
571
+ }
572
+ )
573
+ end
574
+
575
+ if extra_options[:quiet]
576
+ webrick_config.merge!(
577
+ :Logger => WEBrick::Log.new("/dev/null"),
578
+ :AccessLog => []
579
+ )
580
+ end
581
+
582
+ @server = WEBrick::HTTPServer.new(webrick_config)
583
+ end
584
+
585
+ def serve
586
+ @server.mount "/", Servlet, @store, @hostname
587
+ shutdown = proc { @server.shutdown }
588
+ trap "INT", &shutdown
589
+ trap "TERM", &shutdown
590
+ @server.start
591
+ end
592
+
593
+ def shutdown
594
+ @server.shutdown
595
+ end
596
+ end
597
+ end