rubysl-net-ftp 1.0.0

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.
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)