Search code examples
rubynet-sftp

Ruby and Net::SFTP: why can't I move this to a separate function?


Writing a function that checks via SFTP if a file is present on the server.

I have written a function sftp_file_exists_1? that works. Now I want to split this function into two functions, and to my surprise, this does not work.

require "net/sftp"

def sftp_file_exists_1?(host, user, filename)
  Net::SFTP.start(host, user, verify_host_key: :always) do |sftp|
    sftp.stat(filename) do |response|
      return response.ok?
    end
  end
end

def sftp_stat_ok?(sftp, filename)
  sftp.stat(filename) do |response|
    return response.ok?
  end
end

def sftp_file_exists_2?(host, user, filename)
  Net::SFTP.start(host, user, verify_host_key: :always) do |sftp|
    return sftp_stat_ok?(sftp, filename)
  end
end

p sftp_file_exists_1?("localhost", "user", "repos")
p sftp_file_exists_2?("localhost", "user", "repos")

I expected:

true
true

since a file repos actually exists on the server. However, I get (abbreviated):

true
#<Net::SFTP::Request:0x000055f3b56732d0 @callback=#<Proc:0x000055f3b5673280@./test.rb:14>, ...

Addendum: this works:

def sftp_stat_ok?(sftp, filename)
  begin
    sftp.stat!(filename)
  rescue Net::SFTP::StatusException
    return false
  end
  return true
end

Solution

  • Interesting problem.

    Old school debugging

    Let's add some puts to see what happens:

    require "net/sftp"
    
    def sftp_file_exists_1?(host, user, filename)
      Net::SFTP.start(host, user, verify_host_key: :always) do |sftp|
        puts "  BEFORE STAT"
        sftp.stat(filename) do |response|
          puts "  REQUEST FINISHED"
          return response.ok?
        end
        puts "  AFTER STAT"
      end
      puts "  NOT EXECUTED"
    end
    
    def sftp_stat_ok?(sftp, filename)
      request = sftp.stat(filename) do |response|
        puts "  REQUEST FINISHED"
        return response.ok?
      end
      puts "  REQUEST SENT"
      request
    end
    
    def sftp_file_exists_2?(host, user, filename)
      Net::SFTP.start(host, user, verify_host_key: :always) do |sftp|
        puts "  CALL STAT_OK?"
        return sftp_stat_ok?(sftp, filename)
      end
      puts "  NOT EXECUTED"
    end
    

    sftp_file_exists_1? outputs:

      BEFORE STAT
      AFTER STAT
      REQUEST FINISHED
    true
    

    While sftp_file_exists_2? outputs:

      CALL STAT_OK?
      REQUEST SENT
    #<Net::SFTP::Request:0x0000000001db2558>
    

    " REQUEST FINISHED" doesn't appear.

    Async logic

    The block you pass to stat is a callback. It only gets called when the server responds. To make sure the block gets executed before sftp_stat_ok? returns, you need to wait for the request to be complete:

    def sftp_stat_ok?(sftp, filename)
      request = sftp.stat(filename) do |response|
        return response.ok?
      end
      request.wait
    end
    
    def sftp_file_exists_2?(host, user, filename)
      Net::SFTP.start(host, user, verify_host_key: :always) do |sftp|
        return sftp_stat_ok?(sftp, filename)
      end
    end
    

    It isn't needed for the first version because start:

    If a block is given, it will be passed to the SFTP session and will be called once the SFTP session is fully open and initialized. When the block terminates, the new SSH session will automatically be closed.