;#
;# Copyright (c) 1995-1997
;# Ikuo Nakagawa. All rights reserved.
;#
;# Redistribution and use in source and binary forms, with or without
;# modification, are permitted provided that the following conditions
;# are met:
;#
;# 1. Redistributions of source code must retain the above copyright
;# notice unmodified, this list of conditions, and the following
;# disclaimer.
;# 2. Redistributions in binary form must reproduce the above copyright
;# notice, this list of conditions and the following disclaimer in the
;# documentation and/or other materials provided with the distribution.
;#
;# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
;# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
;# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
;# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS
;# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
;# OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
;# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
;# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
;# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
;# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
;# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
;#
;# $Id: Scan.pm,v 1.21 1998/10/10 11:31:10 ikuo Exp $
;#
;# For example, to list up all directories and files in
;# servers tree;
;#
;# use Fan::FTP;
;# use Fan::Scan;
;#
;# $ftp = Fan::FTP->new(ftp_server = 'ftp.freebsd.org');
;# $scan = Fan::Scan->new(
;# scan_type => 'FTP',
;# scan_ftp => $ftp,
;# scan_dir => '/pub/FreeBSD' );
;# while (defined($p = $scan->get)) {
;# last if $p->type eq '.';
;# print $p->path."\n" if $p->type ne 'U';
;# }
;#
package Fan::Scan;
use strict;
use vars qw($VERSION $LOG
$n_object $path_gzip $path_compress
%required %typemap %typerev);
use Carp;
use Fan::Attrib;
use AutoLoader 'AUTOLOAD';
$VERSION = '0.04';
$LOG = 5 unless defined($LOG);
;#
$path_gzip = &findpath("gzip");
$path_compress = &findpath("compress");
;# A special marker for AutoSplit.
1;
__END__
;#
sub findpath ($) {
my $foo = shift;
my @path = split(':', $ENV{'PATH'});
# append some system specific directories...
push(@path, qw(
/usr/ucb
/usr/ccs
/usr/local/bin
/usr/gnu/bin
/usr/contrib/bin
/opt/local/bin
/opt/gnu/bin
/opt/contrib/bin));
# check existence of the executable.
for my $d (@path) {
return "$d/$foo" if -x "$d/$foo" && ! -d _;
}
# return itself if executables were not found.
$foo;
}
;# Destroy Scan object...
;#
sub DESTROY ($) {
my $self = shift;
# Close any files first (if we have).
$self->close;
# Log message.
carp("Scan DESTROYING $self") if $LOG > 5;
}
;#
;# search a directory information for the specified directory.
;# usage:
;# scan_type :
;# LOCAL ... scan_dir
;# INDEX ... scan_index
;# LSLR ... scan_lslR
;# FTP ... scan_ftp, scan_dir
;# HTTP ... scan_ftp, scan_dir
;# $scan = Fan::Scan->new(
;# scan_type => LSLR,
;# scan_lslR => "ls-lR.gz" );
;#
sub new ($%) {
my $this = shift;
my $class = ref($this) || $this;
my %params = @_;
my $self = {};
bless $self, $class;
# copy any scan parameter.
for my $tag (keys %params) {
$self->{$tag} = $params{$tag} if $tag =~ /^scan_/;
}
# Find out scan type.
my $t = $self->{scan_type};
# Check types...
if (!defined($t)) { # not found.
carp("No scan_type defined"), return undef;
} elsif ($t eq 'LOCAL') { # scan local directory
$self->{scan_dir} ne '' && -d $self->{scan_dir}
or carp("No local directory"), return undef;
} elsif ($t eq 'FTP') {
ref($self->{scan_ftp}) # must be some module
or carp("No scan_ftp"), return undef;
$self->{scan_dir} ne ''
or carp("No scan_dir"), return undef;
} elsif ($t eq 'INDEX') {
$self->open($self->{scan_index})
or carp("open(scan_index): $!"), return undef;
} elsif ($t eq 'LSLR') {
$self->open($self->{scan_lslR})
or carp("open(scan_lslR): $!"), return undef;
} else {
carp("Scan does not know method"), return undef;
}
# cleanup
delete($self->{endoffile});
delete($self->{founderror});
delete($self->{subst});
delete($self->{hold});
$self->{magic_array} = {};
$self->{cur_info} = {};
$self->{dirs} = [];
$self->{lslR_curdir} = '.';
$self->{scan_filter} = [];
# Log message.
carp("Scan CREATING $self") if $LOG > 5;
# Return myself
$self;
}
;#
;# To get the current working directory, use $p->where.
;# It tells where you are.
;#
sub where ($) {
my $self = shift;
exists($self->{walk_curdir}) ? $self->{walk_curdir} : undef;
}
;#
;# $p->start initialize all variables used by `where', `up',
;# and `down'. This routine must be called before you call
;# these routines.
;#
sub start ($) {
my $self = shift;
# DEBUG purpose only.
confess("$self->{walk_curdir} exists in stack")
if exists($self->{walk_curdir});
# initial current walk directory is '.'.
$self->{walk_curdir} = '.';
# DEBUG mode only
warn("Scan +DIR=. (start)\n") if $LOG > 7;
# result is current directory
$self->{walk_curdir};
}
;#
;# $p->end is useful for the aftercare and validation of
;# directory stack operations.
;# This routine should be called at last of your directory
;# stack operations.
;#
sub end ($) {
my $self = shift;
# DEBUG purpose only.
confess("$self->{walk_curdir}: remains in stack")
unless ($self->{walk_curdir} eq '.');
# clean work directory entry
delete($self->{walk_curdir});
# DEBUG mode only
warn("Scan -DIR=. (end)\n") if $LOG > 7;
# result is current directory
undef;
}
;#
;# You can go to DOWN stair with $p->down(you_want_to_go).
;# you_want_to_go must no contains a slash(== `/').
;# Additionally, it must not be a null string, `.' or `..'.
;#
sub down ($$) {
my $self = shift;
my $dir = shift; # you_want_to_go
# DEBUG purpose only.
confess("$dir: illegal directory name")
if $dir eq '' || $dir eq '.' || $dir eq '..' || $dir =~ /\//;
# go down one directory
$self->{walk_curdir} .= '/'.$dir;
# DEBUG mode only
warn("Scan +DIR=$self->{walk_curdir}\n") if $LOG > 7;
# result is current directory
$self->{walk_curdir};
}
;#
;# You can go to UP stair with $p->up.
;#
sub up ($) {
my $self = shift;
# DEBUG purpose.
confess("$self->{walk_curdir}: no upper directory")
unless $self->{walk_curdir} =~ s|/[^/]+$||;
# DEBUG mode only.
warn("Scan -DIR=$self->{walk_curdir}\n") if $LOG > 7;
# result is current directory
$self->{walk_curdir};
}
;#
;# Scan::open
;# Open a file, which may be .gz or .Z compressed file.
;#
sub open ($$) {
my $self = shift;
my $file = shift;
# close first
$self->close;
# check filter
if (ref($file) ne 'GLOB') {
my $a = $file =~ /\.gz$/ ? "$path_gzip -cd $file|" :
$file =~ /\.Z$/ ? "$path_compress -cd $file|" :
-f $file ? $file :
-f "$file.gz" ? "$path_gzip -cd $file.gz|" :
-f "$file.Z" ? "$path_compress -cd $file.Z|" :
$file;
# local file handle.
local *FILE;
# Now, try to open.
unless (CORE::open(FILE, $a)) {
carp("CORE::open($a): $!");
return undef;
}
# warn("open[".fileno(*FILE)."] $a: o.k.\n");
# mark as OPENED file.
$self->{needclose}++;
# now store to $file.
$file = *FILE;
}
# Register the file handle
$self->{handle} = $file;
# Increment # of objects
$n_object++;
# Success
1;
}
;# close file handle if needed.
;#
sub close ($) {
my $self = shift;
if ($self->{needclose} && exists($self->{handle})) {
CORE::close($self->{handle});
}
$self->{needclose} = 0;
delete($self->{handle});
1;
}
;#
;# Add a filter to Scan object.
;#
sub add_filter ($@) {
my $self = shift;
my @args = @_;
my $c = $args[$[];
if (ref($c) ne 'CODE') {
carp("$self->add_filter: $c must be a CODE");
return undef;
}
push(@{$self->{scan_filter}}, \@args);
}
;#
;# clear filter
;#
sub clr_filter ($) {
my $self = shift;
$self->{scan_filter} = [];
}
;#
;#
sub perform_filter ($$) {
my $self = shift;
my $y = shift;
# for DEBUG mode only.
confess("$y must be Attrib")
unless ref($y) && $y->isa('Fan::Attrib');
# loop over all scan_filter or finding mismatch.
for my $filter (@{$self->{scan_filter}}) {
my($c, @args) = @{$filter};
# for DEBUG mode only.
confess("$c must be CODE") unless ref($c) eq 'CODE';
# try to perform filter...
&{$c}($y, @args) || return 0;
}
# all filter functions return ok
1;
}
;# Unget means, save current value to hold buffer.
;# Next get operation may returns this.
sub unget ($$) {
my $self = shift;
$self->{hold} = shift;
}
;#
;#
sub get ($) {
my $self = shift;
my $y;
# If there is an entry in hold buffer, we return it.
delete($self->{hold}), return $y if defined($y = $self->{hold});
# Get one!
# When a directory object was marked as 'IGNORED' by filter,
# we should ignore any additional files in that directory.
my $ignore_nest = 0;
while (defined($y = $self->getone)) {
warn($y->to_line."\n") if $LOG >= 8; # in full debug mode
# Check if we are in an ignored directory.
if ($ignore_nest > 0) {
$ignore_nest-- if $y->type eq 'U';
$y->flag('!'); # and skip this entry.
}
# Shall we return this attrib object?
if ($y->flag eq '!') {
$self->{ignore_nest}++ if $y->type eq 'D';
} else {
last; #
}
}
# Check error.
if (!defined($y)) {
if ($self->{founderror}) {
# This may be dangerous, so we should terminate
# any processing.
croak("Scan::get: error was detected");
}
}
# The result
$y;
}
;#
;# Get a single entry from index tree.
;#
sub getone ($) {
my $self = shift;
my $y;
# Check end-of-file or error.
return undef if $self->{endofdata} || $self->{founderror};
# Check array reference. Error in next statement is critical.
if (ref($self->{dirs}) ne 'ARRAY') {
confess("\$self->{dirs} must be ARRAY");
}
# Try to parse and store to $y.
$y = $self->{scan_index} ? $self->get_index : $self->get_misc;
# Check result.
unless (ref($y) && $y->isa('Fan::Attrib')) {
carp("getone: can't get Fan::Attrib object");
$self->{founderror}++;
return undef;
}
# We prefer an abbrev for the type of this Attrib object.
my $t = $y->type;
# If last one is a directory, go down there.
if (exists($self->{nextdir})) {
push(@{$self->{dirs}}, $self->{nextdir});
delete($self->{nextdir}); # clean next directory entry
}
# We assume the first entry is the `.' directory.
if (@{$self->{dirs}} == 0) { # this is the first time
if (!($t eq 'D' && $y->name eq '.')) {
carp("First entry must be the \".\" directory.");
$self->{founderror}++;
return undef;
}
$self->{nextdir} = '.'; # save to hold buffer
return $y;
}
# Type '.' means END-OF-DATA.
if ($t eq '.') {
if (@{$self->{dirs}} == 1) { # verify o.k.
pop(@{$self->{dirs}});
$self->{endofdata}++;
return $y;
}
if (@{$self->{dirs}} > 1) {
carp("remaining directories: \""
.join('/', @{$self->{dirs}})."\"");
} else {
carp("No directory in stack");
}
$self->{founderror}++;
return undef;
}
# Type 'U' is `Go to the up stair'.
if ($t eq 'U') { # one dir up
if (@{$self->{dirs}} < 2) {
carp("Too many dir UP");
$self->{founderror}++;
return undef;
}
$y->name(pop(@{$self->{dirs}})); # not return yet
}
# Remaining cases require `name' attribute. Check it.
my $n = $y->name;
if (!defined($n)) {
confess("$y: no name found.");
} elsif ($n =~ m%[\001\377]%) {
confess("$y: name contains illegal charactor.");
} elsif ($n =~ m%/([^/]+)$%) { # contains slash(es)
confess("$y: name contains slash(es).");
} elsif ($n eq '.' || $n eq '..') {
confess("$y: \"$n\" is invalid.");
} else {
; # o.k.
}
# DEBUG mode only.
# Calculate pathname from directory array.
my $path = join('/', @{$self->{dirs}}, $n);
confess($y->path." != $path") unless $y->path eq $path;
# Type 'U' was already processed.
if ($t eq 'U') {
return $y;
}
# Type 'D' is a directory. Copy to the nextdir hold buffer.
if ($t eq 'D') {
$self->{nextdir} = $n;
return $y;
}
# Type 'L' is a symlink.
if ($t eq 'L') { # symlink
if (!defined($y->linkto)) { # linkto is required
carp("$y: no linkto for symlink");
$self->{founderror}++;
return undef;
}
return $y;
}
# Type 'F' is a normal file.
if ($t eq 'F') { # normal file
return $y;
}
# Or, unknown type is specified.
carp("$t: illegal type");
return undef;
}
;#
sub get_index ($) {
my $self = shift;
my $fh = $self->{handle};
# Check end-of-file or error.
return undef if $self->{endofdata} || $self->{founderror};
# DEBUG mode only.
confess("\$fh must be defined") unless defined($fh);
# Try to read from the file handle.
local $_;
while (defined($_ = <$fh>)) {
# skip trailing spaces, and ignore comment lines.
s/\s+$//;
/^$/ || /^#/ || last;
warn("READ($.): $_\n");
}
# Check end-of-file.
if (!defined($_)) {
carp("file [".fileno($fh)."] unexpected END-OF-FILE at $.");
$self->{founderror}++;
return undef;
}
# Let's parse it.
my $y = Fan::Attrib->new(attr_line => $_);
# Check result.
ref($y) && $y->isa('Fan::Attrib')
or carp("Can't create Attrib from $_"), return undef;
# where am I?
my $d;
if (!defined($d = $self->where)) {
$y->path('.');
$self->start;
} else {
if ($y->type eq '.') {
$self->end;
} elsif ($y->type eq 'U') {
$y->path($d);
$self->up;
} else {
$y->path($d.'/'.$y->name);
if ($y->type eq 'D') {
$self->down($y->name);
}
}
}
# try filter
$y->flag('!') unless $self->perform_filter($y);
# result is Attrib or undef.
$y;
}
;#
;# Scan with directory structure.
;# We should get directory entries per directory, sort them,
;# and hold until they will be used. A special reference of
;# $self->{magic_array} is used to hold directory entires.
;#
sub get_misc ($) {
my $self = shift;
my $y;
# Try to get the current working directory...
my $dir;
if (!defined($dir = $self->where)) {
$self->start;
$y = $self->{scan_type} eq 'LOCAL'
? Fan::Attrib->new(attr_path => "$self->{scan_dir}/.")
: Fan::Attrib->new(y_type => 'D', y_name => '.');
if (defined($y)) {
$y->path('.');
$y->flag('!') unless $self->perform_filter($y);
} else {
carp("Attrib: Can't create object");
}
return $y;
}
# Delete skipped directories.
my $cur = $dir;
my $cur_n = ($cur =~ y|/|\001|);
my $d;
for $d (keys %{$self->{magic_array}}) {
my $tmp = $d;
my $tmp_n = ($tmp =~ y|/|\001|);
if ($tmp_n >= $cur_n && $tmp lt $cur) {
warn("Scan: ($d lt $dir) is true... skipped.\n")
if $LOG > 5;
delete($self->{magic_array}->{$d});
}
}
# Check magic_array first.
my $p = $self->{magic_array}->{$dir};
# Try to get magic array for this directory
if (!defined($p)) {
$p = $self->{scan_type} eq 'LOCAL' ? $self->dir_local($dir)
: $self->{scan_type} eq 'LSLR' ? $self->dir_lslR($dir)
: $self->{scan_type} eq 'FTP' ? $self->dir_ftp($dir)
: undef;
unless (defined($p)) {
carp("get_misc: can't get array");
return undef;
}
}
# Now, ($p == $self->{magic_array}->{$dir}) is true.
if (defined($y = shift(@{$p}))) { # something remains
$y->path($dir.'/'.$y->name);
if ($y->type eq 'D') {
$self->down($y->name);
}
unless ($self->perform_filter($y)) {
$y->flag('!');
if ($y->type eq 'D') { # setup dummy array
$self->{magic_array}->{$self->where} = [];
}
}
} else { # or terminate this directory
delete($self->{magic_array}->{$dir}); # clean
if ($dir eq '.') {
$self->end;
$y = Fan::Attrib->new(y_type => '.');
} else {
my $tail = $dir =~ m|([^/]+)$| ? $1 : '.';
$self->up;
$y = Fan::Attrib->new(
y_type => 'U',
y_name => $tail,
y_path => $dir );
}
if (defined($y)) {
$y->flag('!') unless $self->perform_filter($y);
} else {
confess("Attrib: Can't create object")
}
}
# return Attrib object.
$y;
}
;#
;# Usage:
;# array_ref = $scan->dir_local(directory);
;#
sub dir_local ($$) {
my $self = shift;
my $dir = shift;
my $d = "$self->{scan_dir}/$dir";
my $p = [];
local(*D);
opendir(D, $d) or carp("opendir($d): $!"), return undef;
my @entry = readdir(D);
closedir(D);
# for all entries
my $e;
for $e (sort @entry) {
next if $e eq '' || $e eq '.' || $e eq '..';
my $x = Fan::Attrib->new(
attr_path => "$d/$e",
attr_no_checksum => $self->{scan_no_checksum}
);
if (defined($x)) {
# $x->realpath("$d/$e");
push(@{$p}, $x);
}
}
# return a reference to array.
$self->{magic_array}->{$dir} = $p;
}
;#
;# Usage:
;# array_ref = $scan->dir_ftp(directory);
;#
sub dir_ftp ($$) {
my $self = shift;
my $dir = shift;
my $d = "$self->{scan_dir}/$dir";
my $ftp = $self->{scan_ftp};
my $hash = undef;
# try to get magic array for this directory
if ($self->{scan_dirinfo}) {
use Fan::DIR;
my $tmp = "/tmp/dirinfo.$$";
my $info = Fan::DIR->new();
if ($ftp->get("$d/.dirinfo", $tmp) && $info->load($tmp)) {
my $f;
for $f ($info->index) {
$hash->{$f} = $info->get($f);
}
} elsif ($ftp->fatal) {
carp("Scan: can't get dirinfo, fatal") if $LOG > 5;
return undef;
} else {
carp("Scan: can't get dirinfo, try nest") if $LOG > 5;
}
unlink($tmp);
}
# no scan_dirinfo, or fail to load dirinfo.
if (!defined($hash)) {
local $_ = $ftp->list($d);
unless (defined($_)) {
warn("Scan: can't get list of $d\n") if $LOG > 5;
return undef;
}
$hash = {};
my $x;
for $x (split(/\n/)) {
my $y;
if (!defined($y = Fan::Attrib->new(attr_list => $x))) {
warn("$x: could not parse, ignored.\n")
if $LOG > 6;
} else {
$hash->{$y->name} = $y;
}
}
}
my $x;
my $p = [];
for $x (sort keys %{$hash}) {
if ($x ne '' && $x ne '.' && $x ne '..') {
$hash->{$x}->realpath("$d/$x");
push(@{$p}, $hash->{$x});
}
}
$self->{magic_array}->{$dir} = $p;
}
;#
;# Usage:
;# array_ref = $scan->dir_lslR(directory);
;#
sub dir_lslR ($$) {
my $self = shift;
my $dir = shift;
my $p;
while (!defined($p = $self->{magic_array}->{$dir})) {
$self->getline_lslR || return undef;
}
$p;
}
;#
;# read a line from ls-lR format file, and parse it.
;# if full entries for a directory $d was found,
;# you can access directory info for that directory from
;# $self->{magic_array}->{$d}.
;#
sub getline_lslR ($){
my $self = shift;
my $fh = $self->{handle};
local $_;
# check previouse error or end-of-file
return undef if $self->{endoffile} || $self->{founderror};
# validate file handle
confess("Can't find file handle") unless defined($fh);
# y structure, separator
my $delim = 0;
# read a line
if (!(defined($_ = <$fh>))) {
$self->{endoffile}++, $delim++;
} elsif (/^$/) { # null line
$delim++; # end-of-directory
} elsif (/^total (\d+)/) { # maybe start of a directory.
# we simply ignore this.
} elsif (/^.[-r][-w][-xsS][-r][-w][-xsS][-r][-w][-xtT]\s*/) {
# normal entry - check this format before directory pattern
# tested in next statement, because we hate a filename which
# ends with ':'.
my $y;
if (!defined($y = Fan::Attrib->new(attr_list => $_))) {
warn("$_: could not parse, ignored\n") if $LOG >= 7;
} else {
my $n = $y->name;
if ($n ne '' && $n ne '.' && $n ne '..') {
$self->{cur_info}->{$n} = $y; # registered.
}
}
} elsif (/:$/) { # new directory
my $d = $`;
if (!defined($self->{subst})) {
my $s = quotemeta($d =~ m%[^/]+$% ? $` : '');
$self->{subst} = sub {
local($_) = @_; s|^$s|./|; $_; };
}
$self->{lslR_curdir} = &{$self->{subst}}($d);
} else { # other case?
warn("$_ unknown format, ignored\n") if $LOG >= 7;
$self->{founderror}++;
return undef;
}
# directory delimiter
if ($delim) {
my $d = $self->{lslR_curdir};
delete($self->{lslR_curdir});
my $p = $self->{cur_info};
$self->{cur_info} = {};
my $q = $self->{magic_array}->{$d} = [];
### DEBUG BEGIN (for debug only)
if ($d ne '.' && $d !~ /^\.\//) {
confess("directory name must begin with '.'\n");
}
### DEBUG END
my $n;
for $n (sort keys %{$p}) {
push(@{$q}, $p->{$n});
}
}
# success return
1;
}
;#
;#
sub dump ($) {
my $self = shift;
my $count = 0;
my $d;
for $d (sort keys %{$self->{magic_array}}) {
my $p = $self->{magic_array}->{$d};
my $y;
for $y (@{$p}) {
print "dump: ".$y->path."\n";
}
$count += @{$p};
}
print "dump: total $count entries.\n";
}
;#
;#
sub summary ($) {
my $self = shift;
my $count = 0;
my $d;
for $d (sort keys %{$self->{magic_array}}) {
$count += scalar(@{$self->{magic_array}->{$d}});
}
print "summary: total $count entries.\n";
}
;#
;# Get an array of ``smallest'' attributes from
;# a list of Scan objects.
;#
sub getcmp ($@) {
my @array = @_;
my $end = 0;
my $z = undef; # the smallest attribute
my $pp;
# Search smallest entry, first
for $pp (@array) {
my $p;
if (defined($p = $pp->get)) {
$pp->unget($p);
$z = $p if !defined($z) || $p->compare($z) < 0;
} else {
$end++;
}
}
# Check end-of-data
if ($end == @array) {
return wantarray ? () : undef;
} elsif ($end > 0) {
confess("Some unexpected case occured");
}
# OK, $z is the smallest one, try to generate result.
my @result = ();
# Get an item if smallest values can be found.
for $pp (@array) {
my $p = $pp->get;
if ($z->compare($p)) {
$pp->unget($p);
push(@result, undef);
} else {
push(@result, $p);
}
}
# Result is an list of attributes.
@result;
}
;# guessing..., return the Scan object.
;#
sub guess ($$) {
my $this = shift;
my $foo = shift;
# if we already have scanner, return it.
return $foo if ref($foo) && $foo->isa('Fan::Scan');
# directory?
if (-d $foo) {
return $this->new(scan_type => 'LOCAL', scan_dir => $foo);
}
# regular file? if so, it must be an index file.
if (-f $foo) {
return $this->new(scan_type => 'INDEX', scan_index => $foo);
}
# file glob?
if (ref($foo) eq 'GLOB') {
return $this->new(scan_type => 'INDEX', scan_index => $foo);
}
# file handle?
if (defined(fileno($foo))) {
return $this->new(scan_type => 'INDEX', scan_index => \*{$foo});
}
# what is this?
undef;
}
;# convert file name to a file glob.
;#
sub fileglob ($;$) {
my $file = shift;
my $m = @_ ? shift : ''; # mode...
# do nothing if $file is a file glob.
return $file if ref($file) eq 'GLOB';
# check modes.
$m = '<' if $m eq 'r';
$m = '>' if $m eq 'w';
$m = '>>' if $m eq 'a';
# check modes...
unless ($m eq '' || $m eq '<' || $m eq '>' || $m eq '>>') {
carp("fileglob: wrong mode to open operation") if $LOG >= 5;
return undef;
}
# check special filename.
if ($file eq '' || $file eq '-') {
if ($m eq '' || $m eq '<') {
return \*STDIN;
} else {
return \*STDOUT;
}
}
# try to open the target file.
local *TEMP;
unless (CORE::open(TEMP, $m.$file)) {
carp("fileglob: open($m.$file): $!") if $LOG >= 5;
return undef;
}
# debug log...
warn("fileglob: open[".fileno(*TEMP)."] $file: o.k.\n") if $LOG > 5;
# result
*TEMP;
}
;#
;# scan_mklist(output, directory);
;#
;# Return codes:
;# undef error was detected.
;# 1 success, the newest list was created.
;#
sub scan_mklist ($$) {
my $file = shift;
my $dir = shift;
my $op = &fileglob($file, 'w');
my $no = fileno($op);
if (!defined($no)) {
carp("scan_mklist: fileglog($file): failure.") if $LOG > 5;
return undef;
}
# debug log
if (ref($op) eq 'GLOB') { # normal file.
warn("scan_mklist: use[$no] $file: o.k.\n") if $LOG > 5;
} else {
warn("scan_mklist: open[$no] $file: o.k.\n") if $LOG > 5;
}
# Try to open local directory tree.
my $scan = Fan::Scan->guess($dir);
unless (ref($scan) && $scan->isa('Fan::Scan')) {
carp("scan_mklist: can't create scanner($dir)");
return undef;
}
# generate lists.
my $a;
while (defined($a = $scan->get)) {
$a->fill_checksum; # checksum is optional.
print $op $a->to_line."\n";
}
# close if needed.
if (ref($op) eq 'GLOB') {
warn("scan_mklist: unuse[$no] $file...\n") if $LOG > 5;
} else {
warn("scan_mklist: close[$no] $file...\n") if $LOG > 5;
}
# this causes the target file be closed automatically.
undef $op;
# success.
1;
}
;#
;# Generate diffs for given two index files.
;# scan_mkdiff(output, old, new)
;#
;# Return codes:
;# undef error was detected.
;# 0 diff file was generated, but no change exists.
;# other # of changes. diff file was generated.
;#
sub scan_mkdiff ($$$) {
my $file = shift;
my $old = shift;
my $new = shift;
my $op = &fileglob($file, 'w');
my $no = fileno($op);
if (!defined($no)) {
carp("scan_mkdiff: fileglog($file): failure.") if $LOG > 5;
return undef;
}
# debug log
if (ref($op) eq 'GLOB') { # normal file.
warn("scan_mkdiff: use[$no] $file: o.k.\n") if $LOG > 5;
} else {
warn("scan_mkdiff: open[$no] $file: o.k.\n") if $LOG > 5;
}
my $oldscan = Fan::Scan->guess($old);
unless(ref($oldscan) && $oldscan->isa('Fan::Scan')) {
carp("scan_mkdiff: can't create scanner($old)");
return undef;
}
my $newscan = Fan::Scan->guess($new);
unless(ref($newscan) && $newscan->isa('Fan::Scan')) {
carp("scan_mkdiff: can't create scanner($new)");
return undef;
}
my @dir = (); # directry stack
my $modify = 0; # modification flag
my $a;
my $b;
while (($a, $b) = $oldscan->getcmp($newscan)) {
# check difference first.
if (!defined($a) && !defined($b)) {
confess("scan_mkdiff: UNEXPECTED CASE");
} elsif (!defined($a)) {
$b->flag('+');
} elsif (!defined($b)) {
$b = $a; $b->flag('-');
} elsif (attr_cmp($a, $b)) { # differs
$b->flag('+');
} else {
$b->flag('');
}
# print difference if required.
if ($b->type eq '.') {
print $op ".\n"; # END-OF-DATA.
} elsif ($b->type eq 'D' && $b->name eq '.') {
print $op $b->to_line."\n";
} elsif ($b->flag ne '') {
while (@dir) {
print $op shift(@dir)->to_line."\n";
}
print $op $b->to_line."\n";
$modify++;
} elsif ($b->type eq 'D') {
push(@dir, $b);
} elsif ($b->type eq 'U') {
if (@dir) {
pop(@dir);
} else {
print $op "U\n";
}
} else { # (flag == '' && type !~ /[DU.]/)
; # ignored
}
}
# close if needed.
if (ref($op) eq 'GLOB') {
warn("scan_mkdiff: unuse[$no] $file...\n") if $LOG > 5;
} else {
warn("scan_mkdiff: close[$no] $file...\n") if $LOG > 5;
}
# this causes the target file be closed automatically.
undef $op;
# return # of differences.
$modify;
}
;#
;# scan_update(output, base, diff [, diff...]);
;#
;# Return codes:
;# undef error was detected.
;# 1 success, the newest list was created.
;#
sub scan_update ($$@) {
my $file = shift;
my $base = shift;
my @diff = @_;
my $op = &fileglob($file, 'w');
my $no = fileno($op);
if (!defined($no)) {
carp("scan_update: fileglog($file): failure.") if $LOG > 5;
return undef;
}
# debug log
if (ref($op) eq 'GLOB') { # normal file.
warn("scan_update: use[$no] $file: o.k.\n") if $LOG > 5;
} else {
warn("scan_update: open[$no] $file: o.k.\n") if $LOG > 5;
}
# Open the index who has maximum number.
my $basescan = Fan::Scan->guess($base);
unless(ref($basescan) && $basescan->isa('Fan::Scan')) {
carp("update: can't create scanner($base)");
return undef;
}
warn("scan_update: base $base: o.k.\n") if $LOG > 5;
# Initialize array.
my @array = ();
# Open step files...
for my $file (@diff) {
my $q = Fan::Scan->guess($file);
unless(ref($q) && $q->isa('Fan::Scan')) {
carp("update: can't create scanner($file)");
return undef;
}
push(@array, $q);
warn("scan_update: add $file: o.k.\n") if $LOG > 5;
}
# Try merge
my @a;
while (@a = $basescan->getcmp(@array)) {
my $a;
my $x = undef;
while (@a) {
my $a = shift(@a);
$x = $a if ref($a) && $a->isa('Fan::Attrib');
}
if (!defined($x)) {
confess("scan_update: UNEXPECTED CASE");
} elsif ($x->flag ne '-') {
$x->flag('');
print $op $x->to_line."\n";
}
}
# close if needed.
if (ref($op) eq 'GLOB') {
warn("scan_update: unuse[$no] $file...\n") if $LOG > 5;
} else {
warn("scan_update: close[$no] $file...\n") if $LOG > 5;
}
# this causes the target file be closed automatically.
undef $op;
# Success return.
1;
}
;# Compare two Attrib objects.
;#
sub attr_cmp ($$) {
my $a = shift;
my $b = shift;
my $tag;
my $val;
# check each items...
while (($tag, $val) = each %{$a}) {
next if $tag !~ /^y_/;
return -1 if $b->{$tag} ne $val;
}
# or same
0;
}
;# end of Fan::Scan module
syntax highlighted by Code2HTML, v. 0.9.1