/**
* 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.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.Locale;
import java.util.Scanner;
import java.util.zip.CRC32;
/**
* Perform miscellaneous input-output operations.
*/
class SyncIO
{
/** buffer size (1 Mb) */
private static final int BUFFER_SIZE = 1048576;
/**
* Set the last-modified time of a specified file.
*
* @param file
* File for which to set time
* @param time
* New last-modified time for the specified file
* @return
* null if last-modifed time was successfully set; an error message otherwise
*/
static String setFileTime(
final File file,
final long time)
{
final boolean success = file.setLastModified(time);
if (!success)
return "Failed to set last-modified time using Java's File.setLastModified() method.";
return null; // success
}
/**
* Rename a specified file. An existing file/directory may be overwritten if necessary.
*
* @param source
* File to be renamed
* @param target
* Desired target file
* @return
* null if the file was successfully renamed, an empty string if file rename was aborted,
* an error message otherwise
*/
static String renameFile(
final File source,
final File target)
{
/* rename the specified file? */
boolean renameFile = false;
/* existing target file (to be overwritten), if any */
File existingFile = null;
String existingName = null;
boolean existingIsDirectory = false;
/* check if a distinct target file/directory already exists */
if (target.exists() && !target.equals(source))
{
/* get attributes of the existing file/directory */
try
{
existingFile = target.getCanonicalFile();
}
catch (Exception e)
{
existingFile = target;
}
existingIsDirectory = existingFile.isDirectory();
existingName = trimTrailingSeparator(existingFile.getName()) +
(existingIsDirectory ? File.separatorChar : "");
if (Sync.defaultActionOnOverwrite == 'Y')
{
SyncIO.printFlush("\n Overwriting existing " +
(existingIsDirectory ? "directory" : "file") +
" \"" + existingName + "\"");
renameFile = true;
}
else if (Sync.defaultActionOnOverwrite == 'N')
{
SyncIO.printFlush("\n Skipping overwriting of existing " +
(existingIsDirectory ? "directory" : "file") +
" \"" + existingName + "\"");
}
else if (Sync.defaultActionOnOverwrite == '\0')
{
SyncIO.print("\n Overwrite existing " +
(existingIsDirectory ? "directory" : "file") +
" \"" + existingName + "\"?\n");
final char choice = SyncIO.userCharPrompt(
" (Y)es/(N)o/(A)lways/Neve(R): ",
"YNAR");
if (choice == 'Y')
{
renameFile = true;
}
else if (choice == 'A')
{
Sync.defaultActionOnOverwrite = 'Y';
renameFile = true;
}
else if (choice == 'R')
{
Sync.defaultActionOnOverwrite = 'N';
}
}
}
else
{
/* target file does not exist, or is the same File as the source */
renameFile = true;
}
if (renameFile)
{
/* delete existing file/directory first, if any */
if (existingFile != null)
{
if (existingIsDirectory)
{
final String error = deleteDirTreeOperation(existingFile);
if (error != null)
return "Failed to delete existing directory \"" +
trimTrailingSeparator(existingFile.getPath()) + File.separatorChar +
"\":\n" + error;
}
else
{
final boolean success = existingFile.delete();
if (!success)
return "Failed to delete existing file \"" +
existingFile.getPath() + "\" using Java's File.delete() method.";
}
}
/* rename file */
final boolean success = source.renameTo(target);
if (success)
{
return null; // success
}
else
{
return "Failed to rename file using Java's File.renameTo() method.";
}
}
return ""; // aborted file rename
}
/**
* Copy a specified file. An existing file/directory may be overwritten if necessary.
*
* @param source
* File to be copied
* @param target
* File object representing the target file
* @return
* null if the file was successfully copied, an empty string if file copy was aborted,
* an error message otherwise
*/
static String copyFile(
final File source,
final File target)
{
/* copy the specified file? */
boolean copyFile = false;
/* existing target file (to be overwritten), if any */
File existingFile = null;
String existingName = null;
boolean existingIsDirectory = false;
/* check if a target file/directory already exists */
if (target.exists())
{
/* get attributes of the existing file/directory */
try
{
existingFile = target.getCanonicalFile();
}
catch (Exception e)
{
existingFile = target;
}
existingIsDirectory = existingFile.isDirectory();
existingName = trimTrailingSeparator(existingFile.getName()) +
(existingIsDirectory ? File.separatorChar : "");
if (Sync.defaultActionOnOverwrite == 'Y')
{
SyncIO.printFlush("\n Overwriting existing " +
(existingIsDirectory ? "directory" : "file") +
" \"" + existingName + "\"");
copyFile = true;
}
else if (Sync.defaultActionOnOverwrite == 'N')
{
SyncIO.printFlush("\n Skipping overwriting of existing " +
(existingIsDirectory ? "directory" : "file") +
" \"" + existingName + "\"");
}
else if (Sync.defaultActionOnOverwrite == '\0')
{
SyncIO.print("\n Overwrite existing " +
(existingIsDirectory ? "directory" : "file") +
" \"" + existingName + "\"?\n");
final char choice = SyncIO.userCharPrompt(
" (Y)es/(N)o/(A)lways/Neve(R): ",
"YNAR");
if (choice == 'Y')
{
copyFile = true;
}
else if (choice == 'A')
{
Sync.defaultActionOnOverwrite = 'Y';
copyFile = true;
}
else if (choice == 'R')
{
Sync.defaultActionOnOverwrite = 'N';
}
}
}
else
{
/* target file does not exist */
copyFile = true;
}
if (copyFile)
{
/* delete existing file/directory first, if any */
if (existingFile != null)
{
if (existingIsDirectory)
{
/* delete existing directory first */
final String error = deleteDirTreeOperation(existingFile);
if (error != null)
return "Failed to delete existing directory \"" +
trimTrailingSeparator(existingFile.getPath()) + File.separatorChar +
"\":\n" + error;
}
else
{
final boolean success = existingFile.delete();
if (!success)
return "Failed to delete existing file \"" +
existingFile.getPath() + "\" using Java's File.delete() method.";
}
}
/* copy file */
final String error = copyFileOperation(source, target);
if (error == null)
{
return null; // success
}
else
{
return "Failed to copy file:\n" + error;
}
}
return ""; // aborted file copy
}
/**
* Delete a specified file/directory.
*
* @param file
* File object representing the file/directory to be deleted
* @return
* null if the file/directory is successfully deleted; an error message otherwise
*/
static String deleteFileDir(
final File file)
{
if (file.isDirectory())
{
final String error = deleteDirTreeOperation(file);
if (error != null)
return "Failed to delete directory:\n" + error;
}
else
{
final boolean success = file.delete();
if (!success)
return "Failed to delete file using Java's File.delete() method.";
}
return null; // success
}
/**
* Delete a directory and all its contents (subdirectories and
* files) recursively.
*
* @param dir
* Directory to be deleted, along with all its contents
* @return
* null if operation is successful; an error message otherwise
*/
private static String deleteDirTreeOperation(
final File dir)
{
/* error message(s), if any */
final List<String> errors = new ArrayList<String>();
/* perform a DFS deletion of the directory using two stacks: */
/* - fullDirs contains subdirectories whose files have yet to be deleted */
/* - emptyDirs contains empty subdirectories to be deleted, after their */
/* contents have been deleted */
final Deque<File> fullDirs = new ArrayDeque<File>();
final Deque<File> emptyDirs = new ArrayDeque<File>();
final File marker = new File(""); // special marker
fullDirs.push(dir);
DeleteNextDirectory:
while (!fullDirs.isEmpty())
{
final File fd = fullDirs.pop();
if (fd == marker)
{
final File ed = emptyDirs.pop();
final boolean success = ed.delete();
if (!success)
errors.add("Failed to delete directory \"" + ed.getPath() +
"\" using Java's File.delete() method (it could still be nonempty)");
continue DeleteNextDirectory;
}
/* get directory contents */
final File[] files = fd.listFiles();
if (files == null)
{
errors.add("Failed to get contents of directory \"" + fd.getPath() + "\"");
}
else
{
/* push this directory onto emptyStack for subsequent deletion */
emptyDirs.push(fd);
fullDirs.push(marker); // special marker
for (File f : files)
{
if (f.isDirectory())
{
fullDirs.push(f);
}
else
{
final boolean success = f.delete();
if (!success)
errors.add("Failed to delete file \"" + f.getPath() +
"\" using Java's File.delete() method");
}
}
}
}
if (!errors.isEmpty())
{
/* return concatenated error message */
final StringBuilder t = new StringBuilder();
for (String s : errors)
{
t.append(s);
t.append("; ");
}
t.delete(t.length() - 2, t.length());
return t.toString();
}
return null; // success
}
/**
* Copy a specified file.
*
* @param sourceFile
* Source file
* @param targetFile
* Target file
* @return
* null if operation is successful; an error message otherwise
*/
private static String copyFileOperation(
final File source,
final File target)
{
/* buffered input stream for reading */
BufferedInputStream bis = null;
/* buffered output stream for writing */
BufferedOutputStream bos = null;
try
{
/* error message(s), if any */
final List<String> errors = new ArrayList<String>();
try
{
bis = new BufferedInputStream(new FileInputStream(source));
}
catch (Exception e)
{
return "Failed to open source file for reading (" + e.getMessage() + ")";
}
/* parent directory of the target file */
final File targetParentDir = target.getParentFile();
/* create parent directory of target file, if necessary */
if (!targetParentDir.exists())
targetParentDir.mkdirs();
if (!targetParentDir.isDirectory())
return "Failed to create parent directory of target file";
try
{
bos = new BufferedOutputStream(new FileOutputStream(target));
}
catch (Exception e)
{
return "Failed to open target file for writing (" + e.getMessage() + ")";
}
/* byte buffer */
final byte byteBuffer[] = new byte[SyncIO.BUFFER_SIZE];
try
{
/* copy bytes from the source file to the target file */
while (true)
{
final int byteCount = bis.read(byteBuffer, 0, SyncIO.BUFFER_SIZE);
if (byteCount == -1)
break; /* reached EOF */
bos.write(byteBuffer, 0, byteCount);
}
}
catch (Exception e)
{
return "Failed to copy data from source file to target file (" + e.getMessage() + ")";
}
try
{
bis.close();
bis = null;
}
catch (Exception e)
{
errors.add("Failed to close source file after reading (" + e.getMessage() + ")");
}
try
{
bos.close();
bos = null;
}
catch (Exception e)
{
errors.add("Failed to close target file after writing (" + e.getMessage() + ")");
}
final boolean success = target.setLastModified(source.lastModified());
if (!success)
errors.add("Failed to set last-modified time of target file after writing");
if (!errors.isEmpty())
{
/* return concatenated error message */
final StringBuilder t = new StringBuilder();
for (String s : errors)
{
t.append(s);
t.append("; ");
}
t.delete(t.length() - 2, t.length());
return t.toString();
}
return null; // success
}
finally
{
/* close buffered input stream for reading */
if (bis != null)
{
try
{
bis.close();
}
catch (Exception e)
{
/* ignore */
}
}
/* close buffered output stream for writing */
if (bos != null)
{
try
{
bos.close();
}
catch (Exception e)
{
/* ignore */
}
}
}
}
/**
* Compute the CRC-32 checksum of a file.
*
* @param file
* File for which to compute the CRC-32 checksum
* @return
* Result of the CRC-32 checksum computation
*/
static Crc32Checksum getCrc32Checksum(
final File file)
{
/* checksum of directory is defined as 0 */
if (file.isDirectory())
return new Crc32Checksum(null, 0L);
/* buffered input stream for reading */
BufferedInputStream bis = null;
try
{
try
{
bis = new BufferedInputStream(new FileInputStream(file));
}
catch (Exception e)
{
return new Crc32Checksum("Failed to open file for reading (" + e.getMessage() + ")", 0L);
}
/* byte buffer */
final byte byteBuffer[] = new byte[SyncIO.BUFFER_SIZE];
/* CRC-32 object to track checksum computation */
final CRC32 crc32 = new CRC32();
try
{
/* read bytes from the file, and track the checksum computation */
while (true)
{
final int byteCount = bis.read(byteBuffer, 0, SyncIO.BUFFER_SIZE);
if (byteCount == -1)
break; /* reached EOF */
crc32.update(byteBuffer, 0, byteCount);
}
}
catch (Exception e)
{
return new Crc32Checksum("Failed to read data from file (" + e.getMessage() + ")", 0L);
}
try
{
bis.close();
bis = null;
}
catch (Exception e)
{
/* ignore */
}
/* successful computation of CRC-32 checksum */
return new Crc32Checksum(null, crc32.getValue());
}
finally
{
/* close buffered input stream for reading */
if (bis != null)
{
try
{
bis.close();
}
catch (Exception e)
{
/* ignore */
}
}
}
}
/**
* Removes a trailing separator, if any, in the specified path string.
*
* @param path
* Path string to be trimmed
* @return
* Path string after removal of a trailing separator
*/
static String trimTrailingSeparator(
final String path)
{
if (path.endsWith(File.separator))
return path.substring(0, path.length() - 1);
return path;
}
/**
* Prompt user for a single-character input.
*
* @param prompt
* Prompt string
* @param ops
* Options string containing permitted character responses (automatically converted
* to upper case)
* @return
* Character chosen by the user (automatically converted to upper case)
*/
static char userCharPrompt(
final String prompt,
final String ops)
{
/* case-insensitive comparison; convert everything to uppercase */
final String options = ops.toUpperCase(Locale.ENGLISH);
final Scanner kb = new Scanner(System.in);
while (true)
{
printFlush(prompt);
String response = kb.nextLine();
printLog(response + "\n");
response = response.trim();
if (response.length() != 1)
continue;
/* convert to char */
final char c = response.toUpperCase(Locale.ENGLISH).charAt(0);
if (options.indexOf(c) >= 0)
return c;
}
}
/**
* Convenience method to print to standard output and log file (if any).
*
* @param s
* String to be printed
*/
static void print(
final String s)
{
Sync.stdout.print(s);
if (Sync.log != null)
Sync.log.print(s);
}
/**
* Convenience method to print to standard output and log file (if any),
* and flush the buffers.
*
* @param s
* String to be printed
*/
static void printFlush(
final String s)
{
Sync.stdout.print(s);
Sync.stdout.flush();
if (Sync.log != null)
{
Sync.log.print(s);
Sync.log.flush();
}
}
/**
* Convenience method to print to standard error and log file (if any).
*
* @param s
* String to be printed
*/
static void printToErr(
final String s)
{
Sync.stderr.print(s);
Sync.stderr.flush();
if (Sync.log != null)
{
Sync.log.print(s);
Sync.log.flush();
}
}
/**
* Convenience method to print to log file (if any) only.
*
* @param s
* String to be printed
*/
static void printLog(
final String s)
{
if (Sync.log != null)
{
Sync.log.print(s);
Sync.log.flush();
}
}
/**
* Inner class to represent the result of a file CRC-32 computation.
*/
static class Crc32Checksum
{
/** null if operation is successful; an error message otherwise */
public String error = null;
/** file CRC-32 checksum value */
public long checksum;
/**
* Constructor.
*
* @param error
* null if operation is successful; an error message otherwise
* @param checksum
* File CRC-32 checksum value
*/
Crc32Checksum(
final String error,
final long checksum)
{
this.error = error;
this.checksum = checksum;
}
}
}