Class: UploadCache

Inherits:
Object
  • Object
show all
Defined in:
lib/upload_cache.rb

Defined Under Namespace

Modules: HtmlSafe, WeakReference

Constant Summary collapse

Version =
'2.2.0'
Readme =
<<-__
  NAME
    upload_cache.rb

  DESCRIPTION
    a small utility library to facility caching http file uploads between
    form validation failures.  designed for rails, but usable anywhere.

  USAGE
    in the controller

      def upload
        @upload_cache = UploadCache.for(params, :upload)

        @record = Model.new(params)

        if request.get?
          render and return
        end

        if request.post?
          @record.save!
          @upload_cache.clear!
        end
      end


    in the view

      <input type='file' name='upload />

      <%= @upload_cache.hidden %>

      <!-- optionally, you can show any uploaded upload -->

      <% if url = @upload_cache.url %>
        you already uploaded: <img src='<%= raw url %>' />
      <% end %>


    in a rake task

      UploadCache.clear!  ### nuke old files once per day

    upload_caches ***does this automatically*** at_exit{}, but you can still
    run it manually if you like.

__
UUIDPattern =
%r/^[a-zA-Z0-9-]+$/io
Age =
60 * 60 * 24
IOs =
{}

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(key, *args) ⇒ UploadCache

Returns a new instance of UploadCache.



356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
# File 'lib/upload_cache.rb', line 356

def initialize(key, *args)
  @options = Map.options_for!(args)

  @key = key
  @cache_key = UploadCache.cache_key_for(@key)
  @name = UploadCache.name_for(@cache_key)

  path = args.shift || @options[:path]

  default = Map.for(@options[:default])

  @default_url = default[:url] || @options[:default_url] || UploadCache.default.url
  @default_path = default[:path] || @options[:default_path] || UploadCache.default.path

  if path
    @path = path
    @dirname, @basename = File.split(@path)
    @value = File.join(File.basename(@dirname), @basename).strip
  else
    @path = nil
    @value = nil
  end

  if @path or @default_path
    @io = open(@path || @default_path, 'rb')
    IOs[object_id] = @io.fileno
    ObjectSpace.define_finalizer(self, UploadCache.method(:finalizer).to_proc)
    @io.send(:extend, WeakReference)
    @io.upload_cache = self
  end
end

Instance Attribute Details

#basenameObject

Returns the value of attribute basename.



348
349
350
# File 'lib/upload_cache.rb', line 348

def basename
  @basename
end

#cache_keyObject

Returns the value of attribute cache_key.



343
344
345
# File 'lib/upload_cache.rb', line 343

def cache_key
  @cache_key
end

#default_pathObject

Returns the value of attribute default_path.



352
353
354
# File 'lib/upload_cache.rb', line 352

def default_path
  @default_path
end

#default_urlObject

Returns the value of attribute default_url.



351
352
353
# File 'lib/upload_cache.rb', line 351

def default_url
  @default_url
end

#dirnameObject

Returns the value of attribute dirname.



347
348
349
# File 'lib/upload_cache.rb', line 347

def dirname
  @dirname
end

#ioObject

Returns the value of attribute io.



350
351
352
# File 'lib/upload_cache.rb', line 350

def io
  @io
end

#keyObject

Returns the value of attribute key.



342
343
344
# File 'lib/upload_cache.rb', line 342

def key
  @key
end

#nameObject

Returns the value of attribute name.



345
346
347
# File 'lib/upload_cache.rb', line 345

def name
  @name
end

#optionsObject

Returns the value of attribute options.



344
345
346
# File 'lib/upload_cache.rb', line 344

def options
  @options
end

#pathObject

Returns the value of attribute path.



346
347
348
# File 'lib/upload_cache.rb', line 346

def path
  @path
end

#valueObject

Returns the value of attribute value.



349
350
351
# File 'lib/upload_cache.rb', line 349

def value
  @value
end

Class Method Details

.cache(params, *args) ⇒ Object Also known as: for



199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
# File 'lib/upload_cache.rb', line 199

def cache(params, *args)
  params_map = Map.for(params)
  options = Map.options_for!(args)

  key = Array(options[:key] || args).flatten.compact
  key = [:upload] if key.empty?

  upload_cache = (
    current_upload_cache_for(params_map, key, options) or
    previous_upload_cache_for(params_map, key, options) or
    default_upload_cache_for(params_map, key, options)
  )

  value = params_map.get(key)

  update_params(params, key, value)

  upload_cache
end

.cache_key_for(key) ⇒ Object



124
125
126
127
128
# File 'lib/upload_cache.rb', line 124

