require 'fileutils'
require 'cgi'
require 'tmpdir'
require 'map'
class UploadCache
Version = '1.3.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
' />
<% 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.
__
class << UploadCache
def version
UploadCache::Version
end
def url
@url ||= "file:/#{ root }"
end
def url=(url)
@url = '/' + Array(url).join('/').squeeze('/').sub(%r|^/+|, '').sub(%r|/+$|, '')
end
def root
@root ||= Dir.tmpdir
end
def root=(root)
@root = File.expand_path(root)
end
{
'ffi-uuid' => proc{|*args| FFI::UUID.generate_time.to_s},
'uuid' => proc{|*args| UUID.generate.to_s},
'uuidtools' => proc{|*args| UUIDTools::UUID.timestamp_create.to_s}
}.each do |lib, implementation|
begin
require(lib)
define_method(:uuid, &implementation)
break
rescue LoadError
nil
end
end
abort 'no suitable uuid generation library detected' unless method_defined?(:uuid)
def tmpdir(&block)
tmpdir = File.join(root, uuid)
if block
FileUtils.mkdir_p(tmpdir)
block.call(tmpdir)
else
tmpdir
end
end
def cleanname(path)
basename = File.basename(path.to_s)
CGI.unescape(basename).gsub(%r/[^0-9a-zA-Z_@)(~.-]/, '_').gsub(%r/_+/,'_')
end
def cache_key_for(key)
key.clone.tap do |cache_key|
cache_key[-1] = "#{ cache_key[-1] }_upload_cache"
end
end
def finalizer(object_id)
if fd = IOs[object_id]
IO.for_fd(fd).close
IOs.delete(object_id)
end
end
UUIDPattern = %r/^[a-zA-Z0-9-]+$/io
Age = 60 * 60 * 24
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
at_exit{ UploadCache.clear! }
def turd?
@turd ||= !!ENV['UPLOAD_CACHE_TURD']
end
def name_for(key, &block)
if block
@name_for = block
else
defined?(@name_for) ? @name_for[key] : [prefix, *Array(key)].compact.join('.')
end
end
def prefix(*value)
@prefix = value.shift if value
@prefix
end
def prefix=(value)
@prefix = value
end
def default
@default ||= Map[:url, nil, :path, nil]
end
def for(params, *args)
params = Map.for(params)
options = Map.options_for!(args)
key = Array(options[:key] || args).flatten.compact
key = [:upload] if key.empty?
upload = params.get(key)
if upload.respond_to?(:read)
tmpdir do |tmp|
original_basename =
[:path, :filename, :original_path, :original_filename].
map{|msg| upload.send(msg) if upload.respond_to?(msg)}.compact.first
basename = cleanname(original_basename)
path = File.join(tmp, basename)
open(path, 'wb'){|fd| fd.write(upload.read)}
upload_cache = UploadCache.new(key, path, options)
params.set(key, upload_cache.io)
return upload_cache
end
end
cache_key = cache_key_for(key)
upload_cache = params.get(cache_key)
if upload_cache
dirname, basename = File.split(upload_cache)
relative_dirname = File.expand_path(File.dirname(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
upload_cache = UploadCache.new(key, options)
params.set(key, upload_cache.io) if upload_cache.io
return upload_cache
end
end
attr_accessor :key
attr_accessor :cache_key
attr_accessor :name
attr_accessor :path
attr_accessor :dirname
attr_accessor :basename
attr_accessor :value
attr_accessor :io
attr_accessor :default_url
attr_accessor :default_path
IOs = {}
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)
end
end
def url
if @value
File.join(UploadCache.url, @value)
else
@default_url ? @default_url : nil
end
end
def to_s
url
end
def hidden
raw("") if @value
end
def input
raw("")
end
module HtmlSafe
def html_safe() self end
def html_safe?() self end
end
def raw(*args)
string = args.join
unless string.respond_to?(:html_safe)
string.extend(HtmlSafe)
end
string.html_safe
end
def clear!
return if 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
end
Upload_cache = UploadCache unless defined?(Upload_cache)
if defined?(Rails)
if defined?(Rails.root) and Rails.root
UploadCache.url = '/system/uploads/cache'
UploadCache.root = File.join(Rails.root, 'public', UploadCache.url)
end
end