/* Copyright (c) 2001-2009, The HSQL Development Group
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of the HSQL Development Group nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
* OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.hsqldb.lib.tar;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Properties;
/**
* Works with tar archives containing HSQLDB database instance backups.
* Viz, creating, examining, or extracting these archives.
* <P/>
* This class provides OO Tar backup-creation control.
* The extraction and listing features are implemented only in static fashion
* in the Main method, which provides a consistent interface for all three
* features from the command-line.
* <P/>
* For tar creation, the default behavior is to fail if the target archive
* exists, and to abort if any database change is detected.
* Use the JavaBean setters to changes this behavior.
* <P/>
* See the main(String[]) method for details about command-line usage.
*
* @see <a href="../../../../../guide/deployment-chapt.html#deployment_backup-sect"
* target="guide">
* The database backup section of the HyperSQL User Guide</a>
* @see #main(String[])
* @see #setOverWrite(boolean)
* @see #setAbortUponModify(boolean)
* @author Blaine Simpson (blaine dot simpson at admc dot com)
*/
public class DbBackup {
/**
* Command line invocation to create, examine, or extract HSQLDB database
* backup tar archives.
* <P>
* This class stores tar entries as relative files without specifying
* parent directories, in what is commonly referred to as <I>tar bomb</I>
* format.
* The set of files is small, with known extensions, and the potential
* inconvenience of messing up the user's current directory is more than
* compensated by making it easier for the user to restore to a new
* database URL location at a peer level to the original.
* <P/>
* Automatically calculates buffer sizes based on the largest component
* file (for "save" mode) or tar file size (for other modes).
* <P/>
* Run<CODE><PRE>
* java -cp path/to/hsqldb.jar org.hsqldb.lib.tar.DbBackup
* </PRE></CODE> for syntax help.
*/
static public void main(String[] sa)
throws IOException, TarMalformatException {
try {
if (sa.length < 1) {
System.out.println(RB.singleton.getString(RB.DBBACKUP_SYNTAX,
DbBackup.class.getName()));
System.out.println();
System.out.println(RB.singleton.getString(RB.LISTING_FORMAT));
System.exit(0);
}
if (sa[0].equals("--save")) {
boolean overWrite = sa.length > 1
&& sa[1].equals("--overwrite");
if (sa.length != (overWrite ? 4
: 3)) {
throw new IllegalArgumentException();
}
DbBackup backup = new DbBackup(new File(sa[sa.length - 2]),
sa[sa.length - 1]);
backup.setOverWrite(overWrite);
backup.write();
} else if (sa[0].equals("--list")) {
if (sa.length < 2) {
throw new IllegalArgumentException();
}
String[] patternStrings = null;
if (sa.length > 2) {
patternStrings = new String[sa.length - 2];
for (int i = 2; i < sa.length; i++) {
patternStrings[i - 2] = sa[i];
}
}
new TarReader(new File(sa[1]), TarReader
.LIST_MODE, patternStrings, new Integer(DbBackup
.generateBufferBlockValue(new File(sa[1]))), null)
.read();
} else if (sa[0].equals("--extract")) {
boolean overWrite = sa.length > 1
&& sa[1].equals("--overwrite");
int firstPatInd = overWrite ? 4
: 3;
if (sa.length < firstPatInd) {
throw new IllegalArgumentException();
}
String[] patternStrings = null;
if (sa.length > firstPatInd) {
patternStrings = new String[sa.length - firstPatInd];
for (int i = firstPatInd; i < sa.length; i++) {
patternStrings[i - firstPatInd] = sa[i];
}
}
File tarFile = new File(sa[overWrite ? 2
: 1]);
int tarReaderMode = overWrite ? TarReader.OVERWRITE_MODE
: TarReader.EXTRACT_MODE;
new TarReader(
tarFile, tarReaderMode, patternStrings,
new Integer(DbBackup.generateBufferBlockValue(tarFile)),
new File(sa[firstPatInd - 1])).read();
} else {
throw new IllegalArgumentException();
}
} catch (IllegalArgumentException iae) {
System.out.println(RB.singleton.getString(RB.DBBACKUP_SYNTAXERR,
DbBackup.class.getName()));
System.exit(2);
}
}
/**
* Instantiate a DbBackup instance for creating a Database Instance backup.
*
* Much validation is deferred until the write() method, to prevent
* problems with files changing between the constructor and the write call.
*/
public DbBackup(File archiveFile, String dbPath) throws IOException {
this.archiveFile = archiveFile;
File dbPathFile = new File(dbPath);
dbDir = dbPathFile.getAbsoluteFile().getParentFile();
instanceName = dbPathFile.getName();
}
protected File dbDir;
protected File archiveFile;
protected String instanceName;
protected boolean overWrite = false; // Defaults no NO OVERWRITE
protected boolean abortUponModify = true; // Defaults to ABORT-UPON-MODIFY
/**
* Defaults to false.
*
* If false, then attempts to write a tar file that already exist will
* abort.
*/
public void setOverWrite(boolean overWrite) {
this.overWrite = overWrite;
}
/**
* Defaults to true.
*
* If true, then the write() method will validate that the database is
* closed, and it will verify that no DB file changes between when we
* start writing the tar, and when we finish.
*/
public void setAbortUponModify(boolean abortUponModify) {
this.abortUponModify = abortUponModify;
}
public boolean getOverWrite() {
return overWrite;
}
public boolean getAbortUponModify() {
return abortUponModify;
}
/**
* This method always backs up the .properties and .script files.
* It will back up all of .backup, .data, and .log which exist.
*
* If abortUponModify is set, no tar file will be created, and this
* method will throw.
*
* @throws IOException for any of many possible I/O problems
* @throws IllegalStateException only if abortUponModify is set, and
* database is open or is modified.
*/
public void write() throws IOException, TarMalformatException {
File propertiesFile = new File(dbDir, instanceName + ".properties");
File scriptFile = new File(dbDir, instanceName + ".script");
File[] componentFiles = new File[] {
propertiesFile, scriptFile,
new File(dbDir, instanceName + ".backup"),
new File(dbDir, instanceName + ".data"),
new File(dbDir, instanceName + ".log")
};
boolean[] existList = new boolean[componentFiles.length];
long startTime = new java.util.Date().getTime();
for (int i = 0; i < existList.length; i++) {
existList[i] = componentFiles[i].exists();
if (i < 2 && !existList[i]) {
// First 2 files are REQUIRED
throw new FileNotFoundException(
RB.singleton.getString(
RB.FILE_MISSING, componentFiles[i].getAbsolutePath()));
}
}
if (abortUponModify) {
Properties p = new Properties();
p.load(new FileInputStream(propertiesFile));
String modifiedString = p.getProperty("modified");
if (modifiedString != null
&& (modifiedString.equalsIgnoreCase("yes")
|| modifiedString.equalsIgnoreCase("true"))) {
throw new IllegalStateException(
RB.singleton.getString(
RB.MODIFIED_PROPERTY, modifiedString));
}
}
TarGenerator generator = new TarGenerator(archiveFile, overWrite,
new Integer(DbBackup.generateBufferBlockValue(componentFiles)));
for (int i = 0; i < componentFiles.length; i++) {
if (!componentFiles[i].exists()) {
continue;
// We've already verified that required files exist, therefore
// there is no error condition here.
}
generator.queueEntry(componentFiles[i].getName(),
componentFiles[i]);
}
generator.write();
if (abortUponModify) {
try {
for (int i = 0; i < componentFiles.length; i++) {
if (componentFiles[i].exists()) {
if (!existList[i]) {
throw new FileNotFoundException(
RB.singleton.getString(
RB.FILE_DISAPPEARED,
componentFiles[i].getAbsolutePath()));
}
if (componentFiles[i].lastModified() > startTime) {
throw new FileNotFoundException(
RB.singleton.getString(
RB.FILE_CHANGED,
componentFiles[i].getAbsolutePath()));
}
} else if (existList[i]) {
throw new FileNotFoundException(
RB.singleton.getString(
RB.FILE_APPEARED,
componentFiles[i].getAbsolutePath()));
}
}
} catch (IllegalStateException ise) {
if (!archiveFile.delete()) {
System.out.println(
RB.singleton.getString(
RB.CLEANUP_RMFAIL, archiveFile.getAbsolutePath()));
// Be-it-known. This method can write to stderr if
// abortUponModify is true.
}
throw ise;
}
}
}
/**
* @todo - Supply a version of my MemTest program which people can run
* one time when the server can be starved of RAM, and save the available
* RAM quantity to a text file. We can then really crank up the buffer
* size to make transfers really efficient.
*/
/**
* Return a 512-block buffer size suggestion, based on the size of what
* needs to be read or written, and default and typical JVM constraints.
* <P/>
* <B>Algorithm details:</B>
* <P/>
* Minimum system I want support is a J2SE system with 256M physical
* RAM. This sytem can hold a 61 MB byte array (real 1024^2 M).
* (61MB with Java 1.6, 62MB with Java 1.4).
* This decreases to just 60 MB with (pre-production, non-optimized)
* HSQLDB v. 1.9 on Java 1.6.
* Allow the user 40 MB of for data (this only corresponds to a much
* smaller quantity of real data due to the huge overhead of Java and
* database structures).
* This allows 20 MB for us to use. User can easily use more than this
* by raising JVM settings and/or getting more PRAM or VRAM.
* Therefore, ceiling = 20MB = 20 MB / .5 Kb = 40 k blocks
* <P/>
* We make the conservative simplification that each data file contains
* just one huge data entry component. This is a good estimation, since in
* most cases, the contents of the single largest file will be many orders
* of magnitude larger than the other files and the single block entry
* headers.
* <P/>
* We aim for reading or writing these biggest file with 10 reads/writes.
* In the case of READING Gzip files, there will actually be many more
* reads than this, but that's the price you pay for smaller file size.
*
* @param files Null array elements are permitted. They will just be
* skipped by the algorithm.
*/
static protected int generateBufferBlockValue(File[] files) {
long maxFileSize = 0;
for (int i = 0; i < files.length; i++) {
if (files[i] == null) {
continue;
}
if (files[i].length() > maxFileSize) {
maxFileSize = files[i].length();
}
}
int idealBlocks = (int) (maxFileSize / (10L * 512L));
// I.e., 1/10 of the file, in units of 512 byte blocks.
// It's fine that operations will truncate down instead of round.
if (idealBlocks < 1) {
return 1;
}
if (idealBlocks > 40 * 1024) {
return 40 * 1024;
}
return idealBlocks;
}
/**
* Convenience wrapper for generateBufferBlockValue(File[]).
*
* @see #generateBufferBlockValue(File[])
*/
static protected int generateBufferBlockValue(File file) {
return generateBufferBlockValue(new File[]{ file });
}
}