# frozen_string_literal: true
module Sprockets
# Internal: File and path related utilities. Mixed into Environment.
#
# Probably would be called FileUtils, but that causes namespace annoyances
# when code actually wants to reference ::FileUtils.
module PathUtils
extend self
require 'pathname'
# Public: Like `File.stat`.
#
# path - String file or directory path
#
# Returns nil if the file does not exist.
def stat(path)
if File.exist?(path)
File.stat(path.to_s)
else
nil
end
end
# Public: Like `File.file?`.
#
# path - String file path.
#
# Returns true path exists and is a file.
def file?(path)
if stat = self.stat(path)
stat.file?
else
false
end
end
# Public: Like `File.directory?`.
#
# path - String file path.
#
# Returns true path exists and is a directory.
def directory?(path)
if stat = self.stat(path)
stat.directory?
else
false
end
end
# Public: A version of `Dir.entries` that filters out `.` files and `~`
# swap files.
#
# path - String directory path
#
# Returns an empty `Array` if the directory does not exist.
def entries(path)
if File.directory?(path)
entries = Dir.entries(path, encoding: Encoding.default_internal)
entries.reject! { |entry|
entry.start_with?(".".freeze) ||
(entry.start_with?("#".freeze) && entry.end_with?("#".freeze)) ||
entry.end_with?("~".freeze)
}
entries.sort!
entries
else
[]
end
end
# Public: Check if path is absolute or relative.
#
# path - String path.
#
# Returns true if path is absolute, otherwise false.
if File::ALT_SEPARATOR
# On Windows, ALT_SEPARATOR is \
# Delegate to Pathname since the logic gets complex.
def absolute_path?(path)
Pathname.new(path).absolute?
end
else
def absolute_path?(path)
path.start_with?(File::SEPARATOR)
end
end
if File::ALT_SEPARATOR
SEPARATOR_PATTERN = "#{Regexp.quote(File::SEPARATOR)}|#{Regexp.quote(File::ALT_SEPARATOR)}"
else
SEPARATOR_PATTERN = "#{Regexp.quote(File::SEPARATOR)}"
end
# Public: Check if path is explicitly relative.
# Starts with "./" or "../".
#
# path - String path.
#
# Returns true if path is relative, otherwise false.
def relative_path?(path)
path =~ /^\.\.?($|#{SEPARATOR_PATTERN})/ ? true : false
end
# Public: Get relative path from `start` to `dest`.
#
# start - String start path (file or dir)
# dest - String destination path
#
# Returns relative String path from `start` to `dest`
def relative_path_from(start, dest)
start, dest = Pathname.new(start), Pathname.new(dest)
start = start.dirname unless start.directory?
dest.relative_path_from(start).to_s
end
# Public: Joins path to base path.
#
# base - Root path
# path - Extending path
#
# Example
#
# join('base/path/', '../file.js')
# # => 'base/file.js'
#
# Returns string path starting from base and ending at path
def join(base, path)
(Pathname.new(base) + path).to_s
end
# Public: Sets pipeline for path
#
# path - String path
# extensions - List of file extensions
# pipeline - Pipeline
#
# Examples
#
# set_pipeline('path/file.js.erb', config[:mime_exts], config[:pipeline_exts], :source)
# # => 'path/file.source.js.erb'
#
# set_pipeline('path/some.file.source.js.erb', config[:mime_exts], config[:pipeline_exts], :debug)
# # => 'path/some.file.debug.js.erb'
#
# Returns string path with pipeline parsed in
def set_pipeline(path, mime_exts, pipeline_exts, pipeline)
extension, _ = match_path_extname(path, mime_exts)
path.chomp!(extension)
pipeline_old, _ = match_path_extname(path, pipeline_exts)
path.chomp!(pipeline_old)
"#{path}.#{pipeline}#{extension}"
end
# Internal: Get relative path for root path and subpath.
#
# path - String path
# subpath - String subpath of path
#
# Returns relative String path if subpath is a subpath of path, or nil if
# subpath is outside of path.
def split_subpath(path, subpath)
return "" if path == subpath
path = File.join(path, ''.freeze)
if subpath.start_with?(path)
subpath[path.length..-1]
else
nil
end
end
# Internal: Detect root path and base for file in a set of paths.
#
# paths - Array of String paths
# filename - String path of file expected to be in one of the paths.
#
# Returns [String root, String path]
def paths_split(paths, filename)
paths.each do |path|
if subpath = split_subpath(path, filename)
return path, subpath
end
end
nil
end
# Internal: Get path's extensions.
#
# path - String
#
# Returns an Array of String extnames.
def path_extnames(path)
File.basename(path).scan(/\.[^.]+/)
end
# Internal: Match path extnames against available extensions.
#
# path - String
# extensions - Hash of String extnames to values
#
# Returns [String extname, Object value] or nil nothing matched.
def match_path_extname(path, extensions)
basename = File.basename(path)
i = basename.index('.'.freeze)
while i && i < basename.length - 1
extname = basename[i..-1]
if value = extensions[extname]
return extname, value
end
i = basename.index('.'.freeze, i+1)
end
nil
end
# Internal: Match paths in a directory against available extensions.
#
# path - String directory
# basename - String basename of target file
# extensions - Hash of String extnames to values
#
# Examples
#
# exts = { ".js" => "application/javascript" }
# find_matching_path_for_extensions("app/assets", "application", exts)
# # => ["app/assets/application.js", "application/javascript"]
#
# Returns an Array of [String path, Object value] matches.
def find_matching_path_for_extensions(path, basename, extensions)
matches = []
entries(path).each do |entry|
next unless File.basename(entry).start_with?(basename)
extname, value = match_path_extname(entry, extensions)
if basename == entry.chomp(extname)
filename = File.join(path, entry)
if file?(filename)
matches << [filename, value]
end
end
end
matches
end
# Internal: Returns all parents for path
#
# path - String absolute filename or directory
# root - String path to stop at (default: system root)
#
# Returns an Array of String paths.
def path_parents(path, root = nil)
root = "#{root}#{File::SEPARATOR}" if root
parents = []
loop do
parent = File.dirname(path)
break if parent == path
break if root && !path.start_with?(root)
parents << path = parent
end
parents
end
# Internal: Find target basename checking upwards from path.
#
# basename - String filename: ".sprocketsrc"
# path - String path to start search: "app/assets/javascripts/app.js"
# root - String path to stop at (default: system root)
#
# Returns String filename or nil.
def find_upwards(basename, path, root = nil)
path_parents(path, root).each do |dir|
filename = File.join(dir, basename)
return filename if file?(filename)
end
nil
end
# Public: Stat all the files under a directory.
#
# dir - A String directory
#
# Returns an Enumerator of [path, stat].
def stat_directory(dir)
return to_enum(__method__, dir) unless block_given?
self.entries(dir).each do |entry|
path = File.join(dir, entry)
if stat = self.stat(path)
yield path, stat
end
end
nil
end
# Public: Recursive stat all the files under a directory.
#
# dir - A String directory
#
# Returns an Enumerator of [path, stat].
def stat_tree(dir, &block)
return to_enum(__method__, dir) unless block_given?
self.stat_directory(dir) do |path, stat|
yield path, stat
if stat.directory?
stat_tree(path, &block)
end
end
nil
end
# Public: Recursive stat all the files under a directory in alphabetical
# order.
#
# dir - A String directory
#
# Returns an Enumerator of [path, stat].
def stat_sorted_tree(dir, &block)
return to_enum(__method__, dir) unless block_given?
self.stat_directory(dir).sort_by { |path, stat|
stat.directory? ? "#{path}/" : path
}.each do |path, stat|
yield path, stat
if stat.directory?
stat_sorted_tree(path, &block)
end
end
nil
end
# Public: Write to a file atomically. Useful for situations where you
# don't want other processes or threads to see half-written files.
#
# Utils.atomic_write('important.file') do |file|
# file.write('hello')
# end
#
# Returns nothing.
def atomic_write(filename)
dirname, basename = File.split(filename)
basename = [
basename,
Thread.current.object_id,
Process.pid,
rand(1000000)
].join('.'.freeze)
tmpname = File.join(dirname, basename)
File.open(tmpname, 'wb+') do |f|
yield f
end
File.rename(tmpname, filename)
ensure
File.delete(tmpname) if File.exist?(tmpname)
end
end
end