rubysl-net-ftp 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.travis.yml +8 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE +25 -0
  6. data/README.md +29 -0
  7. data/Rakefile +1 -0
  8. data/lib/net/ftp.rb +1 -0
  9. data/lib/net/ftptls.rb +1 -0
  10. data/lib/rubysl/net/ftp.rb +2 -0
  11. data/lib/rubysl/net/ftp/ftp.rb +926 -0
  12. data/lib/rubysl/net/ftp/version.rb +7 -0
  13. data/rubysl-net-ftp.gemspec +23 -0
  14. data/spec/FTPError_spec.rb +7 -0
  15. data/spec/FTPPermError_spec.rb +11 -0
  16. data/spec/FTPProtoError_spec.rb +11 -0
  17. data/spec/FTPReplyError_spec.rb +11 -0
  18. data/spec/FTPTempError_spec.rb +11 -0
  19. data/spec/abort_spec.rb +61 -0
  20. data/spec/acct_spec.rb +57 -0
  21. data/spec/binary_spec.rb +23 -0
  22. data/spec/chdir_spec.rb +100 -0
  23. data/spec/close_spec.rb +29 -0
  24. data/spec/closed_spec.rb +20 -0
  25. data/spec/connect_spec.rb +48 -0
  26. data/spec/debug_mode_spec.rb +22 -0
  27. data/spec/delete_spec.rb +58 -0
  28. data/spec/dir_spec.rb +7 -0
  29. data/spec/fixtures/putbinaryfile +3 -0
  30. data/spec/fixtures/puttextfile +3 -0
  31. data/spec/fixtures/server.rb +265 -0
  32. data/spec/get_spec.rb +20 -0
  33. data/spec/getbinaryfile_spec.rb +7 -0
  34. data/spec/getdir_spec.rb +6 -0
  35. data/spec/gettextfile_spec.rb +7 -0
  36. data/spec/help_spec.rb +65 -0
  37. data/spec/initialize_spec.rb +86 -0
  38. data/spec/last_response_code_spec.rb +7 -0
  39. data/spec/last_response_spec.rb +24 -0
  40. data/spec/lastresp_spec.rb +7 -0
  41. data/spec/list_spec.rb +7 -0
  42. data/spec/login_spec.rb +215 -0
  43. data/spec/ls_spec.rb +7 -0
  44. data/spec/mdtm_spec.rb +37 -0
  45. data/spec/mkdir_spec.rb +60 -0
  46. data/spec/mtime_spec.rb +49 -0
  47. data/spec/nlst_spec.rb +120 -0
  48. data/spec/noop_spec.rb +37 -0
  49. data/spec/open_spec.rb +54 -0
  50. data/spec/passive_spec.rb +23 -0
  51. data/spec/put_spec.rb +20 -0
  52. data/spec/putbinaryfile_spec.rb +7 -0
  53. data/spec/puttextfile_spec.rb +7 -0
  54. data/spec/pwd_spec.rb +52 -0
  55. data/spec/quit_spec.rb +32 -0
  56. data/spec/rename_spec.rb +93 -0
  57. data/spec/resume_spec.rb +22 -0
  58. data/spec/retrbinary_spec.rb +29 -0
  59. data/spec/retrlines_spec.rb +33 -0
  60. data/spec/return_code_spec.rb +23 -0
  61. data/spec/rmdir_spec.rb +57 -0
  62. data/spec/sendcmd_spec.rb +53 -0
  63. data/spec/set_socket_spec.rb +7 -0
  64. data/spec/shared/getbinaryfile.rb +179 -0
  65. data/spec/shared/gettextfile.rb +129 -0
  66. data/spec/shared/last_response_code.rb +25 -0
  67. data/spec/shared/list.rb +133 -0
  68. data/spec/shared/putbinaryfile.rb +232 -0
  69. data/spec/shared/puttextfile.rb +149 -0
  70. data/spec/shared/pwd.rb +3 -0
  71. data/spec/site_spec.rb +52 -0
  72. data/spec/size_spec.rb +47 -0
  73. data/spec/status_spec.rb +62 -0
  74. data/spec/storbinary_spec.rb +47 -0
  75. data/spec/storlines_spec.rb +42 -0
  76. data/spec/system_spec.rb +47 -0
  77. data/spec/voidcmd_spec.rb +53 -0
  78. data/spec/welcome_spec.rb +24 -0
  79. metadata +243 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 056ab8cdeb2260ec5fc8c32aa3e859384b0f1293
