canistor 0.4.0 → 0.5.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: af06960c106c4d6a66f2e157cf5392df11bd27182c9014f934658b1981f095c8
4
- data.tar.gz: 9c8d3c968c879d31bf6b471d599cc132b1843ed7cf7c8282cc0b21a27b04b351
3
+ metadata.gz: a6c462186c93c3bac4903417214765c33f95816760224eec56c1fb704cf7d5f2
4
+ data.tar.gz: 29ae2992af7256c0b4d86de177c61626f5ebf413b51f04c0ed69ba80e7df2db6
5
5
  SHA512:
6
- metadata.gz: 2275495c50f747fe474ff02c745976dcf1f05a87a8073002ef53fae3b71a3256aff2ebb8d2fc2f3ed18167c064ed6557271839c83d9dd68600b0e1dff306a6c3
7
- data.tar.gz: 4fd8d042c1acb1bbcfbda6be5a8a865d24bb0d1160d971a09cb6742b4c4175139e9b4ca442a7c6206d1e7249749f1b2b1ed3c1b7f5b135a92889351ae9e3c87e
6
+ metadata.gz: 53b6b59d4f5260cc6393996a48b7c20a533bc0f2d5723cba82722044be0bd093184bc666b81e0a6363bf60418a536b62a1d4eb2d5c1859118309ecc2792b2eb5
7
+ data.tar.gz: 5341e4bef20532532f6f2e7b3669b1fd5130b6bc46fa3f45a851d9fd260d2cf4533f488b1675275f57977e1eb306fdc56e8027d884d8690ed7712a3a48c691a3
data/lib/canistor.rb CHANGED
@@ -23,28 +23,42 @@ require "thread"
23
23
  # credentials to be useful. It can be configured using either the
24
24
  # config method on the instance or by specifying the buckets one by one.
25
25
  #
26
- # In the example below Canistor will have two accounts and three buckets. It
26
+ # In the example below Canistor will have two accounts and four buckets. It
27
27
  # also specifies which accounts can access the buckets.
28
28
  #
29
29
  # Canistor.config(
30
30
  # logger: Rails.logger,
31
31
  # credentials: {
32
- # 'global' => {
32
+ # {
33
33
  # access_key_id: 'AKIAIXXXXXXXXXXXXXX1',
34
34
  # secret_access_key: 'phRL+xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx1'
35
35
  # },
36
- # 'accounting' => {
36
+ # {
37
37
  # access_key_id: 'AKIAIXXXXXXXXXXXXXX2',
38
38
  # secret_access_key: 'phRL+xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx2'
39
39
  # }
40
40
  # },
41
41
  # buckets: {
42
42
  # 'us-east-1' => {
43
- # 'com-procore-production-images' => ['global'],
44
- # 'com-procore-production-books' => ['global', 'accounting']
43
+ # 'io-canistor-production-images' => {
44
+ # allow_access_keys: ['AKIAIXXXXXXXXXXXXXX1'],
45
+ # replicate_to: [
46
+ # 'eu-central-1:io-canistor-production-images-replica'
47
+ # ],
48
+ # versioned: true
49
+ # },
50
+ # 'io-canistor-production-books' => {
51
+ # allow_access_keys: ['AKIAIXXXXXXXXXXXXXX1', 'AKIAIXXXXXXXXXXXXXX2']
52
+ # }
45
53
  # },
46
54
  # 'eu-central-1' => {
47
- # 'com-procore-production-sales' => ['global']
55
+ # 'io-canistor-production-sales' => {
56
+ # allow_access_keys: ['AKIAIXXXXXXXXXXXXXX1']
57
+ # },
58
+ # 'io-canistor-production-images-replica' => {
59
+ # allow_access_keys: ['AKIAIXXXXXXXXXXXXXX1'],
60
+ # versioned: true
61
+ # }
48
62
  # }
49
63
  # }
50
64
  # )
@@ -53,6 +67,10 @@ require "thread"
53
67
  # verifies authentication information. It does not implement control lists so
