rubysl-net-ftp 1.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 056ab8cdeb2260ec5fc8c32aa3e859384b0f1293
4
- data.tar.gz: edec86bfc6933c0580c237a73bd6c8dda32b78a9
3
+ metadata.gz: 456965d440f1346a6441b73d14ad9fe6d8ed3c17
4
+ data.tar.gz: 60f559ec7a5f317c8cb42024694b897212dd34ab
5
5
  SHA512:
6
- metadata.gz: 4c78e9ed1e31a894ebd2949aa4d957c3c800b29e341437f7e1b22592d23e16a9cebb16ca275e4de11d09063ebbc059e566af1de9d9680bc7899a6b359e14e122
7
- data.tar.gz: f97d6244aa78a2a07f894f2c71e4d8a9fbe85466e8bc4a493ee201c2c7ea60e997e0c8d8a1dc05d19c6fc28f74464271317e2261e1b5d25f32165a2bcb234aa1
6
+ metadata.gz: c05f1132989fd0faf92a8fcaf0fecc6dc36ba07a48d595230c6f4c693801f7e24fe12fd5696cca096f64a0d1bcecef7598841c2dbb4eb98ba0928d3645057d44
7
+ data.tar.gz: 1b9795aa38d39bc624b3b90eaca65c0302440490b9f5de8fade14b28797409f86543a0c11022f82813663c9c0ddb27ef9779bde99e195cf540832a554a7c8c1d
@@ -1,8 +1,9 @@
1
1
  language: ruby
2
2
  before_install:
3
+ - rvm use $RVM --install --binary --fuzzy
3
4
  - gem update --system
4
5
  - gem --version
5
6
  - gem install rubysl-bundler
7
+ env:
8
+ - RVM=rbx-nightly-d21 RUBYLIB=lib
6
9
  script: bundle exec mspec spec
7
- rvm:
8
- - rbx-nightly-18mode
@@ -1,11 +1,11 @@
1
- #
1
+ #
2
2
  # = net/ftp.rb - FTP Client Library
3
- #
3
+ #
4
4
  # Written by Shugo Maeda <shugo@ruby-lang.org>.
5
5
  #
6
6
  # Documentation by Gavin Sinclair, sourced from "Programming Ruby" (Hunt/Thomas)
7
7
  # and "Ruby In a Nutshell" (Matsumoto), used with permission.
8
- #
8
+ #
9
9
  # This library is distributed under the terms of the Ruby license.
10
10
  # You can freely distribute/modify this library.
11
11
  #
@@ -16,15 +16,17 @@
16
16
 
17
17
  require "socket"
18
18
  require "monitor"
19
+ require "net/protocol"
19
20
 
20
21
  module Net
21
22
 
22
23
  # :stopdoc:
23
24
  class FTPError < StandardError; end
24
25
  class FTPReplyError < FTPError; end
25
- class FTPTempError < FTPError; end
26
- class FTPPermError < FTPError; end
26
+ class FTPTempError < FTPError; end
27
+ class FTPPermError < FTPError; end
27
28
  class FTPProtoError < FTPError; end
29
+ class FTPConnectionError < FTPError; end
28
30
  # :startdoc:
29
31
 
30
32
  #
@@ -34,12 +36,12 @@ module Net
34
36
  # advantage of Ruby's style and strengths.
35
37
  #
36
38
  # == Example
37
- #
39
+ #
38
40
  # require 'net/ftp'
39
41
  #
40
42
  # === Example 1
41
- #
42
- # ftp = Net::FTP.new('ftp.netlab.co.jp')
43
+ #
44
+ # ftp = Net::FTP.new('example.com')
43
45
  # ftp.login
44
46
  # files = ftp.chdir('pub/lang/ruby/contrib')
45
47
  # files = ftp.list('n*')
@@ -48,7 +50,7 @@ module Net
48
50
  #
49
51
  # === Example 2
50
52
  #
51
- # Net::FTP.open('ftp.netlab.co.jp') do |ftp|
53
+ # Net::FTP.open('example.com') do |ftp|
52
54
  # ftp.login
53
55
  # files = ftp.chdir('pub/lang/ruby/contrib')
54
56
  # files = ftp.list('n*')
@@ -71,15 +73,15 @@ module Net
71
73
  #
72
74
  class FTP
73
75
  include MonitorMixin
74
-
76
+
75
77
  # :stopdoc:
76
78
  FTP_PORT = 21
77
79
  CRLF = "\r\n"
78
- DEFAULT_BLOCKSIZE = 4096
80
+ DEFAULT_BLOCKSIZE = BufferedIO::BUFSIZE
79
81
  # :startdoc:
80
-
82
+
81
83
  # When +true+, transfers are performed in binary mode. Default: +true+.
82
- attr_accessor :binary
84
+ attr_reader :binary
83
85
 
84
86
  # When +true+, the connection is in passive mode. Default: +false+.
85
87
  attr_accessor :passive
@@ -92,6 +94,24 @@ module Net
92
94
  # transfers are resumed or restarted. Default: +false+.
93
95
  attr_accessor :resume
94
96
 
97
+ # Number of seconds to wait for the connection to open. Any number
98
+ # may be used, including Floats for fractional seconds. If the FTP
99
+ # object cannot open a connection in this many seconds, it raises a
100
+ # Net::OpenTimeout exception. The default value is +nil+.
101
+ attr_accessor :open_timeout
102
+
103
+ # Number of seconds to wait for one block to be read (via one read(2)
104
+ # call). Any number may be used, including Floats for fractional
105
+ # seconds. If the FTP object cannot read data in this many seconds,
106
+ # it raises a TimeoutError exception. The default value is 60 seconds.
107
+ attr_reader :read_timeout
108
+
109
+ # Setter for the read_timeout attribute.
110
+ def read_timeout=(sec)
111
+ @sock.read_timeout = sec
112
+ @read_timeout = sec
113
+ end
114
+
95
115
  # The server's welcome message.
96
116
  attr_reader :welcome
97
117
 
@@ -101,7 +121,7 @@ module Net
101
121
 
102
122
  # The server's last response.
103
123
  attr_reader :last_response
104
-
124
+
105
125
  #
106
126
  # A synonym for <tt>FTP.new</tt>, but with a mandatory host parameter.
