#!/usr/local/bin/perl -w
my $RCS_Id = '$Id: PsiBackup.pl,v 1.6 1997-04-05 18:40:43+02 jv Exp $ ';

# Author          : Johan Vromans
# Created On      : Tue Sep 15 15:59:04 1992
# Last Modified By: Johan Vromans
# Last Modified On: Fri Apr  4 10:06:51 1997
# Update Count    : 151
# Status          : Unknown, Use with caution!

=head1 NAME

PsiBackup - backup an NFS mounted Psion.

=head1 SYNOPSIS

B<perl -w PsiBackup.pl> [options]

=head1 DESCRIPTION

B<PsiBackup> backs up a Psion Series 3a organizer, mounted to a Unix
system using NFS. The conventions used in file names are identical to
the conventions of Psion's B<PsiWin> and B<RCOM> programs.

B<IMPORTANT:> Before starting the back up, all Psion tasks that use
files on the disk to be backed up should be manually stopped.

=head2 Options

=over 5

=item B<-mount> I<dir>

The NFS mount point for the Psion. The default value F</psion.std/mnt>
corresponds to the default name used by B<p3nfsd>.

=item B<-backup> I<dir>

The destination directory for the backup, e.g. F</@psion>. The names of
the Psion and the disk will be appended to it to form the actual name
of the backup directory.

=item B<-name> I<name>

Optionally, the name of this Psion system. Useful if you want to
backup several organizers in the same backup directory.

=item B<-disk> I<name>

