dis 1.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bf532ee41185ec1d1fb96f36e9cd697a7a1c0ece44da5a30b2ff01d8a6c77010
4
- data.tar.gz: 8d338d164af992ca38b9af28840c97eabc33f6434791a5505b5a602dd1aa0b3f
3
+ metadata.gz: cbda8b2db5161c2dd8357c1f06102a27dfc4ff3e1a3eb79974b29abb6fa27c4d
4
+ data.tar.gz: 4cd6907621cae900a48f021f2bd0976d5721cfaa1a4a368af0f54e728c572543
5
5
  SHA512:
6
- metadata.gz: 8db87ae2b81b5639fc9312a3dbad3580e6b96886bcb22ddcd6d834efa7452e7348738c37d656ea86c343041cbde683b68f4277a28125f0467de4878b3ca2d3b0
7
- data.tar.gz: bde07caf197b9a2ae72f3403c3b0f4ad189dbca90259768354eaa96ae7f026edcb3bc6fa86c3b2378361b9eb76ef8a9f5a8446b8584dd5b375861f0de546c740
6
+ metadata.gz: 2dc2ee898c2864535927cd472c46c6f7d7db082e1adfe95b4366b9a7679af772ff64c26475dcab2257a49284663170269d03627dd69416fe87a89ee6be8b3491
7
+ data.tar.gz: 5c43189d589f280df560496e3fe37ef50ec19217fd031e93f551439e3ce97804d5e71a33b53acda960e96e0a5fe1855fa1ac8caca8881599810b1e790f56d582
data/README.md CHANGED
@@ -104,6 +104,36 @@ class DocumentsController < ApplicationController
104
104
  end