def cache_key_for(key)
  key.clone.tap do |cache_key|
    cache_key[-1] = "#{ cache_key[-1] }_upload_cache"
  end
end

.cleanname(path) ⇒ Object



119
120
121
122
# File 'lib/upload_cache.rb', line 119

def cleanname(path)
  basename = File.basename(path.to_s)
  CGI.unescape(basename).gsub(%r/[^0-9a-zA-Z_@)(~.-]/, '_').gsub(%r/_+/,'_')
end

.clear!(options = {}) ⇒ Object



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/upload_cache.rb', line 140

def clear!(options = {})
  return if UploadCache.turd?

  glob = File.join(root, '*')
  age = Integer(options[:age] || options['age'] || Age)
  since = options[:since] || options['since'] || Time.now

  Dir.glob(glob) do |entry|
    begin
      next unless test(?d, entry)
      next unless File.basename(entry) =~ UUIDPattern

      files = Dir.glob(File.join(entry, '**/**'))

      all_files_are_old =
        files.all? do |file|
          begin
            stat = File.stat(file)
            age = since - stat.atime
            age >= Age
          rescue
            false
          end
        end

      FileUtils.rm_rf(entry) if all_files_are_old
    rescue
      next
    end
  end
end

.current_upload_cache_for(params, key, options) ⇒ Object



243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
# File 'lib/upload_cache.rb', line 243

def current_upload_cache_for(params, key, options)
  upload = params.get(key)
  if upload.respond_to?(:upload_cache) and upload.upload_cache
    return upload.upload_cache
  end

  if upload.respond_to?(:read)
    tmpdir do |tmp|
      original_basename =
        [:original_path, :original_filename, :path, :filename].
          map{|msg| upload.send(msg) if upload.respond_to?(msg)}.compact.first

      basename = cleanname(original_basename)

      path = File.join(tmp, basename)

      copied = false

      rewind(upload) do
        src = upload.path
        dst = path

        strategies = [
          proc{ `ln -f #{ src.inspect } #{ dst.inspect } || cp -f #{ src.inspect } #{ dst.inspect }`},
          proc{ FileUtils.ln(src, dst) },
          proc{ FileUtils.cp(src, dst) },
          proc{ 
            open(dst, 'wb'){|fd| fd.write(upload.read)} 
          }
        ]

        FileUtils.rm_f(dst)

        strategies.each do |strategy|
          strategy.call rescue nil
          break if((copied = test(?e, dst)))
        end
      end

      raise("failed to copy #{ upload.path.inspect } -> #{ path.inspect }") unless copied

      upload_cache = UploadCache.new(key, path, options)
      params.set(key, upload_cache.io)
      return upload_cache
    end
  end

  nil
end

.defaultObject



191
192
193
# File 'lib/upload_cache.rb', line 191

def default
  @default ||= Map[:url, nil, :path, nil]
end

.default_upload_cache_for(params, key, options) ⇒ Object



315
316
317
318
319
# File 'lib/upload_cache.rb', line 315

def default_upload_cache_for(params, key, options)
  upload_cache = UploadCache.new(key, options)
  params.set(key, upload_cache.io)
  return upload_cache
end

.finalizer(object_id) ⇒ Object



130
131
132
133
134
135
# File 'lib/upload_cache.rb', line 130

def finalizer(object_id)
  if fd = IOs[object_id]
    ::IO.for_fd(fd).close rescue nil
    IOs.delete(object_id)
  end
end

.name_for(key, &block) ⇒ Object



178
179
180
181
182
183
184
# File 'lib/upload_cache.rb', line 178

def name_for(key, &block)
  if block
    @name_for = block
  else
    defined?(@name_for) ? @name_for[key] : [prefix, *Array(key)].compact.join('.')
  end
end

.prefix(*value) ⇒ Object



186
187
188
189
# File 'lib/upload_cache.rb', line 186

def prefix(*value)
  @prefix = value.shift if value
  @prefix
end

.prefix=(value) ⇒ Object



195
196
197
# File 'lib/upload_cache.rb', line 195

def prefix=(value)
  @prefix = value
end

.previous_upload_cache_for(params, key, options) ⇒ Object



293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
# File 'lib/upload_cache.rb', line 293

def previous_upload_cache_for(params, key, options)
  upload = params.get(key)
  if upload.respond_to?(:upload_cache) and upload.upload_cache
    return upload.upload_cache
  end

  upload = params.get(cache_key_for(key))

  if upload
    dirname, basename = File.split(File.expand_path(upload))
    relative_dirname = File.basename(dirname)
    relative_basename = File.join(relative_dirname, basename)
    path = root + '/' + relative_basename

    upload_cache = UploadCache.new(key, path, options)
    params.set(key, upload_cache.io)
    return upload_cache
  end

  nil
