fake_aws 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +2 -0
  4. data/README.md +24 -16
  5. data/fake_aws.gemspec +4 -1
  6. data/lib/fake_aws.rb +10 -2
  7. data/lib/fake_aws/error.rb +16 -0
  8. data/lib/fake_aws/s3/bucket_on_disk.rb +26 -0
  9. data/lib/fake_aws/s3/error_index.rb +37 -0
  10. data/lib/fake_aws/s3/object_on_disk.rb +50 -0
  11. data/lib/fake_aws/s3/operations/get_object.rb +30 -23
  12. data/lib/fake_aws/s3/operations/put_bucket.rb +38 -0
  13. data/lib/fake_aws/s3/operations/put_object.rb +22 -36
  14. data/lib/fake_aws/s3/rack_app.rb +19 -6
  15. data/lib/fake_aws/s3/request.rb +100 -0
  16. data/lib/fake_aws/s3/responses/common.rb +37 -0
  17. data/lib/fake_aws/s3/responses/empty.rb +25 -0
  18. data/lib/fake_aws/s3/responses/error.rb +50 -0
  19. data/lib/fake_aws/s3/responses/success.rb +31 -0
  20. data/lib/fake_aws/version.rb +1 -1
  21. data/spec/integration/s3/get_object_spec.rb +35 -12
  22. data/spec/integration/s3/put_bucket_spec.rb +40 -0
  23. data/spec/integration/s3/put_object_spec.rb +12 -4
  24. data/spec/integration/s3/request_styles_spec.rb +54 -0
  25. data/spec/spec_helper.rb +1 -16
  26. data/spec/support/common_header_examples.rb +16 -0
  27. data/spec/support/s3_integration_helpers.rb +18 -0
  28. data/spec/support/xml_parsing_helper.rb +7 -0
  29. data/spec/unit/s3/bucket_on_disk_spec.rb +39 -0
  30. data/spec/unit/s3/error_index_spec.rb +17 -0
  31. data/spec/unit/s3/object_on_disk_spec.rb +82 -0
  32. data/spec/unit/s3/request_spec.rb +92 -0
  33. data/spec/unit/s3/responses/empty_spec.rb +14 -0
  34. data/spec/unit/s3/responses/error_spec.rb +49 -0
  35. data/spec/unit/s3/responses/success_spec.rb +25 -0
  36. metadata +82 -26
  37. data/lib/fake_aws/s3/object_store.rb +0 -66
  38. data/lib/fake_aws/s3/xml_error_response.rb +0 -29
  39. data/spec/unit/s3/object_store_spec.rb +0 -75
  40. data/spec/unit/s3/xml_error_response_spec.rb +0 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ed97453747d15331d9d037d50c15f413163facea
4
- data.tar.gz: f51891713197c2413b3b5a7ad3f7d98261e47c5b
3
+ metadata.gz: fcacd66d77b727c137301c6de05fa146cb88a507
4
+ data.tar.gz: 614b3b3159d8b544a7826d224bf444c35383db95
5
5
  SHA512:
6
- metadata.gz: 6eff8dacdb4d9d42b025debebf69b7bc3c83556c767f71925be8c05c96150be68bdde518ce545ec7d0662a1d3ff83a7576644bab832fcc93e79b34bd36d877fd
7
- data.tar.gz: 31d7f4ae800966929807971739dff4b2b50bd01ab93680a1f7859028c556ab105d584f3e4c3c0c35afe5901e71a0b081a6261c5f5c03190263d8718491c39260
6
+ metadata.gz: 9213550b7d286db1f0f7a62efc0488aa17d637a1e0a20f08c283854dbcb8a2c4a0a9fa31e13f1b288260deaaab9ed6d42930ba604199191f2287e5b91b80f5b7
7
+ data.tar.gz: d20fed38db8168dbf9c9588c3d4ecb198547a74f503f04ce856031b85c9c17e40e8c8d8cafa752cb2dcf91b7d87ae9d4e75d3aa80585f1413492e6a7d1c01151
data/.gitignore CHANGED
@@ -15,3 +15,4 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
+ tags
data/.travis.yml CHANGED
@@ -1,3 +1,5 @@
1
1
  language: ruby
2
2
  rvm:
3
+ - 1.9.3
3
4
  - 2.0.0
5
+ - 2.1.0
data/README.md CHANGED
@@ -20,18 +20,11 @@ Or install it yourself as:
20
20
  $ gem install fake_aws
21
21
 
22
22
 
23
- ## Status
24
-
25
- This is still in the very early stages of development.
23
+ ## Status [![Build Status](https://travis-ci.org/envato/fake_aws.png)](https://travis-ci.org/envato/fake_aws) [![Code Climate](https://codeclimate.com/github/envato/fake_aws.png)](https://codeclimate.com/github/envato/fake_aws)
26
24
 
