lib/sprockets/asset.rb



require 'time'

module Sprockets
  # `Asset` is the base class for `BundledAsset` and `StaticAsset`.
  class Asset
    # Internal initializer to load `Asset` from serialized `Hash`.
    def self.from_hash(environment, hash)
      asset = allocate
      asset.init_with(environment, hash)
      asset
    end

    # Define base set of attributes to be serialized.
    def self.serialized_attributes
      %w( id logical_path pathname )
    end

    attr_reader :environment
    attr_reader :id, :logical_path, :pathname

    def initialize(environment, logical_path, pathname)
      @environment  = environment
      @logical_path = logical_path.to_s
      @pathname     = Pathname.new(pathname)
      @id           = environment.digest.update(object_id.to_s).to_s
    end

    # Initialize `Asset` from serialized `Hash`.
    def init_with(environment, coder)
      @environment = environment
      @pathname = @mtime = @length = nil

      self.class.serialized_attributes.each do |attr|
        instance_variable_set("@#{attr}", coder[attr].to_s) if coder[attr]
      end

      if @pathname && @pathname.is_a?(String)
        # Expand `$root` placeholder and wrapper string in a `Pathname`
        @pathname = Pathname.new(expand_root_path(@pathname))
      end

      if @mtime && @mtime.is_a?(String)
        # Parse time string
        @mtime = Time.parse(@mtime)
      end

      if @length && @length.is_a?(String)
        # Convert length to an `Integer`
        @length = Integer(@length)
      end
    end

    # Copy serialized attributes to the coder object
    def encode_with(coder)
      coder['class'] = self.class.name.sub(/Sprockets::/, '')

      self.class.serialized_attributes.each do |attr|
        value = send(attr)
        coder[attr] = case value
          when Time
            value.iso8601
          else
            value.to_s
          end
      end

      coder['pathname'] = relativize_root_path(coder['pathname'])
    end

    # Returns `Content-Type` from pathname.
    def content_type
      @content_type ||= environment.content_type_of(pathname)
    end

    # Get mtime at the time the `Asset` is built.
    def mtime
      @mtime ||= environment.stat(pathname).mtime
    end

    # Get length at the time the `Asset` is built.
    def length
      @length ||= environment.stat(pathname).size
    end

    # Get content digest at the time the `Asset` is built.
    def digest
      @digest ||= environment.file_digest(pathname).hexdigest
    end

    # Return logical path with digest spliced in.
    #
    #   "foo/bar-37b51d194a7513e45b56f6524f2d51f2.js"
    #
    def digest_path
      environment.attributes_for(logical_path).path_with_fingerprint(digest)
    end

    # Return an `Array` of `Asset` files that are declared dependencies.
    def dependencies
      []
    end

    # Expand asset into an `Array` of parts.
    #
    # Appending all of an assets body parts together should give you
    # the asset's contents as a whole.
    #
    # This allows you to link to individual files for debugging
    # purposes.
    def to_a
      [self]
    end

    # Add enumerator to allow `Asset` instances to be used as Rack
    # compatible body objects.
    def each
      yield to_s
    end

    # Checks if Asset is fresh by comparing the actual mtime and
    # digest to the inmemory model.
    #
    # Used to test if cached models need to be rebuilt.
    #
    # Subclass must override `fresh?` or `stale?`.
    def fresh?
      !stale?
    end

    # Checks if Asset is stale by comparing the actual mtime and
    # digest to the inmemory model.
    #
    # Subclass must override `fresh?` or `stale?`.
    def stale?
      !fresh?
    end

    # Pretty inspect
    def inspect
      "#<#{self.class}:0x#{object_id.to_s(16)} " +
        "pathname=#{pathname.to_s.inspect}, " +
        "mtime=#{mtime.inspect}, " +
        "digest=#{digest.inspect}" +
        ">"
    end

    # Assets are equal if they share the same path, mtime and digest.
    def eql?(other)
      other.class == self.class &&
        other.relative_pathname == self.relative_pathname &&
        other.mtime.to_i == self.mtime.to_i &&
        other.digest == self.digest
    end
    alias_method :==, :eql?

    protected
      # Get pathname with its root stripped.
      def relative_pathname
        Pathname.new(relativize_root_path(pathname))
      end

      # Replace `$root` placeholder with actual environment root.
      def expand_root_path(path)
        environment.attributes_for(path).expand_root
      end

      # Replace actual environment root with `$root` placeholder.
      def relativize_root_path(path)
        environment.attributes_for(path).relativize_root
      end

      # Check if dependency is fresh.
      #
      # `dep` is a `Hash` with `path`, `mtime` and `hexdigest` keys.
      #
      # A `Hash` is used rather than other `Asset` object because we
      # want to test non-asset files and directories.
      def dependency_fresh?(dep = {})
        path, mtime, hexdigest = dep.values_at('path', 'mtime', 'hexdigest')

        stat = environment.stat(path)

        # If path no longer exists, its definitely stale.
        if stat.nil?
          return false
        end

        # Compare dependency mime to the actual mtime. If the
        # dependency mtime is newer than the actual mtime, the file
        # hasn't changed since we created this `Asset` instance.
        #
        # However, if the mtime is newer it doesn't mean the asset is
        # stale. Many deployment environments may recopy or recheckout
        # assets on each deploy. In this case the mtime would be the
        # time of deploy rather than modified time.
        if mtime >= stat.mtime
          return true
        end

        digest = environment.file_digest(path)

        # If the mtime is newer, do a full digest comparsion. Return
        # fresh if the digests match.
        if hexdigest == digest.hexdigest
          return true
        end

        # Otherwise, its stale.
        false
      end
  end
end