shrine-content_addressable 0.3.1 → 0.4.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
  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: