fake_aws 0.0.2 → 0.0.3

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.
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