#!PERL
# file_shift: Shifts older versions of a file.
# Copyright (C) 1999 J.P.M. de Vreught
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
#
# Contact information:
# Hans de Vreught <J.P.M.deVreught@cs.tudelft.nl>
# De Brink 70
# 2553 HA  The Hague
# The Netherlands

$current_version = "2.0.2";

$date = "DATE";
$compress = "COMPRESS";
$suffix = "SUFFIX";

use Getopt::Long;
use File::Copy;

&initialize;
&file_shift;

exit 0;

sub initialize {
	&parse_parameters;
	&parse_configuration if defined $configuration;
	&process_version if defined $version;
	&process_help if defined $help;
	$mode_owner_group = defined $preserve;
	$mode_owner_group ||= defined $mode;
	$mode_owner_group ||= defined $owner;
	$mode_owner_group ||= defined $group;
	&check_consistencies;
	&check_residence_attic;
	&expand_files;
}

sub parse_parameters {
	$Getopt::Long::autoabbrev = 1; 
	$result = GetOptions(
		"configuration=s" => \$configuration,
		"residence=s" => \$residence,
		"attic=s" => \$attic,
		"shift=i", \$shift,
		"time-stamp=s" => \$time_stamp,
		"preserve" => \$preserve,
		"mode=i" => \$mode,
		"user=s" => \$user,
		"group=s" => \$group,
		"zipped" => \$zipped,
		"extra-command=s" => \$extra_command,
		"verbose" => \$verbose,
		"quiet" => \$quiet,
		"help" => \$help,
		"version" => \$version
	);
	exit 1 unless $result;
}

sub parse_configuration {
	return if !defined $configuration;
	open CONFIGURATION, $configuration;
	while (<CONFIGURATION>) {
		chomp;
		study;
		if (/^\s*residence\s*=\s*/o) {
			s///;
			$residence = $_ unless defined $residence;
		} elsif (/^\s*attic\s*=\s*/) {
                       	s///;
                       	$attic = $_ unless defined $attic;
               	} elsif (/^\s*shift\s*=\s*/o) {
                       	s///;
                       	$shift = $_ unless defined $shift;
               	} elsif (/^\s*time-stamp\s*=\s*/o) {
                       	s///;
                       	$time_stamp = $_ unless defined $time_stamp;
               	} elsif (/^\s*preserve\s*$/o) {
                       	s///;
                       	$preserve = 1;
               	} elsif (/^\s*mode\s*=\s*/o) {
                       	s///;
                       	$mode = $_ unless defined $mode;
               	} elsif (/^\s*user\s*=\s*/o) {
                       	s///;
                       	$user = $_ unless defined $user;
               	} elsif (/^\s*group\s*=\s*/o) {
                       	s///;
                       	$group = $_ unless defined $group;
               	} elsif (/^\s*zipped\s*$/o) {
                       	$zipped = 1;
               	} elsif (/^\s*extra-command\s*=\s*/o) {
                       	s///;
                       	$extra_command = $_ unless defined $extra_command;
               	} elsif (/^\s*verbose\s*$/o) {
                       	$verbose = 1;
               	} elsif (/^\s*quiet\s*$/o) {
                       	$quiet = 1;
               	} elsif (/^\s*help\s*$/o) {
                       	$help = 1;
               	} elsif (/^\s*version\s*$/o) {
                       	$version = 1;
               	} else {
			print STDERR "Skipping unkown option '$_'.\n"
				unless $quiet;
		}
	}
}

sub process_version {
	if (defined $version) {
		print <<EOF;
file_shift version $current_version

Copyright (C) 1999 J.P.M. de Vreught

This program comes with ABSOLUTELY NO WARRANTY.  This is free software,
and you are welcome to redistribute it under certain conditions. See the
GNU General Public License for more details.
EOF
		exit 0;
	}
}

