#!perl
# Copyright (c) 2001-2009 Nokia Corporation and/or its subsidiary(-ies).
# All rights reserved.
# This component and the accompanying materials are made available
# under the terms of the License "Eclipse Public License v1.0"
# which accompanies this distribution, and is available
# at the URL "http://www.eclipse.org/legal/epl-v10.html".
# 
# Initial Contributors:
# Nokia Corporation - initial contribution.
# 
# Contributors:
# 
# Description:
# 
#

use strict;
use FindBin;
use lib "$FindBin::Bin";
use Getopt::Long;
use IniData;
use RelData;
use File::Copy;
use File::Path;
use File::Spec;
use File::Basename;
use Cleaner;
use Utils;
use Cwd;

#
# Globals.
#

my $verbose = 0;
my $overwrite = 0;
my $dummyRun = 0;
my $descriptionFile;
my $iniData = IniData->New();
my $cleaner; # object that does most of it
my $cleanTo;
my $expunge = 0; # don't leave reldatas lying around
my $reallyClean;

#
# Main.
#

ProcessCommandLine();
$cleaner = Cleaner->New($iniData, 0, $verbose, $reallyClean); # 0 = local not remote
ParseDescriptionFile($descriptionFile);
$cleaner->SetCleaningSubroutine(\&CleaningSubroutine);
if (!$expunge) {
  $cleaner->SetRevertingSubroutine(\&RevertingSubroutine);
}
$cleaner->Clean();


#
# Subs.
#

sub ProcessCommandLine {
  Getopt::Long::Configure ("bundling");
  my $help;
  GetOptions('h' => \$help, 'd' => \$dummyRun, 'v+' => \$verbose, 'o' => \$overwrite, 'r' => \$reallyClean);

  if ($help) {
    Usage(0);
  }

  $descriptionFile = shift @ARGV;

  unless ($descriptionFile) {
    print "Error: Archive cleaning description file not specified\n";
    Usage(1);
  }

  unless ($#ARGV == -1) {
    print "Error: Invalid number of arguments\n";
    Usage(1);
  }

  if ($dummyRun and not $verbose) {
    $verbose = 1;
  }
}