54
68
  # all accounts have full access to the buckets and objects.
55
69
  #
70
+ # It's possible to turn on replication and versioning. Note that replication is
71
+ # instant in the mock. On actual S3 it takes a while for objects to replicate.
72
+ # Not the entire replication and versioning API is implemented.
73
+ #
56
74
  # The mock can simulate a number of failures. These are triggered by setting
57
75
  # the operation which needs to fail on the mock. For more information see
58
76
  # [Canistor.fail].
@@ -60,6 +78,9 @@ require "thread"
60
78
  # In most cases you should configure the suite to clear the mock before running
61
79
  # each example with [Canistor.clear].
62
80
  module Canistor
81
+ # Raised when Canistor's own configuration is somehow unusable.
82
+ class ConfigurationError < StandardError; end
83
+
63
84
  class << self
64
85
  attr_accessor :logger
65
86
  attr_accessor :store
@@ -102,15 +123,15 @@ module Canistor
102
123
 
103
124
  def self.buckets=(buckets)
104
125
  buckets.each do |region, attributes|
105
- attributes.each do |bucket, access_key_ids|
126
+ attributes.each do |bucket, options|
106
127
  bucket = create_bucket(region, bucket)
107
- bucket.allow_access_to(access_key_ids)
128
+ bucket.update_settings(options)
108
129
  bucket
109
130
  end
110
131
  end
111
132
  end
112
133
 
113
- # Configures a bucket in the mock implementation. Use #allow_access_to on
134
+ # Configures a bucket in the mock implementation. Use #update_settings on
114
135
  # the Container object returned by this method to configure who may access
115
136
  # the bucket.
116
137
  def self.create_bucket(region, bucket_name)
@@ -6,6 +6,8 @@ require 'nokogiri'
6
6
  require 'securerandom'
7
7
 
8
8
  require_relative "storage/bucket"
9
+ require_relative "storage/delete_marker"
9
10
  require_relative "storage/object"
11
+ require_relative "storage/objects"
10
12
  require_relative "storage/part"
11
13
  require_relative "storage/upload"
@@ -6,6 +6,8 @@ require 'nokogiri'
6
6
  require 'singleton'
7
7
  require 'securerandom'
8
8
 
9
+ require_relative "bucket/settings"
10
+
9
11
  module Canistor
10
12
  module Storage
11
13
  # Holds information about a bucket and implements interaction with it.
@@ -13,18 +15,24 @@ module Canistor
13
15
  attr_accessor :region
14
16
  attr_accessor :name
15
17
 
16
- attr_reader :access_keys
18
+ attr_reader :settings
17
19
  attr_reader :objects
18
20
  attr_reader :uploads
19
21
 
20
22
  def initialize(**attributes)
21
- @access_keys = Set.new
23
+ @settings = Settings.new
22
24
  clear
23
25
  attributes.each do |name, value|
24
26
  public_send("#{name}=", value)
25
27
  end
26
28
  end
27
29
 
30
+ # Update bucket settings, see Canistor::Storage::Bucket::Settings for
31
+ # supported configuration.
32
+ def update_settings(settings)
33
+ @settings.update(settings)
34
+ end
35
+
28
36
  def [](name)
29
37
  @objects[name]
30
38
  end
@@ -38,7 +46,7 @@ module Canistor
38
46
  end
39
47
 
40
48
  def head(context, access_key_id, subject)
41
- if !access_keys.include?(access_key_id)
49
+ if !settings.access_keys.include?(access_key_id)
42
50
  Canistor::ErrorHandler.serve_access_denied(context, subject)
43
51
  elsif object = objects[subject.key]
44
52
  object.head(context, subject)
@@ -50,7 +58,7 @@ module Canistor
50
58
  def get(context, access_key_id, subject)
51
59
  params = CGI::parse(context.http_request.endpoint.query.to_s)
52
60
  catch(:rendered_error) do
53
- if !access_keys.include?(access_key_id)
61
+ if !settings.access_keys.include?(access_key_id)
54
62
  Canistor::ErrorHandler.serve_access_denied(context, subject)
