/**
* 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.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
/**
* Represent a ZipSnap catalog.
*/
public class Catalog
{
/** snapshots in this catalog */
final List<SnapshotUnit> snapshots;
/** files/directories in this catalog (to be sorted by their name field) */
final List<FileUnit> files;
/**
* Regex pattern for ZIPSNAP_VERSION line:
* ZIPSNAP_VERSION:version
* 1 (capture group index)
*/
private static final Pattern patternZipSnapVersion = Pattern.compile(
Pattern.quote("v:") + "([0-9]+\\.[0-9]+)");
/**
* Regex pattern for NUMBER_OF_SNAPSHOTS line:
* NUMBER_OF_SNAPSHOTS:number
* 1 (capture group index)
*/
private static final Pattern patternNumberOfSnapshots = Pattern.compile(
Pattern.quote("ns:") + "([0-9]+)");
/**
* Regex pattern for NUMBER_OF_FILES line:
* NUMBER_OF_FILES:number
* 1 (capture group index)
*/
private static final Pattern patternNumberOfFiles = Pattern.compile(
Pattern.quote("nf:") + "([0-9]+)");
/**
* Regex pattern for SNAPSHOT line (before version 2.0):
* SNAPSHOT:number:time
* 1 2 (capture group index)
*/
private static final Pattern patternSnapshot1 = Pattern.compile(
Pattern.quote("s:") + "([0-9]+)" +
Pattern.quote(":") + "([0-9\\-]+)");
/**
* Regex pattern for SNAPSHOT line:
* SNAPSHOT:number;time
* 1 2 (capture group index)
*/
private static final Pattern patternSnapshot = Pattern.compile(
Pattern.quote("s:") + "([0-9]+)" +
Pattern.quote(";") + "([0-9\\-]+)");
/**
* Regex pattern for FILE line (before version 2.0):
* FILE:number:[name],size,time(in sec),checksum,snapshots
* 1 2 3 4 5 6 (capture group index)
*/
private static final Pattern patternFile1 = Pattern.compile(
Pattern.quote("f:") + "([0-9]+)" +
Pattern.quote(":[") + "(.*)" +
Pattern.quote("],") + "([0-9]+)" +
Pattern.quote(",") + "([0-9]+)" +
Pattern.quote(",") + "([0-9]+)" +
Pattern.quote(",") + "([0-9\\,]+)");
/**
* Regex pattern for FILE line:
* FILE:name;size;time;checksum;snapshots
* 1 2 3 4 5 (capture group index)
*/
private static final Pattern patternFile = Pattern.compile(
Pattern.quote("f:") + "(.*)" +
Pattern.quote(";") + "([0-9]+)" +
Pattern.quote(";") + "([0-9]+)" +
Pattern.quote(";") + "([0-9a-fA-F]+)" +
Pattern.quote(";") + "([0-9\\,]+)");
/**
* Constructor for initializing an empty catalog.
*/
Catalog()
{
this.snapshots = new ArrayList<SnapshotUnit>();
this.files = new ArrayList<FileUnit>();
}
/**
* Constructor for intializing a catalog with entries
* read from the catalog of an existing ZipSnap archive.
*
* @param catalogFile
* File object representing the catalog to be read
*/
Catalog(
final File catalogFile)
{
/* initialize empty catalog */
this.snapshots = new ArrayList<SnapshotUnit>();
this.files = new ArrayList<FileUnit>();
final String catalogFileName = catalogFile.getName();
/* check if the catalog file already exists */
if (!catalogFile.exists())
ErrorWarningHandler.reportErrorAndExit("Catalog \"" + catalogFileName + "\" does not exist.");
BufferedReader br = null;
ZipFile zf = null;
JarFile jf = null;
try
{
if (catalogFileName.endsWith(".txt"))
{
/* read a plain text file */
br = new BufferedReader(new FileReader(catalogFile));
}
else if (catalogFileName.endsWith(".txt.zip"))
{
/* read a compressed text file inside a ZIP volume */
zf = new ZipFile(catalogFile);
br = new BufferedReader(new InputStreamReader(
zf.getInputStream(zf.getEntry(
catalogFileName.substring(0, catalogFileName.lastIndexOf("."))))));
}
else if (catalogFileName.endsWith(".txt.jar"))
{
/* read a compressed text file inside a JAR volume */
jf = new JarFile(catalogFile);
br = new BufferedReader(new InputStreamReader(
jf.getInputStream(jf.getEntry(
catalogFileName.substring(0, catalogFileName.lastIndexOf("."))))));
}
else
{
ErrorWarningHandler.reportErrorAndExit("(INTERNAL) Catalog \"" + catalogFileName +
"\" has an invalid file extension.");
}
}
catch (Exception e)
{
ErrorWarningHandler.reportErrorAndExit("Unable to open catalog \"" + catalogFileName +
"\":\n" + ErrorWarningHandler.getExceptionMessage(e));
}
/* number of snapshots */
int numSnapshots = 0;
/* number of files */
int numFiles = 0;
/* ZipSnap program version number */
double versionNum = 0;
/* read and parse each line of the catalog */
ParseNextLine:
for (int lineNum = 1; ; lineNum++)
{
String lineString = null;
try
{
if (!br.ready())
break ParseNextLine;
lineString = br.readLine().trim();
}
catch (Exception e)
{
ErrorWarningHandler.reportErrorAndExit("Unable to read line " + lineNum + " of catalog \"" +
catalogFileName + "\":\n" + ErrorWarningHandler.getExceptionMessage(e));
}
/* skip this line if empty or if it begins with '#' */
if (lineString.isEmpty() || lineString.startsWith("#"))
continue ParseNextLine;
String s = lineString;
try
{
if (s.startsWith("v:"))
{
/* ZIPSNAP_VERSION:version */
/* 1 (capture group index) */
final Matcher m = Catalog.patternZipSnapVersion.matcher(s);
if (!m.matches())
throw new Exception("Malformed ZIPSNAP_VERSION line.");
versionNum = Double.parseDouble(m.group(1));
if (versionNum > ZipSnap.version)
ErrorWarningHandler.reportWarning("Catalog \"" + catalogFileName +
"\" was created by a newer version of ZipSnap, and may not be read correctly.");
}
else if (s.startsWith("ns:"))
{
/* NUMBER_OF_SNAPSHOTS:number */
/* 1 (capture group index) */
final Matcher m = Catalog.patternNumberOfSnapshots.matcher(s);
if (!m.matches())
throw new Exception("Malformed NUMBER_OF_SNAPSHOTS line.");
numSnapshots = Integer.parseInt(m.group(1));
}
else if (s.startsWith("nf:"))
{
/* NUMBER_OF_FILES:number */
/* 1 (capture group index) */
final Matcher m = Catalog.patternNumberOfFiles.matcher(s);
if (!m.matches())
throw new Exception("Malformed NUMBER_OF_FILES line.");
numFiles = Integer.parseInt(m.group(1));
}
else if (s.startsWith("s:"))
{
if (versionNum < 2.0)
{
/* SNAPSHOT:number:time */
/* 1 2 (capture group index) */
final Matcher m = Catalog.patternSnapshot1.matcher(s);
if (!m.matches())
throw new Exception("Malformed SNAPSHOT line.");
final int number = Integer.parseInt(m.group(1));
if (number != (this.snapshots.size() + 1))
{
ErrorWarningHandler.reportWarning("Malformed snapshot entry found on line " + lineNum +
" of catalog \"" + catalogFileName + "\":\n\"" + lineString + "\"\n" +
"Expected snapshot number " + (this.snapshots.size() + 1) +
", but found snapshot number " + number + ".\n" +
"Snapshot entries must be listed in ascending order of snapshot number.\n" +
"This snapshot entry will be ignored.");
continue ParseNextLine;
}
/* add snapshot to catalog */
this.snapshots.add(new SnapshotUnit(m.group(2)));
}
else
{
/* SNAPSHOT:number;time */
/* 1 2 (capture group index) */
final Matcher m = Catalog.patternSnapshot.matcher(s);
if (!m.matches())
throw new Exception("Malformed SNAPSHOT line.");
final int number = Integer.parseInt(m.group(1));
if (number != (this.snapshots.size() + 1))
{
ErrorWarningHandler.reportWarning("Malformed snapshot entry found on line " + lineNum +
" of catalog \"" + catalogFileName + "\":\n\"" + lineString + "\"\n" +
"Expected snapshot number " + (this.snapshots.size() + 1) +
", but found snapshot number " + number + ".\n" +
"Snapshot entries must be listed in ascending order of snapshot number.\n" +
"This snapshot entry will be ignored.");
continue ParseNextLine;
}
/* add snapshot to catalog */
this.snapshots.add(new SnapshotUnit(m.group(2)));
}
}
else if (s.startsWith("f:"))
{
if (versionNum < 2.0)
{
/* FILE:number:[name],size,time(in sec),checksum,snapshots */
/* 1 2 3 4 5 6 (capture group index) */
final Matcher m = Catalog.patternFile1.matcher(s);
if (!m.matches())
throw new Exception("Malformed FILE line.");
final int number = Integer.parseInt(m.group(1));
if (number != (this.files.size() + 1))
{
ErrorWarningHandler.reportWarning("Malformed file entry found on line " + lineNum +
" of catalog \"" + catalogFileName + "\":\n\"" + lineString + "\"\n" +
"Expected file number " + (this.files.size() + 1) +
", but found file number " + number + ".\n" +
"File entries must be listed in ascending order of file number.\n" +
"This file entry will be ignored.");
continue ParseNextLine;
}
/* neutral and relative pathname of file/directory */
final String name = m.group(2);
/* native and relative pathname of file/directory */
final String nativeName = FileIO.neutralToNative(name);
/* File object representing this file/directory (native and absolute pathname) */
final File file = new File(ZipSnap.currentDir, nativeName);
/* true if this FileUnit represents a directory; false otherwise */
final boolean isDirectory = name.endsWith(FileIO.neutralSeparatorChar + "");
final FileUnit u = new FileUnit(nativeName, file, isDirectory);
u.name = name;
u.size = Long.parseLong(m.group(3));
u.time = Long.parseLong(m.group(4)) * 1000; /* convert "seconds" to "milliseconds" */
u.setCrc(Long.parseLong(m.group(5)));
/* snapshots that contain this file/directory */
final List<Integer> snapshots = new ArrayList<Integer>();
final String[] tokens = m.group(6).split("\\,");
for (String t : tokens)
snapshots.add(Integer.parseInt(t) - 1); /* convert "number" to "index" */
u.snapshots.addAll(snapshots);
/* add file/directory to catalog */
this.files.add(u);
}
else
{
/* FILE:name;size;time;checksum;snapshots */
/* 1 2 3 4 5 (capture group index) */
final Matcher m = Catalog.patternFile.matcher(s);
if (!m.matches())
throw new Exception("Malformed FILE line.");
/* neutral and relative pathname of file/directory */
final String name = catalogToNeutral(m.group(1));
if (name == null)
throw new Exception("Malformed file/directory name \"" + m.group(1) + "\".");
/* native and relative pathname of file/directory */
final String nativeName = FileIO.neutralToNative(name);
/* File object representing this file/directory (native and absolute pathname) */
final File file = new File(ZipSnap.currentDir, nativeName);
/* true if this FileUnit represents a directory; false otherwise */
final boolean isDirectory = name.endsWith(FileIO.neutralSeparatorChar + "");
final FileUnit u = new FileUnit(nativeName, file, isDirectory);
u.name = name;
u.size = Long.parseLong(m.group(2));
u.time = Long.parseLong(m.group(3));
u.setCrc(Long.parseLong(m.group(4), 16));
/* snapshots that contain this file/directory */
final List<Integer> snapshots = new ArrayList<Integer>();
final String[] tokens = m.group(5).split("\\,");
for (String t : tokens)
snapshots.add(Integer.parseInt(t) - 1); /* convert "number" to "index" */
u.snapshots.addAll(snapshots);
/* add file/directory to catalog */
this.files.add(u);
}
}
else
{
throw new Exception("Unexpected line encountered.");
}
}
catch (Exception e)
{
ErrorWarningHandler.reportWarning("Unable to parse line " + lineNum +
" of catalog \"" + catalogFileName + "\":\n\"" + lineString + "\"\n" +
ErrorWarningHandler.getExceptionMessage(e) + "\nThis line will be ignored.");
continue ParseNextLine;
}
}
/* close catalog reader */
try
{
if (catalogFileName.endsWith(".txt"))
{
br.close();
}
else if (catalogFileName.endsWith(".txt.zip"))
{
br.close();
zf.close();
}
else if (catalogFileName.endsWith(".txt.jar"))
{
br.close();
jf.close();
}
}
catch (Exception e)
{
ErrorWarningHandler.reportWarning("Unable to close catalog \"" + catalogFileName +
"\"\n(file may not be read successfully):\n" +
ErrorWarningHandler.getExceptionMessage(e));
}
/* check the counts */
if (numSnapshots != this.snapshots.size())
ErrorWarningHandler.reportWarning("Inconsistent number of snapshots found in catalog \"" + catalogFileName + "\":\n" +
"Expected " + numSnapshots + " snapshots, but parsed " + this.snapshots.size() + ".");
if (numFiles != this.files.size())
ErrorWarningHandler.reportWarning("Inconsistent number of files found in catalog \"" + catalogFileName + "\":\n" +
"Expected " + numFiles + " files, but parsed " + this.files.size() + ".");
/* sort files/directories in catalog */
this.sort();
}
/**
* Write catalog to a file.
*
* @param catalogFile
* File object representing the catalog to be written
*/
void writeToFile(
final File catalogFile)
{
final String catalogFileName = catalogFile.getName();
if (ZipSnap.simulateOnly)
{
System.out.print("\n\nSimulating creation of new catalog \"" + catalogFileName + "\"...");
return;
}
System.out.print("\n\nCreating new catalog \"" + catalogFileName + "\"...");
final File parentDir = catalogFile.getParentFile();
/* create parent directories if necessary */
if (!parentDir.exists())
parentDir.mkdirs();
/* check that the parent directory of the catalog exists */
if (!parentDir.isDirectory())
ErrorWarningHandler.reportErrorAndExit( "Unable to create catalog \"" + catalogFileName + "\":\nThe parent directory \"" +
parentDir.getPath() + "\" does not exist, and cannot be created.");
/* check if the catalog file already exists */
if (catalogFile.exists())
ErrorWarningHandler.reportErrorAndExit( "Unable to create catalog \"" + catalogFileName + "\":\nA " +
(catalogFile.isDirectory() ? "directory" : "file") + " of the same name already exists.");
PrintWriter pw = null;
/* open the catalog writer */
try
{
if (catalogFileName.endsWith(".txt"))
{
/* write a plain text file */
pw = new PrintWriter(new BufferedWriter(new FileWriter(catalogFile)));
}
else if (catalogFileName.endsWith(".txt.zip"))
{
/* write a compressed text file to a ZIP volume */
final ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(catalogFile));
zos.setLevel(9); /* use maximum compression */
zos.putNextEntry(new ZipEntry(
catalogFileName.substring(0, catalogFileName.lastIndexOf("."))));
pw = new PrintWriter(new BufferedWriter(new OutputStreamWriter(zos)));
}
else if (catalogFileName.endsWith(".txt.jar"))
{
/* write a compressed text file to a JAR volume */
final JarOutputStream jos = new JarOutputStream(new FileOutputStream(catalogFile));
jos.setLevel(9); /* use maximum compression */
jos.putNextEntry(new ZipEntry(
catalogFileName.substring(0, catalogFileName.lastIndexOf("."))));
pw = new PrintWriter(new BufferedWriter(new OutputStreamWriter(jos)));
}
else
{
ErrorWarningHandler.reportErrorAndExit("(INTERNAL) Catalog \"" + catalogFileName +
"\" has an invalid file extension.");
}
}
catch (Exception e)
{
ErrorWarningHandler.reportErrorAndExit("Unable to create new catalog \"" + catalogFileName + "\":\n" +
ErrorWarningHandler.getExceptionMessage(e));
}
pw.println("# " + ZipSnap.programTitle);
pw.println("# COMMENT: Lines beginning with a '#' character are ignored by ZipSnap");
/* ZipSnap version */
pw.println();
pw.println("# ZipSnap version");
pw.println("v:" + ZipSnap.version);
/* Snapshots */
pw.println();
pw.println("# SNAPSHOT:number;time");
for (int i = 0; i < this.snapshots.size(); i++)
pw.println("s:" + (i + 1) + ";" + this.snapshots.get(i).time);
pw.println("# Number of snapshots");
pw.println("ns:" + this.snapshots.size());
/* Files */
pw.println();
pw.println("# FILE:name;size;time;checksum;snapshots");
for (FileUnit u : this.files)
{
pw.print("f:" +
neutralToCatalog(u.name) + ";" +
u.size + ";" +
u.time + ";" +
Long.toHexString(u.getCrc()).toUpperCase(Locale.ENGLISH) + ";");
for (int i = 0; i < u.snapshots.size() - 1; i++)
pw.print((u.snapshots.get(i) + 1) + ","); /* convert "index" to "number" */
pw.println(u.snapshots.get(u.snapshots.size() - 1) + 1); /* convert "index" to "number" */
}
pw.println("# Number of files/directories");
pw.println("nf:" + this.files.size());
pw.println();
pw.println("# End-of-Catalog");
/* close the catalog writer */
pw.flush();
pw.close();
/* check for errors */
if (pw.checkError())
ErrorWarningHandler.reportWarning("An error was encountered while creating catalog \"" +
catalogFileName + "\" (file may not be written successfully).");
/* set timestamp of catalog file */
final FileIO.FileIOResult result = FileIO.setFileTime(catalogFile, ZipSnap.currentTime.getTime());
if (!result.success)
ErrorWarningHandler.reportWarning("Unable to set last-modified time of catalog \"" + catalogFileName + "\".");
}
/**
* Sort the files/directories in the catalog by their name fields.
*/
void sort()
{
Collections.sort(this.files);
}
/**
* Convert a file name from neutral form to catalog form.
*
* @param path
* The file name in neutral form to be converted
* @return
* The catalog form of the file name
*/
private static String neutralToCatalog(
final String path)
{
/* return value */
final StringBuilder t = new StringBuilder();
/* escape sequences '\n' and '\r' */
for (int i = 0; i < path.length(); i++)
{
final char c = path.charAt(i);
if (c == '\n')
{
t.append("\\n");
}
else if (c == '\r')
{
t.append("\\r");
}
else if (c == '\\')
{
t.append("\\\\");
}
else
{
t.append(c);
}
}
return t.toString();
}
/**
* Convert a file/directory name from catalog form to neutral form.
*
* @param path
* The file/directory name in catalog form to be converted
* @return
* The neutral form of the file name; or null if an error is encountered
*/
private static String catalogToNeutral(
final String path)
{
/* return value */
final StringBuilder t = new StringBuilder();
/* escape sequences '\n' and '\r' */
for (int i = 0; i < path.length(); i++)
{
final char c = path.charAt(i);
if (c == '\\')
{
if (i + 1 < path.length())
{
/* read next character */
i++;
final char d = path.charAt(i);
if (d == 'n')
{
t.append('\n');
}
else if (d == 'r')
{
t.append('\r');
}
else if (d == '\\')
{
t.append('\\');
}
else
{
/* invalid escape sequence */
return null;
}
}
else
{
/* invalid escape sequence */
return null;
}
}
else
{
t.append(c);
}
}
return t.toString();
}
}