/** * Copyright (c) 2002-2012 "Neo Technology," * Network Engine for Objects in Lund AB [http://neotechnology.com] * * This file is part of Neo4j. * * Neo4j is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.neo4j.backup; import static org.neo4j.helpers.collection.MapUtil.stringMap; import java.io.File; import java.io.IOException; import java.io.PrintStream; import java.net.URI; import java.net.URISyntaxException; import java.util.Map; import java.util.NoSuchElementException; import org.neo4j.com.ComException; import org.neo4j.consistency.ConsistencyCheckSettings; import org.neo4j.graphdb.TransactionFailureException; import org.neo4j.graphdb.factory.GraphDatabaseSettings; import org.neo4j.helpers.Args; import org.neo4j.helpers.Service; import org.neo4j.helpers.collection.MapUtil; import org.neo4j.kernel.configuration.Config; import org.neo4j.kernel.configuration.ConfigurationDefaults; import org.neo4j.kernel.impl.storemigration.LogFiles; import org.neo4j.kernel.impl.storemigration.StoreFiles; import org.neo4j.kernel.impl.storemigration.UpgradeNotAllowedByConfigurationException; import org.neo4j.kernel.impl.util.StringLogger; import org.neo4j.kernel.logging.Logging; public class BackupTool { private static final String TO = "to"; private static final String FROM = "from"; private static final String INCREMENTAL = "incremental"; private static final String FULL = "full"; private static final String VERIFY = "verify"; private static final String CONFIG = "config"; public static final String DEFAULT_SCHEME = "single"; public static void main( String[] args ) { BackupTool tool = new BackupTool( new BackupService(), System.out ); try { tool.run( args ); } catch ( ToolFailureException e ) { e.haltJVM(); } } private final BackupService backupService; private final PrintStream systemOut; BackupTool( BackupService backupService, PrintStream systemOut ) { this.backupService = backupService; this.systemOut = systemOut; } void run( String[] args ) throws ToolFailureException { Args arguments = new Args( args ); checkArguments( arguments ); boolean full = arguments.has( FULL ); String from = arguments.get( FROM, null ); String to = arguments.get( TO, null ); boolean verify = arguments.getBoolean( VERIFY, true, true ); Config tuningConfiguration = readTuningConfiguration( TO, arguments ); URI backupURI = null; try { backupURI = new URI( from ); } catch ( URISyntaxException e ) { throw new ToolFailureException( "Please properly specify a location to backup as a valid URI in the form " + "<scheme>://<host>[:port], where scheme is the target database's running mode, eg ha" ); } String module = backupURI.getScheme(); /* * So, the scheme is considered to be the module name and an attempt at * loading the service is made. */ BackupExtensionService service = null; if ( module != null && !DEFAULT_SCHEME.equals( module ) ) { try { service = Service.load( BackupExtensionService.class, module ); } catch ( NoSuchElementException e ) { throw new ToolFailureException( String.format( "%s was specified as a backup module but it was not found. " + "Please make sure that the implementing service is on the classpath.", module ) ); } } if ( service != null ) { // If in here, it means a module was loaded. Use it and substitute the // passed URI backupURI = service.resolve( backupURI, arguments, new Logging() { @Override public StringLogger getLogger( String name ) { return StringLogger.SYSTEM; } } ); } doBackup( full, backupURI, to, verify, tuningConfiguration ); } private void checkArguments( Args arguments ) throws ToolFailureException { boolean full = arguments.has( FULL ); boolean incremental = arguments.has( INCREMENTAL ); if ( full & incremental || !(full | incremental) ) { throw new ToolFailureException( "Specify either " + dash( FULL ) + " or " + dash( INCREMENTAL ) ); } if ( arguments.get( FROM, null ) == null ) { throw new ToolFailureException( "Please specify " + dash( FROM ) + ", examples:\n" + " " + dash( FROM ) + " single://192.168.1.34\n" + " " + dash( FROM ) + " single://192.168.1.34:1234\n" + " " + dash( FROM ) + " ha://192.168.1.15:2181\n" + " " + dash( FROM ) + " ha://192.168.1.15:2181,192.168.1.16:2181" ); } if ( arguments.get( TO, null ) == null ) { throw new ToolFailureException( "Specify target location with " + dash( TO ) + " <target-directory>" ); } } public Config readTuningConfiguration( String storeDir, Args arguments ) throws ToolFailureException { Map<String, String> specifiedProperties = stringMap(); String propertyFilePath = arguments.get( CONFIG, null ); if ( propertyFilePath != null ) { File propertyFile = new File( propertyFilePath ); try { specifiedProperties = MapUtil.load( propertyFile ); } catch ( IOException e ) { throw new ToolFailureException( String.format( "Could not read configuration properties file [%s]", propertyFilePath ), e ); } } specifiedProperties.put( GraphDatabaseSettings.store_dir.name(), storeDir ); return new Config( new ConfigurationDefaults( GraphDatabaseSettings.class, ConsistencyCheckSettings.class ) .apply( specifiedProperties ) ); } private void doBackup( boolean trueForFullFalseForIncremental, URI from, String to, boolean checkConsistency, Config tuningConfiguration ) throws ToolFailureException { if ( trueForFullFalseForIncremental ) { doBackupFull( from, to, checkConsistency, tuningConfiguration ); } else { doBackupIncremental( from, to, checkConsistency, tuningConfiguration ); } systemOut.println( "Done" ); } private void doBackupFull( URI from, String to, boolean checkConsistency, Config tuningConfiguration ) throws ToolFailureException { systemOut.println( "Performing full backup from '" + from + "'" ); try { backupService.doFullBackup( from.getHost(), extractPort( from ), to, checkConsistency, tuningConfiguration ); } catch ( ComException e ) { throw new ToolFailureException( "Couldn't connect to '" + from + "'", e ); } } private void doBackupIncremental( URI from, String to, boolean verify, Config tuningConfiguration ) throws ToolFailureException { systemOut.println( "Performing incremental backup from '" + from + "'" ); boolean failedBecauseOfStoreVersionMismatch = false; try { backupService.doIncrementalBackup( from.getHost(), extractPort( from ), to, verify ); } catch ( TransactionFailureException e ) { if ( e.getCause() instanceof UpgradeNotAllowedByConfigurationException ) { failedBecauseOfStoreVersionMismatch = true; } else { throw new ToolFailureException( "TransactionFailureException from existing backup at '" + from + "'.", e); } } catch ( ComException e ) { throw new ToolFailureException( "Couldn't connect to '" + from + "' ", e ); } if ( failedBecauseOfStoreVersionMismatch ) { systemOut.println( "The database present in the target directory is of an older version. " + "Backing that up in target and performing a full backup from source" ); try { moveExistingDatabase( to ); } catch ( IOException e ) { throw new ToolFailureException( "There was a problem moving the old database out of the way" + " - cannot continue, aborting.", e ); } doBackupFull( from, to, verify, tuningConfiguration ); } } private int extractPort( URI from ) { int port = from.getPort(); if ( port == -1 ) { port = BackupServer.DEFAULT_PORT; } return port; } private static void moveExistingDatabase( String to ) throws IOException { File toDir = new File( to ); File backupDir = new File( toDir, "old-version" ); if ( !backupDir.mkdir() ) { throw new IOException( "Trouble making target backup directory " + backupDir.getAbsolutePath() ); } StoreFiles.move( toDir, backupDir ); LogFiles.move( toDir, backupDir ); } static class ToolFailureException extends Exception { ToolFailureException( String message ) { super( message ); } ToolFailureException( String message, Throwable cause ) { super( message, cause ); } void haltJVM() { System.out.println( getMessage() ); if ( getCause() != null ) { getCause().printStackTrace( System.out ); } System.exit( 1 ); } } private static String dash( String name ) { return "-" + name; } }