sub process_help {
	if (defined $help) {
		print <<EOF;
file_shift version $current_version

Copyright (C) 1999 J.P.M. de Vreught

Usage: file_shift [--configuration file|--residence <string>|--attic <string>|
                   --shift <integer>|--time-stamp <string>|--preserve|
                   --mode <octal>|--user <string>|--group <string>|--zipped|
                   --extra-command <string>|--verbose|--quiet|--help|
                   --version]... <file>...

Shifts older versions of a file and truncates the base file.

Options maybe given with '-' or with '--' and may be abbreviated with a
unique prefix. Options that start with '--' may have an argument appended,
following an equal sign ('=').

<file>:			  The name of the file(s) to be shifted. If no
			  file is given, '.' is assumed to be given. <file>
			  should not contain the leading path; use
			  --residence for that purpose.  When <file> is
			  a directory, all files in that directory and
			  its subdirectories will be shifted. When the
			  attic is located in the residence, the attic
			  will be skipped.  
--configuration <file>:   The options will be located in <file>. The only
                          option which is not allowed is
                          --configuration. Note that the options in <file>
                          are given without prefixing dashes. Options in
                          <file> may be overrule by commandline options.  
--residence <string>:	  The file is located in directory <string>. By
			  default the residence is the current directory.
--attic <string>:	  Older versions of the file are stored in directory
			  <string>. By default the attic is the same as
			  the residence.
--shift <integer>:	  Shifts through file, file.0, ...,
			  file.<integer>. This option is mutually exclusive
			  with --time-stamp. Either --shift or --time-stamp
			  must be given.
--time-stamp <string>:	  The time stamp given in format string <string>
			  is used as an argument for date (see man date(1))
			  to create a suffix for the shifted file. This
			  option is mutually exclusive with --shift. Either
			  --shift or --time-stamp must be given.
--preserve:               Preserve mode and user & group ownership of log
                          files. Parts of the preservation for files may be
                          overruled by using --mode, --user, or --group. When
                          directories need to be created in the attic, it
                          will use the mode, ownership, and group ownership
                          of the corresponding directories in the residence.  
--mode <octal>:           Log files will have mode <octal>.
--user <string>:          Log files will be owned by user <string>. You
                          must have root privileges to do this.
--group <string>:         Log files will be owned by group <string>.
--zipped:		  Compress the shifted files.
--extra-command <string>: The command given by <string> is to be executed
			  after the file is truncated. This command will
			  be executed for each file given.
--verbose:                Give extra information. Basically only interesting
                          for debugging purposes.  
--quiet:                  Suppress warnings.
--help:			  Print this page and exit.
--version:		  Print version and exit.

This program comes with ABSOLUTELY NO WARRANTY.  This is free software,
and you are welcome to redistribute it under certain conditions. See the
GNU General Public License for more details.
EOF
		exit 0;
	}
}

sub check_consistencies {
	if (! defined $shift and ! defined $time_stamp) {
		print STDERR "Either shift or time stamp must be given.\n";
		exit 1;
	} 
	if (defined $shift and defined $time_stamp) {
		print STDERR "Shift and time stamp are mutually exclusive.\n";
		exit 1;
	}
}

sub check_residence_attic {
	if (! defined $residence) {
		print "Residence defaults to '.'.\n" if $verbose;
		$residence = ".";
	} 
	if ( ! -d "$residence/." ) {
		print STDERR "'$residence' is no directory.\n";
		exit 2;
	}
	if (! defined $attic) {
		print "Attic defaults to residence '$residence'.\n"
			if $verbose;
		$attic = $residence;
	} 
	if ( ! -d "$attic/." ) {
		print STDERR "'$attic' is no directory.\n";
		exit 2;
	}
}