55
63
  elsif params.has_key?('uploads')
56
64
  list_bucket_uploads(context)
@@ -67,7 +75,7 @@ module Canistor
67
75
  end
68
76
 
69
77
  def put(context, access_key_id, subject)
70
- if access_keys.include?(access_key_id)
78
+ if settings.access_keys.include?(access_key_id)
71
79
  Canistor.take_fail(:store) { return }
72
80
  params = CGI::parse(context.http_request.endpoint.query.to_s)
73
81
  catch(:rendered_error) do
@@ -85,7 +93,7 @@ module Canistor
85
93
  end
86
94
 
87
95
  def post(context, access_key_id, subject)
88
- if access_keys.include?(access_key_id)
96
+ if settings.access_keys.include?(access_key_id)
89
97
  Canistor.take_fail(:store) { return }
90
98
  params = CGI::parse(context.http_request.endpoint.query.to_s)
91
99
  catch(:rendered_error) do
@@ -105,10 +113,10 @@ module Canistor
105
113
  end
106
114
 
107
115
  def delete(context, access_key_id, subject)
108
- if !access_keys.include?(access_key_id)
116
+ if !settings.access_keys.include?(access_key_id)
109
117
  Canistor::ErrorHandler.serve_access_denied(context, subject)
110
- elsif object = objects[subject.key]
111
- @objects.delete(object.key)
118
+ elsif objects[subject.key]
119
+ object = @objects.delete(subject.key)
112
120
  object.delete(context, subject)
113
121
  else
114
122
  Canistor::ErrorHandler.serve_no_such_key(context, subject)
@@ -116,7 +124,9 @@ module Canistor
116
124
  end
117
125
 
118
126
  def clear
119
- @objects = {}
127
+ @objects = Canistor::Storage::Objects.new(
128
+ versioned: settings.versioned?
129
+ )
120
130
  @uploads = {}
121
131
  end
122
132
 
@@ -133,22 +143,41 @@ module Canistor
133
143
  access_keys.merge(access_key_ids)
134
144
  end
135
145
 
136
- def headers
137
- {}
146
+ def store_replica(object)
147
+ replica = object.copy
148
+ replica.versioned = settings.versioned?
149
+ self[object.key] = replica
138
150
  end
139
151
 
140
152
  private
141
153
 
