# frozen_string_literal: true require 'net/ssh' require 'net/sftp' require 'rainbow' require 'set' module Zine # Deploy changes to a remote host, via SFTP class UploaderSFTP # a folder in a path Node = Struct.new(:name, :path_string) def initialize(build_dir, options, credentials, delete_file_array, upload_file_array) return unless options['method'] == 'sftp' @build_dir = build_dir @host = options['host'] @path = options['path_or_repo'] @verbose = options['verbose'] @credentials = credentials @delete_file_array = delete_file_array @upload_file_array = upload_file_array end def upload delete deploy rescue Errno::ENETUNREACH puts Rainbow("Unable to connect to #{@host}").red rescue Net::SSH::AuthenticationFailed, NameError puts Rainbow("Authentication failed for #{@host}").red puts 'Check your credential file, and maybe run ssh-add?' end private def break_strings_into_arrays(paths_array) paths_array.map do |dir| dir.to_s.split File::SEPARATOR end end def delete Net::SFTP.start(@host, @credentials['username'], password: @credentials['password'], auth_methods: %w[publickey password]) do |sftp| @delete_file_array.each do |rel_file_path| sftp.remove(File.join(@path, rel_file_path)).wait puts "Deleted #{rel_file_path}" if @verbose end end end def deploy Net::SFTP.start(@host, @credentials['username'], password: @credentials['password'], auth_methods: %w[publickey password]) do |sftp| deploy_directories sftp deploy_files sftp end end # make directories def deploy_directories(sftp) node_array = make_sparse_node_array @upload_file_array node_array.each do |level| level.each do |node| next if node.nil? path_string = node.path_string mkdir_p(sftp, path_string) puts "mkdir_p #{path_string}" if @verbose end end end # upload files def deploy_files(sftp) @upload_file_array.each do |rel_file_path| sftp.upload(File.join(@build_dir, rel_file_path), File.join(@path, rel_file_path), permissions: 0o644).wait # -rw-r--r-- puts "Uploaded #{rel_file_path}" if @verbose end end # make a sparse matrix as a directory tree, to make mkdir calls efficiently def make_sparse_node_array(file_array) paths_array = remove_filenames file_array # remove file names paths_array = Set.new(paths_array).to_a # remove duplicates paths_array = break_strings_into_arrays paths_array # to array of arrays paths_array = strings_to_nodes paths_array # ...of nodes level_array = transpose paths_array # arrayed by level remove_duplicates level_array # make it sparse end def mkdir_p(sftp, path) sftp.mkdir!(File.join(@path, path), permissions: 0o755) # drwxr-xr-x rescue Net::SFTP::StatusException => e raise if e.code != 4 && e.code != 11 # folder already exists end # for each level, if a node has the same name & same path it's a duplicate def remove_duplicates(level_array) level_array.map(&:uniq) end # array of file paths to array of directory paths def remove_filenames(file_array) file_array.map do |file| path = File.dirname file if path == '.' nil else path end end end # convert directory names to nodes with knowledge of their parentage def strings_to_nodes(paths_array) paths_array.map do |path| path_string_array = path path.each_with_index.map do |elem, index| Node.new(elem, path_string_array[0..index].join(File::SEPARATOR)) end end end # flip it, to get directories by level in a path, rather than by path def transpose(input) result = [] max_size = input.max_by(&:size).size max_size.times do |i| result[i] = Array.new(input.first.size) input.each_with_index { |r, j| result[i][j] = r[i] } end result end end end