require 'net/ssh/loggable'

module Net; module SFTP; module Operations

  # A convenience class for working with remote directories. It provides methods
  # for searching and enumerating directory entries, similarly to the standard
  # ::Dir class.
  #
  #   sftp.dir.foreach("/remote/path") do |entry|
  #     puts entry.name
  #   end
  #
  #   p sftp.dir.entries("/remote/path").map { |e| e.name }
  #
  #   sftp.dir.glob("/remote/path", "**/*.rb") do |entry|
  #     puts entry.name
  #   end
  class Dir
    # The SFTP session object that drives this directory factory.
    attr_reader :sftp

    # Create a new instance on top of the given SFTP session instance.
    def initialize(sftp)
      @sftp = sftp
    end

    # Calls the block once for each entry in the named directory on the
    # remote server. Yields a Name object to the block, rather than merely
    # the name of the entry.
    def foreach(path)
      handle = sftp.opendir!(path)
      while entries = sftp.readdir!(handle)
        entries.each { |entry| yield entry }
      end
      return nil
    ensure
      sftp.close!(handle) if handle
    end

    # Returns an array of Name objects representing the items in the given
    # remote directory, +path+.
    def entries(path)
      results = []
      foreach(path) { |entry| results << entry }
      return results
    end

    # Works as ::Dir.glob, matching (possibly recursively) all directory
    # entries under +path+ against +pattern+. If a block is given, matches
    # will be yielded to the block as they are found; otherwise, they will
    # be returned in an array when the method finishes.
    #
    # Because working over an SFTP connection is always going to be slower than
    # working purely locally, don't expect this method to perform with the
    # same level of alacrity that ::Dir.glob does; it will work best for
    # shallow directory hierarchies with relatively few directories, though
    # it should be able to handle modest numbers of files in each directory.
    def glob(path, pattern, flags=0)
      flags |= ::File::FNM_PATHNAME
      path = path.chop if path.end_with?('/') && path != '/'

      results = [] unless block_given?
      queue = entries(path).reject { |e| %w(. ..).include?(e.name) }
      while queue.any?
        entry = queue.shift

        if entry.directory? && !%w(. ..).include?(::File.basename(entry.name))
          queue += entries("#{path}/#{entry.name}").map do |e|
            e.name.replace("#{entry.name}/#{e.name}")
            e
          end
        end

        if ::File.fnmatch(pattern, entry.name, flags)
          if block_given?
            yield entry
          else
            results << entry
          end
        end
      end

      return results unless block_given?
    end

    # Identical to calling #glob with a +flags+ parameter of 0 and no block.
    # Simply returns the matched entries as an array.
    def [](path, pattern)
      glob(path, pattern, 0)
    end
  end

end; end; end