107
127
  #
@@ -120,7 +140,7 @@ module Net
120
140
  new(host, user, passwd, acct)
121
141
  end
122
142
  end
123
-
143
+
124
144
  #
125
145
  # Creates and returns a new +FTP+ object. If a +host+ is given, a connection
126
146
  # is made. Additionally, if the +user+ is given, the given user name,
@@ -132,35 +152,88 @@ module Net
132
152
  @passive = false
133
153
  @debug_mode = false
134
154
  @resume = false
155
+ @sock = NullSocket.new
156
+ @logged_in = false
157
+ @open_timeout = nil
158
+ @read_timeout = 60
135
159
  if host
136
- connect(host)
137
- if user
138
- login(user, passwd, acct)
139
- end
160
+ connect(host)
161
+ if user
162
+ login(user, passwd, acct)
163
+ end
164
+ end
165
+ end
166
+
167
+ # A setter to toggle transfers in binary mode.
168
+ # +newmode+ is either +true+ or +false+
169
+ def binary=(newmode)
170
+ if newmode != @binary
171
+ @binary = newmode
172
+ send_type_command if @logged_in
173
+ end
174
+ end
175
+
176
+ # Sends a command to destination host, with the current binary sendmode
177
+ # type.
178
+ #
179
+ # If binary mode is +true+, then "TYPE I" (image) is sent, otherwise "TYPE
180
+ # A" (ascii) is sent.
181
+ def send_type_command # :nodoc:
182
+ if @binary
183
+ voidcmd("TYPE I")
184
+ else
185
+ voidcmd("TYPE A")
186
+ end
187
+ end
188
+ private :send_type_command
189
+
190
+ # Toggles transfers in binary mode and yields to a block.
191
+ # This preserves your current binary send mode, but allows a temporary
192
+ # transaction with binary sendmode of +newmode+.
193
+ #
194
+ # +newmode+ is either +true+ or +false+
195
+ def with_binary(newmode) # :nodoc:
196
+ oldmode = binary
197
+ self.binary = newmode
198
+ begin
199
+ yield
200
+ ensure
201
+ self.binary = oldmode
140
202
  end
141
203
  end
204
+ private :with_binary
142
205
 
143
206
  # Obsolete
144
- def return_code
207
+ def return_code # :nodoc:
145
208
  $stderr.puts("warning: Net::FTP#return_code is obsolete and do nothing")
146
209
  return "\n"
147
210
  end
148
211
 
149
212
  # Obsolete
150
- def return_code=(s)
213
+ def return_code=(s) # :nodoc:
151
214
  $stderr.puts("warning: Net::FTP#return_code= is obsolete and do nothing")
152
215
  end
153
216
 
154
- def open_socket(host, port)
155
- if defined? SOCKSSocket and ENV["SOCKS_SERVER"]
156
- @passive = true
157
- return SOCKSSocket.open(host, port)
158
- else
159
- return TCPSocket.open(host, port)
160
- end
217
+ # Constructs a socket with +host+ and +port+.
218
+ #
219
+ # If SOCKSSocket is defined and the environment (ENV) defines
220
+ # SOCKS_SERVER, then a SOCKSSocket is returned, else a TCPSocket is
221
+ # returned.
222
+ def open_socket(host, port) # :nodoc:
223
+ return Timeout.timeout(@open_timeout, Net::OpenTimeout) {
224
+ if defined? SOCKSSocket and ENV["SOCKS_SERVER"]
225
+ @passive = true
226
+ sock = SOCKSSocket.open(host, port)
227
+ else
228
+ sock = TCPSocket.open(host, port)
229
+ end
230
+ io = BufferedSocket.new(sock)
231
+ io.read_timeout = @read_timeout
232
+ io
233
+ }
161
234
  end
162
235
  private :open_socket
163
-
236
+
164
237
  #
165
238
  # Establishes an FTP connection to host, optionally overriding the default
166
239
  # port. If the environment variable +SOCKS_SERVER+ is set, sets up the
@@ -169,11 +242,11 @@ module Net
169
242
  #
170
243
  def connect(host, port = FTP_PORT)
171
244
  if @debug_mode
172
- print "connect: ", host, ", ", port, "\n"
245
+ print "connect: ", host, ", ", port, "\n"
173
246
  end
174
247
  synchronize do
175
- @sock = open_socket(host, port)
176
- voidresp
248
+ @sock = open_socket(host, port)
249
+ voidresp
177
250
  end
178
251
  end
179
252
 
@@ -182,185 +255,192 @@ module Net
182
255
  #
183
256
  def set_socket(sock, get_greeting = true)
184
257
  synchronize do
185
- @sock = sock
186
- if get_greeting
187
- voidresp
188
- end
258
+ @sock = sock
259
+ if get_greeting
260
+ voidresp
261
+ end
189
262
  end
190
263
  end
191
264
 
192
- def sanitize(s)
265
+ # If string +s+ includes the PASS command (password), then the contents of
266
+ # the password are cleaned from the string using "*"
267
+ def sanitize(s) # :nodoc:
193
268
  if s =~ /^PASS /i
194
- return s[0, 5] + "*" * (s.length - 5)
269
+ return s[0, 5] + "*" * (s.length - 5)
195
270
  else
196
- return s
271
+ return s
197
272
  end
198
273
  end
199
274
  private :sanitize
200
-
201
- def putline(line)
275
+
276
+ # Ensures that +line+ has a control return / line feed (CRLF) and writes
277
+ # it to the socket.
278
+ def putline(line) # :nodoc:
202
279
  if @debug_mode
203
- print "put: ", sanitize(line), "\n"
280
+ print "put: ", sanitize(line), "\n"
204
281
  end
205
282
  line = line + CRLF
206
283
  @sock.write(line)
207
284
  end
208
285
  private :putline
209
-
210
- def getline
286
+
287
+ # Reads a line from the sock. If EOF, then it will raise EOFError
288
+ def getline # :nodoc:
211
289
  line = @sock.readline # if get EOF, raise EOFError
212
290
  line.sub!(/(\r\n|\n|\r)\z/n, "")
213
291
  if @debug_mode
214
- print "get: ", sanitize(line), "\n"
292
+ print "get: ", sanitize(line), "\n"
215
293
  end
216
294
  return line
