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 +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:
|