Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
268 lines (226 sloc) 7.765 kB
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
alias_method :bytesize, :length
def initialize(environment, logical_path, pathname)
raise ArgumentError, "Asset logical path has no extension: #{logical_path}" if File.extname(logical_path) == ""
@root = environment.root
@logical_path = logical_path.to_s
@pathname = Pathname.new(pathname)
@content_type = environment.content_type_of(pathname)
# drop precision to 1 second, same pattern followed elsewhere
@mtime = Time.at(environment.stat(pathname).mtime.to_i)
@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']
@mtime = Time.at(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.to_i
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'
FileUtils.mkdir_p File.dirname(filename)
File.open("#{filename}+", 'wb') do |f|
if options[:compress]
# Run contents through `Zlib`
gz = Zlib::GzipWriter.new(f, Zlib::BEST_COMPRESSION)
gz.mtime = mtime.to_i
gz.write to_s
gz.close
else
# Write out as is
f.write to_s
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 mtime 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.
#
# Note: to_i is used in eql? and write_to we assume fidelity of 1 second
# if people save files more frequently than 1 second sprockets may
# not pick it up, by design
if mtime.to_i >= stat.mtime.to_i
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
Jump to Line
Something went wrong with that request. Please try again.