217
295
  end
218
296
  private :getline
219
-
220
- def getmultiline
297
+
298
+ # Receive a section of lines until the response code's match.
299
+ def getmultiline # :nodoc:
221
300
  line = getline
222
301
  buff = line
223
302
  if line[3] == ?-
224
- code = line[0, 3]
225
- begin
226
- line = getline
227
- buff << "\n" << line
228
- end until line[0, 3] == code and line[3] != ?-
303
+ code = line[0, 3]
304
+ begin
305
+ line = getline
306
+ buff << "\n" << line
307
+ end until line[0, 3] == code and line[3] != ?-
229
308
  end
230
309
  return buff << "\n"
231
310
  end
232
311
  private :getmultiline
233
-
234
- def getresp
312
+
313
+ # Recieves a response from the destination host.
314
+ #
315
+ # Returns the response code or raises FTPTempError, FTPPermError, or
316
+ # FTPProtoError
317
+ def getresp # :nodoc:
235
318
  @last_response = getmultiline
236
319
  @last_response_code = @last_response[0, 3]
237
320
  case @last_response_code
238
321
  when /\A[123]/
239
- return @last_response
322
+ return @last_response
240
323
  when /\A4/
241
- raise FTPTempError, @last_response
324
+ raise FTPTempError, @last_response
242
325
  when /\A5/
243
- raise FTPPermError, @last_response
326
+ raise FTPPermError, @last_response
244
327
  else
245
- raise FTPProtoError, @last_response
328
+ raise FTPProtoError, @last_response
246
329
  end
247
330
  end
248
331
  private :getresp
249
-
250
- def voidresp
332
+
333
+ # Recieves a response.
334
+ #
335
+ # Raises FTPReplyError if the first position of the response code is not
336
+ # equal 2.
337
+ def voidresp # :nodoc:
251
338
  resp = getresp
252
339
  if resp[0] != ?2
253
- raise FTPReplyError, resp
340
+ raise FTPReplyError, resp
254
341
  end
255
342
  end
256
343
  private :voidresp
257
-
344
+
258
345
  #
259
346
  # Sends a command and returns the response.
260
347
  #
261
348
  def sendcmd(cmd)
262
349
  synchronize do
263
- putline(cmd)
264
- return getresp
350
+ putline(cmd)
351
+ return getresp
265
352
  end
266
353
  end
267
-
354
+
268
355
  #
269
356
  # Sends a command and expect a response beginning with '2'.
270
357
  #
271
358
  def voidcmd(cmd)
272
359
  synchronize do
273
- putline(cmd)
274
- voidresp
360
+ putline(cmd)
361
+ voidresp
275
362
  end
276
363
  end
277
-
278
- def sendport(host, port)
364
+
365
+ # Constructs and send the appropriate PORT (or EPRT) command
366
+ def sendport(host, port) # :nodoc:
279
367
  af = (@sock.peeraddr)[0]
280
368
  if af == "AF_INET"
281
- cmd = "PORT " + (host.split(".") + port.divmod(256)).join(",")
369
+ cmd = "PORT " + (host.split(".") + port.divmod(256)).join(",")
282
370
  elsif af == "AF_INET6"
283
- cmd = sprintf("EPRT |2|%s|%d|", host, port)
371
+ cmd = sprintf("EPRT |2|%s|%d|", host, port)
284
372
  else
285
- raise FTPProtoError, host
373
+ raise FTPProtoError, host
286
374
  end
287
375
  voidcmd(cmd)
288
376
  end
289
377
  private :sendport
290
-
291
- def makeport
378
+
379
+ # Constructs a TCPServer socket, and sends it the PORT command
380
+ #
381
+ # Returns the constructed TCPServer socket
382
+ def makeport # :nodoc:
292
383
  sock = TCPServer.open(@sock.addr[3], 0)
293
384
  port = sock.addr[1]
294
385
  host = sock.addr[3]
295
- resp = sendport(host, port)
386
+ sendport(host, port)
296
387
  return sock
297
388
  end
298
389
  private :makeport
299
-
300
- def makepasv
390
+
391
+ # sends the appropriate command to enable a passive connection
392
+ def makepasv # :nodoc:
301
393
  if @sock.peeraddr[0] == "AF_INET"
302
- host, port = parse227(sendcmd("PASV"))
394
+ host, port = parse227(sendcmd("PASV"))
303
395
  else
304
- host, port = parse229(sendcmd("EPSV"))
305
- # host, port = parse228(sendcmd("LPSV"))
396
+ host, port = parse229(sendcmd("EPSV"))
397
+ # host, port = parse228(sendcmd("LPSV"))
306
398
  end
307
399
  return host, port
308
400
  end
309
401
  private :makepasv
310
-
311
- def transfercmd(cmd, rest_offset = nil)
402
+
403
+ # Constructs a connection for transferring data
404
+ def transfercmd(cmd, rest_offset = nil) # :nodoc:
312
405
  if @passive
313
- host, port = makepasv
314
- conn = open_socket(host, port)
315
- if @resume and rest_offset
316
- resp = sendcmd("REST " + rest_offset.to_s)
317
- if resp[0] != ?3
318
- raise FTPReplyError, resp
319
- end
320
- end
321
- resp = sendcmd(cmd)
406
+ host, port = makepasv
407
+ conn = open_socket(host, port)
408
+ if @resume and rest_offset
409
+ resp = sendcmd("REST " + rest_offset.to_s)
410
+ if resp[0] != ?3
411
+ raise FTPReplyError, resp
412
+ end
413
+ end
414
+ resp = sendcmd(cmd)
322
415
  # skip 2XX for some ftp servers
323
416
  resp = getresp if resp[0] == ?2
324
- if resp[0] != ?1
325
- raise FTPReplyError, resp
326
- end
417
+ if resp[0] != ?1
418
+ raise FTPReplyError, resp
419
+ end
327
420
  else
328
- sock = makeport
329
- if @resume and rest_offset
330
- resp = sendcmd("REST " + rest_offset.to_s)
331
- if resp[0] != ?3
332
- raise FTPReplyError, resp
333
- end
334
- end
335
- resp = sendcmd(cmd)
421
+ sock = makeport
422
+ if @resume and rest_offset
423
+ resp = sendcmd("REST " + rest_offset.to_s)
424
+ if resp[0] != ?3
425
+ raise FTPReplyError, resp
426
+ end
427
+ end
428
+ resp = sendcmd(cmd)
336
429
  # skip 2XX for some ftp servers
