/**
* ZipSnap 2.1
* Copyright 2007 Zach Scrivena
* 2007-08-26
* zachscrivena@gmail.com
* http://zipsnap.sourceforge.net/
*
* ZipSnap is a simple command-line incremental backup tool for directories.
*
* TERMS AND CONDITIONS:
* 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 3 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, see <http://www.gnu.org/licenses/>.
*/
package zipsnap;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.NavigableMap;
import java.util.Stack;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* ZipSnap is a simple command-line directory incremental backup tool.
*/
public class ZipSnap
{
/*********************
* GENERAL CONSTANTS *
*********************/
/** constant: program title */
static final String programTitle =
"ZipSnap 2.1 Copyright 2007 Zach Scrivena 2007-08-26";
/** constant: program version */
static final double version = 2.1;
/** constant: size in bytes of read/write buffer (1 Mb) */
static final int bufferSize = 1048576;
/**
* constant: minimum file size in bytes for compression (256 bytes);
* files smaller than this limit will be stored without compression
*/
static final int minSizeForCompression = 256;
/** constant: current time */
static final Date currentTime = new Date();
/**********************
* GENERAL PARAMETERS *
**********************/
/** parameter: archive name (as specified by the command-line argument) */
private static String archive = null;
/** parameter: archive directory (absolute and canonical path) */
private static File archiveDir = null;
/** parameter: archive directory absolute and canonical pathname (ends with a trailing separator) */
private static String archiveDirName = null;
/** parameter: current directory (absolute and canonical path) */
static File currentDir = null;
/** parameter: current directory absolute and canonical pathname (ends with a trailing separator) */
private static String currentDirName = null;
/** parameter: simulate only (do not actually add/restore snapshot); by default, simulation mode is not used */
static boolean simulateOnly = false;
/*********************************
* COMMAND AND OPTION PARAMETERS *
*********************************/
/** ZipSnap commands to be executed (ADD, RESTORE, INFO) */
private static enum Command
{
ADD, RESTORE, INFO;
}
/** parameter: ZipSnap command to be executed (ADD, RESTORE, INFO) */
private static Command command;
/** parameter: use file CRC-32 checksum in file matching; by default, the checksum is not used */
static boolean useCrc = false;
/**
* parameter: time-tolerance in milliseconds to be used in file matching;
* by default, a 0-millisecond time-tolerance is used
*/
private static long timeTolerance = 0;
/**
* parameter: Add/extract all files/directories when adding/restoring
* a snapshot without performing file matching first. More explicitly, when
* adding a snapshot, add all files/directories, not just those different
* from previously archived files/directories; when restoring a snapshot,
* extract all files/directories, not just those different from existing
* files/directories. By default, only files/directories that fail file
* matching are added/extracted.
*/
private static boolean all = false;
/** parameter: snapshot index to be restored */
private static int snapshotIndex = -1;
/**
* parameter: force addition of snapshot, even if it is identical
* to the last snapshot; by default, an identical snapshot is not added
*/
private static boolean forceAdd = false;
/**
* parameter: default action on overwriting existing files;
* by default, the user is prompted on whether to overwrite existing files
*/
static char defaultActionOnOverwrite = '\0';
/**
* parameter: default action on deleting existing files;
* by default, the user is prompted on whether to delete existing files
*/
static char defaultActionOnDelete = '\0';
/** archive volume types (ZIP, JAR, LIST) */
private static enum VolumeType
{
ZIP, JAR, LIST;
}
/** parameter: archive volume type (ZIP, JAR, LIST); by default, the ZIP volume type is used */
private static VolumeType volumeType = VolumeType.ZIP;
/** parameter: compression level (0 = min, 9 = max); default compression level is 9 */
static int compressionLevel = 9;
/** parameter: search paths for volume files when restoring a snapshot */
static final List<File> searchPaths = new ArrayList<File>();
/** filter types (GLOB, REGEX) */
private static enum FilterType
{
GLOB, REGEX;
}
/** parameter: filter type (GLOB, REGEX) */
private static FilterType filterType;
/** parameter: filter string for matching filenames */
private static String filterString = null;
/**
* parameter: match filter string against full (relative) pathnames;
* by default, only the name of the file/directory is used for matching
*/
static boolean filterFullPathname = false;
/** parameter: filter pattern for matching filenames */
static Pattern filterPattern = null;
/**
* Main entry point for the ZipSnap program.
*
* @param args
* Command-line argument strings
*/
public static void main(
final String[] args)
{
/* print program title */
System.out.print("\n" + ZipSnap.programTitle + "\n");
/* create error and warning handler */
ErrorWarningHandler.setPauseOnWarning(true); /* pause on warning by default */
try
{
/* process command-line arguments */
processArguments(args);
/* construct Java regex Pattern, if necessary */
if (ZipSnap.filterString != null)
{
if (ZipSnap.filterType == ZipSnap.FilterType.GLOB)
{
ZipSnap.filterPattern = getGlobFilterPattern();
}
else if (ZipSnap.filterType == ZipSnap.FilterType.REGEX)
{
ZipSnap.filterPattern = getRegexFilterPattern();
}
}
/* execute requested command */
if (ZipSnap.command == ZipSnap.Command.ADD)
{
addSnapshotToArchive();
}
else if (ZipSnap.command == ZipSnap.Command.RESTORE)
{
restoreSnapshotFromArchive();
}
else if (ZipSnap.command == ZipSnap.command.INFO)
{
displayArchiveInfo();
}
System.out.print("\n\nZipSnap is done!");
final int numWarnings = ErrorWarningHandler.getNumWarnings();
if (numWarnings > 0)
System.out.print("\n(" + numWarnings + " " + ((numWarnings == 1) ? "warning" : "warnings") + " encountered)");
System.out.print("\n\n");
}
catch (Exception e)
{
ErrorWarningHandler.reportErrorAndExit("An unexpected error has occurred:\n" +
ErrorWarningHandler.getExceptionMessage(e));
}
System.exit(0);
}
/**
* Process command-line arguments.
*
* @param args
* Command-line argument strings
*/
private static void processArguments(
final String[] args)
{
/* error message */
String err = null;
CheckArguments:
do
{
/* run one iteration */
if (args.length == 0)
{
/* print usage help */
printUsage();
System.exit(0);
break CheckArguments;
}
else if (args.length < 2)
{
err = "Insufficient arguments:\nA command and a ZipSnap archive directory must be specified.";
break CheckArguments;
}
/* archive name is the last argument */
ZipSnap.archive = args[args.length - 1];
/* command is the first argument */
final String command = args[0];
if ("a".equals(command))
{
ZipSnap.command = ZipSnap.Command.ADD;
}
else if ("r".equals(command))
{
ZipSnap.command = ZipSnap.Command.RESTORE;
}
else if ("i".equals(command))
{
ZipSnap.command = ZipSnap.Command.INFO;
}
else
{
err = "Invalid command \"" + command +
"\" specified:\nCommand must be \"a\", \"r\", or \"i\".";
break CheckArguments;
}
/* flags for volume type */
int swZip = 0;
int swJar = 0;
int swList = 0;
/* process command-specific switches */
ProcessNextSwitch:
for (int i = 1; i < args.length - 1; i++)
{
final String sw = args[i];
/******************************************
* (1) SWITCHES FOR ADD, RESTORE, OR INFO *
******************************************/
if ((ZipSnap.command == ZipSnap.Command.ADD) ||
(ZipSnap.command == ZipSnap.Command.RESTORE) ||
(ZipSnap.command == ZipSnap.Command.INFO))
{
if ("-i".equals(sw) || "--ignorewarnings".equals(sw))
{
/* ignore warnings */
ErrorWarningHandler.setPauseOnWarning(false);
continue ProcessNextSwitch;
}
else if (sw.startsWith("-f:") || sw.startsWith("--filter:"))
{
/* glob filter for file/directory name */
final String a = sw.substring(sw.indexOf(':') + 1);
if (a.isEmpty())
{
err = "Empty --filter parameter specified:\nGlob filter string must be nonempty.";
break CheckArguments;
}
ZipSnap.filterString = a;
ZipSnap.filterType = ZipSnap.FilterType.GLOB;
ZipSnap.filterFullPathname = false;
continue ProcessNextSwitch;
}
else if (sw.startsWith("-F:") || sw.startsWith("--FILTER:"))
{
/* glob filter for full (relative) file/directory pathname */
final String a = sw.substring(sw.indexOf(':') + 1);
if (a.isEmpty())
{
err = "Empty --FILTER parameter specified:\nGlob filter string must be nonempty.";
break CheckArguments;
}
ZipSnap.filterString = a;
ZipSnap.filterType = ZipSnap.FilterType.GLOB;
ZipSnap.filterFullPathname = true;
continue ProcessNextSwitch;
}
else if (sw.startsWith("-e:") || sw.startsWith("--filterregex:"))
{
/* regex filter for file/directory name */
final String a = sw.substring(sw.indexOf(':') + 1);
if (a.isEmpty())
{
err = "Empty --filterregex parameter specified:\nRegex filter string must be nonempty.";
break CheckArguments;
}
ZipSnap.filterString = a;
ZipSnap.filterType = ZipSnap.FilterType.REGEX;
ZipSnap.filterFullPathname = false;
continue ProcessNextSwitch;
}
else if (sw.startsWith("-E:") || sw.startsWith("--FILTERREGEX:"))
{
/* regex filter for full (relative) file/directory pathname */
final String a = sw.substring(sw.indexOf(':') + 1);
if (a.isEmpty())
{
err = "Empty --FILTERREGEX parameter specified:\nRegex filter string must be nonempty.";
break CheckArguments;
}
ZipSnap.filterString = a;
ZipSnap.filterType = ZipSnap.FilterType.REGEX;
ZipSnap.filterFullPathname = true;
continue ProcessNextSwitch;
}
}
/************************************
* (2) SWITCHES FOR ADD, OR RESTORE *
************************************/
if ((ZipSnap.command == ZipSnap.Command.ADD) ||
(ZipSnap.command == ZipSnap.Command.RESTORE))
{
if ("-s".equals(sw) || "--simulate".equals(sw))
{
/* simulate only; do not actually add/restore snapshot */
ZipSnap.simulateOnly = true;
ErrorWarningHandler.setPauseOnWarning(false);
continue ProcessNextSwitch;
}
else if ("--all".equals(sw))
{
/* add/restore all files/directories without performing matching */
ZipSnap.all = true;
continue ProcessNextSwitch;
}
else if ("-c".equals(sw) || "--crc".equals(sw))
{
/* use file/directory CRC-32 checksum for matching */
ZipSnap.useCrc = true;
continue ProcessNextSwitch;
}
else if (sw.startsWith("-t:") || sw.startsWith("--time:"))
{
/* use specified time-tolerance (in milliseconds) for matching */
final String a = sw.substring(sw.indexOf(':') + 1);
if (a.isEmpty())
{
err = "Empty --time parameter specified:\nTime-tolerance (in milliseconds) must be a nonnegative integer.";
break CheckArguments;
}
try
{
ZipSnap.timeTolerance = Long.parseLong(a);
}
catch (Exception e)
{
ZipSnap.timeTolerance = -1;
}
if (ZipSnap.timeTolerance < 0)
{
err = "Invalid --time parameter \"" + a +
"\" specified:\nTime-tolerance (in milliseconds) must be a nonnegative integer.";
break CheckArguments;
}
continue ProcessNextSwitch;
}
else if ("-l".equals(sw) || "--list".equals(sw))
{
/* create a list of files/directories to be added/extracted */
swList = 1;
continue ProcessNextSwitch;
}
}
/************************
* (3) SWITCHES FOR ADD *
************************/
if (ZipSnap.command == ZipSnap.Command.ADD)
{
if ("--forceadd".equals(sw))
{
/* force addition of snapshot, even if identical to the last snapshot */
ZipSnap.forceAdd = true;
continue ProcessNextSwitch;
}
else if ("-z".equals(sw) || "--zip".equals(sw))
{
/* add files/directories to a ZIP volume */
swZip = 1;
continue ProcessNextSwitch;
}
else if (sw.startsWith("-z:") || sw.startsWith("--zip:"))
{
/* add files/directories to a ZIP volume with specified compression level */
final String a = sw.substring(sw.indexOf(':') + 1);
if (a.isEmpty())
{
err = "Empty --zip parameter specified:\nCompression level must be an integer 0-9.";
break CheckArguments;
}
try
{
ZipSnap.compressionLevel = Integer.parseInt(a);
}
catch (Exception e)
{
ZipSnap.compressionLevel = -1;
}
if ((ZipSnap.compressionLevel < 0) || (ZipSnap.compressionLevel > 9))
{
err = "Invalid --zip parameter \"" + a +
"\" specified.\nCompression level must be an integer 0-9.";
break CheckArguments;
}
swZip = 1;
continue ProcessNextSwitch;
}
else if ("-j".equals(sw) || "--jar".equals(sw))
{
/* add files/directories to a JAR volume */
swJar = 1;
continue ProcessNextSwitch;
}
else if (sw.startsWith("-j:") || sw.startsWith("--jar:"))
{
/* add files/directories to a JAR volume with specified compression level */
final String a = sw.substring(sw.indexOf(':') + 1);
if (a.isEmpty())
{
err = "Empty --jar parameter specified:\nCompression level must be an integer 0-9.";
break CheckArguments;
}
try
{
ZipSnap.compressionLevel = Integer.parseInt(a);
}
catch (Exception e)
{
ZipSnap.compressionLevel = -1;
}
if ((ZipSnap.compressionLevel < 0) || (ZipSnap.compressionLevel > 9))
{
err = "Invalid --jar parameter \"" + a +
"\" specified.\nCompression level must be an integer 0-9.";
break CheckArguments;
}
swJar = 1;
continue ProcessNextSwitch;
}
}
/****************************
* (4) SWITCHES FOR RESTORE *
****************************/
if (ZipSnap.command == ZipSnap.Command.RESTORE)
{
if (sw.startsWith("--snapshot:"))
{
/* snapshot number to be restored */
final String a = sw.substring(sw.indexOf(':') + 1);
if (a.isEmpty())
{
err = "Empty --snapshot parameter specified:\nSnapshot number must be a positive integer.";
break CheckArguments;
}
try
{
ZipSnap.snapshotIndex = Integer.parseInt(a) - 1; /* convert "number" to "index" */
}
catch (Exception e)
{
ZipSnap.snapshotIndex = -1;
}
if (ZipSnap.snapshotIndex < 0)
{
err = "Invalid --snapshot parameter \"" + a +
"\" specified:\nSnapshot number must be a positive integer.";
break CheckArguments;
}
continue ProcessNextSwitch;
}
else if (sw.startsWith("-o:") || sw.startsWith("--overwrite:"))
{
/* overwrite existing files/directories? */
final String a = sw.substring(sw.indexOf(':') + 1);
if (a.isEmpty())
{
err = "Empty --overwrite parameter specified:\nOverwrite parameter must be \"y\" or \"n\".";
break CheckArguments;
}
if ("y".equals(a))
{
ZipSnap.defaultActionOnOverwrite = 'Y';
}
else if ("n".equals(a))
{
ZipSnap.defaultActionOnOverwrite = 'N';
}
else
{
err = "Invalid --overwrite parameter \"" + a +
"\" specified:\nOverwrite parameter must be \"y\" or \"n\".";
break CheckArguments;
}
continue ProcessNextSwitch;
}
else if (sw.startsWith("-d:") || sw.startsWith("--delete:"))
{
/* delete unmatched existing files/directories? */
final String a = sw.substring(sw.indexOf(':') + 1);
if (a.isEmpty())
{
err = "Empty --delete parameter specified:\nDelete parameter must be \"y\" or \"n\".";
break CheckArguments;
}
if ("y".equals(a))
{
ZipSnap.defaultActionOnDelete = 'Y';
}
else if ("n".equals(a))
{
ZipSnap.defaultActionOnDelete = 'N';
}
else
{
err = "Invalid --delete parameter \"" + a +
"\" specified:\nDelete parameter must be \"y\" or \"n\".";
break CheckArguments;
}
continue ProcessNextSwitch;
}
else if (sw.startsWith("-p:") || sw.startsWith("--path:"))
{
/* additional search path for volume files */
final String a = sw.substring(sw.indexOf(':') + 1);
if (a.isEmpty())
{
err = "Empty --path parameter specified:\nPath parameter must be an existing directory.";
break CheckArguments;
}
final File dir;
try
{
dir = (new File(a)).getCanonicalFile();
}
catch (Exception e)
{
err = "Invalid --path parameter specified:\nCannot get absolute path of directory \"" +
a + "\":\n" + ErrorWarningHandler.getExceptionMessage(e);
break CheckArguments;
}
if (!dir.exists())
{
err = "Invalid --path parameter specified:\nDirectory \"" +
a + "\" does not exist.";
break CheckArguments;
}
if (!dir.isDirectory())
{
err = "Invalid --path parameter specified:\n\"" +
a + "\" is not a directory; could it be a file?";
break CheckArguments;
}
ZipSnap.searchPaths.add(dir);
continue ProcessNextSwitch;
}
}
/**********************
* (5) INVALID SWITCH *
**********************/
/* MUST BE LAST BLOCK */
err = "\"" + sw + "\" is not a valid switch for command \"" + command + "\".";
break CheckArguments;
}
if (ZipSnap.all && (ZipSnap.timeTolerance > 0))
{
err = "Switches --all and --time cannot be used together.";
break CheckArguments;
}
if (ZipSnap.all && ZipSnap.useCrc)
{
err = "Switches --all and --crc cannot be used together.";
break CheckArguments;
}
if (ZipSnap.all && ZipSnap.forceAdd)
{
err = "Switches --all and --forceadd cannot be used together.";
break CheckArguments;
}
if (swZip + swJar + swList > 1)
{
err = "Only one of the three switches --zip, --jar, and --list can be used.";
break CheckArguments;
}
/* volume type */
if (swZip > 0)
ZipSnap.volumeType = ZipSnap.VolumeType.ZIP;
if (swJar > 0)
ZipSnap.volumeType = ZipSnap.VolumeType.JAR;
if (swList > 0)
ZipSnap.volumeType = ZipSnap.VolumeType.LIST;
}
while (false);
/* invalid command-line arguments encountered */
if (err != null)
ErrorWarningHandler.reportErrorAndExit(e