require 'net/ssh/loggable' module Net; module SFTP; module Operations # A wrapper around an SFTP file handle, that exposes an IO-like interface # for interacting with the remote file. All operations are synchronous # (blocking), making this a very convenient way to deal with remote files. # # A wrapper is usually created via the Net::SFTP::Session#file factory: # # file = sftp.file.open("/path/to/remote") # puts file.gets # file.close class File # A reference to the Net::SFTP::Session instance that drives this wrapper attr_reader :sftp # The SFTP file handle object that this object wraps attr_reader :handle # The current position within the remote file attr_reader :pos # Creates a new wrapper that encapsulates the given +handle+ (such as # would be returned by Net::SFTP::Session#open!). The +sftp+ parameter # must be the same Net::SFTP::Session instance that opened the file. def initialize(sftp, handle) @sftp = sftp @handle = handle @pos = 0 @real_pos = 0 @real_eof = false @buffer = "" end # Repositions the file pointer to the given offset (relative to the # start of the file). This will also reset the EOF flag. def pos=(offset) @real_pos = @pos = offset @buffer = "" @real_eof = false end # Closes the underlying file and sets the handle to +nil+. Subsequent # operations on this object will fail. def close sftp.close!(handle) @handle = nil end # Returns true if the end of the file has been encountered by a previous # read. Setting the current file position via #pos= will reset this # flag (useful if the file's contents have changed since the EOF was # encountered). def eof? @real_eof && @buffer.empty? end # Reads up to +n+ bytes of data from the stream. Fewer bytes will be # returned if EOF is encountered before the requested number of bytes # could be read. Without an argument (or with a nil argument) all data # to the end of the file will be read and returned. # # This will advance the file pointer (#pos). def read(n=nil) loop do break if n && @buffer.length >= n break unless fill end if n result, @buffer = @buffer[0,n], (@buffer[n..-1] || "") else result, @buffer = @buffer, "" end @pos += result.length return result end # Reads up to the next instance of +sep_string+ in the stream, and # returns the bytes read (including +sep_string+). If +sep_string+ is # omitted, it defaults to +$/+. If EOF is encountered before any data # could be read, #gets will return +nil+. If the first argument is an # integer, or optional second argument is given, the returning string # would not be longer than the given value in bytes. def gets(sep_or_limit=$/, limit=Float::INFINITY) if sep_or_limit.is_a? Integer sep_string = $/ lim = sep_or_limit else sep_string = sep_or_limit lim = limit end delim = if sep_string && sep_string.length == 0 "#{$/}#{$/}" else sep_string end loop do at = @buffer.index(delim) if delim if at offset = [at + delim.length, lim].min @pos += offset line, @buffer = @buffer[0,offset], @buffer[offset..-1] return line elsif lim < @buffer.length @pos += lim line, @buffer = @buffer[0,lim], @buffer[lim..-1] return line elsif !fill return nil if @buffer.empty? @pos += @buffer.length line, @buffer = @buffer, "" return line end end end # Same as #gets, but raises EOFError if EOF is encountered before any # data could be read. def readline(sep_or_limit=$/, limit=Float::INFINITY) line = gets(sep_or_limit, limit) raise EOFError if line.nil? return line end # Writes the given data to the stream, incrementing the file position and # returning the number of bytes written. def write(data) data = data.to_s sftp.write!(handle, @real_pos, data) @real_pos += data.length @pos = @real_pos data.length end # Writes each argument to the stream. If +$\+ is set, it will be written # after all arguments have been written. def print(*items) items.each { |item| write(item) } write($\) if $\ nil end def size stat.size end # Resets position to beginning of file def rewind self.pos = 0 end # Writes each argument to the stream, appending a newline to any item # that does not already end in a newline. Array arguments are flattened. def puts(*items) items.each do |item| if Array === item puts(*item) else write(item) write("\n") unless item[-1] == ?\n end end nil end # Performs an fstat operation on the handle and returns the attribute # object (Net::SFTP::Protocol::V01::Attributes, Net::SFTP::Protool::V04::Attributes, # or Net::SFTP::Protocol::V06::Attributes, depending on the SFTP protocol # version in use). def stat sftp.fstat!(handle) end private # Fills the buffer. Returns +true+ if it succeeded, and +false+ if # EOF was encountered before any data was read. def fill data = sftp.read!(handle, @real_pos, 8192) if data.nil? @real_eof = true return false else @real_pos += data.length @buffer << data end !@real_eof end end end; end; end