337
430
  resp = getresp if resp[0] == ?2
338
- if resp[0] != ?1
339
- raise FTPReplyError, resp
340
- end
341
- conn = sock.accept
342
- sock.close
431
+ if resp[0] != ?1
432
+ raise FTPReplyError, resp
433
+ end
434
+ conn = BufferedSocket.new(sock.accept)
435
+ conn.read_timeout = @read_timeout
436
+ sock.shutdown(Socket::SHUT_WR) rescue nil
437
+ sock.read rescue nil
438
+ sock.close
343
439
  end
344
440
  return conn
345
441
  end
346
442
  private :transfercmd
347
-
348
- def getaddress
349
- thishost = Socket.gethostname rescue ""
350
- if not thishost.index(".")
351
- thishost = Socket.gethostbyname(thishost)[0] rescue ""
352
- end
353
- if ENV.has_key?("LOGNAME")
354
- realuser = ENV["LOGNAME"]
355
- elsif ENV.has_key?("USER")
356
- realuser = ENV["USER"]
357
- else
358
- realuser = "anonymous"
359
- end
360
- return realuser + "@" + thishost
361
- end
362
- private :getaddress
363
-
443
+
364
444
  #
365
445
  # Logs in to the remote host. The session must have been previously
366
446
  # connected. If +user+ is the string "anonymous" and the +password+ is
@@ -371,27 +451,29 @@ module Net
371
451
  #
372
452
  def login(user = "anonymous", passwd = nil, acct = nil)
373
453
  if user == "anonymous" and passwd == nil
374
- passwd = getaddress
454
+ passwd = "anonymous@"
375
455
  end
376
-
456
+
377
457
  resp = ""
378
458
  synchronize do
379
- resp = sendcmd('USER ' + user)
380
- if resp[0] == ?3
459
+ resp = sendcmd('USER ' + user)
460
+ if resp[0] == ?3
381
461
  raise FTPReplyError, resp if passwd.nil?
382
- resp = sendcmd('PASS ' + passwd)
383
- end
384
- if resp[0] == ?3
462
+ resp = sendcmd('PASS ' + passwd)
463
+ end
464
+ if resp[0] == ?3
385
465
  raise FTPReplyError, resp if acct.nil?
386
- resp = sendcmd('ACCT ' + acct)
387
- end
466
+ resp = sendcmd('ACCT ' + acct)
467
+ end
388
468
  end
389
469
  if resp[0] != ?2
390
- raise FTPReplyError, resp
470
+ raise FTPReplyError, resp
391
471
  end
392
472
  @welcome = resp
473
+ send_type_command
474
+ @logged_in = true
393
475
  end
394
-
476
+
395
477
  #
396
478
  # Puts the connection into binary (image) mode, issues the given command,
397
479
  # and fetches the data returned, passing it to the associated block in
@@ -400,18 +482,25 @@ module Net
400
482
  #
401
483
  def retrbinary(cmd, blocksize, rest_offset = nil) # :yield: data
402
484
  synchronize do
403
- voidcmd("TYPE I")
404
- conn = transfercmd(cmd, rest_offset)
405
- loop do
406
- data = conn.read(blocksize)
407
- break if data == nil
408
- yield(data)
409
- end
410
- conn.close
411
- voidresp
485
+ with_binary(true) do
486
+ begin
487
+ conn = transfercmd(cmd, rest_offset)
488
+ loop do
489
+ data = conn.read(blocksize)
490
+ break if data == nil
491
+ yield(data)
492
+ end
493
+ conn.shutdown(Socket::SHUT_WR)
494
+ conn.read_timeout = 1
495
+ conn.read
496
+ ensure
497
+ conn.close if conn
498
+ end
499
+ voidresp
500
+ end
412
501
  end
413
502
  end
414
-
503
+
415
504
  #
416
505
  # Puts the connection into ASCII (text) mode, issues the given command, and
417
506
  # passes the resulting data, one line at a time, to the associated block. If
@@ -420,110 +509,146 @@ module Net
420
509
  #
421
510
  def retrlines(cmd) # :yield: line
422
511
  synchronize do
423
- voidcmd("TYPE A")
424
- conn = transfercmd(cmd)
425
- loop do
426
- line = conn.gets
427
- break if line == nil
428
- if line[-2, 2] == CRLF
429
- line = line[0 .. -3]
430
- elsif line[-1] == ?\n
431
- line = line[0 .. -2]
432
- end
433
- yield(line)
434
- end
435
- conn.close
436
- voidresp
437
- end
438
- end
439
-
512
+ with_binary(false) do
513
+ begin
514
+ conn = transfercmd(cmd)
515
+ loop do
516
+ line = conn.gets
517
+ break if line == nil
518
+ yield(line.sub(/\r?\n\z/, ""), !line.match(/\n\z/).nil?)
519
+ end
520
+ conn.shutdown(Socket::SHUT_WR)
521
+ conn.read_timeout = 1
522
+ conn.read
523
+ ensure
524
+ conn.close if conn
525
+ end
526
+ voidresp
527
+ end
528
+ end
529
+ end
530
+
440
531
  #
441
532
  # Puts the connection into binary (image) mode, issues the given server-side
442
533
  # command (such as "STOR myfile"), and sends the contents of the file named
443
534
  # +file+ to the server. If the optional block is given, it also passes it
444
535
  # the data, in chunks of +blocksize+ characters.
445
536
  #
446
- def storbinary(cmd, file, blocksize, rest_offset = nil, &block) # :yield: data
537
+ def storbinary(cmd, file, blocksize, rest_offset = nil) # :yield: data
447
538
  if rest_offset
448
539
  file.seek(rest_offset, IO::SEEK_SET)
449
540
  end
450
541
  synchronize do
