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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.travis.yml +8 -0
- data/Gemfile +4 -0
- data/LICENSE +25 -0
- data/README.md +29 -0
- data/Rakefile +1 -0
- data/lib/net/ftp.rb +1 -0
- data/lib/net/ftptls.rb +1 -0
- data/lib/rubysl/net/ftp.rb +2 -0
- data/lib/rubysl/net/ftp/ftp.rb +926 -0
- data/lib/rubysl/net/ftp/version.rb +7 -0
- data/rubysl-net-ftp.gemspec +23 -0
- data/spec/FTPError_spec.rb +7 -0
- data/spec/FTPPermError_spec.rb +11 -0
- data/spec/FTPProtoError_spec.rb +11 -0
- data/spec/FTPReplyError_spec.rb +11 -0
- data/spec/FTPTempError_spec.rb +11 -0
- data/spec/abort_spec.rb +61 -0
- data/spec/acct_spec.rb +57 -0
- data/spec/binary_spec.rb +23 -0
- data/spec/chdir_spec.rb +100 -0
- data/spec/close_spec.rb +29 -0
- data/spec/closed_spec.rb +20 -0
- data/spec/connect_spec.rb +48 -0
- data/spec/debug_mode_spec.rb +22 -0
- data/spec/delete_spec.rb +58 -0
- data/spec/dir_spec.rb +7 -0
- data/spec/fixtures/putbinaryfile +3 -0
- data/spec/fixtures/puttextfile +3 -0
- data/spec/fixtures/server.rb +265 -0
- data/spec/get_spec.rb +20 -0
- data/spec/getbinaryfile_spec.rb +7 -0
- data/spec/getdir_spec.rb +6 -0
- data/spec/gettextfile_spec.rb +7 -0
- data/spec/help_spec.rb +65 -0
- data/spec/initialize_spec.rb +86 -0
- data/spec/last_response_code_spec.rb +7 -0
- data/spec/last_response_spec.rb +24 -0
- data/spec/lastresp_spec.rb +7 -0
- data/spec/list_spec.rb +7 -0
- data/spec/login_spec.rb +215 -0
- data/spec/ls_spec.rb +7 -0
- data/spec/mdtm_spec.rb +37 -0
- data/spec/mkdir_spec.rb +60 -0
- data/spec/mtime_spec.rb +49 -0
- data/spec/nlst_spec.rb +120 -0
- data/spec/noop_spec.rb +37 -0
- data/spec/open_spec.rb +54 -0
- data/spec/passive_spec.rb +23 -0
- data/spec/put_spec.rb +20 -0
- data/spec/putbinaryfile_spec.rb +7 -0
- data/spec/puttextfile_spec.rb +7 -0
- data/spec/pwd_spec.rb +52 -0
- data/spec/quit_spec.rb +32 -0
- data/spec/rename_spec.rb +93 -0
- data/spec/resume_spec.rb +22 -0
- data/spec/retrbinary_spec.rb +29 -0
- data/spec/retrlines_spec.rb +33 -0
- data/spec/return_code_spec.rb +23 -0
- data/spec/rmdir_spec.rb +57 -0
- data/spec/sendcmd_spec.rb +53 -0
- data/spec/set_socket_spec.rb +7 -0
- data/spec/shared/getbinaryfile.rb +179 -0
- data/spec/shared/gettextfile.rb +129 -0
- data/spec/shared/last_response_code.rb +25 -0
- data/spec/shared/list.rb +133 -0
- data/spec/shared/putbinaryfile.rb +232 -0
- data/spec/shared/puttextfile.rb +149 -0
- data/spec/shared/pwd.rb +3 -0
- data/spec/site_spec.rb +52 -0
- data/spec/size_spec.rb +47 -0
- data/spec/status_spec.rb +62 -0
- data/spec/storbinary_spec.rb +47 -0
- data/spec/storlines_spec.rb +42 -0
- data/spec/system_spec.rb +47 -0
- data/spec/voidcmd_spec.rb +53 -0
- data/spec/welcome_spec.rb +24 -0
- 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
data/.travis.yml
ADDED
data/Gemfile
ADDED
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,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)
|