123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258 |
- require 'time'
- require 'set'
- 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)
- return unless hash.is_a?(Hash)
- klass = case hash['class']
- when 'BundledAsset'
- BundledAsset
- when 'ProcessedAsset'
- ProcessedAsset
- when 'StaticAsset'
- StaticAsset
- else
- nil
- end
- if klass
- asset = klass.allocate
- asset.init_with(environment, hash)
- asset
- end
- rescue UnserializeError
- nil
- end
- attr_reader :logical_path, :pathname
- attr_reader :content_type, :mtime, :length, :digest
- def initialize(environment, logical_path, pathname)
- @root = environment.root
- @logical_path = logical_path.to_s
- @pathname = Pathname.new(pathname)
- @content_type = environment.content_type_of(pathname)
- @mtime = environment.stat(pathname).mtime
- @length = environment.stat(pathname).size
- @digest = environment.file_digest(pathname).hexdigest
- end
- # Initialize `Asset` from serialized `Hash`.
- def init_with(environment, coder)
- @root = environment.root
- @logical_path = coder['logical_path']
- @content_type = coder['content_type']
- @digest = coder['digest']
- if pathname = coder['pathname']
- # Expand `$root` placeholder and wrapper string in a `Pathname`
- @pathname = Pathname.new(expand_root_path(pathname))
- end
- if mtime = coder['mtime']
- # Parse time string
- @mtime = Time.parse(mtime)
- end
- if length = coder['length']
- # 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::/, '')
- coder['logical_path'] = logical_path
- coder['pathname'] = relativize_root_path(pathname).to_s
- coder['content_type'] = content_type
- coder['mtime'] = mtime.iso8601
- coder['length'] = length
- coder['digest'] = digest
- end
- # Return logical path with digest spliced in.
- #
- # "foo/bar-37b51d194a7513e45b56f6524f2d51f2.js"
- #
- def digest_path
- logical_path.sub(/\.(\w+)$/) { |ext| "-#{digest}#{ext}" }
- 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
- # `body` is aliased to source by default if it can't have any dependencies.
- def body
- source
- end
- # Return `String` of concatenated source.
- def to_s
- source
- 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.
- def fresh?(environment)
- # Check current mtime and digest
- dependency_fresh?(environment, self)
- 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?(environment)
- !fresh?(environment)
- end
- # Save asset to disk.
- def write_to(filename, options = {})
- # Gzip contents if filename has '.gz'
- options[:compress] ||= File.extname(filename) == '.gz'
- File.open("#{filename}+", 'wb') do |f|
- if options[:compress]
- # Run contents through `Zlib`
- gz = Zlib::GzipWriter.new(f, Zlib::BEST_COMPRESSION)
- gz.write to_s
- gz.close
- else
- # Write out as is
- f.write to_s
- f.close
- end
- end
- # Atomic write
- FileUtils.mv("#{filename}+", filename)
- # Set mtime correctly
- File.utime(mtime, mtime, filename)
- nil
- ensure
- # Ensure tmp file gets cleaned up
- FileUtils.rm("#{filename}+") if File.exist?("#{filename}+")
- 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
- def hash
- digest.hash
- end
- # Assets are equal if they share the same path, mtime and digest.
- def eql?(other)
- other.class == self.class &&
- other.logical_path == self.logical_path &&
- other.mtime.to_i == self.mtime.to_i &&
- other.digest == self.digest
- end
- alias_method :==, :eql?
- protected
- # Internal: String paths that are marked as dependencies after processing.
- #
- # Default to an empty `Array`.
- def dependency_paths
- @dependency_paths ||= []
- end
- # Internal: `ProccessedAsset`s that are required after processing.
- #
- # Default to an empty `Array`.
- def required_assets
- @required_assets ||= []
- end
- # Get pathname with its root stripped.
- def relative_pathname
- @relative_pathname ||= Pathname.new(relativize_root_path(pathname))
- end
- # Replace `$root` placeholder with actual environment root.
- def expand_root_path(path)
- path.to_s.sub(/^\$root/, @root)
- end
- # Replace actual environment root with `$root` placeholder.
- def relativize_root_path(path)
- path.to_s.sub(/^#{Regexp.escape(@root)}/, '$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?(environment, dep)
- path, mtime, hexdigest = dep.pathname.to_s, dep.mtime, dep.digest
- 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
|