451
- voidcmd("TYPE I")
452
- conn = transfercmd(cmd, rest_offset)
453
- loop do
454
- buf = file.read(blocksize)
455
- break if buf == nil
456
- conn.write(buf)
457
- yield(buf) if block
458
- end
459
- conn.close
460
- voidresp
542
+ with_binary(true) do
543
+ conn = transfercmd(cmd)
544
+ loop do
545
+ buf = file.read(blocksize)
546
+ break if buf == nil
547
+ conn.write(buf)
548
+ yield(buf) if block_given?
549
+ end
550
+ conn.close
551
+ voidresp
552
+ end
461
553
  end
554
+ rescue Errno::EPIPE
555
+ # EPIPE, in this case, means that the data connection was unexpectedly
556
+ # terminated. Rather than just raising EPIPE to the caller, check the
557
+ # response on the control connection. If getresp doesn't raise a more
558
+ # appropriate exception, re-raise the original exception.
559
+ getresp
560
+ raise
462
561
  end
463
-
562
+
464
563
  #
465
564
  # Puts the connection into ASCII (text) mode, issues the given server-side
466
565
  # command (such as "STOR myfile"), and sends the contents of the file
467
566
  # named +file+ to the server, one line at a time. If the optional block is
468
567
  # given, it also passes it the lines.
469
568
  #
470
- def storlines(cmd, file, &block) # :yield: line
569
+ def storlines(cmd, file) # :yield: line
471
570
  synchronize do
472
- voidcmd("TYPE A")
473
- conn = transfercmd(cmd)
474
- loop do
475
- buf = file.gets
476
- break if buf == nil
477
- if buf[-2, 2] != CRLF
478
- buf = buf.chomp + CRLF
479
- end
480
- conn.write(buf)
481
- yield(buf) if block
482
- end
483
- conn.close
484
- voidresp
571
+ with_binary(false) do
572
+ conn = transfercmd(cmd)
573
+ loop do
574
+ buf = file.gets
575
+ break if buf == nil
576
+ if buf[-2, 2] != CRLF
577
+ buf = buf.chomp + CRLF
578
+ end
579
+ conn.write(buf)
580
+ yield(buf) if block_given?
581
+ end
582
+ conn.close
583
+ voidresp
584
+ end
485
585
  end
586
+ rescue Errno::EPIPE
587
+ # EPIPE, in this case, means that the data connection was unexpectedly
588
+ # terminated. Rather than just raising EPIPE to the caller, check the
589
+ # response on the control connection. If getresp doesn't raise a more
590
+ # appropriate exception, re-raise the original exception.
591
+ getresp
592
+ raise
486
593
  end
487
594
 
488
595
  #
489
596
  # Retrieves +remotefile+ in binary mode, storing the result in +localfile+.
597
+ # If +localfile+ is nil, returns retrieved data.
490
598
  # If a block is supplied, it is passed the retrieved data in +blocksize+
491
599
  # chunks.
492
600
  #
493
601
  def getbinaryfile(remotefile, localfile = File.basename(remotefile),
494
- blocksize = DEFAULT_BLOCKSIZE, &block) # :yield: data
495
- if @resume
496
- rest_offset = File.size?(localfile)
497
- f = open(localfile, "a")
498
- else
499
- rest_offset = nil
500
- f = open(localfile, "w")
602
+ blocksize = DEFAULT_BLOCKSIZE) # :yield: data
603
+ result = nil
604
+ if localfile
605
+ if @resume
606
+ rest_offset = File.size?(localfile)
607
+ f = open(localfile, "a")
608
+ else
609
+ rest_offset = nil
610
+ f = open(localfile, "w")
611
+ end
612
+ elsif !block_given?
613
+ result = ""
501
614
  end
502
615
  begin
503
- f.binmode
504
- retrbinary("RETR " + remotefile, blocksize, rest_offset) do |data|
505
- f.write(data)
506
- yield(data) if block
507
- end
616
+ f.binmode if localfile
617
+ retrbinary("RETR " + remotefile.to_s, blocksize, rest_offset) do |data|
618
+ f.write(data) if localfile
619
+ yield(data) if block_given?
620
+ result.concat(data) if result
621
+ end
622
+ return result
508
623
  ensure
509
- f.close
624
+ f.close if localfile
510
625
  end
511
626
  end
512
-
627
+
513
628
  #
514
629
  # Retrieves +remotefile+ in ASCII (text) mode, storing the result in
515
- # +localfile+. If a block is supplied, it is passed the retrieved data one
630
+ # +localfile+.
631
+ # If +localfile+ is nil, returns retrieved data.
632
+ # If a block is supplied, it is passed the retrieved data one
516
633
  # line at a time.
517
634
  #
518
- def gettextfile(remotefile, localfile = File.basename(remotefile), &block) # :yield: line
519
- f = open(localfile, "w")
635
+ def gettextfile(remotefile, localfile = File.basename(remotefile)) # :yield: line
636
+ result = nil
637
+ if localfile
638
+ f = open(localfile, "w")
639
+ elsif !block_given?
640
+ result = ""
641
+ end
520
642
  begin
521
- retrlines("RETR " + remotefile) do |line|
522
- f.puts(line)
523
- yield(line) if block
524
- end
643
+ retrlines("RETR " + remotefile) do |line, newline|
644
+ l = newline ? line + "\n" : line
645
+ f.print(l) if localfile
646
+ yield(line, newline) if block_given?
647
+ result.concat(l) if result
648
+ end
649
+ return result
525
650
  ensure
526
- f.close
651
+ f.close if localfile
527
652
  end
528
653
  end
529
654
 
@@ -532,21 +657,21 @@ module Net
532
657
  # binary). See #gettextfile and #getbinaryfile.
533
658
  #
534
659
  def get(remotefile, localfile = File.basename(remotefile),
535
- blocksize = DEFAULT_BLOCKSIZE, &block) # :yield: data
536
- unless @binary
537
- gettextfile(remotefile, localfile, &block)
660
+ blocksize = DEFAULT_BLOCKSIZE, &block) # :yield: data
661
+ if @binary
662
+ getbinaryfile(remotefile, localfile, blocksize, &block)
538
663
  else
539
- getbinaryfile(remotefile, localfile, blocksize, &block)
664
+ gettextfile(remotefile, localfile, &block)
540
665
  end
541
666
  end
542
-
667
+
543
668
  #
544
669
  # Transfers +localfile+ to the server in binary mode, storing the result in
545
670
  # +remotefile+. If a block is supplied, calls it, passing in the transmitted