142
- def build_upload(context, subject)
143
- Canistor::Storage::Upload.new(
154
+ def build_object(subject)
155
+ Canistor::Storage::Object.new(
144
156
  region: subject.region,
145
157
  bucket: subject.bucket,
146
- key: subject.key
158
+ key: subject.key,
159
+ versioned: settings.versioned?
147
160
  )
148
161
  end
149
162
 
150
- def find_or_build_object(context, subject)
151
- objects[subject.key] || Canistor::Storage::Object.new(
163
+ def put_object(context, subject)
164
+ object = build_object(subject)
165
+ object.versioned = settings.versioned?
166
+ object.put(context, subject)
167
+ self[subject.key] = object
168
+ replicate(object)
169
+ end
170
+
171
+ def replicate(object)
172
+ if settings.replicated?
173
+ settings.replicate_to_buckets.each do |bucket|
174
+ bucket.store_replica(object)
175
+ end
176
+ end
177
+ end
178
+
179
+ def build_upload(subject)
180
+ Canistor::Storage::Upload.new(
152
181
  region: subject.region,
153
182
  bucket: subject.bucket,
154
183
  key: subject.key
@@ -156,7 +185,7 @@ module Canistor
156
185
  end
157
186
 
158
187
  def post_upload(context, subject)
159
- upload = build_upload(context, subject)
188
+ upload = build_upload(subject)
160
189
  @uploads[upload.id] = upload
161
190
  upload.put(context, subject)
162
191
  end
@@ -180,12 +209,6 @@ module Canistor
180
209
  end
181
210
  end
182
211
 
183
- def put_object(context, subject)
184
- object = find_or_build_object(context, subject)
185
- self[subject.key] = object
186
- object.put(context, subject)
187
- end
188
-
189
212
  # Iterate over all objects in the bucket using the filter and pagination
190
213
  # options which exist in S3.
191
214
  def each(prefix:, marker:, max_keys:, &block)
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Canistor
4
+ module Storage
5
+ class Bucket
6
+ # Stores settings for the bucket.
7
+ #
8
+ # +allow_access_keys+: Allow full access to the bucket using these
9
+ # access keys.
10
+ # +replicate_to+: Replicate all updates to the bucket to this bucket.
11
+ # +versioned+: Use versioning on the bucket.
12
+ class Settings
13
+ def initialize(settings = {})
14
+ clear
15
+ update(settings)
16
+ end
17
+
18
+ def update(settings)
19
+ case settings
20
+ # Old style setup allowed settings to be an array of allowed access
21
+ # keys.
22
+ when Set, Array
23
+ allow_access_keys(settings)
24
+ else
25
+ settings.each do |name, value|
26
+ public_send("#{name}", value)
27
+ end
28
+ end
29
+ end
30
+
31
+ def access_keys
32
+ @access_keys || []
33
+ end
34
+
35
+ def replicates_to
36
+ @replicates_to || []
37
+ end
38
+
39
+ def versioned?
40
+ !!@versioned
41
+ end
42
+
43
+ def allow_access_keys(access_keys)
44
+ @access_keys = access_keys
45
+ end
46
+
47
+ def replicated?
48
+ @replicates_to && !@replicates_to.empty?
49
+ end
50
+
51
+ def replicate_to(replicates_to)
52
+ @replicates_to = replicates_to
53
+ end
54
+
55
+ def replicate_to_buckets
56
+ return [] unless replicated?
57
+ @replicates_to.map do |location|
58
+ if bucket = Canistor.store.dig(*location.split(':'))
59
+ bucket
60
+ else
61
+ raise(
62
+ Canistor::ConfigurationError,
63
+ "Can't locate bucket `#{location}' when trying to replicate " \
64
+ "object. Please make sure the replication location is in the " \
65
+ "form of region:bucket and configured in Canistor."
66
+ )
67
+ end
68
+ end
69
+ end
70
+
71
+ def versioned(versioned)
72
+ @versioned = versioned
73
+ end
74
+
75
+ def clear
76
+ @access_keys = []
77
+ @replicates_to = []
78
+ @versioned = false
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Canistor
4
+ module Storage
5
+ # Represents the non-existence of an object on a versioned bucket.
6
+ class DeleteMarker
7
+ attr_reader :version_id
8
+
9
+ def initialize
10
+ @version_id = generate_version_id
11
+ end
12
+
13
+ def headers
14
+ {
15
+ 'x-amz-delete-marker' => 'true',
16
+ 'x-amz-version-id' => version_id
17
+ }
18
+ end
19
+
20
+ def head(context, subject)
21
+ Canistor::ErrorHandler.serve_no_such_key(context, subject)
22
+ end
23
+
24
+ def get(context, subject)
25
+ Canistor::ErrorHandler.serve_no_such_key(context, subject)
26
+ end
27
+
28
+ def delete(context, subject)
29
+ context.http_response.signal_headers(200, headers)
30
+ end
31
+
32
+ private
33
+
34
+ def generate_version_id
35
+ Base64.strict_encode64(SecureRandom.hex(16)).gsub('=', '')
36
+ end
37
+ end
38
+ end
39
+ end
@@ -1,10 +1,38 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "digest/sha1"
3
+ require "digest/md5"
4
4
 
5
5
  module Canistor
6
6
  module Storage
7
7
  class Object
8
+ # Renders the result of a copy request.
9
+ class CopyObjectResult
10
+ def initialize(source_object, object)
11
+ @source_object, @object = source_object, object
12
+ end
13
+
14
+ def headers
15
+ @object.identity_headers.merge(
16
+ 'x-amz-copy-source-version-id' => @source_object.version_id
17
+ )
18
+ end
19
+
20
+ def to_xml
21
+ Nokogiri::XML::Builder.new do |xml|
22
+ xml.CopyObjectResult do
23
+ xml.LastModified @object.last_modified
24
+ xml.ETag @object.etag
25
+ end
26
+ end.to_xml
27
+ end
28
+
29
+ def self.serve(context, subject, source_object, object)
30
+ result = new(source_object, object)
31
+ context.http_response.signal_headers(200, result.headers)
32
+ context.http_response.signal_data(result.to_xml)
33
+ end
34
+ end
35
+
8
36
  attr_accessor :region
9
37
  attr_accessor :bucket
10
38
  attr_accessor :key
@@ -13,6 +41,7 @@ module Canistor
13
41
  attr_reader :last_modified
14
42
 
15
43
  def initialize(**attributes)
44
+ @versioned = false
16
45
  @headers = {}
17
46
  @data = ''
18
47
  attributes.each do |name, value|
@@ -21,6 +50,20 @@ module Canistor
21
50
  @digest = nil
22
51
  end
23
52
 
53
+ def versioned=(versioned)
54
+ @versioned = versioned
55
+ end
56
+
57
+ def versioned?
58
+ !!@versioned
59
+ end
60
+
61
+ def version_id
62
+ if versioned?
63
+ @version_id ||= generate_version_id
64
+ end
65
+ end
66
+
24
67
  def size
25
68
  data&.size
26
69
  end
@@ -35,7 +78,7 @@ module Canistor
35
78
  end
36
79
 
37
80
  def digest
38
- @digest ||= Digest::SHA1.hexdigest(data)
81
+ @digest ||= Digest::MD5.hexdigest(data)
39
82
  end
40
83
 
41
84
  def etag
@@ -65,11 +108,12 @@ module Canistor
65
108
 
66
109
  def identity_headers
67
110
  {
68
- 'x-amz-request-id' => Base64.strict_encode64(SecureRandom.hex(16)),
111
+ 'etag' => etag,
69
112
  'x-amz-id' => digest[0, 16],
70
113
  'x-amz-id-2' => Base64.strict_encode64(digest),
71
- 'etag' => etag
72
- }
114
+ 'x-amz-request-id' => Base64.strict_encode64(SecureRandom.hex(16)),
115
+ 'x-amz-version-id' => version_id
116
+ }.compact
73
117
  end
74
118
 
75
119
  def head(context, subject)
@@ -82,23 +126,38 @@ module Canistor
82
126
  end
83
127
 
84
128
  def put(context, subject)
85
- catch(:rendered_error) do
86
- source_object = source_object(context, subject)
87
- write(
88
- object_headers(context, source_object),
89
- object_data(context, source_object)
90
- )
91
- context.http_response.signal_headers(200, identity_headers)
129
+ if source_object = find_source(context, subject)
130
+ apply_source_object(context, subject, source_object)
131
+ else
132
+ apply_request(context, subject)
92
133
  end
93
134
  end
94
135
 
136
+ def copy
137
+ object = self.class.new(region: region, bucket: bucket, key: key)
138
+ object.write(headers, data)
139
+ object
140
+ end
141
+
95
142
  def delete(context, subject)
96
143
  context.http_response.signal_headers(200, {})
97
144
  end
98
145
 
146
+ def copy_headers(context)
147
+ directive = context.http_request.headers['x-amz-metadata-directive']
148
+ case directive
149
+ when 'COPY'
150
+ identity_headers
151
+ when 'REPLACE', nil
152
+ context.http_request.headers
153
+ else
154
+ raise ArgumentError, "Unsupported metadata directive: `#{directive}'"
155
+ end
156
+ end
157
+
99
158
  private
100
159
 
101
- def source_object(context, subject)
160
+ def find_source(context, subject)
102
161
  if source = context.http_request.headers['x-amz-copy-source']
103
162
  bucket_name, key = source.split('/', 2)
104
163
  if bucket = Canistor.store.dig(region, bucket_name)
@@ -115,24 +174,18 @@ module Canistor
115
174
  end
116
175
  end
117
176
 
118
- def object_data(context, source_object)
119
- if source_object
120
- source_object.data
121
- else
122
- context.http_request.body
123
- end
177
+ def apply_request(context, subject)
178
+ write(context.http_request.headers, context.http_request.body)
179
+ context.http_response.signal_headers(200, identity_headers)
124
180
  end
125
181
 
126
- def object_headers(context, source_object)
127
- directive = context.http_request.headers['x-amz-metadata-directive']
128
- case directive
129
- when 'COPY'
130
- source_object.headers
131
- when 'REPLACE', nil
132
- context.http_request.headers
133
- else
134
- raise ArgumentError, "Unsupported metadata directive: `#{directive}'"
135
- end
182
+ def apply_source_object(context, subject, source_object)
183
+ write(source_object.copy_headers(context), source_object.data)
184
+ CopyObjectResult.serve(context, subject, source_object, self)
185
+ end
186
+
187
+ def generate_version_id
188
+ Base64.strict_encode64(SecureRandom.hex(16)).gsub('=', '')
136
189
  end
137
190
 
138
191
  META_HEADERS = %w(
@@ -143,7 +196,7 @@ module Canistor
143
196
  def headers=(headers)
144
197
  return if headers.nil?
145
198
  headers.each do |name, value|
146
- if META_HEADERS.include?(name)
199
+ if META_HEADERS.include?(name) || name.start_with?('x-amz-meta')
147
200
  @headers[name] = value
148
201
  end
149
202
  end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Canistor
4
+ module Storage
5
+ # Manages a lookup table of versioned objects.
6
+ class Objects
7
+ def initialize(versioned: true)
8
+ @versioned = versioned
9
+ @objects = {}
10
+ end
11
+
12
+ def versioned?
13
+ @versioned
14
+ end
15
+
16
+ def [](key)
17
+ if stack = @objects[key]
18
+ stack.last
19
+ end
20
+ end
21
+
22
+ def []=(key, object)
23
+ @objects[key] ||= []
24
+ if versioned?
25
+ @objects[key].push(object)
26
+ else
27
+ @objects[key] = [object]
28
+ end
29
+ end
30
+
31
+ def delete(key)
32
+ if versioned?
33
+ self[key] = Canistor::Storage::DeleteMarker.new
34
+ else
35
+ @objects.delete(key).last
36
+ end
37
+ end
38
+
39
+ def dig(*segments)
40
+ @objects.dig(*segments)&.last
41
+ end
42
+
43
+ def each
44
+ @objects.each do |key, stack|
45
+ yield [key, stack.last]
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -10,7 +10,6 @@ module Canistor
10
10
  attr_accessor :number
11
11
 
12
12
  attr_reader :data
13
- attr_reader :etag
14
13
 
15
14
  def initialize(**attributes)
16
15
  attributes.each do |name, value|
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Canistor
4
- VERSION = '0.4.0'
4
+ VERSION = '0.5.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: canistor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Manfred Stienstra
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-08-24 00:00:00.000000000 Z
11
+ date: 2018-10-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -98,7 +98,10 @@ files:
98
98
  - lib/canistor/plugin.rb
99
99
  - lib/canistor/storage.rb
100
100
  - lib/canistor/storage/bucket.rb
101
+ - lib/canistor/storage/bucket/settings.rb
102
+ - lib/canistor/storage/delete_marker.rb
101
103
  - lib/canistor/storage/object.rb
104
+ - lib/canistor/storage/objects.rb
102
105
  - lib/canistor/storage/part.rb
103
106
  - lib/canistor/storage/upload.rb
104
107
  - lib/canistor/subject.rb
@@ -123,7 +126,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
123
126
  version: '0'
124
127
  requirements: []
125
128
  rubyforge_project:
126
- rubygems_version: 2.7.3
129
+ rubygems_version: 2.7.6
127
130
  signing_key:
128
131
  specification_version: 4
129
132
  summary: Canistor is mock for Aws::S3 defined by the AWS SDK gem.