require 'cgi' require 'fileutils' # Must specify absolute path of ::Archive when using # this module to avoid conflicts with Fig::Statement::Archive require 'libarchive_ruby' require 'rbconfig' require 'fig/at_exit' require 'fig/environment_variables/case_insensitive' require 'fig/environment_variables/case_sensitive' require 'fig/logging' require 'fig/network_error' require 'fig/protocol/file' require 'fig/protocol/ftp' require 'fig/protocol/http' require 'fig/protocol/sftp' require 'fig/protocol/ssh' require 'fig/repository_error' require 'fig/url' require 'fig/user_input_error' module Fig; end # Does things requiring real O/S interaction, primarilly taking care of file # transfers and running external commands. class Fig::OperatingSystem WINDOWS_FILE_NAME_ILLEGAL_CHARACTERS = %w[ \\ / : * ? " < > | ] UNIX_FILE_NAME_ILLEGAL_CHARACTERS = %w[ / ] def self.windows? return !! (RbConfig::CONFIG['host_os'] =~ /mswin|mingw/) end def self.unix? ! Fig::OperatingSystem.windows? end def self.file_name_illegal_characters() if Fig::OperatingSystem.windows? return WINDOWS_FILE_NAME_ILLEGAL_CHARACTERS end return UNIX_FILE_NAME_ILLEGAL_CHARACTERS end def self.wrap_variable_name_with_shell_expansion(variable_name) if Fig::OperatingSystem.windows? return "%#{variable_name}%" else return "$#{variable_name}" end end def self.get_environment_variables(initial_values = nil) if Fig::OperatingSystem.windows? return Fig::EnvironmentVariables::CaseInsensitive.new(initial_values) end return Fig::EnvironmentVariables::CaseSensitive.new(initial_values) end def initialize(login) @protocols = {} @protocols['file'] = Fig::Protocol::File.new @protocols['ftp'] = Fig::Protocol::FTP.new login @protocols['http'] = Fig::Protocol::HTTP.new @protocols['sftp'] = Fig::Protocol::SFTP.new @protocols['ssh'] = Fig::Protocol::SSH.new end def list(dir) Dir.entries(dir) - ['.', '..'] end def write(path, content) File.open(path, 'wb') { |f| f.binmode; f << content } end def download_list(url) begin protocol, uri = decode_protocol url return protocol.download_list uri rescue SocketError => error Fig::Logging.debug error.message raise Fig::NetworkError.new "#{url}: #{error.message}" rescue Errno::ETIMEDOUT => error Fig::Logging.debug error.message raise Fig::NetworkError.new "#{url}: #{error.message}" end end # Determine whether we need to update something. Returns nil to indicate # "don't know". def path_up_to_date?(url, path, prompt_for_login) return false if ! File.exist? path protocol, uri = decode_protocol url return protocol.path_up_to_date? uri, path, prompt_for_login end # Returns whether the file was not downloaded because the file already # exists and is already up-to-date. def download(url, path, prompt_for_login) protocol, uri = decode_protocol url FileUtils.mkdir_p(File.dirname path) return protocol.download uri, path, prompt_for_login end # Returns the basename and full path to the download. def download_resource(url, download_directory) FileUtils.mkdir_p(download_directory) basename = CGI.unescape Fig::URL.parse(url).path.split('/').last path = File.join(download_directory, basename) download(url, path, false) return basename, path end def download_and_unpack_archive(url, download_directory, unpack_directory) basename, path = download_resource(url, download_directory) case path when /\.tar\.gz$/ unpack_archive(unpack_directory, path) when /\.tgz$/ unpack_archive(unpack_directory, path) when /\.tar\.bz2$/ unpack_archive(unpack_directory, path) when /\.zip$/ unpack_archive(unpack_directory, path) else Fig::Logging.fatal "Unknown archive type: #{basename}" raise Fig::NetworkError.new("Unknown archive type: #{basename}") end return end def upload(local_file, remote_file) Fig::Logging.debug "Uploading #{local_file} to #{remote_file}." protocol, uri = decode_protocol remote_file protocol.upload local_file, uri return end def delete_and_recreate_directory(dir) FileUtils.rm_rf(dir) FileUtils.mkdir_p(dir) end def copy(source, target, msg = nil) if File.directory?(source) FileUtils.mkdir_p(target) Dir.foreach(source) do |child| if child != '.' and child != '..' copy(File.join(source, child), File.join(target, child), msg) end end else if ! File.exist?(target) || File.mtime(source) != File.mtime(target) Fig::Logging.info "#{msg} #{target}" if msg FileUtils.mkdir_p(File.dirname(target)) FileUtils.cp(source, target) File.utime(File.atime(source), File.mtime(source), target) end end end def move_file(directory, from, to) Dir.chdir(directory) { FileUtils.mv(from, to, :force => true) } end # Expects paths_to_archive as an Array of paths. def create_archive(archive_name, paths_to_archive) # TODO: Need to verify files_to_archive exists. ::Archive.write_open_filename( archive_name, ::Archive::COMPRESSION_GZIP, ::Archive::FORMAT_TAR ) do |writer| paths_to_archive.each do |path| add_path_to_archive(path, writer) end end end # This method can handle the following archive types: # .tar.bz2 # .tar.gz # .tgz # .zip def unpack_archive(directory, archive_path) FileUtils.mkdir_p directory Dir.chdir(directory) do if ! File.exists? archive_path raise Fig::RepositoryError.new "#{archive_path} does not exist." end running_on_windows = Fig::OperatingSystem.windows? ::Archive.read_open_filename(archive_path) do |reader| while entry = reader.next_header if running_on_windows check_archive_entry_for_windows entry, archive_path end begin reader.extract(entry) rescue Archive::Error => exception # Nice how the error message doesn't include any information about # what was having the problem. message = exception.message.sub(/^Extract archive failed: /, '') new_exception = Fig::RepositoryError.new( "Could not extract #{entry.pathname} from #{archive_path}: #{message}" ) new_exception.set_backtrace exception.backtrace raise new_exception end end end end end def shell_exec(command) if Fig::OperatingSystem.windows? plain_exec [ ENV['ComSpec'], '/c', command ] else plain_exec [ ENV['SHELL'] || '/bin/sh', '-c', command ] end end def plain_exec(command) # Kernel#exec won't run Kernel#at_exit handlers. Fig::AtExit.execute() if ENV['FIG_COVERAGE'] SimpleCov.at_exit.call end begin Kernel.exec(*command) rescue SystemCallError => exception raise Fig::UserInputError.new exception end end # *sigh* Apparently Ruby < v1.9.3 does some wacko thing with single argument # exec that causes it to not invoke the shell, so we've got this mess. def plain_or_shell_exec(command) if command.size > 1 plain_exec(command) else shell_exec(command[0]) end end private def decode_protocol(url) uri = Fig::URL.parse(url) protocol = @protocols[uri.scheme] raise_unknown_protocol(url) if protocol.nil? return protocol, uri end def add_path_to_archive(path, archive_writer) children = [] archive_writer.new_entry do |entry| entry.copy_lstat(path) entry.pathname = path if entry.symbolic_link? linked = File.readlink(path) entry.symlink = linked end archive_writer.write_header(entry) if entry.regular? archive_writer.write_data(open(path) {|f| f.binmode; f.read }) elsif entry.directory? children = Dir.entries(path) - ['.', '..'] end end children.each do |child| add_path_to_archive(File.join(path, child), archive_writer) end return end def check_archive_entry_for_windows(entry, archive_path) bad_type = nil if entry.symbolic_link? bad_type = 'symbolic link' elsif entry.block_special? bad_type = 'block device' elsif entry.character_special? bad_type = 'character device' elsif entry.fifo? bad_type = 'pipe' elsif entry.socket? bad_type = 'socket' end if bad_type raise Fig::RepositoryError.new( "Could not extract #{entry.pathname} from #{archive_path} because it is a #{bad_type}." ) end return end def raise_unknown_protocol(url) Fig::Logging.fatal %Q raise Fig::NetworkError.new( %Q ) return end end