546
671
  # data in +blocksize+ chunks.
547
672
  #
548
673
  def putbinaryfile(localfile, remotefile = File.basename(localfile),
549
- blocksize = DEFAULT_BLOCKSIZE, &block) # :yield: data
674
+ blocksize = DEFAULT_BLOCKSIZE, &block) # :yield: data
550
675
  if @resume
551
676
  begin
552
677
  rest_offset = size(remotefile)
@@ -554,17 +679,21 @@ module Net
554
679
  rest_offset = nil
555
680
  end
556
681
  else
557
- rest_offset = nil
682
+ rest_offset = nil
558
683
  end
559
684
  f = open(localfile)
560
685
  begin
561
- f.binmode
562
- storbinary("STOR " + remotefile, f, blocksize, rest_offset, &block)
686
+ f.binmode
687
+ if rest_offset
688
+ storbinary("APPE " + remotefile, f, blocksize, rest_offset, &block)
689
+ else
690
+ storbinary("STOR " + remotefile, f, blocksize, rest_offset, &block)
691
+ end
563
692
  ensure
564
- f.close
693
+ f.close
565
694
  end
566
695
  end
567
-
696
+
568
697
  #
569
698
  # Transfers +localfile+ to the server in ASCII (text) mode, storing the result
570
699
  # in +remotefile+. If callback or an associated block is supplied, calls it,
@@ -573,9 +702,9 @@ module Net
573
702
  def puttextfile(localfile, remotefile = File.basename(localfile), &block) # :yield: line
574
703
  f = open(localfile)
575
704
  begin
576
- storlines("STOR " + remotefile, f, &block)
705
+ storlines("STOR " + remotefile, f, &block)
577
706
  ensure
578
- f.close
707
+ f.close
579
708
  end
580
709
  end
581
710
 
@@ -584,37 +713,40 @@ module Net
584
713
  # (text or binary). See #puttextfile and #putbinaryfile.
585
714
  #
586
715
  def put(localfile, remotefile = File.basename(localfile),
587
- blocksize = DEFAULT_BLOCKSIZE, &block)
588
- unless @binary
589
- puttextfile(localfile, remotefile, &block)
716
+ blocksize = DEFAULT_BLOCKSIZE, &block)
717
+ if @binary
718
+ putbinaryfile(localfile, remotefile, blocksize, &block)
590
719
  else
591
- putbinaryfile(localfile, remotefile, blocksize, &block)
720
+ puttextfile(localfile, remotefile, &block)
592
721
  end
593
722
  end
594
723
 
595
724
  #
596
- # Sends the ACCT command. TODO: more info.
725
+ # Sends the ACCT command.
726
+ #
727
+ # This is a less common FTP command, to send account
728
+ # information if the destination host requires it.
597
729
  #
598
730
  def acct(account)
599
731
  cmd = "ACCT " + account
600
732
  voidcmd(cmd)
601
733
  end
602
-
734
+
603
735
  #
604
736
  # Returns an array of filenames in the remote directory.
605
737
  #
606
738
  def nlst(dir = nil)
607
739
  cmd = "NLST"
608
740
  if dir
609
- cmd = cmd + " " + dir
741
+ cmd = cmd + " " + dir
610
742
  end
611
743
  files = []
612
744
  retrlines(cmd) do |line|
613
- files.push(line)
745
+ files.push(line)
614
746
  end
615
747
  return files
616
748
  end
617
-
749
+
618
750
  #
619
751
  # Returns an array of file information in the directory (the output is like
620
752
  # `ls -l`). If a block is given, it iterates through the listing.
@@ -622,78 +754,79 @@ module Net
622
754
  def list(*args, &block) # :yield: line
623
755
  cmd = "LIST"
624
756
  args.each do |arg|
625
- cmd = cmd + " " + arg
757
+ cmd = cmd + " " + arg.to_s
626
758
  end
627
759
  if block
628
- retrlines(cmd, &block)
760
+ retrlines(cmd, &block)
629
761
  else
630
- lines = []
631
- retrlines(cmd) do |line|
632
- lines << line
633
- end
634
- return lines
762
+ lines = []
763
+ retrlines(cmd) do |line|
764
+ lines << line
765
+ end
766
+ return lines
635
767
  end
636
768
  end
637
769
  alias ls list
638
770
  alias dir list
639
-
771
+
640
772
  #
641
773
  # Renames a file on the server.
642
774
  #
643
775
  def rename(fromname, toname)
644
776
  resp = sendcmd("RNFR " + fromname)
645
777
  if resp[0] != ?3
646
- raise FTPReplyError, resp
778
+ raise FTPReplyError, resp
647
779
  end
648
780
  voidcmd("RNTO " + toname)
649
781
  end
650
-
782
+
651
783
  #
652
784
  # Deletes a file on the server.
653
785
  #
654
786
  def delete(filename)
655
787
  resp = sendcmd("DELE " + filename)
656
788
  if resp[0, 3] == "250"
657
- return
789
+ return
658
790
  elsif resp[0] == ?5
659
- raise FTPPermError, resp
791
+ raise FTPPermError, resp
660
792
  else
661
- raise FTPReplyError, resp
793
+ raise FTPReplyError, resp
662
794
  end
663
795
  end
664
-
796
+
665
797
  #
666
798
  # Changes the (remote) directory.
667
799
  #
668
800
  def chdir(dirname)
669
801
  if dirname == ".."
670
- begin
671
- voidcmd("CDUP")
672
- return
673
- rescue FTPPermError => e
674
- if e.message[0, 3] != "500"
675
- raise e
676
- end
677
- end
802
+ begin
803
+ voidcmd("CDUP")
804
+ return
805
+ rescue FTPPermError => e
806
+ if e.message[0, 3] != "500"
807
+ raise e
808
+ end
809
+ end
678
810
  end
679
811
  cmd = "CWD " + dirname
680
812
  voidcmd(cmd)
681
813
  end
682
-
814
+
683
815
  #
684
816
  # Returns the size of the given (remote) filename.
685
817
  #
686
818
  def size(filename)
