module Eco module API module Common module Session class SFTP include Eco::Language::AuxiliarLogger def initialize(enviro:) invalid_env = enviro && !enviro.is_a?(Eco::API::Common::Session::Environment) raise "Required Environment object (enviro:). Given: #{enviro}" if invalid_env @enviro = enviro end def host @host ||= fetch_host end # @see Net::SFTP::Session def sftp_session require "net/sftp" @sftp_session ||= Net::SFTP.start( host, fetch_user, **session_options ) rescue StandardError => err log(:error) { "Could not open SFTP session. Possible misconfiguration: #{err}" } raise end # @see Net::SFTP::Operations::Dir#entries def entries(path) sftp_session.dir.entries(path).sort_by(&:name) end # **Files** of the remote directory. # @see Net::SFTP::Operations::Dir#entries # @param path [String] remote directory `path` # @param pattern [Regexp] if given, filters by using this pattern # @return [Array] def files(path, pattern: nil) entries = entries(path).select(&:file?) return entries unless pattern entries.select {|remote_file| remote_file.name =~ pattern} end # **Folders** of the remote directory. # @see Net::SFTP::Operations::Dir#entries # @param path [String] remote directory `path` # @param pattern [Regexp] if given, filters by using this pattern # @return [Array] def folders(path, pattern: nil) entries = entries(path).select(&:directory?) return entries unless pattern entries.select {|remote_file| remote_file.name =~ pattern} end # @see Net::SFTP::Session#rename def move(fullname_source, fullname_dest, flags = 0x0001, override: true, &callback) sftp_session.rename!(fullname_source, fullname_dest, flags, &callback) true rescue Net::SFTP::StatusException => err case err.code when Net::SFTP::Constants::StatusCodes::FX_FILE_ALREADY_EXISTS log(:waning) { msg = "Remote file '#{fullname_dest}' already exists." msg << "Overriding..." if override } if override sftp_session.remove(fullname_dest) # sftp_session.rename!(fullname_source, fullname_dest, flags, &callback) move(fullname_source, fullname_dest, flags, override: false, &callback) end when Net::SFTP::Constants::StatusCodes::FX_PERMISSION_DENIED, Net::SFTP::Constants::StatusCodes::FX_WRITE_PROTECT log(:error) { "Not allowed to rename '#{fullname_source}' to '#{fullname_dest}'" } when Net::SFTP::Constants::StatusCodes::FX_NO_SUCH_FILE log(:error) { "Remote file '#{fullname_source}' does not exist"} when Net::SFTP::Constants::StatusCodes::FX_NO_SUCH_PATH log(:error) { err } else msg = "Error when trying to move file '#{fullname_source}' to '#{fullname_dest}'" log(:error) { "#{msg}\n#{err}" } # raise err, msg, err.backtrace end false end # Downloads the files specified to a local folder # @see Net::SFTP::Operations::Download # @param files [String, Array] full path to remote file(s) to be downloaded # @param local_folder [String] local destination folder (`"."` if not specified) # @return [Array] list of created files def download(files, local_folder: nil, &block) puts "Creating local files:" created_files = [] [files].flatten.compact.map do |fullname| basename = windows_basename(fullname) dest_fullname = File.join(local_folder || ".", basename) puts " • #{dest_fullname}" created_files << dest_fullname sftp_session.download(fullname, dest_fullname) end.each(&:wait) # run SSH event loop while dw.active? created_files.tap { created_files.each(&block) } end # Upload a file to the specific `remote_folder` def upload(local_file, remote_folder:, gid: nil) return false unless local_file && File.exist?(local_file) dest_file = "#{remote_folder}/#{File.basename(local_file)}" res = sftp_session.upload!(local_file, dest_file) unless gid.nil? attrs = sftp_session.stat!(dest_file) unless gid == attrs.gid flags = {permissions: 0o660, uid: attrs.uid, gid: gid} _stat_res = sftp_session.setstat!(dest_file, flags) end end log(:info) { "Uploaded '#{local_file}' (#{res})" } true end private def session_options { non_interactive: true }.tap do |opts| if password? opts.merge!({password: fetch_password}) else opts.merge!({ keys: fetch_key_files, keys_only: true }) end end end def windows_basename(remote_fullname) dir_sep = /[\\\/]/ patr_re = /[<>:\\\/|?*]/ parts = remote_fullname.split(dir_sep).map do |node| node.gsub(patr_re, '_') end local_fullname = File.join(*parts) File.basename(local_fullname) end def logger @enviro&.logger || (defined?(super)? super : ::Logger.new(IO::NULL)) end def config @enviro.config || {} end def fetch_host config.sftp.host || ENV['SFTP_HOST'] end def fetch_user config.sftp.user || ENV['SFTP_USERNAME'] end def password? !!fetch_password end def fetch_password config.sftp.password || ENV['SFTP_PASSWORD'] end def fetch_key_files [config.sftp.key_file || ENV['SFTP_KEY_FILE']] end def fetch_base_path config.sftp.base_path || ENV['SFTP_BASE_PATH'] end end end end end end