shrine-content_addressable 0.3.1 → 0.4.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
  SHA1:
3
- metadata.gz: 4f20e5a9c6b4b2a0414709d361b48b4720595775
4
- data.tar.gz: 88466ca818aea8fd7bab8e8bfcc39702333711ad
3
+ metadata.gz: a508236ee58a19f0f50bc8dbbc8f01e769f1f726
4
+ data.tar.gz: ca1e2593444f15e57671f5553cfe1cd98fad16f9
5
5
  SHA512:
6
- metadata.gz: ae88579392a93003c7e9c547bbe54cc7109e8441d1d17aa8632bd7911ee7cca04739f1358d9c4ebec994b2f98af0b75292dc65746cd031adeb50b0058577b718
7
- data.tar.gz: c3be4fd82bd522cea507600680c0e35ec085af7a3ace7b72691a3947f64442340ed4025f7afbd727717ba8a36c1464a4453365cb3a45e56b74088be093d70c0c
6
+ metadata.gz: 977f6d34d1f29027ae121a310e599de195eabe4befde9e5b98a95ee880cb9a5c54131e15a7a649aa8bcd18a21c339b58adbaa3bd54cfa7faae41905a634675d8
7
+ data.tar.gz: f2e4df13b082e8896ba9178f6faecae1fc20c06b46532561b7daf481335ce370ea5a64f2037b2054ac38e15633818b0a204fe0eed13ce968ee4d8c41ab6db02f
@@ -9,3 +9,6 @@ AllCops:
9
9
 
10
10
  Style/EndOfLine:
11
11
  EnforcedStyle: lf
12
+
13
+ Naming/UncommunicativeMethodParamName:
14
+ AllowedNames: [_, x, y, z, i, io, id]
data/README.md CHANGED
@@ -45,6 +45,19 @@ correctly supported by the `signature` plugin, and has a `multihash` code, add t
45
45
  plugin :content_addressable, hash: :blake2_b, multihash: 'blake2b'
46
46
  ```
47
47
 
48
+ Your uploaded files will be extended with some multihash capability:
49
+
50
+ ```Ruby
51
+ uploader = Uploader.new(:cache)
52
+ uploaded_file = uploader.upload(my_file)
53
+
54
+ uploaded_file.content_addressable # => the content addressable hash, regardless of location
55
+ uploaded_file.decode # => the decoded multihash
56
+ uploaded_file.digest # => the decoded digest (in bytes. Use .unpack('H*') to turn into hex)
57
+ uploaded_file.digest_function # => the digest function used to create the multihash
58
+ uploaded_file.to_content_addressable! # => ContentAddressableFile, and auto registers the storage
59
+ ```
60
+
48
61
  ### ContentAddressable IO
49
62
  Since a content-addressable stored file is the same across whichever storage, it *MUST* not matter what storage the file
50
63
  is accessed from when it comes to reading. A wrapper is provided so files can be looked up by their content-addressable
@@ -53,14 +66,15 @@ id / hash, instead of a data hash (default for Shrine).
53
66
  ```Ruby
54
67
  require 'content_addressable_file'
55
68
 
56
- # You currently need to register the storages
69
+ # You currently need to register the storages, unless you use uploaded_file.to_content_addressable!
57
70
  ContentAddressableFile.register_storage(lookup, lookup, lookup)
58
71
 
59
- # You can disallow deletion using
72
+ # You can disallow deletion
60
73
  ContentAddressableFile.register_read_only_storage(lookup, lookup, lookup)
61
74
 
62
75
  file = ContentAddressableFile.new(content_addressable_hash)
63
76
 
77
+ # Shares the interface with UploadedFile
64
78
  # => file methods like open, rewind, read, close and eof? are available
65
79
  # => file.url gives the first url that exists
66
80
  # => file.exists? is true if it exists in any storage
@@ -72,20 +86,25 @@ To reset known storages use:
72
86
  ContentAddressableFile.reset