687
- voidcmd("TYPE I")
688
- resp = sendcmd("SIZE " + filename)
689
- if resp[0, 3] != "213"
690
- raise FTPReplyError, resp
819
+ with_binary(true) do
820
+ resp = sendcmd("SIZE " + filename)
821
+ if resp[0, 3] != "213"
822
+ raise FTPReplyError, resp
823
+ end
824
+ return resp[3..-1].strip.to_i
691
825
  end
692
- return resp[3..-1].strip.to_i
693
826
  end
694
-
827
+
695
828
  MDTM_REGEXP = /^(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)/ # :nodoc:
696
-
829
+
697
830
  #
698
831
  # Returns the last modification time of the (remote) file. If +local+ is
699
832
  # +true+, it is returned as a local time, otherwise it's a UTC time.
@@ -703,7 +836,7 @@ module Net
703
836
  ary = str.scan(MDTM_REGEXP)[0].collect {|i| i.to_i}
704
837
  return local ? Time.local(*ary) : Time.gm(*ary)
705
838
  end
706
-
839
+
707
840
  #
708
841
  # Creates a remote directory.
709
842
  #
@@ -711,14 +844,14 @@ module Net
711
844
  resp = sendcmd("MKD " + dirname)
712
845
  return parse257(resp)
713
846
  end
714
-
847
+
715
848
  #
716
849
  # Removes a remote directory.
717
850
  #
718
851
  def rmdir(dirname)
719
852
  voidcmd("RMD " + dirname)
720
853
  end
721
-
854
+
722
855
  #
723
856
  # Returns the current remote directory.
724
857
  #
@@ -727,18 +860,18 @@ module Net
727
860
  return parse257(resp)
728
861
  end
729
862
  alias getdir pwd
730
-
863
+
731
864
  #
732
865
  # Returns system information.
733
866
  #
734
867
  def system
735
868
  resp = sendcmd("SYST")
736
869
  if resp[0, 3] != "215"
737
- raise FTPReplyError, resp
870
+ raise FTPReplyError, resp
738
871
  end
739
872
  return resp[4 .. -1]
740
873
  end
741
-
874
+
742
875
  #
743
876
  # Aborts the previous command (ABOR command).
744
877
  #
@@ -748,11 +881,11 @@ module Net
748
881
  @sock.send(line, Socket::MSG_OOB)
749
882
  resp = getmultiline
750
883
  unless ["426", "226", "225"].include?(resp[0, 3])
751
- raise FTPProtoError, resp
884
+ raise FTPProtoError, resp
752
885
  end
753
886
  return resp
754
887
  end
755
-
888
+
756
889
  #
757
890
  # Returns the status (STAT command).
758
891
  #
@@ -762,28 +895,28 @@ module Net
762
895
  @sock.send(line, Socket::MSG_OOB)
763
896
  return getresp
764
897
  end
765
-
898
+
766
899
  #
767
900
  # Issues the MDTM command. TODO: more info.
768
901
  #
769
902
  def mdtm(filename)
770
903
  resp = sendcmd("MDTM " + filename)
771
904
  if resp[0, 3] == "213"
772
- return resp[3 .. -1].strip
905
+ return resp[3 .. -1].strip
773
906
  end
774
907
  end
775
-
908
+
776
909
  #
777
910
  # Issues the HELP command.
778
911
  #
779
912
  def help(arg = nil)
780
913
  cmd = "HELP"
781
914
  if arg
782
- cmd = cmd + " " + arg
915
+ cmd = cmd + " " + arg
783
916
  end
784
917
  sendcmd(cmd)
785
918
  end
786
-
919
+
787
920
  #
788
921
  # Exits the FTP session.
789
922
  #
@@ -794,6 +927,8 @@ module Net
794
927
  #
795
928
  # Issues a NOOP command.
796
929
  #
930
+ # Does nothing except return a response.
931
+ #
797
932
  def noop
798
933
  voidcmd("NOOP")
799
934
  end
@@ -805,118 +940,175 @@ module Net
805
940
  cmd = "SITE " + arg
806
941
  voidcmd(cmd)
807
942
  end
808
-
943
+
809
944
  #
810
945
  # Closes the connection. Further operations are impossible until you open
811
946
  # a new connection with #connect.
812
947
  #
813
948
  def close
814
- @sock.close if @sock and not @sock.closed?
949
+ if @sock and not @sock.closed?
950
+ begin
951
+ @sock.shutdown(Socket::SHUT_WR) rescue nil
952
+ orig, self.read_timeout = self.read_timeout, 3
953
+ @sock.read rescue nil
954
+ ensure
955
+ @sock.close
956
+ self.read_timeout = orig
957
+ end
958
+ end
815
959
  end
816
-
960
+
817
961
  #
818
962
  # Returns +true+ iff the connection is closed.
819
963
  #
820
964
  def closed?
821
965
  @sock == nil or @sock.closed?
822
966
  end
823
-
824
- def parse227(resp)
967
+
968
+ # handler for response code 227
969
+ # (Entering Passive Mode (h1,h2,h3,h4,p1,p2))
970
+ #
971
+ # Returns host and port.
972
+ def parse227(resp) # :nodoc:
825
973
  if resp[0, 3] != "227"
826
- raise FTPReplyError, resp
827
- end
828
- left = resp.index("(")
829
- right = resp.index(")")
830
- if left == nil or right == nil
831
- raise FTPProtoError, resp
974
+ raise FTPReplyError, resp
832
975
  end
833
- numbers = resp[left + 1 .. right - 1].split(",")
834
- if numbers.length != 6
835
- raise FTPProtoError, resp
976
+ if m = /\((?<host>\d+(,\d+){3}),(?<port>\d+,\d+)\)/.match(resp)
977
+ return parse_pasv_ipv4_host(m["host"]), parse_pasv_port(m["port"])
978
+ else
979
+ raise FTPProtoError, resp
836
980
  end
837
- host = numbers[0, 4].join(".")
838
- port = (numbers[4].to_i << 8) + numbers[5].to_i
839
- return host, port
840
981
  end
841
982
  private :parse227
842
-
843
- def parse228(resp)
983
+
984
+ # handler for response code 228
985
+ # (Entering Long Passive Mode)
986
+ #
987
+ # Returns host and port.
988
+ def parse228(resp) # :nodoc:
844
989
  if resp[0, 3] != "228"
