module Bulldog # # Gives IO, StringIO, Tempfile, and SavedFile a common interface. # # In particular, this takes care of writing it to a file so external # programs may be called on it, while keeping the number of file # writes to a minimum. # module Stream def self.new(object) klass = case object when ::Tempfile ForTempfile when File ForFile when SavedFile ForSavedFile when MissingFile ForMissingFile when StringIO ForStringIO when IO ForIO end klass.new(object) end class Base def initialize(target) @target = target end # # The underlying object. # attr_reader :target # # Return the path of a file where the content can be found. # def path @target.path end # # Return true if this stream represents a missing file. # def missing? false end # # Return the original file name of the underlying object. This # is the basename of the file as the user uploaded it (for an # uploaded file), or the file on the filesystem (for a File # object). For other IO objects, return nil. # def file_name if @target.respond_to?(:original_path) @target.original_path else nil end end # # Return the size of the content. # def size File.size(path) end # # Return the mime-type of the content. # def content_type @content_type ||= `file --brief --mime #{path}`.strip end # # Write the content to the given path. # def write_to(path) src, dst = self.path, path unless src == dst FileUtils.mkdir_p File.dirname(path) FileUtils.cp src, dst end end # # Reload data from the target. # def reload @content_type = nil end end class ForTempfile < Base def initialize(target) super @path = target.path or raise ArgumentError, "Tempfile is closed - cannot retrieve information" end def path @target.flush unless @target.closed? super end def size # Tempfile#size returns nil when closed. @target.flush unless @target.closed? File.size(@target.path) end end class ForFile < Base def path @target.flush unless @target.closed? rescue IOError # not open for writing super end def file_name super || File.basename(@target.path) end end class ForSavedFile < Base delegate :file_name, :to => :target end class ForMissingFile < Base def missing? true end delegate :file_name, :to => :target def content_type @target.content_type || super end end class ForIO < Base def path return @path if @path # Keep extension if it's available. Some commands may use the # file extension of the input file, for example. tempfile_name = 'bulldog' if @target.respond_to?(:original_path) && @target.original_path tempfile_name = [tempfile_name, File.extname(@target.original_path)] end Tempfile.open(tempfile_name) do |file| write_to_io(file) @path = file.path # Don't let the tempfile be GC'd until the stream is, as the # tempfile's finalizer deletes the file. @tempfile = file end @path end def write_to(path) if @path super else open(path, 'w') do |file| write_to_io(file) @path = file.path end end end private # ----------------------------------------------------- BLOCK_SIZE = 64*1024 def write_to_io(io) target.rewind rescue nil # not rewindable buffer = "" while target.read(BLOCK_SIZE, buffer) io.write(buffer) end end end class ForStringIO < ForIO delegate :size, :to => :target end end end