net-ftp-list 0.3
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 +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
|
+
|