845
- raise FTPReplyError, resp
846
- end
847
- left = resp.index("(")
848
- right = resp.index(")")
849
- if left == nil or right == nil
850
- raise FTPProtoError, resp
851
- end
852
- numbers = resp[left + 1 .. right - 1].split(",")
853
- if numbers[0] == "4"
854
- if numbers.length != 9 || numbers[1] != "4" || numbers[2 + 4] != "2"
855
- raise FTPProtoError, resp
856
- end
857
- host = numbers[2, 4].join(".")
858
- port = (numbers[7].to_i << 8) + numbers[8].to_i
859
- elsif numbers[0] == "6"
860
- if numbers.length != 21 || numbers[1] != "16" || numbers[2 + 16] != "2"
861
- raise FTPProtoError, resp
862
- end
863
- v6 = ["", "", "", "", "", "", "", ""]
864
- for i in 0 .. 7
865
- v6[i] = sprintf("%02x%02x", numbers[(i * 2) + 2].to_i,
866
- numbers[(i * 2) + 3].to_i)
867
- end
868
- host = v6[0, 8].join(":")
869
- port = (numbers[19].to_i << 8) + numbers[20].to_i
870
- end
871
- return host, port
990
+ raise FTPReplyError, resp
991
+ end
992
+ if m = /\(4,4,(?<host>\d+(,\d+){3}),2,(?<port>\d+,\d+)\)/.match(resp)
993
+ return parse_pasv_ipv4_host(m["host"]), parse_pasv_port(m["port"])
994
+ elsif m = /\(6,16,(?<host>\d+(,(\d+)){15}),2,(?<port>\d+,\d+)\)/.match(resp)
995
+ return parse_pasv_ipv6_host(m["host"]), parse_pasv_port(m["port"])
996
+ else
997
+ raise FTPProtoError, resp
998
+ end
872
999
  end
873
1000
  private :parse228
874
-
875
- def parse229(resp)
1001
+
1002
+ def parse_pasv_ipv4_host(s)
1003
+ return s.tr(",", ".")
1004
+ end
1005
+ private :parse_pasv_ipv4_host
1006
+
1007
+ def parse_pasv_ipv6_host(s)
1008
+ return s.split(/,/).map { |i|
1009
+ "%02x" % i.to_i
1010
+ }.each_slice(2).map(&:join).join(":")
1011
+ end
1012
+ private :parse_pasv_ipv6_host
1013
+
1014
+ def parse_pasv_port(s)
1015
+ return s.split(/,/).map(&:to_i).inject { |x, y|
1016
+ (x << 8) + y
1017
+ }
1018
+ end
1019
+ private :parse_pasv_port
1020
+
1021
+ # handler for response code 229
1022
+ # (Extended Passive Mode Entered)
1023
+ #
1024
+ # Returns host and port.
1025
+ def parse229(resp) # :nodoc:
876
1026
  if resp[0, 3] != "229"
877
- raise FTPReplyError, resp
878
- end
879
- left = resp.index("(")
880
- right = resp.index(")")
881
- if left == nil or right == nil
882
- raise FTPProtoError, resp
1027
+ raise FTPReplyError, resp
883
1028
  end
884
- numbers = resp[left + 1 .. right - 1].split(resp[left + 1, 1])
885
- if numbers.length != 4
886
- raise FTPProtoError, resp
1029
+ if m = /\((?<d>[!-~])\k<d>\k<d>(?<port>\d+)\k<d>\)/.match(resp)
1030
+ return @sock.peeraddr[3], m["port"].to_i
1031
+ else
1032
+ raise FTPProtoError, resp
887
1033
  end
888
- port = numbers[3].to_i
889
- host = (@sock.peeraddr())[3]
890
- return host, port
891
1034
  end
892
1035
  private :parse229
893
-
894
- def parse257(resp)
1036
+
1037
+ # handler for response code 257
1038
+ # ("PATHNAME" created)
1039
+ #
1040
+ # Returns host and port.
1041
+ def parse257(resp) # :nodoc:
895
1042
  if resp[0, 3] != "257"
896
- raise FTPReplyError, resp
1043
+ raise FTPReplyError, resp
897
1044
  end
898
1045
  if resp[3, 2] != ' "'
899
- return ""
1046
+ return ""
900
1047
  end
901
1048
  dirname = ""
902
1049
  i = 5
903
1050
  n = resp.length
904
1051
  while i < n
905
- c = resp[i, 1]
906
- i = i + 1
907
- if c == '"'
908
- if i > n or resp[i, 1] != '"'
909
- break
910
- end
911
- i = i + 1
912
- end
913
- dirname = dirname + c
1052
+ c = resp[i, 1]
1053
+ i = i + 1
1054
+ if c == '"'
1055
+ if i > n or resp[i, 1] != '"'
1056
+ break
1057
+ end
1058
+ i = i + 1
1059
+ end
1060
+ dirname = dirname + c
914
1061
  end
915
1062
  return dirname
916
1063
  end
917
1064
  private :parse257
918
- end
919
1065
 
1066
+ # :stopdoc:
1067
+ class NullSocket
1068
+ def read_timeout=(sec)
1069
+ end
1070
+
1071
+ def close
1072
+ end
1073
+
1074
+ def method_missing(mid, *args)
1075
+ raise FTPConnectionError, "not connected"
1076
+ end
1077
+ end
1078
+
1079
+ class BufferedSocket < BufferedIO
1080
+ [:addr, :peeraddr, :send, :shutdown].each do |method|
1081
+ define_method(method) { |*args|
1082
+ @io.__send__(method, *args)
1083
+ }
1084
+ end
1085
+
1086
+ def read(len = nil)
1087
+ if len
1088
+ s = super(len, "", true)
1089
+ return s.empty? ? nil : s
1090
+ else
1091
+ result = ""
1092
+ while s = super(DEFAULT_BLOCKSIZE, "", true)
1093
+ break if s.empty?
1094
+ result << s
1095
+ end
1096
+ return result
1097
+ end
1098
+ end
1099
+
1100
+ def gets
1101
+ return readuntil("\n")
1102
+ rescue EOFError
1103
+ return nil
1104
+ end
1105
+
1106
+ def readline
1107
+ return readuntil("\n")
1108
+ end
1109
+ end
1110
+ # :startdoc:
1111
+ end
920
1112
  end
921
1113
 
922
1114