net-sftp 1.1.1 → 2.0.0

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