/**
* 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(err +
"\nTo display help, run ZipSnap without any command-line arguments.\n");
}
/**
* Add a snapshot of the current directory to the ZipSnap archive.
*/
private static void addSnapshotToArchive()
{
System.out.print("\nADD SNAPSHOT TO ARCHIVE" +
(ZipSnap.simulateOnly ? " (SIMULATION MODE)" : "") + "\n");
/* get current time */
final String timestamp = (new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.ENGLISH)).format(ZipSnap.currentTime);
/*******************************
* (1) CHECK CURRENT DIRECTORY *
*******************************/
try
{
/* get absolute and canonical pathname of the current directory */
ZipSnap.currentDir = (new File("")).getCanonicalFile();
}
catch (Exception e)
{
ErrorWarningHandler.reportErrorAndExit("Unable to get absolute pathname of the current directory:\n" +
ErrorWarningHandler.getExceptionMessage(e));
}
ZipSnap.currentDirName = FileIO.trimTrailingSeparator(ZipSnap.currentDir.getPath()) + File.separatorChar;
System.out.print("\nCurrent directory : \"" + ZipSnap.currentDirName + "\"");
/* check if current directory is valid */
if (!ZipSnap.currentDir.exists())
ErrorWarningHandler.reportErrorAndExit("The current directory \"" + ZipSnap.currentDirName + "\" does not exist.");
if (!ZipSnap.currentDir.isDirectory())
ErrorWarningHandler.reportErrorAndExit("The current directory \"" + ZipSnap.currentDirName + "\" is not a directory; could it be a file?");
if (!ZipSnap.currentDir.canRead())
ErrorWarningHandler.reportErrorAndExit("The current directory \"" + ZipSnap.currentDirName + "\" cannot be read.");
/***************************************
* (2) CHECK ZIPSNAP ARCHIVE DIRECTORY *
***************************************/
try
{
/* get absolute and canonical pathname of the archive directory */
ZipSnap.archiveDir = (new File(ZipSnap.archive)).getCanonicalFile();
}
catch (Exception e)
{
ErrorWarningHandler.reportErrorAndExit("Unable to get absolute pathname of ZipSnap archive directory \"" +
ZipSnap.archive + "\":\n" + ErrorWarningHandler.getExceptionMessage(e));
}
ZipSnap.archiveDirName = FileIO.trimTrailingSeparator(ZipSnap.archiveDir.getPath()) + File.separatorChar;
System.out.print("\nZipSnap archive : \"" + ZipSnap.archiveDirName + "\"");
/* check that archive directory is not a subdirectory of the current directory */
if (ZipSnap.archiveDirName.startsWith(ZipSnap.currentDirName))
ErrorWarningHandler.reportWarning("The ZipSnap archive directory \"" + ZipSnap.archiveDirName +
"\" should not be a subdirectory of the current directory \"" + ZipSnap.currentDirName + "\".");
/****************************************
* (3) READ CATALOG FOR ZIPSNAP ARCHIVE *
****************************************/
/* File object representing the latest catalog (txt/txt.zip/txt.jar) */
File catalogFile = null;
/* check if ZipSnap archive directory is valid */
if (ZipSnap.archiveDir.exists())
{
/* archive directory already exists */
if (!ZipSnap.archiveDir.isDirectory())
ErrorWarningHandler.reportErrorAndExit("The specified ZipSnap archive directory \"" + ZipSnap.archiveDirName + "\" is not a directory; could it be a file?");
if (!ZipSnap.archiveDir.canRead())
ErrorWarningHandler.reportErrorAndExit("The specified ZipSnap archive directory \"" + ZipSnap.archiveDirName + "\" cannot be read.");
/* get the latest catalog in the ZipSnap archive directory */
catalogFile = getLatestCatalog();
}
/* catalog for the ZipSnap archive */
Catalog catalog = null;
if (catalogFile == null)
{
/* no existing catalog found; this is a new archive */
System.out.print("\n - Latest catalog : (none found)");
System.out.flush();
/* initialize a new empty catalog */
catalog = new Catalog();
}
else
{
/* found an existing catalog; proceed to read it */
System.out.print("\n - Latest catalog : \"" + catalogFile.getName() + "\"");
System.out.flush();
/* read existing catalog file */
catalog = new Catalog(catalogFile);
}
/* display this snapshot number and timestamp */
ZipSnap.snapshotIndex = catalog.snapshots.size();
System.out.print("\n - This snapshot : " + timestamp + "." + (ZipSnap.snapshotIndex + 1));
System.out.flush();
/* display filter, if defined */
if (ZipSnap.filterPattern != null)
{
if (ZipSnap.filterType == ZipSnap.FilterType.GLOB)
{
System.out.print("\nGlob filter : \"" + ZipSnap.filterString + "\"");
}
else if (ZipSnap.filterType == ZipSnap.FilterType.REGEX)
{
System.out.print("\nRegex filter : \"" + ZipSnap.filterString + "\"");
}
if (ZipSnap.filterFullPathname)
{
System.out.print(" (match full relative pathname)");
}
else
{
System.out.print(" (match name only)");
}
System.out.flush();
}
/***************************************
* (4) SCAN CURRENT DIRECTORY CONTENTS *
***************************************/
/* get filtered contents of the current directory */
/* (currentDirContents is sorted in lexicographic order of the name field) */
final List<FileUnit> currentDirContents = getCurrentDirContents();
if (currentDirContents.isEmpty())
ErrorWarningHandler.reportWarning("The current directory is empty.");
/***********************************************************************************
* (5) DETERMINE NEW/MODIFIED FILES/DIRECTORIES TO BE ADDED TO THE ZIPSNAP ARCHIVE *
***********************************************************************************/
/* determine new/modified files/directories to be added to the ZipSnap archive */
final List<FileUnit> addFiles = new ArrayList<FileUnit>();
if (ZipSnap.all || (catalogFile == null))
{
/* add all files/directories without matching previously archived files/directories */
System.out.print("\n\nAdding all " + currentDirContents.size() +
" files/directories in this snapshot.");
System.out.flush();
addFiles.addAll(currentDirContents);
}
else
{
/* match files/directories in the current directory */
/* against previously archived files/directories */
System.out.print("\n\nMatching current directory contents against the catalog..." +
(ZipSnap.useCrc ? "\n(file checksum computations can be time-intensive)" : ""));
System.out.flush();
/* count number of matched files/directories that belong to the last snapshot */
int numLastSnapshotFilesMatched = 0;
for (FileUnit u : currentDirContents)
{
/* if match found, update snapshot number of cataloged item */
/* if match not found, add item to new/modified list of items */
/* search for a file/directory in the catalog with the same name */
final int i = Collections.binarySearch(catalog.files, u);
if (i < 0)
{
/* this file/directory is not cataloged, so we add it to the new volume */
addFiles.add(u);
}
else
{
/* this file/directory is cataloged, so we look for the best match */
/* index of the best match */
int bestMatch = -1;
/* latest snapshot index of the best match */
int bestMatchLatestSnapshot = -1;
/* match files upwards */
MatchNextFileUp:
for (int j = i - 1; j >= 0; j--)
{
final FileUnit c = catalog.files.get(j);
if (!u.name.equals(c.name))
break MatchNextFileUp;
/* name is already matched, next match size, time, and (checksum) */
if ((u.size != c.size) ||
(Math.abs(u.time - c.time) > ZipSnap.timeTolerance) ||
(ZipSnap.useCrc && (u.getCrc() != c.getCrc())))
{
continue MatchNextFileUp;
}
/* match found: proceed to compare latest snapshot indices, if necessary */
final int latestSnapshot = Collections.max(c.snapshots);
if ((bestMatch < 0) ||
(latestSnapshot > bestMatchLatestSnapshot))
{
bestMatch = j;
bestMatchLatestSnapshot = latestSnapshot;
}
}
/* match files downwards */
MatchNextFileDown:
for (int j = i; j < catalog.files.size(); j++)
{
final FileUnit c = catalog.files.get(j);
if (!u.name.equals(c.name))
break MatchNextFileDown;
/* name is already matched, next match size, time, and (checksum) */
if ((u.size != c.size) ||
(Math.abs(u.time - c.time) > ZipSnap.timeTolerance) ||
(ZipSnap.useCrc && (u.getCrc() != c.getCrc())))
{
continue MatchNextFileDown;
}
/* match found: proceed to compare latest snapshot indices, if necessary */
final int latestSnapshot = Collections.max(c.snapshots);
if ((bestMatch < 0) ||
(latestSnapshot >= bestMatchLatestSnapshot))
{
bestMatch = j;
bestMatchLatestSnapshot = latestSnapshot;
}
}
/* add best match */
if (bestMatch >= 0)
{
/* found a valid match */
final List<Integer> snapshots = catalog.files.get(bestMatch).snapshots;
/* add snapshot to previously archived file/directory */
snapshots.add(ZipSnap.snapshotIndex);
/* check if this matched file/directory belongs to the last snapshot */
if (snapshots.contains(ZipSnap.snapshotIndex - 1))
numLastSnapshotFilesMatched++;
}
else
{
/* no valid match found; so we add this file/directory to the new volume */
addFiles.add(u);
}
}
}
System.out.print("\n\nThis snapshot contains " + (currentDirContents.size() - addFiles.size()) +
" previously archived files/directories,\nand " + addFiles.size() +
" new/modified files/directories.");
System.out.flush();
/* check if this snapshot is identical to the last snapshot */
if (addFiles.isEmpty())
{
/* get size of (unfiltered) last snapshot */
int numLastSnapshotFiles = 0;
for (FileUnit u : catalog.files)
{
if (u.snapshots.contains(ZipSnap.snapshotIndex - 1))
numLastSnapshotFiles++;
}
if (numLastSnapshotFilesMatched == numLastSnapshotFiles)
{
/* this snapshot is identical to the (unfiltered) last snapshot */
if (ZipSnap.forceAdd)
{
System.out.print("\n\nThis snapshot is identical to the last snapshot, but will still be added.");
}
else
{
System.out.print("\n\nThis snapshot is identical to the last snapshot, and will not be added.");
return;
}
}
}
}
/**********************************************************
* (6) ADD NEW/MODIFIED FILES/DIRECTORIES TO A NEW VOLUME *
**********************************************************/
/* add new/modified files/directories to a new volume */
final List<FileUnit> addedFiles = new ArrayList<FileUnit>();
/* create new volume to contain new/modified files */
final String volumeName = ZipSnap.archiveDir.getName() + "." + timestamp +
"." + (ZipSnap.snapshotIndex + 1);
if (ZipSnap.volumeType == ZipSnap.VolumeType.ZIP)
{
CompressionIO.zipFiles(new File(ZipSnap.archiveDir, volumeName + ".zip"), addFiles, addedFiles);
}
else if (ZipSnap.volumeType == ZipSnap.VolumeType.JAR)
{
CompressionIO.jarFiles(new File(ZipSnap.archiveDir, volumeName + ".jar"), addFiles, addedFiles);
}
else if (ZipSnap.volumeType == ZipSnap.VolumeType.LIST)
{
listFiles(new File(ZipSnap.archiveDir, volumeName + ".list"), addFiles, addedFiles);
}
/******************************************
* (7) UPDATE CATALOG FOR ZIPSNAP ARCHIVE *
******************************************/
/* add new snapshot to the catalog */
final SnapshotUnit s = new SnapshotUnit(timestamp);
catalog.snapshots.add(ZipSnap.snapshotIndex, s);
/* add new/modified files/directories to the catalog */
for (FileUnit u : addedFiles)
{
/* add this snapshot index to the newly archived file/directory */
u.snapshots.add(ZipSnap.snapshotIndex);
/* add newly archived file/directory to catalog */
catalog.files.add(u);
}
/* sort files/directories in catalog */
catalog.sort();
/*********************************************
* (8) WRITE NEW CATALOG FOR ZIPSNAP ARCHIVE *
*********************************************/
/* write new catalog */
final String newCatalogName = ZipSnap.archiveDir.getName() + "." + timestamp +
"." + (ZipSnap.snapshotIndex + 1);
if (ZipSnap.volumeType == ZipSnap.VolumeType.ZIP)
{
catalog.writeToFile(new File(ZipSnap.archiveDir, newCatalogName + ".txt.zip"));
}
else if (ZipSnap.volumeType == ZipSnap.VolumeType.JAR)
{
catalog.writeToFile(new File(ZipSnap.archiveDir, newCatalogName + ".txt.jar"));
}
else if (ZipSnap.volumeType == ZipSnap.VolumeType.LIST)
{
catalog.writeToFile(new File(ZipSnap.archiveDir, newCatalogName + ".txt"));
}
}
/**
* Restore a snapshot to the current directory from the ZipSnap archive.
*/
private static void restoreSnapshotFromArchive()
{
System.out.print("\nRESTORE SNAPSHOT FROM ARCHIVE" +
(ZipSnap.simulateOnly ? " (SIMULATION MODE)" : "") + "\n");
/*******************************
* (1) CHECK CURRENT DIRECTORY *
*******************************/
try
{
/* get absolute, canonical path of the current directory */
ZipSnap.currentDir = (new File("")).getCanonicalFile();
}
catch (Exception e)
{
ErrorWarningHandler.reportErrorAndExit("Unable to get absolute pathname of the current directory:\n" +
ErrorWarningHandler.getExceptionMessage(e));
}
ZipSnap.currentDirName = FileIO.trimTrailingSeparator(ZipSnap.currentDir.getPath()) + File.separatorChar;
System.out.print("\nCurrent directory : \"" + ZipSnap.currentDirName + "\"");
/* check if current directory is valid */
if (!ZipSnap.currentDir.exists())
ErrorWarningHandler.reportErrorAndExit("The current directory \"" + ZipSnap.currentDirName + "\" does not exist.");
if (!ZipSnap.currentDir.isDirectory())
ErrorWarningHandler.reportErrorAndExit("The current directory \"" + ZipSnap.currentDirName + "\" is not a directory; could it be a file?");
if (!ZipSnap.currentDir.canRead())
ErrorWarningHandler.reportErrorAndExit("The current directory \"" + ZipSnap.currentDirName + "\" cannot be read.");
/***************************************
* (2) CHECK ZIPSNAP ARCHIVE DIRECTORY *
***************************************/
try
{
/* get absolute, canonical path of the archive directory */
ZipSnap.archiveDir = (new File(ZipSnap.archive)).getCanonicalFile();
}
catch (Exception e)
{
ErrorWarningHandler.reportErrorAndExit("Unable to get absolute pathname of ZipSnap archive directory \"" +
ZipSnap.archive + "\":\n" + ErrorWarningHandler.getExceptionMessage(e));
}
ZipSnap.archiveDirName = FileIO.trimTrailingSeparator(ZipSnap.archiveDir.getPath()) + File.separatorChar;
System.out.print("\nZipSnap archive : \"" + ZipSnap.archiveDirName + "\"");
/* check if ZipSnap archive directory is valid */
if (!ZipSnap.archiveDir.exists())
ErrorWarningHandler.reportErrorAndExit("The specified ZipSnap archive directory \"" + ZipSnap.archiveDirName + "\" does not exist.");
if (!ZipSnap.archiveDir.isDirectory())
ErrorWarningHandler.reportErrorAndExit("The specified ZipSnap archive directory \"" + ZipSnap.archiveDirName + "\" is not a directory; could it be a file?");
if (!ZipSnap.archiveDir.canRead())
ErrorWarningHandler.reportErrorAndExit("The specified ZipSnap archive directory \"" + ZipSnap.archiveDirName + "\" cannot be read.");
/* check that archive directory is not a subdirectory of the current directory */
if (ZipSnap.archiveDirName.startsWith(ZipSnap.currentDirName))
ErrorWarningHandler.reportWarning("The ZipSnap archive directory \"" + ZipSnap.archiveDirName +
"\" should not be a subdirectory of the current directory \"" +
ZipSnap.currentDirName + "\".");
/* display additional search paths for volume files, if specified */
if (!ZipSnap.searchPaths.isEmpty())
{
System.out.print("\n - Additional search paths for volume files:");
int i = 0;
for (File dir : ZipSnap.searchPaths)
System.out.print("\n [" + (++i) + "] \"" + dir.getPath() + "\"");
}
/* add archive directory to search paths for volume files */
ZipSnap.searchPaths.add(ZipSnap.archiveDir);
/****************************************
* (3) READ CATALOG FOR ZIPSNAP ARCHIVE *
****************************************/
/* get the latest catalog in the ZipSnap archive directory */
final File catalogFile = getLatestCatalog();
/* check if a catalog is found */
if (catalogFile == null)
ErrorWarningHandler.reportErrorAndExit("Unable to find a catalog in the specified ZipSnap archive directory \"" + ZipSnap.archiveDirName + "\".");
/* found a catalog; proceed to read it */
System.out.print("\n - Latest catalog : \"" + catalogFile.getName() + "\"");
System.out.flush();
/* read catalog for the ZipSnap archive */
final Catalog catalog = new Catalog(catalogFile);
/* check that catalog contains at least one snapshot */
if (catalog.snapshots.isEmpty())
ErrorWarningHandler.reportErrorAndExit("No snapshots found in the catalog \"" + catalogFile.getName() + "\" for this ZipSnap archive.");
/* pick the latest snapshot, if not specified */
if (ZipSnap.snapshotIndex < 0)
ZipSnap.snapshotIndex = catalog.snapshots.size() - 1;
/* display specified snapshot number and timestamp */
System.out.print("\n - Restore snapshot: " +
catalog.snapshots.get(ZipSnap.snapshotIndex).time + "." +
(ZipSnap.snapshotIndex + 1));
System.out.flush();
/* display filter, if defined */
if (ZipSnap.filterPattern != null)
{
if (ZipSnap.filterType == ZipSnap.FilterType.GLOB)
{
System.out.print("\nGlob filter : \"" + ZipSnap.filterString + "\"");
}
else if (ZipSnap.filterType == ZipSnap.FilterType.REGEX)
{
System.out.print("\nRegex filter : \"" + ZipSnap.filterString + "\"");
}
if (ZipSnap.filterFullPathname)
{
System.out.print(" (match full relative pathname)");
}
else
{
System.out.print(" (match name only)");
}
System.out.flush();
}
/* get filtered files/directories that belong to the specified snapshot */
final List<FileUnit> snapshotFiles = new ArrayList<FileUnit>();
for (FileUnit u : catalog.files)
{
if (u.snapshots.contains(ZipSnap.snapshotIndex) && u.matchesFilter())
snapshotFiles.add(u);
}
System.out.print("\n\nThis snapshot contains " + snapshotFiles.size() + " files/directories.");
System.out.flush();
/***************************************
* (4) SCAN CURRENT DIRECTORY CONTENTS *
***************************************/
/* get filtered contents of the current directory */
/* (currentDirContents is sorted in lexicographic order of the name field) */
final List<FileUnit> currentDirContents = getCurrentDirContents();
/****************************************************************************
* (5) DETERMINE FILES/DIRECTORIES TO BE DELETED FROM THE CURRENT DIRECTORY *
****************************************************************************/
/* determine files/directories to be deleted from the current directory */
/* (initialized to ALL files/directories in the current directory) */
final List<FileUnit> deleteFiles = new ArrayList<FileUnit>(currentDirContents);
/* unmark file/directory to be deleted from the current directory */
deleteFiles.removeAll(snapshotFiles);
/* reverse sort the files/directories to be deleted */
Collections.reverse(deleteFiles);
/*****************************************************************************
* (6) DETERMINE FILES/DIRECTORIES TO BE EXTRACTED FROM THE ZIPSNAP ARCHIVE, *
* AND THE VOLUMES FROM WHICH TO EXTRACT THEM *
*****************************************************************************/
/* determine files/directories to be extracted from the ZipSnap archive */
final List<List<FileUnit>> extractFiles =
new ArrayList<List<FileUnit>>(ZipSnap.snapshotIndex + 1);
int numExtractFiles = 0;
for (int i = 0; i <= ZipSnap.snapshotIndex; i++)
extractFiles.add(i, new ArrayList<FileUnit>());
if (ZipSnap.all)
{
/* extract all files/directories that belong to this snapshot, */
/* without matching files/directories against the current directory */
for (FileUnit u : snapshotFiles)
{
extractFiles.get(u.snapshots.get(0)).add(u);
numExtractFiles++;
}
}
else
{
/* match files/directories of this snapshot against */
/* the files/directories in the current directory */
System.out.print("\n\nMatching snapshot files/directories against current directory contents..." +
(ZipSnap.useCrc ? "\n(file checksum computations can be time-intensive)" : ""));
System.out.flush();
for (FileUnit u : snapshotFiles)
{
/* if match not found, mark the item for extraction */
/* file/directory from the current directory */
final int i = Collections.binarySearch(currentDirContents, u);
final FileUnit c = (i >= 0) ? currentDirContents.get(i) : null;
if ((c == null) ||
(u.size != c.size) ||
(Math.abs(u.time - c.time) > ZipSnap.timeTolerance) ||
(ZipSnap.useCrc && (u.getCrc() != c.getCrc())))
{
/* no matching snapshot item found in current directory, */
/* so we mark file/directory for extraction */
extractFiles.get(u.snapshots.get(0)).add(u);
numExtractFiles++;
}
}
}
System.out.print("\n\nZipSnap will extract " + numExtractFiles +
" files/directories from the archive,\nand delete " +
deleteFiles.size() + " files/directories from the current directory.");
System.out.flush();
/***************************************************************
* (7) EXTRACT FILES/DIRECTORIES FROM THEIR RESPECTIVE VOLUMES *
***************************************************************/
/* candidate volume file extensions */
final String volumeFileExtensions[] = {".zip", ".jar"};
/* successfully extracted files/directories */
final List<FileUnit> extractedFiles = new ArrayList<FileUnit>();
/* extract files/directories from their respective volumes */
int numExtractedFiles = 0;
ExtractNextVolume:
for (int index = 0; index <= ZipSnap.snapshotIndex; index++)
{
final List<FileUnit> extractFilesVolume = extractFiles.get(index);
if (extractFilesVolume.isEmpty())
continue ExtractNextVolume;
final String volumeName = ZipSnap.archiveDir.getName() + "." +
catalog.snapshots.get(index).time + "." + (index + 1);
if (ZipSnap.volumeType == ZipSnap.VolumeType.LIST)
{
/* just list the files/directories to be extracted */
final File listFile = new File(ZipSnap.currentDir, volumeName + ".list");
numExtractedFiles += listFiles(listFile, extractFilesVolume, extractedFiles);
continue ExtractNextVolume;
}
/* select the volume file with the latest last-modified time */
File volumeFile = null;
for (File dir : ZipSnap.searchPaths)
{
for (String ext : volumeFileExtensions)
{
final File f = new File(dir, volumeName + ext);
if (f.exists() && !f.isDirectory() &&
((volumeFile == null) ||
(f.lastModified() > volumeFile.lastModified())))
{
volumeFile = f;
}
}
}
/* extract files from the selected volume */
if (volumeFile == null)
{
ErrorWarningHandler.reportWarning("Unable to find volume \"" + volumeName +
".{zip,jar}\";\nthe " + extractFilesVolume.size() +
" files/directories from this volume will not be extracted.");
continue ExtractNextVolume;
}
if (volumeFile.getName().endsWith(".zip"))
{
numExtractedFiles += CompressionIO.unZipFiles(volumeFile, extractFilesVolume, extractedFiles);
}
else if (volumeFile.getName().endsWith(".jar"))
{
numExtractedFiles += CompressionIO.unJarFiles(volumeFile, extractFilesVolume, extractedFiles);
}
}
if (numExtractFiles > 0)
{
if (ZipSnap.volumeType == ZipSnap.VolumeType.LIST)
{
System.out.print("\n\nZipSnap has listed " + numExtractedFiles +
" out of " + numExtractFiles + " files/directories from the archive.");
}
else
{
System.out.print("\n\nZipSnap has extracted " + numExtractedFiles +
" out of " + numExtractFiles + " files/directories from the archive.");
}
System.out.flush();
}
/*********************************************************
* (8) DELETE FILES/DIRECTORIES IN THE CURRENT DIRECTORY *
*********************************************************/
/* successfully deleted files/directories */
final List<FileUnit> deletedFiles = new ArrayList<FileUnit>();
/* delete files/directories in the current directory */
if (!deleteFiles.isEmpty())
deleteFiles(deleteFiles, deletedFiles);
/* stop here if simulating only */
if (ZipSnap.simulateOnly)
return;
/*************************************************************
* (9) RESTORE DIRECTORY TIMESTAMPS IN THE CURRENT DIRECTORY *
*************************************************************/
/* updated current directory contents */
final List<FileUnit> updatedCurrentDirContents = new ArrayList<FileUnit>(currentDirContents);
/* add extracted files/directories */
for (FileUnit u : extractedFiles)
{
final int i = Collections.binarySearch(currentDirContents, u);
if (i >= 0)
{
/* overwrite existing file/directory */
updatedCurrentDirContents.set(i, u);
}
else
{
/* add extracted file/directory */
updatedCurrentDirContents.add(u);
}
}
/* sort in lexicographic order of the name field */
Collections.sort(updatedCurrentDirContents);
/* remove deleted files/directories */
updatedCurrentDirContents.removeAll(deletedFiles);
/* determine directories with timestamps to be restored */
final List<FileUnit> restoreDirs = new ArrayList<FileUnit>();
for (FileUnit u : updatedCurrentDirContents)
{
if (u.isDirectory && u.file.isDirectory() &&
(u.file.lastModified() != u.time))
{
/* get canonical pathname of directory */
String pathname = null;
try
{
pathname = u.file.getCanonicalPath();
}
catch (Exception e)
{
pathname = null;
}
if ((pathname != null) && pathname.equals(u.file.getPath()))
restoreDirs.add(u);
}
}
/* reverse sort the directories to be restored */
Collections.reverse(restoreDirs);
/* directories with timestamps successfully restored */
final List<FileUnit> restoredDirs = new ArrayList<FileUnit>();
/* restore directory timestamps in the current directory */
if (!restoreDirs.isEmpty())
restoreDirTime(restoreDirs, restoredDirs);
}
/**
* Display information about the ZipSnap archive.
*/
private static void displayArchiveInfo()
{
System.out.print("\nDISPLAY ARCHIVE INFORMATION\n");
/***************************************
* (1) CHECK ZIPSNAP ARCHIVE DIRECTORY *
***************************************/
try
{
/* get absolute, canonical path of the archive directory */
ZipSnap.archiveDir = (new File(ZipSnap.archive)).getCanonicalFile();
}
catch (Exception e)
{
ErrorWarningHandler.reportErrorAndExit("Unable to get absolute pathname of ZipSnap archive directory \"" +
ZipSnap.archive + "\":\n" + ErrorWarningHandler.getExceptionMessage(e));
}
ZipSnap.archiveDirName = FileIO.trimTrailingSeparator(ZipSnap.archiveDir.getPath()) + File.separatorChar;
System.out.print("\nZipSnap archive : \"" + ZipSnap.archiveDirName + "\"");
/* check if ZipSnap archive directory is valid */
if (!ZipSnap.archiveDir.exists())
ErrorWarningHandler.reportErrorAndExit("The specified ZipSnap archive directory \"" + ZipSnap.archiveDirName + "\" does not exist.");
if (!ZipSnap.archiveDir.isDirectory())
ErrorWarningHandler.reportErrorAndExit("The specified ZipSnap archive directory \"" + ZipSnap.archiveDirName + "\" is not a directory; could it be a file?");
if (!ZipSnap.archiveDir.canRead())
ErrorWarningHandler.reportErrorAndExit("The specified ZipSnap archive directory \"" + ZipSnap.archiveDirName + "\" cannot be read.");
/****************************************
* (2) READ CATALOG FOR ZIPSNAP ARCHIVE *
****************************************/
/* get the latest catalog in the ZipSnap archive directory */
final File catalogFile = getLatestCatalog();
/* check if a catalog is found */
if (catalogFile == null)
ErrorWarningHandler.reportErrorAndExit("Unable to find a catalog in the specified ZipSnap archive directory \"" + ZipSnap.archiveDirName + "\".");
/* found a catalog; proceed to read it */
System.out.print("\n - Latest catalog : \"" + catalogFile.getName() + "\"");
System.out.flush();
/* read catalog for the ZipSnap archive */
final Catalog catalog = new Catalog(catalogFile);
/* check that catalog contains at least one snapshot */
if (catalog.snapshots.isEmpty())
ErrorWarningHandler.reportErrorAndExit("No snapshots found in the catalog \"" + catalogFile.getName() + "\" for this ZipSnap archive.");
/* display filter, if defined */
if (ZipSnap.filterPattern != null)
{
if (ZipSnap.filterType == ZipSnap.FilterType.GLOB)
{
System.out.print("\nGlob filter : \"" + ZipSnap.filterString + "\"");
}
else if (ZipSnap.filterType == ZipSnap.FilterType.REGEX)
{
System.out.print("\nRegex filter : \"" + ZipSnap.filterString + "\"");
}
if (ZipSnap.filterFullPathname)
{
System.out.print(" (match full relative pathname)");
}
else
{
System.out.print(" (match name only)");
}
System.out.flush();
}
/**************************************************
* (3) COUNT FILES/DIRECTORIES FROM EACH SNAPSHOT *
**************************************************/
/* total number of files/directories */
int numSnapshotFiles[] = new int[catalog.snapshots.size()];
Arrays.fill(numSnapshotFiles, 0);
/* number of new/modified files/directories */
int numSnapshotFilesAdded[] = new int[catalog.snapshots.size()];
Arrays.fill(numSnapshotFilesAdded, 0);
/* count filtered files/directories from each snapshot */
for (FileUnit u : catalog.files)
{
if (u.matchesFilter())
{
numSnapshotFilesAdded[u.snapshots.get(0)]++;
for (int i : u.snapshots)
numSnapshotFiles[i]++;
}
}
/************************************
* (4) DISPLAY SNAPSHOT INFORMATION *
************************************/
System.out.print("\n\nThis ZipSnap archive contains " + catalog.snapshots.size() + " snapshots and " +
catalog.files.size() + " files/directories:\n" +
"\nSnapshot Snapshot No. of new/modified Total no. of" +
"\n Number Time files/directories added files/directories" +
"\n-------- ----------------- ----------------------- -----------------");
for (int i = 0; i < catalog.snapshots.size(); i++)
{
System.out.print("\n" +
StringManipulator.centerJustify(i + 1, 8) + " " +
StringManipulator.centerJustify(catalog.snapshots.get(i).time, 17) + " " +
StringManipulator.centerJustify(numSnapshotFilesAdded[i], 23) + " " +
StringManipulator.centerJustify(numSnapshotFiles[i], 17));
}
}
/**
* Get the latest catalog in the archive.
*
* @return
* File corresponding to the latest catalog;
* null if no catalog is found
*/
private static File getLatestCatalog()
{
/* get contents of the ZipSnap archive directory */
final File dirContents[] = ZipSnap.archiveDir.listFiles();
if (dirContents == null)
ErrorWarningHandler.reportErrorAndExit("Unable to get contents of the specified ZipSnap archive directory \"" + ZipSnap.archiveDirName + "\".");
/* create regex pattern to match files: */
/* "archiveName.yyyyMMdd-HHmmss.n.{txt,txt.zip,txt.jar}" */
/* (capture group 1 = n, i.e. the snapshot number) */
final Pattern catalogNamePattern = Pattern.compile(
Pattern.quote(ZipSnap.archiveDir.getName()) +
"\\.[0-9]{8}\\-[0-9]{6}\\.([1-9][0-9]*)\\.(?:txt|txt\\.zip|txt\\.jar)");
/* select the latest catalog file using the following sequence of "keys": */
/* 1. Largest snapshot number (value of n) */
/* 2. Latest last-modified time */
final NavigableMap<Integer,File> candidateCatalogs = new TreeMap<Integer,File>();
ProcessNextFile:
for (File f : dirContents)
{
if (f.isFile())
{
final Matcher m = catalogNamePattern.matcher(f.getName());
if (m.matches())
{
/* found a match, i.e. this file is a catalog; get value of n */
final int n = Integer.parseInt(m.group(1));
final File c = candidateCatalogs.get(n);
if ((c == null) ||
(f.lastModified() > c.lastModified()))
{
/* add this catalog file as a candidate */
candidateCatalogs.put(n, f);
}
}
}
}
/* check if there are any candidate catalog files */
if (candidateCatalogs.isEmpty())
return null;
/* return catalog file corresponding to highest snapshot number */
return candidateCatalogs.get(candidateCatalogs.lastKey());
}
/**
* Get filtered contents (files and directories) of the current directory.
* The returned FileUnits are sorted in lexicographic order of their name field.
*
* @return
* FileUnits corresponding to the filtered contents (files and
* directories) of the current directory
*/
private static List<FileUnit> getCurrentDirContents()
{
/* get contents (files and directories) of the current directory */
System.out.print("\n\nScanning current directory contents...");
System.out.flush();
/* return value */
final List<FileUnit> currentDirContents = new ArrayList<FileUnit>();
/* get contents of the current directory recursively */
getDirContents(ZipSnap.currentDir, currentDirContents);
/* sort in lexicographic order of the name field */
Collections.sort(currentDirContents);
System.out.print("\nThe current directory contains " + currentDirContents.size() + " files/directories.");
System.out.flush();
return currentDirContents;
}
/**
* Get filtered files and subdirectories in the specified directory.
* This method is called recursively if necessary.
*
* @param dir
* Subdirectory to be scanned
* @param dirContents
* Directory contents (to be populated)
*/
private static void getDirContents(
final File dir,
final List<FileUnit> dirContents)
{
/* files and subdirectories of the specified directory */
final List<File> files = new ArrayList<File>();
final List<File> dirs = new ArrayList<File>();
/* get directory contents */
final FileIO.FileIOResult result = FileIO.getDirContents(dir, files, dirs);
if (!result.success)
{
ErrorWarningHandler.reportWarning("Unable to get contents of directory \"" +
FileIO.trimTrailingSeparator(dir.getPath()) + File.separatorChar + "\":\n" +
result.errorMessage + "\nThis directory will be ignored.");
return;
}
final int currentDirNameLen = ZipSnap.currentDirName.length();
/**********************************************
* (1) ADD THIS DIRECTORY TO THE RETURN VALUE *
**********************************************/
if (!dir.equals(ZipSnap.currentDir))
{
/* native and absolute pathname */
final String absoluteNativeName = FileIO.trimTrailingSeparator(dir.getPath()) + File.separatorChar;
if (absoluteNativeName.startsWith(ZipSnap.currentDirName))
{
final FileUnit u = new FileUnit(
absoluteNativeName.substring(currentDirNameLen), /* native and relative pathname of directory */
dir, /* File object representing this directory */
true); /* this FileUnit represents a directory */
/* apply filter */
if (u.matchesFilter())
{
/* neutral and relative pathname of directory */
u.name = FileIO.nativeToNeutral(u.nativeName);
/* size of directory in bytes (size of directory is fixed at zero bytes) */
u.size = 0L;
/* last-modified time in milliseconds since the epoch (00:00:00 GMT, January 1, 1970) */
u.time = dir.lastModified();
dirContents.add(u);
}
}
else
{
ErrorWarningHandler.reportWarning("The absolute pathname of directory \"" + absoluteNativeName +
"\" does not begin with the absolute pathname of the current directory \"" +
ZipSnap.currentDirName + "\".\nThis directory will be ignored.");
}
}
/*******************************************************
* (2) ADD FILES OF THIS DIRECTORY TO THE RETURN VALUE *
*******************************************************/
ProcessNextFile:
for (File f: files)
{
/* native and absolute pathname */
final String absoluteNativeName = f.getPath();
if (absoluteNativeName.startsWith(ZipSnap.currentDirName))
{
final FileUnit u = new FileUnit(
absoluteNativeName.substring(currentDirNameLen), /* native and relative pathname of file */
f, /* File object representing this file */
false); /* this FileUnit represents a file */
/* apply filter */
if (u.matchesFilter())
{
/* neutral and relative pathname of file */
u.name = FileIO.nativeToNeutral(u.nativeName);
/* size of file in bytes */
u.size = f.length();
/* last-modified time in milliseconds since the epoch (00:00:00 GMT, January 1, 1970) */
u.time = f.lastModified();
dirContents.add(u);
}
}
else
{
ErrorWarningHandler.reportWarning("The absolute pathname of file \"" + absoluteNativeName +
"\" does not begin with the absolute pathname of the current directory \"" +
ZipSnap.currentDirName + "\".\nThis file will be ignored.");
}
}
/***********************************
* (3) RECURSE INTO SUBDIRECTORIES *
***********************************/
for (File d : dirs)
getDirContents(d, dirContents);
}
/**
* Return the Java regex Pattern corresponding to the glob filter
* string ZipSnap.filterString.
*
* @return
* Corresponding Java Regex Pattern
*/
private static Pattern getGlobFilterPattern()
{
/* regex expression to be compiled into a Java regex Pattern */
final StringBuilder t = new StringBuilder();
/* Stack to keep track of the parser mode: */
/* "--" : Base mode (first on the stack) */
/* "[]" : Square brackets mode "[...]" */
/* "{}" : Curly braces mode "{...}" */
final Stack<String> parserMode = new Stack<String>();
parserMode.push("--"); /* base mode */
final StringBuilder remainingChars = new StringBuilder(ZipSnap.filterString);
/* parse each character of the fitler string */
ParseNextChar:
while (remainingChars.length() > 0)
{
/* read one character */
char c = remainingChars.charAt(0);
remainingChars.deleteCharAt(0);
/***********************
* (1) ESCAPE SEQUENCE *
***********************/
/* handle escape sequence in the filter string (MUST BE FIRST BLOCK) */
if (c == '\\')
{
if (remainingChars.length() == 0)
{
/* no characters left, so treat '\' as literal char */
t.append(Pattern.quote(c + ""));
continue ParseNextChar;
}
/* read next character */
c = remainingChars.charAt(0);
remainingChars.deleteCharAt(0);
final String s = c + "";
if ("--".equals(parserMode.peek()) && !"\\[]{}?*".contains(s))
{
ErrorWarningHandler.reportErrorAndExit("Invalid escape sequence \"\\" +
c + "\" near position " +
(ZipSnap.filterString.length() - remainingChars.length()) +
" of glob filter string.");
}
else if ("[]".equals(parserMode.peek()) && !"\\[]{}?*!-".contains(s))
{
ErrorWarningHandler.reportErrorAndExit("Invalid escape sequence \"\\" +
c + "\" near position " +
(ZipSnap.filterString.length() - remainingChars.length()) +
" of glob filter string.");
}
else if ("{}".equals(parserMode.peek()) && !"\\[]{}?*,".contains(s))
{
ErrorWarningHandler.reportErrorAndExit("Invalid escape sequence \"\\" +
c + "\" near position " +
(ZipSnap.filterString.length() - remainingChars.length()) +
" of glob filter string.");
}
t.append(Pattern.quote(s));
continue ParseNextChar;
}
/************************
* (2) GLOB PATTERN '*' *
************************/
/* handle glob pattern '*' */
if (c == '*')
{
/* create non-capturing group to match zero or more characters */
t.append(".*");
continue ParseNextChar;
}
/************************
* (3) GLOB PATTERN '?' *
************************/
/* handle glob pattern '?' */
if (c == '?')
{
/* create non-capturing group to match exactly one character */
t.append('.');
continue ParseNextChar;
}
/****************************
* (4) GLOB PATTERN "[...]" *
****************************/
/* handle glob pattern "[...]" */
/* opening square bracket '[' */
if (c == '[')
{
/* create non-capturing group to match exactly one character */
/* inside the sequence */
t.append('[');
parserMode.push("[]");
/* check for negation character '!' immediately after */
/* the opening bracket '[' */
if ((remainingChars.length() > 0) &&
(remainingChars.charAt(0) == '!'))
{
c = remainingChars.charAt(0);
remainingChars.deleteCharAt(0);
t.append('^');
}
continue ParseNextChar;
}
/* closing square bracket ']' */
if ("[]".equals(parserMode.peek()) && (c == ']'))
{
t.append(']');
parserMode.pop();
continue ParseNextChar;
}
/* character range '-' in "[...]" */
if ("[]".equals(parserMode.peek()) && (c == '-'))
{
t.append('-');
continue ParseNextChar;
}
/* end: handle glob pattern "[...]" */
/****************************
* (5) GLOB PATTERN "{...}" *
****************************/
/* handle glob pattern "{...}" */
/* opening curly brace '{' */
if (c == '{')
{
/* create non-capturing group to match one of the */
/* strings inside the sequence */
t.append("(?:(?:");
parserMode.push("{}");
continue ParseNextChar;
}
/* closing curly brace '}' */
if ("{}".equals(parserMode.peek()) && (c == '}'))
{
t.append("))");
parserMode.pop();
continue ParseNextChar;
}
/* comma between strings in "{...}" */
if ("{}".equals(parserMode.peek()) && (c == ','))
{
t.append(")|(?:");
continue ParseNextChar;
}
/* end: handle glob pattern "{...}" */
/*************************
* (6) LITERAL CHARACTER *
*************************/
/* convert literal character to a regex sequence (MUST BE LAST BLOCK) */
t.append(Pattern.quote(c + ""));
continue ParseNextChar;
}
/* done parsing all chars of the filter string */
/* check for mismatched [...] or {...} */
if ("[]".equals(parserMode.peek()))
ErrorWarningHandler.reportErrorAndExit("Cannot find matching closing square bracket ']' in glob filter string \"" + ZipSnap.filterString + "\".");
if ("{}".equals(parserMode.peek()))
ErrorWarningHandler.reportErrorAndExit("Cannot find matching closing curly brace '}' in glob filter string \"" + ZipSnap.filterString + "\".");
/* compile the Java regex expression */
Pattern filterPattern = null;
try
{
filterPattern = Pattern.compile(t.toString());
}
catch (Exception e)
{
ErrorWarningHandler.reportErrorAndExit("Unable to compile the specified glob filter string \"" +
ZipSnap.filterString + "\":\n" + ErrorWarningHandler.getExceptionMessage(e));
}
return filterPattern;
}
/**
* Return the Java regex Pattern corresponding to the regex filter
* string ZipSnap.filterString.
*
* @return
* Corresponding Java Regex Pattern
*/
private static Pattern getRegexFilterPattern()
{
Pattern filterPattern = null;
try
{
filterPattern = Pattern.compile(ZipSnap.filterString);
}
catch (Exception e)
{
ErrorWarningHandler.reportErrorAndExit("Unable to compile the specified regex filter string \"" +
ZipSnap.filterString + "\":\n" + ErrorWarningHandler.getExceptionMessage(e));
}
return filterPattern;
}
/**
* Write a plain text file containing a given list of files/directories.
*
* @param listFile
* Plain text file to be created
* @param files
* The files/directories to be listed in the file
* @param addedFiles
* The files/directories that were successfully listed in the file
* @return
* Number of files/directories successfully listed in the file
*/
private static int listFiles(
final File listFile,
final List<FileUnit> files,
final List<FileUnit> listedFiles)
{
final String listFileName = listFile.getName();
if (ZipSnap.simulateOnly)
{
return CompressionIO.simulateProcessFiles(
"\n\nSimulating creation of list \"" + listFileName + "\" of " + files.size() + " files/directories:",
"",
"No. of files/directories listed:",
true,
files,
listedFiles);
}
System.out.print("\n\nCreating list \"" + listFileName + "\" of " + files.size() + " files/directories:");
final File parentDir = listFile.getParentFile();
/* create parent directories if necessary */
if ((parentDir != null) && !parentDir.exists())
parentDir.mkdirs();
/* check that the parent directory of the list file exists */
if ((parentDir == null) || !parentDir.isDirectory())
ErrorWarningHandler.reportErrorAndExit("Unable to create list \"" + listFile.getPath() +
"\":\nThe parent directory of the file does not exist, and cannot be created.");
/* check if the list file already exists */
if (listFile.exists())
ErrorWarningHandler.reportErrorAndExit("Unable to create list \"" + listFile.getPath() +
"\":\nA " + (listFile.isDirectory() ? "directory" : "file") + " of the same name already exists.");
/* return value */
int numListedFiles = 0;
if (files.isEmpty())
{
/* create an empty file and return */
try
{
final FileOutputStream fos = new FileOutputStream(listFile);
fos.flush();
fos.close();
}
catch (Exception e)
{
ErrorWarningHandler.reportErrorAndExit("Unable to create list \"" + listFileName + "\":\n" +
ErrorWarningHandler.getExceptionMessage(e));
}
}
else
{
/* at least one file/directory to be listed */
PrintWriter pw = null;
try
{
pw = new PrintWriter(new BufferedWriter(new FileWriter(listFile)));
}
catch (Exception e)
{
ErrorWarningHandler.reportErrorAndExit("Unable to create list \"" + listFileName + "\":\n" +
ErrorWarningHandler.getExceptionMessage(e));
}
int i = 0;
AddNextFile:
for (FileUnit u : files)
{
i++;
System.out.print("\n [" + i + "] \"" + u.nativeName + "\"");
System.out.flush();
pw.println(u.nativeName);
/* file/directory successfully added to list file */
listedFiles.add(u);
numListedFiles++;
}
/* close the file */
try
{
pw.flush();
pw.close();
}
catch (Exception e)
{
ErrorWarningHandler.reportWarning("Unable to close list \"" + listFileName +
"\"\n(list may not be written successfully):\n" +
ErrorWarningHandler.getExceptionMessage(e));
}
}
/* print summary */
System.out.print(
"\n --------------------------------" +
"\n No. of files/directories listed: " +
numListedFiles + " out of " + files.size());
System.out.flush();
/* set timestamp of list */
final FileIO.FileIOResult result = FileIO.setFileTime(listFile, ZipSnap.currentTime.getTime());
if (!result.success)
ErrorWarningHandler.reportWarning("Unable to set last-modified time of list \"" + listFileName + "\".");
return numListedFiles;
}
/**
* Delete a given list of files/directories from the current directory.
*
* @param files
* The files/directories to be deleted
* (must be in reverse lexicographic order of their name field)
* @param deletedFiles
* The files/directories that were successfully deleted
* @return
* Number of files/directories successfully deleted
*/
private static int deleteFiles(
final List<FileUnit> files,
final List<FileUnit> deletedFiles)
{
if (ZipSnap.simulateOnly)
{
if (ZipSnap.defaultActionOnDelete == 'N')
{
return CompressionIO.simulateProcessFiles(
"\n\nSimulating deletion of " + files.size() + " files/directories from the current directory:",
"Skipping ",
"No. of files/directories deleted:",
false,
files,
deletedFiles);
}
else
{
return CompressionIO.simulateProcessFiles(
"\n\nSimulating deletion of " + files.size() + " files/directories from the current directory:",
"",
"No. of files/directories deleted:",
true,
files,
deletedFiles);
}
}
System.out.print("\n\nDeleting " + files.size() + " files/directories from the current directory:");
/* return value */
int numDeletedFiles = 0;
int i = 0;
/* delete files/directories in reverse lexicographic order so */
/* that subdirectories are deleted before parent directories */
DeleteNextFile:
for (FileUnit u : files)
{
i++;
if (!u.file.exists() ||
(u.isDirectory != u.file.isDirectory()))
{
System.out.print("\n [" + i + "] \"" + u.nativeName + "\" does not exist anymore.");
System.out.flush();
continue DeleteNextFile;
}
/* get canonical pathname of file/directory to be deleted */
String pathname = null;
try
{
pathname = u.file.getCanonicalPath();
}
catch (Exception e)
{
pathname = null;
}
if ((pathname == null) ||
!pathname.equals(u.file.getPath()))
{
System.out.print("\n [" + i + "] \"" + u.nativeName + "\" does not exist anymore.");
System.out.flush();
continue DeleteNextFile;
}
/* prompt user on action (default = false) */
boolean deleteFile = false;
if (ZipSnap.defaultActionOnDelete == 'Y')
{
System.out.print("\n [" + i + "] \"" + u.nativeName + "\"");
deleteFile = true;
}
else if (ZipSnap.defaultActionOnDelete == 'N')
{
System.out.print("\n [" + i + "] Skipping \"" + u.nativeName + "\"");
}
else if (ZipSnap.defaultActionOnDelete == '\0')
{
System.out.print("\n [" + i + "] Delete \"" + u.nativeName + "\"?\n ");
final char choice = UserIO.userCharPrompt(
"(Y)es/(N)o/(A)lways/Neve(R): ",
"YNAR");
if (choice == 'Y')
{
deleteFile = true;
}
else if (choice == 'A')
{
ZipSnap.defaultActionOnDelete = 'Y';
deleteFile = true;
}
else if (choice == 'R')
{
ZipSnap.defaultActionOnDelete = 'N';
}
}
System.out.flush();
/* take action */
if (deleteFile)
{
FileIO.FileIOResult result = null;
if (u.isDirectory)
{
result = FileIO.deleteDir(u.file);
}
else
{
result = FileIO.deleteFile(u.file);
}
if (result.success)
{
deletedFiles.add(u);
numDeletedFiles++;
}
else
{
ErrorWarningHandler.reportWarning("Unable to delete " +
(u.isDirectory ? "directory" : "file") + " \"" +
u.nativeName + "\":\n" + result.errorMessage);
}
}
}
/* print summary */
System.out.print(
"\n ---------------------------------" +
"\n No. of files/directories deleted: " +
numDeletedFiles + " out of " + files.size());
System.out.flush();
return numDeletedFiles;
}
/**
* Restore directory timestamps.
*
* @param dirs
* The directories for which timestamps are to be restored
* (must be in reverse lexicographic order of their name field)
* @param restoredDirs
* The directories for which timestamps were successfully restored
* @return
* Number of directory timestamps successfully restored
*/
private static int restoreDirTime(
final List<FileUnit> dirs,
final List<FileUnit> restoredDirs)
{
System.out.print("\n\nRestoring timestamps of " + dirs.size() + " directories:");
/* return value */
int numRestoredDirs = 0;
int i = 0;
/* restore timestamps of directories in reverse lexicographic order */
/* so that subdirectories are synchronized before parent directories */
for (FileUnit u : dirs)
{
i++;
System.out.print("\n [" + i + "] \"" + u.nativeName + "\"");
System.out.flush();
final FileIO.FileIOResult result = FileIO.setDirTime(u.file, u.time);
if (result.success)
{
restoredDirs.add(u);
numRestoredDirs++;
}
else
{
ErrorWarningHandler.reportWarning("Unable to restore timestamp of directory \"" + u.nativeName + "\".");
}
}
/* print summary */
System.out.print(
"\n -------------------------------------" +
"\n No. of directory timestamps restored: " +
numRestoredDirs + " out of " + dirs.size());
System.out.flush();
return numRestoredDirs;
}
/**
* Print out usage syntax, notes, and comments.
*/
private static void printUsage()
{
/* RULER 00000000011111111112222222222333333333344444444445555555555666666666677777777778 */
/* RULER 12345678901234567890123456789012345678901234567890123456789012345678901234567890 */
System.out.print(
"\nZipSnap is a simple command-line incremental backup tool for directories."+
"\n"+
"\nUSAGE: java -jar ZipSnap.jar [command] <switches> [\"Archive\"]"+
"\n"+
"\n[Commands]:"+
"\n" +
"\n a ADD a snapshot of the current directory to the archive:"+
"\n ZipSnap looks for the latest catalog in directory [\"Archive\"] and performs" +
"\n file matching between the current directory contents and previously" +
"\n archived files and directories as described by the catalog. By default," +
"\n files and directories are matched by full (relative) pathname, size, and" +
"\n last-modified time (in milliseconds). The new or modified files and" +
"\n directories in the current directory (i.e. the unmatched contents) are" +
"\n then added to a new compressed volume (by default, a ZIP file), and a new" +
"\n catalog (by default, a plain text file compressed as a ZIP file) is" +
"\n written. If the archive does not exist yet, a new archive is created." +
"\n" +
"\n r RESTORE a snapshot from the archive to the current directory:"+
"\n ZipSnap looks for the latest catalog in directory [\"Archive\"] and by" +
"\n default, restores the latest snapshot from the archive. File matching is" +
"\n performed between the current directory contents and the snapshot" +
"\n contents, to determine the unmatched files and directories that need to be" +
"\n extracted from their respective compressed volumes, and to be deleted from" +
"\n the current directory. By default, files and directories are matched by" +
"\n full (relative) pathname, size, and last-modified time (in milliseconds)." +
"\n" +
"\n i Display INFORMATION on the archive:" +
"\n ZipSnap looks for the latest catalog in directory [\"Archive\"] and displays" +
"\n the file counts for each snapshot in the archive." +
"\n"+
"\n<Switches>:"+
"\n" +
"\n -s, --simulate Simulate only; do not actually add/restore snapshot" +
"\n -i, --ignorewarnings Ignore warnings; do not pause" +
"\n" +
"\n -c, --crc Use file CRC-32 checksum for file matching, in addition" +
"\n to full (relative) pathname, size, and last-modified" +
"\n time (in milliseconds)" +
"\n -t, --time:[x] Use a x-millisecond time-tolerance for file matching" +
"\n (by default, a 0-millisecond time-tolerance is used)" +
"\n --all Add/extract ALL files/directories when adding/restoring" +
"\n a snapshot without performing file matching first" +
"\n --forceadd Force addition of snapshot, even if it is identical to" +
"\n the last snapshot (by default, an identical snapshot" +
"\n is not added)" +
"\n" +
"\n -l, --list Create a list of files/directories to be added/extracted" +
"\n when adding/restoring a snapshot" +
"\n -z, --zip:<x> Add files/directories to a ZIP volume using compression" +
"\n level x, with 0 and 9 representing minimum and maximum" +
"\n compression respectively (by default, maximum" +
"\n compression is used)" +
"\n -j, --jar:<x> Add files/directories to a JAR volume ..." +
"\n" +
"\n --snapshot:[x] Restore snapshot number x (by default, the latest" +
"\n snapshot is restored)" +
"\n -o, --overwrite:[y|n] Always[y]/never[n] overwrite existing files/directories" +
"\n when restoring a snapshot" +
"\n -d, --delete:[y|n] Always[y]/never[n] delete unmatched existing" +
"\n files/directories when restoring a snapshot" +
"\n -p, --path:[\"x\"] Include additional search path x for volume files when" +
"\n restoring a snapshot (can be used repeatedly to specify" +
"\n multiple search paths)" +
"\n" +
"\n -f, --filter:[\"x\"] Apply GLOB filter string x for file/directory names" +
"\n -F, --FILTER:[\"x\"] ... for full (relative) file/directory pathnames" +
"\n -e, --filterregex:[\"x\"] Apply REGEX filter string x for file/directory names" +
"\n -E, --FILTERREGEX:[\"x\"] ... for full (relative) file/directory pathnames" +
"\n"+
"\n[\"Archive\"]:"+
"\n" +
"\n ZipSnap archive, i.e. the directory containing the catalogs and compressed" +
"\n volumes"+
"\n"+
"\nNOTES:"+
"\n" +
"\n 1. ZipSnap creates archives that are collections of point-in-time snapshots." +
"\n An archive is just a directory of catalogs (plain text files compressed as" +
"\n ZIP files) and compressed volumes (ZIP files)." +
"\n" +
"\n 2. Catalogs are written cumulatively, i.e. a later catalog contains all the" +
"\n information from previous catalogs. Therefore, only the latest catalog is" +
"\n needed when adding/restoring snapshots." +
"\n" +
"\n 3. Catalogs and compressed volumes are automatically timestamped as" +
"\n ArchiveName.yyyyMMdd-HHmmss.n, with extensions .txt.zip and .zip" +
"\n respectively, where n is the snapshot number." +
"\n" +
"\n 4. The latest catalog selected by ZipSnap is the catalog with the largest" +
"\n snapshot number in the archive directory. To break ties, the catalog with" +
"\n the latest last-modified time is selected." +
"\n" +
"\n 5. When restoring a snapshot, ZipSnap looks for volume files with specific" +
"\n timestamps and snapshot numbers, in the archive directory. Additional" +
"\n search paths, if specified, are also searched. To break ties, the volume" +
"\n with the latest last-modified time is selected." +
"\n" +
"\n 6. Because ZipSnap never modifies a catalog or compressed volume after it is"+
"\n created, archives can be stored on write-once-only media." +
"\n"+
"\n 7. ZipSnap supports GLOB and REGEX filters for file/directory names, or their" +
"\n full (relative) pathnames. The supported REGEX patterns are given by the" +
"\n Java API documentation." +
"\n" +
"\n GLOB Patterns and Wildcards:" +
"\n * Matches a string of zero or more characters" +
"\n ? Matches exactly one character" +
"\n [ ] Matches exactly one character from within the brackets:" +
"\n [abc] matches a, b, or c" +
"\n [!abc] matches any character except a, b, or c (negation)" +
"\n [a-z0-9] matches any character a through z, or 0 through 9," +
"\n inclusive (range)" +
"\n { } Matches exactly one comma-delimited string from within the braces:" +
"\n {a,bc,def} matches either a, bc, or def" +
"\n"+
"\nEXAMPLES:"+
"\n"+
"\n 1. ADD a snapshot to the archive; if the archive does not exist, a new archive" +
"\n is created:" +
"\n java -jar ZipSnap.jar a \"C:\\Backups\\Work\"" +
"\n" +
"\n 2. RESTORE the latest snapshot from the archive:" +
"\n java -jar ZipSnap.jar r \"C:\\Backups\\Work\"" +
"\n" +
"\n 3. RESTORE the latest snapshot from the archive, and always overwrite existing"+
"\n files/directories, but never delete unmatched existing files/directories:" +
"\n java -jar ZipSnap.jar r --overwrite:y --delete:n \"C:\\Backups\\Work\""+
"\n" +
"\n 4. RESTORE snapshot number 2 from the archive:" +
"\n java -jar ZipSnap.jar r --snapshot:2 \"C:\\Backups\\Work\"" +
"\n" +
"\n 5. RESTORE only jpg or html files in the latest snapshot from the archive:" +
"\n java -jar ZipSnap.jar r --filter:\"*.{jpg,html}\" \"C:\\Backups\\Work\"" +
"\n" +
"\n 6. Display INFORMATION on the archive:" +
"\n java -jar ZipSnap.jar i \"C:\\Backups\\Work\"" +
"\n\n");
}
}