105
105
  ```
106
106
 
107
+ ### Accessing the data
108
+
109
+ Which accessor you want depends on how long the data needs to stay
110
+ valid.
111
+
112
+ `data` returns the content as a binary string.
113
+
114
+ ```ruby
115
+ document.data # => "foobar"
116
+ ```
117
+
118
+ `with_data_file` yields a path, for tools that want a file name rather
119
+ than the bytes. It is valid for the duration of the block, so resolve
120
+ anything lazy before returning.
121
+
122
+ ```ruby
123
+ document.with_data_file { |path| Vips::Image.new_from_file(path.to_s).avg }
124
+ ```
125
+
126
+ `open_data` returns an open file, valid until you close it — even if
127
+ the content is deleted or evicted from a cache layer meanwhile. Use it
128
+ when the reader outlives the current call stack, such as a response
129
+ body handed to the web server.
130
+
131
+ ```ruby
132
+ document.open_data { |file| file.read }
133
+
134
+ file = document.open_data # caller closes it
135
+ ```
136
+
107
137
  ## Layers
108
138
 
109
139
  The underlying storage consists of one or more layers. Each layer
data/lib/dis/layer.rb CHANGED
@@ -213,25 +213,45 @@ module Dis
213
213
  result = debug_log("Get #{type}/#{key} from #{name}") do
214
214
  dir.files.get(key_component(type, key))
215
215
  end
216
- touch_file(type, key) if result && cache?
216
+ refresh_cache_mtime(local_file_path(type, key)) if result && cache?
217
217
  result
218
218
  end
219
219
 
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.
223
+ #
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
234
+
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
241
+ end
242
+
220
243
  # Returns the absolute file path for a locally stored file, or
221
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.
222
247
  #
223
248
  # @param type [String] the type scope
224
249
  # @param key [String] the content hash
225
250
  # @return [String, nil]
226
251
  def file_path(type, key)
227
- return unless connection.respond_to?(:local_root)
228
- return unless exists?(type, key)
229
-
230
- File.join(
231
- connection.local_root,
232
- directory_component(type, key),
233
- key_component(type, key)
234
- )
252
+ path = local_file_path(type, key)
253
+ refresh_cache_mtime(path) if cache?
254
+ path
235
255
  end
236
256
 
237
257
  # Deletes a file from the store.
@@ -292,9 +312,35 @@ module Dis
292
312
  mtime: file.mtime, size: file.size }
293
313
  end
294
314
 
295
- def touch_file(type, key)
296
- fp = file_path(type, key)
297
- FileUtils.touch(fp) if fp
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
298
344
  end
299
345
 
300
346
  def directory_component(_type, _key)
@@ -93,37 +93,42 @@ module Dis
93
93
  Dis::Storage.store(storage_type, raw)
94
94
  end
95
95
 
96
- # Clears cached data and tempfiles, allowing them to be
97
- # garbage collected. Subsequent calls to +read+ or +tempfile+
98
- # will re-fetch from storage.
96
+ # Clears cached data, allowing it to be garbage collected.
97
+ # Subsequent calls to +read+ will re-fetch from storage.
99
98
  #
100
99
  # @return [void]
101
100
  def reset_read_cache!
102
101
  @read = nil
103
- return unless @tempfile
104
-
105
- @tempfile.close!
106
- @tempfile = nil
107
102
  end
108
103
 
109
- # Returns the file path to the data. Prefers a local storage
110
- # path to avoid unnecessary copies, falls back to a tempfile.
104
+ # Yields a path to the data, removing any temporary copy
105
+ # afterwards.
111
106
  #
112
- # @return [String]
113
- def file_path
114
- local_path || tempfile.path
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
115
119
  end
116
120
 
117
- # Writes the data to a temporary file.
121
+ # Returns the data as an open file. Any temporary copy is
122
+ # unlinked first, leaving the kernel to reclaim it on close.
118
123
  #
119
- # @return [Tempfile]
120
- def tempfile
121
- unless @tempfile
122
- @tempfile = Tempfile.new(binmode: true)
123
- @tempfile.write(@read || read_from(closest))
124
- @tempfile.open
125
- end
126
- @tempfile
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
127
132
  end
128
133
 
129
134
  protected
@@ -185,6 +190,48 @@ module Dis
185
190
  Dis::Storage.file_path(storage_type, content_hash)
186
191
  end
187
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
+
188
235
  attr_reader :raw
189
236
  end
190
237
  end
data/lib/dis/model.rb CHANGED
@@ -151,19 +151,36 @@ module Dis
151
151
  dis_set :filename, file.original_filename
152
152
  end
153
153
 
154
- # Returns a file path to the data, preferring local storage
155
- # paths. Falls back to a tempfile path if no local layer has
156
- # the file.
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.
157
158
  #
158
- # @return [String]
159
- def data_file_path
160
- dis_data.file_path
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(&)
161
166
  end
162
167
 
163
- # Returns the data as a temporary file.
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.
164
171
  #
165
- # @return [Tempfile]
166
- delegate :tempfile, to: :dis_data
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
183
+ end
167
184
 
168
185
  private
169
186
 
data/lib/dis/storage.rb CHANGED
@@ -166,6 +166,29 @@ module Dis
166
166
  nil
167
167
  end
168
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
+
169
192
  # Deletes a file from all layers. Kicks off a
170
193
  # {Dis::Jobs::Delete} job if any delayed layers are defined.
171
194
  #
@@ -304,6 +327,22 @@ module Dis
304
327
  end
305
328
  end
306
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
+
307
346
  def fetch_from_layer(layer, type, key)
308
347
  layer.get(type, key)
309
348
  rescue StandardError => e
data/lib/dis/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dis
4
- VERSION = "1.3.0"
4
+ VERSION = "1.3.1"
5
5
  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.3.0
4
+ version: 1.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Inge Jørgensen
@@ -150,7 +150,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
150
150
  - !ruby/object:Gem::Version
151
151
  version: '0'
152
152
  requirements: []
153
- rubygems_version: 4.0.3
153
+ rubygems_version: 4.0.10
154
154
  specification_version: 4
155
155
  summary: A file store for your Rails app
156
156
  test_files: []