4
+ data.tar.gz: edec86bfc6933c0580c237a73bd6c8dda32b78a9
5
+ SHA512:
6
+ metadata.gz: 4c78e9ed1e31a894ebd2949aa4d957c3c800b29e341437f7e1b22592d23e16a9cebb16ca275e4de11d09063ebbc059e566af1de9d9680bc7899a6b359e14e122
7
+ data.tar.gz: f97d6244aa78a2a07f894f2c71e4d8a9fbe85466e8bc4a493ee201c2c7ea60e997e0c8d8a1dc05d19c6fc28f74464271317e2261e1b5d25f32165a2bcb234aa1
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.travis.yml ADDED
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ before_install:
3
+ - gem update --system
4
+ - gem --version
5
+ - gem install rubysl-bundler
6
+ script: bundle exec mspec spec
7
+ rvm:
8
+ - rbx-nightly-18mode
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in rubysl-net-ftp.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,25 @@
1
+ Copyright (c) 2013, Brian Shirai
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+
7
+ 1. Redistributions of source code must retain the above copyright notice, this
8
+ list of conditions and the following disclaimer.
9
+ 2. Redistributions in binary form must reproduce the above copyright notice,
10
+ this list of conditions and the following disclaimer in the documentation
11
+ and/or other materials provided with the distribution.
12
+ 3. Neither the name of the library nor the names of its contributors may be
13
+ used to endorse or promote products derived from this software without
14
+ specific prior written permission.
15
+
16
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19
+ DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY DIRECT,
20
+ INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
21
+ BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
23
+ OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
24
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
25
+ EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # Rubysl::Net::Ftp
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'rubysl-net-ftp'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install rubysl-net-ftp
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/lib/net/ftp.rb ADDED
@@ -0,0 +1 @@
1
+ require "rubysl/net/ftp"
data/lib/net/ftptls.rb ADDED
@@ -0,0 +1 @@
1
+ require 'openssl/net/ftptls'
@@ -0,0 +1,2 @@
1
+ require "rubysl/net/ftp/ftp"
2
+ require "rubysl/net/ftp/version"
@@ -0,0 +1,926 @@
1
+ #
2
+ # = net/ftp.rb - FTP Client Library
3
+ #
4
+ # Written by Shugo Maeda <shugo@ruby-lang.org>.
5
+ #
6
+ # Documentation by Gavin Sinclair, sourced from "Programming Ruby" (Hunt/Thomas)
7
+ # and "Ruby In a Nutshell" (Matsumoto), used with permission.
8
+ #
9
+ # This library is distributed under the terms of the Ruby license.
10
+ # You can freely distribute/modify this library.
11
+ #
12
+ # It is included in the Ruby standard library.
13
+ #
14
+ # See the Net::FTP class for an overview.
15
+ #
16
+
17
+ require "socket"
18
+ require "monitor"
19
+
20
+ module Net
21
+
22
+ # :stopdoc:
23
+ class FTPError < StandardError; end
24
+ class FTPReplyError < FTPError; end
25
+ class FTPTempError < FTPError; end
26
+ class FTPPermError < FTPError; end
27
+ class FTPProtoError < FTPError; end
28
+ # :startdoc:
29
+
30
+ #
31
+ # This class implements the File Transfer Protocol. If you have used a
32
+ # command-line FTP program, and are familiar with the commands, you will be
33
+ # able to use this class easily. Some extra features are included to take
34
+ # advantage of Ruby's style and strengths.
35
+ #
36
+ # == Example
37
+ #
38
+ # require 'net/ftp'
39
+ #
40
+ # === Example 1
41
+ #
42
+ # ftp = Net::FTP.new('ftp.netlab.co.jp')
43
+ # ftp.login
44
+ # files = ftp.chdir('pub/lang/ruby/contrib')
45
+ # files = ftp.list('n*')
46
+ # ftp.getbinaryfile('nif.rb-0.91.gz', 'nif.gz', 1024)
47
+ # ftp.close
48
+ #
49
+ # === Example 2
50
+ #
51
+ # Net::FTP.open('ftp.netlab.co.jp') do |ftp|
52
+ # ftp.login
53
+ # files = ftp.chdir('pub/lang/ruby/contrib')
54
+ # files = ftp.list('n*')
55
+ # ftp.getbinaryfile('nif.rb-0.91.gz', 'nif.gz', 1024)
56
+ # end
57
+ #
58
+ # == Major Methods
59
+ #
60
+ # The following are the methods most likely to be useful to users:
61
+ # - FTP.open
62
+ # - #getbinaryfile
63
+ # - #gettextfile
64
+ # - #putbinaryfile
65
+ # - #puttextfile
66
+ # - #chdir
67
+ # - #nlst
68
+ # - #size
69
+ # - #rename
70
+ # - #delete
71
+ #
72
+ class FTP
73
+ include MonitorMixin
74
+
75
+ # :stopdoc:
76
+ FTP_PORT = 21
77
+ CRLF = "\r\n"
78
+ DEFAULT_BLOCKSIZE = 4096
79
+ # :startdoc:
80
+
81
+ # When +true+, transfers are performed in binary mode. Default: +true+.
82
+ attr_accessor :binary
83
+
84
+ # When +true+, the connection is in passive mode. Default: +false+.
85
+ attr_accessor :passive
86
+
87
+ # When +true+, all traffic to and from the server is written
88
+ # to +$stdout+. Default: +false+.
89
+ attr_accessor :debug_mode
90
+
91
+ # Sets or retrieves the +resume+ status, which decides whether incomplete
92
+ # transfers are resumed or restarted. Default: +false+.
93
+ attr_accessor :resume
94
+
95
+ # The server's welcome message.
96
+ attr_reader :welcome
97
+
98
+ # The server's last response code.
99
+ attr_reader :last_response_code
100
+ alias lastresp last_response_code
101
+
102
+ # The server's last response.
103
+ attr_reader :last_response
104
+
105
+ #
106
+ # A synonym for <tt>FTP.new</tt>, but with a mandatory host parameter.
107
+ #
108
+ # If a block is given, it is passed the +FTP+ object, which will be closed
109
+ # when the block finishes, or when an exception is raised.
110
+ #
111
+ def FTP.open(host, user = nil, passwd = nil, acct = nil)
112
+ if block_given?
113
+ ftp = new(host, user, passwd, acct)
114
+ begin
115
+ yield ftp
116
+ ensure
117
+ ftp.close
118
+ end
119
+ else
120
+ new(host, user, passwd, acct)
121
+ end
122
+ end
123
+
124
+ #
125
+ # Creates and returns a new +FTP+ object. If a +host+ is given, a connection
126
+ # is made. Additionally, if the +user+ is given, the given user name,
127
+ # password, and (optionally) account are used to log in. See #login.
128
+ #
129
+ def initialize(host = nil, user = nil, passwd = nil, acct = nil)
130
+ super()
131
+ @binary = true
132
+ @passive = false
133
+ @debug_mode = false
134
+ @resume = false
135
+ if host
136
+ connect(host)
137
+ if user
138
+ login(user, passwd, acct)
139
+ end
140
+ end
141
+ end
142
+
143
+ # Obsolete
144
+ def return_code
145
+ $stderr.puts("warning: Net::FTP#return_code is obsolete and do nothing")
146
+ return "\n"
147
+ end
148
+
149
+ # Obsolete
150
+ def return_code=(s)
151
+ $stderr.puts("warning: Net::FTP#return_code= is obsolete and do nothing")
152
+ end
153
+
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
161
+ end
162
+ private :open_socket
163
+
164
+ #
165
+ # Establishes an FTP connection to host, optionally overriding the default
166
+ # port. If the environment variable +SOCKS_SERVER+ is set, sets up the
167
+ # connection through a SOCKS proxy. Raises an exception (typically
168
+ # <tt>Errno::ECONNREFUSED</tt>) if the connection cannot be established.
169
+ #
170
+ def connect(host, port = FTP_PORT)
171
+ if @debug_mode
172
+ print "connect: ", host, ", ", port, "\n"
173
+ end
174
+ synchronize do
175
+ @sock = open_socket(host, port)
176
+ voidresp
177
+ end
178
+ end
179
+
180
+ #
181
+ # WRITEME or make private
182
+ #
183
+ def set_socket(sock, get_greeting = true)
184
+ synchronize do
185
+ @sock = sock
186
+ if get_greeting
187
+ voidresp
188
+ end
189
+ end
190
+ end
191
+
192
+ def sanitize(s)
193
+ if s =~ /^PASS /i
194
+ return s[0, 5] + "*" * (s.length - 5)
195
+ else
196
+ return s
197
+ end
198
+ end
199
+ private :sanitize
200
+
201
+ def putline(line)
202
+ if @debug_mode
203
+ print "put: ", sanitize(line), "\n"
204
+ end
205
+ line = line + CRLF
206
+ @sock.write(line)
207
+ end
208
+ private :putline
209
+
210
+ def getline
211
+ line = @sock.readline # if get EOF, raise EOFError
212
+ line.sub!(/(\r\n|\n|\r)\z/n, "")
213
+ if @debug_mode
214
+ print "get: ", sanitize(line), "\n"
215
+ end
216
+ return line
217
+ end
218
+ private :getline
219
+
220
+ def getmultiline
221
+ line = getline
222
+ buff = line
223
+ 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] != ?-
229
+ end
230
+ return buff << "\n"
231
+ end
232
+ private :getmultiline
233
+
234
+ def getresp
235
+ @last_response = getmultiline
236
+ @last_response_code = @last_response[0, 3]
237
+ case @last_response_code
238
+ when /\A[123]/
239
+ return @last_response
240
+ when /\A4/
241
+ raise FTPTempError, @last_response
242
+ when /\A5/
243
+ raise FTPPermError, @last_response
244
+ else
245
+ raise FTPProtoError, @last_response
246
+ end
247
+ end
248
+ private :getresp
249
+
250
+ def voidresp
251
+ resp = getresp
252
+ if resp[0] != ?2
253
+ raise FTPReplyError, resp
254
+ end
255
+ end
256
+ private :voidresp
257
+
258
+ #
259
+ # Sends a command and returns the response.
260
+ #
261
+ def sendcmd(cmd)
262
+ synchronize do
263
+ putline(cmd)
264
+ return getresp
265
+ end
266
+ end
267
+
268
+ #
269
+ # Sends a command and expect a response beginning with '2'.
270
+ #
271
+ def voidcmd(cmd)
272
+ synchronize do
273
+ putline(cmd)
274
+ voidresp
275
+ end
276
+ end
277
+
278
+ def sendport(host, port)
279
+ af = (@sock.peeraddr)[0]
280
+ if af == "AF_INET"
281
+ cmd = "PORT " + (host.split(".") + port.divmod(256)).join(",")
282
+ elsif af == "AF_INET6"
283
+ cmd = sprintf("EPRT |2|%s|%d|", host, port)
284
+ else
285
+ raise FTPProtoError, host
286
+ end
287
+ voidcmd(cmd)
288
+ end
289
+ private :sendport
290
+
291
+ def makeport
292
+ sock = TCPServer.open(@sock.addr[3], 0)
293
+ port = sock.addr[1]
294
+ host = sock.addr[3]
295
+ resp = sendport(host, port)
296
+ return sock
297
+ end
298
+ private :makeport
299
+
300
+ def makepasv
301
+ if @sock.peeraddr[0] == "AF_INET"
302
+ host, port = parse227(sendcmd("PASV"))
303
+ else
304
+ host, port = parse229(sendcmd("EPSV"))
305
+ # host, port = parse228(sendcmd("LPSV"))
306
+ end
307
+ return host, port
308
+ end
309
+ private :makepasv
310
+
311
+ def transfercmd(cmd, rest_offset = nil)
312
+ 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)
322
+ # skip 2XX for some ftp servers
323
+ resp = getresp if resp[0] == ?2
324
+ if resp[0] != ?1
325
+ raise FTPReplyError, resp
326
+ end
327
+ 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)
336
+ # skip 2XX for some ftp servers
337
+ resp = getresp if resp[0] == ?2
338
+ if resp[0] != ?1
339
+ raise FTPReplyError, resp
340
+ end
341
+ conn = sock.accept
342
+ sock.close
343
+ end
344
+ return conn
345
+ end
346
+ 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
+
364
+ #
365
+ # Logs in to the remote host. The session must have been previously
366
+ # connected. If +user+ is the string "anonymous" and the +password+ is
367
+ # +nil+, a password of <tt>user@host</tt> is synthesized. If the +acct+
368
+ # parameter is not +nil+, an FTP ACCT command is sent following the
369
+ # successful login. Raises an exception on error (typically
370
+ # <tt>Net::FTPPermError</tt>).
371
+ #
372
+ def login(user = "anonymous", passwd = nil, acct = nil)
373
+ if user == "anonymous" and passwd == nil
374
+ passwd = getaddress
375
+ end
376
+
377
+ resp = ""
378
+ synchronize do
379
+ resp = sendcmd('USER ' + user)
380
+ if resp[0] == ?3
381
+ raise FTPReplyError, resp if passwd.nil?
382
+ resp = sendcmd('PASS ' + passwd)
383
+ end
384
+ if resp[0] == ?3
385
+ raise FTPReplyError, resp if acct.nil?
386
+ resp = sendcmd('ACCT ' + acct)
387
+ end
388
+ end
389
+ if resp[0] != ?2
390
+ raise FTPReplyError, resp
391
+ end
392
+ @welcome = resp
393
+ end
394
+
395
+ #
396
+ # Puts the connection into binary (image) mode, issues the given command,
397
+ # and fetches the data returned, passing it to the associated block in
398
+ # chunks of +blocksize+ characters. Note that +cmd+ is a server command
399
+ # (such as "RETR myfile").
400
+ #
401
+ def retrbinary(cmd, blocksize, rest_offset = nil) # :yield: data
402
+ 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
412
+ end
413
+ end
414
+
415
+ #
416
+ # Puts the connection into ASCII (text) mode, issues the given command, and
417
+ # passes the resulting data, one line at a time, to the associated block. If
418
+ # no block is given, prints the lines. Note that +cmd+ is a server command
419
+ # (such as "RETR myfile").
420
+ #
421
+ def retrlines(cmd) # :yield: line
422
+ 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
+
440
+ #
441
+ # Puts the connection into binary (image) mode, issues the given server-side
442
+ # command (such as "STOR myfile"), and sends the contents of the file named
443
+ # +file+ to the server. If the optional block is given, it also passes it
444
+ # the data, in chunks of +blocksize+ characters.
445
+ #
446
+ def storbinary(cmd, file, blocksize, rest_offset = nil, &block) # :yield: data
447
+ if rest_offset
448
+ file.seek(rest_offset, IO::SEEK_SET)
449
+ end
450
+ 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
461
+ end
462
+ end
463
+
464
+ #
465
+ # Puts the connection into ASCII (text) mode, issues the given server-side
466
+ # command (such as "STOR myfile"), and sends the contents of the file
467
+ # named +file+ to the server, one line at a time. If the optional block is
468
+ # given, it also passes it the lines.
469
+ #
470
+ def storlines(cmd, file, &block) # :yield: line
471
+ 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
485
+ end
486
+ end
487
+
488
+ #
489
+ # Retrieves +remotefile+ in binary mode, storing the result in +localfile+.
490
+ # If a block is supplied, it is passed the retrieved data in +blocksize+
491
+ # chunks.
492
+ #
493
+ 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")
501
+ end
502
+ begin
503
+ f.binmode
504
+ retrbinary("RETR " + remotefile, blocksize, rest_offset) do |data|
505
+ f.write(data)
506
+ yield(data) if block
507
+ end
508
+ ensure
509
+ f.close
510
+ end
511
+ end
512
+
513
+ #
514
+ # Retrieves +remotefile+ in ASCII (text) mode, storing the result in
515
+ # +localfile+. If a block is supplied, it is passed the retrieved data one
516
+ # line at a time.
517
+ #
518
+ def gettextfile(remotefile, localfile = File.basename(remotefile), &block) # :yield: line
519
+ f = open(localfile, "w")
520
+ begin
521
+ retrlines("RETR " + remotefile) do |line|
522
+ f.puts(line)
523
+ yield(line) if block
524
+ end
525
+ ensure
526
+ f.close
527
+ end
528
+ end
529
+
530
+ #
531
+ # Retrieves +remotefile+ in whatever mode the session is set (text or
532
+ # binary). See #gettextfile and #getbinaryfile.
533
+ #
534
+ def get(remotefile, localfile = File.basename(remotefile),
535
+ blocksize = DEFAULT_BLOCKSIZE, &block) # :yield: data
536
+ unless @binary
537
+ gettextfile(remotefile, localfile, &block)
538
+ else
539
+ getbinaryfile(remotefile, localfile, blocksize, &block)
540
+ end
541
+ end
542
+
543
+ #
544
+ # Transfers +localfile+ to the server in binary mode, storing the result in
545
+ # +remotefile+. If a block is supplied, calls it, passing in the transmitted
546
+ # data in +blocksize+ chunks.
547
+ #
548
+ def putbinaryfile(localfile, remotefile = File.basename(localfile),
549
+ blocksize = DEFAULT_BLOCKSIZE, &block) # :yield: data
550
+ if @resume
551
+ begin
552
+ rest_offset = size(remotefile)
553
+ rescue Net::FTPPermError
554
+ rest_offset = nil
555
+ end
556
+ else
557
+ rest_offset = nil
558
+ end
559
+ f = open(localfile)
560
+ begin
561
+ f.binmode
562
+ storbinary("STOR " + remotefile, f, blocksize, rest_offset, &block)
563
+ ensure
564
+ f.close
565
+ end
566
+ end
567
+
568
+ #
569
+ # Transfers +localfile+ to the server in ASCII (text) mode, storing the result
570
+ # in +remotefile+. If callback or an associated block is supplied, calls it,
571
+ # passing in the transmitted data one line at a time.
572
+ #
573
+ def puttextfile(localfile, remotefile = File.basename(localfile), &block) # :yield: line
574
+ f = open(localfile)
575
+ begin
576
+ storlines("STOR " + remotefile, f, &block)
577
+ ensure
578
+ f.close
579
+ end
580
+ end
581
+
582
+ #
583
+ # Transfers +localfile+ to the server in whatever mode the session is set
584
+ # (text or binary). See #puttextfile and #putbinaryfile.
585
+ #
586
+ def put(localfile, remotefile = File.basename(localfile),
587
+ blocksize = DEFAULT_BLOCKSIZE, &block)
588
+ unless @binary
589
+ puttextfile(localfile, remotefile, &block)
590
+ else
591
+ putbinaryfile(localfile, remotefile, blocksize, &block)
592
+ end
593
+ end
594
+
595
+ #
596
+ # Sends the ACCT command. TODO: more info.
597
+ #
598
+ def acct(account)
599
+ cmd = "ACCT " + account
600
+ voidcmd(cmd)
601
+ end
602
+
603
+ #
604
+ # Returns an array of filenames in the remote directory.
605
+ #
606
+ def nlst(dir = nil)
607
+ cmd = "NLST"
608
+ if dir
609
+ cmd = cmd + " " + dir
610
+ end
611
+ files = []
612
+ retrlines(cmd) do |line|
613
+ files.push(line)
614
+ end
615
+ return files
616
+ end
617
+
618
+ #
619
+ # Returns an array of file information in the directory (the output is like
620
+ # `ls -l`). If a block is given, it iterates through the listing.
621
+ #
622
+ def list(*args, &block) # :yield: line
623
+ cmd = "LIST"
624
+ args.each do |arg|
625
+ cmd = cmd + " " + arg
626
+ end
627
+ if block
628
+ retrlines(cmd, &block)
629
+ else
630
+ lines = []
631
+ retrlines(cmd) do |line|
632
+ lines << line
633
+ end
634
+ return lines
635
+ end
636
+ end
637
+ alias ls list
638
+ alias dir list
639
+
640
+ #
641
+ # Renames a file on the server.
642
+ #
643
+ def rename(fromname, toname)
644
+ resp = sendcmd("RNFR " + fromname)
645
+ if resp[0] != ?3
646
+ raise FTPReplyError, resp
647
+ end
648
+ voidcmd("RNTO " + toname)
649
+ end
650
+
651
+ #
652
+ # Deletes a file on the server.
653
+ #
654
+ def delete(filename)
655
+ resp = sendcmd("DELE " + filename)
656
+ if resp[0, 3] == "250"
657
+ return
658
+ elsif resp[0] == ?5
659
+ raise FTPPermError, resp
660
+ else
661
+ raise FTPReplyError, resp
662
+ end
663
+ end
664
+
665
+ #
666
+ # Changes the (remote) directory.
667
+ #
668
+ def chdir(dirname)
669
+ 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
678
+ end
679
+ cmd = "CWD " + dirname
680
+ voidcmd(cmd)
681
+ end
682
+
683
+ #
684
+ # Returns the size of the given (remote) filename.
685
+ #
686
+ def size(filename)
687
+ voidcmd("TYPE I")
688
+ resp = sendcmd("SIZE " + filename)
689
+ if resp[0, 3] != "213"
690
+ raise FTPReplyError, resp
691
+ end
692
+ return resp[3..-1].strip.to_i
693
+ end
694
+
695
+ MDTM_REGEXP = /^(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)/ # :nodoc:
696
+
697
+ #
698
+ # Returns the last modification time of the (remote) file. If +local+ is
699
+ # +true+, it is returned as a local time, otherwise it's a UTC time.
700
+ #
701
+ def mtime(filename, local = false)
702
+ str = mdtm(filename)
703
+ ary = str.scan(MDTM_REGEXP)[0].collect {|i| i.to_i}
704
+ return local ? Time.local(*ary) : Time.gm(*ary)
705
+ end
706
+
707
+ #
708
+ # Creates a remote directory.
709
+ #
710
+ def mkdir(dirname)
711
+ resp = sendcmd("MKD " + dirname)
712
+ return parse257(resp)
713
+ end
714
+
715
+ #
716
+ # Removes a remote directory.
717
+ #
718
+ def rmdir(dirname)
719
+ voidcmd("RMD " + dirname)
720
+ end
721
+
722
+ #
723
+ # Returns the current remote directory.
724
+ #
725
+ def pwd
726
+ resp = sendcmd("PWD")
727
+ return parse257(resp)
728
+ end
729
+ alias getdir pwd
730
+
731
+ #
732
+ # Returns system information.
733
+ #
734
+ def system
735
+ resp = sendcmd("SYST")
736
+ if resp[0, 3] != "215"
737
+ raise FTPReplyError, resp
738
+ end
739
+ return resp[4 .. -1]
740
+ end
741
+
742
+ #
743
+ # Aborts the previous command (ABOR command).
744
+ #
745
+ def abort
746
+ line = "ABOR" + CRLF
747
+ print "put: ABOR\n" if @debug_mode
748
+ @sock.send(line, Socket::MSG_OOB)
749
+ resp = getmultiline
750
+ unless ["426", "226", "225"].include?(resp[0, 3])
751
+ raise FTPProtoError, resp
752
+ end
753
+ return resp
754
+ end
755
+
756
+ #
757
+ # Returns the status (STAT command).
758
+ #
759
+ def status
760
+ line = "STAT" + CRLF
761
+ print "put: STAT\n" if @debug_mode
762
+ @sock.send(line, Socket::MSG_OOB)
763
+ return getresp
764
+ end
765
+
766
+ #
767
+ # Issues the MDTM command. TODO: more info.
768
+ #
769
+ def mdtm(filename)
770
+ resp = sendcmd("MDTM " + filename)
771
+ if resp[0, 3] == "213"
772
+ return resp[3 .. -1].strip
773
+ end
774
+ end
775
+
776
+ #
777
+ # Issues the HELP command.
778
+ #
779
+ def help(arg = nil)
780
+ cmd = "HELP"
781
+ if arg
782
+ cmd = cmd + " " + arg
783
+ end
784
+ sendcmd(cmd)
785
+ end
786
+
787
+ #
788
+ # Exits the FTP session.
789
+ #
790
+ def quit
791
+ voidcmd("QUIT")
792
+ end
793
+
794
+ #
795
+ # Issues a NOOP command.
796
+ #
797
+ def noop
798
+ voidcmd("NOOP")
799
+ end
800
+
801
+ #
802
+ # Issues a SITE command.
803
+ #
804
+ def site(arg)
805
+ cmd = "SITE " + arg
806
+ voidcmd(cmd)
807
+ end
808
+
809
+ #
810
+ # Closes the connection. Further operations are impossible until you open
811
+ # a new connection with #connect.
812
+ #
813
+ def close
814
+ @sock.close if @sock and not @sock.closed?
815
+ end
816
+
817
+ #
818
+ # Returns +true+ iff the connection is closed.
819
+ #
820
+ def closed?
821
+ @sock == nil or @sock.closed?
822
+ end
823
+
824
+ def parse227(resp)
825
+ 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
832
+ end
833
+ numbers = resp[left + 1 .. right - 1].split(",")
834
+ if numbers.length != 6
835
+ raise FTPProtoError, resp
836
+ end
837
+ host = numbers[0, 4].join(".")
838
+ port = (numbers[4].to_i << 8) + numbers[5].to_i
839
+ return host, port
840
+ end
841
+ private :parse227
842
+
843
+ def parse228(resp)
844
+ 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
872
+ end
873
+ private :parse228
874
+
875
+ def parse229(resp)
876
+ 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
883
+ end
884
+ numbers = resp[left + 1 .. right - 1].split(resp[left + 1, 1])
885
+ if numbers.length != 4
886
+ raise FTPProtoError, resp
887
+ end
888
+ port = numbers[3].to_i
889
+ host = (@sock.peeraddr())[3]
890
+ return host, port
891
+ end
892
+ private :parse229
893
+
894
+ def parse257(resp)
895
+ if resp[0, 3] != "257"
896
+ raise FTPReplyError, resp
897
+ end
898
+ if resp[3, 2] != ' "'
899
+ return ""
900
+ end
901
+ dirname = ""
902
+ i = 5
903
+ n = resp.length
904
+ 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
914
+ end
915
+ return dirname
916
+ end
917
+ private :parse257
918
+ end
919
+
920
+ end
921
+
922
+
923
+ # Documentation comments:
924
+ # - sourced from pickaxe and nutshell, with improvements (hopefully)
925
+ # - three methods should be private (search WRITEME)
926
+ # - two methods need more information (search TODO)