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.
@@ -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.
@@ -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
@@ -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
+