/**
* Sync 2.1
* Copyright 2007 Zach Scrivena
* 2007-12-09
* zachscrivena@gmail.com
* http://syncdir.sourceforge.net/
*
* Sync performs one-way directory or file synchronization.
*
* 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 sync;
import java.io.Console;
import java.io.File;
import java.io.PrintWriter;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Scanner;
import java.util.TreeMap;
import java.util.regex.PatternSyntaxException;
/**
* Sync performs one-way directory or file synchronization.
*/
public class Sync
{
/******************************************
* CONSTANTS AND MISCELLANEOUS PARAMETERS *
******************************************/
/** constant: program title */
private static final String PROGRAM_TITLE =
"Sync 2.1 Copyright 2007 Zach Scrivena 2007-12-09";
/** constant: time format string (yyyy-MM-dd HH:mm:ss.SSS) */
static final String TIME_FORMAT_STRING = "%1$tF %1$tT.%1$tL";
/** parameter: true if this is a Windows OS, false otherwise */
private static boolean isWindowsOperatingSystem = false;
/******************************
* SYNCHRONIZATION PARAMETERS *
******************************/
/** parameter: simulate only; do not modify target (default = false) */
private static boolean simulateOnly = false;
/** parameter: ignore warnings; do not pause (default = false) */
private static boolean ignoreWarnings = false;
/** parameter: canonical full pathname of log file (default = null) */
private static String logName = null;
/** parameter: log file PrintWriter (default = null) */
static PrintWriter log = null;
/** parameter: standard output */
static PrintWriter stdout = null;
/** parameter: standard error */
static PrintWriter stderr = null;
/** parameter: do not recurse into subdirectories (default = false) */
private static boolean noRecurse = false;
/** enum type: synchronization mode (DIRECTORY, FILE) */
private static enum SyncMode
{
DIRECTORY,
FILE;
}
/** parameter: synchronization mode (DIRECTORY, FILE) */
private static SyncMode syncMode;
/** parameter: match file/directory name (default = true) */
private static boolean matchName = true;
/** parameter: match file/directory size (always true) */
private static final boolean matchSize = true;
/** parameter: match file/directory last-modified time (default = true) */
private static boolean matchTime = true;
/** parameter: match file/directory CRC-32 checksum (default = true) */
private static boolean matchCrc = true;
/** parameter: time-tolerance in milliseconds for file-matching (default = 0) */
private static long matchTimeTolerance = 0L;
/** parameter: string representation of match attributes, e.g. "(name,size,time,CRC)" */
private static String matchNstcString;
/** partial FileUnit comparator for file-matching */
private static FileUnitComparator matchFileUnitComparator;
/** partial FileUnit comparator for searching (should be a "truncated" version of Sync.matchFileUnitComparator) */
private static FileUnitComparator searchFileUnitComparator;
/** name-only FileUnit comparator */
private static FileUnitComparator nameOnlyFileUnitComparator;
/** parameter: canonical full pathname of source file/directory (ends with a separator for a directory) */
private static String sourceName;
/** parameter: canonical full pathname of target file/directory (ends with a separator for a directory) */
private static String targetName;
/** parameter: source file/directory (canonical full pathname) */
private static File source;
/** parameter: target file/directory (canonical full pathname) */
private static File target;
/** parameter: default action on renaming matched files (default = '\0') */
private static char defaultActionOnRenameMatched = '\0';
/** parameter: default action on synchronizing time of matched files (default = '\0') */
private static char defaultActionOnTimeSyncMatched = '\0';
/** parameter: default action on deleting unmatched target files/directories (default = '\0') */
private static char defaultActionOnDeleteUnmatched = '\0';
/** parameter: default action on overwriting existing target files (default = '\0') */
static char defaultActionOnOverwrite = '\0';
/** parameter: filter for source file/directory names (default = null) */
private static FilterNode sourceFilter = null;
/** parameter: filter for target file/directory names (default = null) */
private static FilterNode targetFilter = null;
/** parameter: filter relative pathnames instead of filenames (default = false) */
private static boolean filterRelativePathname = false;
/** parameter: use lower case names for filtering (default = false) */
private static boolean filterLowerCase = false;
/*********************
* REPORT STATISTICS *
*********************/
/** statistic: number of warnings encountered */
private static int reportNumWarnings = 0;
/**
* Main entry point for the Sync program.
*
* @param args
* Command-line argument strings
*/
public static void main(
final String[] args)
{
/* initialize standard output and error streams */
final Console console = System.console();
if (console == null)
{
Sync.stdout = new PrintWriter(System.out);
Sync.stderr = new PrintWriter(System.err);
}
else
{
Sync.stdout = console.writer();
Sync.stderr = console.writer();
}
/* display program title */
SyncIO.printFlush("\n" + Sync.PROGRAM_TITLE);
/* exit status code to be reported to the OS when exiting (default = 0) */
int exitCode = 0;
try
{
/* determine if this is a Windows OS */
Sync.isWindowsOperatingSystem = System.getProperty("os.name").toUpperCase(Locale.ENGLISH).contains("WINDOWS") &&
(File.separatorChar == '\\');
/* process command-line arguments and configure synchronization parameters */
processArguments(args);
SyncIO.printLog("\n" + Sync.PROGRAM_TITLE);
/* perform synchronization */
switch (Sync.syncMode)
{
case DIRECTORY:
syncDirectory();
break;
case FILE:
syncFile();
break;
}
SyncIO.print("\n\nSync is done!\n\n");
}
catch (TerminatingException e)
{
/* terminating exception thrown; proceed to abort program */
/* (this should be the only place where a TerminatingException is caught) */
exitCode = e.getExitCode();
if (exitCode != 0)
{
/* abnormal termination; SyncIO.print error message */
SyncIO.printToErr("\n\nERROR: " + e.getMessage() + "\n");
SyncIO.print("\nSync aborted.\n\n");
}
}
catch (Exception e)
{
/* catch all other exceptions; proceed to abort program */
SyncIO.printToErr("\n\nERROR: An unexpected error has occurred:\n" +
getExceptionMessage(e) + "\n");
exitCode = 1;
SyncIO.print("\nSync aborted.\n\n");
}
finally
{
/* perform clean-up before exiting */
Sync.stdout.flush();
Sync.stderr.flush();
if (Sync.log != null)
{
Sync.log.flush();
Sync.log.close();
Sync.log = null;
}
}
System.exit(exitCode);
}
/**
* Process command-line arguments and configure synchronization parameters.
*
* @param args
* Command-line argument strings
*/
private static void processArguments(
final String[] args)
{
final String howHelp = "\nTo display help, run Sync without any command-line arguments.";
/* SyncIO.print usage documentation, if no arguments */
if (args.length == 0)
{
printUsage();
throw new TerminatingException(null, 0);
}
/* check if sufficient arguments */
if (args.length < 2)
throw new TerminatingException("Insufficient arguments:\nThe source and target directories/files must be specified." + howHelp);
/* process source directory/file */
Sync.source = new File(args[args.length - 2]);
try
{
Sync.source = Sync.source.getCanonicalFile();
}
catch (Exception e)
{
throw new TerminatingException("Source \"" + Sync.source.getPath() + "\" is not a valid directory/file:\n" + getExceptionMessage(e) + howHelp);
}
/* process target directory/file */
Sync.target = new File(args[args.length - 1]);
try
{
Sync.target = Sync.target.getCanonicalFile();
}
catch (Exception e)
{
throw new TerminatingException("Target \"" + Sync.target.getPath() + "\" is not a valid directory/file:\n" + getExceptionMessage(e) + howHelp);
}
/* determine synchronization mode */
if (Sync.source.isDirectory())
{
/* source is a directory; must check that target is NOT a file */
if (Sync.target.exists() && !Sync.target.isDirectory())
throw new TerminatingException("Target \"" + Sync.target.getPath() + "\" is a file.\nFor DIRECTORY synchronization, the target (if it exists) must also be a directory." + howHelp);
/* DIRECTORY synchronization */
Sync.syncMode = Sync.SyncMode.DIRECTORY;
Sync.sourceName = SyncIO.trimTrailingSeparator(Sync.source.getPath()) + File.separatorChar;
Sync.targetName = SyncIO.trimTrailingSeparator(Sync.target.getPath()) + File.separatorChar;
}
else if (source.exists())
{
/* source is a file; must check that target is NOT a directory */
if (Sync.target.isDirectory())
throw new TerminatingException("Target \"" + Sync.target.getPath() + "\" is a directory.\nFor FILE synchronization, the target (if it exists) must also be a file." + howHelp);
/* FILE synchronization */
Sync.syncMode = Sync.SyncMode.FILE;
Sync.sourceName = SyncIO.trimTrailingSeparator(Sync.source.getPath());
Sync.targetName = SyncIO.trimTrailingSeparator(Sync.target.getPath());
}
else
{
/* source does not exist */
throw new TerminatingException("Source \"" + Sync.source.getPath() + "\" does not exist." + howHelp);
}
/* initialize filename filters */
final List<String> includeSource = new ArrayList<String>();
final List<String> excludeSource = new ArrayList<String>();
final List<String> includeTarget = new ArrayList<String>();
final List<String> excludeTarget = new ArrayList<String>();
boolean regexFilter = false;
/* process command-line switches */
for (int i = 0, n = args.length - 2; i < n; i++)
{
final String sw = args[i];
if ("--simulate".equals(sw) || "-s".equals(sw))
{
/* simulate only; do not modify target */
Sync.simulateOnly = true;
Sync.ignoreWarnings = true;
}
else if ("--ignorewarnings".equals(sw))
{
/* ignore warnings; do not pause */
Sync.ignoreWarnings = true;
}
else if ("--log".equals(sw) || "-l".equals(sw))
{
/* create log file "sync.yyyyMMdd-HHmmss.log" */
if (Sync.logName != null)
throw new TerminatingException("Switch --log can be specified at most once." + howHelp);
final String timestamp = String.format("%1$tY%1$tm%1$td-%1$tH%1$tM%1$tS", Calendar.getInstance(Locale.ENGLISH));
File f = new File("sync." + timestamp + ".log");
if (f.exists())
{
/* find an unused file name */
for (long k = 0; k < Long.MAX_VALUE; k++)
{
f = new File("sync." + timestamp + "." + k + ".log");
if (f.exists())
{
f = null;
}
else
{
/* use this unused name */
break;
}
}
if (f == null)
throw new TerminatingException("Failed to create an unused filename for log file:\nRan out of suffixes n in \"sync." +
timestamp + ".n.log\"; try specifying a filename, e.g. --log:\"record.txt\"." + howHelp);
}
try
{
Sync.logName = f.getCanonicalPath();
}
catch (Exception e)
{
throw new TerminatingException("Failed to create log file \"" + f.getPath() + "\":\n" + getExceptionMessage(e) + howHelp);
}
}
else if (sw.startsWith("--log:") || sw.startsWith("-l:"))
{
/* create log file with the specified name */
if (Sync.logName != null)
throw new TerminatingException("Switch --log can be specified at most once." + howHelp);
final String a = sw.substring(sw.indexOf(':') + 1);
if (a.isEmpty())
throw new TerminatingException("Empty --log parameter:\nA log filename must be specified, e.g. --log:\"record.txt\"." + howHelp);
File f = new File(a);
if (f.exists())
throw new TerminatingException("Log file \"" + f.getPath() + "\" already exists:\nA nonexistent file must be specified." + howHelp);
try
{
Sync.logName = f.getCanonicalPath();
}
catch (Exception e)
{
throw new TerminatingException("Failed to create log file \"" + f.getPath() + "\":\n" + getExceptionMessage(e) + howHelp);
}
}
else if ("--norecurse".equals(sw) || "-r".equals(sw))
{
/* do not recurse into subdirectories */
Sync.noRecurse = true;
}
else if ("--noname".equals(sw) || "-n".equals(sw))
{
/* do not use filename for file-matching */
Sync.matchName = false;
}
else if ("--notime".equals(sw) || "-t".equals(sw))
{
/* do not use last-modified time for file-matching */
Sync.matchTime = false;
}
else if ("--nocrc".equals(sw)|| "-c".equals(sw))
{
/* do not use CRC-32 checksum for file-matching */
Sync.matchCrc = false;
}
else if (sw.startsWith("--time:"))
{
/* use specified time-tolerance (in milliseconds) for file-matching */
final String a = sw.substring(sw.indexOf(':') + 1);
if (a.isEmpty())
throw new TerminatingException("Empty --time parameter:\nTime-tolerance (in milliseconds) must be a nonnegative integer, e.g. --time:2000." + howHelp);
try
{
Sync.matchTimeTolerance = Long.parseLong(a);
}
catch (Exception e)
{
Sync.matchTimeTolerance = -1L;
}
if (Sync.matchTimeTolerance < 0L)
throw new TerminatingException("Invalid --time parameter \"" +
a + "\":\nTime-tolerance (in milliseconds) must be a nonnegative integer, e.g. --time:2000." + howHelp);
}
else if (sw.startsWith("--rename:"))
{
/* rename matched target files? */
final String a = sw.substring(sw.indexOf(':') + 1);
if (a.isEmpty())
throw new TerminatingException("Empty --rename parameter:\nParameter must be \"y\" or \"n\", e.g. --rename:y." + howHelp);
if ("y".equals(a))
{
Sync.defaultActionOnRenameMatched = 'Y';
}
else if ("n".equals(a))
{
Sync.defaultActionOnRenameMatched = 'N';
}
else
{
throw new TerminatingException("Invalid --rename parameter \"" + a + "\":\nParameter must be \"y\" or \"n\", e.g. --rename:y." + howHelp);
}
}
else if (sw.startsWith("--synctime:"))
{
/* synchronize time of matched target files? */
final String a = sw.substring(sw.indexOf(':') + 1);
if (a.isEmpty())
throw new TerminatingException("Empty --synctime parameter:\nParameter must be \"y\" or \"n\", e.g. --synctime:y." + howHelp);
if ("y".equals(a))
{
Sync.defaultActionOnTimeSyncMatched = 'Y';
}
else if ("n".equals(a))
{
Sync.defaultActionOnTimeSyncMatched = 'N';
}
else
{
throw new TerminatingException("Invalid --synctime parameter \"" + a + "\":\nParameter must be \"y\" or \"n\", e.g. --synctime:y." + howHelp);
}
}
else if (sw.startsWith("--overwrite:"))
{
/* overwrite existing target files? */
final String a = sw.substring(sw.indexOf(':') + 1);
if (a.isEmpty())
throw new TerminatingException("Empty --overwrite parameter:\nParameter must be \"y\" or \"n\", e.g. --overwrite:y." + howHelp);
if ("y".equals(a))
{
Sync.defaultActionOnOverwrite = 'Y';
}
else if ("n".equals(a))
{
Sync.defaultActionOnOverwrite = 'N';
}
else
{
throw new TerminatingException("Invalid --overwrite parameter \"" + a + "\":\nParameter must be \"y\" or \"n\", e.g. --overwrite:y." + howHelp);
}
}
else if (sw.startsWith("--delete:"))
{
/* delete unmatched target files/directories? */
if (syncMode != syncMode.DIRECTORY)
throw new TerminatingException("Switch --delete can be used for only DIRECTORY synchronization." + howHelp);
final String a = sw.substring(sw.indexOf(':') + 1);
if (a.isEmpty())
throw new TerminatingException("Empty --delete parameter:\nParameter must be \"y\" or \"n\", e.g. --delete:y." + howHelp);
if ("y".equals(a))
{
Sync.defaultActionOnDeleteUnmatched = 'Y';
}
else if ("n".equals(a))
{
Sync.defaultActionOnDeleteUnmatched = 'N';
}
else
{
throw new TerminatingException("Invalid --delete parameter \"" + a + "\":\nParameter must be \"y\" or \"n\", e.g. --delete:y." + howHelp);
}
}
else if ("--force".equals(sw))
{
/* equivalent to the combination: "--rename:y --synctime:y --overwrite:y --delete:y" */
Sync.defaultActionOnRenameMatched = 'Y';
Sync.defaultActionOnTimeSyncMatched = 'Y';
Sync.defaultActionOnOverwrite = 'Y';
Sync.defaultActionOnDeleteUnmatched = 'Y';
}
else if ("--path".equals(sw) || "-p".equals(sw))
{
/* filter relative pathnames instead of filenames (e.g. "work\report\jan.txt" instead of "jan.txt") */
if (Sync.syncMode != Sync.syncMode.DIRECTORY)
throw new TerminatingException("Switch --path can be used for only DIRECTORY synchronization." + howHelp);
Sync.filterRelativePathname = true;
}
else if ("--lower".equals(sw) || "-w".equals(sw))
{
/* use lower case names for filtering (e.g. "HelloWorld2007.JPG" ---> "helloworld2007.jpg") */
if (Sync.syncMode != Sync.syncMode.DIRECTORY)
throw new TerminatingException("Switch --lower can be used for only DIRECTORY synchronization." + howHelp);
Sync.filterLowerCase = true;
}
else if ("--regex".equals(sw))
{
/* use REGEX instead of GLOB filename filters */
regexFilter = true;
}
else if (sw.startsWith("--include:") || sw.startsWith("-i:"))
{
/* include source and target files/directories with names matching specified GLOB/REGEX expression */
if (Sync.syncMode != Sync.syncMode.DIRECTORY)
throw new TerminatingException("Switch --include can be used for only DIRECTORY synchronization." + howHelp);
final String a = sw.substring(sw.indexOf(':') + 1);
if (a.isEmpty())
throw new TerminatingException("Empty --include parameter:\nA GLOB (or REGEX) expression must be specified, e.g. --include:\"*.{mp3,jpg}\"." + howHelp);
includeSource.add(a);
includeTarget.add(a);
}
else if (sw.startsWith("--exclude:") || sw.startsWith("-x:"))
{
/* exclude source and target files/directories with names matching specified GLOB/REGEX expression */
if (Sync.syncMode != Sync.syncMode.DIRECTORY)
throw new TerminatingException("Switch --exclude can be used for only DIRECTORY synchronization." + howHelp);
final String a = sw.substring(sw.indexOf(':') + 1);
if (a.isEmpty())
throw new TerminatingException("Empty --exclude parameter:\nA GLOB (or REGEX) expression must be specified, e.g. --exclude:\"*.{mp3,jpg}\"." + howHelp);
excludeSource.add(a);
excludeTarget.add(a);
}
else if (sw.startsWith("--includesource:") || sw.startsWith("-is:"))
{
/* include source files/directories with names matching specified GLOB/REGEX expression */
if (Sync.syncMode != Sync.syncMode.DIRECTORY)
throw new TerminatingException("Switch --includesource can be used for only DIRECTORY synchronization." + howHelp);
final String a = sw.substring(sw.indexOf(':') + 1);
if (a.isEmpty())
throw new TerminatingException("Empty --includesource parameter:\nA GLOB (or REGEX) expression must be specified, e.g. --includesource:\"*.{mp3,jpg}\"." + howHelp);
includeSource.add(a);
}
else if (sw.startsWith("--excludesource:") || sw.startsWith("-xs:"))
{
/* exclude source files/directories with names matching specified GLOB/REGEX expression */
if (Sync.syncMode != Sync.syncMode.DIRECTORY)
throw new TerminatingException("Switch --excludesource can be used for only DIRECTORY synchronization." + howHelp);
final String a = sw.substring(sw.indexOf(':') + 1);
if (a.isEmpty())
throw new TerminatingException("Empty --excludesource parameter:\nA GLOB (or REGEX) expression must be specified, e.g. --excludesource:\"*.{mp3,jpg}\"." + howHelp);
excludeSource.add(a);
}
else if (sw.startsWith("--includetarget:") || sw.startsWith("-it:"))
{
/* include target files/directories with names matching specified GLOB/REGEX expression */
if (Sync.syncMode != Sync.syncMode.DIRECTORY)
throw new TerminatingException("Switch --includetarget can be used for only DIRECTORY synchronization." + howHelp);
final String a = sw.substring(sw.indexOf(':') + 1);
if (a.isEmpty())
throw new TerminatingException("Empty --includetarget parameter:\nA GLOB (or REGEX) expression must be specified, e.g. --includetarget:\"*.{mp3,jpg}\"." + howHelp);
includeTarget.add(a);
}
else if (sw.startsWith("--excludetarget:") || sw.startsWith("-xt:"))
{
/* exclude target files/directories with names matching specified GLOB/REGEX expression */
if (Sync.syncMode != Sync.syncMode.DIRECTORY)
throw new TerminatingException("Switch --excludetarget can be used for only DIRECTORY synchronization." + howHelp);
final String a = sw.substring(sw.indexOf(':') + 1);
if (a.isEmpty())
throw new TerminatingException("Empty --excludetarget parameter:\nA GLOB (or REGEX) expression must be specified, e.g. --excludetarget:\"*.{mp3,jpg}\"." + howHelp);
excludeTarget.add(a);
}
else
{
/* invalid switch */
throw new TerminatingException("\"" + sw + "\" is not a valid switch." + howHelp);
}
}
/* process source filename filters, if any */
if (includeSource.isEmpty())
{
if (excludeSource.isEmpty())
{
Sync.sourceFilter = null;
}
else
{
Sync.sourceFilter = new FilterNode(FilterNode.LogicType.NOR);
for (String s : excludeSource)
{
try
{
Sync.sourceFilter.addFilter(new FilterNode(
regexFilter ? FilterNode.FilterType.REGEX : FilterNode.FilterType.GLOB,
false,
Sync.isWindowsOperatingSystem ? s.replace("/", "\\\\") : s));
}
catch (PatternSyntaxException e)
{
throw new TerminatingException("Failed to compile the specified " +
(regexFilter ? "REGEX" : "GLOB") + " expression \"" + s + "\":\n" + getExceptionMessage(e) + howHelp);
}
}
}
}
else
{
if (excludeSource.isEmpty())
{
Sync.sourceFilter = new FilterNode(FilterNode.LogicType.OR);
for (String s : includeSource)
{
try
{
Sync.sourceFilter.addFilter(new FilterNode(
regexFilter ? FilterNode.FilterType.REGEX : FilterNode.FilterType.GLOB,
false,
Sync.isWindowsOperatingSystem ? s.replace("/", "\\\\") : s));
}
catch (PatternSyntaxException e)
{
throw new TerminatingException("Failed to compile the specified " +
(regexFilter ? "REGEX" : "GLOB") + " expression \"" + s + "\":\n" + getExceptionMessage(e) + howHelp);
}
}
}
else
{
final FilterNode includes = new FilterNode(FilterNode.LogicType.OR);
final FilterNode excludes = new FilterNode(FilterNode.LogicType.NOR);
for (String s : includeSource)
{
try
{
includes.addFilter(new FilterNode(
regexFilter ? FilterNode.FilterType.REGEX : FilterNode.FilterType.GLOB,
false,
Sync.isWindowsOperatingSystem ? s.replace("/", "\\\\") : s));
}
catch (PatternSyntaxException e)
{
throw new TerminatingException("Failed to compile the specified " +
(regexFilter ? "REGEX" : "GLOB") + " expression \"" + s + "\":\n" + getExceptionMessage(e) + howHelp);
}
}
for (String s : excludeSource)
{
try
{
excludes.addFilter(new FilterNode(
regexFilter ? FilterNode.FilterType.REGEX : FilterNode.FilterType.GLOB,
false,
Sync.isWindowsOperatingSystem ? s.replace("/", "\\\\") : s));
}
catch (PatternSyntaxException e)
{
throw new TerminatingException("Failed to compile the specified " +
(regexFilter ? "REGEX" : "GLOB") + " expression \"" + s + "\":\n" + getExceptionMessage(e) + howHelp);
}
}
Sync.sourceFilter = new FilterNode(FilterNode.LogicType.AND);
Sync.sourceFilter.addFilter(includes);
Sync.sourceFilter.addFilter(excludes);
}
}
/* process target filename filters, if any */
if (includeTarget.isEmpty())
{
if (excludeTarget.isEmpty())
{
Sync.targetFilter = null;
}
else
{
Sync.targetFilter = new FilterNode(FilterNode.LogicType.NOR);
for (String s : excludeTarget)
{
try
{
Sync.targetFilter.addFilter(new FilterNode(
regexFilter ? FilterNode.FilterType.REGEX : FilterNode.FilterType.GLOB,
false,
Sync.isWindowsOperatingSystem ? s.replace("/", "\\\\") : s));
}
catch (PatternSyntaxException e)
{
throw new TerminatingException("Failed to compile the specified " +
(regexFilter ? "REGEX" : "GLOB") + " expression \"" + s + "\":\n" + getExceptionMessage(e) + howHelp);
}
}
}
}
else
{
if (excludeTarget.isEmpty())
{
Sync.targetFilter = new FilterNode(FilterNode.LogicType.OR);
for (String s : includeTarget)
{
try
{
Sync.targetFilter.addFilter(new FilterNode(
regexFilter ? FilterNode.FilterType.REGEX : FilterNode.FilterType.GLOB,
false,
Sync.isWindowsOperatingSystem ? s.replace("/", "\\\\") : s));
}
catch (PatternSyntaxException e)
{
throw new TerminatingException("Failed to compile the specified " +
(regexFilter ? "REGEX" : "GLOB") + " expression \"" + s + "\":\n" + getExceptionMessage(e) + howHelp);
}
}
}
else
{
final FilterNode includes = new FilterNode(FilterNode.LogicType.OR);
final FilterNode excludes = new FilterNode(FilterNode.LogicType.NOR);
for (String s : includeTarget)
{
try
{
includes.addFilter(new FilterNode(
regexFilter ? FilterNode.FilterType.REGEX : FilterNode.FilterType.GLOB,
false,
Sync.isWindowsOperatingSystem ? s.replace("/", "\\\\") : s));
}
catch (PatternSyntaxException e)
{
throw new TerminatingException("Failed to compile the specified " +
(regexFilter ? "REGEX" : "GLOB") + " expression \"" + s + "\":\n" + getExceptionMessage(e) + howHelp);
}
}
for (String s : excludeTarget)
{
try
{
excludes.addFilter(new FilterNode(
regexFilter ? FilterNode.FilterType.REGEX : FilterNode.FilterType.GLOB,
false,
Sync.isWindowsOperatingSystem ? s.replace("/", "\\\\") : s));
}
catch (PatternSyntaxException e)
{
throw new TerminatingException("Failed to compile the specified " +
(regexFilter ? "REGEX" : "GLOB") + " expression \"" + s + "\":\n" + getExceptionMessage(e) + howHelp);
}
}
Sync.targetFilter = new FilterNode(FilterNode.LogicType.AND);
Sync.targetFilter.addFilter(includes);
Sync.targetFilter.addFilter(excludes);
}
}
/* check certain combinations of switches */
if ((Sync.sourceFilter == null) && (Sync.targetFilter == null))
{
if (Sync.filterRelativePathname)
throw new TerminatingException("Switch --path cannot be used when no filter is specified." + howHelp);
if (Sync.filterLowerCase)
throw new TerminatingException("Switch --lower cannot be used when no filter is specified." + howHelp);
}
if (Sync.simulateOnly)
{
if (Sync.defaultActionOnRenameMatched != '\0')
throw new TerminatingException("Switch --rename cannot be used in simulation mode." + howHelp);
if (Sync.defaultActionOnTimeSyncMatched != '\0')
throw new TerminatingException("Switch --synctime cannot be used in simulation mode." + howHelp);
if (Sync.defaultActionOnOverwrite != '\0')
throw new TerminatingException("Switch --overwrite cannot be used in simulation mode." + howHelp);
if (Sync.defaultActionOnDeleteUnmatched != '\0')
throw new TerminatingException("Switch --delete cannot be used in simulation mode." + howHelp);
Sync.defaultActionOnRenameMatched = 'Y';
Sync.defaultActionOnTimeSyncMatched = 'Y';
Sync.defaultActionOnOverwrite = 'Y';
Sync.defaultActionOnDeleteUnmatched = 'Y';
}
/* prepare log file, if specified */
if (Sync.logName != null)
{
try
{
Sync.log = new PrintWriter(Sync.logName);
}
catch (Exception e)
{
throw new TerminatingException("Failed to create log file \"" + Sync.logName + "\":\n" + getExceptionMessage(e) + howHelp);
}
}
/* disable filename matching for FILE synchronization */
if (Sync.syncMode == Sync.syncMode.FILE)
Sync.matchName = false;
/* create string representation of match attributes, e.g. "(name,size,time,crc)" */
Sync.matchNstcString = getNstcString(
Sync.matchName, Sync.matchSize, Sync.matchTime, Sync.matchCrc);
/* create partial FileUnit comparator for file-matching */
Sync.matchFileUnitComparator = new FileUnitComparator(
Sync.matchName, Sync.matchSize, Sync.matchTime, Sync.matchCrc);
/* create partial FileUnit comparator for searching (should be a "truncated" version of Sync.matchFileUnitComparator) */
Sync.searchFileUnitComparator = new FileUnitComparator(
Sync.matchName, Sync.matchSize, false, false);
/* create name-only FileUnit comparator */
Sync.nameOnlyFileUnitComparator = new FileUnitComparator(
true, false, false, false);
}
/**
* Perform DIRECTORY synchronization from Sync.source to Sync.target.
* The source must be an existing directory; the target must be a directory if it exists.
*/
private static void syncDirectory()
{
final StringBuilder s = new StringBuilder();
s.append("\n\nDIRECTORY SYNCHRONIZATION");
if (Sync.simulateOnly)
s.append(" (SIMULATION MODE)");
/* display log file, if any */
if (Sync.log != null)
s.append("\n\nLog file: \"" + Sync.logName + "\"");
/* display source and target directories */
s.append("\n\nSource directory: \"" + Sync.sourceName + "\"" +
"\nTarget directory: \"" + Sync.targetName + "\"\n");
/* display file-matching attributes */
s.append("\nFile-matching attributes: " + Sync.matchNstcString + "\n");
if (Sync.matchTimeTolerance > 0L)
s.append(", with " + Sync.matchTimeTolerance + "-millisecond time-tolerance\n");
/* display source and target file/directorry filters, if any */
if (Sync.sourceFilter != null)
s.append("\nSource file/directory filter: " + Sync.sourceFilter.toString());
if (Sync.targetFilter != null)
s.append("\nTarget file/directory filter: " + Sync.targetFilter.toString());
if ((Sync.sourceFilter != null) || (Sync.targetFilter != null))
s.append("\nFilter mode: " + (Sync.filterLowerCase ? "lower-case " : "") +
(Sync.filterRelativePathname ? "relative pathname" : "filename") + "\n");
SyncIO.printFlush(s.toString());
/* validate source and target */
if (Sync.source.equals(Sync.target))
throw new TerminatingException("The source directory \"" + Sync.sourceName +
"\" cannot be the same as the target directory \"" + Sync.targetName + "\".");
if (Sync.sourceName.startsWith(Sync.targetName))
throw new TerminatingException("The source directory \"" + Sync.sourceName +
"\" cannot be a subdirectory of the target directory \"" + Sync.targetName + "\".");
if (Sync.targetName.startsWith(Sync.sourceName))
throw new TerminatingException("The target directory \"" + Sync.targetName +
"\" cannot be a subdirectory of the source directory \"" + Sync.sourceName + "\".");
final int sourceNameLength = Sync.sourceName.length();
final int targetNameLength = Sync.targetName.length();
/* report statistics */
int reportNumSourceDirsScanned = 0;
int reportNumSourceFilesScanned = 0;
int reportNumTargetFilesScanned = 0;
int reportNumSourceFilesMatched = 0;
int reportNumSyncTime = 0;
int reportNumSyncTimeSuccess = 0;
int reportNumRenameOperations = 0;
int reportNumRenameOperationsSuccess = 0;
int reportNumUnmatchedSourceFiles = 0;
int reportNumUnmatchedSourceFilesCopied = 0;
int reportNumUnmatchedTargetFilesDirs = 0;
int reportNumUnmatchedTargetFilesDirsDeleted = 0;
/* perform a DFS synchronization of the subdirectories using two stacks: */
/* - contentStack contains directories whose contents are to be synced */
/* - timeStack contains directories whose time should be synced after */
/* processing all their contents */
final Deque<FilePair> contentStack = new ArrayDeque<FilePair>();
final Deque<FilePair> timeStack = new ArrayDeque<FilePair>();
final FilePair marker = new FilePair(null, null); // special marker
contentStack.push(new FilePair(Sync.source, Sync.target));
SyncNextDirectory:
while (!contentStack.isEmpty())
{
/* get subdirectory-pair for synchronizing contents */
final FilePair pair = contentStack.pop();
/* check for special marker */
if (pair == marker)
{
/**************************************************
* SYNC LAST-MODIFIED TIME OF TARGET SUBDIRECTORY *
**************************************************/
final FilePair timePair = timeStack.pop();
final File timeSourceDir = timePair.source;
final File timeTargetDir = timePair.target;
if (timeSourceDir.isDirectory() && timeTargetDir.isDirectory())
{
final long sourceTime = timeSourceDir.lastModified();
final long targetTime = timeTargetDir.lastModified();
/* create the target subdirectory only if the times are different, and */
/* the target's name matches the filter (if specified) */
boolean syncTime = false;
if (targetTime != sourceTime)
{
if (Sync.sourceFilter == null)
{
syncTime = true;
}
else
{
final String timeTargetDirName = SyncIO.trimTrailingSeparator(timeTargetDir.getPath()) + File.separatorChar;
String name = null;
if (timeTargetDirName.length() == targetNameLength)
{
/* this is the base target directory */
name = "";
}
else
{
name = Sync.filterRelativePathname ?
timeTargetDirName.substring(targetNameLength) :
(SyncIO.trimTrailingSeparator(timeTargetDir.getName()) + File.separatorChar);
}
if (Sync.filterLowerCase)
name = name.toLowerCase(Locale.ENGLISH);
if (Sync.sourceFilter.matches(name))
syncTime = true;
}
}
if (syncTime)
{
if (!Sync.simulateOnly)
{
final boolean success = timeTargetDir.setLastModified(sourceTime);
if (!success)
reportWarning("Failed to set last-modified time of target subdirectory \"" +
SyncIO.trimTrailingSeparator(timeTargetDir.getPath()) + File.separatorChar +
"\":\n " + String.format(Locale.ENGLISH, Sync.TIME_FORMAT_STRING, new Date(targetTime)) +
" ---> " + String.format(Locale.ENGLISH, Sync.TIME_FORMAT_STRING, new Date(sourceTime)) + ".");
}
}
}
continue SyncNextDirectory;
}
/*********************************
* (1) DISPLAY RELATIVE PATHNAME *
*********************************/
/* source subdirectory */
final File sourceDir = pair.source;
final String sourceDirName = SyncIO.trimTrailingSeparator(sourceDir.getPath()) + File.separatorChar;
/* target subdirectory */
final File targetDir = pair.target;
final String targetDirName = SyncIO.trimTrailingSeparator(targetDir.getPath()) + File.separatorChar;
/* relative pathname of the subdirectory */
final String relativePathname = sourceDirName.substring(sourceNameLength);
SyncIO.printFlush("\n\nSUBDIRECTORY: \"" + (relativePathname.isEmpty() ? ("." + File.separatorChar) : relativePathname) + "\"");
if (targetDir.exists() && !targetDir.isDirectory())
{
reportWarning("The target \"" + targetDir.getPath() +
"\" already exists but is not a directory; could it be a file?\nThis subdirectory will be ignored.");
continue SyncNextDirectory;
}
/*******************************************
* (2) GET CONTENTS OF SOURCE SUBDIRECTORY *
*******************************************/
/* get source files (filtered if necessary) and subdirectories */
final File[] sFileList = sourceDir.listFiles();
if (sFileList == null)
{
reportWarning("Failed to get contents of source subdirectory \"" +
sourceDirName + "\".\nThis subdirectory will be ignored.");
continue SyncNextDirectory;
}
final List<FileUnit> sFiles = new ArrayList<FileUnit>();
final List<FileUnit> sDirs = new ArrayList<FileUnit>();
for (File f : sFileList)
{
final FileUnit u = new FileUnit(f);
if (u.isDirectory)
{
sDirs.add(u);
}
else
{
/* apply filter on file, if necessary */
boolean addFile = false;
if (Sync.sourceFilter == null)
{
addFile = true;
}
else
{
String name = Sync.filterRelativePathname ?
u.file.getPath().substring(sourceNameLength) : u.name;
if (Sync.filterLowerCase)
name = name.toLowerCase(Locale.ENGLISH);
if (Sync.sourceFilter.matches(name))
addFile = true;
}
if (addFile)
sFiles.add(u);
}
}
reportNumSourceDirsScanned++;
reportNumSourceFilesScanned += sFiles.size();
/*******************************************
* (3) GET CONTENTS OF TARGET SUBDIRECTORY *
*******************************************/
/* get target files and subdirectories */
final List<FileUnit> tFiles = new ArrayList<FileUnit>();
final List<FileUnit> tDirs = new ArrayList<FileUnit>();
if (targetDir.isDirectory())
{
/* target subdirectory already exists; get its contents */
final File[] tFileList = targetDir.listFiles();
if (tFileList == null)
{
reportWarning("Failed to get contents of target subdirectory \"" +
targetDirName + "\".\nThis subdirectory will be ignored.");
continue SyncNextDirectory;
}
for (File f : tFileList)
{
final FileUnit u = new FileUnit(f);
if (u.isDirectory)
{
tDirs.add(u);
}
else
{
/* apply filter on file, if necessary */
boolean addFile = false;
if (Sync.targetFilter == null)
{
addFile = true;
}
else
{
String name = Sync.filterRelativePathname ?
u.file.getPath().substring(targetNameLength) : u.name;
if (Sync.filterLowerCase)
name = name.toLowerCase(Locale.ENGLISH);
if (Sync.targetFilter.matches(name))
addFile = true;
}
if (addFile)
tFiles.add(u);
}
}
}
else if (targetDir.exists())
{
/* target already exists, but is not a directory */
reportWarning("Target \"" + targetDir.getPath() +
"\" already exists but is not a directory; could it be a file?" +
"\nThis subdirectory will be ignored.");
continue SyncNextDirectory;
}
else
{
/* target subdirectory does not exist; */
/* proceed to create it only if its name matches the filter (if specified) */
boolean createDir = false;
if (Sync.sourceFilter == null)
{
createDir = true;
}
else
{
String name = null;
if (targetDirName.length() == targetNameLength)
{
/* this is the base target directory */
name = "";
}
else
{
name = Sync.filterRelativePathname ?
targetDirName.substring(targetNameLength) :
(SyncIO.trimTrailingSeparator(targetDir.getName()) + File.separatorChar);
}
if (Sync.filterLowerCase)
name = name.toLowerCase(Locale.ENGLISH);
if (Sync.sourceFilter.matches(name))
createDir = true;
}
if (createDir)
{
if (!Sync.simulateOnly)
{
targetDir.mkdirs();
if (!targetDir.isDirectory())
{
reportWarning("Failed to create target subdirectory \"" +
targetDirName + "\".\nThis subdirectory will be ignored.");
continue SyncNextDirectory;
}
}
}
}
reportNumTargetFilesScanned += tFiles.size();
/**************************************************************
* (4) CHECK FOR FILE-MATCHING KEY CLASHES AMONG SOURCE FILES *
**************************************************************/
/* sort source files by file-matching attributes */
Collections.sort(sFiles, Sync.matchFileUnitComparator);
FileUnit w = null;
for (FileUnit u : sFiles)
{
/* check if consecutive files have the same file-matching attributes */
if ((w != null) && (Sync.matchFileUnitComparator.compare(u, w) == 0))
{
reportWarning("File-matching key clash in source subdirectory \"" + sourceDirName +
"\":\nThe following source files have the same " + Sync.matchNstcString + ":" +
"\n [1] \"" + u.file.getPath() + "\"" +
"\n [2] \"" + w.file.getPath() + "\"" +
"\nThe files in this subdirectory will be ignored.");
/* recurse into subdirectories: */
/* (this block of code should be identical to the one below) */
/* push subdirectory-pair onto time-stack for subsequent time-sync */
timeStack.push(new FilePair(sourceDir, targetDir));
contentStack.push(marker); // special marker
if (!Sync.noRecurse)
{
for (int i = sDirs.size() - 1; i >= 0; i--)
{
/* source subdirectory */
final File sDir = sDirs.get(i).file;
/* corresponding target subdirectory */
final File tDir = new File(targetDir, sDir.getName());
/* push subdirectory-pair onto content-stack for subsequent content-sync */
contentStack.push(new FilePair(sDir, tDir));
}
}
continue SyncNextDirectory;
}
w = u;
}
/*************************************************************
* (5) PERFORM FILE-MATCHING BETWEEN SOURCE AND TARGET FILES *
*************************************************************/
/* perform file-matching */
final boolean uniqueMatching = performSourceTargetFileMatching(sFiles, tFiles);
/* matched source files, to be time-synced or renamed if necessary */
final List<FileUnit> sFilesMatched = new ArrayList<FileUnit>();
/* unmatched source files, to be copied */
final List<FileUnit> sFilesUnmatched = new ArrayList<FileUnit>();
/* unmatched target files and subdirectories, to be deleted */
final Map<File,FileUnit> tFilesDirsUnmatched = new TreeMap<File,FileUnit>();
/* process source files */
for (FileUnit u : sFiles)
{
if (u.match == null)
{
/* this is an unmatched source file, to be copied */
sFilesUnmatched.add(u);
}
else
{
/* this is a matched source file */
sFilesMatched.add(u);
}
}
/* process target files */
for (FileUnit u : tFiles)
{
if (u.match == null)
{
/* this is an unmatched target file, to be deleted */
tFilesDirsUnmatched.put(u.file, u);
}
}
/* perform directory-matching (matching by name only) */
performSourceTargetDirMatching(sDirs, tDirs);
/* process target directories */
for (FileUnit u : tDirs)
{
if (u.match == null)
{
/* this is an unmatched target subdirectory, to be deleted */
tFilesDirsUnmatched.put(u.file, u);
}
}
/*******************************************
* (6) DISPLAY MATCHED SOURCE-TARGET FILES *
*******************************************/
/* number of matched target files to time-sync and rename */
int numSyncTime = 0;
int numSyncName = 0;
if (!sFiles.isEmpty())
{
final int numSourceFilesMatched = sFilesMatched.size();
SyncIO.print("\n\n No. of source files matched: " + numSourceFilesMatched + " of " + sFiles.size());
for (FileUnit u : sFilesMatched)
{
reportNumSourceFilesMatched++;
/* display matched source-target pair and matching attributes */
SyncIO.print("\n [M" + reportNumSourceFilesMatched + ":" +
(u.sameName ? "n" : " ") +
(u.sameSize ? "s" : " ") +
(u.sameTime ? "t" : " ") +
(Sync.matchCrc ? (u.sameCrc ? "c" : " ") : "") +
"] \"" + u.name + "\"" +
(u.sameName ? "" : (" <---> \"" + u.match.name + "\"")));
/* need to sync filename of matched target file? */
if (!u.sameName)
numSyncName++;
/* need to sync time of matched target file? */
if (!u.sameTime)
numSyncTime++;
}
/* warn on poor file-matching */
if (!uniqueMatching)
reportWarning("Matching between files in source subdirectory \"" + sourceDirName +
"\" and target subdirectory \"" + targetDirName + "\" involves arbitrarily broken ties.");
}
/*****************************************
* (7) SYNC TIME OF MATCHED TARGET FILES *
*****************************************/
if (numSyncTime > 0)
{
boolean syncTime = false;
if (Sync.defaultActionOnTimeSyncMatched == 'Y')
{
SyncIO.print("\n\n Synchronizing last-modified time of " +
numSyncTime + " matched target " +
((numSyncTime == 1) ? "file:" : "files:"));
syncTime = true;
}
else if (Sync.defaultActionOnTimeSyncMatched == 'N')
{
SyncIO.print("\n\n Skipping last-modified time synchronization of " +
numSyncTime + " matched target " +
((numSyncTime == 1) ? "file" : "files"));
}
else if (Sync.defaultActionOnTimeSyncMatched == '\0')
{
SyncIO.print("\n\n Synchronize last-modified time of " +
numSyncTime + " matched target " +
((numSyncTime == 1) ? "file" : "files") + "?\n");
final char choice = SyncIO.userCharPrompt(
" (Y)es/(N)o/(A)lways/Neve(R): ",
"YNAR");
if (choice == 'Y')
{
syncTime = true;
}
else if (choice == 'A')
{
Sync.defaultActionOnTimeSyncMatched = 'Y';
syncTime = true;
}
else if (choice == 'R')
{
Sync.defaultActionOnTimeSyncMatched = 'N';
}
}
if (syncTime)
{
/* proceed to synchronize time of matched target files */
for (FileUnit u : sFilesMatched)
{
final FileUnit t = u.match;
if (!t.sameTime)
{
/* set last-modified time of the matched target file to that of the source file */
reportNumSyncTime++;
SyncIO.printFlush("\n [T" + reportNumSyncTime + "] \"" +
t.name + "\"\n " + t.getTimeString() + " ---> " + u.getTimeString());
if (!Sync.simulateOnly)
{
final String error = SyncIO.setFileTime(t.file, u.time);
if (error == null)
{
/* last-modified time of file was successfully set */
reportNumSyncTimeSuccess++;
}
else if (!error.isEmpty())
{
reportWarning("Failed to set last-modified time of matched target file \"" +
t.file.getPath() + "\":\n " + t.getTimeString() + " ---> " + u.getTimeString() +
":\n" + error);
}
}
}
}
}
}
/***********************************
* (8) RENAME MATCHED TARGET FILES *
***********************************/
if (numSyncName > 0)
{
boolean syncName = false;
if (Sync.defaultActionOnRenameMatched == 'Y')
{
SyncIO.print("\n\n Renaming " +
numSyncName + " matched target " +
((numSyncName == 1) ? "file:" : "files:"));
syncName = true;
}
else if (Sync.defaultActionOnRenameMatched == 'N')
{
SyncIO.print("\n\n Skipping renaming of " +
numSyncName + " matched target " +
((numSyncName == 1) ? "file" : "files"));
}
else if (Sync.defaultActionOnRenameMatched == '\0')
{
SyncIO.print("\n\n Rename " +
numSyncName + " matched target " +
((numSyncName == 1) ? "file" : "files") +
"?\n");
final char choice = SyncIO.userCharPrompt(
" (Y)es/(N)o/(A)lways/Neve(R): ",
"YNAR");
if (choice == 'Y')
{
syncName = true;
}
else if (choice == 'A')
{
Sync.defaultActionOnRenameMatched = 'Y';
syncName = true;
}
else if (choice == 'R')
{
Sync.defaultActionOnRenameMatched = 'N';
}
}
if (syncName)
{
/* determine actual file renaming operations */
final List<FilePair> renamePairs = new ArrayList<FilePair>();
/* get desired source-target rename pair */
for (FileUnit u : sFilesMatched)
{
if (!u.sameName)
renamePairs.add(new FilePair(u.match.file, new File(targetDir, u.name)));
}
final List<FilePair> renameOperations = getRenameOperations(renamePairs);
/* proceed to rename matched target file */
for (FilePair p : renameOperations)
{
reportNumRenameOperations++;
SyncIO.printFlush("\n [R" + reportNumRenameOperations + "] \"" +
p.source.getName() + "\" ---> \"" + p.target.getName() + "\"");
if (!Sync.simulateOnly)
{
final String error = SyncIO.renameFile(p.source, p.target);
if (error == null)
{
/* file was successfully renamed */
reportNumRenameOperationsSuccess++;
tFilesDirsUnmatched.remove(p.target);
}
else if (!error.isEmpty())
{
reportWarning("Failed to rename matched target file \"" +
p.source.getPath() + "\" ---> \"" +
p.target.getPath() + "\":\n" + error);
}
}
}
}
}
/***********************************
* (9) COPY UNMATCHED SOURCE FILES *
***********************************/
if (!sFilesUnmatched.isEmpty())
{
final int numUnmatchedSourceFiles = sFilesUnmatched.size();
/* display unmatched source files to be copied to the target subdirectory */
SyncIO.print("\n\n No. of unmatched source files to be copied: " + numUnmatchedSourceFiles);
for (FileUnit u : sFilesUnmatched)
{
reportNumUnmatchedSourceFiles++;
SyncIO.printFlush("\n [C" + reportNumUnmatchedSourceFiles + "] \"" +
u.name + "\" (" + u.getSizeString() + ")");
if (!Sync.simulateOnly)
{
/* desired target file for copy operation */
final File targetFile = new File(targetDir, u.name);
final String error = SyncIO.copyFile(u.file, targetFile);
if (error == null)
{
/* file was successfully copied */
reportNumUnmatchedSourceFilesCopied++;
tFilesDirsUnmatched.remove(targetFile);
}
else if (!error.isEmpty())
{
Sync.reportWarning("Failed to copy unmatched source file \"" +
u.file.getPath() + "\" ---> \"" +
targetFile.getPath() + "\":\n" + error);
}
}
}
}
/**************************************************
* (10) DELETE UNMATCHED TARGET FILES/DIRECTORIES *
**************************************************/
if (!tFilesDirsUnmatched.isEmpty())
{
final int numUnmatchedTargetFilesDirs = tFilesDirsUnmatched.size();
SyncIO.print("\n\n No. of unmatched target files/directories to be deleted: " + numUnmatchedTargetFilesDirs);
/* delete unmatched target files (first pass), and subdirectories (second pass) */
for (boolean isDirectory : new boolean[]{false, true})
{
for (FileUnit u : tFilesDirsUnmatched.values())
{
if (u.isDirectory == isDirectory)
{
reportNumUnmatchedTargetFilesDirs++;
SyncIO.print("\n [D" + reportNumUnmatchedTargetFilesDirs + "] ");
boolean stillExists = false;
if (u.file.exists() &&
(u.file.isDirectory() == u.isDirectory))
{
/* proceed to check full canonical pathname */
String pathname = null;
try
{
pathname = u.file.getCanonicalPath();
}
catch (Exception e)
{
pathname = null;
}
if ((pathname != null) && pathname.equals(u.file.getPath()))
stillExists = true;
}
if (stillExists)
{
boolean deleteFileDir = false;
if (Sync.defaultActionOnDeleteUnmatched == 'Y')
{
SyncIO.printFlush("\"" + u.name + "\"");
deleteFileDir = true;
}
else if (Sync.defaultActionOnDeleteUnmatched == 'N')
{
SyncIO.printFlush("Skipping \"" + u.name + "\"");
}
else if (Sync.defaultActionOnDeleteUnmatched == '\0')
{
SyncIO.print("Delete \"" + u.name + "\"?\n");
final char choice = SyncIO.userCharPrompt(
" (Y)es/(N)o/(A)lways/Neve(R): ",
"YNAR");
if (choice == 'Y')
{
deleteFileDir = true;
}
else if (choice == 'A')
{
Sync.defaultActionOnDeleteUnmatched = 'Y';
deleteFileDir = true;
}
else if (choice == 'R')
{
Sync.defaultActionOnDeleteUnmatched = 'N';
}
}
if (deleteFileDir)
{
if (!Sync.simulateOnly)
{
final String error = SyncIO.deleteFileDir(u.file);
if (error == null)
{
/* file/directory was successfully deleted */
reportNumUnmatchedTargetFilesDirsDeleted++;
}
else if (!error.isEmpty())
{
Sync.reportWarning("Failed to delete unmatched target " +
(u.isDirectory ? "directory" : "file") + " \"" +
SyncIO.trimTrailingSeparator(u.file.getPath()) +
(u.isDirectory ? File.separatorChar : "") +
"\":\n" + error);
}
}
}
}
else
{
/* file/directory does not exist anymore */
SyncIO.printFlush("\"" + u.name + "\" does not exist anymore");
}
}
}
}
}
/************************************
* (11) RECURSE INTO SUBDIRECTORIES *
************************************/
/* push subdirectory-pair onto time-stack for subsequent time-sync */
timeStack.push(new FilePair(sourceDir, targetDir));
contentStack.push(marker); // special marker
if (!Sync.noRecurse)
{
for (int i = sDirs.size() - 1; i >= 0; i--)
{
/* source subdirectory */
final File sDir = sDirs.get(i).file;
/* corresponding target subdirectory */
final File tDir = new File(targetDir, sDir.getName());
/* push subdirectory-pair onto content-stack for subsequent content-sync */
contentStack.push(new FilePair(sDir, tDir));
}
}
}
/*********************
* REPORT STATISTICS *
*********************/
final StringBuilder report = new StringBuilder();
report.append("\n\nSYNCHRONIZATION REPORT");
if (Sync.reportNumWarnings > 0)
report.append("\n " + Sync.reportNumWarnings + ((Sync.reportNumWarnings == 1) ? " warning" : " warnings") + " encountered.");
report.append(
"\n No. of source subdirectories scanned : " + reportNumSourceDirsScanned +
"\n No. of source files scanned : " + reportNumSourceFilesScanned +
"\n No. of target files scanned : " + reportNumTargetFilesScanned +
"\n No. of source files matched [M] : " + reportNumSourceFilesMatched);
if (reportNumSyncTime > 0)
report.append("\n No. of successful time-sync operations [T] : " +
reportNumSyncTimeSuccess + " of " + reportNumSyncTime);
if (reportNumRenameOperations > 0)
report.append("\n No. of successful file rename operations [R] : " +
reportNumRenameOperationsSuccess + " of " + reportNumRenameOperations);
report.append(
"\n No. of unmatched source files [C] : " + reportNumUnmatchedSourceFiles +
" (" + reportNumUnmatchedSourceFilesCopied + " copied)" +
"\n No. of unmatched target files/directories [D] : " + reportNumUnmatchedTargetFilesDirs +
" (" + reportNumUnmatchedTargetFilesDirsDeleted + " deleted)");
SyncIO.print(report.toString());
}
/**
* Perform FILE synchronization from Sync.source to Sync.target.
* The source must be an existing file; the target must be a file if it exists.
*/
private static void syncFile()
{
final StringBuilder s = new StringBuilder();
s.append("\n\nFILE SYNCHRONIZATION");
if (Sync.simulateOnly)
s.append(" (SIMULATION MODE)");
/* display log file, if any */
if (Sync.log != null)
s.append("\n\nLog file: \"" + Sync.logName + "\"");
/* display source and target directories */
s.append("\n\nSource file: \"" + Sync.sourceName + "\"" +
"\nTarget file: \"" + Sync.targetName + "\"\n");
/* display file-matching attributes */
s.append("\nFile-matching attributes: " + Sync.matchNstcString);
if (Sync.matchTimeTolerance > 0L)
s.append(",\n with " + Sync.matchTimeTolerance + "-millisecond time-tolerance");
SyncIO.printFlush(s.toString());
/* validate source and target */
if (Sync.source.equals(Sync.target))
throw new TerminatingException("The source file \"" + Sync.sourceName +
"\" cannot be the same as the target file \"" + Sync.targetName + "\".");
/* source and target files */
final FileUnit sourceFile = new FileUnit(Sync.source);
final FileUnit targetFile = Sync.target.exists() ? new FileUnit(Sync.target) : null;
if (targetFile == null)
{
/***************************************************
* (1) COPY SOURCE FILE TO NONEXISTENT TARGET FILE *
***************************************************/
/* target file does not exist; proceed to copy source to target */
SyncIO.printFlush("\n\nTarget file does not exist\n\nCopying \"" +
Sync.source.getPath() + "\"\n ---> \"" +
Sync.target.getPath() + "\"");
if (!Sync.simulateOnly)
{
final String error = SyncIO.copyFile(Sync.source, Sync.target);
if (error == null)
{
/* file was successfully copied */
SyncIO.printFlush("\n\n1 file copied.");
}
else if (!error.isEmpty())
{
Sync.reportWarning("Failed to copy source file \"" +
Sync.source.getPath() + "\" ---> \"" +
Sync.target.getPath() + "\":\n" + error);
}
}
}
else if (Sync.matchFileUnitComparator.compare(sourceFile, targetFile) != 0)
{
/***********************************************************
* (2) COPY UNMATCHED SOURCE FILE TO EXISTING TARGET FILE *
***********************************************************/
/* source and target files do not match; proceed to copy source to target */
SyncIO.printFlush("\n\nSource and target files do not match\n\nCopying \"" +
Sync.source.getPath() + "\"\n ---> \"" +
Sync.target.getPath() + "\"");
if (!Sync.simulateOnly)
{
final String error = SyncIO.copyFile(Sync.source, Sync.target);
if (error == null)
{
/* file was successfully copied */
SyncIO.printFlush("\n\n1 file copied.");
}
else if (!error.isEmpty())
{
Sync.reportWarning("Failed to copy unmatched source file \"" +
Sync.source.getPath() + "\" ---> \"" +
Sync.target.getPath() + "\":\n" + error);
}
}
}
else
{
/*************************************
* (3) SOURCE AND TARGET FILES MATCH *
*************************************/
SyncIO.printFlush("\n\nSource and target files have the same ");
targetFile.sameName = sourceFile.name.equals(targetFile.name);
targetFile.sameSize = (sourceFile.size == targetFile.size);
targetFile.sameTime = (sourceFile.time == targetFile.time);
targetFile.sameCrc = Sync.matchCrc ? (sourceFile.getCrc() == targetFile.getCrc()) : false;
/* display matched source-target pair and matching attributes */
SyncIO.printFlush(getNstcString(
targetFile.sameName, targetFile.sameSize, targetFile.sameTime, targetFile.sameCrc));
/******************************************
* (3.1) SYNC TIME OF MATCHED TARGET FILE *
******************************************/
if (!targetFile.sameTime)
{
boolean syncTime = false;
if (Sync.defaultActionOnTimeSyncMatched == 'Y')
{
SyncIO.printFlush("\n\n Synchronizing last-modified time of matched target file\n " +
targetFile.getTimeString() + " ---> " + sourceFile.getTimeString());
syncTime = true;
}
else if (Sync.defaultActionOnTimeSyncMatched == 'N')
{
SyncIO.printFlush("\n\n Skipping last-modified time synchronization of matched target file");
}
else if (Sync.defaultActionOnTimeSyncMatched == '\0')
{
SyncIO.print("\n\n Synchronize last-modified time of matched target file\n " +
targetFile.getTimeString() + " ---> " + sourceFile.getTimeString() + "?\n");
final char choice = SyncIO.userCharPrompt(
" (Y)es/(N)o: ",
"YN");
if (choice == 'Y')
syncTime = true;
}
if (syncTime)
{
if (!Sync.simulateOnly)
{
final String error = SyncIO.setFileTime(Sync.target, sourceFile.time);
if ((error != null) && !error.isEmpty())
reportWarning("Failed to set last-modified time of matched target file \"" +
Sync.target.getPath() + "\":\n " +
targetFile.getTimeString() + " ---> " + sourceFile.getTimeString() +
":\n" + error);
}
}
}
/************************************
* (3.2) RENAME MATCHED TARGET FILE *
************************************/
if (!targetFile.sameName)
{
boolean syncName = false;
if (Sync.defaultActionOnRenameMatched == 'Y')
{
SyncIO.printFlush("\n\n Renaming matched target file\n \"" +
targetFile.name + "\" ---> \"" + sourceFile.name + "\"");
syncName = true;
}
else if (Sync.defaultActionOnRenameMatched == 'N')
{
SyncIO.printFlush("\n\n Skipping renaming of matched target file");
}
else if (Sync.defaultActionOnRenameMatched == '\0')
{
SyncIO.print("\n\n Rename matched target file\n \"" +
targetFile.name + "\" ---> \"" + sourceFile.name + "\"?\n");
final char choice = SyncIO.userCharPrompt(
" (Y)es/(N)o: ",
"YN");
if (choice == 'Y')
syncName = true;
}
if (syncName)
{
if (!Sync.simulateOnly)
{
final File newTarget = new File(Sync.target.getParentFile(), sourceFile.name);
final String error = SyncIO.renameFile(Sync.target, newTarget);
if ((error != null) && !error.isEmpty())
reportWarning("Failed to rename matched target file \"" +
Sync.target.getPath() + "\" ---> \"" +
newTarget.getPath() + "\":\n" + error);
}
}
}
}
/*********************
* REPORT STATISTICS *
*********************/
if (Sync.reportNumWarnings > 0)
SyncIO.print("\n\n" + Sync.reportNumWarnings + ((Sync.reportNumWarnings == 1) ? " warning" : " warnings") + " encountered.");
}
/**
* Perform source-target file-matching.
*
* @param sFiles
* Source files to be matched
* @param tFiles
* Target files (candidate matches)
* @return
* True if matching is unique; false otherwise
*/
private static boolean performSourceTargetFileMatching(
final List<FileUnit> sFiles,
final List<FileUnit> tFiles)
{
/* return value */
boolean uniqueMatching = true;
/* no source files to be matched? */
if (sFiles.isEmpty())
return uniqueMatching;
/* sort target files by "search" attributes for file-matching */
Collections.sort(tFiles, Sync.searchFileUnitComparator);
/* for each source file, find a matching target file */
MatchNextSourceFile:
for (FileUnit s : sFiles)
{
final int i = Collections.binarySearch(tFiles, s, Sync.searchFileUnitComparator);
/* no candidate match found */
if (i < 0)
continue MatchNextSourceFile;
/* candidate match found; proceed to find a valid matching target file */
int matchIndex = -1;
MatchNextTargetFileUp:
for (int j = i - 1; j >= 0; j--)
{
final FileUnit t = tFiles.get(j);
if (Sync.searchFileUnitComparator.compare(s, t) != 0)
break MatchNextTargetFileUp;
/* proceed to match last-modified time and CRC-32 checksum, if necessary */
if ((Sync.matchTime && (Math.abs(s.time - t.time) > Sync.matchTimeTolerance)) ||
((Sync.matchCrc && (s.getCrc() != t.getCrc()))))
{
continue MatchNextTargetFileUp;
}
if ((matchIndex >= 0) || (t.match != null))
{
/* the source or target file has already been matched */
uniqueMatching = false;
continue MatchNextTargetFileUp;
}
/* valid matching target file found */
matchIndex = j;
}
MatchNextTargetFileDown:
for (int j = i; j < tFiles.size(); j++)
{
final FileUnit t = tFiles.get(j);
if (Sync.searchFileUnitComparator.compare(s, t) != 0)
break MatchNextTargetFileDown;
/* proceed to match last-modified time and CRC-32 checksum, if necessary */
if ((Sync.matchTime && (Math.abs(s.time - t.time) > Sync.matchTimeTolerance)) ||
((Sync.matchCrc && (s.getCrc() != t.getCrc()))))
{
continue MatchNextTargetFileDown;
}
if ((matchIndex >= 0) || (t.match != null))
{
/* the source or target file has already been matched */
uniqueMatching = false;
continue MatchNextTargetFileDown;
}
/* valid matching target file found */
matchIndex = j;
}
if (matchIndex >= 0)
{
/* valid matching target file found */
final FileUnit t = tFiles.get(matchIndex);
s.match = t;
t.match = s;
/* file-matching attributes */
s.sameName = s.name.equals(t.name);
s.sameSize = (s.size == t.size);
s.sameTime = (s.time == t.time);
s.sameCrc = Sync.matchCrc ? (s.getCrc() == t.getCrc()) : false;
t.sameName = s.sameName;
t.sameSize = s.sameSize;
t.sameTime = s.sameTime;
t.sameCrc = s.sameCrc;
}
}
return uniqueMatching;
}
/**
* Perform source-target directory-matching (matching by name only).
*
* @param sDirs
* Source subdirectories to be matched
* @param tDirs
* Target subdirectories (candidate matches)
*/
private static void performSourceTargetDirMatching(
final List<FileUnit> sDirs,
final List<FileUnit> tDirs)
{
/* no source subdirectories to be matched? */
if (sDirs.isEmpty())
return;
/* sort target directories by name only for directory-matching */
Collections.sort(tDirs, Sync.nameOnlyFileUnitComparator);
/* for each source subdirectory, find a matching target subdirectory */
for (FileUnit s : sDirs)
{
final int i = Collections.binarySearch(tDirs, s, Sync.nameOnlyFileUnitComparator);
if (i >= 0)
{
/* valid matching target directory found */
final FileUnit t = tDirs.get(i);
s.match = t;
t.match = s;
}
}
}
/**
* Determine sequence of actual rename operations to be performed, in order
* to effect the desired rename operations.
*
* @param renamePairs
* Desired rename operations
* @return
* Sequence of actual rename operations to be performed
*/
private static List<FilePair> getRenameOperations(
final List<FilePair> renamePairs)
{
/* determine target files, check validity, and detect clashes */
final Map<File,FilePair> targetMap = new TreeMap<File,FilePair>();
for (FilePair p : renamePairs)
{
/* check for clash (i.e. nonunique target filenames) */
final FilePair q = targetMap.get(p.target);
if (q == null)
{
targetMap.put(p.target, p);
}
else
{
throw new TerminatingException("(INTERNAL) Target filename clash:\n" +
"[1] \"" + q.source.getPath() + "\"\n ---> \"" + q.target.getPath() + "\"\n" +
"[2] \"" + p.source.getPath() + "\"\n ---> \"" + p.target.getPath() + "\"");
}
}
/* determine actual renaming sequence */
final Map<File,LinkedList<FilePair>> sequenceHeads = new TreeMap<File,LinkedList<FilePair>>();
final Map<File,LinkedList<FilePair>> sequenceTails = new TreeMap<File,LinkedList<FilePair>>();
for (FilePair p : renamePairs)
{
/* look for a sequence head with source = this target */
final LinkedList<FilePair> headSequence = sequenceHeads.get(p.target);
/* look for a sequence tail with target = this source */
final LinkedList<FilePair> tailSequence = sequenceTails.get(p.source);
if ((headSequence == null) && (tailSequence == null))
{
/* add this file rename pair as a new sequence */
final LinkedList<FilePair> s = new LinkedList<FilePair>();
s.add(p);
sequenceHeads.put(p.source, s);
sequenceTails.put(p.target, s);
}
else if ((headSequence != null) && (tailSequence == null))
{
/* add this pair to the head of an existing sequence */
headSequence.addFirst(p);
sequenceHeads.remove(p.target);
sequenceHeads.put(p.source, headSequence);
}
else if ((headSequence == null) && (tailSequence != null))
{
/* add this pair to the tail of an existing sequence */
tailSequence.addLast(p);
sequenceTails.remove(p.source);
sequenceTails.put(p.target, tailSequence);
}
else if ((headSequence != null) && (tailSequence != null))
{
if (headSequence == tailSequence)
{
/* loop detected, so we use a temporary target file/directory name */
/* create a temporary file/directory name */
File temp = new File(p.target.getParentFile(),
p.target.getName() + ".sync");
if (temp.exists() || targetMap.containsKey(temp))
{
for (long i = 0; i < Long.MAX_VALUE; i++)
{
temp = new File(p.target.getParentFile(),
p.target.getName() + ".sync." + i);
if (temp.exists() || targetMap.containsKey(temp))
{
temp = null;
}
else
{
/* use this unused name */
break;
}
}
}
if (temp == null)
throw new TerminatingException("Ran out of suffixes for temporary name of file \"" +
p.target.getPath() + "\".");
/* use this unused filename */
targetMap.put(temp, null);
/* add a leading and trailing rename file pair to the existing sequence */
final FilePair tempTail = new FilePair(p.source, temp);
final FilePair tempHead = new FilePair(temp, p.target);
headSequence.addFirst(tempHead);
tailSequence.addLast(tempTail);
sequenceHeads.remove(p.target);
sequenceHeads.put(temp, headSequence);
sequenceTails.remove(p.source);
sequenceTails.put(temp, tailSequence);
}
else
{
/* link two distinct sequences together */
tailSequence.addLast(p);
tailSequence.addAll(headSequence);
sequenceHeads.remove(p.target);
sequenceTails.remove(p.source);
sequenceTails.put(tailSequence.peekLast().target, tailSequence);
}
}
}
/* prepare return value */
final List<FilePair> renameOperations = new ArrayList<FilePair>();
for (LinkedList<FilePair> s : sequenceHeads.values())
{
/* get reversed order of rename file pairs within the sequence */
Collections.reverse(s);
renameOperations.addAll(s);
}
return renameOperations;
}
/**
* Return string representation of match attributes, e.g. "(name,size,time,crc)".
*/
private static String getNstcString(
final boolean n,
final boolean s,
final boolean t,
final boolean c)
{
final StringBuilder a = new StringBuilder();
a.append('(');
if (n) a.append("name,");
if (s) a.append("size,");
if (t) a.append("time,");
if (c) a.append("crc,");
a.deleteCharAt(a.length() - 1);
a.append(')');
return a.toString();
}
/**
* Print a warning message and pause.
*
* @param message
* Warning message to be printed on issuing the warning
*/
static void reportWarning(
final Object message)
{
Sync.reportNumWarnings++;
if (Sync.ignoreWarnings)
{
SyncIO.printToErr("\n\nWARNING: " + message + "\n");
}
else
{
SyncIO.printToErr("\n\nWARNING: " + message + "\nPress ENTER to continue...");
(new Scanner(System.in)).nextLine(); // blocks until user responds
}
}
/**
* Get custom exception message string for the given exception.
* Message contains the exception class name, error description string,
* and stack trace.
*
* @param e
* Exception for which to generate the custom message string
*/
static String getExceptionMessage(
final Exception e)
{
final StringBuilder s = new StringBuilder();
s.append("\nJava exception information (" + e.getClass() +
"):\n\"" + e.getMessage() + "\"");
for (StackTraceElement t : e.getStackTrace())
{
s.append("\n at ");
s.append(t.toString());
}
s.append('\n');
return s.toString();
}
/**
* Print usage documentation.
*/
private static void printUsage()
{
/* RULER 00000000011111111112222222222333333333344444444445555555555666666666677777777778 */
/* RULER 12345678901234567890123456789012345678901234567890123456789012345678901234567890 */
SyncIO.print("\n" +
"\nSync performs one-way directory or file synchronization." +
"\n" +
"\nUSAGE: java -jar Sync.jar <switches> [\"Source\"] [\"Target\"]" +
"\n" +
"\nSynchronize [\"Target\"] to match [\"Source\"]. Only [\"Target\"] is modified." +
"\nBy default, the filename, size, last-modified time, and CRC-32 checksum" +
"\nare used for file-matching. The synchronization mode depends on [\"Source\"]:" +
"\n" +
"\n [\"Source\"] is a DIRECTORY: Match source and target directories recursively." +
"\n Matched target files are time-synced and renamed if necessary," +
"\n unmatched source files are copied to the target directory, and" +
"\n unmatched target files/directories are deleted." +
"\n" +
"\n [\"Source\"] is a FILE: Match source and target files, ignoring filename." +
"\n If files match, then the target file is time-synced and renamed if necessary." +
"\n If target file does not exist, then the source file is copied to the target." +
"\n" +
"\n<Switches>:" +
"\n" +
"\n -s, --simulate Simulate only; do not modify target" +
"\n --ignorewarnings Ignore warnings; do not pause" +
"\n -l, --log:<\"x\"> Create log file x; if x is not specified," +
"\n \"sync.yyyyMMdd-HHmmss.log\" is used" +
"\n -r, --norecurse Do not recurse into subdirectories" +
"\n" +
"\n -n, --noname Do not use filename for file-matching" +
"\n -t, --notime Do not use last-modified time for file-matching" +
"\n -c, --nocrc Do not use CRC-32 checksum for file-matching" +
"\n" +
"\n --time:[x] Use a x-millisecond time-tolerance for file-matching" +
"\n (0-millisecond time-tolerance is used by default;" +
"\n use --time:1000 or more to avoid mismatches across" +
"\n different file systems)" +
"\n" +
"\n --rename:[y|n] Always[y]/never[n] rename matched target files" +
"\n --synctime:[y|n] ... synchronize time of matched target files" +
"\n --overwrite:[y|n] ... overwrite existing target files/directories" +
"\n --delete:[y|n] ... delete unmatched target files/directories" +
"\n --force Equivalent to the combination:" +
"\n --rename:y --synctime:y --overwrite:y --delete:y" +
"\n" +
"\n A subset of source and/or target files/directories can be selected for" +
"\n synchronization using GLOB (or REGEX) filename filters. A file/directory is" +
"\n selected if it matches any of the \"include\" filters and none of the \"exclude\"" +
"\n filters." +
"\n" +
"\n -i, --include:[\"x\"] Include source and target files/directories with names" +
"\n matching GLOB expression x" +
"\n -x, --exclude:[\"x\"] Exclude source and target files/directories with names" +
"\n matching GLOB expression x" +
"\n -is, --includesource:[\"x\"] Include source files/directories ..." +
"\n -xs, --excludesource:[\"x\"] Exclude source files/directories ..." +
"\n -it, --includetarget:[\"x\"] Include target files/directories ..." +
"\n -xt, --excludetarget:[\"x\"] Exclude target files/directories ..." +
"\n -p, --path Filter relative pathnames instead of filenames" +
"\n (e.g. \"work\\report\\jan.txt\" instead of \"jan.txt\")" +
"\n -w, --lower Use lower case names for filtering" +
"\n (e.g. \"HelloWorld2007.JPG\" ---> \"helloworld2007.jpg\")" +
"\n --regex Use REGEX instead of GLOB filename filters" +
"\n (see Java API for REGEX syntax)" +
"\n" +
"\n GLOB syntax:" +
"\n * Match a string of 0 or more characters" +
"\n ? Match exactly 1 character" +
"\n [ ] Match exactly 1 character inside the brackets:" +
"\n [abc] match a, b, or c" +
"\n [!abc] match any character except a, b, or c (negation)" +
"\n [a-z0-9] match any character a through z, or 0 through 9," +
"\n inclusive (range)" +
"\n { } Match exactly 1 comma-delimited string inside the braces:" +
"\n {a,bc,def} match either a, bc, or def" +
"\n" +
"\n To use a construct symbol (e.g. [, {, ?) as a literal character," +
"\n insert a backslash before it, e.g. use \\[ for the literal character [." +
"\n Use \\\\ for the literal backslash character \\." +
"\n The file separator in Windows can be specified by \\\\ or /." +
"\n" +
"\nEXAMPLES:" +
"\n" +
"\n 1. Synchronize target \"C:\\Backup\" to look like source \"C:\\Original\"," +
"\n matching files by (name,size,time,crc):" +
"\n java -jar Sync.jar \"C:\\Original\" \"C:\\Backup\"" +
"\n" +
"\n 2. As in example 1, but never delete unmatched target files/directories:" +
"\n java -jar Sync.jar --delete:n \"C:\\Original\" \"C:\\Backup\"" +
"\n" +
"\n 3. As in example 1, but match files by (name,size,time) with a time-tolerance" +
"\n of 2 seconds instead:" +
"\n java -jar Sync.jar --nocrc --time:2000 \"C:\\Original\" \"C:\\Backup\"" +
"\n" +
"\n 4. As in example 1, but always rename and synchronize time of matched target" +
"\n files, overwrite existing target files, and delete unmatched target" +
"\n files/directories:" +
"\n java -jar Sync.jar --force \"C:\\Original\" \"C:\\Backup\"" +
"\n" +
"\n 5. As in example 1, but synchronize only jpg and html files:" +
"\n java -jar Sync.jar --include:\"*.{jpg,html}\" \"C:\\Original\" \"C:\\Backup\"" +
"\n" +
"\n 6. As in example 5, but skip files that begin with a tilde '~':" +
"\n java -jar Sync.jar --include:\"*.{jpg,html}\" --exclude:\"~*\"" +
"\n \"C:\\Original\" \"C:\\Backup\"" +
"\n\n");
}
}