net-ftp-list 0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/README.txt +77 -0
- data/Rakefile +51 -0
- data/lib/net/ftp/list.rb +67 -0
- data/lib/net/ftp/list/parser.rb +103 -0
- data/lib/net/ftp/list/unix.rb +56 -0
- data/test/test_net_ftp_list.rb +32 -0
- metadata +51 -0
data/README.txt
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
= Net::Ftp::List
|
2
|
+
|
3
|
+
== DESCRIPTION
|
4
|
+
|
5
|
+
Ruby lib to parse FTP LIST responses.
|
6
|
+
|
7
|
+
According to the FTP RFC the LIST command "information on a file may vary widely from system to system, this
|
8
|
+
information may be hard to use automatically in a program, but may be quite useful to a human user".
|
9
|
+
Unfortunately the NLST command "intended to return information that can be used by a program to further process
|
10
|
+
the files automatically" only returns the filename and no other information. If you want to know to know even
|
11
|
+
simple things like 'is the NLST entry a directory or file' you are left with the choice of attempting to CWD to
|
12
|
+
(and back from) each entry or parsing the LIST command. This extension is an attempt at parsing the LIST command
|
13
|
+
and as such is subject to all the variability that results from such an undertaking, take responses with a grain
|
14
|
+
of salt and expect failures.
|
15
|
+
|
16
|
+
See the RFC for more guff on LIST and NLST: http://www.ietf.org/rfc/rfc0959.txt
|
17
|
+
|
18
|
+
== TODO & PROBLEMS
|
19
|
+
|
20
|
+
* I'm new to Ruby, I'm sure some exist :)
|
21
|
+
* The factory and abstract base class for parsers are one and the same. OO geeks will cry.
|
22
|
+
* More OS's and server types. Only servers that return Unix like LIST responses will work at the moment.
|
23
|
+
* Calling <tt>if entry.file? or entry.dir?</tt> is hard work when you really mean <tt>unless entry.unknown?</tt>
|
24
|
+
* I'm not sure about overwriting Net::FTP's +list+, +ls+ and +dir+. It's a base lib after all and people will be
|
25
|
+
expecting String. Perhaps I'd be better to <tt>class Parser < String</tt> for the abstract parser.
|
26
|
+
* The block handling for +list+, +ls+ and +dir+ has a nasty +map+ that's essentially building up an unused Array.
|
27
|
+
|
28
|
+
== SYNOPSIS
|
29
|
+
|
30
|
+
By requiring Net::FTP::List instances are created by calling any of Net::FTP's <tt>list</tt>, <tt>ls</tt> or
|
31
|
+
<tt>dir</tt> metods. The <tt>to_s</tt> method still returns the raw line so for the most part this should be
|
32
|
+
transparent.
|
33
|
+
|
34
|
+
require 'net/ftp' # Not really required but I like to list dependencies sometimes.
|
35
|
+
require 'net/ftp/list'
|
36
|
+
|
37
|
+
ftp = Net::FTP.open('somehost.com', 'user', 'pass')
|
38
|
+
ftp.list('/some/path') do |entry|
|
39
|
+
# Ignore everything that's not a file (so symlinks, directories and devices etc.)
|
40
|
+
next unless entry.file?
|
41
|
+
|
42
|
+
# 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
|
43
|
+
# same name as ftp.nlist('/some/path') would have returned.
|
44
|
+
puts entry.basename
|
45
|
+
end
|
46
|
+
|
47
|
+
== CREDITS
|
48
|
+
|
49
|
+
* ASF, Commons. http://commons.apache.org/
|
50
|
+
* The good people at Stateless Systems who let me write Ruby and give it away.
|
51
|
+
|
52
|
+
== LICENSE
|
53
|
+
|
54
|
+
(The MIT License, go nuts)
|
55
|
+
|
56
|
+
Copyright (c) 2007 Shane Hanna
|
57
|
+
Stateless Systems
|
58
|
+
http://statelesssystems.com
|
59
|
+
|
60
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
61
|
+
a copy of this software and associated documentation files (the
|
62
|
+
'Software'), to deal in the Software without restriction, including
|
63
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
64
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
65
|
+
permit persons to whom the Software is furnished to do so, subject to
|
66
|
+
the following conditions:
|
67
|
+
|
68
|
+
The above copyright notice and this permission notice shall be
|
69
|
+
included in all copies or substantial portions of the Software.
|
70
|
+
|
71
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
72
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
73
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
74
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
75
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
76
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
77
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
require 'rake'
|
3
|
+
require 'rake/clean'
|
4
|
+
require 'rake/gempackagetask'
|
5
|
+
require 'rake/testtask'
|
6
|
+
|
7
|
+
NAME = 'net-ftp-list'
|
8
|
+
VERS = '0.3'
|
9
|
+
|
10
|
+
CLEAN.include ['**/*.log', '*.gem']
|
11
|
+
CLOBBER.include ['**/*.log']
|
12
|
+
|
13
|
+
spec = Gem::Specification.new do |s|
|
14
|
+
s.name = NAME
|
15
|
+
s.version = VERS
|
16
|
+
s.platform = Gem::Platform::RUBY
|
17
|
+
s.has_rdoc = true
|
18
|
+
s.extra_rdoc_files = ["README.txt"]
|
19
|
+
s.summary = 'Parse FTP LIST command output.'
|
20
|
+
s.description = s.summary
|
21
|
+
s.author = 'Shane Hanna'
|
22
|
+
s.email = 'shane@statelesssystems.com'
|
23
|
+
s.homepage = 'http://statelesssystems.com'
|
24
|
+
|
25
|
+
s.files = FileList['Rakefile', '**/*.{rb,txt}'].to_a
|
26
|
+
s.test_files = FileList['tests/*.rb'].to_a
|
27
|
+
end
|
28
|
+
|
29
|
+
desc 'Default: Run unit tests.'
|
30
|
+
task :default => :test
|
31
|
+
|
32
|
+
Rake::GemPackageTask.new(spec) do |p|
|
33
|
+
p.need_tar = true
|
34
|
+
p.gem_spec = spec
|
35
|
+
end
|
36
|
+
|
37
|
+
desc 'Run the unit tests.'
|
38
|
+
Rake::TestTask.new do |t|
|
39
|
+
t.verbose = true
|
40
|
+
end
|
41
|
+
|
42
|
+
desc 'Package and install as gem.'
|
43
|
+
task :install do
|
44
|
+
sh %{rake package}
|
45
|
+
sh %{sudo gem install pkg/net-ftp-list-#{VERS}}
|
46
|
+
end
|
47
|
+
|
48
|
+
desc 'Uninstall the gem.'
|
49
|
+
task :uninstall => [:clean] do
|
50
|
+
sh %{sudo gem uninstall #{NAME}}
|
51
|
+
end
|
data/lib/net/ftp/list.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'net/ftp'
|
2
|
+
require 'net/ftp/list/parser'
|
3
|
+
|
4
|
+
# The order here is important for the time being. Corse grained parsers should appear before specializations because
|
5
|
+
# the whole thing is searched in reverse order.
|
6
|
+
require 'net/ftp/list/unix'
|
7
|
+
|
8
|
+
module Net #:nodoc:
|
9
|
+
class FTP #:nodoc:
|
10
|
+
|
11
|
+
alias_method :raw_list, :list
|
12
|
+
def list(*args, &block)
|
13
|
+
# TODO: Map in void context when you pass a block.
|
14
|
+
raw_list(*args).map do |raw|
|
15
|
+
entry = Net::FTP::List.parse(raw)
|
16
|
+
block ? yield(entry) : entry
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Parse FTP LIST responses.
|
21
|
+
#
|
22
|
+
# Simply require the library and each entry from Net::FTP +list+, +ls+ or +dir+ methods will
|
23
|
+
# be parsed by the LIST parse as best it can.
|
24
|
+
#
|
25
|
+
# == Creation
|
26
|
+
#
|
27
|
+
# By requiring Net::FTP::List instances are created by calling any of Net::FTP's +list+, +ls+ or
|
28
|
+
# +dir+ metods. The +to_s+ method still returns the raw line so for the most part this should be
|
29
|
+
# transparent.
|
30
|
+
#
|
31
|
+
# require 'net/ftp' # Not really required but I like to list dependencies sometimes.
|
32
|
+
# require 'net/ftp/list'
|
33
|
+
#
|
34
|
+
# ftp = Net::FTP.open('somehost.com', 'user', 'pass')
|
35
|
+
# ftp.list('/some/path') do |entry|
|
36
|
+
# # Ignore everything that's not a file (so symlinks, directories and devices etc.)
|
37
|
+
# next unless entry.file?
|
38
|
+
#
|
39
|
+
# # 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
|
40
|
+
# # same name as ftp.nlist('/some/path') would have returned.
|
41
|
+
# puts entry.basename
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# == Exceptions
|
45
|
+
#
|
46
|
+
# None at this time. At worst you'll end up with an Net::FTP::List::Unknown instance which won't have any extra
|
47
|
+
# useful information. Methods like <tt>dir?</tt>, <tt>file?</tt> and <tt>symlink?</tt> will all return +false+.
|
48
|
+
module List
|
49
|
+
class << self
|
50
|
+
|
51
|
+
# Parse a raw FTP LIST line.
|
52
|
+
#
|
53
|
+
# Net::FTP::List.parse(raw_list_string) # => Net::FTP::List::Parser instance.
|
54
|
+
def parse(raw)
|
55
|
+
Parser.parse(raw)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Parse a raw FTP LIST line.
|
59
|
+
#
|
60
|
+
# An alias for +parse+.
|
61
|
+
alias_method :new, :parse
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module Net
|
2
|
+
class FTP
|
3
|
+
module List
|
4
|
+
|
5
|
+
# ParserError
|
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
|
13
|
+
|
14
|
+
# Abstract FTP LIST parser.
|
15
|
+
#
|
16
|
+
# It really just defines and documents the interface.
|
17
|
+
#
|
18
|
+
# == Exceptions
|
19
|
+
#
|
20
|
+
# +ParserError+ -- Raw entry could not be parsed.
|
21
|
+
class Parser
|
22
|
+
@@parsers = []
|
23
|
+
|
24
|
+
# The raw list entry string.
|
25
|
+
attr_reader :raw
|
26
|
+
|
27
|
+
# The items basename (filename).
|
28
|
+
attr_reader :basename
|
29
|
+
|
30
|
+
# Looks like a directory, try CWD.
|
31
|
+
def dir?
|
32
|
+
!!self.dir
|
33
|
+
end
|
34
|
+
|
35
|
+
# Looks like a file, try RETR.
|
36
|
+
def file?
|
37
|
+
self.file
|
38
|
+
end
|
39
|
+
|
40
|
+
# Looks like a symbolic link.
|
41
|
+
def symlink?
|
42
|
+
!!self.symlink
|
43
|
+
end
|
44
|
+
|
45
|
+
# Parse a raw FTP LIST line.
|
46
|
+
#
|
47
|
+
# By default just takes and set the raw list entry.
|
48
|
+
#
|
49
|
+
# Net::FTP::List.parse(raw_list_string) # => Net::FTP::List::Parser instance.
|
50
|
+
def initialize(raw)
|
51
|
+
self.raw = raw
|
52
|
+
end
|
53
|
+
|
54
|
+
# Stringify.
|
55
|
+
def to_s
|
56
|
+
raw
|
57
|
+
end
|
58
|
+
alias_method :raw, :to_s
|
59
|
+
|
60
|
+
class << self
|
61
|
+
|
62
|
+
# Acts as a factory.
|
63
|
+
#
|
64
|
+
# TODO: Having a class be both factory and abstract implementation seems a little nutty to me. If it ends up
|
65
|
+
# too confusing or gives anyone the shits I'll move it.
|
66
|
+
def inherited(klass)
|
67
|
+
@@parsers << klass
|
68
|
+
end
|
69
|
+
|
70
|
+
# Factory method.
|
71
|
+
#
|
72
|
+
# Attempt to find and parse a list item with the a parser.
|
73
|
+
def parse(raw)
|
74
|
+
@@parsers.reverse.each do |parser|
|
75
|
+
begin
|
76
|
+
return parser.new(raw)
|
77
|
+
rescue ParserError
|
78
|
+
next
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
protected
|
85
|
+
# Protected boolean.
|
86
|
+
attr_accessor :file, :dir, :symlink
|
87
|
+
|
88
|
+
# Protected raw list entry string.
|
89
|
+
attr_writer :raw
|
90
|
+
|
91
|
+
# Protected item basename (filename).
|
92
|
+
attr_writer :basename
|
93
|
+
end
|
94
|
+
|
95
|
+
# Unknown parser.
|
96
|
+
#
|
97
|
+
# If all other attempts to parse the entry fail this class will be returned. Only the +raw+ and +to_s+
|
98
|
+
# methods will return anything useful.
|
99
|
+
class Unknown < Parser
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'net/ftp/list/parser'
|
2
|
+
|
3
|
+
module Net
|
4
|
+
class FTP
|
5
|
+
module List
|
6
|
+
|
7
|
+
# Parse Unix like FTP LIST entries.
|
8
|
+
#
|
9
|
+
# == MATCHES
|
10
|
+
#
|
11
|
+
# drwxr-xr-x 4 steve group 4096 Dec 10 20:23 etc
|
12
|
+
# -rw-r--r-- 1 root other 531 Jan 29 03:26 README.txt
|
13
|
+
#
|
14
|
+
# == SYNOPSIS
|
15
|
+
#
|
16
|
+
# entry = Net::FTP::List::Unix.new('drwxr-xr-x 4 steve group 4096 Dec 10 20:23 etc')
|
17
|
+
# entry.dir? # => true
|
18
|
+
# entry.basename # => 'etc'
|
19
|
+
class Unix < Parser
|
20
|
+
|
21
|
+
# Stolen straight from the ASF's commons Java FTP LIST parser library.
|
22
|
+
# http://svn.apache.org/repos/asf/commons/proper/net/trunk/src/java/org/apache/commons/net/ftp/
|
23
|
+
REGEXP = %r{
|
24
|
+
([bcdlfmpSs-])
|
25
|
+
(((r|-)(w|-)([xsStTL-]))((r|-)(w|-)([xsStTL-]))((r|-)(w|-)([xsStTL-])))\+?\s+
|
26
|
+
(\d+)\s+
|
27
|
+
(\S+)\s+
|
28
|
+
(?:(\S+(?:\s\S+)*)\s+)?
|
29
|
+
(\d+)\s+
|
30
|
+
((?:\d+[-/]\d+[-/]\d+)|(?:\S+\s+\S+))\s+
|
31
|
+
(\d+(?::\d+)?)\s+
|
32
|
+
(\S*)(\s*.*)
|
33
|
+
}x
|
34
|
+
|
35
|
+
# Parse a Unix like FTP LIST entries.
|
36
|
+
def initialize(raw)
|
37
|
+
super(raw)
|
38
|
+
match = REGEXP.match(raw.strip) or raise ParserError
|
39
|
+
|
40
|
+
case match[1]
|
41
|
+
when /d/ then self.dir = true
|
42
|
+
when /l/ then self.symlink = true
|
43
|
+
when /[f-]/ then self.file = true
|
44
|
+
when /[bc]/ then # Do nothing with devices for now.
|
45
|
+
else ParserError 'Unknown LIST entry type.'
|
46
|
+
end
|
47
|
+
|
48
|
+
# TODO: Permissions, users, groups, date/time.
|
49
|
+
|
50
|
+
self.basename = match[21].match(/^(.+)(?:\s+\->.+)?$/)[0].strip
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'net/ftp/list'
|
3
|
+
|
4
|
+
class TestNetFTPList < Test::Unit::TestCase
|
5
|
+
|
6
|
+
def setup
|
7
|
+
@dir = Net::FTP::List.new('drwxr-xr-x 4 user group 4096 Dec 10 20:23 etc')
|
8
|
+
@file = Net::FTP::List.new('-rw-r--r-- 1 root other 531 Jan 29 03:26 README')
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_parse_new
|
12
|
+
assert_instance_of Net::FTP::List::Unix, @dir, 'LIST unixish directory'
|
13
|
+
assert_instance_of Net::FTP::List::Unix, @file, 'LIST unixish file'
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_rubbish_lines
|
17
|
+
assert_instance_of Net::FTP::List::Unknown, Net::FTP::List.new("++ bah! ++")
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_ruby_unix_like_dir
|
21
|
+
assert_equal 'etc', @dir.basename
|
22
|
+
assert @dir.dir?
|
23
|
+
assert !@dir.file?
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_ruby_unix_like_file
|
27
|
+
assert_equal 'README', @file.basename
|
28
|
+
assert @file.file?
|
29
|
+
assert !@file.dir?
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
metadata
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.9.4
|
3
|
+
specification_version: 1
|
4
|
+
name: net-ftp-list
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: "0.3"
|
7
|
+
date: 2008-01-13 00:00:00 +11:00
|
8
|
+
summary: Parse FTP LIST command output.
|
9
|
+
require_paths:
|
10
|
+
- lib
|
11
|
+
email: shane@statelesssystems.com
|
12
|
+
homepage: http://statelesssystems.com
|
13
|
+
rubyforge_project:
|
14
|
+
description: Parse FTP LIST command output.
|
15
|
+
autorequire:
|
16
|
+
default_executable:
|
17
|
+
bindir: bin
|
18
|
+
has_rdoc: true
|
19
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">"
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.0.0
|
24
|
+
version:
|
25
|
+
platform: ruby
|
26
|
+
signing_key:
|
27
|
+
cert_chain:
|
28
|
+
post_install_message:
|
29
|
+
authors:
|
30
|
+
- Shane Hanna
|
31
|
+
files:
|
32
|
+
- Rakefile
|
33
|
+
- lib/net/ftp/list/parser.rb
|
34
|
+
- lib/net/ftp/list/unix.rb
|
35
|
+
- lib/net/ftp/list.rb
|
36
|
+
- test/test_net_ftp_list.rb
|
37
|
+
- README.txt
|
38
|
+
test_files: []
|
39
|
+
|
40
|
+
rdoc_options: []
|
41
|
+
|
42
|
+
extra_rdoc_files:
|
43
|
+
- README.txt
|
44
|
+
executables: []
|
45
|
+
|
46
|
+
extensions: []
|
47
|
+
|
48
|
+
requirements: []
|
49
|
+
|
50
|
+
dependencies: []
|
51
|
+
|