end

.rewind(io, &block) ⇒ Object



321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
# File 'lib/upload_cache.rb', line 321

def rewind(io, &block)
  begin
    pos = io.pos
    io.flush
    io.rewind
  rescue
    nil
  end

  begin
    block.call
  ensure
    begin
      io.pos = pos
    rescue
      nil
    end
  end
end

.rootObject



79
80
81
82
83
84
85
86
87
# File 'lib/upload_cache.rb', line 79

def root
  @root ||= (
    if defined?(Rails.root) and Rails.root
      File.join(Rails.root, 'public', UploadCache.url)
    else
      Dir.tmpdir
    end
  )
end

.root=(root) ⇒ Object



89
90
91
# File 'lib/upload_cache.rb', line 89

def root=(root)
  @root = File.expand_path(root)
end

.tmpdir(&block) ⇒ Object



108
109
110
111
112
113
114
115
116
117
# File 'lib/upload_cache.rb', line 108

def tmpdir(&block)
  tmpdir = File.join(root, uuid)

  if block
    FileUtils.mkdir_p(tmpdir)
    block.call(tmpdir)
  else
    tmpdir
  end
end

.turd?Boolean

Returns:

  • (Boolean)


174
175
176
# File 'lib/upload_cache.rb', line 174

def turd?
  @turd ||= !!ENV['UPLOAD_CACHE_TURD']
end

.update_params(params, key, value) ⇒ Object



220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/upload_cache.rb', line 220

def update_params(params, key, value)
  key = Array(key).flatten

  leaf = key.pop
  path = key
  node = params

  until path.empty?
    key = path.shift
    case node
      when Array
        index = Integer(key)
        break unless node[index]
        node = node[index]
      else
        break unless node.has_key?(key)
        node = node[key]
    end
  end

  node[leaf] = value
end

.urlObject



65
66
67
68
69
70
71
72
73
# File 'lib/upload_cache.rb', line 65

def url
  @url ||= (
    if defined?(Rails.root) and Rails.root
      '/system/upload_cache'
    else
      "file:/#{ root }"
    end
  )
end

.url=(url) ⇒ Object



75
76
77
# File 'lib/upload_cache.rb', line 75

def url=(url)
  @url = '/' + Array(url).join('/').squeeze('/').sub(%r|^/+|, '').sub(%r|/+$|, '')
end

.versionObject



60
61
62
# File 'lib/upload_cache.rb', line 60

def version
  UploadCache::Version
end

Instance Method Details

#blank?Boolean

Returns:

  • (Boolean)


413
414
415
# File 'lib/upload_cache.rb', line 413

def blank?
  @path.blank?
end

#clear!(&block) ⇒ Object Also known as: clear



451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
# File 'lib/upload_cache.rb', line 451

def clear!(&block)
  result = block ? block.call(@path) : nil 

  unless UploadCache.turd?
    begin
      FileUtils.rm_rf(@dirname) if test(?d, @dirname)
    rescue
      nil
    ensure
      @io.close rescue nil
      IOs.delete(object_id)
      Thread.new{ UploadCache.clear! }
    end
  end

  result
end

#hiddenObject



430
431
432
# File 'lib/upload_cache.rb', line 430

def hidden
  raw("<input type='hidden' name='#{ @name }' value='#{ @value }' class='upload_cache hidden' />") if @value
end

#inputObject



434
435
436
# File 'lib/upload_cache.rb', line 434

def input
  raw("<input type='file' name='#{ @name }' class='upload_cache input' />")
end

#inspectObject



404
405
406
407
408
409
410
411
# File 'lib/upload_cache.rb', line 404

def inspect
  {
    UploadCache.name =>
      {
        :key => key, :cache_key => key, :name => name, :path => path, :io => io
      }
  }.inspect
end

#raw(*args) ⇒ Object



443
444
445
446
447
448
449
# File 'lib/upload_cache.rb', line 443

def raw(*args)
  string = args.join
  unless string.respond_to?(:html_safe)
    string.extend(HtmlSafe)
  end
  string.html_safe
end

#to_sObject



426
427
428
# File 'lib/upload_cache.rb', line 426

def to_s
  url
end

#urlObject



417
418
419
420
421
422
423
424
# File 'lib/upload_cache.rb', line 417

def url
  if @value
    File.join(UploadCache.url, @value)
  else
    @default_url ? @default_url : nil
    #defined?(@placeholder) ? @placeholder : nil
  end
end