/**
* 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.File;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.zip.CRC32;
/**
* Simple class for performing common file input/output operations.
*/
public class FileIO
{
/** buffer size (1 Mb) */
private static final int bufferSize = 1048576;
/** neutral separator char */
public static final char neutralSeparatorChar = '/';
/**
* Rename a given file or directory.
*
* @param isDir
* True if renaming a directory, false otherwise
* @param sourceFile
* Source file/directory to be renamed
* @param targetFile
* Target file/directory to which the source file/directory is to be renamed
* @return
* Result of the rename operation
*/
private static FileIOResult renameFileDir(
final boolean isDir,
final File sourceFile,
final File targetFile)
{
/* check if source file/directory exists */
if (!sourceFile.exists())
return new FileIOResult(false,
"Source " + ((isDir) ? "directory" : "file") + " does not exist.");
/* check if source is a file/directory as specified */
if (isDir != sourceFile.isDirectory())
return new FileIOResult(false,
"Source is not a " + ((isDir) ? "directory." : "file."));
/* check if target file is a distinct existing file/directory */
if (targetFile.exists() && !targetFile.equals(sourceFile))
return new FileIOResult(false,
"Target is an existing " + ((targetFile.isDirectory()) ? "directory." : "file."));
boolean success = false;
String exceptionMessage = null;
try
{
success = sourceFile.renameTo(targetFile);
}
catch (Exception e)
{
exceptionMessage = ErrorWarningHandler.getExceptionMessage(e);
}
if (success)
{
return new FileIOResult(true, null);
}
else
{
if (exceptionMessage == null)
{
return new FileIOResult(false,
((isDir) ? "Directory" : "File") + " rename was unsuccessful.");
}
else
{
return new FileIOResult(false,
"Java exception encountered during " +
((isDir) ? "directory" : "file") + " rename:\n" + exceptionMessage);
}
}
}
/**
* Rename a given file.
*
* @param sourceFile
* Source file to be renamed
* @param targetFile
* Target file to which the source file is to be renamed
* @return
* Result of the rename operation
*/
public static FileIOResult renameFile(
final File sourceFile,
final File targetFile)
{
return renameFileDir(false, sourceFile, targetFile);
}
/**
* Rename a given directory.
*
* @param sourceDir
* Source directory to be renamed
* @param targetDir
* Target directory to which the source directory is to be renamed
* @return
* Result of the rename operation
*/
public static FileIOResult renameDir(
final File sourceDir,
final File targetDir)
{
return renameFileDir(true, sourceDir, targetDir);
}
/**
* Set last-modified time of a given file or directory.
*
* @param isDir
* True if setting last-modified time of a directory, false otherwise
* @param file
* File/directory of which last-modified time is to be set
* @param time
* New last-modified time, measured in milliseconds since the epoch
* (00:00:00 GMT, January 1, 1970)
* @return
* Result of the set last-modified time operation
*/
private static FileIOResult setFileDirTime(
final boolean isDir,
final File file,
final long time)
{
/* check if file/directory exists */
if (!file.exists())
return new FileIOResult(false,
((isDir) ? "Directory" : "File") + " does not exist.");
/* check if file/directory is a file/directory as specified */
if (isDir != file.isDirectory())
return new FileIOResult(false,
"Specified " + ((isDir) ? "directory" : "file") +
" is not a " + ((isDir) ? "directory." : "file."));
boolean success = false;
String exceptionMessage = null;
try
{
success = file.setLastModified(time);
}
catch (Exception e)
{
exceptionMessage = ErrorWarningHandler.getExceptionMessage(e);
}
if (success)
{
return new FileIOResult(true, null);
}
else
{
if (exceptionMessage == null)
{
return new FileIOResult(false,
"Setting of last-modified time of " +
((isDir) ? "directory" : "file") + " was unsuccessful.");
}
else
{
return new FileIOResult(false,
"Java exception encountered during " + "setting of last-modified time of " +
((isDir) ? "directory:\n" : "file:\n") + exceptionMessage);
}
}
}
/**
* Set last-modified time of a given file.
*
* @param file
* File of which last-modified time is to be set
* @param time
* New last-modified time, measured in milliseconds since the epoch
* (00:00:00 GMT, January 1, 1970)
* @return
* Result of the set last-modified time operation
*/
public static FileIOResult setFileTime(
final File file,
final long time)
{
return setFileDirTime(false, file, time);
}
/**
* Set last-modified time of a given directory.
*
* @param dir
* Directory of which last-modified time is to be set
* @param time
* New last-modified time, measured in milliseconds since the epoch
* (00:00:00 GMT, January 1, 1970)
* @return
* Result of the set last-modified time operation
*/
public static FileIOResult setDirTime(
final File dir,
final long time)
{
return setFileDirTime(true, dir, time);
}
/**
* Create directory.
*
* @param dir
* Directory to be created
* @return
* Result of the directory creation operation
*/
public static FileIOResult createDir(
final File dir)
{
boolean success = false;
String exceptionMessage = null;
try
{
success = dir.mkdirs();
}
catch (Exception e)
{
exceptionMessage = ErrorWarningHandler.getExceptionMessage(e);
}
if (success)
{
return new FileIOResult(true, null);
}
else
{
if (exceptionMessage == null)
{
return new FileIOResult(false,
"Directory creation was unsuccessful.");
}
else
{
return new FileIOResult(false,
"Java exception encountered during directory creation:\n" + exceptionMessage);
}
}
}
/**
* Get directory contents.
*
* @param dir
* Directory of which to get contents
* @param files
* ArrayList of files to which files in the specified directory
* will be added
* @param dirs
* ArrayList of directories to which directories in the specified
* directory will be added
* @return
* Result of the directory operation
*/
public static FileIOResult getDirContents(
final File dir,
final List<File> files,
final List<File> dirs)
{
/* check if specified file/directory exists */
if (!dir.exists())
return new FileIOResult(false,
"Directory does not exist.");
/* check if specified directory is a directory */
if (!dir.isDirectory())
return new FileIOResult(false,
"Specified directory is not a directory.");
/* get list of files and directories in the specified directory */
final File listFiles[] = dir.listFiles();
if (listFiles == null)
{
return new FileIOResult(false,
"Failed to list contents of directory.");
}
else
{
for (File f : listFiles)
{
if (f.isDirectory())
{
dirs.add(f);
}
else
{
files.add(f);
}
}
/* sort lists */
Collections.sort(files);
Collections.sort(dirs);
return new FileIOResult(true, null);
}
}
/**
* Delete a file or directory.
*
* @param isDir
* True if deleting a directory, false otherwise
* @param file
* File/directory to be deleted
* @return
* Result of the file/directory delete operation
*/
private static FileIOResult deleteFileDir(
final boolean isDir,
final File file)
{
/* check if specified file/directory exists */
if (!file.exists())
return new FileIOResult(false,
((isDir) ? "Directory" : "File") + " does not exist.");
/* check if file/directory is a file/directory as specified */
if (isDir != file.isDirectory())
return new FileIOResult(false,
"Specified " + ((isDir) ? "directory" : "file") +
" is not a " + ((isDir) ? "directory." : "file."));
boolean success = false;
String exceptionMessage = null;
try
{
success = file.delete();
}
catch (Exception e)
{
exceptionMessage = ErrorWarningHandler.getExceptionMessage(e);
}
if (success)
{
return new FileIOResult(true, null);
}
else
{
if (exceptionMessage == null)
{
return new FileIOResult(false,
"Deleting of " + ((isDir) ? "directory" : "file") + " was unsuccessful.");
}
else
{
return new FileIOResult(false,
"Java exception encountered during deleting of " +
((isDir) ? "directory:\n" : "file:\n") + exceptionMessage);
}
}
}
/**
* Delete a file.
*
* @param file
* File to be deleted
* @return
* Result of the file delete operation
*/
public static FileIOResult deleteFile(
final File file)
{
return deleteFileDir(false, file);
}
/**
* Delete a directory.
*
* @param dir
* Directory to be deleted
* @return
* Result of the directory delete operation
*/
public static FileIOResult deleteDir(
final File dir)
{
return deleteFileDir(true, dir);
}
/**
* Delete a directory and all its contents (subdirectories and
* files) recursively.
*
* @param dir
* Directory to be deleted, along with all its contents
* @return
* Result of the directory tree delete operation
*/
public static FileIOResult deleteDirTree(
final File dir)
{
final List<File> files = new ArrayList<File>();
final List<File> dirs = new ArrayList<File>();
/* get contents of the specified directory */
final FileIOResult getDirContentsResult = getDirContents(dir, files, dirs);
if (!getDirContentsResult.success)
return new FileIOResult(false, getDirContentsResult.errorMessage);
/* error messages, if any */
final StringBuilder errorMessages = new StringBuilder();
/* delete files */
for (File f : files)
{
final FileIOResult deleteFileResult = deleteFile(f);
if (!deleteFileResult.success)
errorMessages.append("\nFailed to delete file \"" + f.getPath() +
"\":\n" + deleteFileResult.errorMessage);
}
/* delete directories recursively */
for (File d : dirs)
{
final FileIOResult deleteDirTreeResult = deleteDirTree(d);
if (!deleteDirTreeResult.success)
errorMessages.append("\nFailed to delete directory \"" + d.getPath() +
"\":\n" + deleteDirTreeResult.errorMessage);
}
/* delete this (specified) directory itself */
final FileIOResult deleteDirResult = deleteDir(dir);
if (!deleteDirResult.success)
errorMessages.append("\nFailed to delete directory \"" + dir.getPath() +
"\":\n" + deleteDirResult.errorMessage);
/* result of directory tree delete operation */
return new FileIOResult(deleteDirResult.success, errorMessages.toString());
}
/**
* Copy a file.
*
* @param sourceFile
* Source file
* @param targetFile
* Target file
* @return
* Result of file copy operation
*/
public static FileIOResult copyFile(
final File sourceFile,
final File targetFile)
{
/* buffered input stream for reading */
BufferedInputStream bis = null;
/* buffered output stream for writing */
BufferedOutputStream bos = null;
try
{
/* error messages, if any */
final StringBuilder errorMessages = new StringBuilder();
try
{
bis = new BufferedInputStream(new FileInputStream(sourceFile));
}
catch (Exception e)
{
errorMessages.append("\nUnable to open source file for reading:\n" +
ErrorWarningHandler.getExceptionMessage(e));
return new FileIOResult(false, errorMessages.toString());
}
/* parent directory of the target file */
final File targetParentDir = targetFile.getParentFile();
/* create parent directory of target file, if necessary */
if (!targetParentDir.exists())
createDir(targetParentDir);
if (!targetParentDir.isDirectory())
{
errorMessages.append("\nUnable to create parent directory of target file.");
return new FileIOResult(false, errorMessages.toString());
}
try
{
bos = new BufferedOutputStream(new FileOutputStream(targetFile));
}
catch (Exception e)
{
errorMessages.append("\nUnable to open target file for writing:\n" +
ErrorWarningHandler.getExceptionMessage(e));
return new FileIOResult(false, errorMessages.toString());
}
/* byte buffer */
final byte byteBuffer[] = new byte[FileIO.bufferSize];
try
{
/* copy bytes from the source file to the target file */
while (true)
{
final int byteCount = bis.read(byteBuffer, 0, FileIO.bufferSize);
if (byteCount == -1)
break; /* reached EOF */
bos.write(byteBuffer, 0, byteCount);
}
}
catch (Exception e)
{
errorMessages.append("\nUnable to copy data from source file to target file:\n" +
ErrorWarningHandler.getExceptionMessage(e));
return new FileIOResult(false, errorMessages.toString());
}
try
{
bis.close();
bis = null;
}
catch (Exception e)
{
errorMessages.append("\nUnable to close source file after reading:\n");
errorMessages.append(ErrorWarningHandler.getExceptionMessage(e));
}
try
{
bos.close();
bos = null;
}
catch (Exception e)
{
errorMessages.append("\nUnable to close target file after writing:\n");
errorMessages.append(ErrorWarningHandler.getExceptionMessage(e));
}
final FileIOResult setFileTimeResult = setFileTime(targetFile, sourceFile.lastModified());
if (!setFileTimeResult.success)
{
errorMessages.append("\nUnable to set last-modified time of target file:\n");
errorMessages.append(setFileTimeResult.errorMessage);
}
/* result of file copy operation */
return new FileIOResult(setFileTimeResult.success, errorMessages.toString());
}
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
*/
public static ComputeFileCRC32Result computeFileCRC32(
final File file)
{
/* checksum of directory is defined as 0 */
if (file.isDirectory())
return new ComputeFileCRC32Result(true, null, 0L);
/* buffered input stream for reading */
BufferedInputStream bis = null;
try
{
/* error messages, if any */
final StringBuilder errorMessages = new StringBuilder();
try
{
bis = new BufferedInputStream(new FileInputStream(file));
}
catch (Exception e)
{
errorMessages.append("\nUnable to open file for reading:\n" +
ErrorWarningHandler.getExceptionMessage(e) +
"\nAssuming file CRC-32 checksum of 0.");
return new ComputeFileCRC32Result(false, errorMessages.toString(), 0L);
}
/* byte buffer */
final byte byteBuffer[] = new byte[FileIO.bufferSize];
/* 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, FileIO.bufferSize);
if (byteCount == -1)
break; /* reached EOF */
crc32.update(byteBuffer, 0, byteCount);
}
}
catch (Exception e)
{
errorMessages.append("\nUnable to read data from file to compute checksum:\n" +
ErrorWarningHandler.getExceptionMessage(e) +
"\nAssuming file CRC-32 checksum of 0.");
return new ComputeFileCRC32Result(false, errorMessages.toString(), 0L);
}
try
{
bis.close();
bis = null;
}
catch (Exception e)
{
errorMessages.append("\nUnable to close file after reading:\n");
errorMessages.append(ErrorWarningHandler.getExceptionMessage(e));
}
/* successful computation of CRC-32 checksum */
return new ComputeFileCRC32Result(true, errorMessages.toString(), crc32.getValue());
}
finally
{
/* close buffered input stream for reading */
if (bis != null)
{
try
{
bis.close();
}
catch (Exception e)
{
/* ignore */
}
}
}
}
/**
* Convert a path string from native form to neutral form.
* In neutral form, the separator is always FileIO.neutralSeparatorChar.
*
* @param path
* The native path string to be converted.
* @return
* The neutral form of the path string.
*/
public static String nativeToNeutral(
final String path)
{
return path.replace(File.separatorChar, FileIO.neutralSeparatorChar);
}
/**
* Convert a path string from neutral form to native form.
* In neutral form, the separator is always FileIO.neutralSeparatorChar.
*
* @param path
* The neutral path string to be converted.
* @return
* The native form of the path string.
*/
public static String neutralToNative(
final String path)
{
return path.replace(FileIO.neutralSeparatorChar, File.separatorChar);
}
/**
* 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
*/
public static String trimTrailingSeparator(
final String path)
{
if (path.endsWith(File.separator))
return path.substring(0, path.length() - 1);
return path;
}
/**
* Inner class to represent the result of a file IO operation.
*/
public static class FileIOResult
{
/** file IO operation is successful if true; unsuccessful otherwise */
public boolean success = false;
/** error message, if any */
public String errorMessage = null;
/**
* Constructor.
*
* @param success
* File IO operation is successful if true; unsuccessful otherwise
* @param errorMessage
* Error message, if any
*/
FileIOResult(
final boolean success,
final String errorMessage)
{
this.success = success;
this.errorMessage = errorMessage;
}
}
/**
* Inner class to represent the result of a file CRC-32 computation.
*/
public static class ComputeFileCRC32Result
{
/** file CRC-32 computation is successful if true; unsuccessful otherwise */
public boolean success = false;
/** error message, if any */
public String errorMessage = null;
/** file CRC-32 checksum value */
public long checksum;
/**
* Constructor.
*
* @param success
* File CRC-32 computation is successful if true; unsuccessful otherwise
* @param errorMessage
* Error message, if any
* @param checksum
* File CRC-32 checksum value
*/
ComputeFileCRC32Result(
final boolean success,
final String errorMessage,
final long checksum)
{
this.success = success;
this.errorMessage = errorMessage;
this.checksum = checksum;
}
}
}