fakes3testing1 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,589 @@
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
+ class Servlet < WEBrick::HTTPServlet::AbstractServlet
51
+ def initialize(server,store,hostname)
52
+ super(server)
53
+ @store = store
54
+ @hostname = hostname
55
+ @port = server.config[:Port]
56
+ @root_hostnames = [hostname,'localhost','s3.amazonaws.com','s3.localhost']
57
+ end
58
+
59
+ def validate_request(request)
60
+ req = request.webrick_request
61
+ return if req.nil?
62
+ return if not req.header.has_key?('expect')
63
+ req.continue if req.header['expect'].first=='100-continue'
64
+ end
65
+
66
+ def do_GET(request, response)
67
+ s_req = normalize_request(request)
68
+
69
+ case s_req.type
70
+ when 'LIST_BUCKETS'
71
+ response.status = 200
72
+ response['Content-Type'] = 'application/xml'
73
+ buckets = @store.buckets
74
+ response.body = XmlAdapter.buckets(buckets)
75
+ when 'LS_BUCKET'
76
+ bucket_obj = @store.get_bucket(s_req.bucket)
77
+ if bucket_obj
78
+ response.status = 200
79
+ response['Content-Type'] = "application/xml"
80
+ query = {
81
+ :marker => s_req.query["marker"] ? s_req.query["marker"].to_s : nil,
82
+ :prefix => s_req.query["prefix"] ? s_req.query["prefix"].to_s : nil,
83
+ :max_keys => s_req.query["max-keys"] ? s_req.query["max-keys"].to_i : nil,
84
+ :delimiter => s_req.query["delimiter"] ? s_req.query["delimiter"].to_s : nil
85
+ }
86
+ bq = bucket_obj.query_for_range(query)
87
+ response.body = XmlAdapter.bucket_query(bq)
88
+ else
89
+ response.status = 404
90
+ response.body = XmlAdapter.error_no_such_bucket(s_req.bucket)
91
+ response['Content-Type'] = "application/xml"
92
+ end
93
+ when 'GET_ACL'
94
+ response.status = 200
95
+ response.body = XmlAdapter.acl
96
+ response['Content-Type'] = 'application/xml'
97
+ when 'GET'
98
+ real_obj = @store.get_object(s_req.bucket, s_req.object, request)
99
+ if !real_obj
100
+ response.status = 404
101
+ response.body = XmlAdapter.error_no_such_key(s_req.object)
102
+ response['Content-Type'] = "application/xml"
103
+ return
104
+ end
105
+
106
+ if_none_match = request["If-None-Match"]
107
+ if if_none_match == "\"#{real_obj.md5}\"" or if_none_match == "*"
108
+ response.status = 304
109
+ return
110
+ end
111
+
112
+ if_modified_since = request["If-Modified-Since"]
113
+ if if_modified_since
114
+ time = Time.httpdate(if_modified_since)
115
+ if time >= Time.iso8601(real_obj.modified_date)
116
+ response.status = 304
117
+ return
118
+ end
119
+ end
120
+
121
+ response.status = 200
122
+ response['Content-Type'] = real_obj.content_type
123
+
124
+ if real_obj.content_encoding
125
+ response.header['X-Content-Encoding'] = real_obj.content_encoding
126
+ response.header['Content-Encoding'] = real_obj.content_encoding
127
+ end
128
+
129
+ response['Content-Disposition'] = real_obj.content_disposition if real_obj.content_disposition
130
+ stat = File::Stat.new(real_obj.io.path)
131
+
132
+ response['Last-Modified'] = Time.iso8601(real_obj.modified_date).httpdate
133
+ response.header['ETag'] = "\"#{real_obj.md5}\""
134
+ response['Accept-Ranges'] = "bytes"
135
+ response['Last-Ranges'] = "bytes"
136
+ response['Access-Control-Allow-Origin'] = '*'
137
+
138
+ real_obj.custom_metadata.each do |header, value|
139
+ response.header['x-amz-meta-' + header] = value
140
+ end
141
+
142
+ content_length = stat.size
143
+
144
+ # Added Range Query support
145
+ range = request.header["range"].first
146
+ if range
147
+ response.status = 206
148
+ if range =~ /bytes=(\d*)-(\d*)/
149
+ start = $1.to_i
150
+ finish = $2.to_i
151
+ finish_str = ""
152
+ if finish == 0
153
+ finish = content_length - 1
154
+ finish_str = "#{finish}"
155
+ else
156
+ finish_str = finish.to_s
157
+ end
158
+
159
+ bytes_to_read = finish - start + 1
160
+ response['Content-Range'] = "bytes #{start}-#{finish_str}/#{content_length}"
161
+ real_obj.io.pos = start
162
+ response.body = real_obj.io.read(bytes_to_read)
163
+ return
164
+ end
165
+ end
166
+ response['Content-Length'] = File::Stat.new(real_obj.io.path).size
167
+ response['Content-Disposition'] = 'attachment'
168
+ if s_req.http_verb == 'HEAD'
169
+ response.body = ""
170
+ real_obj.io.close
171
+ else
172
+ response.body = real_obj.io
173
+ end
174
+ end
175
+ end
176
+
177
+ def do_PUT(request, response)
178
+ s_req = normalize_request(request)
179
+ query = CGI::parse(request.request_uri.query || "")
180
+
181
+ return do_multipartPUT(request, response) if query['uploadId'].first
182
+
183
+ response.status = 200
184
+ response.body = ""
185
+ response['Content-Type'] = "text/xml"
186
+ response['Access-Control-Allow-Origin'] = '*'
187
+
188
+ case s_req.type
189
+ when Request::COPY
190
+ object = @store.copy_object(s_req.src_bucket, s_req.src_object, s_req.bucket, s_req.object, request)
191
+ response.body = XmlAdapter.copy_object_result(object)
192
+ when Request::STORE
193
+ bucket_obj = @store.get_bucket(s_req.bucket)
194
+ if !bucket_obj
195
+ # Lazily create a bucket. TODO fix this to return the proper error
196
+ bucket_obj = @store.create_bucket(s_req.bucket)
197
+ end
198
+
199
+ real_obj = @store.store_object(bucket_obj, s_req.object, s_req.webrick_request)
200
+ response.header['ETag'] = "\"#{real_obj.md5}\""
201
+ when Request::CREATE_BUCKET
202
+ @store.create_bucket(s_req.bucket)
203
+ end
204
+ end
205
+
206
+ def do_multipartPUT(request, response)
207
+ s_req = normalize_request(request)
208
+ query = CGI::parse(request.request_uri.query)
209
+
210
+ part_number = query['partNumber'].first
211
+ upload_id = query['uploadId'].first
212
+ part_name = "#{upload_id}_#{s_req.object}_part#{part_number}"
213
+
214
+ # store the part
215
+ if s_req.type == Request::COPY
216
+ real_obj = @store.copy_object(
217
+ s_req.src_bucket, s_req.src_object,
218
+ s_req.bucket , part_name,
219
+ request
220
+ )
221
+
222
+ response['Content-Type'] = "text/xml"
223
+ response.body = XmlAdapter.copy_object_result real_obj
224
+ else
225
+ bucket_obj = @store.get_bucket(s_req.bucket)
226
+ if !bucket_obj
227
+ bucket_obj = @store.create_bucket(s_req.bucket)
228
+ end
229
+ real_obj = @store.store_object(
230
+ bucket_obj, part_name,
231
+ request
232
+ )
233
+
234
+ response.body = ""
235
+ response.header['ETag'] = "\"#{real_obj.md5}\""
236
+ end
237
+
238
+ response['Access-Control-Allow-Origin'] = '*'
239
+ response['Access-Control-Allow-Headers'] = 'Authorization, Content-Length'
240
+ response['Access-Control-Expose-Headers'] = 'ETag'
241
+
242
+ response.status = 200
243
+ end
244
+
245
+ def do_POST(request,response)
246
+ if request.query_string === 'delete'
247
+ return do_DELETE(request, response)
248
+ end
249
+
250
+ s_req = normalize_request(request)
251
+ key = request.query['key']
252
+ query = CGI::parse(request.request_uri.query || "")
253
+
254
+ if query.has_key?('uploads')
255
+ upload_id = SecureRandom.hex
256
+
257
+ response.body = <<-eos.strip
258
+ <?xml version="1.0" encoding="UTF-8"?>
259
+ <InitiateMultipartUploadResult>
260
+ <Bucket>#{ s_req.bucket }</Bucket>
261
+ <Key>#{ key }</Key>
262
+ <UploadId>#{ upload_id }</UploadId>
263
+ </InitiateMultipartUploadResult>
264
+ eos
265
+ elsif query.has_key?('uploadId')
266
+ upload_id = query['uploadId'].first
267
+ bucket_obj = @store.get_bucket(s_req.bucket)
268
+ real_obj = @store.combine_object_parts(
269
+ bucket_obj,
270
+ upload_id,
271
+ s_req.object,
272
+ parse_complete_multipart_upload(request),
273
+ request
274
+ )
275
+
276
+ response.body = XmlAdapter.complete_multipart_result real_obj
277
+ elsif request.content_type =~ /^multipart\/form-data; boundary=(.+)/
278
+ key = request.query['key']
279
+
280
+ success_action_redirect = request.query['success_action_redirect']
281
+ success_action_status = request.query['success_action_status']
282
+
283
+ filename = 'default'
284
+ filename = $1 if request.body =~ /filename="(.*)"/
285
+ key = key.gsub('${filename}', filename)
286
+
287
+ bucket_obj = @store.get_bucket(s_req.bucket) || @store.create_bucket(s_req.bucket)
288
+ real_obj = @store.store_object(bucket_obj, key, s_req.webrick_request)
289
+
290
+ response['Etag'] = "\"#{real_obj.md5}\""
291
+
292
+ if success_action_redirect
293
+ object_params = [ [ :bucket, s_req.bucket ], [ :key, key ] ]
294
+ location_uri = URI.parse(success_action_redirect)
295
+ original_location_params = URI.decode_www_form(String(location_uri.query))
296
+ location_uri.query = URI.encode_www_form(original_location_params + object_params)
297
+
298
+ response.status = 303
299
+ response.body = ""
300
+ response['Location'] = location_uri.to_s
301
+ else
302
+ response.status = success_action_status || 204
303
+ if response.status == "201"
304
+
305
+ puts '##### here is the request.body: '
306
+ puts request.body
307
+ puts '##### request.body ^^^'
308
+ str = request.inspect
309
+ port = str[str.index('@port')+6..str[str.index('@port')..str.length].index(',')+str.index('@port')-1]
310
+ host = str[str.index('@host')+7..str[str.index('@host')..str.length].index(',')+str.index('@host')-2]
311
+ response.body = <<-eos.strip
312
+ <?xml version="1.0" encoding="UTF-8"?>
313
+ <PostResponse>
314
+ <Location>http://#{host}:#{port}/#{s_req.bucket}/#{key}</Location>
315
+ <Bucket>#{s_req.bucket}</Bucket>
316
+ <Key>#{key}</Key>
317
+ <ETag>#{response['Etag']}</ETag>
318
+ </PostResponse>
319
+ eos
320
+ end
321
+
322
+ end
323
+ else
324
+ raise WEBrick::HTTPStatus::BadRequest
325
+ end
326
+
327
+ response['Content-Type'] = 'text/xml'
328
+ response['Access-Control-Allow-Origin'] = '*'
329
+ response['Access-Control-Allow-Headers'] = 'Authorization, Content-Length'
330
+ response['Access-Control-Expose-Headers'] = 'ETag'
331
+ end
332
+
333
+ def do_DELETE(request, response)
334
+ s_req = normalize_request(request)
335
+
336
+ case s_req.type
337
+ when Request::DELETE_OBJECTS
338
+ bucket_obj = @store.get_bucket(s_req.bucket)
339
+ keys = XmlParser.delete_objects(s_req.webrick_request)
340
+ @store.delete_objects(bucket_obj,keys,s_req.webrick_request)
341
+ when Request::DELETE_OBJECT
342
+ bucket_obj = @store.get_bucket(s_req.bucket)
343
+ @store.delete_object(bucket_obj,s_req.object,s_req.webrick_request)
344
+ when Request::DELETE_BUCKET
345
+ @store.delete_bucket(s_req.bucket)
346
+ end
347
+
348
+ response.status = 204
349
+ response.body = ""
350
+ end
351
+
352
+ def do_OPTIONS(request, response)
353
+ super
354
+ response['Access-Control-Allow-Origin'] = '*'
355
+ response['Access-Control-Allow-Methods'] = 'PUT, POST, HEAD, GET, OPTIONS'
356
+ response['Access-Control-Allow-Headers'] = 'Accept, Content-Type, Authorization, Content-Length, ETag, X-CSRF-Token, Content-Disposition'
357
+ response['Access-Control-Expose-Headers'] = 'ETag'
358
+ end
359
+
360
+ private
361
+
362
+ def normalize_delete(webrick_req, s_req)
363
+ path = webrick_req.path
364
+ path_len = path.size
365
+ query = webrick_req.query
366
+ if path == "/" and s_req.is_path_style
367
+ # Probably do a 404 here
368
+ else
369
+ if s_req.is_path_style
370
+ elems = path[1,path_len].split("/")
371
+ s_req.bucket = elems[0]
372
+ else
373
+ elems = path.split("/")
374
+ end
375
+
376
+ if elems.size == 0
377
+ raise UnsupportedOperation
378
+ elsif elems.size == 1
379
+ s_req.type = webrick_req.query_string == 'delete' ? Request::DELETE_OBJECTS : Request::DELETE_BUCKET
380
+ s_req.query = query
381
+ s_req.webrick_request = webrick_req
382
+ else
383
+ s_req.type = Request::DELETE_OBJECT
384
+ object = elems[1,elems.size].join('/')
385
+ s_req.object = object
386
+ end
387
+ end
388
+ end
389
+
390
+ def normalize_get(webrick_req, s_req)
391
+ path = webrick_req.path
392
+ path_len = path.size
393
+ query = webrick_req.query
394
+ if path == "/" and s_req.is_path_style
395
+ s_req.type = Request::LIST_BUCKETS
396
+ else
397
+ if s_req.is_path_style
398
+ elems = path[1,path_len].split("/")
399
+ s_req.bucket = elems[0]
400
+ else
401
+ elems = path.split("/")
402
+ end
403
+
404
+ if elems.size < 2
405
+ s_req.type = Request::LS_BUCKET
406
+ s_req.query = query
407
+ else
408
+ if query["acl"] == ""
409
+ s_req.type = Request::GET_ACL
410
+ else
411
+ s_req.type = Request::GET
412
+ end
413
+ object = elems[1,elems.size].join('/')
414
+ s_req.object = object
415
+ end
416
+ end
417
+ end
418
+
419
+ def normalize_put(webrick_req, s_req)
420
+ path = webrick_req.path
421
+ path_len = path.size
422
+ if path == "/"
423
+ if s_req.bucket
424
+ s_req.type = Request::CREATE_BUCKET
425
+ end
426
+ else
427
+ if s_req.is_path_style
428
+ elems = path[1,path_len].split("/")
429
+ s_req.bucket = elems[0]
430
+ if elems.size == 1
431
+ s_req.type = Request::CREATE_BUCKET
432
+ else
433
+ if webrick_req.request_line =~ /\?acl/
434
+ s_req.type = Request::SET_ACL
435
+ else
436
+ s_req.type = Request::STORE
437
+ end
438
+ s_req.object = elems[1,elems.size].join('/')
439
+ end
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 = webrick_req.path[1..-1]
447
+ end
448
+ end
449
+
450
+ # TODO: also parse the x-amz-copy-source-range:bytes=first-last header
451
+ # for multipart copy
452
+ copy_source = webrick_req.header["x-amz-copy-source"]
453
+ if copy_source and copy_source.size == 1
454
+ src_elems = copy_source.first.split("/")
455
+ root_offset = src_elems[0] == "" ? 1 : 0
456
+ s_req.src_bucket = src_elems[root_offset]
457
+ s_req.src_object = src_elems[1 + root_offset,src_elems.size].join("/")
458
+ s_req.type = Request::COPY
459
+ end
460
+
461
+ s_req.webrick_request = webrick_req
462
+ end
463
+
464
+ def normalize_post(webrick_req,s_req)
465
+ path = webrick_req.path
466
+ path_len = path.size
467
+
468
+ s_req.path = webrick_req.query['key']
469
+ s_req.webrick_request = webrick_req
470
+
471
+ if s_req.is_path_style
472
+ elems = path[1, path_len].split("/")
473
+ s_req.bucket = elems[0]
474
+ s_req.object = elems[1..-1].join('/') if elems.size >= 2
475
+ else
476
+ s_req.object = path[1..-1]
477
+ end
478
+ end
479
+
480
+ # This method takes a webrick request and generates a normalized FakeS3 request
481
+ def normalize_request(webrick_req)
482
+ host_header= webrick_req["Host"]
483
+ host = host_header.split(':')[0]
484
+
485
+ s_req = Request.new
486
+ s_req.path = webrick_req.path
487
+ s_req.is_path_style = true
488
+
489
+ if !@root_hostnames.include?(host) && !(IPAddr.new(host) rescue nil)
490
+ s_req.bucket = host.split(".")[0]
491
+ s_req.is_path_style = false
492
+ end
493
+
494
+ s_req.http_verb = webrick_req.request_method
495
+
496
+ case webrick_req.request_method
497
+ when 'PUT'
498
+ normalize_put(webrick_req,s_req)
499
+ when 'GET','HEAD'
500
+ normalize_get(webrick_req,s_req)
501
+ when 'DELETE'
502
+ normalize_delete(webrick_req,s_req)
503
+ when 'POST'
504
+ if webrick_req.query_string != 'delete'
505
+ normalize_post(webrick_req,s_req)
506
+ else
507
+ normalize_delete(webrick_req,s_req)
508
+ end
509
+ else
510
+ raise "Unknown Request"
511
+ end
512
+
513
+ validate_request(s_req)
514
+
515
+ return s_req
516
+ end
517
+
518
+ def parse_complete_multipart_upload(request)
519
+ parts_xml = ""
520
+ request.body { |chunk| parts_xml << chunk }
521
+
522
+ # TODO: improve parsing xml
523
+ parts_xml = parts_xml.scan(/<Part>.*?<\/Part>/m)
524
+
525
+ parts_xml.collect do |xml|
526
+ {
527
+ number: xml[/<PartNumber>(\d+)<\/PartNumber>/, 1].to_i,
528
+ etag: FakeS3::Util.strip_before_and_after(xml[/\<ETag\>(.+)<\/ETag>/, 1], '"')
529
+ }
530
+ end
531
+ end
532
+
533
+ def dump_request(request)
534
+ puts "----------Dump Request-------------"
535
+ puts request.request_method
536
+ puts request.path
537
+ request.each do |k,v|
538
+ puts "#{k}:#{v}"
539
+ end
540
+ puts "----------End Dump -------------"
541
+ end
542
+ end
543
+
544
+
545
+ class Server
546
+ def initialize(address, port, store, hostname, ssl_cert_path, ssl_key_path, extra_options={})
547
+ @address = address
548
+ @port = port
549
+ @store = store
550
+ @hostname = hostname
551
+ @ssl_cert_path = ssl_cert_path
552
+ @ssl_key_path = ssl_key_path
553
+ webrick_config = {
554
+ :BindAddress => @address,
555
+ :Port => @port
556
+ }
557
+ if !@ssl_cert_path.to_s.empty?
558
+ webrick_config.merge!(
559
+ {
560
+ :SSLEnable => true,
561
+ :SSLCertificate => OpenSSL::X509::Certificate.new(File.read(@ssl_cert_path)),
562
+ :SSLPrivateKey => OpenSSL::PKey::RSA.new(File.read(@ssl_key_path))
563
+ }
564
+ )
565
+ end
566
+
567
+ if extra_options[:quiet]
568
+ webrick_config.merge!(
569
+ :Logger => WEBrick::Log.new("/dev/null"),
570
+ :AccessLog => []
571
+ )
572
+ end
573
+
574
+ @server = WEBrick::HTTPServer.new(webrick_config)
575
+ end
576
+
577
+ def serve
578
+ @server.mount "/", Servlet, @store, @hostname
579
+ shutdown = proc { @server.shutdown }
580
+ trap "INT", &shutdown
581
+ trap "TERM", &shutdown
582
+ @server.start
583
+ end
584
+
585
+ def shutdown
586
+ @server.shutdown
587
+ end
588
+ end
589
+ end