Chris Roberts ac8ad59823 Provide custom error when attempting to push too many files with FTP
When the configured directory for FTP push has too many files, it will
generate an exception and fail due to a stack overflow. When this happens
just rescue out the exception and re-raise a custom error to provide
some context to the user on the actual problem.
2018-06-26 08:28:01 -07:00

127 lines
3.9 KiB
Ruby

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<String>]
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<String>]
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<String>] list
# the filepaths
# @param [Array<String>] 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<String>]
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