sub Usage {
  my $exitCode = shift;

  Utils::PrintDeathMessage($exitCode, "\nUsage: cleanlocalarch [options] description-file

options:

-h  help
-d  dummy run (don't do anything) - assumes -v
-r  really clean (removes corrupt and partially released components)
-v  verbose output (-vv very verbose)
-o  overwrite destination (delete destination then normal copy)

Please note, if you are in the process of publishing components to the archive
and specify the -r option you may lose partially released components.\n");

}

sub ParseDescriptionFile {
  if ($dummyRun) { print "Running in dummy mode...\n"; }
  if ($verbose) { print "Parsing \"$descriptionFile\"...\n"; }
  open (DES, $descriptionFile) or die "Unable to open \"$descriptionFile\" for reading: $!\n";

  while (my $line = <DES>) {
    # Remove line feed, white space and comments.
    chomp($line);
    $line =~ s/^\s*$//;
    $line =~ s/#.*//;
    if ($line eq '') {
      # Nothing left.
      next;
    }

    my $keyWord;
    my @operand;
    if ($line =~ /^(\w+)\s+(.*)/) {
      $keyWord = $1;
      @operand = ();
      if ($2) {
        @operand = split /\s+/, $2;
      }
    } else {
      $keyWord = $line;
    }

    unless (defined $keyWord) {
      die "Error: Invalid line in \"$descriptionFile\":\n$line\n";
      next;
    }

    if ($cleaner->ProcessDescriptionLine($descriptionFile, $keyWord, @operand)) {
      # We're happy because Cleaner.pm knows what to do with this line
    } elsif ($keyWord =~ /^clean_to$/) {
      unless ($#operand == 0) {
        die "Error: Incorrect number of arguments to \'$keyWord\' keyword in \"$descriptionFile\"\nSyntax: clean_to <path>\n";
      }
      if ($cleanTo) {
        die "Error: \'$keyWord\' keyword specifed more than once in \"$descriptionFile\"\n";
      }
      $cleanTo = $operand[0];
    } elsif ($keyWord =~ /^delete$/) {
      if (@operand) {
        die "Error: Incorrect number of arguments to \'$keyWord\' keyword in \"$descriptionFile\"\nSyntax: delete\n";
      }
    } elsif ($keyWord =~ /^expunge$/) {
      $expunge = 1;
      $cleaner->{expunge_already_cleaned} = 1;
    } elsif ($keyWord =~ /^no_prompt$/) {
      print "Warning: currently, CleanLocalArch does not prompt. 'no_prompt' keyword is redundant.\n";
    } else {
      die "Error: Unknown keyword \'$keyWord\' in \"$descriptionFile\"\n";
    }
  }

  close (DES);

  unless ($cleanTo || $expunge) {
    die "Error: \"Clean to\" path not specified in \"$descriptionFile\"\n";
  }
  if ($cleanTo && $expunge) {
    die "Error: can't specify both \"clean_to\" and \"expunge\" in \"$descriptionFile\"\n";
  }

  if ($verbose > 1) {
    $cleaner->PrintEnvsToKeep();
  }
}

sub CleaningSubroutine {
  # This actually gets run by Cleaner.pm (it's a callback)
  my $thisComp = shift;
  my $thisVer = shift;
  my $relDir = shift;
  if ($expunge) {
    print "Expunging $thisComp $thisVer from $relDir...\n" if ($verbose);
    return DeleteComp($relDir);
  }
  print "Archiving $thisComp $thisVer from $relDir to $cleanTo...\n" if ($verbose);
  my $cleanDir = "$cleanTo\\$thisComp\\$thisVer";
  
  if (CopyComp($relDir, $cleanDir)) {
    print "Wiping $thisComp $thisVer from $relDir...\n" if ($verbose);
    if (DeleteComp("$relDir")) {
      # Check if the remaining dir is empty
      my ($parent, $file, $ext) = Utils::SplitFileName($relDir);
      return DeleteCompIfEmpty($parent);
    }
    else {
      # Call the reverting subroutine here because cleaner.pm will only revert clean components
      RevertingSubroutine($thisComp, $thisVer, $relDir);
    }
  }
  
  return 0;
}

sub RevertingSubroutine {
  # Again, this gets run by Cleaner.pm
  my $thisComp = shift;
  my $thisVer = shift;
  my $relDir = shift;
  
  print "Restoring $thisComp $thisVer to $relDir...\n" if ($verbose);
  
  # create the reldir if required
  if(!-d $relDir) {
    Utils::MakeDir($relDir);
  }
  
  my $fullCleanToPath = File::Spec->catdir($cleanTo, $thisComp, $thisVer);
  
  my $dirContents = Utils::ReadDir($fullCleanToPath);
  foreach my $thisFile (@$dirContents) {
     copy(File::Spec->catdir($fullCleanToPath, $thisFile), $relDir);
  }  

  print "Removing copy of $thisComp $thisVer from $cleanTo...\n" if ($verbose);
  if (DeleteComp("$cleanTo\\$thisComp\\$thisVer")) {
    # Check if the remaining dir is empty
    return DeleteCompIfEmpty("$cleanTo\\$thisComp");
  }
  else {
    # Failed to even delete component
    return 0;
  }
}

sub CopyComp {
  my $dir = shift;
  my $destDir = shift;

  if (-e $destDir) {
    if ($overwrite) {
      if ($verbose > 0) { print "Overwriting by deleting \"$destDir\"\n"; }
      DeleteComp("$destDir");
    }
    else {
      print "Error: Can't copy \"$dir\" to \"$destDir\" because directory \"$destDir\" already exists\n";
      return 0;
    }
  }

  my $failed = 0;
  my @copied;
  eval {
    Utils::MakeDir($destDir) unless $dummyRun;
  };
  if ($@) {
    print "$@";
    $failed = 1;
  }

  if($failed==0) {
    my $dirContents = Utils::ReadDir($dir);
    foreach my $thisFile (@$dirContents) {
      if ($verbose > 1) { print "\tCopying \"$dir\\$thisFile\" to \"$destDir\"...\n"; }
      if ($dummyRun) {
        return 1;
      }
      else {
        if (copy($dir."\\".$thisFile, $destDir)) {
          push @copied, $thisFile;
        }
        else {
          print "Error: Couldn't copy \"$dir\\$thisFile\" to \"$destDir\": $!\n";
          $failed = 1;
          if (-f $destDir."\\".$thisFile) {
            # Must've part-copied this file
            push @copied, $thisFile;
          }
          last;
        }
      }
    }
  }

  if ($failed) {
    # Revert copied files
    foreach my $thisFile (@copied) {
      unlink $destDir."\\".$thisFile or print "Error: Couldn't delete $destDir\\$thisFile when cleaning up\n";
    }
    DeleteCompIfEmpty($destDir) or print "Error: Couldn't clean up empty directory $destDir\n";
  }

  return ($failed == 0);
}

sub DeleteComp {
  my $dir = shift;

  if (!$dummyRun) {
    local $SIG{__WARN__} = sub {my $line = shift;
                                $line =~ s/ at .*$//;
                                print "Error: $line\n";};
    
    my $reldataFile = File::Spec->catdir($dir, 'reldata');

    my $origDir = cwd();
    chdir(dirname($dir));
    
    if (-e $reldataFile) {
      # Delete the reldata file first, if something goes wrong other tools will identify the archived component
      # as corrupt by the absence of reldata
      if (!unlink $reldataFile) {
        print "Error: Couldn't delete \"$reldataFile\"\n";
        return 0;
      }
    }
    
    if (!rmtree($dir, 0, 0) or -d $dir) {
      print "Error: Couldn't delete \"$dir\"\n";
      return 0;
    }
    else {
      chdir($origDir);
      return 1;
    }
  }
  else {
    return 1;
  }
}

sub DeleteCompIfEmpty {
  my $dir = shift;

  if (!$dummyRun) {
    if (opendir(DIR, $dir)) {
      my @files = grep( !/\.\.?$/, readdir DIR);
      if (!closedir(DIR)) {
        die "Error: Couldn't close '$dir' after reading. Aborting\n";
      }
      if (scalar(@files) == 0) {
        print "Tidying $dir...\n" if ($verbose);
        return DeleteComp("$dir");

      }
      else {
        return 1; # Nothing to do
      }
    }
    else {
      print "Warning: Couldn't open '$dir' directory for reading. An empty directory may have been left behind.\n";
      return 1; # Warning only
    }
  }
  else {
    return 1; # Dummy run
  }
}

__END__

=head1 NAME

CleanLocalArch - Cleans unwanted releases from the local release archive.

=head1 SYNOPSIS

  cleanlocalarch [options] <description_file>

options:

  -h  help
  -d  dummy run (don't do anything) - assumes -v
  -r  really clean (removes corrupt and partially released components)
  -v  verbose output (-vv very verbose)
  -o  overwrite destination (delete destination then normal copy)

Please note, if you are in the process of publishing components to the archive and specify the -r option you may lose partially released components.

=head1 DESCRIPTION

C<CleanLocalArch> allows releases to be cleaned out of a local archive. This may be useful if a local archive is consuming a large amount of disk space and there are old releases present that are no longer required. Note that releases to be cleaned are normally backed up to a user defined directory before being deleted. This allows the cleaned releases to be permanently archived (to say a writable CDROM) before they are deleted.

If C<CleanLocalArch> encounters an error while backing up releases to be cleaned, it will attempt to back out of the change by deleting the backups of any releases already done. If C<CleanLocalArch> encounters errors while backing out of a clean, it has the potential to leave releases in the backup directory. Similarly, if after backing up all releases to delete, it encounters errors while actually deleting them, it may leave releases in the local archive. However the clean can be repeated to a fresh backup directory once the problem has been isolated to get rid of these releases.

Before using C<CleanLocalArchive> you must write a plain text file that describes which releases you want to keep etc. The following keywords are supported:

=over 4

=item keep_env <component> <version>

Instructs C<CleanLocalArchive> to keep all the component versions in the environment from which the specified component was released. This keyword may be used multiple times.

=item keep_rel <component> <version>

Instructs C<CleanLocalArchive> to keep a specific component release. This keyword may be used multiple times.

=item keep_recent_env <component> <num_days>

Instructs C<CleanLocalArchive> to keep all named component releases, including their environments, where the component release has been made within the specified number of days (since the current time). This keyword may be used multiple times provided it is used for different components each time.

=item keep_recent_rel [component] <num_days>

Instructs C<CleanLocalArchive> to keep any component releases made within the specified number of days (since the current time). If a component name is specified, C<CleanLocalArchive> will only keep component releases which match that name (and are sufficiently recent). This keyword may be used multiple times if the command is used for different components.

=item keep_recent <num_days>

B<Depricated:> Equivalent to keep_recent_rel without a component name entered.

=item clean_to

Specifies where to move release to be cleaned. Use of this keyword is mandatory and may only be used once. There is an alternative, 'expunge', which will actually delete the releases - but this is only intended for test scripts and use on real, important archives is strongly discouraged.

=item force

This keyword, which takes no operands, specifies that cleanlocalarch should be non-interactive.

=back

For example:

 keep_env     pixie alpha
 keep_env     pixie beta
 keep_rel     comp1 rel1
 keep_recent  10
 clean_to     \\backup\pixie_cleaned_releases

C<CleanLocalArch> will work out which component releases need to be kept in order to satisfy the specified keep criteria. All other component releases found in the archive will be moved to the C<clean_to> directory. B<It is therefore extremely important that the list of environments to keep is complete>. It is recommended that this file be controlled using a configuration management tool. It is also recommended that each project has only one description file, and that all users of C<CleanLocalArch> know where to find it.

Recommended procedure for using C<CleanLocalArch>:

=over 4

=item 1

Inform all users of the archive that a clean is about to be performed, and that the archive will be unavailable whilst this is happening.

=item 2

Take the archive off-line or alter directory permissions such that you are the only person that can access it.

=item 3

Backup the archive.

=item 4

Run C<CleanLocalArchive> and carefully check the list of components that are about to be cleaned. If you are happy, type 'yes' to continue, otherwise type 'no', modify your description file and re-run C<CleanLocalArchive>.

=item 5

Backup the C<clean_to> directory.

=item 6

Bring the archive back on-line.

=item 7

Inform all users of the archive that it is available for use once more.

=back

=head1 STATUS

Supported. If you find a problem, please report it to us.

=head1 COPYRIGHT

 Copyright (c) 2001-2009 Nokia Corporation and/or its subsidiary(-ies).
 All rights reserved.
 This component and the accompanying materials are made available
 under the terms of the License "Eclipse Public License v1.0"
 which accompanies this distribution, and is available
 at the URL "http://www.eclipse.org/legal/epl-v10.html".
 
 Initial Contributors:
 Nokia Corporation - initial contribution.
 
 Contributors:
 
 Description:
 

=cut