27
25
  So far there's only a tiny bit of S3 implemented, but it's well tested and
28
26
  fairly easy to extend. Pull requests for more features are welcome.
29
27
 
30
- The S3 implementation only supports basic PUT Object and GET Object requests.
31
- The bucket name must be in the path, not the host. No authentication or
32
- security is implemented. `Content-Type` and `x-amz-metadata` headers are
33
- stored and returned. Responses may or may not be properly formatted.
34
-
35
28
 
36
29
  ## Usage
37
30
 
@@ -57,15 +50,29 @@ end
57
50
 
58
51
  will create a file `root_directory/test_bucket/test_path/test_file.txt`.
59
52
 
60
- (Note: `root_directory/test_bucket` must already exist! As there's no
61
- implementation of the Create Bucket operation yet, you'll need to make the
62
- directory for the bucket yourself before doing a PUT Object.)
63
-
64
53
  It will also create
65
54
  `root_directory/test_bucket/test_path/test_file.txt.metadata.json`, which holds
66
55
  the metadata for the file as a JSON hash.
67
56
 
68
57
 
58
+ ## Implemented Operations
59
+
60
+ ### S3
61
+
62
+ Path-style, virtual-hosted-style, and CNAME-style requests are supported for
63
+ all operations.
64
+
65
+ No authentication or security is implemented.
66
+
67
+ `Content-Type` and `x-amz-metadata` headers are stored and returned.
68
+
69
+ - [GET Object](http://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectGET.html)
70
+ - [PUT Bucket](http://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketPUT.html)
71
+ - [PUT Object](http://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectPUT.html)
72
+
73
+ Pull requests for other operations are welcome!
74
+
75
+
69
76
  ## Contributing
70
77
 
71
78
  1. Fork it
@@ -76,8 +83,9 @@ the metadata for the file as a JSON hash.
76
83
 
77
84
  ## To Do
78
85
 
79
- - Handle bucket names in the host as well as the path
80
- - Spit out a properly formatted response on a successful PUT object operation
81
- - Complete the missing fields in XML error responses
86
+ - Implement [GET Bucket](http://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketGET.html) requests
87
+ - Improve the [response headers](http://docs.aws.amazon.com/AmazonS3/latest/API/RESTCommonResponseHeaders.html) (Content-Length, ETag, etc.)
82
88
  - Check signing of requests
83
- - Handle PUT Object copy requests
89
+ - Handle PUT Object Copy requests
90
+
91
+
data/fake_aws.gemspec CHANGED
@@ -18,9 +18,12 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
+ spec.add_dependency "finer_struct", "~> 0.0.5"
22
+
21
23
  spec.add_development_dependency "bundler", "~> 1.3"
22
24
  spec.add_development_dependency "rake"
23
- spec.add_development_dependency "rspec"
25
+ spec.add_development_dependency "rspec", "~> 3.0.0"
24
26
  spec.add_development_dependency "faraday"
25
27
  spec.add_development_dependency "rack-test"
28
+ spec.add_development_dependency "nori", "~> 2.3.0"
26
29
  end
data/lib/fake_aws.rb CHANGED
@@ -1,6 +1,14 @@
1
1
  require "fake_aws/version"
2
- require "fake_aws/s3/object_store"
2
+ require "fake_aws/error"
3
+ require "fake_aws/s3/bucket_on_disk"
4
+ require "fake_aws/s3/error_index"
5
+ require "fake_aws/s3/object_on_disk"
3
6
  require "fake_aws/s3/rack_app"
4
- require "fake_aws/s3/xml_error_response"
7
+ require "fake_aws/s3/request"
5
8
  require "fake_aws/s3/operations/get_object"
9
+ require "fake_aws/s3/operations/put_bucket"
6
10
  require "fake_aws/s3/operations/put_object"
11
+ require "fake_aws/s3/responses/common"
12
+ require "fake_aws/s3/responses/empty"
13
+ require "fake_aws/s3/responses/error"
14
+ require "fake_aws/s3/responses/success"
@@ -0,0 +1,16 @@
1
+ module FakeAWS
2
+ class Error < RuntimeError
3
+ end
4
+
5
+ class UnknownResponseErrorCode < Error
6
+ def initialize
7
+ super("Attempting to look up an AWS error code that doesn't exist. This is probably a bug in FakeAWS.")
8
+ end
9
+ end
10
+
11
+ class UnsupportedRequestError < Error
12
+ def initialize
13
+ super("FakeAWS doesn't (yet) support this type of AWS request.")
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,26 @@
1
+ module FakeAWS
2
+ module S3
3
+
4
+ class BucketOnDisk
5
+
6
+ def initialize(root_directory, bucket)
7
+ @root_directory = root_directory
8
+ @bucket = bucket
9
+ end
10
+
11
+ def exists?
12
+ Dir.exists?(path)
13
+ end
14
+
15
+ def create
16
+ FileUtils.mkdir_p(path)
17
+ end
18
+
19
+ def path
20
+ @path ||= File.join(@root_directory, @bucket)
21
+ end
22
+
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,37 @@
1
+ require 'finer_struct'
2
+
3
+ module FakeAWS
4
+ module S3
5
+
6
+ # S3 error information, extracted from:
7
+ #
8
+ # http://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html
9
+ module ErrorIndex
10
+ class Error < FinerStruct::Immutable(:description, :status_code); end
11
+
12
+ def self.error_for_code(error_code)
13
+ errors[error_code] || raise(FakeAWS::UnknownResponseErrorCode)
14
+ end
15
+
16
+ private
17
+
18
+ def self.errors
19
+ @errors ||= {
20
+ "NoSuchBucket" => Error.new(
21
+ :description => "The specified bucket does not exist.",
22
+ :status_code => 404
23
+ ),
24
+ "NoSuchKey" => Error.new(
25
+ :description => "The specified key does not exist.",
26
+ :status_code => 404
27
+ ),
28
+ "BucketAlreadyExists" => Error.new(
29
+ :description => "The requested bucket name is not available. The bucket namespace is shared by all users of the system. Please select a different name and try again.",
30
+ :status_code => 409
31
+ )
32
+ }
33
+ end
34
+ end
35
+
36
+ end
37
+ end
@@ -0,0 +1,50 @@
1
+ require 'json'
2
+
3
+ module FakeAWS
4
+ module S3
5
+
6
+ class ObjectOnDisk
7
+
8
+ def initialize(bucket_on_disk, key = nil)
9
+ @bucket_on_disk = bucket_on_disk
10
+ @key = key
11
+ end
12
+
13
+ def exists?
14
+ File.exists?(object_path)
15
+ end
16
+
17
+ def write(content, metadata)
18
+ FileUtils.mkdir_p(directory_path)
19
+ File.write(object_path, content)
20
+ File.write(metadata_path, metadata.to_json)
21
+ end
22
+
23
+ def read_content
24
+ File.new(object_path)
25
+ end
26
+
27
+ def read_metadata
28
+ if File.exists?(metadata_path)
29
+ JSON.parse(File.read(metadata_path))
30
+ else
31
+ {}
32
+ end
33
+ end
34
+
35
+ def object_path
36
+ @object_path ||= File.join(@bucket_on_disk.path, @key)
37
+ end
38
+
39
+ def metadata_path
40
+ @metadata_path ||= File.join("#{object_path}.metadata.json")
41
+ end
42
+
43
+ def directory_path
44
+ @directory_path ||= File.dirname(object_path)
45
+ end
46
+
47
+ end
48
+
49
+ end
50
+ end
@@ -3,49 +3,56 @@ module FakeAWS
3
3
  module Operations
4
4
 
5
5
  class GetObject
6
- def initialize(root_directory, env)
6
+ def initialize(root_directory, request)
7
7
  @root_directory = root_directory
8
- @env = env
8
+ @request = request
9
9
  end
10
10
 
11
11
  def call
12
- if object_store.object_exists?
13
- success_response
14
- else
15
- no_such_key_response
16
- end
12
+ return no_such_bucket_response unless bucket_on_disk.exists?
13
+ return no_such_key_response unless object_on_disk.exists?
14
+
15
+ success_response
17
16
  end
18
17
 
19
18
  private
20
19
 
21
20
  def success_response
22
- [
23
- 200,
24
- { "Content-Type" => content_type },
25
- object_store.read_object
26
- ]
21
+ Responses::Success.new(headers, object_on_disk.read_content)
22
+ end
23
+
24
+ def no_such_bucket_response
25
+ Responses::Error.new("NoSuchBucket", "BucketName" => @request.bucket)
27
26
  end
28
27
 
29
28
  def no_such_key_response
30
- [
31
- 404,
32
- { "Content-Type" => "application/xml" },
33
- # TODO: need to figure out what the resource should be here.
34
- XMLErrorResponse.new("NoSuchKey", "The specified key does not exist.", "")
35
- ]
29
+ Responses::Error.new("NoSuchKey", "Resource" => @request.key)
30
+ end
31
+
32
+ def headers
33
+ [content_type_header, user_headers].inject(:merge)
36
34
  end
37
35
 
38
- def content_type
39
- metadata["Content-Type"] || "application/octet-stream"
36
+ def user_headers
37
+ metadata.select {|key, _| key.start_with?("x-amz-meta-") }
38
+ end
39
+
40
+ def content_type_header
41
+ { "Content-Type" => metadata["Content-Type"] || "application/octet-stream" }
40
42
  end
41
43
 
42
44
  def metadata
43
- @metadata ||= object_store.read_metadata
45
+ @metadata ||= object_on_disk.read_metadata
44
46
  end
45
47
 
46
- def object_store
47
- @object_store ||= ObjectStore.new(@root_directory, @env["PATH_INFO"])
48
+ def object_on_disk
49
+ @object_on_disk ||= ObjectOnDisk.new(bucket_on_disk, @request.key)
48
50
  end
51
+
52
+ def bucket_on_disk
53
+ @bucket_on_disk ||= BucketOnDisk.new(@root_directory, @request.bucket)
54
+ end
55
+
49
56
  end
50
57
 
51
58
  end
@@ -0,0 +1,38 @@
1
+ module FakeAWS
2
+ module S3
3
+
4
+ module Operations
5
+
6
+ class PutBucket
7
+ def initialize(root_directory, request)
8
+ @root_directory = root_directory
9
+ @request = request
10
+ end
11
+
12
+ def call
13
+ return bucket_already_exists_response if bucket_on_disk.exists?
14
+
15
+ bucket_on_disk.create
16
+ success_response
17
+ end
18
+
19
+ private
20
+
21
+ def success_response
22
+ Responses::Empty.new
23
+ end
24
+
25
+ def bucket_already_exists_response
26
+ Responses::Error.new("BucketAlreadyExists", "BucketName" => @request.bucket)
27
+ end
28
+
29
+ def bucket_on_disk
30
+ @bucket_on_disk ||= BucketOnDisk.new(@root_directory, @request.bucket)
31
+ end
32
+
33
+ end
34
+
35
+ end
36
+
37
+ end
38
+ end
@@ -1,63 +1,49 @@
1
1
  module FakeAWS
2
2
  module S3
3
+
3
4
  module Operations
4
5
 
5
6
  class PutObject
6
- def initialize(root_directory, env)
7
+ def initialize(root_directory, request)
7
8
  @root_directory = root_directory
8
- @env = env
9
+ @request = request
9
10
  end
10
11
 
11
12
  def call
12
- # TODO: Bit of a tell-don't-ask violation here. Can it be fixed?
13
- if object_store.bucket_exists?
14
- object_store.write_object(content, metadata)
15
- success_response
16
- else
17
- no_such_bucket_response
18
- end
13
+ return no_such_bucket_response unless bucket_on_disk.exists?
14
+
15
+ object_on_disk.write(@request.content, metadata)
16
+ success_response
19
17
  end
20
18
 
21
19
  private
22
20
 
23
21
  def success_response
24
- [
25
- 200,
26
- { "Content-Type" => "application/xml" },
27
- ["hello world"] # TODO: Uh huh.
28
- ]
22
+ Responses::Empty.new
29
23
  end
30
24
 
31
25
  def no_such_bucket_response
32
- [
33
- 404,
34
- { "Content-Type" => "application/xml" },
35
- XMLErrorResponse.new(
36
- "NoSuchBucket",
37
- "The specified bucket does not exist.",
38
- "/#{object_store.bucket}"
39
- )
40
- ]
26
+ Responses::Error.new("NoSuchBucket", "BucketName" => @request.bucket)
41
27
  end
42
28
 
43
- def content
44
- @env["rack.input"].read
29
+ def metadata
30
+ @metadata ||= [content_type_metadata, user_metadata].inject(:merge)
45
31
  end
46
32
 
47
- def metadata
48
- @metadata ||= {}.tap do |metadata|
49
- metadata["Content-Type"] = @env['CONTENT_TYPE']
33
+ def content_type_metadata
34
+ { "Content-Type" => @request.content_type }
35
+ end
36
+
37
+ def user_metadata
38
+ @request.http_headers.select {|key, _| key.start_with?("x-amz-meta-") }
39
+ end
50
40
 
51
- user_metadata_env_keys = @env.keys.select {|key| key =~ /^HTTP_X_AMZ_META_/ }
52
- user_metadata_env_keys.each do |env_key|
53
- metadata_key = env_key.sub(/^HTTP_/, "").gsub("_", "-").downcase
54
- metadata[metadata_key] = @env[env_key]
55
- end
56
- end
41
+ def object_on_disk
42
+ @object_on_disk ||= ObjectOnDisk.new(bucket_on_disk, @request.key)
57
43
  end
58
44
 
59
- def object_store
60
- @object_store ||= ObjectStore.new(@root_directory, @env["PATH_INFO"])
45
+ def bucket_on_disk
46
+ @bucket_on_disk ||= BucketOnDisk.new(@root_directory, @request.bucket)
61
47
  end
62
48
 
63
49
  end