sub expand_files {
	($attic_dev, $attic_ino) = (stat "$attic")[1, 2];
	unshift @ARGV, "." if $#ARGV < 0;
	while ($file = shift @ARGV) {
		$file =~ s/\/*$//o;
		if (! -e "$residence/$file") {
			print STDERR
				"Skipping nonexistent '$residence/$file'.\n"
				unless $quiet;
		}
		next if -l "$residence/$file"
			or -S "$residence/$file"
			or -p "$residence/$file"
			or -b "$residence/$file"
			or -c "$residence/$file";
		if (-f "$residence/$file") {
			push @files, $file;
		} elsif (-d "$residence/$file") {
			&expand_directories;
		}
	}
}

sub expand_directories {
	($file_dev, $file_ino) = (stat "$residence/$file")[1, 2];
	if ($attic_dev eq $file_dev and $attic_ino eq $file_ino) {
		print STDERR "Skipping  attic '$residence/$file'.\n"
			unless defined $quiet;
	} else {
		if (! -d "$attic/$file") {
			mkdir "$attic/$file",07777 & ~ umask;
			&copy_mode_owner_group(
				"$residence/$file",
				"$attic/$file"
			) if defined $preserve;
		}
		opendir DIR, "$residence/$file";
		@dir = readdir DIR;
		@dir = grep !/^\.\.?$/, @dir;
		unshift @ARGV, map "$file/$_", @dir;
		closedir DIR;
	}
}

sub file_shift {
	while ($file = shift @files) {
		print "File '$file' is being processed.\n" if $verbose;
		if ($mode_owner_group) {
			get_mode_owner_group("$residence/$file");
		}
		if (defined $time_stamp) {
			&do_time_stamping;
		} else {
			&do_shifting;
		}
	}
}

sub do_time_stamping {
	$age = `$date '$time_stamp'`;
	chomp($age);
	print "Time stamp = '$age'.\n" if $verbose;
	chomp($age);
	rename "$residence/$file", "$residence/$file.$$";
	open FILE, ">$residence/$file";
	close FILE;
	system("$extra_command") if defined $extra_command;
	&copy_mode_owner_group("$residence/$file.$$", "$residence/$file");
	copy "$residence/$file.$$", "$attic/$file.$age";
	&set_mode_owner_group("$attic/$file.$age") if $mode_owner_group;
	unlink "$residence/$file.$$";
	system("$compress $attic/$file.$age") if defined $zipped;
}

sub do_shifting {
	print "Max shift = '$shift'.\n" if $verbose;
	for ($i = $shift; $i > 0; $i--) {
		$age = $i == 1 ? "0" : $i - 1;
		$new = "$attic/$file.$age";
		$new .= ".$suffix" if $zipped;
		if (-f "$new") {
			$old = "$attic/$file.$i";
			$old .= ".$suffix" if $zipped;
			rename $new, $old;
		}
	}
	rename "$residence/$file", "$residence/$file.$$";
	open FILE, ">$residence/$file";
	close FILE;
	system("$extra_command") if defined $extra_command;
	&copy_mode_owner_group("$residence/$file.$$", "$residence/$file");
	copy "$residence/$file.$$", "$attic/$file.0";
	&set_mode_owner_group("$attic/$file.0") if $mode_owner_group;
	unlink "$residence/$file.$$";
	system("$compress $attic/$file.0") if defined $zipped;
}

sub get_mode_owner_group {
	my ($file) = @_;
	($filemode, $uid, $gid) = (stat $file)[2, 4, 5];
	$filemode &= 07777;
       	$filemode = oct $mode if defined $mode;
	$uid = (getpwnam($user))[2] if defined $user;
	$gid = (getgrnam($group))[2] if defined $group;
}

sub set_mode_owner_group {
	my ($file) = @_;
	if (defined $mode or defined $preserve) {
		chmod $filemode, $file;
	}
	if (defined $user or defined $group or defined $preserve) {
		chown $uid, $gid, $file;
	}
}

sub copy_mode_owner_group {
	my ($from, $to) = @_;
	my ($mode, $owner, $group) = (stat $from)[2, 4, 5];
	chmod $mode & 07777, $to;
	chown $owner, $group, $to;
}

__END__

=head1 NAME

B<file_shift> - Shifts older versions of a file.

=head1 SYNOPSIS

B<file_shift> [B<--configuration> I<file> | B<--residence> I<string> |
B<--attic> I<string> | B<--shift> I<integer> | B<--time-stamp> I<string>
| B<--preserve> | B<--mode> I<octal> | B<--user> I<string> | B<--group>
I<string> | B<--zipped> | B<--extra-command> I<string> | B<--verbose> |
B<--quiet> | B<--help> | B<--version>]... I<file>...

=head1 DESCRIPTION

Shifts older versions of a file and truncates the base file. Basically, it
is a program like I<newsyslog> that takes a file like /var/log/syslog and
shifts older copies of /var/log/syslog.[0-7]: /var/log/syslog.6 becomes
/var/log/syslog.7, /var/log/syslog.5 becomes /var/log/syslog.6, ...,
/var/log/syslog.0 becomes /var/log/syslog.1, and finally /var/log/syslog
becomes /var/log/syslog.0 and /var/log/syslog is emptied.

However B<file_shift> also allows you to move the older versions to
a different directory, to have more than 8 older versions, have older
versions suffixed by time stamps, and have older versions compressed. It
also has the possibility to perform extra commands to make daemons aware
of the new situation.

=head1 OPTIONS

Options maybe given with '-' or with '--' and may be abbreviated with a
unique prefix. Options that start with '--' may have an argument appended,
following an equal sign ('=').

=over
=item I<file>:
The name of the file(s) to be shifted. If no files are qiven, '.' is assumed
to be given. <file> should not contain the leading path; use --residence
for that purpose.  When <file> is a directory, all files in that directory
and its subdirectories will be shifted. When the attic is located in the
residence, the attic will be skipped.

=item B<--configuration> I<file>:
The options will be located in <file>. The only option which is not allowed
is --configuration. Note that the options in <file> are given without
prefixing dashes. Option in <file> may be overrule by commandline options.

=item B<--residence> I<string>:

The file is located in directory I<string>. By default the residence is
the current directory.

=item B<--attic> I<string>:

Older versions of the file are stored in directory I<string>. By default
the attic is the same as the residence.

=item B<--shift> I<integer>:

Shifts through file, file.0, ..., file.I<integer>. This option is mutually
exclusive with B<--time-stamp>. Either B<--shift> or B<--time-stamp>
must be given.

=item B<--time-stamp> I<string>:

The time stamp given in format string I<string> is used as an argument for
I<date> (see I<man date(1)>) to create a suffix for the shift file. This
option is mutually exclusive with B<--shift>. Either B<--shift> or
B<--time-stamp> must be given.

=item B<--preserve>:
Preserve mode and user & group ownership of log files. Parts of the
preservation for files may be overruled by using B<--mode>, B<--user>, or
B<--group>. When directories need to be created in the attic, it will use
the mode, ownership, and group ownership of the corresponding directories
in the residence.

=item B<--mode> I<octal>:

Log files will have mode I<octal>.

=item B<--user> I<string>:

Log files will be owned by user I<string>. You must have root privileges
to do this.

=item B<--group> I<string>:

Log files will be owned by group I<string>.

=item B<--zipped>:

Compress the shifted files.

=item B<--extra-command> I<string>:

The command given by I<string> is to be executed after the file is truncated.
This command will be executed for each file given.

=item B<--verbose>:
Give extra information. Basically only interesting for debugging purposes.

=item B<--quiet>:
Suppress warnings.

=item B<--help>:

Print this page and exit.

=item B<--version>:

Print version and exit.

=back

=head1 EXAMPLES

To shift /var/log/syslog to /var/log/syslog.0 till /var/log/syslog.7 and
to make the syslog deamon aware of the truncation, do:

B<file_shift> B<--residence> /var/log B<--shift> 7 B<--extra-command>
'kill -HUP `cat /etc/syslog.pid`' syslog

To shift /var/adm/messages to /var/adm/messages.Sun.SUFFIX till
/var/adm/messages.Sat.SUFFIX, make the log files from root, group adm,
and with mode 640 (rw-r-----), do:

B<file_shift> B<-r> /var/adm B<-a> /var/tmp B<-t> '+%a' B<-m> 640 B<-u>
root B<-g> adm B<-z> messages

Note that '+%a' is a format string of the program I<date>.

To shift all files from /home/www/logs to /home/www/logs/attic, time
stamped by day, and compressed, do:

B<file_shift> B<--configuration> /home/www/file_shift.www .

with in /home/www/file_shift.www:

B<residence>  = /home/www/logs

B<attic>      = /home/www/logs/attic

B<time-stamp> = +%a

B<zipped>

=head1 AUTHOR

Copyright (C) 1999 J.P.M. de Vreught

This program comes with ABSOLUTELY NO WARRANTY.  This is free software,
and you are welcome to redistribute it under certain conditions. See the
GNU General Public License for more details.
