arsduo/koala

View on GitHub
lib/koala/http_service/uploadable_io.rb

Summary

Maintainability
A
1 hr
Test Coverage
B
83%
require "tempfile"

module Koala
  module HTTPService
    class UploadableIO
      attr_reader :io_or_path, :content_type, :filename

      def initialize(io_or_path_or_mixed, content_type = nil, filename = nil)
        # see if we got the right inputs
        parse_init_mixed_param io_or_path_or_mixed, content_type

        # filename is used in the Ads API
        # if it's provided, take precedence over the detected filename
        # otherwise, fall back to a dummy name
        @filename = filename || @filename || "koala-io-file.dum"

        raise KoalaError.new("Invalid arguments to initialize an UploadableIO") unless @io_or_path
        raise KoalaError.new("Unable to determine MIME type for UploadableIO") if !@content_type
      end

      def to_upload_io
        UploadIO.new(@io_or_path, @content_type, @filename)
      end

      def to_file
        @io_or_path.is_a?(String) ? File.open(@io_or_path) : @io_or_path
      end

      def self.binary_content?(content)
        content.is_a?(UploadableIO) || DETECTION_STRATEGIES.detect {|method| send(method, content)}
      end

      private
      DETECTION_STRATEGIES = [
        :sinatra_param?,
        :rails_3_param?,
        :file_param?
      ]

      PARSE_STRATEGIES = [
        :parse_rails_3_param,
        :parse_sinatra_param,
        :parse_file_object,
        :parse_string_path,
        :parse_io
      ]

      def parse_init_mixed_param(mixed, content_type = nil)
        PARSE_STRATEGIES.each do |method|
          send(method, mixed, content_type)
          return if @io_or_path && @content_type
        end
      end

      # Expects a parameter of type ActionDispatch::Http::UploadedFile
      def self.rails_3_param?(uploaded_file)
        uploaded_file.respond_to?(:content_type) and uploaded_file.respond_to?(:tempfile) and uploaded_file.tempfile.respond_to?(:path)
      end

      def parse_rails_3_param(uploaded_file, content_type = nil)
        if UploadableIO.rails_3_param?(uploaded_file)
          @io_or_path = uploaded_file.tempfile.path
          @content_type = content_type || uploaded_file.content_type
          @filename = uploaded_file.original_filename
        end
      end

      # Expects a Sinatra hash of file info
      def self.sinatra_param?(file_hash)
        file_hash.kind_of?(Hash) and file_hash.has_key?(:type) and file_hash.has_key?(:tempfile)
      end

      def parse_sinatra_param(file_hash, content_type = nil)
        if UploadableIO.sinatra_param?(file_hash)
          @io_or_path = file_hash[:tempfile]
          @content_type = content_type || file_hash[:type] || detect_mime_type(tempfile)
          @filename = file_hash[:filename]
        end
      end

      # takes a file object
      def self.file_param?(file)
        file.kind_of?(File) || file.kind_of?(Tempfile)
      end

      def parse_file_object(file, content_type = nil)
        if UploadableIO.file_param?(file)
          @io_or_path = file
          @content_type = content_type || detect_mime_type(file.path)
          @filename = File.basename(file.path)
        end
      end

      def parse_string_path(path, content_type = nil)
        if path.kind_of?(String)
          @io_or_path = path
          @content_type = content_type || detect_mime_type(path)
          @filename = File.basename(path)
        end
      end

      def parse_io(io, content_type = nil)
        if io.respond_to?(:read)
          @io_or_path = io
          @content_type = content_type
        end
      end

      MIME_TYPE_STRATEGIES = [
        :use_mime_module,
        :use_simple_detection
      ]

      def detect_mime_type(filename)
        if filename
          MIME_TYPE_STRATEGIES.each do |method|
            result = send(method, filename)
            return result if result
          end
        end
        nil # if we can't find anything
      end

      def use_mime_module(filename)
        # if the user has installed mime/types, we can use that
        # if not, rescue and return nil
        begin
          type = MIME::Types.type_for(filename).first
          type ? type.to_s : nil
        rescue
          nil
        end
      end

      def use_simple_detection(filename)
        # very rudimentary extension analysis for images
        # first, get the downcased extension, or an empty string if it doesn't exist
        extension = ((filename.match(/\.([a-zA-Z0-9]+)$/) || [])[1] || "").downcase
        case extension
          when ""
            nil
          # images
          when "jpg", "jpeg"
            "image/jpeg"
          when "png"
            "image/png"
          when "gif"
            "image/gif"

          # video
          when "3g2"
              "video/3gpp2"
          when "3gp", "3gpp"
              "video/3gpp"
          when "asf"
              "video/x-ms-asf"
          when "avi"
              "video/x-msvideo"
          when "flv"
              "video/x-flv"
          when "m4v"
              "video/x-m4v"
          when "mkv"
              "video/x-matroska"
          when "mod"
              "video/mod"
          when "mov", "qt"
              "video/quicktime"
          when "mp4", "mpeg4"
              "video/mp4"
          when "mpe", "mpeg", "mpg", "tod", "vob"
              "video/mpeg"
          when "nsv"
              "application/x-winamp"
          when "ogm", "ogv"
              "video/ogg"
          when "wmv"
              "video/x-ms-wmv"
        end
      end
    end
  end
end