package com.mindprod.backuptozip;
import com.mindprod.commandline.CommandLine;
import com.mindprod.common11.Misc;
import com.mindprod.fastcat.FastCat;
import com.mindprod.filter.AllDirectoriesFilter;
import com.mindprod.filter.AvoidJunkFilter;
import de.schlichtherle.io.ArchiveDetector;
import de.schlichtherle.io.DefaultArchiveDetector;
import de.schlichtherle.io.File;
import de.schlichtherle.io.FileOutputStream;
import de.schlichtherle.util.zip.ZipOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import static java.lang.System.err;
import static java.lang.System.out;
/**
* Backup a set of files to a Zip file for backup to DVD or USB drive.
* <p/>
* On repeated use, updates the zip file rather than recreating it from scratch.
* Unlike other zip utilities, any deleted files are also deleted from the zip.
* Requires TrueZip jar installed in ext dirs. Available from http://repo1.maven.org/maven2/de/schlichtherle/truezip/
* Note that File, FileOutputStream and ZipOutputStream work like their Sun equivalents
* but they are de.schlichtherle enhancements.
*
* @author Roedy Green, Canadian Mind Products
* @version 1.4 2011-03-26 add online HTML manual.
* @since 2009-05-12
*/
public class BackupToZip
{
private static final int FIRST_COPYRIGHT_YEAR = 2009;
/**
* undisplayed copyright notice
*/
@SuppressWarnings({ "UnusedDeclaration" })
private static final String EMBEDDED_COPYRIGHT =
"Copyright: (c) 2009-2017 Roedy Green, Canadian Mind Products, http://mindprod.com";
/**
* when this version released to the public
*/
private static final String RELEASE_DATE = "2011-03-26";
/**
* name of this application.
*/
private static final String TITLE_STRING = "B a c k u p T o Z i p";
/**
* how to use the command line
*/
private static final String USAGE = "BackupToZip needs a filename.zip then a space-separated list of " +
"filenames/dirs, with optional -s -q -v switches";
/**
* latest version released to the public
*/
private static final String VERSION_STRING = "1.4";
/**
* used to collect list of files already backed up
*/
private static ArrayList<File> collector;
/**
* compare two lists, files to be backup up with ones in archive already and do necessary
* additions, updates and deletes to the archive to bring it up to date.
*
* @param zipFile archive file we are updating
* @param filesToBackup array of files to back up
* @param filesPreviouslyBackedUp array of files already in the archive
*/
private static void compareToBackupWithPreviouslyBackedUp( File zipFile, final java.io.File[] filesToBackup,
final File[] filesPreviouslyBackedUp )
{
final int filesToBackupCount = filesToBackup.length;
final int filesPreviouslyBackedUpCount = filesPreviouslyBackedUp.length;
final int leadIgnore = Misc.getCanOrAbsPath( zipFile ).length() + 1;
int i = 0, j = 0;
int deletedCount = 0, addedCount = 0, updatedCount = 0, unchangedCount = 0;
while ( i < filesToBackupCount && j < filesPreviouslyBackedUpCount )
{
java.io.File p = filesToBackup[ i ];
final File s = filesPreviouslyBackedUp[ j ];
String pPath = Misc.getCanOrAbsPath( p );
String sPath = Misc.getCanOrAbsPath( s ).substring( leadIgnore );
int diff = pPath.compareTo( sPath );
if ( diff == 0 )
{
if ( p.length() == s.length() && Math.abs( p.lastModified() - s.lastModified() ) <= 2000 )
{
unchangedCount++;
}
else
{
if ( s.archiveCopyFrom( p ) )
{
out.println( " update > " + pPath );
updatedCount++;
}
else
{
err.println( "failed to update " + pPath );
err.println( p.length() + " : " + s.length() + " : " + p.lastModified() + " : " + s
.lastModified() );
err.println( s.isArchive() + " : " + s.isDirectory() + " : " + s.isEntry() + " : " + s.isFile
() );
}
}
i++;
j++;
}
else if ( diff < 0 )
{
final File zipEntry = new File( zipFile, pPath, ArchiveDetector.NULL );
if ( zipEntry.archiveCopyFrom( p ) )
{
out.println( " add > " + pPath );
addedCount++;
}
else
{
err.println( "failed to add " + pPath );
}
i++;
}
else
{
if ( s.delete() )
{
out.println( " delete > " + sPath );
deletedCount++;
}
else
{
err.println( "failed to delete " + sPath );
}
j++;
}
}
for (; i < filesToBackupCount; i++ )
{
java.io.File p = filesToBackup[ i ];
String pPath = Misc.getCanOrAbsPath( p );
final File zipEntry = new File( zipFile, pPath, ArchiveDetector.NULL );
if ( zipEntry.archiveCopyFrom( p ) )
{
out.println( " add > " + pPath );
addedCount++;
}
else
{
err.println( "failed to add " + pPath );
}
}
for (; j < filesPreviouslyBackedUpCount; j++ )
{
final File s = filesPreviouslyBackedUp[ j ];
String sPath = Misc.getCanOrAbsPath( s ).substring( leadIgnore );
if ( s.delete() )
{
out.println( " delete > " + sPath );
deletedCount++;
}
else
{
err.println( "failed to delete " + sPath );
}
}
out.println( ">>>> "
+ deletedCount
+ " deleted, "
+ addedCount
+ " added, "
+ updatedCount
+ " updated, "
+ unchangedCount
+ " unchanged in "
+ Misc.getCanOrAbsPath( zipFile ) );
if ( updatedCount > 0 || addedCount > 0 || deletedCount > 0 )
{
out.println( " flushing " + Misc.getCanOrAbsPath( zipFile ) );
zipFile.setLastModified( System.currentTimeMillis() );
}
}
/**
* ensure zip file exists as archive
*
* @param zipFile zip file handle
*/
private static void createZip( final File zipFile )
{
if ( !zipFile.exists() )
{
try
{
ZipOutputStream zos =
new ZipOutputStream( new FileOutputStream( zipFile ), "UTF-8" );
zos.close();
}
catch ( IOException e )
{
throw new IllegalArgumentException( "Unable to create the zip archive. " + e.getMessage() );
}
if ( !zipFile.exists() )
{
throw new IllegalArgumentException( "Unable to create the zip archive." );
}
}
if ( !( zipFile.isDirectory() && zipFile.isArchive() ) )
{
throw new IllegalArgumentException( "Zip file " + zipFile.toString() + " is not the correct format. " +
"Delete it and try again." );
}
}
/**
* remove duplicates if any from the list
*
* @param filesToBackup sorted array of files
*
* @return array of files with dups removed
*/
private static java.io.File[] dedup
(
final java.io.File[] filesToBackup )
{
int dups = 0;
java.io.File prev = null;
for ( int i = 0; i < filesToBackup.length; i++ )
{
if ( filesToBackup[ i ].equals( prev ) )
{
filesToBackup[ i ] = null;
dups++;
}
else
{
prev = filesToBackup[ i ];
}
}
if ( dups == 0 )
{
return filesToBackup;
}
final java.io.File[] deduped = new java.io.File[ filesToBackup.length - dups ];
int i = 0;
for ( java.io.File f : filesToBackup )
{
if ( f != null )
{
deduped[ i++ ] = f;
}
}
return deduped;
}
/**
* Display exception error
*
* @param e the error exception
* @param args arguments from the command line
*/
private static void displayError( final Exception e, String[] args )
{
err.println();
if ( e != null )
{
err.println( e.getMessage() );
}
err.println( "Usage: java.exe -jar " + "BackupToZip" + ".jar backup.zip -q someFile1.txt -s someDir" );
err.println( "or: " + "BackupToZip" + ".jar backup.zip -q someFile1.txt -s someDir" );
err.println( "Jet: " + "BackupToZip" + ".exe backup.zip -q someFile1.txt -s someDir" );
err.println();
if ( e != null )
{
err.println();
e.printStackTrace( err );
err.println();
}
err.println();
err.println( "command line parameters:" );
for ( String arg : args )
{
err.println( '[' + arg + ']' );
}
}
/**
* get list of files in the archive previously backed up
*
* @param zipFile the zip archive
* @param estimatedSize estimated number of files.
*
* @return array of files already backed up in the archive
*/
private static File[] getSortListOfFilesPreviouslyBackedUp( final File zipFile, final int estimatedSize )
{
collector = new ArrayList<File>( estimatedSize );
scanZip( zipFile );
final File[] filesPreviouslyBackedUp = collector.toArray( new File[ collector.size() ] );
Arrays.sort( filesPreviouslyBackedUp, new CaseSensitive() );
return filesPreviouslyBackedUp;
}
/**
* @param args command line args with zip file nullified.
*
* @return Files that currently exist that both backed up and not yet backed up.
*/
private static java.io.File[] getSortedListOfFilesToBackup( final String[] args )
{
final AvoidJunkFilter avoidJunkFilter = new AvoidJunkFilter();
avoidJunkFilter.setExtensions( "aps",
"bak",
"csm",
"dmp",
"ilk",
"log",
"lst",
"map",
"ncb",
"obj",
"pch",
"pdb",
"temp",
"tmp" );
avoidJunkFilter.setFilenames( "temp", "temp1", "temp2" );
avoidJunkFilter.setStartsWith( "delete_me" );
final CommandLine commandLine = new CommandLine( args,
new AllDirectoriesFilter(),
avoidJunkFilter );
if ( commandLine.size() == 0 )
{
throw new IllegalArgumentException( "No files found to process\n" + USAGE );
}
final java.io.File[] filesToBackup = new java.io.File[ commandLine.size() ];
int k = 0;
for ( java.io.File f : commandLine )
{
java.io.File clean;
try
{
clean = f.getCanonicalFile();
}
catch ( IOException e )
{
clean = f.getAbsoluteFile();
}
filesToBackup[ k++ ] = clean;
}
Arrays.sort( filesToBackup, new CaseSensitive() );
return dedup( filesToBackup );
}
/**
* recursively collect a list of all files in the archive already backed up .
* Adds findings to collector.
*
* @param dir directory to scan, initially the entire zip as a virtual directory
*/
private static void scanZip( File dir )
{
final File[] dirContents = ( File[] ) dir.listFiles();
if ( dirContents != null )
{
for ( File f : dirContents )
{
if ( f.isFile() )
{
collector.add( f );
}
else if ( f.isArchive() )
{
collector.add( new File( dir, f.getName(), ArchiveDetector.NULL ) );
}
else
{
scanZip( f );
}
}
}
}
/**
* Sort files by absolute path.
* <p/>
* Defines an alternate sort order for java.io.File.
*/
private static class CaseSensitive implements Comparator<java.io.File>
{
/**
* Sort files by absolute path.
* Defines an alternate sort order for java.io.File.
* Compare two java.io.File Objects.
* Compares getAbsolutePath.
* Informally, returns (a-b), or +ve if a is more positive than b.
*
* @param a first java.io.File to compare
* @param b second java.io.File to compare
*
* @return +ve if a>b, 0 if a==b, -ve if a<b
*/
public final int compare( java.io.File a, java.io.File b )
{
return Misc.getCanOrAbsPath( a ).compareTo( Misc.getCanOrAbsPath( b ) );
}
}
/**
* Extracts lines in files that contain a given string.
*
* @param args strings to search for, then a -, then names of files to process, no wildcards.
* strings are case-sensitive, not regexes.
* -all switch means all strings must match
* -where switch means display where each line found file/line #
*/
public static void main( String[] args )
{
try
{
out.println( TITLE_STRING + " " + VERSION_STRING + " released: " + RELEASE_DATE );
File.setDefaultArchiveDetector( ArchiveDetector.NULL );
if ( args.length < 2 )
{
displayError( null, args );
System.exit( 1 );
}
final ArchiveDetector zipArchiveDetector = new DefaultArchiveDetector( "zip" );
final File zipFile = new File( args[ 0 ], zipArchiveDetector );
createZip( zipFile );
args[ 0 ] = null;
out.println( " searching for changed/new/deleted files to backup to " + Misc.getCanOrAbsPath(
zipFile ) );
final java.io.File[] filesToBackup = getSortedListOfFilesToBackup( args );
final File[] filesPreviouslyBackedUp = getSortListOfFilesPreviouslyBackedUp( zipFile,
filesToBackup.length + 100 );
final FastCat sb = new FastCat( 6 );
sb.append( " " );
sb.append( filesToBackup.length );
sb.append( " files to back up, " );
sb.append( filesPreviouslyBackedUp.length );
sb.append( " files previously backed up to " );
sb.append( Misc.getCanOrAbsPath( zipFile ) );
out.println( sb.toString() );
compareToBackupWithPreviouslyBackedUp( zipFile, filesToBackup, filesPreviouslyBackedUp );
File.umount();
out.println( "done" );
}
catch ( Exception e )
{
displayError( e, args );
}
}
}