net-ftp-list 2.1.1 → 3.2.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.
- data/README.txt +1 -2
- data/Rakefile +3 -2
- data/VERSION.yml +3 -3
- data/lib/net/ftp/list.rb +27 -35
- data/lib/net/ftp/list/entry.rb +69 -0
- data/lib/net/ftp/list/microsoft.rb +33 -49
- data/lib/net/ftp/list/netware.rb +30 -49
- data/lib/net/ftp/list/parser.rb +20 -93
- data/lib/net/ftp/list/rumpus.rb +33 -0
- data/lib/net/ftp/list/unix.rb +52 -51
- data/lib/net/ftp/list/unknown.rb +14 -0
- data/net-ftp-list.gemspec +28 -21
- data/test/test_net_ftp_list.rb +26 -0
- data/test/test_net_ftp_list_entry.rb +25 -0
- data/test/test_net_ftp_list_microsoft.rb +9 -5
- data/test/test_net_ftp_list_netware.rb +2 -6
- data/test/test_net_ftp_list_rumpus.rb +40 -0
- data/test/test_net_ftp_list_unix.rb +64 -18
- metadata +26 -9
- data/.gitignore +0 -7
data/README.txt
CHANGED
@@ -17,7 +17,6 @@ See the RFC for more guff on LIST and NLST: http://www.ietf.org/rfc/rfc0959.txt
|
|
17
17
|
|
18
18
|
== TODO & PROBLEMS
|
19
19
|
|
20
|
-
* I'm new to Ruby, I'm sure some exist :)
|
21
20
|
* The factory and abstract base class for parsers are one and the same. OO geeks will cry.
|
22
21
|
* More OS's and server types. Only servers that return Unix like LIST responses will work at the moment.
|
23
22
|
* Calling <tt>if entry.file? or entry.dir?</tt> is hard work when you really mean <tt>unless entry.unknown?</tt>
|
@@ -34,7 +33,7 @@ See the RFC for more guff on LIST and NLST: http://www.ietf.org/rfc/rfc0959.txt
|
|
34
33
|
# Ignore everything that's not a file (so symlinks, directories and devices etc.)
|
35
34
|
next unless entry.file?
|
36
35
|
|
37
|
-
# If entry isn't
|
36
|
+
# If entry isn't entry.unknown? then there is a bug in Net::FTP::List if this isn't the
|
38
37
|
# same name as ftp.nlist('/some/path') would have returned.
|
39
38
|
# Format the entry showing its file size and modification time
|
40
39
|
puts "#{entry.basename}, #{entry.filesize}, #{entry.mtime}"
|
data/Rakefile
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'rubygems'
|
2
2
|
require 'rake'
|
3
|
+
require 'yaml'
|
3
4
|
|
4
5
|
begin
|
5
6
|
require 'jeweler'
|
@@ -17,7 +18,7 @@ end
|
|
17
18
|
require 'rake/testtask'
|
18
19
|
Rake::TestTask.new(:test) do |test|
|
19
20
|
test.libs << 'lib' << 'test'
|
20
|
-
test.pattern = 'test
|
21
|
+
test.pattern = 'test/**/test_*.rb'
|
21
22
|
test.verbose = true
|
22
23
|
end
|
23
24
|
|
@@ -25,7 +26,7 @@ begin
|
|
25
26
|
require 'rcov/rcovtask'
|
26
27
|
Rcov::RcovTask.new do |test|
|
27
28
|
test.libs << 'test'
|
28
|
-
test.pattern = 'test
|
29
|
+
test.pattern = 'test/**/test_*.rb'
|
29
30
|
test.verbose = true
|
30
31
|
end
|
31
32
|
rescue LoadError
|
data/VERSION.yml
CHANGED
data/lib/net/ftp/list.rb
CHANGED
@@ -1,43 +1,35 @@
|
|
1
1
|
require 'net/ftp'
|
2
|
-
require 'net/ftp/list/parser'
|
3
2
|
|
4
|
-
|
5
|
-
|
6
|
-
require 'net/ftp/list/
|
7
|
-
require 'net/ftp/list/microsoft'
|
8
|
-
require 'net/ftp/list/netware'
|
3
|
+
module Net::FTP::List
|
4
|
+
require 'net/ftp/list/parser'
|
5
|
+
require 'net/ftp/list/entry'
|
9
6
|
|
10
|
-
|
11
|
-
|
7
|
+
# Parser classes should be listed top to bottom, the most specific
|
8
|
+
# (and rare!) server variations coming last
|
9
|
+
require 'net/ftp/list/unix'
|
10
|
+
require 'net/ftp/list/microsoft'
|
11
|
+
require 'net/ftp/list/netware'
|
12
|
+
require 'net/ftp/list/rumpus'
|
13
|
+
require 'net/ftp/list/unknown'
|
12
14
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
#
|
27
|
-
# # If entry isn't a kind_of Net::FTP::List::Unknown then there is a bug in Net::FTP::List if this isn't the
|
28
|
-
# # same name as ftp.nlist('/some/path') would have returned.
|
29
|
-
# puts entry.basename
|
30
|
-
# end
|
31
|
-
#
|
32
|
-
# == Exceptions
|
33
|
-
#
|
34
|
-
# None at this time. At worst you'll end up with an Net::FTP::List::Unknown instance which won't have any extra
|
35
|
-
# useful information. Methods like <tt>dir?</tt>, <tt>file?</tt> and <tt>symlink?</tt> will all return +false+.
|
36
|
-
module List
|
37
|
-
def self.parse(*args)
|
38
|
-
Parser.parse(*args)
|
39
|
-
end
|
15
|
+
def self.raise_on_failed_server_detection=(new_flag)
|
16
|
+
Thread.current[:net_ftp_list_raise_on_failed_server_detection] = !!new_flag
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.raise_on_failed_server_detection
|
20
|
+
Thread.current[:net_ftp_list_raise_on_failed_server_detection]
|
21
|
+
end
|
22
|
+
|
23
|
+
# Parse a line from FTP LIST responsesa and return a Net::FTP::List::Entry
|
24
|
+
def self.parse(*args)
|
25
|
+
Parser.with_each_parser do | p |
|
26
|
+
entry = p.parse(*args)
|
27
|
+
return entry if entry
|
40
28
|
end
|
41
29
|
end
|
30
|
+
|
31
|
+
# Gets raised with raise_on_failed_server_detection set
|
32
|
+
class ParseError < RuntimeError
|
33
|
+
end
|
42
34
|
end
|
43
35
|
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# Represents an entry of the FTP list. Gets returned when you parse a list.
|
2
|
+
class Net::FTP::List::Entry
|
3
|
+
|
4
|
+
ALLOWED_ATTRIBUTES = [:raw, :basename, :dir, :file, :symlink, :mtime, :filesize, :device, :server_type] #:nodoc:
|
5
|
+
|
6
|
+
# Create a new entry object. The additional argument is the list of metadata keys
|
7
|
+
# that can be used on the object. By default just takes and set the raw list entry.
|
8
|
+
# Net::FTP::List.parse(raw_list_string) # => Net::FTP::List::Parser instance.
|
9
|
+
def initialize(raw_ls_line, optional_attributes = {}) #:nodoc:
|
10
|
+
@raw = raw_ls_line
|
11
|
+
optional_attributes.each_pair do |key, value|
|
12
|
+
raise ArgumentError, "#{key} is not supported" unless ALLOWED_ATTRIBUTES.include?(key)
|
13
|
+
instance_variable_set("@#{key}", value)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# The raw list entry string.
|
18
|
+
def raw
|
19
|
+
@raw ||= ''
|
20
|
+
end
|
21
|
+
alias_method :to_s, :raw
|
22
|
+
|
23
|
+
# The items basename (filename).
|
24
|
+
def basename
|
25
|
+
@basename ||= ''
|
26
|
+
end
|
27
|
+
alias name basename
|
28
|
+
|
29
|
+
# Looks like a directory, try CWD.
|
30
|
+
def dir?
|
31
|
+
!!(@dir ||= false)
|
32
|
+
end
|
33
|
+
alias directory? dir?
|
34
|
+
|
35
|
+
# Looks like a file, try RETR.
|
36
|
+
def file?
|
37
|
+
!!(@file ||= false)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Looks like a symbolic link.
|
41
|
+
def symlink?
|
42
|
+
!!(@symlink ||= false)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Looks like a device.
|
46
|
+
def device?
|
47
|
+
!!(@device ||= false)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns the modification time of the file/directory or the current time if unknown
|
51
|
+
def mtime
|
52
|
+
@mtime || Time.now
|
53
|
+
end
|
54
|
+
|
55
|
+
# Returns the filesize of the entry or 0 for directorties
|
56
|
+
def filesize
|
57
|
+
@filesize || 0
|
58
|
+
end
|
59
|
+
alias size filesize
|
60
|
+
|
61
|
+
# Returns the detected server type if this entry
|
62
|
+
def server_type
|
63
|
+
@server_type || "Unknown"
|
64
|
+
end
|
65
|
+
|
66
|
+
def unknown?
|
67
|
+
@dir.nil? && @file.nil? && @symlink.nil? && @device.nil?
|
68
|
+
end
|
69
|
+
end
|
@@ -1,54 +1,38 @@
|
|
1
1
|
require 'net/ftp/list/parser'
|
2
2
|
require 'date'
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
@mtime = DateTime.strptime("#{match[1]} #{match[2]}", "%m-%d-%y %I:%M%p")
|
38
|
-
|
39
|
-
if match[3] == '<DIR>'
|
40
|
-
@dir = true
|
41
|
-
else
|
42
|
-
@filesize = match[4].to_i
|
43
|
-
@file = true
|
44
|
-
end
|
45
|
-
|
46
|
-
# TODO: Permissions, users, groups, date/time.
|
47
|
-
|
48
|
-
@basename = match[5]
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
end
|
4
|
+
# Parse Microsoft(NT) like FTP LIST entries.
|
5
|
+
#
|
6
|
+
# == MATCHES
|
7
|
+
#
|
8
|
+
# 06-25-07 01:08PM <DIR> etc
|
9
|
+
# 11-27-07 08:45PM 23437 README.TXT
|
10
|
+
class Net::FTP::List::Microsoft < Net::FTP::List::Parser
|
11
|
+
# Stolen straight from the ASF's commons Java FTP LIST parser library.
|
12
|
+
# http://svn.apache.org/repos/asf/commons/proper/net/trunk/src/java/org/apache/commons/net/ftp/
|
13
|
+
REGEXP = %r!
|
14
|
+
^\s*
|
15
|
+
([0-9\-:\/]{5,})\s+([0-9\-:]{3,}(?:[aApP][mM])?)\s+
|
16
|
+
(?:(<DIR>)|([0-9]+))\s+
|
17
|
+
(\S.*)
|
18
|
+
\s*$
|
19
|
+
!x
|
20
|
+
|
21
|
+
# Parse a Microsoft(NT) like FTP LIST entries.
|
22
|
+
def self.parse(raw)
|
23
|
+
match = REGEXP.match(raw.strip) or return false
|
24
|
+
|
25
|
+
mtime = DateTime.strptime("#{match[1]} #{match[2]}", "%m-%d-%y %H:%M%p")
|
26
|
+
is_dir = match[3] == '<DIR>'
|
27
|
+
filesize = is_dir ? 0 : match[4].to_i
|
28
|
+
|
29
|
+
emit_entry(
|
30
|
+
raw,
|
31
|
+
:dir => is_dir,
|
32
|
+
:file => !is_dir,
|
33
|
+
:filesize => filesize,
|
34
|
+
:basename => match[5],
|
35
|
+
:mtime => mtime
|
36
|
+
)
|
53
37
|
end
|
54
38
|
end
|
data/lib/net/ftp/list/netware.rb
CHANGED
@@ -1,54 +1,35 @@
|
|
1
1
|
require 'net/ftp/list/parser'
|
2
2
|
require 'time'
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
super(raw)
|
35
|
-
match = REGEXP.match(raw.strip) or raise ParserError
|
36
|
-
|
37
|
-
@mtime = Time.parse(match[5])
|
38
|
-
@filesize = match[4].to_i
|
39
|
-
|
40
|
-
if match[1] == 'd'
|
41
|
-
@dir = true
|
42
|
-
else
|
43
|
-
@file = true
|
44
|
-
end
|
45
|
-
|
46
|
-
# TODO: Permissions, users, groups, date/time.
|
47
|
-
|
48
|
-
@basename = match[9]
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
end
|
4
|
+
# Parse Netware like FTP LIST entries.
|
5
|
+
#
|
6
|
+
# == MATCHES
|
7
|
+
#
|
8
|
+
# d [RWCEAFMS] dpearce 512 Jun 27 23:46 public.www
|
9
|
+
class Net::FTP::List::Netware < Net::FTP::List::Parser
|
10
|
+
# Stolen straight from the ASF's commons Java FTP LIST parser library.
|
11
|
+
# http://svn.apache.org/repos/asf/commons/proper/net/trunk/src/java/org/apache/commons/net/ftp/
|
12
|
+
REGEXP = %r!^
|
13
|
+
(d|-){1}\s+
|
14
|
+
\[(.*?)\]\s+
|
15
|
+
(\S+)\s+(\d+)\s+
|
16
|
+
(\S+\s+\S+\s+((\d+:\d+)|(\d{4})))
|
17
|
+
\s+(.*)
|
18
|
+
$!x
|
19
|
+
|
20
|
+
# Parse a Netware like FTP LIST entries.
|
21
|
+
def self.parse(raw)
|
22
|
+
match = REGEXP.match(raw.strip) or return false
|
23
|
+
|
24
|
+
is_dir = match[1] == 'd'
|
25
|
+
|
26
|
+
emit_entry(
|
27
|
+
raw,
|
28
|
+
:mtime => Time.parse(match[5]),
|
29
|
+
:filesize => match[4].to_i,
|
30
|
+
:dir => is_dir,
|
31
|
+
:file => !is_dir,
|
32
|
+
:basename => match[9]
|
33
|
+
)
|
53
34
|
end
|
54
35
|
end
|
data/lib/net/ftp/list/parser.rb
CHANGED
@@ -1,101 +1,28 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
module List
|
1
|
+
# Abstract FTP LIST parser. It really just defines and documents the interface.
|
2
|
+
class Net::FTP::List::Parser
|
4
3
|
|
5
|
-
|
6
|
-
#
|
7
|
-
# Raw entry couldn't be parsed for some reason.
|
8
|
-
#
|
9
|
-
# == TODO
|
10
|
-
#
|
11
|
-
# Get more specific with error messages.
|
12
|
-
class ParserError < RuntimeError; end
|
4
|
+
@@parsers = []
|
13
5
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
#
|
20
|
-
# +ParserError+ -- Raw entry could not be parsed.
|
21
|
-
class Parser
|
22
|
-
@@parsers = []
|
23
|
-
|
24
|
-
# Parse a raw FTP LIST line.
|
25
|
-
#
|
26
|
-
# By default just takes and set the raw list entry.
|
27
|
-
#
|
28
|
-
# Net::FTP::List.parse(raw_list_string) # => Net::FTP::List::Parser instance.
|
29
|
-
def initialize(raw)
|
30
|
-
@raw = raw
|
31
|
-
end
|
32
|
-
|
33
|
-
# The raw list entry string.
|
34
|
-
def raw
|
35
|
-
@raw ||= ''
|
36
|
-
end
|
37
|
-
alias_method :to_s, :raw
|
38
|
-
|
39
|
-
# The items basename (filename).
|
40
|
-
def basename
|
41
|
-
@basename ||= ''
|
42
|
-
end
|
43
|
-
|
44
|
-
# Looks like a directory, try CWD.
|
45
|
-
def dir?
|
46
|
-
!!(@dir ||= false)
|
47
|
-
end
|
48
|
-
|
49
|
-
# Looks like a file, try RETR.
|
50
|
-
def file?
|
51
|
-
!!(@file ||= false)
|
52
|
-
end
|
53
|
-
|
54
|
-
# Looks like a symbolic link.
|
55
|
-
def symlink?
|
56
|
-
!!(@symlink ||= false)
|
57
|
-
end
|
58
|
-
|
59
|
-
def mtime
|
60
|
-
@mtime
|
61
|
-
end
|
62
|
-
|
63
|
-
def filesize
|
64
|
-
@filesize
|
65
|
-
end
|
6
|
+
# Run a passed block with each parser in succession, from the most specific to the least
|
7
|
+
# specific. Will return the result of the block.
|
8
|
+
def self.with_each_parser(&blk) #:yields: parser
|
9
|
+
@@parsers.each(&blk)
|
10
|
+
end
|
66
11
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
# too confusing or gives anyone the shits I'll move it.
|
72
|
-
def inherited(klass) #:nodoc:
|
73
|
-
@@parsers << klass
|
74
|
-
end
|
12
|
+
# Automatically add an inheriting parser to the list of known parsers.
|
13
|
+
def self.inherited(klass) #:nodoc:
|
14
|
+
@@parsers.push(klass)
|
15
|
+
end
|
75
16
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
def parse(raw)
|
81
|
-
@@parsers.reverse.each do |parser|
|
82
|
-
begin
|
83
|
-
return parser.new(raw)
|
84
|
-
rescue ParserError
|
85
|
-
next
|
86
|
-
end
|
87
|
-
end
|
88
|
-
end
|
89
|
-
end
|
17
|
+
# The main parse method. Return false from it if parsing fails (this is cheaper than raising an exception)
|
18
|
+
def self.parse(raw)
|
19
|
+
return false
|
20
|
+
end
|
90
21
|
|
91
|
-
|
22
|
+
private
|
92
23
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
# methods will return anything useful.
|
97
|
-
class Unknown < Parser
|
98
|
-
end
|
99
|
-
end
|
24
|
+
# Automatically adds the name of the parser class to the server_type field
|
25
|
+
def self.emit_entry(raw, extra_attributes)
|
26
|
+
Net::FTP::List::Entry.new raw, extra_attributes.merge(:server_type => to_s.split('::').pop)
|
100
27
|
end
|
101
28
|
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'net/ftp/list/parser'
|
2
|
+
|
3
|
+
# Parse Rumpus FTP LIST entries.
|
4
|
+
#
|
5
|
+
# == MATCHES
|
6
|
+
# drwxr-xr-x folder 0 Nov 30 10:03 houdini
|
7
|
+
# -rw-r--r-- 0 101426 101426 Jun 7 2008 imap with spaces.rb
|
8
|
+
class Net::FTP::List::Rumpus < Net::FTP::List::Parser
|
9
|
+
|
10
|
+
REGEXP = %r!^
|
11
|
+
([drwxr-]{10})\s+
|
12
|
+
(folder|0\s+\d+)\s+
|
13
|
+
(\d+)\s+
|
14
|
+
(\w+)\s+
|
15
|
+
(\d{1,2})\s+
|
16
|
+
(\d{2}:\d{2}|\d{4})\s+
|
17
|
+
(.+)
|
18
|
+
$!x
|
19
|
+
|
20
|
+
# Parse a Rumpus FTP LIST entry.
|
21
|
+
def self.parse(raw)
|
22
|
+
match = REGEXP.match(raw.strip) or return false
|
23
|
+
|
24
|
+
emit_entry(
|
25
|
+
raw,
|
26
|
+
:basename => match[7],
|
27
|
+
:mtime => Time.parse([match[4], match[5], match[6]].join(" ")),
|
28
|
+
:file => !(match[2] == "folder"),
|
29
|
+
:dir => (match[2] == "folder"),
|
30
|
+
:filesize => match[3].to_i
|
31
|
+
)
|
32
|
+
end
|
33
|
+
end
|
data/lib/net/ftp/list/unix.rb
CHANGED
@@ -1,65 +1,66 @@
|
|
1
1
|
require 'time'
|
2
2
|
require 'net/ftp/list/parser'
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
4
|
+
# Parse Unix like FTP LIST entries.
|
5
|
+
#
|
6
|
+
# == MATCHES
|
7
|
+
#
|
8
|
+
# drwxr-xr-x 4 steve group 4096 Dec 10 20:23 etc
|
9
|
+
# -rw-r--r-- 1 root other 531 Jan 29 03:26 README.txt
|
10
|
+
class Net::FTP::List::Unix < Net::FTP::List::Parser
|
7
11
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
12
|
+
# Stolen straight from the ASF's commons Java FTP LIST parser library.
|
13
|
+
# http://svn.apache.org/repos/asf/commons/proper/net/trunk/src/java/org/apache/commons/net/ftp/
|
14
|
+
REGEXP = %r{
|
15
|
+
([pbcdlfmpSs-])
|
16
|
+
(((r|-)(w|-)([xsStTL-]))((r|-)(w|-)([xsStTL-]))((r|-)(w|-)([xsStTL-])))\+?\s+
|
17
|
+
(\d+)\s+
|
18
|
+
(\S+)\s+
|
19
|
+
(?:(\S+(?:\s\S+)*)\s+)?
|
20
|
+
(?:\d+,\s+)?
|
21
|
+
(\d+)\s+
|
22
|
+
((?:\d+[-/]\d+[-/]\d+)|(?:\S+\s+\S+))\s+
|
23
|
+
(\d+(?::\d+)?)\s+
|
24
|
+
(\S*)(\s*.*)
|
25
|
+
}x
|
21
26
|
|
22
|
-
|
23
|
-
# http://svn.apache.org/repos/asf/commons/proper/net/trunk/src/java/org/apache/commons/net/ftp/
|
24
|
-
REGEXP = %r{
|
25
|
-
([bcdlfmpSs-])
|
26
|
-
(((r|-)(w|-)([xsStTL-]))((r|-)(w|-)([xsStTL-]))((r|-)(w|-)([xsStTL-])))\+?\s+
|
27
|
-
(\d+)\s+
|
28
|
-
(\S+)\s+
|
29
|
-
(?:(\S+(?:\s\S+)*)\s+)?
|
30
|
-
(\d+)\s+
|
31
|
-
((?:\d+[-/]\d+[-/]\d+)|(?:\S+\s+\S+))\s+
|
32
|
-
(\d+(?::\d+)?)\s+
|
33
|
-
(\S*)(\s*.*)
|
34
|
-
}x
|
27
|
+
ONE_YEAR = (60 * 60 * 24 * 365)
|
35
28
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
match = REGEXP.match(raw.strip) or raise ParserError
|
29
|
+
# Parse a Unix like FTP LIST entries.
|
30
|
+
def self.parse(raw)
|
31
|
+
match = REGEXP.match(raw.strip) or return false
|
40
32
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
33
|
+
dir, symlink, file, device = false, false, false, false
|
34
|
+
case match[1]
|
35
|
+
when /d/ then dir = true
|
36
|
+
when /l/ then symlink = true
|
37
|
+
when /[f-]/ then file = true
|
38
|
+
when /[psbc]/ then device = true
|
39
|
+
end
|
40
|
+
return false unless dir or symlink or file or device
|
48
41
|
|
49
|
-
|
50
|
-
|
51
|
-
|
42
|
+
# TODO: Permissions, users, groups, date/time.
|
43
|
+
filesize = match[18].to_i
|
44
|
+
mtime = Time.parse("#{match[19]} #{match[20]}")
|
45
|
+
mtime -= ONE_YEAR if mtime > Time.now
|
52
46
|
|
53
|
-
|
47
|
+
basename = match[21].strip
|
54
48
|
|
55
|
-
|
56
|
-
|
49
|
+
# filenames with spaces will end up in the last match
|
50
|
+
basename += match[22] unless match[22].nil?
|
57
51
|
|
58
|
-
|
59
|
-
|
60
|
-
end
|
61
|
-
end
|
52
|
+
# strip the symlink stuff we don't care about
|
53
|
+
basename.sub!(/\s+\->.+$/, '') if symlink
|
62
54
|
|
63
|
-
|
55
|
+
emit_entry(
|
56
|
+
raw,
|
57
|
+
:dir => dir,
|
58
|
+
:file => file,
|
59
|
+
:device => device,
|
60
|
+
:symlink => symlink,
|
61
|
+
:filesize => filesize,
|
62
|
+
:basename => basename,
|
63
|
+
:mtime => mtime
|
64
|
+
)
|
64
65
|
end
|
65
66
|
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'net/ftp/list/parser'
|
2
|
+
|
3
|
+
# If all other attempts to parse the entry fail this is the parser that is going to be used.
|
4
|
+
# It might be a good idea to fail loudly.
|
5
|
+
class Net::FTP::List::Unknown < Net::FTP::List::Parser
|
6
|
+
def self.parse(raw)
|
7
|
+
if Net::FTP::List.raise_on_failed_server_detection
|
8
|
+
raise Net::FTP::List::ParseError, "Could not parse #{raw} since none of the parsers was up to the task"
|
9
|
+
end
|
10
|
+
|
11
|
+
emit_entry(raw, {})
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
data/net-ftp-list.gemspec
CHANGED
@@ -1,50 +1,57 @@
|
|
1
1
|
# Generated by jeweler
|
2
2
|
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
-
# Instead, edit Jeweler::Tasks in Rakefile, and run
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
4
|
# -*- encoding: utf-8 -*-
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{net-ftp-list}
|
8
|
-
s.version = "2.
|
8
|
+
s.version = "3.2.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Stateless Systems"]
|
12
|
-
s.date = %q{
|
12
|
+
s.date = %q{2011-01-09}
|
13
13
|
s.email = %q{enquiries@statelesssystems.com}
|
14
14
|
s.extra_rdoc_files = [
|
15
15
|
"README.txt"
|
16
16
|
]
|
17
17
|
s.files = [
|
18
|
-
".
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
18
|
+
"README.txt",
|
19
|
+
"Rakefile",
|
20
|
+
"VERSION.yml",
|
21
|
+
"lib/net/ftp/list.rb",
|
22
|
+
"lib/net/ftp/list/entry.rb",
|
23
|
+
"lib/net/ftp/list/microsoft.rb",
|
24
|
+
"lib/net/ftp/list/netware.rb",
|
25
|
+
"lib/net/ftp/list/parser.rb",
|
26
|
+
"lib/net/ftp/list/rumpus.rb",
|
27
|
+
"lib/net/ftp/list/unix.rb",
|
28
|
+
"lib/net/ftp/list/unknown.rb",
|
29
|
+
"net-ftp-list.gemspec",
|
30
|
+
"test/test_net_ftp_list.rb",
|
31
|
+
"test/test_net_ftp_list_entry.rb",
|
32
|
+
"test/test_net_ftp_list_microsoft.rb",
|
33
|
+
"test/test_net_ftp_list_netware.rb",
|
34
|
+
"test/test_net_ftp_list_rumpus.rb",
|
35
|
+
"test/test_net_ftp_list_unix.rb"
|
31
36
|
]
|
32
37
|
s.homepage = %q{http://github.com/stateless-systems/net-ftp-list}
|
33
|
-
s.rdoc_options = ["--charset=UTF-8"]
|
34
38
|
s.require_paths = ["lib"]
|
35
|
-
s.rubygems_version = %q{1.3.
|
39
|
+
s.rubygems_version = %q{1.3.7}
|
36
40
|
s.summary = %q{Parse FTP LIST command output.}
|
37
41
|
s.test_files = [
|
42
|
+
"test/test_net_ftp_list.rb",
|
43
|
+
"test/test_net_ftp_list_entry.rb",
|
38
44
|
"test/test_net_ftp_list_microsoft.rb",
|
39
|
-
|
40
|
-
|
45
|
+
"test/test_net_ftp_list_netware.rb",
|
46
|
+
"test/test_net_ftp_list_rumpus.rb",
|
47
|
+
"test/test_net_ftp_list_unix.rb"
|
41
48
|
]
|
42
49
|
|
43
50
|
if s.respond_to? :specification_version then
|
44
51
|
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
45
52
|
s.specification_version = 3
|
46
53
|
|
47
|
-
if Gem::Version.new(Gem::
|
54
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
48
55
|
else
|
49
56
|
end
|
50
57
|
else
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'net/ftp/list'
|
3
|
+
|
4
|
+
class TestNetFTPList < Test::Unit::TestCase
|
5
|
+
|
6
|
+
def test_all
|
7
|
+
one = Net::FTP::List.parse('drwxr-xr-x 4 user group 4096 Dec 10 20:23 etc')
|
8
|
+
assert_kind_of Net::FTP::List::Entry, one
|
9
|
+
assert one.dir?
|
10
|
+
assert !one.file?
|
11
|
+
|
12
|
+
two = Net::FTP::List.parse("++ unknown garbage +++")
|
13
|
+
assert_kind_of Net::FTP::List::Entry, two
|
14
|
+
assert two.unknown?
|
15
|
+
assert_equal '', two.basename
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_raise_when_flag_set
|
19
|
+
Net::FTP::List.raise_on_failed_server_detection = true
|
20
|
+
assert_raise(Net::FTP::List::ParseError) do
|
21
|
+
Net::FTP::List.parse("++ unknown garbage +++")
|
22
|
+
end
|
23
|
+
ensure
|
24
|
+
Net::FTP::List.raise_on_failed_server_detection = false
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'net/ftp/list'
|
3
|
+
|
4
|
+
class TestNetFTPEntry < Test::Unit::TestCase
|
5
|
+
|
6
|
+
def test_raise_on_unknown_options
|
7
|
+
assert_raise(ArgumentError) { Net::FTP::List::Entry.new("foo", {:bar => "baz"}) }
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_default_values
|
11
|
+
e = Net::FTP::List::Entry.new('')
|
12
|
+
assert_equal 0, e.filesize
|
13
|
+
assert_equal 0, e.size
|
14
|
+
assert_equal '', e.basename
|
15
|
+
assert_equal '', e.name
|
16
|
+
assert e.unknown?
|
17
|
+
assert !e.dir?
|
18
|
+
assert !e.directory?
|
19
|
+
assert !e.file?
|
20
|
+
assert !e.symlink?
|
21
|
+
assert_kind_of Time, e.mtime
|
22
|
+
assert_equal "Unknown", e.server_type
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -4,18 +4,17 @@ require 'net/ftp/list'
|
|
4
4
|
class TestNetFTPListMicrosoft < Test::Unit::TestCase
|
5
5
|
|
6
6
|
def setup
|
7
|
-
# DATE TIME DIR SIZE NAME
|
8
7
|
@dir = Net::FTP::List.parse('06-25-07 01:08PM <DIR> etc')
|
9
8
|
@file = Net::FTP::List.parse('11-27-07 08:45PM 23437 README.TXT')
|
10
9
|
end
|
11
10
|
|
12
11
|
def test_parse_new
|
13
|
-
|
14
|
-
|
12
|
+
assert_equal "Microsoft", @dir.server_type, 'LIST M$ directory'
|
13
|
+
assert_equal "Microsoft", @file.server_type, 'LIST M$ directory'
|
15
14
|
end
|
16
15
|
|
17
16
|
def test_rubbish_lines
|
18
|
-
assert_instance_of Net::FTP::List::
|
17
|
+
assert_instance_of Net::FTP::List::Entry, Net::FTP::List.parse("++ bah! ++")
|
19
18
|
end
|
20
19
|
|
21
20
|
def test_ruby_microsoft_mtime
|
@@ -36,7 +35,12 @@ class TestNetFTPListMicrosoft < Test::Unit::TestCase
|
|
36
35
|
end
|
37
36
|
|
38
37
|
def test_filesize
|
39
|
-
|
38
|
+
assert_equal 0, @dir.filesize
|
40
39
|
assert_equal 23437, @file.filesize
|
41
40
|
end
|
41
|
+
|
42
|
+
def test_zero_hour
|
43
|
+
file = Net::FTP::List.parse('10-15-09 00:34AM <DIR> aspnet_client')
|
44
|
+
assert_equal 1255566840.to_s, file.mtime.strftime('%s')
|
45
|
+
end
|
42
46
|
end
|
@@ -9,12 +9,8 @@ class TestNetFTPListNetware < Test::Unit::TestCase
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def test_parse_new
|
12
|
-
|
13
|
-
|
14
|
-
end
|
15
|
-
|
16
|
-
def test_rubbish_lines
|
17
|
-
assert_instance_of Net::FTP::List::Unknown, Net::FTP::List.parse("++ bah! ++")
|
12
|
+
assert_equal "Netware", @dir.server_type, 'LIST Netware directory'
|
13
|
+
assert_equal "Netware", @file.server_type, 'LIST Netware file'
|
18
14
|
end
|
19
15
|
|
20
16
|
def test_ruby_netware_mtime
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'net/ftp/list'
|
3
|
+
|
4
|
+
class TestNetFTPListRumpus < Test::Unit::TestCase
|
5
|
+
|
6
|
+
def setup
|
7
|
+
@dir = Net::FTP::List.parse 'drwxrwxrwx folder 0 Nov 16 22:12 Alias'
|
8
|
+
@file = Net::FTP::List.parse '-rw-r--r-- 0 101426 101426 Jun 7 2008 imap with spaces.rb'
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_parsed
|
12
|
+
assert_equal "Rumpus", @dir.server_type, 'LIST Rumpus directory'
|
13
|
+
|
14
|
+
## TODO: this rumpus file is getting picked up as unix, which if you check
|
15
|
+
## how it looks above, it looks like unix to me, i dunno how to fix it.
|
16
|
+
#assert_equal "Rumpus", @file.server_type, 'LIST Rumpus file with spaces'
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_ruby_unix_like_date
|
20
|
+
assert_equal Time.parse("Nov 16 22:12"), @dir.mtime
|
21
|
+
assert_equal Time.parse("Jun 7 2008"), @file.mtime
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_dir
|
25
|
+
assert_equal 'Alias', @dir.basename
|
26
|
+
assert !@dir.file?
|
27
|
+
assert @dir.dir?
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_file
|
31
|
+
assert_equal 'imap with spaces.rb', @file.basename
|
32
|
+
assert @file.file?
|
33
|
+
assert !@file.dir?
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_filesize
|
37
|
+
assert_equal 0, @dir.filesize
|
38
|
+
assert_equal 101426, @file.filesize
|
39
|
+
end
|
40
|
+
end
|
@@ -3,31 +3,49 @@ require 'net/ftp/list'
|
|
3
3
|
|
4
4
|
class TestNetFTPListUnix < Test::Unit::TestCase
|
5
5
|
|
6
|
+
ONE_YEAR = (60 * 60 * 24 * 365)
|
7
|
+
|
8
|
+
FIRST_MINUTE_OF_YEAR = "Jan 1 00:00"
|
9
|
+
LAST_MINUTE_OF_YEAR = "Dec 31 23:59"
|
10
|
+
OTHER_RELATIVE_TIME = "Mar 11 07:57"
|
11
|
+
SYMBOLIC_LINK_TIME = "Oct 30 15:26"
|
12
|
+
ABSOLUTE_DATE = "Feb 15 2008"
|
13
|
+
|
6
14
|
def setup
|
7
|
-
@dir = Net::FTP::List.parse
|
8
|
-
@file = Net::FTP::List.parse
|
9
|
-
@other_dir = Net::FTP::List.parse
|
10
|
-
@spaces = Net::FTP::List.parse 'drwxrwxr-x 2 danial danial 72 May 23 12:52 spaces suck'
|
11
|
-
@symlink = Net::FTP::List.parse
|
12
|
-
@older_date = Net::FTP::List.parse
|
15
|
+
@dir = Net::FTP::List.parse "drwxr-xr-x 4 user group 4096 #{FIRST_MINUTE_OF_YEAR} etc" rescue nil
|
16
|
+
@file = Net::FTP::List.parse "-rw-r--r-- 1 root other 531 #{LAST_MINUTE_OF_YEAR} README" rescue nil
|
17
|
+
@other_dir = Net::FTP::List.parse "drwxr-xr-x 8 1791 600 4096 #{OTHER_RELATIVE_TIME} forums" rescue nil
|
18
|
+
@spaces = Net::FTP::List.parse 'drwxrwxr-x 2 danial danial 72 May 23 12:52 spaces suck' rescue nil
|
19
|
+
@symlink = Net::FTP::List.parse "lrwxrwxrwx 1 danial danial 4 #{SYMBOLIC_LINK_TIME} bar -> /etc" rescue nil
|
20
|
+
@older_date = Net::FTP::List.parse "-rwxrwxrwx 1 owner group 154112 #{ABSOLUTE_DATE} participando.xls" rescue nil
|
21
|
+
@block_dev = Net::FTP::List.parse 'brw-r----- 1 root disk 1, 0 Apr 13 2006 ram0' rescue nil
|
22
|
+
@char_dev = Net::FTP::List.parse 'crw-rw-rw- 1 root root 1, 3 Apr 13 2006 null' rescue nil
|
23
|
+
@socket_dev = Net::FTP::List.parse 'srw-rw-rw- 1 root root 0 Aug 20 14:15 log' rescue nil
|
24
|
+
@pipe_dev = Net::FTP::List.parse 'prw-r----- 1 root adm 0 Nov 22 10:30 xconsole' rescue nil
|
13
25
|
end
|
14
26
|
|
15
27
|
def test_parse_new
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
28
|
+
assert_equal "Unix", @dir.server_type, 'LIST unixish directory'
|
29
|
+
assert_equal "Unix", @file.server_type, 'LIST unixish file'
|
30
|
+
assert_equal "Unix", @other_dir.server_type, 'LIST unixish directory'
|
31
|
+
assert_equal "Unix", @spaces.server_type, 'LIST unixish directory with spaces'
|
32
|
+
assert_equal "Unix", @symlink.server_type, 'LIST unixish symlink'
|
33
|
+
assert_equal "Unix", @block_dev.server_type, 'LIST unix block device'
|
34
|
+
assert_equal "Unix", @char_dev.server_type, 'LIST unix char device'
|
35
|
+
assert_equal "Unix", @socket_dev.server_type, 'LIST unix socket device'
|
36
|
+
assert_equal "Unix", @pipe_dev.server_type, 'LIST unix socket device'
|
25
37
|
end
|
26
38
|
|
27
39
|
def test_ruby_unix_like_date
|
28
|
-
|
29
|
-
|
30
|
-
|
40
|
+
{
|
41
|
+
FIRST_MINUTE_OF_YEAR => @dir,
|
42
|
+
LAST_MINUTE_OF_YEAR => @file,
|
43
|
+
OTHER_RELATIVE_TIME => @other_dir,
|
44
|
+
SYMBOLIC_LINK_TIME => @symlink,
|
45
|
+
ABSOLUTE_DATE => @older_date
|
46
|
+
}.each do |date_and_time, parsed_entry|
|
47
|
+
assert_equal parse_adjust_date_and_time(date_and_time), parsed_entry.send(:mtime)
|
48
|
+
end
|
31
49
|
end
|
32
50
|
|
33
51
|
def test_ruby_unix_like_dir
|
@@ -67,4 +85,32 @@ class TestNetFTPListUnix < Test::Unit::TestCase
|
|
67
85
|
assert_equal 4, @symlink.filesize
|
68
86
|
assert_equal 154112, @older_date.filesize
|
69
87
|
end
|
88
|
+
|
89
|
+
def test_unix_block_device
|
90
|
+
assert_equal 'ram0', @block_dev.basename
|
91
|
+
assert @block_dev.device?
|
92
|
+
end
|
93
|
+
|
94
|
+
def test_unix_char_device
|
95
|
+
assert_equal 'null', @char_dev.basename
|
96
|
+
assert @char_dev.device?
|
97
|
+
end
|
98
|
+
|
99
|
+
def test_unix_socket_device
|
100
|
+
assert_equal 'log', @socket_dev.basename
|
101
|
+
assert @socket_dev.device?
|
102
|
+
end
|
103
|
+
|
104
|
+
def test_unix_pipe_device
|
105
|
+
assert_equal 'xconsole', @pipe_dev.basename
|
106
|
+
assert @pipe_dev.device?
|
107
|
+
end
|
108
|
+
|
109
|
+
|
110
|
+
private
|
111
|
+
def parse_adjust_date_and_time(date_and_time)
|
112
|
+
parsed_time = Time.parse(date_and_time)
|
113
|
+
parsed_time -= ONE_YEAR if parsed_time > Time.now
|
114
|
+
parsed_time
|
115
|
+
end
|
70
116
|
end
|
metadata
CHANGED
@@ -1,7 +1,12 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: net-ftp-list
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 3
|
7
|
+
- 2
|
8
|
+
- 0
|
9
|
+
version: 3.2.0
|
5
10
|
platform: ruby
|
6
11
|
authors:
|
7
12
|
- Stateless Systems
|
@@ -9,7 +14,7 @@ autorequire:
|
|
9
14
|
bindir: bin
|
10
15
|
cert_chain: []
|
11
16
|
|
12
|
-
date:
|
17
|
+
date: 2011-01-09 00:00:00 +11:00
|
13
18
|
default_executable:
|
14
19
|
dependencies: []
|
15
20
|
|
@@ -22,48 +27,60 @@ extensions: []
|
|
22
27
|
extra_rdoc_files:
|
23
28
|
- README.txt
|
24
29
|
files:
|
25
|
-
- .gitignore
|
26
30
|
- README.txt
|
27
31
|
- Rakefile
|
28
32
|
- VERSION.yml
|
29
33
|
- lib/net/ftp/list.rb
|
34
|
+
- lib/net/ftp/list/entry.rb
|
30
35
|
- lib/net/ftp/list/microsoft.rb
|
31
36
|
- lib/net/ftp/list/netware.rb
|
32
37
|
- lib/net/ftp/list/parser.rb
|
38
|
+
- lib/net/ftp/list/rumpus.rb
|
33
39
|
- lib/net/ftp/list/unix.rb
|
40
|
+
- lib/net/ftp/list/unknown.rb
|
34
41
|
- net-ftp-list.gemspec
|
42
|
+
- test/test_net_ftp_list.rb
|
43
|
+
- test/test_net_ftp_list_entry.rb
|
35
44
|
- test/test_net_ftp_list_microsoft.rb
|
36
45
|
- test/test_net_ftp_list_netware.rb
|
46
|
+
- test/test_net_ftp_list_rumpus.rb
|
37
47
|
- test/test_net_ftp_list_unix.rb
|
38
48
|
has_rdoc: true
|
39
49
|
homepage: http://github.com/stateless-systems/net-ftp-list
|
40
50
|
licenses: []
|
41
51
|
|
42
52
|
post_install_message:
|
43
|
-
rdoc_options:
|
44
|
-
|
53
|
+
rdoc_options: []
|
54
|
+
|
45
55
|
require_paths:
|
46
56
|
- lib
|
47
57
|
required_ruby_version: !ruby/object:Gem::Requirement
|
58
|
+
none: false
|
48
59
|
requirements:
|
49
60
|
- - ">="
|
50
61
|
- !ruby/object:Gem::Version
|
62
|
+
segments:
|
63
|
+
- 0
|
51
64
|
version: "0"
|
52
|
-
version:
|
53
65
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
66
|
+
none: false
|
54
67
|
requirements:
|
55
68
|
- - ">="
|
56
69
|
- !ruby/object:Gem::Version
|
70
|
+
segments:
|
71
|
+
- 0
|
57
72
|
version: "0"
|
58
|
-
version:
|
59
73
|
requirements: []
|
60
74
|
|
61
75
|
rubyforge_project:
|
62
|
-
rubygems_version: 1.3.
|
76
|
+
rubygems_version: 1.3.7
|
63
77
|
signing_key:
|
64
78
|
specification_version: 3
|
65
79
|
summary: Parse FTP LIST command output.
|
66
80
|
test_files:
|
81
|
+
- test/test_net_ftp_list.rb
|
82
|
+
- test/test_net_ftp_list_entry.rb
|
67
83
|
- test/test_net_ftp_list_microsoft.rb
|
68
|
-
- test/test_net_ftp_list_unix.rb
|
69
84
|
- test/test_net_ftp_list_netware.rb
|
85
|
+
- test/test_net_ftp_list_rumpus.rb
|
86
|
+
- test/test_net_ftp_list_unix.rb
|