Identification of the Psion disk to back up. Possible values are F<i>
(default, the internal disk), F<a> and F<b> (the SSD's),
F<c> (the 3-Link ROM) and F<r> (the Psion's internal ROM).

=item B<->[no]B<archive>

Do [not] archive old versions of files under a F<@archive> tree.
Archiving is default enabled for the disks, and disabled for the ROMs.

=item B<-limit> I<nn>

Limit the number of archive copies to I<nn> versions. Default is 16.
Note that B<RCOM> maintains only one copy, and B<PsiWin> allows up
to 3 copies.

=item B<-help>

A short help for the B<PsiBackup> command.

=item B<-ident>

Show the program identification.

=item B<-verbose>

Supply verbose progress information during the process. 

=item B<-quiet>

Supply no progress information during the process. 

=back

=head1 ENVIRONMENT

B<PsiBackup> uses no environment variables.

=head1 AUTHOR

Johan Vromans E<lt>F<jvromans@squirrel.nl>E<gt>

=head1 BUGS

The program can not stop active applications on the Psion, nor can it
check the Psion's owner information to verify that the correct
organizer is being backed up.

=cut

################ Common stuff ################

# $LIBDIR = $ENV{'LIBDIR'} || '/usr/local/lib/sample';
# unshift (@INC, $LIBDIR);
# require 'common.pl';
use strict;
my $my_package = 'Sciurix';
my ($my_name, $my_version) = $RCS_Id =~ /: (.+).pl,v ([\d.]+)/;
$my_version .= '*' if length('$Locker:  $ ') > 12;

################ Program parameters ################

# The Psion's mount pount. DO *NOT* specify the disk here.
my $src_dir = "/psion.std/mnt";
# The backup tree. Do *NOT* specify the Psion name and disk here.
my $backup_dir = "/\@psion";
# The archive dir. Will be overwritten later.
my $archive_dir = '/@archive';

use Carp;
use Getopt::Long 2.00;
use DirHandle;
use File::Basename qw (dirname);
sub sys (@);

my $archive = 1;		# archive obsolete files
my $limit = 16;			# max. number of backup copies to retain
my $drive = 'i';		# disk to back up
my $name = '';			# optional name of the Psion to back up

my $totfiles = 0;
my $totnew = 0;
my $totbackedup = 0;
my $totobsolete = 0;

my $verbose = 0;
my $quiet = 0;
my ($debug, $trace, $test) = (0, 0, 0);
options ();
$verbose = 0 if $quiet;
$trace |= $debug;
$verbose |= $trace;

################ The Process ################

# Append psion name and disk to the directories.
if ( $drive eq 'r' ) {
    $src_dir .= '/rom::';
}
else {
    $src_dir .= '/loc::' . ($drive eq 'i' ? 'm' : $drive) . ':';
}
unless ( -d $src_dir ) {
    print STDERR ("Source directory $src_dir does not exist.\n",
		  "Backup aborted.\n");
    exit (1);
}
$backup_dir .= '/' . $name unless $name eq '';
unless ( -d $backup_dir ) {
    print STDERR ("Backup directory $backup_dir does not exist.\n",
		  "Create manually before proceeding.\n");
    exit (1);
}

$backup_dir .= '/' . $drive;
$archive_dir = $backup_dir . '/@archive';

# No archiving for ROM and 3-Link.
$archive = 0 if $drive =~ /^[rc]$/;

# Do the backup.
do_backup ($src_dir);

if ( $verbose ) {
    print STDERR ("Total: ", $totfiles, 
		  $totfiles == 1 ? " file" : " files",
		  $totbackedup > 0 ? ", $totbackedup backed up" : "",
		  $totnew > 0 ? " ($totnew new)" : "",
		  $totobsolete > 0 ? ", $totobsolete obsolete" : "",
		  ".\n");
}

exit 0;

################ Subroutines ################

sub do_backup ($) {
    my ($src) = @_;
    my $f;

    print STDERR ("Processing directory $src ... ") if $verbose;

    # Read the source directory, build file and directory lists.
    my $dir = new DirHandle ($src);
    unless ( defined $dir ) {
	carp ("Cannot access $src: $!\n");
	return;
    }

    my %flist = ();
    my @dlist = ();
    while ( defined ($f = $dir->read) ) {
	next if $f eq '.' or $f eq '..';
	my @st = stat ("$src/$f");
	unless ( defined @st and @st > 0 ) {
	    carp ("Cannot stat source $src/$f: $!\n");
	    next;
	}
	# Push directories, for files save the stat info.
	if ( -d _ ) {
	    push (@dlist, "$src/$f");
	}
	else {
	    $flist{"$src/$f"} = [ @st ];
	}
    }

    if ( $verbose ) {
	my $nfiles = scalar (keys (%flist));
	my $ndirs = scalar (@dlist);
	print STDERR ($nfiles, $nfiles == 1 ? " file" : " files") 
	  if $nfiles;
	print STDERR (", ") if $nfiles and $ndirs;
	print STDERR ($ndirs, " director", $ndirs == 1 ? "y" : "ies")
	  if $ndirs;
	print STDERR (".") if $nfiles or $ndirs;
	print STDERR ("\n");
	$totfiles += $nfiles;
    }

    print STDERR ("dlist = @dlist\n") if $debug;
    print STDERR ("flist = ", join(" ", sort(keys(%flist))), "\n") if $debug;

    # Read the backup directory, same procedure.
    my $dst = backup_name ($src);
    my %bflist = ();
    my %bdlist = ();
    $dir = new DirHandle ($dst);
    if ( defined $dir ) {
	while ( defined ($f = $dir->read) ) {
	    next if $f eq '.' or $f eq '..' or $f eq '@archive';
	    my @st = stat ("$dst/$f");
	    unless ( defined @st and @st > 0 ) {
		carp ("Cannot stat $dst/$f: $!\n");
		next;
	    }
	    if ( -d _ ) {
		$bdlist{"$dst/$f"} = 1;
	    }
	    else {
		$bflist{"$dst/$f"} = [ @st ];
	    }
	}
	
	print STDERR ("bdlist = ",
		      join(" ", sort(keys(%bdlist))), "\n") if $debug;
	print STDERR ("bflist = ", 
		      join(" ", sort(keys(%bflist))), "\n") if $debug;
    }

    # Process the files from the source.
    foreach $f ( sort (keys (%flist)) ) {
	backup ($f, $flist{$f});
	# If found, delete from the backup list.
	delete $bflist{backup_name($f)};
    }

    # Process the directories.
    foreach $f ( sort (@dlist) ) {
	do_backup ($f);
	# If found, delete from the backup list.
	delete $bdlist{backup_name($f)};
    }

    # Remove obsolete files from the backup.
    foreach $f ( sort (keys (%bflist)) ) {
	if ( $archive ) {
	    archive ($f, $archive_dir . substr ($f, length($backup_dir)));
	}
	else {
	    print STDERR ("Removing $f\n") if $verbose;
	    sys ("rm", $f);
	}
	$totobsolete++;
    }

    # Remove obsolete directories.
    foreach $f ( reverse (sort (keys (%bdlist))) ) {
	print STDERR ("Removing $f\n") if $verbose;
	sys ("rmdir", $f);
    }
}

# Build backup name for a file.
sub backup_name ($) {
    $backup_dir . substr($_[0], length($src_dir));
}

# Build archive name for a file.
sub archive_name ($) {
    $archive_dir . substr($_[0], length($src_dir));
}

# Backup a file, if needed.
sub backup ($$) {
    my ($src,$st) = @_;
    my $dst = backup_name ($src);

    print STDERR ("=> try $src\n") if $debug;

    my @st = stat ($dst);	# extraneous -- but out of reach...
    my $need = '';

    if ( defined @st and @st > 0 ) {
	if ( $st[7] != $st->[7] ) {
	    $need = "size differs $st[7] -> $st->[7]";
	}
	if ( $st[9] != $st->[9] ) {
	    $need .= "\n   and " if $need;
	    $need .= "mtime differs ".localtime($st[9])." -> ".
	      localtime($st->[9]);
	}
    }
    else {
	$need = 'new file';
	$totnew++;
    }
    return if $need eq '';
    $totbackedup++;
    print STDERR ("Processing file $src: $need\n") if $trace;

    if ( $archive and -f $dst ) {
	archive ($dst, archive_name($src));
    }
    print STDERR ("Backing up $src => $dst\n")
      unless $quiet;
    my $dir = dirname ($dst);
    sys ("mkdir", "-p", $dir) unless -d $dir;
    sys ("cp", $src, $dst);
    utime (time, $st->[9], $dst) or carp ("utime $dst: $!\n");
}

# Archive a file, moving up older copies.
# sequence is
#   foo.bar -> @archive/foo.bar
#   @archive/foo.bar -> @archive/@a.002/foo.bar
#   @archive/@a.002/foo.bar -> @archive/@a.003/foo.bar
#   ...etc...
sub archive ($$) {
    my ($src, $dst) = @_;
    print STDERR ("Archiving ", $src, "\n") if $verbose;
    my $dir;
    if ( -f $dst and $limit > 0 ) {
	moveup ($dst, 1);
    }
    $dir = dirname ($dst);
    sys ("mkdir", "-p", $dir) unless -d $dir;
    sys ("mv", $src, $dst);
}

# Move /path/@archive/@a.(N)/foo.bar -> /path/@archive/@a.(N+1)/foo.bar
# Input name is /path/@archive/foo.bar.
# N = 1 -> source is /path/@archive/foo.bar.
sub moveup ($$) {
    my ($dst, $i) = @_;
    my ($old, $new);
    $old = $dst;
    $old =~ s:/\@archive/:sprintf("%s\@a.%03d/",$&,$i):e if $i > 1;
    ($new = $dst) =~ s:/\@archive/:sprintf("%s\@a.%03d/",$&,$i+1):e;
    if ( -f $new and $i < $limit ) {
	moveup ($dst, $i+1);
    }
    print STDERR ("Moving up $old\n") if $verbose;
    my $dir = dirname ($new);
    sys ("mkdir", "-p", $dir) unless -d $dir;
    sys ("mv", $old, $new);
}

# Adviced system command.
sub sys (@) {
    my (@cmd) = @_;
    print STDERR ("+ @cmd\n") if $trace;
    my $ret = 0;
    $ret = system (@cmd);
    print STDERR ("=> ret = $ret from \"@cmd\"\n") unless $ret == 0;
    exit (255) if $ret == 2 or $ret == 3; # INT and QUIT
    $ret;
}

# Command line options.
sub options () {
    my $help = 0;		# handled locally
    my $ident = 0;		# handled locally

    # Process options.
    if ( @ARGV > 0 && $ARGV[0] =~ /^[-+]/ ) {
	usage (1)
	    unless GetOptions ('ident' => \$ident,
			       'name=s' => \$name,
			       'mount=s' => \$src_dir,
			       'backup=s' => \$backup_dir,
			       'disk=s' => \$drive,
			       'archive!' => \$archive,
			       'limit=i' => $limit,
			       'verbose' => \$verbose,
			       'quiet' => \$quiet,
			       'trace' => \$trace,
			       'help' => \$help,
			       'debug' => \$debug)
	      && !$help;
    }
    print STDERR ("This is $my_package [$my_name $my_version]\n")
	if $ident;

    $drive = lc ($drive);
    usage (1)
      unless $drive =~ /^[iabcr]$/;
}

# Usage.
sub usage ($) {
    my ($xit) = @_;
    print STDERR <<EndOfUsage;
This is $my_package [$my_name $my_version]
Usage: $0 [options] [file ...]
    -mount XXX		NFS mounted Psion dir, e.g. $src_dir
    -backup XXX		destination dir, e.g. $backup_dir
    -name XXX		optional name of this Psion
    -disk M		the Psion disk: 'i' (default), 'a', 'b',
                        'c' (3-Link) or 'r' (ROM)
    -[no]archive	do [not] archive old versions
    -limit NN		limit archive to NN versions
    -help		this message
    -ident		show identification
    -verbose		verbose progress information
    -quiet		no progress information
EndOfUsage
    exit $xit if defined $xit and $xit != 0;
}
