dis 1.2.0 → 1.3.1
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/LICENSE +20 -0
- data/README.md +129 -66
- data/lib/dis/errors.rb +8 -0
- data/lib/dis/jobs/change_type.rb +9 -2
- data/lib/dis/jobs/delete.rb +6 -1
- data/lib/dis/jobs/evict.rb +24 -0
- data/lib/dis/jobs/store.rb +8 -1
- data/lib/dis/jobs.rb +1 -0
- data/lib/dis/layer.rb +228 -63
- data/lib/dis/layers.rb +56 -5
- data/lib/dis/model/class_methods.rb +24 -7
- data/lib/dis/model/data.rb +102 -25
- data/lib/dis/model.rb +65 -22
- data/lib/dis/storage.rb +248 -42
- data/lib/dis/validations/data_presence.rb +9 -1
- data/lib/dis/version.rb +1 -1
- data/lib/dis.rb +14 -1
- data/lib/rails/generators/dis/install/templates/initializer.rb +11 -0
- data/lib/tasks/dis.rake +43 -38
- metadata +28 -12
data/lib/dis/storage.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module Dis
|
|
4
4
|
# = Dis Storage
|
|
5
5
|
#
|
|
6
|
-
#
|
|
6
|
+
# Interface for interacting with the storage layers.
|
|
7
7
|
#
|
|
8
8
|
# All queries are scoped by object type, which will default to the table
|
|
9
9
|
# name of the model. Take care to use your own scope if you interact with
|
|
@@ -17,8 +17,13 @@ module Dis
|
|
|
17
17
|
# one writeable, non-delayed layer must exist.
|
|
18
18
|
class Storage
|
|
19
19
|
class << self
|
|
20
|
-
# Returns a hex digest for a given binary. Accepts
|
|
21
|
-
# and Fog models.
|
|
20
|
+
# Returns a hex digest for a given binary. Accepts File/IO objects,
|
|
21
|
+
# strings, and Fog models.
|
|
22
|
+
#
|
|
23
|
+
# @param file [File, IO, String, Fog::Model] the content to digest
|
|
24
|
+
# @yield [hash] if a block is given, yields the hex digest
|
|
25
|
+
# @yieldparam hash [String] the computed SHA1 hex digest
|
|
26
|
+
# @return [String] the SHA1 hex digest
|
|
22
27
|
def file_digest(file)
|
|
23
28
|
hash = case file
|
|
24
29
|
when Fog::Model
|
|
@@ -32,15 +37,25 @@ module Dis
|
|
|
32
37
|
hash
|
|
33
38
|
end
|
|
34
39
|
|
|
35
|
-
# Exposes the layer set
|
|
36
|
-
#
|
|
40
|
+
# Exposes the layer set.
|
|
41
|
+
#
|
|
42
|
+
# @return [Dis::Layers]
|
|
37
43
|
def layers
|
|
38
44
|
@layers ||= Dis::Layers.new
|
|
39
45
|
end
|
|
40
46
|
|
|
41
47
|
# Changes the type of an object. Kicks off a
|
|
42
|
-
#
|
|
48
|
+
# {Dis::Jobs::ChangeType} job if any delayed layers are defined.
|
|
49
|
+
#
|
|
50
|
+
# @param prev_type [String] the current type scope
|
|
51
|
+
# @param new_type [String] the new type scope
|
|
52
|
+
# @param key [String] the content hash
|
|
53
|
+
# @return [String] the content hash
|
|
54
|
+
# @raise [Dis::Errors::NoLayersError] if no writeable immediate
|
|
55
|
+
# layers exist
|
|
56
|
+
# @raise [Dis::Errors::NotFoundError] if the file is not found
|
|
43
57
|
#
|
|
58
|
+
# @example
|
|
44
59
|
# Dis::Storage.change_type("old_things", "new_things", key)
|
|
45
60
|
def change_type(prev_type, new_type, key)
|
|
46
61
|
require_writeable_layers!
|
|
@@ -49,29 +64,37 @@ module Dis
|
|
|
49
64
|
layers.immediate.writeable.each do |layer|
|
|
50
65
|
layer.delete(prev_type, key)
|
|
51
66
|
end
|
|
52
|
-
|
|
53
|
-
Dis::Jobs::ChangeType.perform_later(prev_type, new_type, key)
|
|
54
|
-
end
|
|
67
|
+
enqueue_delayed_jobs(prev_type, new_type, key)
|
|
55
68
|
key
|
|
56
69
|
end
|
|
57
70
|
|
|
58
|
-
# Stores a file and returns a
|
|
59
|
-
#
|
|
71
|
+
# Stores a file and returns a content hash. Kicks off a
|
|
72
|
+
# {Dis::Jobs::Store} job if any delayed layers are defined.
|
|
60
73
|
#
|
|
61
|
-
#
|
|
74
|
+
# @param type [String] the type scope (e.g. table name)
|
|
75
|
+
# @param file [File, IO, String, Fog::Model] the content to store
|
|
76
|
+
# @return [String] the SHA1 content hash
|
|
77
|
+
# @raise [Dis::Errors::NoLayersError] if no writeable immediate
|
|
78
|
+
# layers exist
|
|
79
|
+
#
|
|
80
|
+
# @example
|
|
81
|
+
# hash = Dis::Storage.store("things", File.open("foo.bin"))
|
|
62
82
|
# # => "8843d7f92416211de9ebb963ff4ce28125932878"
|
|
63
83
|
def store(type, file)
|
|
64
84
|
require_writeable_layers!
|
|
65
85
|
hash = store_immediately!(type, file)
|
|
66
|
-
if layers.delayed.writeable.any?
|
|
67
|
-
|
|
68
|
-
end
|
|
86
|
+
Dis::Jobs::Store.perform_later(type, hash) if layers.delayed.writeable.any?
|
|
87
|
+
Dis::Jobs::Evict.perform_later if layers.cache?
|
|
69
88
|
hash
|
|
70
89
|
end
|
|
71
90
|
|
|
72
91
|
# Transfers files from immediate layers to all delayed layers.
|
|
92
|
+
# Called internally by {Dis::Jobs::Store}.
|
|
73
93
|
#
|
|
74
|
-
#
|
|
94
|
+
# @param type [String] the type scope
|
|
95
|
+
# @param hash [String] the content hash
|
|
96
|
+
# @return [void]
|
|
97
|
+
# @raise [Dis::Errors::NotFoundError] if the file is not found
|
|
75
98
|
def delayed_store(type, hash)
|
|
76
99
|
file = get(type, hash)
|
|
77
100
|
layers.delayed.writeable.each do |layer|
|
|
@@ -81,75 +104,171 @@ module Dis
|
|
|
81
104
|
|
|
82
105
|
# Returns true if the file exists in any layer.
|
|
83
106
|
#
|
|
107
|
+
# @param type [String] the type scope
|
|
108
|
+
# @param key [String] the content hash
|
|
109
|
+
# @return [Boolean]
|
|
110
|
+
# @raise [Dis::Errors::NoLayersError] if no layers are configured
|
|
111
|
+
#
|
|
112
|
+
# @example
|
|
84
113
|
# Dis::Storage.exists?("things", key) # => true
|
|
85
114
|
def exists?(type, key)
|
|
86
115
|
require_layers!
|
|
87
116
|
layers.each do |layer|
|
|
88
117
|
return true if layer.exists?(type, key)
|
|
118
|
+
rescue StandardError => e
|
|
119
|
+
report_layer_error(e, layer:, type:, key:)
|
|
89
120
|
end
|
|
90
121
|
false
|
|
91
122
|
end
|
|
92
123
|
|
|
93
|
-
# Retrieves a file from the store.
|
|
124
|
+
# Retrieves a file from the store. If the first layer misses,
|
|
125
|
+
# the file is fetched from the next available layer and
|
|
126
|
+
# backfilled to all immediate layers.
|
|
94
127
|
#
|
|
95
|
-
#
|
|
128
|
+
# @param type [String] the type scope
|
|
129
|
+
# @param key [String] the content hash
|
|
130
|
+
# @return [Fog::Model] the stored file
|
|
131
|
+
# @raise [Dis::Errors::NoLayersError] if no layers are configured
|
|
132
|
+
# @raise [Dis::Errors::NotFoundError] if the file is not found
|
|
133
|
+
# in any layer
|
|
96
134
|
#
|
|
97
|
-
#
|
|
98
|
-
#
|
|
99
|
-
#
|
|
100
|
-
# Returns an instance of Fog::Model.
|
|
135
|
+
# @example
|
|
136
|
+
# file = Dis::Storage.get("things", hash)
|
|
137
|
+
# file.body # => "file contents..."
|
|
101
138
|
def get(type, key)
|
|
102
139
|
require_layers!
|
|
103
|
-
|
|
104
140
|
fetch_count = 0
|
|
105
141
|
result = layers.inject(nil) do |res, layer|
|
|
106
|
-
res
|
|
107
|
-
fetch_count += 1
|
|
108
|
-
layer.get(type, key)
|
|
109
|
-
end.call
|
|
110
|
-
end || raise(Dis::Errors::NotFoundError)
|
|
142
|
+
next res if res
|
|
111
143
|
|
|
112
|
-
|
|
144
|
+
fetch_count += 1
|
|
145
|
+
fetch_from_layer(layer, type, key)
|
|
146
|
+
end || raise(Dis::Errors::NotFoundError)
|
|
147
|
+
backfill!(type, result) if fetch_count > 1
|
|
113
148
|
result
|
|
114
149
|
end
|
|
115
150
|
|
|
116
151
|
# Returns the absolute file path from the first layer that has a
|
|
117
152
|
# local copy, or nil if no layer stores files locally.
|
|
118
153
|
#
|
|
119
|
-
#
|
|
154
|
+
# @param type [String] the type scope
|
|
155
|
+
# @param key [String] the content hash
|
|
156
|
+
# @return [String, nil] the absolute file path, or nil
|
|
157
|
+
# @raise [Dis::Errors::NoLayersError] if no layers are configured
|
|
120
158
|
def file_path(type, key)
|
|
121
159
|
require_layers!
|
|
122
160
|
layers.each do |layer|
|
|
123
161
|
path = layer.file_path(type, key)
|
|
124
162
|
return path if path
|
|
163
|
+
rescue StandardError => e
|
|
164
|
+
report_layer_error(e, layer:, type:, key:)
|
|
125
165
|
end
|
|
126
166
|
nil
|
|
127
167
|
end
|
|
128
168
|
|
|
169
|
+
# Streams the contents of a file into the given file, fetching
|
|
170
|
+
# from the first layer that has it. Backfills faster layers if
|
|
171
|
+
# the content had to be fetched from further down.
|
|
172
|
+
#
|
|
173
|
+
# @param type [String] the type scope
|
|
174
|
+
# @param key [String] the content hash
|
|
175
|
+
# @param file [File] the destination, must respond to +path+
|
|
176
|
+
# @return [File] the file that was written to, positioned at
|
|
177
|
+
# the start
|
|
178
|
+
# @raise [Dis::Errors::NoLayersError] if no layers are configured
|
|
179
|
+
# @raise [Dis::Errors::NotFoundError] if the file is not found
|
|
180
|
+
def get_file(type, key, file)
|
|
181
|
+
require_layers!
|
|
182
|
+
fetch_count = 0
|
|
183
|
+
found = layers.detect do |layer|
|
|
184
|
+
fetch_count += 1
|
|
185
|
+
stream_from_layer(layer, type, key, file)
|
|
186
|
+
end
|
|
187
|
+
raise Dis::Errors::NotFoundError unless found
|
|
188
|
+
|
|
189
|
+
finalize_fetched_file(type, file, fetch_count)
|
|
190
|
+
end
|
|
191
|
+
|
|
129
192
|
# Deletes a file from all layers. Kicks off a
|
|
130
|
-
#
|
|
131
|
-
#
|
|
132
|
-
#
|
|
133
|
-
#
|
|
134
|
-
#
|
|
135
|
-
#
|
|
136
|
-
#
|
|
137
|
-
#
|
|
193
|
+
# {Dis::Jobs::Delete} job if any delayed layers are defined.
|
|
194
|
+
#
|
|
195
|
+
# @param type [String] the type scope
|
|
196
|
+
# @param key [String] the content hash
|
|
197
|
+
# @return [Boolean] true if the file existed in any immediate
|
|
198
|
+
# layer
|
|
199
|
+
# @raise [Dis::Errors::NoLayersError] if no writeable immediate
|
|
200
|
+
# layers exist
|
|
201
|
+
#
|
|
202
|
+
# @example
|
|
203
|
+
# Dis::Storage.delete("things", key) # => true
|
|
204
|
+
# Dis::Storage.delete("things", key) # => false
|
|
138
205
|
def delete(type, key)
|
|
139
206
|
require_writeable_layers!
|
|
140
207
|
deleted = false
|
|
141
208
|
layers.immediate.writeable.each do |layer|
|
|
142
209
|
deleted = true if layer.delete(type, key)
|
|
143
210
|
end
|
|
144
|
-
if layers.delayed.writeable.any?
|
|
145
|
-
Dis::Jobs::Delete.perform_later(type, key)
|
|
146
|
-
end
|
|
211
|
+
Dis::Jobs::Delete.perform_later(type, key) if layers.delayed.writeable.any?
|
|
147
212
|
deleted
|
|
148
213
|
end
|
|
149
214
|
|
|
215
|
+
# Evicts cached files from all cache layers that exceed
|
|
216
|
+
# their size limit. Only evicts files that have been
|
|
217
|
+
# replicated to a non-cache writeable layer.
|
|
218
|
+
#
|
|
219
|
+
# @return [void]
|
|
220
|
+
def evict_caches
|
|
221
|
+
layers.cache.each { |layer| evict_cache(layer) }
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Returns content hashes from the model's table that exist in
|
|
225
|
+
# no non-cache layer.
|
|
226
|
+
#
|
|
227
|
+
# @param model [Class] an ActiveRecord model that includes
|
|
228
|
+
# {Dis::Model}
|
|
229
|
+
# @yield [batch_size] called after each batch is checked
|
|
230
|
+
# @yieldparam batch_size [Integer] the number of keys in the
|
|
231
|
+
# batch
|
|
232
|
+
# @return [Array<String>] content hashes with no backing file
|
|
233
|
+
#
|
|
234
|
+
# @example
|
|
235
|
+
# Dis::Storage.missing_keys(Image)
|
|
236
|
+
def missing_keys(model)
|
|
237
|
+
attr = model.dis_attributes[:content_hash]
|
|
238
|
+
missing = []
|
|
239
|
+
|
|
240
|
+
model.where.not(attr => nil).in_batches(of: 200) do |batch|
|
|
241
|
+
keys = batch.pluck(attr)
|
|
242
|
+
missing.concat(uncovered_keys(keys.uniq, model.dis_type))
|
|
243
|
+
yield keys.size if block_given?
|
|
244
|
+
end
|
|
245
|
+
missing.uniq
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Returns a hash of layer => orphaned content hashes for files
|
|
249
|
+
# that exist in storage but have no matching database record.
|
|
250
|
+
#
|
|
251
|
+
# @param model [Class] an ActiveRecord model that includes
|
|
252
|
+
# {Dis::Model}
|
|
253
|
+
# @return [Hash{Dis::Layer => Array<String>}] orphaned content
|
|
254
|
+
# hashes per layer
|
|
255
|
+
#
|
|
256
|
+
# @example
|
|
257
|
+
# Dis::Storage.orphaned_keys(Image)
|
|
258
|
+
def orphaned_keys(model)
|
|
259
|
+
layers.non_cache.each_with_object({}) do |layer, result|
|
|
260
|
+
orphans = layer_orphans(layer, model.dis_type, model,
|
|
261
|
+
model.dis_attributes[:content_hash])
|
|
262
|
+
result[layer] = orphans if orphans.any?
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
150
266
|
# Deletes content from all delayed layers.
|
|
267
|
+
# Called internally by {Dis::Jobs::Delete}.
|
|
151
268
|
#
|
|
152
|
-
#
|
|
269
|
+
# @param type [String] the type scope
|
|
270
|
+
# @param key [String] the content hash
|
|
271
|
+
# @return [void]
|
|
153
272
|
def delayed_delete(type, key)
|
|
154
273
|
layers.delayed.writeable.each do |layer|
|
|
155
274
|
layer.delete(type, key)
|
|
@@ -158,6 +277,85 @@ module Dis
|
|
|
158
277
|
|
|
159
278
|
private
|
|
160
279
|
|
|
280
|
+
def enqueue_delayed_jobs(prev_type, new_type, key)
|
|
281
|
+
if layers.delayed.writeable.any?
|
|
282
|
+
Dis::Jobs::ChangeType.perform_later(
|
|
283
|
+
prev_type, new_type, key
|
|
284
|
+
)
|
|
285
|
+
end
|
|
286
|
+
Dis::Jobs::Evict.perform_later if layers.cache?
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def uncovered_keys(keys, type)
|
|
290
|
+
remaining = keys.dup
|
|
291
|
+
layers.non_cache.each do |layer|
|
|
292
|
+
break if remaining.empty?
|
|
293
|
+
|
|
294
|
+
remaining -= layer.existing(type, remaining)
|
|
295
|
+
end
|
|
296
|
+
remaining
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def layer_orphans(layer, type, model, attr)
|
|
300
|
+
stored = layer.stored_keys(type)
|
|
301
|
+
return [] if stored.empty?
|
|
302
|
+
|
|
303
|
+
referenced = model.where(attr => stored).pluck(attr)
|
|
304
|
+
stored - referenced
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def evict_cache(layer)
|
|
308
|
+
return if layer.size <= layer.max_size
|
|
309
|
+
|
|
310
|
+
current_size = layer.size
|
|
311
|
+
layer.cached_files.each do |entry|
|
|
312
|
+
break if current_size <= layer.max_size
|
|
313
|
+
|
|
314
|
+
next unless replicated?(entry[:type], entry[:key])
|
|
315
|
+
|
|
316
|
+
layer.delete(entry[:type], entry[:key])
|
|
317
|
+
current_size -= entry[:size]
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def replicated?(type, key)
|
|
322
|
+
layers.non_cache.writeable.any? do |l|
|
|
323
|
+
l.exists?(type, key)
|
|
324
|
+
rescue StandardError => e
|
|
325
|
+
report_layer_error(e, layer: l, type:, key:)
|
|
326
|
+
false
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def finalize_fetched_file(type, file, fetch_count)
|
|
331
|
+
file.flush
|
|
332
|
+
backfill!(type, file) if fetch_count > 1
|
|
333
|
+
file.rewind
|
|
334
|
+
file
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def stream_from_layer(layer, type, key, file)
|
|
338
|
+
file.truncate(0)
|
|
339
|
+
file.rewind
|
|
340
|
+
layer.stream(type, key, file)
|
|
341
|
+
rescue StandardError => e
|
|
342
|
+
report_layer_error(e, layer:, type:, key:)
|
|
343
|
+
false
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def fetch_from_layer(layer, type, key)
|
|
347
|
+
layer.get(type, key)
|
|
348
|
+
rescue StandardError => e
|
|
349
|
+
report_layer_error(e, layer:, type:, key:)
|
|
350
|
+
nil
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def backfill!(type, file)
|
|
354
|
+
store_immediately!(type, file)
|
|
355
|
+
rescue StandardError => e
|
|
356
|
+
report_layer_error(e, type:)
|
|
357
|
+
end
|
|
358
|
+
|
|
161
359
|
def store_immediately!(type, file)
|
|
162
360
|
file_digest(file) do |hash|
|
|
163
361
|
layers.immediate.writeable.each do |layer|
|
|
@@ -174,6 +372,14 @@ module Dis
|
|
|
174
372
|
raise Dis::Errors::NoLayersError unless layers.immediate.writeable.any?
|
|
175
373
|
end
|
|
176
374
|
|
|
375
|
+
def report_layer_error(err, layer: nil, type: nil, key: nil)
|
|
376
|
+
Rails.error.report(
|
|
377
|
+
err, handled: true,
|
|
378
|
+
severity: :warning,
|
|
379
|
+
context: { layer: layer&.name, type:, key: }
|
|
380
|
+
)
|
|
381
|
+
end
|
|
382
|
+
|
|
177
383
|
def digest
|
|
178
384
|
Digest::SHA1
|
|
179
385
|
end
|
|
@@ -4,9 +4,17 @@ module Dis
|
|
|
4
4
|
module Validations
|
|
5
5
|
# = Dis Data Presence Validation
|
|
6
6
|
#
|
|
7
|
+
# Validates that data has been assigned to a {Dis::Model} record.
|
|
8
|
+
# Empty strings are treated as missing data.
|
|
9
|
+
#
|
|
10
|
+
# @see Dis::Model::ClassMethods#validates_data_presence
|
|
7
11
|
class DataPresence < ActiveModel::Validator
|
|
8
12
|
# Validates that a record has data, either freshly assigned or
|
|
9
|
-
# persisted in the storage. Adds a
|
|
13
|
+
# persisted in the storage. Adds a +:blank+ error on +:data+
|
|
14
|
+
# if not.
|
|
15
|
+
#
|
|
16
|
+
# @param record [ActiveRecord::Base]
|
|
17
|
+
# @return [void]
|
|
10
18
|
def validate(record)
|
|
11
19
|
return if record.data? && record.content_hash != self.class.empty_hash
|
|
12
20
|
|
data/lib/dis/version.rb
CHANGED
data/lib/dis.rb
CHANGED
|
@@ -5,7 +5,7 @@ require "digest/sha1"
|
|
|
5
5
|
require "fog/core"
|
|
6
6
|
require "fog/local"
|
|
7
7
|
require "active_job"
|
|
8
|
-
require "
|
|
8
|
+
require "concurrent"
|
|
9
9
|
require "dis/engine"
|
|
10
10
|
require "dis/errors"
|
|
11
11
|
require "dis/jobs"
|
|
@@ -16,5 +16,18 @@ require "dis/model"
|
|
|
16
16
|
require "dis/storage"
|
|
17
17
|
require "dis/validations"
|
|
18
18
|
|
|
19
|
+
# Dis is a content-addressable store for file uploads in Rails.
|
|
20
|
+
#
|
|
21
|
+
# Files are stored as binary blobs keyed by the SHA1 digest of their
|
|
22
|
+
# contents, enabling automatic deduplication. Storage is organized in
|
|
23
|
+
# layers (see {Dis::Layer}) that can target local disk or any cloud
|
|
24
|
+
# provider supported by Fog.
|
|
25
|
+
#
|
|
26
|
+
# Include {Dis::Model} in an ActiveRecord model to get started, and
|
|
27
|
+
# configure layers via {Dis::Storage.layers}.
|
|
28
|
+
#
|
|
29
|
+
# @see Dis::Model
|
|
30
|
+
# @see Dis::Storage
|
|
31
|
+
# @see Dis::Layer
|
|
19
32
|
module Dis
|
|
20
33
|
end
|
|
@@ -9,6 +9,17 @@ Dis::Storage.layers << Dis::Layer.new(
|
|
|
9
9
|
path: Rails.env
|
|
10
10
|
)
|
|
11
11
|
|
|
12
|
+
# You can also use a cache layer with bounded storage and LRU eviction:
|
|
13
|
+
|
|
14
|
+
# Dis::Storage.layers << Dis::Layer.new(
|
|
15
|
+
# Fog::Storage.new(
|
|
16
|
+
# provider: "Local",
|
|
17
|
+
# local_root: Rails.root.join("tmp/dis")
|
|
18
|
+
# ),
|
|
19
|
+
# path: Rails.env,
|
|
20
|
+
# cache: 1.gigabyte
|
|
21
|
+
# )
|
|
22
|
+
|
|
12
23
|
# You can also add cloud storage:
|
|
13
24
|
|
|
14
25
|
# require 'fog/aws/storage'
|
data/lib/tasks/dis.rake
CHANGED
|
@@ -1,58 +1,63 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "ruby-progressbar"
|
|
4
|
+
|
|
3
5
|
namespace :dis do
|
|
4
|
-
desc "
|
|
5
|
-
task
|
|
6
|
+
desc "List records with no backing file in any storage layer"
|
|
7
|
+
task missing: :environment do
|
|
6
8
|
unless ENV["MODELS"]
|
|
7
|
-
puts "Usage: #{$PROGRAM_NAME} dis:
|
|
8
|
-
"MODELS=Avatar,Document"
|
|
9
|
+
puts "Usage: #{$PROGRAM_NAME} dis:missing MODELS=Avatar,Document"
|
|
9
10
|
exit
|
|
10
11
|
end
|
|
11
12
|
|
|
12
13
|
models = ENV["MODELS"].split(",").map(&:strip).map(&:constantize)
|
|
13
14
|
|
|
14
|
-
jobs = Set.new
|
|
15
|
-
|
|
16
15
|
models.each do |model|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
16
|
+
bar = ProgressBar.create(
|
|
17
|
+
title: model.name,
|
|
18
|
+
total: model.where.not(
|
|
19
|
+
model.dis_attributes[:content_hash] => nil
|
|
20
|
+
).count,
|
|
21
|
+
format: "%t: |%B| %c/%C records"
|
|
22
|
+
)
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
missing = objects - existing
|
|
30
|
-
global_missing -= existing
|
|
31
|
-
puts "#{existing.length} existing, #{missing.length} missing" +
|
|
32
|
-
(layer.readonly? ? " (read-only)" : "")
|
|
33
|
-
|
|
34
|
-
next unless layer.delayed? && !layer.readonly?
|
|
35
|
-
|
|
36
|
-
jobs += (missing - global_missing).pmap do |hash|
|
|
37
|
-
[model.dis_type, hash]
|
|
38
|
-
end.compact
|
|
24
|
+
missing = ActiveRecord::Base.logger.silence do
|
|
25
|
+
Dis::Storage.missing_keys(model) do |count|
|
|
26
|
+
bar.progress += count
|
|
27
|
+
end
|
|
39
28
|
end
|
|
29
|
+
bar.finish
|
|
40
30
|
|
|
41
|
-
if
|
|
42
|
-
puts "
|
|
43
|
-
|
|
31
|
+
if missing.any?
|
|
32
|
+
puts "#{missing.length} missing:"
|
|
33
|
+
missing.each { |key| puts " #{key}" }
|
|
34
|
+
else
|
|
35
|
+
puts "0 missing"
|
|
44
36
|
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
45
39
|
|
|
46
|
-
|
|
40
|
+
desc "List stored files with no matching database record"
|
|
41
|
+
task orphaned: :environment do
|
|
42
|
+
unless ENV["MODELS"]
|
|
43
|
+
puts "Usage: #{$PROGRAM_NAME} dis:orphaned MODELS=Avatar,Document"
|
|
44
|
+
exit
|
|
47
45
|
end
|
|
48
46
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
47
|
+
models = ENV["MODELS"].split(",").map(&:strip).map(&:constantize)
|
|
48
|
+
|
|
49
|
+
models.each do |model|
|
|
50
|
+
orphans = ActiveRecord::Base.logger.silence do
|
|
51
|
+
Dis::Storage.orphaned_keys(model)
|
|
52
|
+
end
|
|
53
|
+
if orphans.any?
|
|
54
|
+
orphans.each do |layer, keys|
|
|
55
|
+
puts "#{model.name} (#{layer.name}): " \
|
|
56
|
+
"#{keys.length} orphaned"
|
|
57
|
+
keys.each { |key| puts " #{key}" }
|
|
58
|
+
end
|
|
59
|
+
else
|
|
60
|
+
puts "#{model.name}: 0 orphaned"
|
|
56
61
|
end
|
|
57
62
|
end
|
|
58
63
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dis
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Inge Jørgensen
|
|
@@ -23,6 +23,20 @@ dependencies:
|
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: concurrent-ruby
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.1'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '1.1'
|
|
26
40
|
- !ruby/object:Gem::Dependency
|
|
27
41
|
name: fog-core
|
|
28
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -58,33 +72,33 @@ dependencies:
|
|
|
58
72
|
- !ruby/object:Gem::Version
|
|
59
73
|
version: '0'
|
|
60
74
|
- !ruby/object:Gem::Dependency
|
|
61
|
-
name:
|
|
75
|
+
name: rails
|
|
62
76
|
requirement: !ruby/object:Gem::Requirement
|
|
63
77
|
requirements:
|
|
64
|
-
- - "
|
|
78
|
+
- - ">="
|
|
65
79
|
- !ruby/object:Gem::Version
|
|
66
|
-
version:
|
|
80
|
+
version: '7.1'
|
|
67
81
|
type: :runtime
|
|
68
82
|
prerelease: false
|
|
69
83
|
version_requirements: !ruby/object:Gem::Requirement
|
|
70
84
|
requirements:
|
|
71
|
-
- - "
|
|
85
|
+
- - ">="
|
|
72
86
|
- !ruby/object:Gem::Version
|
|
73
|
-
version:
|
|
87
|
+
version: '7.1'
|
|
74
88
|
- !ruby/object:Gem::Dependency
|
|
75
|
-
name:
|
|
89
|
+
name: ruby-progressbar
|
|
76
90
|
requirement: !ruby/object:Gem::Requirement
|
|
77
91
|
requirements:
|
|
78
|
-
- - "
|
|
92
|
+
- - "~>"
|
|
79
93
|
- !ruby/object:Gem::Version
|
|
80
|
-
version: '
|
|
94
|
+
version: '1.11'
|
|
81
95
|
type: :runtime
|
|
82
96
|
prerelease: false
|
|
83
97
|
version_requirements: !ruby/object:Gem::Requirement
|
|
84
98
|
requirements:
|
|
85
|
-
- - "
|
|
99
|
+
- - "~>"
|
|
86
100
|
- !ruby/object:Gem::Version
|
|
87
|
-
version: '
|
|
101
|
+
version: '1.11'
|
|
88
102
|
description: Dis is a Rails plugin that stores your file uploads and other binary
|
|
89
103
|
blobs.
|
|
90
104
|
email:
|
|
@@ -93,6 +107,7 @@ executables: []
|
|
|
93
107
|
extensions: []
|
|
94
108
|
extra_rdoc_files: []
|
|
95
109
|
files:
|
|
110
|
+
- LICENSE
|
|
96
111
|
- README.md
|
|
97
112
|
- lib/dis.rb
|
|
98
113
|
- lib/dis/engine.rb
|
|
@@ -100,6 +115,7 @@ files:
|
|
|
100
115
|
- lib/dis/jobs.rb
|
|
101
116
|
- lib/dis/jobs/change_type.rb
|
|
102
117
|
- lib/dis/jobs/delete.rb
|
|
118
|
+
- lib/dis/jobs/evict.rb
|
|
103
119
|
- lib/dis/jobs/store.rb
|
|
104
120
|
- lib/dis/layer.rb
|
|
105
121
|
- lib/dis/layers.rb
|
|
@@ -134,7 +150,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
134
150
|
- !ruby/object:Gem::Version
|
|
135
151
|
version: '0'
|
|
136
152
|
requirements: []
|
|
137
|
-
rubygems_version: 4.0.
|
|
153
|
+
rubygems_version: 4.0.10
|
|
138
154
|
specification_version: 4
|
|
139
155
|
summary: A file store for your Rails app
|
|
140
156
|
test_files: []
|