73
87
  ```
74
88
 
75
- In a later version registration might be automatic. A lookup storage needs to respond to:
89
+ Registration is only automatic when using `#to_content_addressable!`. Do not rely on that behaviour
90
+ if you're not always uploading files, but trying to retrieve them.
91
+
92
+ A lookup storage needs to respond to:
76
93
  ```Ruby
77
94
  lookup = Shrine::Storage::Memory.new
78
- id = content_addressable_hash
95
+ content_addressable = content_addressable_hash
79
96
 
80
- lookup.open(id) # IO.open
81
- lookup.exists?(id) # true if storage has it (and can open)
97
+ lookup.open(content_addressable) # IO.open
98
+ lookup.exists?(content_addressable) # true if storage has it (and can open)
82
99
 
83
100
  # optional
84
- lookup.url(id) # url to the io (only if storage has it)
85
- lookup.delete(id) # delete from storage
86
- lookup.download(id) # download the io
101
+ lookup.url(content_addressable) # url to the io (only if storage has it)
102
+ lookup.delete(content_addressable) # delete from storage
103
+ lookup.download(content_addressable) # download the io
87
104
  ```
88
105
 
106
+ Note that you input the hash, and not some arbitrary path.
107
+
89
108
  ## Development
90
109
 
91
110
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can
@@ -107,5 +126,5 @@ The gem is available as open source under the terms of the [MIT License](https:/
107
126
 
108
127
  ## Code of Conduct
109
128
 
110
- Everyone interacting in the Shrine::ConfigurableStorage project’s codebases, issue trackers, chat rooms and mailing
111
- lists is expected to follow the [code of conduct](https://github.com/SleeplessByte/shrine-configurable_storage/blob/master/CODE_OF_CONDUCT.md).
129
+ Everyone interacting in the `Shrine::Plugins::ContentAddressable` project’s codebases, issue trackers, chat rooms and
130
+ mailing lists is expected to follow the [code of conduct](https://github.com/SleeplessByte/shrine-configurable_storage/blob/master/CODE_OF_CONDUCT.md).
@@ -1,52 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'English'
4
3
  require 'multihashes'
5
- require 'forwardable'
4
+ require 'set'
6
5
 
7
- class ReadOnlyStorage
8
- extend Forwardable
9
- def_delegators :@storage, :exists?, :download, :url
10
-
11
- def initialize(storage)
12
- @storage = storage
13
- end
14
- end
6
+ require 'content_addressable_file/acts_as_uploaded_file'
7
+ require 'content_addressable_file/shared_interface'
15
8
 
16
9
  class ContentAddressableFile
17
-
18
- class << self
19
- attr_accessor :storages
20
-
21
- # Registers a storage to be used with content-addressable file. All shrine
22
- # storages are supported by default, as is any duck type that responds to
23
- # Storage#open(content-addressable-hash) and Storage#exists?(hash).
24
- #
25
- # Additional functionality needs Storage#url(hash), Storage#delete(hash)
26
- # and Storage#download(hash).
27
- #
28
- # When a content-addressable file is deleted, it's deleted from all
29
- # registered storages. use #register_read_only_storage to prevent deletion.
30
- #
31
- def register_storage(*storage)
32
- self.storages = Array(storages).push(*storage)
33
- self
34
- end
35
-
36
- # Same as #register_storage, but only forwards read only methods.
37
- def register_read_only_storage(*storage)
38
- read_only = Array(storage).map { |s| ReadOnlyStorage.new(s) }
39
- self.storages = Array(storages).push(*read_only)
40
- self
41
- end
42
-
43
- # Removes all registered storages
44
- def reset
45
- self.storages = []
46
- self
47
- end
48
- end
49
-
50
10
  attr_reader :id
51
11
 
52
12
  # Creates a new content-addressable file wrapper that uses the given id as
@@ -58,212 +18,10 @@ class ContentAddressableFile
58
18
  self.id = id
59
19
  end
60
20
 
61
- # Tries to decode the multihash. This is a good check to see if the given id
62
- # is actually a content-addressable, but also easy to "fake", as the only way
63
- # to be certain that the id is a content addressable is actually getting the
64
- # file and hashing it again.
65
- def decode
66
- @decode ||= Multihashes.decode id
67
- end
68
-
69
- # The #deocode digest as a byte array
70
- def digest
71
- decode[:digest]
72
- end
73
-
74
- # The #decode digest length
75
- def digest_length
76
- decode[:length]
77
- end
78
-
79
- # The #decode hash function
80
- def digest_hash_function
81
- decode[:hash_function]
82
- end
83
-
84
- # Calls `#open` on the storages to open the uploaded file for reading.
85
- # Most storages will return a lazy IO object which dynamically
86
- # retrieves file content from the storage as the object is being read.
87
- #
88
- # If a block is given, the opened IO object is yielded to the block,
89
- # and at the end of the block it's automatically closed. In this case
90
- # the return value of the method is the block return value.
91
- #
92
- # If no block is given, the opened IO object is returned.
93
- #
94
- # @example
95
- #
96
- # content_addressable.open #=> IO object returned by the storage
97
- # content_addressable.read #=> "..."
98
- # content_addressable.close
99
- #
100
- # # or
101
- #
102
- # content_addressable.open { |io| io.read }
103
- # #=> "..."
104
- def open(*args)
105
- return to_io unless block_given?
106
-
107
- begin
108
- @io = pin_storage(:open, id, *args)
109
- yield @io
110
- ensure
111
- @io&.close
112
- @io = nil
113
- end
114
- end
115
-
116
- alias safe_open open
117
-
118
- # Calls `#download` on the storages if the storage that has the file
119
- # implements it, otherwise streams content into a newly created Tempfile.
120
- #
121
- # If the file exists in multiple storages, any that allows download will
122
- # be pinned.
123
- #
124
- # If a block is given, the opened Tempfile object is yielded to the
125
- # block, and at the end of the block it's automatically closed and
126
- # deleted. In this case the return value of the method is the block
127
- # return value.
128
- #
129
- # If no block is given, the opened Tempfile is returned.
130
- #
131
- # content_addressable.download
132
- # #=> #<File:/var/folders/.../20180302-33119-1h1vjbq.jpg>
133
- #
134
- # # or
135
- #
136
- # content_addressable.download { |tempfile| tempfile.read }
137
- # # tempfile is deleted
138
- # #=> "..."
139
- def download(*args)
140
- if any_storage(:respond_to?, :download)
141
- tempfile = pin_storage(:download, id, *args)
142
- else
143
- tempfile = Tempfile.new(['content-addressable', id], binmode: true)
144
- stream(tempfile, *args)
145
- tempfile.open
146
- end
147
-
148
- block_given? ? yield(tempfile) : tempfile
149
- ensure
150
- tempfile.close! if ($ERROR_INFO || block_given?) && tempfile
151
- end
152
-
153
- # Streams uploaded file content into the specified destination. The
154
- # destination object is given directly to `IO.copy_stream`, so it can
155
- # be either a path on disk or an object that responds to `#write`.
156
- #
157
- # If the uploaded file is already opened, it will be simply rewinded
158
- # after streaming finishes. Otherwise the uploaded file is opened and
159
- # then closed after streaming.
160
- #
161
- # content_addressable.stream(StringIO.new)
162
- # # or
163
- # content_addressable.stream("/path/to/destination")
164
- def stream(destination, *args)
165
- if @io
166
- IO.copy_stream(io, destination)
167
- io.rewind
168
- else
169
- safe_open(*args) { |io| IO.copy_stream(io, destination) }
170
- end
171
- end
172
-
173
- # Part of complying to the IO interface. It delegates to the internally
174
- # opened IO object.
175
- def read(*args)
176
- io.read(*args)
177
- end
178
-
179
- # Part of complying to the IO interface. It delegates to the internally
180
- # opened IO object.
181
- def eof?
182
- io.eof?
183
- end
184
-
185
- # Part of complying to the IO interface. It delegates to the internally
186
- # opened IO object.
187
- def rewind
188
- io.rewind
189
- end
190
-
191
- # Part of complying to the IO interface. It delegates to the internally
192
- # opened IO object.
193
- def close
194
- io.close if @io
195
- end
196
-
197
- # Calls `#url` on the storage where the file is first found, forwarding any
198
- # given URL options.
199
- def url(**options)
200
- pin_storage(:url, id, **options)
201
- end
202
-
203
- # Calls `#exists?` on the storages, which checks whether the file exists
204
- # on any of the storages.
205
- def exists?
206
- pin_storage(:exists?, id)
207
- end
208
-
209
- # Calls `#delete` on the storages, which deletes the file from the
210
- # storage.
211
- def delete
212
- all_storages(:delete, id)
213
- end
214
-
215
- # Returns an opened IO object for the uploaded file.
216
- def to_io
217
- io
218
- end
219
-
220
- # Returns true if the other File has the same id
221
- def ==(other)
222
- other.is_a?(self.class) && id == other.id
223
- end
224
- alias eql? ==
225
-
226
- # Enables using File objects as hash keys.
227
- def hash
228
- [id].hash
229
- end
230
-
231
- # Returns the storage that this file was uploaded to.
232
- def storage
233
- pin_storage(:exists?, id) && @pin_storage
234
- end
21
+ include ActsAsUploadedFile
22
+ include SharedInterface
235
23
 
236
24
  private
237
25
 
238
26
  attr_writer :id
239
-
240
- # Returns an opened IO object for the uploaded file by calling `#open`
241
- # on the storage.
242
- def io
243
- @io ||= pin_storage(:open, id)
244
- end
245
-
246
- # rubocop:disable Style/RescueModifier
247
- def all_storages(method, *args)
248
- self.class.storages.map do |storage|
249
- storage.send(method, *args) rescue next
250
- end
251
- end
252
-
253
- def any_storage(method, *args)
254
- self.class.storages.each do |storage|
255
- result = storage.send(method, *args) rescue next
256
- break result if result
257
- end
258
- end
259
-
260
- def pin_storage(method, *args)
261
- @pin_storage = self.class.storages.find do |storage|
262
- storage.send(:exists?, id) rescue next
263
- end
264
-
265
- @pin_storage&.send(method, *args)
266
- end
267
- # rubocop:enable Style/RescueModifier
268
-
269
27
  end
@@ -0,0 +1,220 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'English'
4
+ require 'set'
5
+ require 'content_addressable_file/read_only_storage'
6
+
7
+ class ContentAddressableFile
8
+ module ActsAsUploadedFile
9
+ def self.included(base)
10
+ base.send :include, InstanceMethods
11
+ base.extend ClassMethods
12
+ end
13
+
14
+ module InstanceMethods
15
+ # Calls `#open` on the storages to open the uploaded file for reading.
16
+ # Most storages will return a lazy IO object which dynamically
17
+ # retrieves file content from the storage as the object is being read.
18
+ #
19
+ # If a block is given, the opened IO object is yielded to the block,
20
+ # and at the end of the block it's automatically closed. In this case
21
+ # the return value of the method is the block return value.
22
+ #
23
+ # If no block is given, the opened IO object is returned.
24
+ #
25
+ # @example
26
+ #
27
+ # content_addressable.open #=> IO object returned by the storage
28
+ # content_addressable.read #=> "..."
29
+ # content_addressable.close
30
+ #
31
+ # # or
32
+ #
33
+ # content_addressable.open { |io| io.read }
34
+ # #=> "..."
35
+ def open(*args)
36
+ return to_io unless block_given?
37
+
38
+ begin
39
+ @io = pin_storage(:open, id, *args)
40
+ yield @io
41
+ ensure
42
+ @io&.close
43
+ @io = nil
44
+ end
45
+ end
46
+
47
+ alias safe_open open
48
+
49
+ # Calls `#download` on the storages if the storage that has the file
50
+ # implements it, otherwise streams content into a newly created Tempfile.
51
+ #
52
+ # If the file exists in multiple storages, any that allows download will
53
+ # be pinned.
54
+ #
55
+ # If a block is given, the opened Tempfile object is yielded to the
56
+ # block, and at the end of the block it's automatically closed and
57
+ # deleted. In this case the return value of the method is the block
58
+ # return value.
59
+ #
60
+ # If no block is given, the opened Tempfile is returned.
61
+ #
62
+ # content_addressable.download
63
+ # #=> #<File:/var/folders/.../20180302-33119-1h1vjbq.jpg>
64
+ #
65
+ # # or
66
+ #
67
+ # content_addressable.download { |tempfile| tempfile.read }
68
+ # # tempfile is deleted
69
+ # #=> "..."
70
+ def download(*args)
71
+ if any_storage(:respond_to?, :download)
72
+ tempfile = pin_storage(:download, id, *args)
73
+ else
74
+ tempfile = Tempfile.new(['content-addressable', id], binmode: true)
75
+ stream(tempfile, *args)
76
+ tempfile.open
77
+ end
78
+
79
+ block_given? ? yield(tempfile) : tempfile
80
+ ensure
81
+ tempfile.close! if ($ERROR_INFO || block_given?) && tempfile
82
+ end
83
+
84
+ # Streams uploaded file content into the specified destination. The
85
+ # destination object is given directly to `IO.copy_stream`, so it can
86
+ # be either a path on disk or an object that responds to `#write`.
87
+ #
88
+ # If the uploaded file is already opened, it will be simply rewinded
89
+ # after streaming finishes. Otherwise the uploaded file is opened and
90
+ # then closed after streaming.
91
+ #
92
+ # content_addressable.stream(StringIO.new)
93
+ # # or
94
+ # content_addressable.stream("/path/to/destination")
95
+ def stream(destination, *args)
96
+ if @io
97
+ IO.copy_stream(io, destination)
98
+ io.rewind
99
+ else
100
+ safe_open(*args) { |io| IO.copy_stream(io, destination) }
101
+ end
102
+ end
103
+
104
+ # Part of complying to the IO interface. It delegates to the internally
105
+ # opened IO object.
106
+ def read(*args)
107
+ io.read(*args)
108
+ end
109
+
110
+ # Part of complying to the IO interface. It delegates to the internally
111
+ # opened IO object.
112
+ def eof?
113
+ io.eof?
114
+ end
115
+
116
+ # Part of complying to the IO interface. It delegates to the internally
117
+ # opened IO object.
118
+ def rewind
119
+ io.rewind
120
+ end
121
+
122
+ # Part of complying to the IO interface. It delegates to the internally
123
+ # opened IO object.
124
+ def close
125
+ io.close if @io
126
+ end
127
+
128
+ # Calls `#url` on the storage where the file is first found, forwarding any
129
+ # given URL options.
130
+ def url(**options)
131
+ pin_storage(:url, id, **options)
132
+ end
133
+
134
+ # Calls `#exists?` on the storages, which checks whether the file exists
135
+ # on any of the storages.
136
+ def exists?
137
+ pin_storage(:exists?, id)
138
+ end
139
+
140
+ # Calls `#delete` on the storages, which deletes the file from the
141
+ # storage.
142
+ def delete
143
+ all_storages(:delete, id)
144
+ end
145
+
146
+ # Returns an opened IO object for the uploaded file.
147
+ def to_io
148
+ io
149
+ end
150
+
151
+ # Returns the storage that this file was uploaded to.
152
+ def storage
153
+ pin_storage(:exists?, id) && @pin_storage
154
+ end
155
+
156
+ # Returns an opened IO object for the uploaded file by calling `#open`
157
+ # on the storage.
158
+ def io
159
+ @io ||= pin_storage(:open, id)
160
+ end
161
+
162
+ private
163
+
164
+ # rubocop:disable Style/RescueModifier
165
+ def all_storages(method, *args)
166
+ self.class.storages.map do |storage|
167
+ storage.send(method, *args) rescue next
168
+ end
169
+ end
170
+
171
+ def any_storage(method, *args)
172
+ self.class.storages.each do |storage|
173
+ result = storage.send(method, *args) rescue next
174
+ break result if result
175
+ end
176
+ end
177
+
178
+ def pin_storage(method, *args)
179
+ @pin_storage = self.class.storages.find do |storage|
180
+ storage.send(:exists?, id) rescue next
181
+ end
182
+
183
+ @pin_storage&.send(method, *args)
184
+ end
185
+ # rubocop:enable Style/RescueModifier
186
+ end
187
+
188
+ module ClassMethods
189
+ attr_accessor :storages
190
+
191
+ # Registers a storage to be used with content-addressable file. All shrine
192
+ # storages are supported by default, as is any duck type that responds to
193
+ # Storage#open(content-addressable-hash) and Storage#exists?(hash).
194
+ #
195
+ # Additional functionality needs Storage#url(hash), Storage#delete(hash)
196
+ # and Storage#download(hash).
197
+ #
198
+ # When a content-addressable file is deleted, it's deleted from all
199
+ # registered storages. use #register_read_only_storage to prevent deletion.
200
+ #
201
+ def register_storage(*storage)
202
+ self.storages = Set.new(storages).merge(Array(storage))
203
+ self
204
+ end
205
+
206
+ # Same as #register_storage, but only forwards read only methods.
207
+ def register_read_only_storage(*storage)
208
+ read_only = Array(storage).map { |s| ReadOnlyStorage.new(s) }
209
+ self.storages = Set.new(storages).merge(read_only)
210
+ self
211
+ end
212
+
213
+ # Removes all registered storages
214
+ def reset
215
+ self.storages = Set.new(storages).clear
216
+ self
217
+ end
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ class ContentAddressableFile
6
+ class ReadOnlyStorage
7
+ extend Forwardable
8
+ def_delegators :@storage,
9
+ :exists?, :download, :url, :class, :equal?, :eql?, :hash
10
+
11
+ def initialize(storage)
12
+ @storage = storage
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'multihashes'
4
+
5
+ class ContentAddressableFile
6
+ module SharedInterface
7
+ def content_addressable
8
+ @content_addressable ||= String(id).rpartition('/').last
9
+ end
10
+
11
+ # Tries to decode the multihash. This is a good check to see if the given id
12
+ # is actually a content-addressable, but also easy to "fake", as the only way
13
+ # to be certain that the id is a content addressable is actually getting the
14
+ # file and hashing it again.
15
+ def decode
16
+ @decode ||= Multihashes.decode([content_addressable].pack('H*'))
17
+ end
18
+
19
+ # The #deocode digest as a byte array
20
+ def digest
21
+ decode[:digest]
22
+ end
23
+
24
+ # The #decode digest length
25
+ def digest_length
26
+ decode[:length]
27
+ end
28
+
29
+ # The #decode hash function
30
+ def digest_hash_function
31
+ decode[:hash_function]
32
+ end
33
+
34
+ # Returns true if the other File has the same id
35
+ def ==(other)
36
+ other.respond_to?(:digest) && content_addressable == other.content_addressable
37
+ end
38
+ alias eql? ==
39
+
40
+ # Enables using File objects as hash keys.
41
+ def hash
42
+ [content_addressable].hash
43
+ end
44
+ end
45
+ end
@@ -6,6 +6,8 @@ require 'shrine/plugins/signature'
6
6
  require 'multihashes'
7
7
  require 'digest'
8
8
 
9
+ require 'shrine/plugins/content_addressable/file_methods'
10
+
9
11
  class Shrine
10
12
  module Plugins
11
13
  ##
@@ -75,7 +77,9 @@ class Shrine
75
77
  end
76
78
 
77
79
  def generate_location(io, _)
78
- [opts[:content_addressable_prefix], content_addressable_hex(io)].compact.join('/')
80
+ [opts[:content_addressable_prefix], content_addressable_hex(io)]
81
+ .compact
82
+ .join('/')
79
83
  end
80
84
  end
81
85
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'content_addressable_file'
4
+ require 'content_addressable_file/shared_interface'
5
+
6
+ class Shrine
7
+ module Plugins
8
+ module ContentAddressable
9
+ module FileMethods
10
+ include ContentAddressableFile::SharedInterface
11
+
12
+ def to_content_addressable!
13
+ ContentAddressableFile.register_storage(storage)
14
+ ContentAddressableFile.new(id)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -5,7 +5,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
5
 
6
6
  Gem::Specification.new do |spec|
7
7
  spec.name = 'shrine-content_addressable'
8
- spec.version = '0.3.1'
8
+ spec.version = '0.4.0'
9
9
  spec.authors = ['Derk-Jan Karrenbeld']
10
10
  spec.email = ['derk-jan+github@karrenbeld.info']
11
11
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shrine-content_addressable
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Derk-Jan Karrenbeld
@@ -133,7 +133,11 @@ files:
133
133
  - bin/console
134
134
  - bin/setup
135
135
  - lib/content_addressable_file.rb
136
+ - lib/content_addressable_file/acts_as_uploaded_file.rb
137
+ - lib/content_addressable_file/read_only_storage.rb
138
+ - lib/content_addressable_file/shared_interface.rb
136
139
  - lib/shrine/plugins/content_addressable.rb
140
+ - lib/shrine/plugins/content_addressable/file_methods.rb
137
141
  - shrine-content_addressable.gemspec
138
142
  homepage:
139
143
  licenses: