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 +4 -4
- data/.rubocop.yml +3 -0
- data/README.md +30 -11
- data/lib/content_addressable_file.rb +5 -247
- data/lib/content_addressable_file/acts_as_uploaded_file.rb +220 -0
- data/lib/content_addressable_file/read_only_storage.rb +15 -0
- data/lib/content_addressable_file/shared_interface.rb +45 -0
- data/lib/shrine/plugins/content_addressable.rb +5 -1
- data/lib/shrine/plugins/content_addressable/file_methods.rb +19 -0
- data/shrine-content_addressable.gemspec +1 -1
- metadata +5 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a508236ee58a19f0f50bc8dbbc8f01e769f1f726
|
4
|
+
data.tar.gz: ca1e2593444f15e57671f5553cfe1cd98fad16f9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 977f6d34d1f29027ae121a310e599de195eabe4befde9e5b98a95ee880cb9a5c54131e15a7a649aa8bcd18a21c339b58adbaa3bd54cfa7faae41905a634675d8
|
7
|
+
data.tar.gz: f2e4df13b082e8896ba9178f6faecae1fc20c06b46532561b7daf481335ce370ea5a64f2037b2054ac38e15633818b0a204fe0eed13ce968ee4d8c41ab6db02f
|
data/.rubocop.yml
CHANGED
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
|
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
|
-
|
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
|
-
|
95
|
+
content_addressable = content_addressable_hash
|
79
96
|
|
80
|
-
lookup.open(
|
81
|
-
lookup.exists?(
|
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(
|
85
|
-
lookup.delete(
|
86
|
-
lookup.download(
|
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::
|
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 '
|
4
|
+
require 'set'
|
6
5
|
|
7
|
-
|
8
|
-
|
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
|
-
|
62
|
-
|
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)]
|
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.
|
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.
|
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:
|