/usr/lib/ruby/vendor_ruby/net/sftp/session.rb is in ruby-net-sftp 1:2.0.5-3.
This file is owned by root:root, with mode 0o644.
The actual contents of the file can be viewed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 | require 'net/ssh'
require 'net/sftp/constants'
require 'net/sftp/errors'
require 'net/sftp/protocol'
require 'net/sftp/request'
require 'net/sftp/operations/dir'
require 'net/sftp/operations/upload'
require 'net/sftp/operations/download'
require 'net/sftp/operations/file_factory'
module Net; module SFTP
# The Session class encapsulates a single SFTP channel on a Net::SSH
# connection. Instances of this class are what most applications will
# interact with most, as it provides access to both low-level (mkdir,
# rename, remove, symlink, etc.) and high-level (upload, download, etc.)
# SFTP operations.
#
# Although Session makes it easy to do SFTP operations serially, you can
# also set up multiple operations to be done in parallel, too, without
# needing to resort to threading. You merely need to fire off the requests,
# and then run the event loop until all of the requests have completed:
#
# handle1 = sftp.open!("/path/to/file1")
# handle2 = sftp.open!("/path/to/file2")
#
# r1 = sftp.read(handle1, 0, 1024)
# r2 = sftp.read(handle2, 0, 1024)
# sftp.loop { [r1, r2].any? { |r| r.pending? } }
#
# puts "chunk #1: #{r1.response[:data]}"
# puts "chunk #2: #{r2.response[:data]}"
#
# By passing blocks to the operations, you can set up powerful state
# machines, to fire off subsequent operations. In fact, the Net::SFTP::Operations::Upload
# and Net::SFTP::Operations::Download classes set up such state machines, so that
# multiple uploads and/or downloads can be running simultaneously.
#
# The convention with the names of the operations is as follows: if the method
# name ends with an exclamation mark, like #read!, it will be synchronous
# (e.g., it will block until the server responds). Methods without an
# exclamation mark (e.g. #read) are asynchronous, and return before the
# server has responded. You will need to make sure the SSH event loop is
# run in order to process these requests. (See #loop.)
class Session
include Net::SSH::Loggable
include Net::SFTP::Constants::PacketTypes
# The highest protocol version supported by the Net::SFTP library.
HIGHEST_PROTOCOL_VERSION_SUPPORTED = 6
# A reference to the Net::SSH session object that powers this SFTP session.
attr_reader :session
# The Net::SSH::Connection::Channel object that the SFTP session is being
# processed by.
attr_reader :channel
# The state of the SFTP connection. It will be :opening, :subsystem, :init,
# :open, or :closed.
attr_reader :state
# The protocol instance being used by this SFTP session. Useful for
# querying the protocol version in effect.
attr_reader :protocol
# The hash of pending requests. Any requests that have been sent and which
# the server has not yet responded to will be represented here.
attr_reader :pending_requests
# Creates a new Net::SFTP instance atop the given Net::SSH connection.
# This will return immediately, before the SFTP connection has been properly
# initialized. Once the connection is ready, the given block will be called.
# If you want to block until the connection has been initialized, try this:
#
# sftp = Net::SFTP::Session.new(ssh)
# sftp.loop { sftp.opening? }
def initialize(session, &block)
@session = session
@input = Net::SSH::Buffer.new
self.logger = session.logger
@state = :closed
connect(&block)
end
public # high-level SFTP operations
# Initiates an upload from +local+ to +remote+, asynchronously. This
# method will return a new Net::SFTP::Operations::Upload instance, and requires
# the event loop to be run in order for the upload to progress. See
# Net::SFTP::Operations::Upload for a full discussion of how this method can be
# used.
#
# uploader = sftp.upload("/local/path", "/remote/path")
# uploader.wait
def upload(local, remote, options={}, &block)
Operations::Upload.new(self, local, remote, options, &block)
end
# Identical to #upload, but blocks until the upload is complete.
def upload!(local, remote, options={}, &block)
upload(local, remote, options, &block).wait
end
# Initiates a download from +remote+ to +local+, asynchronously. This
# method will return a new Net::SFTP::Operations::Download instance, and requires
# that the event loop be run in order for the download to progress. See
# Net::SFTP::Operations::Download for a full discussion of hos this method can be
# used.
#
# download = sftp.download("/remote/path", "/local/path")
# download.wait
def download(remote, local, options={}, &block)
Operations::Download.new(self, local, remote, options, &block)
end
# Identical to #download, but blocks until the download is complete.
# If +local+ is omitted, downloads the file to an in-memory buffer
# and returns the result as a string; otherwise, returns the
# Net::SFTP::Operations::Download instance.
def download!(remote, local=nil, options={}, &block)
require 'stringio' unless defined?(StringIO)
destination = local || StringIO.new
result = download(remote, destination, options, &block).wait
local ? result : destination.string
end
# Returns an Net::SFTP::Operations::FileFactory instance, which can be used to
# mimic synchronous, IO-like file operations on a remote file via
# SFTP.
#
# sftp.file.open("/path/to/file") do |file|
# while line = file.gets
# puts line
# end
# end
#
# See Net::SFTP::Operations::FileFactory and Net::SFTP::Operations::File for more details.
def file
@file ||= Operations::FileFactory.new(self)
end
# Returns a Net::SFTP::Operations::Dir instance, which can be used to
# conveniently iterate over and search directories on the remote server.
#
# sftp.dir.glob("/base/path", "*/**/*.rb") do |entry|
# p entry.name
# end
#
# See Net::SFTP::Operations::Dir for a more detailed discussion of how
# to use this.
def dir
@dir ||= Operations::Dir.new(self)
end
public # low-level SFTP operations
# :call-seq:
# open(path, flags="r", options={}) -> request
# open(path, flags="r", options={}) { |response| ... } -> request
#
# Opens a file on the remote server. The +flags+ parameter determines
# how the flag is open, and accepts the same format as IO#open (e.g.,
# either a string like "r" or "w", or a combination of the IO constants).
# The +options+ parameter is a hash of attributes to be associated
# with the file, and varies greatly depending on the SFTP protocol
# version in use, but some (like :permissions) are always available.
#
# Returns immediately with a Request object. If a block is given, it will
# be invoked when the server responds, with a Response object as the only
# parameter. The :handle property of the response is the handle of the
# opened file, and may be passed to other methods (like #close, #read,
# #write, and so forth).
#
# sftp.open("/path/to/file") do |response|
# raise "fail!" unless response.ok?
# sftp.close(response[:handle])
# end
# sftp.loop
def open(path, flags="r", options={}, &callback)
request :open, path, flags, options, &callback
end
# Identical to #open, but blocks until the server responds. It will raise
# a StatusException if the request was unsuccessful. Otherwise, it will
# return the handle of the newly opened file.
#
# handle = sftp.open!("/path/to/file")
def open!(path, flags="r", options={}, &callback)
wait_for(open(path, flags, options, &callback), :handle)
end
# :call-seq:
# close(handle) -> request
# close(handle) { |response| ... } -> request
#
# Closes an open handle, whether obtained via #open, or #opendir. Returns
# immediately with a Request object. If a block is given, it will be
# invoked when the server responds.
#
# sftp.open("/path/to/file") do |response|
# raise "fail!" unless response.ok?
# sftp.close(response[:handle])
# end
# sftp.loop
def close(handle, &callback)
request :close, handle, &callback
end
# Identical to #close, but blocks until the server responds. It will
# raise a StatusException if the request was unsuccessful. Otherwise,
# it returns the Response object for this request.
#
# sftp.close!(handle)
def close!(handle, &callback)
wait_for(close(handle, &callback))
end
# :call-seq:
# read(handle, offset, length) -> request
# read(handle, offset, length) { |response| ... } -> request
#
# Requests that +length+ bytes, starting at +offset+ bytes from the
# beginning of the file, be read from the file identified by
# +handle+. (The +handle+ should be a value obtained via the #open
# method.) Returns immediately with a Request object. If a block is
# given, it will be invoked when the server responds.
#
# The :data property of the response will contain the requested data,
# assuming the call was successful.
#
# request = sftp.read(handle, 0, 1024) do |response|
# if response.eof?
# puts "end of file reached before reading any data"
# elsif !response.ok?
# puts "error (#{response})"
# else
# print(response[:data])
# end
# end
# request.wait
#
# To read an entire file will usually require multiple calls to #read,
# unless you know in advance how large the file is.
def read(handle, offset, length, &callback)
request :read, handle, offset, length, &callback
end
# Identical to #read, but blocks until the server responds. It will raise
# a StatusException if the request was unsuccessful. If the end of the file
# was reached, +nil+ will be returned. Otherwise, it returns the data that
# was read, as a String.
#
# data = sftp.read!(handle, 0, 1024)
def read!(handle, offset, length, &callback)
wait_for(read(handle, offset, length, &callback), :data)
end
# :call-seq:
# write(handle, offset, data) -> request
# write(handle, offset, data) { |response| ... } -> request
#
# Requests that +data+ be written to the file identified by +handle+,
# starting at +offset+ bytes from the start of the file. The file must
# have been opened for writing via #open. Returns immediately with a
# Request object. If a block is given, it will be invoked when the
# server responds.
#
# request = sftp.write(handle, 0, "hello, world!\n")
# request.wait
def write(handle, offset, data, &callback)
request :write, handle, offset, data, &callback
end
# Identical to #write, but blocks until the server responds. It will raise
# a StatusException if the request was unsuccessful, or the end of the file
# was reached. Otherwise, it returns the Response object for this request.
#
# sftp.write!(handle, 0, "hello, world!\n")
def write!(handle, offset, data, &callback)
wait_for(write(handle, offset, data, &callback))
end
# :call-seq:
# lstat(path, flags=nil) -> request
# lstat(path, flags=nil) { |response| ... } -> request
#
# This method is identical to the #stat method, with the exception that
# it will not follow symbolic links (thus allowing you to stat the
# link itself, rather than what it refers to). The +flags+ parameter
# is not used in SFTP protocol versions prior to 4, and will be ignored
# in those versions of the protocol that do not use it. For those that
# do, however, you may provide hints as to which file proprties you wish
# to query (e.g., if all you want is permissions, you could pass the
# Net::SFTP::Protocol::V04::Attributes::F_PERMISSIONS flag as the value
# for the +flags+ parameter).
#
# The method returns immediately with a Request object. If a block is given,
# it will be invoked when the server responds. The :attrs property of
# the response will contain an Attributes instance appropriate for the
# the protocol version (see Protocol::V01::Attributes, Protocol::V04::Attributes,
# and Protocol::V06::Attributes).
#
# request = sftp.lstat("/path/to/file") do |response|
# raise "fail!" unless response.ok?
# puts "permissions: %04o" % response[:attrs].permissions
# end
# request.wait
def lstat(path, flags=nil, &callback)
request :lstat, path, flags, &callback
end
# Identical to the #lstat method, but blocks until the server responds.
# It will raise a StatusException if the request was unsuccessful.
# Otherwise, it will return the attribute object describing the path.
#
# puts sftp.lstat!("/path/to/file").permissions
def lstat!(path, flags=nil, &callback)
wait_for(lstat(path, flags, &callback), :attrs)
end
# The fstat method is identical to the #stat and #lstat methods, with
# the exception that it takes a +handle+ as the first parameter, such
# as would be obtained via the #open or #opendir methods. (See the #lstat
# method for full documentation).
def fstat(handle, flags=nil, &callback)
request :fstat, handle, flags, &callback
end
# Identical to the #fstat method, but blocks until the server responds.
# It will raise a StatusException if the request was unsuccessful.
# Otherwise, it will return the attribute object describing the path.
#
# puts sftp.fstat!(handle).permissions
def fstat!(handle, flags=nil, &callback)
wait_for(fstat(handle, flags, &callback), :attrs)
end
# :call-seq:
# setstat(path, attrs) -> request
# setstat(path, attrs) { |response| ... } -> request
#
# This method may be used to set file metadata (such as permissions, or
# user/group information) on a remote file. The exact metadata that may
# be tweaked is dependent on the SFTP protocol version in use, but in
# general you may set at least the permissions, user, and group. (See
# Protocol::V01::Attributes, Protocol::V04::Attributes, and Protocol::V06::Attributes
# for the full lists of attributes that may be set for the different
# protocols.)
#
# The +attrs+ parameter is a hash, where the keys are symbols identifying
# the attributes to set.
#
# The method returns immediately with a Request object. If a block is given,
# it will be invoked when the server responds.
#
# request = sftp.setstat("/path/to/file", :permissions => 0644)
# request.wait
# puts "success: #{request.response.ok?}"
def setstat(path, attrs, &callback)
request :setstat, path, attrs, &callback
end
# Identical to the #setstat method, but blocks until the server responds.
# It will raise a StatusException if the request was unsuccessful.
# Otherwise, it will return the Response object for the request.
#
# sftp.setstat!("/path/to/file", :permissions => 0644)
def setstat!(path, attrs, &callback)
wait_for(setstat(path, attrs, &callback))
end
# The fsetstat method is identical to the #setstat method, with the
# exception that it takes a +handle+ as the first parameter, such as
# would be obtained via the #open or #opendir methods. (See the
# #setstat method for full documentation.)
def fsetstat(handle, attrs, &callback)
request :fsetstat, handle, attrs, &callback
end
# Identical to the #fsetstat method, but blocks until the server responds.
# It will raise a StatusException if the request was unsuccessful.
# Otherwise, it will return the Response object for the request.
#
# sftp.fsetstat!(handle, :permissions => 0644)
def fsetstat!(handle, attrs, &callback)
wait_for(fsetstat(handle, attrs, &callback))
end
# :call-seq:
# opendir(path) -> request
# opendir(path) { |response| ... } -> request
#
# Attempts to open a directory on the remote host for reading. Once the
# handle is obtained, directory entries may be retrieved using the
# #readdir method. The method returns immediately with a Request object.
# If a block is given, it will be invoked when the server responds.
#
# sftp.opendir("/path/to/directory") do |response|
# raise "fail!" unless response.ok?
# sftp.close(response[:handle])
# end
# sftp.loop
def opendir(path, &callback)
request :opendir, path, &callback
end
# Identical to #opendir, but blocks until the server responds. It will raise
# a StatusException if the request was unsuccessful. Otherwise, it will
# return a handle to the given path.
#
# handle = sftp.opendir!("/path/to/directory")
def opendir!(path, &callback)
wait_for(opendir(path, &callback), :handle)
end
# :call-seq:
# readdir(handle) -> request
# raeddir(handle) { |response| ... } -> request
#
# Reads a set of entries from the given directory handle (which must
# have been obtained via #opendir). If the response is EOF, then there
# are no more entries in the directory. Otherwise, the entries will be
# in the :names property of the response:
#
# loop do
# request = sftp.readdir(handle).wait
# break if request.response.eof?
# raise "fail!" unless request.response.ok?
# request.response[:names].each do |entry|
# puts entry.name
# end
# end
#
# See also Protocol::V01::Name and Protocol::V04::Name for the specific
# properties of each individual entry (which vary based on the SFTP
# protocol version in use).
def readdir(handle, &callback)
request :readdir, handle, &callback
end
# Identical to #readdir, but blocks until the server responds. It will raise
# a StatusException if the request was unsuccessful. Otherwise, it will
# return nil if there were no more names to read, or an array of name
# entries.
#
# while (entries = sftp.readdir!(handle)) do
# entries.each { |entry| puts(entry.name) }
# end
def readdir!(handle, &callback)
wait_for(readdir(handle, &callback), :names)
end
# :call-seq:
# remove(filename) -> request
# remove(filename) { |response| ... } -> request
#
# Attempts to remove the given file from the remote file system. Returns
# immediately with a Request object. If a block is given, the block will
# be invoked when the server responds, and will be passed a Response
# object.
#
# sftp.remove("/path/to/file").wait
def remove(filename, &callback)
request :remove, filename, &callback
end
# Identical to #remove, but blocks until the server responds. It will raise
# a StatusException if the request was unsuccessful. Otherwise, it will
# return the Response object for the request.
#
# sftp.remove!("/path/to/file")
def remove!(filename, &callback)
wait_for(remove(filename, &callback))
end
# :call-seq:
# mkdir(path, attrs={}) -> request
# mkdir(path, attrs={}) { |response| ... } -> request
#
# Creates the named directory on the remote server. If an attribute hash
# is given, it must map to the set of attributes supported by the version
# of the SFTP protocol in use. (See Protocol::V01::Attributes,
# Protocol::V04::Attributes, and Protocol::V06::Attributes.)
#
# sftp.mkdir("/path/to/directory", :permissions => 0550).wait
def mkdir(path, attrs={}, &callback)
request :mkdir, path, attrs, &callback
end
# Identical to #mkdir, but blocks until the server responds. It will raise
# a StatusException if the request was unsuccessful. Otherwise, it will
# return the Response object for the request.
#
# sftp.mkdir!("/path/to/directory", :permissions => 0550)
def mkdir!(path, attrs={}, &callback)
wait_for(mkdir(path, attrs, &callback))
end
# :call-seq:
# rmdir(path) -> request
# rmdir(path) { |response| ... } -> request
#
# Removes the named directory on the remote server. The directory must
# be empty before it can be removed.
#
# sftp.rmdir("/path/to/directory").wait
def rmdir(path, &callback)
request :rmdir, path, &callback
end
# Identical to #rmdir, but blocks until the server responds. It will raise
# a StatusException if the request was unsuccessful. Otherwise, it will
# return the Response object for the request.
#
# sftp.rmdir!("/path/to/directory")
def rmdir!(path, &callback)
wait_for(rmdir(path, &callback))
end
# :call-seq:
# realpath(path) -> request
# realpath(path) { |response| ... } -> request
#
# Tries to canonicalize the given path, turning any given path into an
# absolute path. This is primarily useful for converting a path with
# ".." or "." segments into an identical path without those segments.
# The answer will be in the response's :names attribute, as a
# one-element array.
#
# request = sftp.realpath("/path/../to/../directory").wait
# puts request[:names].first.name
def realpath(path, &callback)
request :realpath, path, &callback
end
# Identical to #realpath, but blocks until the server responds. It will raise
# a StatusException if the request was unsuccessful. Otherwise, it will
# return a name object identifying the path.
#
# puts(sftp.realpath!("/path/../to/../directory"))
def realpath!(path, &callback)
wait_for(realpath(path, &callback), :names).first
end
# Identical to the #lstat method, except that it follows symlinks
# (e.g., if you give it the path to a symlink, it will stat the target
# of the symlink rather than the symlink itself). See the #lstat method
# for full documentation.
def stat(path, flags=nil, &callback)
request :stat, path, flags, &callback
end
# Identical to #stat, but blocks until the server responds. It will raise
# a StatusException if the request was unsuccessful. Otherwise, it will
# return an attribute object for the named path.
#
# attrs = sftp.stat!("/path/to/file")
def stat!(path, flags=nil, &callback)
wait_for(stat(path, flags, &callback), :attrs)
end
# :call-seq:
# rename(name, new_name, flags=nil) -> request
# rename(name, new_name, flags=nil) { |response| ... } -> request
#
# Renames the given file. This operation is only available in SFTP
# protocol versions two and higher. The +flags+ parameter is ignored
# in versions prior to 5. In versions 5 and higher, the +flags+
# parameter can be used to specify how the rename should be performed
# (atomically, etc.).
#
# The following flags are defined in protocol version 5:
#
# * 0x0001 - overwrite an existing file if the new name specifies a file
# that already exists.
# * 0x0002 - perform the rewrite atomically.
# * 0x0004 - allow the server to perform the rename as it prefers.
def rename(name, new_name, flags=nil, &callback)
request :rename, name, new_name, flags, &callback
end
# Identical to #rename, but blocks until the server responds. It will raise
# a StatusException if the request was unsuccessful. Otherwise, it will
# return the Response object for the request.
#
# sftp.rename!("/path/to/old", "/path/to/new")
def rename!(name, new_name, flags=nil, &callback)
wait_for(rename(name, new_name, flags, &callback))
end
# :call-seq:
# readlink(path) -> request
# readlink(path) { |response| ... } -> request
#
# Queries the server for the target of the specified symbolic link.
# This operation is only available in protocol versions 3 and higher.
# The response to this request will include a names property, a one-element
# array naming the target of the symlink.
#
# request = sftp.readlink("/path/to/symlink").wait
# puts request.response[:names].first.name
def readlink(path, &callback)
request :readlink, path, &callback
end
# Identical to #readlink, but blocks until the server responds. It will raise
# a StatusException if the request was unsuccessful. Otherwise, it will
# return the Name object for the path that the symlink targets.
#
# item = sftp.readlink!("/path/to/symlink")
def readlink!(path, &callback)
wait_for(readlink(path, &callback), :names).first
end
# :call-seq:
# symlink(path, target) -> request
# symlink(path, target) { |response| ... } -> request
#
# Attempts to create a symlink to +path+ at +target+. This operation
# is only available in protocol versions 3, 4, and 5, but the Net::SFTP
# library mimics the symlink behavior in protocol version 6 using the
# #link method, so it is safe to use this method in protocol version 6.
#
# sftp.symlink("/path/to/file", "/path/to/symlink").wait
def symlink(path, target, &callback)
request :symlink, path, target, &callback
end
# Identical to #symlink, but blocks until the server responds. It will raise
# a StatusException if the request was unsuccessful. Otherwise, it will
# return the Response object for the request.
#
# sftp.symlink!("/path/to/file", "/path/to/symlink")
def symlink!(path, target, &callback)
wait_for(symlink(path, target, &callback))
end
# :call-seq:
# link(new_link_path, existing_path, symlink=true) -> request
# link(new_link_path, existing_path, symlink=true) { |response| ... } -> request
#
# Attempts to create a link, either hard or symbolic. This operation is
# only available in SFTP protocol versions 6 and higher. If the +symlink+
# paramter is true, a symbolic link will be created, otherwise a hard
# link will be created. The link will be named +new_link_path+, and will
# point to the path +existing_path+.
#
# sftp.link("/path/to/symlink", "/path/to/file", true).wait
#
# Note that #link is only available for SFTP protocol 6 and higher. You
# can use #symlink for protocols 3 and higher.
def link(new_link_path, existing_path, symlink=true, &callback)
request :link, new_link_path, existing_path, symlink, &callback
end
# Identical to #link, but blocks until the server responds. It will raise
# a StatusException if the request was unsuccessful. Otherwise, it will
# return the Response object for the request.
#
# sftp.link!("/path/to/symlink", "/path/to/file", true)
def link!(new_link_path, existing_path, symlink=true, &callback)
wait_for(link(new_link_path, existing_path, symlink, &callback))
end
# :call-seq:
# block(handle, offset, length, mask) -> request
# block(handle, offset, length, mask) { |response| ... } -> request
#
# Creates a byte-range lock on the file specified by the given +handle+.
# This operation is only available in SFTP protocol versions 6 and
# higher. The lock may be either mandatory or advisory.
#
# The +handle+ parameter is a file handle, as obtained by the #open method.
#
# The +offset+ and +length+ parameters describe the location and size of
# the byte range.
#
# The +mask+ describes how the lock should be defined, and consists of
# some combination of the following bit masks:
#
# * 0x0040 - Read lock. The byte range may not be accessed for reading
# by via any other handle, though it may be written to.
# * 0x0080 - Write lock. The byte range may not be written to via any
# other handle, though it may be read from.
# * 0x0100 - Delete lock. No other handle may delete this file.
# * 0x0200 - Advisory lock. The server need not honor the lock instruction.
#
# Once created, the lock may be removed via the #unblock method.
def block(handle, offset, length, mask, &callback)
request :block, handle, offset, length, mask, &callback
end
# Identical to #block, but blocks until the server responds. It will raise
# a StatusException if the request was unsuccessful. Otherwise, it will
# return the Response object for the request.
def block!(handle, offset, length, mask, &callback)
wait_for(block(handle, offset, length, mask, &callback))
end
# :call-seq:
# unblock(handle, offset, length) -> request
# unblock(handle, offset, length) { |response| ... } -> request
#
# Removes a previously created byte-range lock. This operation is only
# available in protocol versions 6 and higher. The +offset+ and +length+
# parameters must exactly match those that were given to #block when the
# lock was acquired.
def unblock(handle, offset, length, &callback)
request :unblock, handle, offset, length, &callback
end
# Identical to #unblock, but blocks until the server responds. It will raise
# a StatusException if the request was unsuccessful. Otherwise, it will
# return the Response object for the request.
def unblock!(handle, offset, length, &callback)
wait_for(unblock(handle, offset, length, &callback))
end
public # miscellaneous methods
# Closes the SFTP connection, but not the SSH connection. Blocks until the
# session has terminated. Once the session has terminated, further operations
# on this object will result in errors. You can reopen the SFTP session
# via the #connect method.
def close_channel
return unless open?
channel.close
loop { !closed? }
end
# Returns true if the connection has been initialized.
def open?
state == :open
end
# Returns true if the connection has been closed.
def closed?
state == :closed
end
# Returns true if the connection is in the process of being initialized
# (e.g., it is not closed, but is not yet fully open).
def opening?
!(open? || closed?)
end
# Attempts to establish an SFTP connection over the SSH session given when
# this object was instantiated. If the object is already open, this will
# simply execute the given block (if any), passing the SFTP session itself
# as argument. If the session is currently being opened, this will add
# the given block to the list of callbacks, to be executed when the session
# is fully open.
#
# This method does not block, and will return immediately. If you pass a
# block to it, that block will be invoked when the connection has been
# fully established. Thus, you can do something like this:
#
# sftp.connect do
# puts "open!"
# end
#
# If you just want to block until the connection is ready, see the #connect!
# method.
def connect(&block)
case state
when :open
block.call(self) if block
when :closed
@state = :opening
@channel = session.open_channel(&method(:when_channel_confirmed))
@packet_length = nil
@protocol = nil
@on_ready = Array(block)
else # opening
@on_ready << block if block
end
self
end
# Same as the #connect method, but blocks until the SFTP connection has
# been fully initialized.
def connect!(&block)
connect(&block)
loop { opening? }
self
end
alias :loop_forever :loop
# Runs the SSH event loop while the given block returns true. This lets
# you set up a state machine and then "fire it off". If you do not specify
# a block, the event loop will run for as long as there are any pending
# SFTP requests. This makes it easy to do thing like this:
#
# sftp.remove("/path/to/file")
# sftp.loop
def loop(&block)
block ||= Proc.new { pending_requests.any? }
session.loop(&block)
end
# Formats, constructs, and sends an SFTP packet of the given type and with
# the given data. This does not block, but merely enqueues the packet for
# sending and returns.
#
# You should probably use the operation methods, rather than building and
# sending the packet directly. (See #open, #close, etc.)
def send_packet(type, *args)
data = Net::SSH::Buffer.from(*args)
msg = Net::SSH::Buffer.from(:long, data.length+1, :byte, type, :raw, data)
channel.send_data(msg.to_s)
end
private
#--
# "ruby -w" hates private attributes, so we have to do this longhand
#++
# The input buffer used to accumulate packet data
def input; @input; end
# Create and enqueue a new SFTP request of the given type, with the
# given arguments. Returns a new Request instance that encapsulates the
# request.
def request(type, *args, &callback)
request = Request.new(self, type, protocol.send(type, *args), &callback)
info { "sending #{type} packet (#{request.id})" }
pending_requests[request.id] = request
end
# Waits for the given request to complete. If the response is
# EOF, nil is returned. If the response was not successful
# (e.g., !response.ok?), a StatusException will be raised.
# If +property+ is given, the corresponding property from the response
# will be returned; otherwise, the response object itself will be
# returned.
def wait_for(request, property=nil)
request.wait
if request.response.eof?
nil
elsif !request.response.ok?
raise StatusException.new(request.response)
elsif property
request.response[property.to_sym]
else
request.response
end
end
# Called when the SSH channel is confirmed as "open" by the server.
# This is one of the states of the SFTP state machine, and is followed
# by the #when_subsystem_started state.
def when_channel_confirmed(channel)
debug { "requesting sftp subsystem" }
@state = :subsystem
channel.subsystem("sftp", &method(:when_subsystem_started))
end
# Called when the SSH server confirms that the SFTP subsystem was
# successfully started. This sets up the appropriate callbacks on the
# SSH channel and then starts the SFTP protocol version negotiation
# process.
def when_subsystem_started(channel, success)
raise Net::SFTP::Exception, "could not start SFTP subsystem" unless success
debug { "sftp subsystem successfully started" }
@state = :init
channel.on_data { |c,data| input.append(data) }
channel.on_extended_data { |c,t,data| debug { data } }
channel.on_close(&method(:when_channel_closed))
channel.on_process(&method(:when_channel_polled))
send_packet(FXP_INIT, :long, HIGHEST_PROTOCOL_VERSION_SUPPORTED)
end
# Called when the SSH server closes the underlying channel.
def when_channel_closed(channel)
debug { "sftp channel closed" }
@channel = nil
@state = :closed
end
# Called whenever Net::SSH polls the SFTP channel for pending activity.
# This basically checks the input buffer to see if enough input has been
# accumulated to handle. If there has, the packet is parsed and
# dispatched, according to its type (see #do_version and #dispatch_request).
def when_channel_polled(channel)
while input.length > 0
if @packet_length.nil?
# make sure we've read enough data to tell how long the packet is
return unless input.length >= 4
@packet_length = input.read_long
end
return unless input.length >= @packet_length
packet = Net::SFTP::Packet.new(input.read(@packet_length))
input.consume!
@packet_length = nil
debug { "received sftp packet #{packet.type} len #{packet.length}" }
if packet.type == FXP_VERSION
do_version(packet)
else
dispatch_request(packet)
end
end
end
# Called to handle FXP_VERSION packets. This performs the SFTP protocol
# version negotiation, instantiating the appropriate Protocol instance
# and invoking the callback given to #connect, if any.
def do_version(packet)
debug { "negotiating sftp protocol version, mine is #{HIGHEST_PROTOCOL_VERSION_SUPPORTED}" }
server_version = packet.read_long
debug { "server reports sftp version #{server_version}" }
negotiated_version = [server_version, HIGHEST_PROTOCOL_VERSION_SUPPORTED].min
info { "negotiated version is #{negotiated_version}" }
extensions = {}
until packet.eof?
name = packet.read_string
data = packet.read_string
extensions[name] = data
end
@protocol = Protocol.load(self, negotiated_version)
@pending_requests = {}
@state = :open
@on_ready.each { |callback| callback.call(self) }
@on_ready = nil
end
# Parses the packet, finds the associated Request instance, and tells
# the Request instance to respond to the packet (see Request#respond_to).
def dispatch_request(packet)
id = packet.read_long
request = pending_requests.delete(id) or raise Net::SFTP::Exception, "no such request `#{id}'"
request.respond_to(packet)
end
end
end; end
|