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/model/data.rb
CHANGED
|
@@ -7,12 +7,18 @@ module Dis
|
|
|
7
7
|
# Facilitates communication between the model and the storage,
|
|
8
8
|
# and holds any newly assigned data before the record is saved.
|
|
9
9
|
class Data
|
|
10
|
+
# @param record [ActiveRecord::Base] the model instance
|
|
11
|
+
# @param raw [File, IO, String, nil] newly assigned data
|
|
10
12
|
def initialize(record, raw = nil)
|
|
11
13
|
@record = record
|
|
12
14
|
@raw = raw
|
|
13
15
|
end
|
|
14
16
|
|
|
15
17
|
# Returns true if two Data objects represent the same data.
|
|
18
|
+
#
|
|
19
|
+
# @param other [Dis::Model::Data, #read, Object] the object to
|
|
20
|
+
# compare
|
|
21
|
+
# @return [Boolean]
|
|
16
22
|
def ==(other)
|
|
17
23
|
if !raw? && other.is_a?(self.class) && !other.changed?
|
|
18
24
|
content_hash == other.content_hash
|
|
@@ -24,24 +30,33 @@ module Dis
|
|
|
24
30
|
end
|
|
25
31
|
|
|
26
32
|
# Returns true if data exists either in memory or in storage.
|
|
33
|
+
#
|
|
34
|
+
# @return [Boolean]
|
|
27
35
|
def any?
|
|
28
36
|
raw? || stored?
|
|
29
37
|
end
|
|
30
38
|
|
|
31
39
|
# Returns the data as a binary string.
|
|
40
|
+
#
|
|
41
|
+
# @return [String, nil]
|
|
32
42
|
def read
|
|
33
43
|
@read ||= read_from(closest)
|
|
34
44
|
end
|
|
35
45
|
|
|
36
|
-
# Will be true if data has been
|
|
46
|
+
# Will be true if data has been explicitly set.
|
|
47
|
+
#
|
|
48
|
+
# @return [Boolean]
|
|
37
49
|
#
|
|
50
|
+
# @example
|
|
38
51
|
# Dis::Model::Data.new(record).changed? # => false
|
|
39
|
-
# Dis::Model::Data.new(record,
|
|
52
|
+
# Dis::Model::Data.new(record, file).changed? # => true
|
|
40
53
|
def changed?
|
|
41
54
|
raw?
|
|
42
55
|
end
|
|
43
56
|
|
|
44
|
-
# Returns the length of the data.
|
|
57
|
+
# Returns the length of the data in bytes.
|
|
58
|
+
#
|
|
59
|
+
# @return [Integer]
|
|
45
60
|
def content_length
|
|
46
61
|
if raw? && raw.respond_to?(:length)
|
|
47
62
|
raw.length
|
|
@@ -50,9 +65,13 @@ module Dis
|
|
|
50
65
|
end
|
|
51
66
|
end
|
|
52
67
|
|
|
53
|
-
# Expires a data object from the storage if it's no longer
|
|
54
|
-
# by existing records. This is triggered from
|
|
55
|
-
# whenever they are changed or
|
|
68
|
+
# Expires a data object from the storage if it's no longer
|
|
69
|
+
# being used by existing records. This is triggered from
|
|
70
|
+
# callbacks on the record whenever they are changed or
|
|
71
|
+
# destroyed.
|
|
72
|
+
#
|
|
73
|
+
# @param hash [String] the content hash to expire
|
|
74
|
+
# @return [void]
|
|
56
75
|
def expire(hash)
|
|
57
76
|
return if hash.blank?
|
|
58
77
|
|
|
@@ -63,37 +82,53 @@ module Dis
|
|
|
63
82
|
end
|
|
64
83
|
end
|
|
65
84
|
|
|
66
|
-
# Stores the data
|
|
85
|
+
# Stores the data and returns the content hash.
|
|
86
|
+
#
|
|
87
|
+
# @return [String] the SHA1 content hash
|
|
88
|
+
# @raise [Dis::Errors::NoDataError] if no data has been
|
|
89
|
+
# assigned
|
|
67
90
|
def store!
|
|
68
91
|
raise Dis::Errors::NoDataError unless raw?
|
|
69
92
|
|
|
70
93
|
Dis::Storage.store(storage_type, raw)
|
|
71
94
|
end
|
|
72
95
|
|
|
73
|
-
# Clears cached data
|
|
74
|
-
#
|
|
96
|
+
# Clears cached data, allowing it to be garbage collected.
|
|
97
|
+
# Subsequent calls to +read+ will re-fetch from storage.
|
|
98
|
+
#
|
|
99
|
+
# @return [void]
|
|
75
100
|
def reset_read_cache!
|
|
76
101
|
@read = nil
|
|
77
|
-
return unless @tempfile
|
|
78
|
-
|
|
79
|
-
@tempfile.close!
|
|
80
|
-
@tempfile = nil
|
|
81
102
|
end
|
|
82
103
|
|
|
83
|
-
#
|
|
84
|
-
#
|
|
85
|
-
|
|
86
|
-
|
|
104
|
+
# Yields a path to the data, removing any temporary copy
|
|
105
|
+
# afterwards.
|
|
106
|
+
#
|
|
107
|
+
# @yieldparam path [Pathname] path to the data
|
|
108
|
+
# @return [Object] the return value of the block
|
|
109
|
+
def with_file
|
|
110
|
+
path = local_path
|
|
111
|
+
return yield(Pathname.new(path)) if path
|
|
112
|
+
|
|
113
|
+
file = materialize
|
|
114
|
+
begin
|
|
115
|
+
yield(Pathname.new(file.path))
|
|
116
|
+
ensure
|
|
117
|
+
close_and_unlink(file)
|
|
118
|
+
end
|
|
87
119
|
end
|
|
88
120
|
|
|
89
|
-
#
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
121
|
+
# Returns the data as an open file. Any temporary copy is
|
|
122
|
+
# unlinked first, leaving the kernel to reclaim it on close.
|
|
123
|
+
#
|
|
124
|
+
# @return [File] an open file, positioned at the start
|
|
125
|
+
def open
|
|
126
|
+
path = local_path
|
|
127
|
+
return File.open(path, "rb") if path
|
|
128
|
+
|
|
129
|
+
file = materialize
|
|
130
|
+
File.unlink(file.path)
|
|
131
|
+
file
|
|
97
132
|
end
|
|
98
133
|
|
|
99
134
|
protected
|
|
@@ -155,6 +190,48 @@ module Dis
|
|
|
155
190
|
Dis::Storage.file_path(storage_type, content_hash)
|
|
156
191
|
end
|
|
157
192
|
|
|
193
|
+
# Writes the data to a new temporary file. The caller owns it.
|
|
194
|
+
def materialize
|
|
195
|
+
file = Tempfile.create
|
|
196
|
+
file.binmode
|
|
197
|
+
fill(file)
|
|
198
|
+
file.rewind
|
|
199
|
+
file
|
|
200
|
+
rescue StandardError
|
|
201
|
+
close_and_unlink(file) if file
|
|
202
|
+
raise
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def fill(file)
|
|
206
|
+
if raw?
|
|
207
|
+
write_raw_to(file)
|
|
208
|
+
else
|
|
209
|
+
Dis::Storage.get_file(storage_type, content_hash, file)
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def write_raw_to(file)
|
|
214
|
+
if raw.respond_to?(:read)
|
|
215
|
+
rewind_raw
|
|
216
|
+
IO.copy_stream(raw, file)
|
|
217
|
+
rewind_raw
|
|
218
|
+
else
|
|
219
|
+
file.write(raw)
|
|
220
|
+
end
|
|
221
|
+
file.flush
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def rewind_raw
|
|
225
|
+
raw.rewind if raw.respond_to?(:rewind)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def close_and_unlink(file)
|
|
229
|
+
file.close unless file.closed?
|
|
230
|
+
File.unlink(file.path)
|
|
231
|
+
rescue Errno::ENOENT
|
|
232
|
+
nil
|
|
233
|
+
end
|
|
234
|
+
|
|
158
235
|
attr_reader :raw
|
|
159
236
|
end
|
|
160
237
|
end
|
data/lib/dis/model.rb
CHANGED
|
@@ -7,7 +7,7 @@ module Dis
|
|
|
7
7
|
# = Dis Model
|
|
8
8
|
#
|
|
9
9
|
# ActiveModel extension for the model holding your data. To use it,
|
|
10
|
-
#
|
|
10
|
+
# include the module in your model:
|
|
11
11
|
#
|
|
12
12
|
# class Document < ActiveRecord::Base
|
|
13
13
|
# include Dis::Model
|
|
@@ -36,24 +36,24 @@ module Dis
|
|
|
36
36
|
#
|
|
37
37
|
# == Usage
|
|
38
38
|
#
|
|
39
|
-
# To save a file,
|
|
39
|
+
# To save a file, assign to the <tt>file</tt> attribute.
|
|
40
40
|
#
|
|
41
41
|
# document = Document.create(file: params.permit(:file))
|
|
42
42
|
#
|
|
43
43
|
# <tt>content_type</tt> and <tt>filename</tt> will automatically be set if
|
|
44
|
-
# the supplied object quacks like a file. <tt>content_length</tt>
|
|
45
|
-
#
|
|
44
|
+
# the supplied object quacks like a file. <tt>content_length</tt> and
|
|
45
|
+
# <tt>content_hash</tt> will always be set.
|
|
46
46
|
#
|
|
47
|
-
#
|
|
48
|
-
#
|
|
47
|
+
# To store a binary blob without filenames or content types, set the
|
|
48
|
+
# <tt>data</tt> attribute directly.
|
|
49
49
|
#
|
|
50
50
|
# my_data = File.read('document.pdf')
|
|
51
51
|
# document.update(data: my_data)
|
|
52
52
|
#
|
|
53
|
-
# The data won't be stored until the record is saved, and
|
|
53
|
+
# The data won't be stored until the record is saved, and only if
|
|
54
54
|
# the record is valid.
|
|
55
55
|
#
|
|
56
|
-
# To retrieve your data,
|
|
56
|
+
# To retrieve your data, read the <tt>data</tt> attribute. The file
|
|
57
57
|
# will be lazily loaded from the store on demand and cached in memory as long
|
|
58
58
|
# as the record stays in scope.
|
|
59
59
|
#
|
|
@@ -73,7 +73,7 @@ module Dis
|
|
|
73
73
|
# validates_data_presence
|
|
74
74
|
# end
|
|
75
75
|
#
|
|
76
|
-
# If you want to validate content types, size or similar,
|
|
76
|
+
# If you want to validate content types, size or similar, use standard
|
|
77
77
|
# Rails validations on the metadata attributes:
|
|
78
78
|
#
|
|
79
79
|
# validates :content_type, presence: true, format: /\Aapplication\/pdf\z/
|
|
@@ -89,18 +89,26 @@ module Dis
|
|
|
89
89
|
attribute :data, :binary
|
|
90
90
|
end
|
|
91
91
|
|
|
92
|
-
# Returns the data as a binary string, or nil if no data has
|
|
92
|
+
# Returns the data as a binary string, or nil if no data has
|
|
93
|
+
# been set.
|
|
94
|
+
#
|
|
95
|
+
# @return [String, nil]
|
|
93
96
|
def data
|
|
94
97
|
dis_data.read
|
|
95
98
|
end
|
|
96
99
|
|
|
97
100
|
# Returns true if data is set.
|
|
101
|
+
#
|
|
102
|
+
# @return [Boolean]
|
|
98
103
|
def data?
|
|
99
104
|
dis_data.any?
|
|
100
105
|
end
|
|
101
106
|
|
|
102
|
-
# Assigns new data. This also sets
|
|
103
|
-
#
|
|
107
|
+
# Assigns new data. This also sets +content_length+ and
|
|
108
|
+
# +content_hash+.
|
|
109
|
+
#
|
|
110
|
+
# @param raw_data [File, IO, String, nil] the content to store
|
|
111
|
+
# @return [void]
|
|
104
112
|
def data=(raw_data)
|
|
105
113
|
new_data = Dis::Model::Data.new(self, raw_data)
|
|
106
114
|
attribute_will_change!("data") unless new_data == dis_data
|
|
@@ -113,32 +121,67 @@ module Dis
|
|
|
113
121
|
dis_set :content_length, dis_data.content_length
|
|
114
122
|
end
|
|
115
123
|
|
|
116
|
-
# Returns true if the data has been changed since the object
|
|
124
|
+
# Returns true if the data has been changed since the object
|
|
125
|
+
# was last saved.
|
|
126
|
+
#
|
|
127
|
+
# @return [Boolean]
|
|
117
128
|
def data_changed?
|
|
118
129
|
changes.include?("data")
|
|
119
130
|
end
|
|
120
131
|
|
|
132
|
+
# Returns true if the record has been persisted and its data
|
|
133
|
+
# has not been changed since the last save.
|
|
134
|
+
#
|
|
135
|
+
# @return [Boolean]
|
|
121
136
|
def dis_stored?
|
|
122
137
|
!(new_record? || data_changed?)
|
|
123
138
|
end
|
|
124
139
|
|
|
125
|
-
# Assigns new data from an uploaded file. In addition to the
|
|
126
|
-
# performed by
|
|
127
|
-
#
|
|
140
|
+
# Assigns new data from an uploaded file. In addition to the
|
|
141
|
+
# actions performed by {#data=}, this will set +content_type+
|
|
142
|
+
# and +filename+.
|
|
143
|
+
#
|
|
144
|
+
# @param file [ActionDispatch::Http::UploadedFile,
|
|
145
|
+
# Rack::Test::UploadedFile] an uploaded file that responds to
|
|
146
|
+
# +content_type+ and +original_filename+
|
|
147
|
+
# @return [void]
|
|
128
148
|
def file=(file)
|
|
129
149
|
self.data = file
|
|
130
150
|
dis_set :content_type, file.content_type
|
|
131
151
|
dis_set :filename, file.original_filename
|
|
132
152
|
end
|
|
133
153
|
|
|
134
|
-
#
|
|
135
|
-
|
|
136
|
-
|
|
154
|
+
# Yields the path to a file containing the data, for tools that
|
|
155
|
+
# need a file name rather than the bytes. The path is valid for
|
|
156
|
+
# the duration of the block only, so resolve anything lazy before
|
|
157
|
+
# returning.
|
|
158
|
+
#
|
|
159
|
+
# @yieldparam path [Pathname] path to the data
|
|
160
|
+
# @return [Object] the return value of the block
|
|
161
|
+
#
|
|
162
|
+
# @example
|
|
163
|
+
# document.with_data_file { |path| Vips::Image.new_from_file(path.to_s).avg }
|
|
164
|
+
def with_data_file(&)
|
|
165
|
+
dis_data.with_file(&)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Returns the data as an open, read-only file, or yields it and
|
|
169
|
+
# closes it afterwards. The data stays readable until the file is
|
|
170
|
+
# closed, even if the content is deleted or evicted meanwhile.
|
|
171
|
+
#
|
|
172
|
+
# @yieldparam file [File] an open file, positioned at the start
|
|
173
|
+
# @return [File, Object] the open file, or the block's value
|
|
174
|
+
def open_data
|
|
175
|
+
file = dis_data.open
|
|
176
|
+
return file unless block_given?
|
|
177
|
+
|
|
178
|
+
begin
|
|
179
|
+
yield file
|
|
180
|
+
ensure
|
|
181
|
+
file.close unless file.closed?
|
|
182
|
+
end
|
|
137
183
|
end
|
|
138
184
|
|
|
139
|
-
# Returns the data as a temporary file.
|
|
140
|
-
delegate :tempfile, to: :dis_data
|
|
141
|
-
|
|
142
185
|
private
|
|
143
186
|
|
|
144
187
|
def cleanup_data
|