require "net/ftp" require "pathname" require_relative "adapter" require_relative "errors" module VagrantPlugins module FTPPush class Push < Vagrant.plugin("2", :push) IGNORED_FILES = %w(. ..).freeze DEFAULT_EXCLUDES = %w(.git .hg .svn .vagrant).freeze def initialize(*) super @logger = Log4r::Logger.new("vagrant::pushes::ftp") end def push # Grab files early so if there's an exception or issue, we don't have to # wait and close the (S)FTP connection as well files = nil begin files = Hash[*all_files.flat_map do |file| relative_path = relative_path_for(file, config.dir) destination = File.join(config.destination, relative_path) file = File.expand_path(file, env.root_path) [file, destination] end] rescue SystemStackError raise Errors::TooManyFiles end ftp = "#{config.username}@#{config.host}:#{config.destination}" env.ui.info "Uploading #{env.root_path} to #{ftp}" connect do |ftp| files.each do |local, remote| @logger.info "Uploading #{local} => #{remote}" ftp.upload(local, remote) end end end # Helper method for creating the FTP or SFTP connection. # @yield [Adapter] def connect(&block) klass = config.secure ? SFTPAdapter : FTPAdapter ftp = klass.new(config.host, config.username, config.password, passive: config.passive) ftp.connect(&block) end # The list of all files that should be pushed by this push. This method # only returns **files**, not folders or symlinks! # @return [Array] def all_files files = glob("#{config.dir}/**/*") + includes_files filter_excludes!(files, config.excludes) files.reject! { |f| !File.file?(f) } files end # The list of files to include in addition to those specified in `dir`. # @return [Array] def includes_files includes = config.includes.flat_map do |i| path = absolute_path_for(i, config.dir) [path, "#{path}/**/*"] end glob("{#{includes.join(",")}}") end # Filter the excludes out of the given list. This method modifies the # given list in memory! # # @param [Array] list # the filepaths # @param [Array] excludes # the exclude patterns or files def filter_excludes!(list, excludes) excludes = Array(excludes) excludes = excludes + DEFAULT_EXCLUDES excludes = excludes.flat_map { |e| [e, "#{e}/*"] } list.reject! do |file| basename = relative_path_for(file, config.dir) # Handle the special case where the file is outside of the working # directory... if basename.start_with?("../") basename = file end excludes.any? { |e| File.fnmatch?(e, basename, File::FNM_DOTMATCH) } end end # Get the list of files that match the given pattern. # @return [Array] def glob(pattern) Dir.glob(pattern, File::FNM_DOTMATCH).sort.reject do |file| IGNORED_FILES.include?(File.basename(file)) end end # The absolute path to the given `path` and `parent`, unless the given # path is absolute. # @return [String] def absolute_path_for(path, parent) path = Pathname.new(path) return path if path.absolute? File.expand_path(path, parent) end # The relative path from the given `parent`. If files exist on another # device, this will probably blow up. # @return [String] def relative_path_for(path, parent) Pathname.new(path).relative_path_from(Pathname.new(parent)).to_s rescue ArgumentError return path end end end end