au.net.causal.maven.plugins.boxdb.db.FileBasedDatabase Maven / Gradle / Ivy
Show all versions of boxdb-maven-plugin Show documentation
package au.net.causal.maven.plugins.boxdb.db;
import au.net.causal.maven.plugins.boxdb.JdbcSqlRunner;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.codehaus.plexus.archiver.Archiver;
import org.codehaus.plexus.archiver.ArchiverException;
import org.codehaus.plexus.archiver.manager.NoSuchArchiverException;
import org.codehaus.plexus.archiver.tar.TarUnArchiver;
import org.codehaus.plexus.archiver.util.DefaultFileSet;
import org.codehaus.plexus.util.FileUtils;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Date;
import java.util.Objects;
import java.util.concurrent.TimeoutException;
/**
* Superclass for databases that exist on the local filesystem.
*
*
* Backups are implemented by tarring the directory containing the database files while the database is stopped.
*/
public abstract class FileBasedDatabase implements BoxDatabase
{
private final BoxConfiguration boxConfiguration;
private final ProjectConfiguration projectConfiguration;
private final BoxContext context;
protected FileBasedDatabase(BoxConfiguration boxConfiguration,
ProjectConfiguration projectConfiguration,
BoxContext context)
{
Objects.requireNonNull(boxConfiguration, "boxConfiguration == null");
Objects.requireNonNull(projectConfiguration, "projectConfiguration == null");
Objects.requireNonNull(context, "context == null");
this.boxConfiguration = boxConfiguration;
this.projectConfiguration = projectConfiguration;
this.context = context;
}
@Override
public void backup(Path backupFile, BackupFileTypeHint backupFileTypeHint)
throws BoxDatabaseException, IOException, SQLException
{
//TODO would be nice to hint to caller whether backup is offline or online
//File-based database backups must be done offline
boolean runningBeforeBackup = isRunning();
if (runningBeforeBackup)
{
getContext().getLog().info("Stopping database to make offline backup.");
stop();
}
else
{
getContext().getLog().info("Database is already stopped before backup.");
}
Path dbDirectory = getBoxConfiguration().getDatabaseFile();
if (Files.notExists(dbDirectory))
{
throw new SQLException("Cannot back up a database that has not been created yet.",
new FileNotFoundException(dbDirectory.toAbsolutePath().toString()));
}
//Archive this directory
createBackupArchiveFromDirectory(dbDirectory, backupFile);
//Restore running state after offline backup
if (runningBeforeBackup)
startAndWait();
}
protected void createBackupArchiveFromDirectory(Path directory, Path archiveFile)
throws IOException, BoxDatabaseException
{
try
{
Archiver archiver = getContext().getArchiverManager().getArchiver("tar");
//TODO should we allow the archiver to be configured by configuration?
archiver.setDestFile(archiveFile.toFile());
archiver.addFileSet(new DefaultFileSet(directory.toFile()));
archiver.createArchive();
}
catch (NoSuchArchiverException e)
{
throw new BoxDatabaseException("Could not find archiver: " + e, e);
}
}
@Override
public void restore(Path backupFile)
throws BoxDatabaseException, IOException, SQLException
{
restore(backupFile.toUri().toURL());
/* This can be used if we don't need to support URLs not from the filesystem
//Stop database if running
boolean runningBeforeRestore = isRunning();
if (runningBeforeRestore)
stop();
//Clean out existing database before restoring
cleanDatabaseDirectory();
try
{
UnArchiver unArchiver = getContext().getArchiverManager().getUnArchiver("tar");
unArchiver.setDestDirectory(getBoxConfiguration().getDatabaseFile().toFile());
unArchiver.setSourceFile(backupFile.toFile());
unArchiver.extract();
}
catch (NoSuchArchiverException e)
{
throw new BoxDatabaseException("Could not find unarchiver: " + e, e);
}
if (runningBeforeRestore)
startAndWait();
*/
}
@Override
public void restore(URL backupResource) throws BoxDatabaseException, IOException, SQLException
{
//Stop database if running
boolean runningBeforeRestore = isRunning();
if (runningBeforeRestore)
stop();
//Clean out existing database before restoring
cleanDatabaseDirectory();
File destDirectory = getBoxConfiguration().getDatabaseFile().toFile();
//Extract from URL resource using a bit of a hack
//The unarchiver has protected access to extractFile which does some fancy things regarding symlinks and permissions
//that I don't want to just copy+paste
MyTarUnArchiver unarchiver = new MyTarUnArchiver();
try (TarArchiveInputStream tis = new TarArchiveInputStream(backupResource.openStream()))
{
TarArchiveEntry te;
while ( ( te = tis.getNextTarEntry() ) != null )
{
final String symlinkDestination = te.isSymbolicLink() ? te.getLinkName() : null;
unarchiver.extractFile( null, destDirectory, tis, te.getName(), te.getModTime(), te.isDirectory(),
te.getMode() != 0 ? te.getMode() : null, symlinkDestination );
}
}
if (runningBeforeRestore)
startAndWait();
}
protected void startAndWait()
throws BoxDatabaseException
{
start();
try
{
this.waitUntilStarted(projectConfiguration.getBackupTimeout());
}
catch (TimeoutException e)
{
throw new BoxDatabaseException("Timeout waiting for database to start back up.", e);
}
}
/**
* Delete and recreate the database directory to prepare for restore.
*
* @throws IOException if an error occurs.
*/
protected void cleanDatabaseDirectory()
throws IOException
{
Path databaseDirectory = getBoxConfiguration().getDatabaseFile();
if (Files.exists(databaseDirectory))
FileUtils.deleteDirectory(databaseDirectory.toFile());
Files.createDirectories(databaseDirectory);
}
/**
* Detect whether the specified backup resource is a TAR archive created with this class's backup.
* Return value of false means it's not a TAR file. Use this method if subclass has overridden backup
* to support backup hints that conditionally delegates to this class's method depending on
* backup hint.
*
* @param backupResource the resource file.
*
* @return true if the file was an archive likely created with this class's backup method, false if not.
*
* @throws IOException if an I/O error occurs.
*/
protected boolean isBackupArchive(URL backupResource)
throws IOException
{
try (TarArchiveInputStream tis = new TarArchiveInputStream(backupResource.openStream()))
{
tis.getNextTarEntry();
return true;
}
catch (IOException e)
{
getContext().getLog().debug("Backup resource " + backupResource.toExternalForm() + " detected as not a TAR.", e);
//No specific invalid format exception from TarInputStream, so assume all I/O exceptions
//mean the format is not a TAR
return false;
}
}
@Override
public void executeJdbcScript(Reader scriptReader, DatabaseTarget targetDatabase)
throws IOException, SQLException, BoxDatabaseException
{
try (Connection con = createJdbcConnection(targetDatabase);
JdbcSqlRunner sqlRunner = new JdbcSqlRunner(con, getContext().getLog()))
{
sqlRunner.executeSql(new BufferedReader(scriptReader));
}
}
protected BoxConfiguration getBoxConfiguration()
{
return boxConfiguration;
}
protected ProjectConfiguration getProjectConfiguration()
{
return projectConfiguration;
}
protected BoxContext getContext()
{
return context;
}
/**
* This is a hack class to re-use logic from Tar Unarchiver to extract files. extractFile() is protected in
* TarArchiver
class so we make a subclass and override this method to make it public and usable.
*/
private static class MyTarUnArchiver extends TarUnArchiver
{
@Override
public void extractFile(File srcF, File dir, InputStream compressedInputStream, String entryName, Date entryDate, boolean isDirectory, Integer mode, String symlinkDestination)
throws IOException, ArchiverException
{
super.extractFile(srcF, dir, compressedInputStream, entryName, entryDate, isDirectory, mode, symlinkDestination);
}
}
}