canistor 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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.