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/layer.rb
CHANGED
|
@@ -3,52 +3,47 @@
|
|
|
3
3
|
module Dis
|
|
4
4
|
# = Dis Layer
|
|
5
5
|
#
|
|
6
|
-
# Represents a layer of storage.
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# ==== Options
|
|
10
|
-
#
|
|
11
|
-
# * <tt>:delayed</tt> - Delayed layers will be processed outside of
|
|
12
|
-
# the request cycle by ActiveJob.
|
|
13
|
-
# * <tt>:readonly</tt> - Readonly layers can only be read from,
|
|
14
|
-
# not written to.
|
|
15
|
-
# * <tt>:public</tt> - Objects stored in public layers will have the
|
|
16
|
-
# public readable flag set if supported by the storage provider.
|
|
17
|
-
# * <tt>:path</tt> - Directory name to use for the store. For Amazon S3,
|
|
18
|
-
# this will be the name of the bucket.
|
|
19
|
-
#
|
|
20
|
-
# ==== Examples
|
|
21
|
-
#
|
|
22
|
-
# This creates a local storage layer. It's a good idea to have a local layer
|
|
23
|
-
# first, this provides you with a cache on disk that will be faster than
|
|
24
|
-
# reading from the cloud.
|
|
6
|
+
# Represents a layer of storage. Wraps a +Fog::Storage+ connection;
|
|
7
|
+
# any provider supported by Fog should be usable.
|
|
25
8
|
#
|
|
9
|
+
# @example Local storage layer
|
|
26
10
|
# Dis::Layer.new(
|
|
27
|
-
# Fog::Storage.new(
|
|
28
|
-
# provider:
|
|
29
|
-
# local_root: Rails.root.join(
|
|
30
|
-
#
|
|
11
|
+
# Fog::Storage.new(
|
|
12
|
+
# provider: "Local",
|
|
13
|
+
# local_root: Rails.root.join("db/dis")
|
|
14
|
+
# ),
|
|
31
15
|
# path: Rails.env
|
|
32
16
|
# )
|
|
33
17
|
#
|
|
34
|
-
#
|
|
35
|
-
# and transfer content from one of the immediate layers later at it's
|
|
36
|
-
# leisure.
|
|
37
|
-
#
|
|
18
|
+
# @example Delayed layer on Amazon S3
|
|
38
19
|
# Dis::Layer.new(
|
|
39
|
-
# Fog::Storage.new(
|
|
40
|
-
# provider:
|
|
41
|
-
# aws_access_key_id:
|
|
20
|
+
# Fog::Storage.new(
|
|
21
|
+
# provider: "AWS",
|
|
22
|
+
# aws_access_key_id: YOUR_AWS_ACCESS_KEY_ID,
|
|
42
23
|
# aws_secret_access_key: YOUR_AWS_SECRET_ACCESS_KEY
|
|
43
|
-
#
|
|
24
|
+
# ),
|
|
44
25
|
# path: "my_bucket",
|
|
45
26
|
# delayed: true
|
|
46
27
|
# )
|
|
47
28
|
class Layer
|
|
48
29
|
include Dis::Logging
|
|
49
30
|
|
|
31
|
+
# @return [Fog::Storage] the underlying Fog connection
|
|
50
32
|
attr_reader :connection
|
|
51
33
|
|
|
34
|
+
# @param connection [Fog::Storage] a Fog storage connection
|
|
35
|
+
# @param options [Hash] layer configuration options
|
|
36
|
+
# @option options [Boolean] :delayed (false) process writes
|
|
37
|
+
# asynchronously via ActiveJob
|
|
38
|
+
# @option options [Boolean] :readonly (false) only allow reads
|
|
39
|
+
# @option options [Boolean] :public (false) set the public
|
|
40
|
+
# readable flag on stored objects (provider-dependent)
|
|
41
|
+
# @option options [String] :path (nil) directory or bucket name
|
|
42
|
+
# @option options [Integer, false] :cache (false) enable bounded
|
|
43
|
+
# cache with this soft size limit in bytes. Cannot be combined
|
|
44
|
+
# with +:delayed+ or +:readonly+
|
|
45
|
+
# @raise [ArgumentError] if +:cache+ is combined with +:delayed+
|
|
46
|
+
# or +:readonly+
|
|
52
47
|
def initialize(connection, options = {})
|
|
53
48
|
options = default_options.merge(options)
|
|
54
49
|
@connection = connection
|
|
@@ -56,44 +51,102 @@ module Dis
|
|
|
56
51
|
@readonly = options[:readonly]
|
|
57
52
|
@public = options[:public]
|
|
58
53
|
@path = options[:path]
|
|
54
|
+
@cache = options[:cache]
|
|
55
|
+
validate_cache_options!
|
|
59
56
|
end
|
|
60
57
|
|
|
61
58
|
# Returns true if the layer is a delayed layer.
|
|
59
|
+
#
|
|
60
|
+
# @return [Boolean]
|
|
62
61
|
def delayed?
|
|
63
62
|
@delayed
|
|
64
63
|
end
|
|
65
64
|
|
|
66
65
|
# Returns true if the layer isn't a delayed layer.
|
|
66
|
+
#
|
|
67
|
+
# @return [Boolean]
|
|
67
68
|
def immediate?
|
|
68
69
|
!delayed?
|
|
69
70
|
end
|
|
70
71
|
|
|
71
72
|
# Returns true if the layer is public.
|
|
73
|
+
#
|
|
74
|
+
# @return [Boolean]
|
|
72
75
|
def public?
|
|
73
76
|
@public
|
|
74
77
|
end
|
|
75
78
|
|
|
76
79
|
# Returns true if the layer is read only.
|
|
80
|
+
#
|
|
81
|
+
# @return [Boolean]
|
|
77
82
|
def readonly?
|
|
78
83
|
@readonly
|
|
79
84
|
end
|
|
80
85
|
|
|
81
86
|
# Returns true if the layer is writeable.
|
|
87
|
+
#
|
|
88
|
+
# @return [Boolean]
|
|
82
89
|
def writeable?
|
|
83
90
|
!readonly?
|
|
84
91
|
end
|
|
85
92
|
|
|
86
|
-
#
|
|
93
|
+
# Returns true if the layer is a cache layer.
|
|
87
94
|
#
|
|
88
|
-
#
|
|
89
|
-
|
|
95
|
+
# @return [Boolean]
|
|
96
|
+
def cache?
|
|
97
|
+
!!@cache
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Returns the cache size limit in bytes, or nil if not a cache.
|
|
101
|
+
#
|
|
102
|
+
# @return [Integer, nil]
|
|
103
|
+
def max_size
|
|
104
|
+
@cache if cache?
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Returns the total size in bytes of all files stored locally.
|
|
108
|
+
# Returns 0 for non-local providers.
|
|
90
109
|
#
|
|
91
|
-
#
|
|
92
|
-
|
|
93
|
-
|
|
110
|
+
# @return [Integer]
|
|
111
|
+
def size
|
|
112
|
+
return 0 unless connection.respond_to?(:local_root)
|
|
113
|
+
|
|
114
|
+
root = local_root_path
|
|
115
|
+
return 0 unless root.exist?
|
|
116
|
+
|
|
117
|
+
root.glob("**/*").sum { |f| f.file? ? f.size : 0 }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Returns cached file entries sorted by mtime ascending
|
|
121
|
+
# (oldest first).
|
|
94
122
|
#
|
|
95
|
-
#
|
|
96
|
-
#
|
|
123
|
+
# @return [Array<Hash>] each entry has keys +:path+
|
|
124
|
+
# (Pathname), +:type+ (String), +:key+ (String), +:mtime+
|
|
125
|
+
# (Time), +:size+ (Integer)
|
|
126
|
+
def cached_files
|
|
127
|
+
return [] unless connection.respond_to?(:local_root)
|
|
128
|
+
|
|
129
|
+
root = local_root_path
|
|
130
|
+
return [] unless root.exist?
|
|
131
|
+
|
|
132
|
+
entries = root.glob("**/*").select(&:file?)
|
|
133
|
+
entries.filter_map { |f| cached_file_entry(f, root) }
|
|
134
|
+
.sort_by { |e| e[:mtime] }
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Stores a file. The key must be a hex digest of the file
|
|
138
|
+
# content. If an object with the supplied hash already exists,
|
|
139
|
+
# no action will be performed.
|
|
140
|
+
#
|
|
141
|
+
# @param type [String] the type scope
|
|
142
|
+
# @param key [String] the content hash
|
|
143
|
+
# @param file [File, IO, String, Fog::Model] the content
|
|
144
|
+
# @return [Fog::Model] the stored file
|
|
145
|
+
# @raise [Dis::Errors::ReadOnlyError] if the layer is readonly
|
|
146
|
+
#
|
|
147
|
+
# @example
|
|
148
|
+
# key = Digest::SHA1.file(file.path).hexdigest
|
|
149
|
+
# layer.store("documents", key, file)
|
|
97
150
|
def store(type, key, file)
|
|
98
151
|
raise Dis::Errors::ReadOnlyError if readonly?
|
|
99
152
|
|
|
@@ -104,18 +157,42 @@ module Dis
|
|
|
104
157
|
|
|
105
158
|
# Returns all the given keys that exist in the layer.
|
|
106
159
|
#
|
|
107
|
-
#
|
|
160
|
+
# @param type [String] the type scope
|
|
161
|
+
# @param keys [Array<String>] content hashes to check
|
|
162
|
+
# @return [Array<String>] the subset of keys that exist
|
|
108
163
|
def existing(type, keys)
|
|
109
164
|
return [] if keys.empty?
|
|
110
165
|
|
|
111
|
-
|
|
166
|
+
futures = keys.map do |key|
|
|
167
|
+
Concurrent::Promises.future { key if exists?(type, key) }
|
|
168
|
+
end
|
|
169
|
+
futures.filter_map(&:value!)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Returns all content hashes stored under the given type.
|
|
173
|
+
#
|
|
174
|
+
# @param type [String] the type scope
|
|
175
|
+
# @return [Array<String>] content hashes
|
|
176
|
+
def stored_keys(type)
|
|
177
|
+
dir = connection.directories.get(path || "")
|
|
178
|
+
return [] unless dir
|
|
179
|
+
|
|
180
|
+
prefix = "#{type}/"
|
|
181
|
+
dir.files.filter_map do |file|
|
|
182
|
+
next unless file.key.start_with?(prefix)
|
|
112
183
|
|
|
113
|
-
|
|
184
|
+
parts = file.key.delete_prefix(prefix).split("/")
|
|
185
|
+
next unless parts.length == 2
|
|
186
|
+
|
|
187
|
+
"#{parts[0]}#{parts[1]}"
|
|
188
|
+
end
|
|
114
189
|
end
|
|
115
190
|
|
|
116
|
-
# Returns true if
|
|
191
|
+
# Returns true if an object with the given key exists.
|
|
117
192
|
#
|
|
118
|
-
#
|
|
193
|
+
# @param type [String] the type scope
|
|
194
|
+
# @param key [String] the content hash
|
|
195
|
+
# @return [Boolean]
|
|
119
196
|
def exists?(type, key)
|
|
120
197
|
if directory(type, key)&.files&.head(key_component(type, key))
|
|
121
198
|
true
|
|
@@ -126,37 +203,64 @@ module Dis
|
|
|
126
203
|
|
|
127
204
|
# Retrieves a file from the store.
|
|
128
205
|
#
|
|
129
|
-
#
|
|
206
|
+
# @param type [String] the type scope
|
|
207
|
+
# @param key [String] the content hash
|
|
208
|
+
# @return [Fog::Model, nil] the file, or nil if not found
|
|
130
209
|
def get(type, key)
|
|
131
210
|
dir = directory(type, key)
|
|
132
211
|
return unless dir
|
|
133
212
|
|
|
134
|
-
debug_log("Get #{type}/#{key} from #{name}") do
|
|
213
|
+
result = debug_log("Get #{type}/#{key} from #{name}") do
|
|
135
214
|
dir.files.get(key_component(type, key))
|
|
136
215
|
end
|
|
216
|
+
refresh_cache_mtime(local_file_path(type, key)) if result && cache?
|
|
217
|
+
result
|
|
137
218
|
end
|
|
138
219
|
|
|
139
|
-
#
|
|
140
|
-
#
|
|
220
|
+
# Writes the contents of a file into the given IO. Local layers
|
|
221
|
+
# copy from disk; remote layers stream in chunks, never holding
|
|
222
|
+
# the whole body in memory.
|
|
141
223
|
#
|
|
142
|
-
#
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
224
|
+
# @param type [String] the type scope
|
|
225
|
+
# @param key [String] the content hash
|
|
226
|
+
# @param io [IO] the destination to write to
|
|
227
|
+
# @return [Boolean] true if the file was found and written
|
|
228
|
+
def stream(type, key, io)
|
|
229
|
+
local = local_file_path(type, key)
|
|
230
|
+
if local
|
|
231
|
+
copy_local(local, io)
|
|
232
|
+
return true
|
|
233
|
+
end
|
|
146
234
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
235
|
+
dir = directory(type, key)
|
|
236
|
+
return false unless dir
|
|
237
|
+
|
|
238
|
+
debug_log("Stream #{type}/#{key} from #{name}") do
|
|
239
|
+
!fetch_chunks(dir, type, key, io).nil?
|
|
240
|
+
end
|
|
152
241
|
end
|
|
153
242
|
|
|
154
|
-
#
|
|
243
|
+
# Returns the absolute file path for a locally stored file, or
|
|
244
|
+
# nil if the provider is not local or the file does not exist.
|
|
245
|
+
# On cache layers this counts as a read and may refresh the
|
|
246
|
+
# file's mtime.
|
|
155
247
|
#
|
|
156
|
-
#
|
|
248
|
+
# @param type [String] the type scope
|
|
249
|
+
# @param key [String] the content hash
|
|
250
|
+
# @return [String, nil]
|
|
251
|
+
def file_path(type, key)
|
|
252
|
+
path = local_file_path(type, key)
|
|
253
|
+
refresh_cache_mtime(path) if cache?
|
|
254
|
+
path
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Deletes a file from the store.
|
|
157
258
|
#
|
|
158
|
-
#
|
|
159
|
-
#
|
|
259
|
+
# @param type [String] the type scope
|
|
260
|
+
# @param key [String] the content hash
|
|
261
|
+
# @return [Boolean] true if the file was deleted, false if not
|
|
262
|
+
# found
|
|
263
|
+
# @raise [Dis::Errors::ReadOnlyError] if the layer is readonly
|
|
160
264
|
def delete(type, key)
|
|
161
265
|
raise Dis::Errors::ReadOnlyError if readonly?
|
|
162
266
|
|
|
@@ -167,6 +271,9 @@ module Dis
|
|
|
167
271
|
|
|
168
272
|
# Returns a name for the layer.
|
|
169
273
|
#
|
|
274
|
+
# @return [String]
|
|
275
|
+
#
|
|
276
|
+
# @example
|
|
170
277
|
# layer.name # => "Fog::Storage::Local::Real/development"
|
|
171
278
|
def name
|
|
172
279
|
"#{connection.class.name}/#{path}"
|
|
@@ -175,7 +282,65 @@ module Dis
|
|
|
175
282
|
private
|
|
176
283
|
|
|
177
284
|
def default_options
|
|
178
|
-
{ delayed: false, readonly: false, public: false,
|
|
285
|
+
{ delayed: false, readonly: false, public: false,
|
|
286
|
+
path: nil, cache: false }
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def validate_cache_options!
|
|
290
|
+
return unless cache?
|
|
291
|
+
|
|
292
|
+
if delayed?
|
|
293
|
+
raise ArgumentError,
|
|
294
|
+
"cache layers cannot be delayed"
|
|
295
|
+
end
|
|
296
|
+
return unless readonly?
|
|
297
|
+
|
|
298
|
+
raise ArgumentError,
|
|
299
|
+
"cache layers cannot be readonly"
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def local_root_path
|
|
303
|
+
root = Pathname.new(connection.local_root)
|
|
304
|
+
path ? root.join(path) : root
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def cached_file_entry(file, root)
|
|
308
|
+
parts = file.relative_path_from(root).to_s.split("/")
|
|
309
|
+
return unless parts.length == 3
|
|
310
|
+
|
|
311
|
+
{ path: file, type: parts[0], key: parts[1] + parts[2],
|
|
312
|
+
mtime: file.mtime, size: file.size }
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def local_file_path(type, key)
|
|
316
|
+
return unless connection.respond_to?(:local_root)
|
|
317
|
+
return unless exists?(type, key)
|
|
318
|
+
|
|
319
|
+
File.join(
|
|
320
|
+
connection.local_root,
|
|
321
|
+
directory_component(type, key),
|
|
322
|
+
key_component(type, key)
|
|
323
|
+
)
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def refresh_cache_mtime(path)
|
|
327
|
+
return unless path
|
|
328
|
+
return unless File.mtime(path) < 1.minute.ago
|
|
329
|
+
|
|
330
|
+
FileUtils.touch(path)
|
|
331
|
+
rescue Errno::ENOENT
|
|
332
|
+
nil
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def copy_local(path, io)
|
|
336
|
+
refresh_cache_mtime(path) if cache?
|
|
337
|
+
IO.copy_stream(path, io)
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def fetch_chunks(dir, type, key, io)
|
|
341
|
+
dir.files.get(key_component(type, key)) do |chunk, _rem, _total|
|
|
342
|
+
io.write(chunk)
|
|
343
|
+
end
|
|
179
344
|
end
|
|
180
345
|
|
|
181
346
|
def directory_component(_type, _key)
|
|
@@ -183,7 +348,7 @@ module Dis
|
|
|
183
348
|
end
|
|
184
349
|
|
|
185
350
|
def key_component(type, key)
|
|
186
|
-
[type, key[0...2], key[2..
|
|
351
|
+
[type, key[0...2], key[2..]].compact.join("/")
|
|
187
352
|
end
|
|
188
353
|
|
|
189
354
|
def delete!(type, key)
|
data/lib/dis/layers.rb
CHANGED
|
@@ -3,10 +3,15 @@
|
|
|
3
3
|
module Dis
|
|
4
4
|
# = Dis Layers
|
|
5
5
|
#
|
|
6
|
-
# Represents a collection of
|
|
6
|
+
# Represents a filterable collection of {Dis::Layer} instances.
|
|
7
|
+
# Supports chained filtering by layer properties.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# Dis::Storage.layers.delayed.writeable.each { |l| ... }
|
|
7
11
|
class Layers
|
|
8
12
|
include Enumerable
|
|
9
13
|
|
|
14
|
+
# @param layers [Array<Dis::Layer>] initial layers
|
|
10
15
|
def initialize(layers = [])
|
|
11
16
|
@layers = layers
|
|
12
17
|
end
|
|
@@ -15,6 +20,8 @@ module Dis
|
|
|
15
20
|
delegate :<<, to: :@layers
|
|
16
21
|
|
|
17
22
|
# Clears all layers from the collection.
|
|
23
|
+
#
|
|
24
|
+
# @return [void]
|
|
18
25
|
def clear!
|
|
19
26
|
@layers = []
|
|
20
27
|
end
|
|
@@ -25,43 +32,87 @@ module Dis
|
|
|
25
32
|
end
|
|
26
33
|
|
|
27
34
|
# Returns a new instance containing only the delayed layers.
|
|
35
|
+
#
|
|
36
|
+
# @return [Dis::Layers]
|
|
28
37
|
def delayed
|
|
29
38
|
self.class.new select(&:delayed?)
|
|
30
39
|
end
|
|
31
40
|
|
|
32
41
|
# Returns true if one or more delayed layers exist.
|
|
42
|
+
#
|
|
43
|
+
# @return [Boolean]
|
|
33
44
|
def delayed?
|
|
34
|
-
|
|
45
|
+
any?(&:delayed?)
|
|
35
46
|
end
|
|
36
47
|
|
|
37
48
|
# Returns a new instance containing only the immediate layers.
|
|
49
|
+
#
|
|
50
|
+
# @return [Dis::Layers]
|
|
38
51
|
def immediate
|
|
39
52
|
self.class.new select(&:immediate?)
|
|
40
53
|
end
|
|
41
54
|
|
|
42
55
|
# Returns true if one or more immediate layers exist.
|
|
56
|
+
#
|
|
57
|
+
# @return [Boolean]
|
|
43
58
|
def immediate?
|
|
44
|
-
|
|
59
|
+
any?(&:immediate?)
|
|
45
60
|
end
|
|
46
61
|
|
|
47
62
|
# Returns a new instance containing only the readonly layers.
|
|
63
|
+
#
|
|
64
|
+
# @return [Dis::Layers]
|
|
48
65
|
def readonly
|
|
49
66
|
self.class.new select(&:readonly?)
|
|
50
67
|
end
|
|
51
68
|
|
|
52
69
|
# Returns true if one or more readonly layers exist.
|
|
70
|
+
#
|
|
71
|
+
# @return [Boolean]
|
|
53
72
|
def readonly?
|
|
54
|
-
|
|
73
|
+
any?(&:readonly?)
|
|
55
74
|
end
|
|
56
75
|
|
|
57
76
|
# Returns a new instance containing only the writeable layers.
|
|
77
|
+
#
|
|
78
|
+
# @return [Dis::Layers]
|
|
58
79
|
def writeable
|
|
59
80
|
self.class.new select(&:writeable?)
|
|
60
81
|
end
|
|
61
82
|
|
|
62
83
|
# Returns true if one or more writeable layers exist.
|
|
84
|
+
#
|
|
85
|
+
# @return [Boolean]
|
|
63
86
|
def writeable?
|
|
64
|
-
|
|
87
|
+
any?(&:writeable?)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Returns a new instance containing only the cache layers.
|
|
91
|
+
#
|
|
92
|
+
# @return [Dis::Layers]
|
|
93
|
+
def cache
|
|
94
|
+
self.class.new select(&:cache?)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Returns true if one or more cache layers exist.
|
|
98
|
+
#
|
|
99
|
+
# @return [Boolean]
|
|
100
|
+
def cache?
|
|
101
|
+
any?(&:cache?)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Returns a new instance containing only the non-cache layers.
|
|
105
|
+
#
|
|
106
|
+
# @return [Dis::Layers]
|
|
107
|
+
def non_cache
|
|
108
|
+
self.class.new reject(&:cache?)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Returns true if one or more non-cache layers exist.
|
|
112
|
+
#
|
|
113
|
+
# @return [Boolean]
|
|
114
|
+
def non_cache?
|
|
115
|
+
any? { |l| !l.cache? }
|
|
65
116
|
end
|
|
66
117
|
end
|
|
67
118
|
end
|
|
@@ -4,13 +4,21 @@ module Dis
|
|
|
4
4
|
module Model
|
|
5
5
|
module ClassMethods
|
|
6
6
|
# Returns the mapping of attribute names.
|
|
7
|
+
#
|
|
8
|
+
# @return [Hash{Symbol => Symbol}]
|
|
7
9
|
def dis_attributes
|
|
8
10
|
default_dis_attributes.merge(@dis_attributes ||= {})
|
|
9
11
|
end
|
|
10
12
|
|
|
11
|
-
# Sets the current mapping of attribute names. Use this if you
|
|
12
|
-
# override the attributes and database columns that
|
|
13
|
+
# Sets the current mapping of attribute names. Use this if you
|
|
14
|
+
# want to override the attributes and database columns that
|
|
15
|
+
# Dis will use. Valid keys: +:content_hash+, +:content_type+,
|
|
16
|
+
# +:content_length+, +:filename+.
|
|
17
|
+
#
|
|
18
|
+
# @param new_attributes [Hash{Symbol => Symbol}] attribute
|
|
19
|
+
# name overrides
|
|
13
20
|
#
|
|
21
|
+
# @example
|
|
14
22
|
# class Document < ActiveRecord::Base
|
|
15
23
|
# include Dis::Model
|
|
16
24
|
# self.dis_attributes = { filename: :my_custom_filename }
|
|
@@ -20,8 +28,11 @@ module Dis
|
|
|
20
28
|
end
|
|
21
29
|
|
|
22
30
|
# Returns the storage type name, which Dis will use for
|
|
23
|
-
# directory scoping. Defaults to the name
|
|
31
|
+
# directory scoping. Defaults to the table name.
|
|
32
|
+
#
|
|
33
|
+
# @return [String]
|
|
24
34
|
#
|
|
35
|
+
# @example
|
|
25
36
|
# class Document < ActiveRecord::Base; end
|
|
26
37
|
# Document.dis_type # => "documents"
|
|
27
38
|
def dis_type
|
|
@@ -30,17 +41,23 @@ module Dis
|
|
|
30
41
|
|
|
31
42
|
# Sets the storage type name.
|
|
32
43
|
#
|
|
33
|
-
# Take care not to set the same name for multiple models,
|
|
34
|
-
# cause data loss when a record is destroyed.
|
|
44
|
+
# Take care not to set the same name for multiple models,
|
|
45
|
+
# this will cause data loss when a record is destroyed.
|
|
46
|
+
#
|
|
47
|
+
# @param new_type [String] the new type scope
|
|
48
|
+
# @return [void]
|
|
35
49
|
def dis_type=(new_type)
|
|
36
50
|
@dis_type = new_type
|
|
37
51
|
end
|
|
38
52
|
|
|
39
53
|
# Adds a presence validation on the +data+ attribute.
|
|
40
54
|
#
|
|
41
|
-
# This is
|
|
42
|
-
#
|
|
55
|
+
# This is preferred over +validates :data, presence: true+,
|
|
56
|
+
# which would load the data from storage on each save.
|
|
57
|
+
#
|
|
58
|
+
# @return [void]
|
|
43
59
|
#
|
|
60
|
+
# @example
|
|
44
61
|
# class Document < ActiveRecord::Base
|
|
45
62
|
# include Dis::Model
|
|
46
63
|
# validates_data_presence
|