lib/palimpsest/utils.rb
require 'mkmf'
require 'open3'
module Palimpsest
# Utility module.
module Utils
# Available backends for {.copy_directory}.
COPY_BACKENDS = %i(rsync)
# Available backends for {.search_files}.
FILE_SEARCH_BACKENDS = %i(grep)
# Write contents to file.
# @param contents [String]
# @param file [String]
def self.write_to_file(contents, file, preserve: false)
original_time = File.mtime file if preserve
File.open(file, 'w') { |f| f.write contents }
File.utime original_time, original_time, file if preserve
end
# Copy contents of a directory.
# When using the `:rsync` backend,
# `exclude` should be a list of rsync exclude patterns.
# @param source [String] directory to copy
# @param destination [String] where to copy contents of the directory
# @param exclude [Symbol] files and directories to exclude from copy
# @param mirror [Boolean] remove any non-excluded paths from destination
# @param backend [Symbol] copy backed to use
def self.copy_directory(source, destination, exclude: [], mirror: false, backend: :auto)
available_backends = backends(COPY_BACKENDS)
backend = available_backends.first if backend == :auto
fail 'Requested copy backend not available.' unless available_backends.include? backend
send "copy_directory_with_#{backend}".to_sym,
source, destination,
exclude: exclude, mirror: mirror
end
# Search all non-binary files against regular expression.
# When using the `:grep` backend,
# `regex` should be an extended regular expression as a string.
# @param regex [Regexp, String] regular expression to match
# @param path [String] where to search
# @param backend [Symbol] search backed to use
# @return [Array] matched files
def self.search_files(regex, path, backend: :auto)
available_backends = backends(FILE_SEARCH_BACKENDS)
backend = available_backends.first if backend == :auto
fail 'Requested search backend not available.' unless available_backends.include? backend
send "search_files_with_#{backend}".to_sym, regex, path
end
# Forbids use of `../` and `~/` in path.
# Forbids absolute paths.
# @param path [String]
# @return [Boolean]
def self.safe_path?(path)
case
when path[%r{(\.\./|~/)}] then return false
when path[%r{^/}] then return false
else return true
end
end
private
# Checks if command exists.
# @param command [String] command name to check
# @return [String, nil] full path to command or nil if not found
def self.command?(command)
MakeMakefile::Logging.instance_variable_set :@logfile, File::NULL
MakeMakefile::Logging.quiet = true
MakeMakefile.find_executable command.to_s
end
# Determines available backends from list of commands
# by checking if each command exists.
# The backend `:stdlib` is always available if given.
def self.backends(commands)
backends = []
commands.each do |backend|
backends << backend if command?(backend) || backend == :stdlib
end
backends
end
#
# Backend specific methods below.
#
# @todo Add support for stdlib backend.
def self.copy_directory_with_stdlib(_source, _destination, _exclude: [], _mirror: false)
fail 'Directory copy via stdlib not yet implemented.'
end
def self.copy_directory_with_rsync(source, destination, exclude: [], mirror: false)
cmd = %w(rsync -rt)
cmd << '--del' if mirror
exclude.each { |e| cmd << %(--exclude=#{e}) }
cmd << "#{source}/"
cmd << destination
system(*cmd)
end
# @todo Add support for stdlib backend.
def self.search_files_with_stdlib(_source, _destination, _exclude: [])
fail 'File search via stdlib not yet implemented.'
end
def self.search_files_with_grep(regex, path)
fail 'Must specify regex as string.' unless regex.is_a? String
cmd = ['grep']
cmd.concat %w(-l -I -r -E)
cmd << regex
cmd << path
files = []
Open3.capture2(*cmd).first.each_line { |l| files << l.chomp unless l.empty? }
files
end
end
end