zbackup/zbackup_base.cc

463 lines
13 KiB
C++

// Copyright (c) 2012-2014 Konstantin Isakov <ikm@zbackup.org> and ZBackup contributors, see CONTRIBUTORS
// Part of ZBackup. Licensed under GNU GPLv2 or later + OpenSSL, see LICENSE
#include <sys/wait.h>
#include <cerrno>
#include <unistd.h>
#include <signal.h>
#include "zbackup_base.hh"
#include "storage_info_file.hh"
#include "compression.hh"
#include "debug.hh"
// TODO: make configurable by cmake
#if defined(PATH_VI)
# define EDITOR PATH_VI
#else
# define EDITOR "/bin/vi"
#endif
#ifndef PATH_BSHELL
# define PATH_BSHELL "/bin/sh"
#endif
using std::string;
Paths::Paths( string const & storageDir ): storageDir( storageDir )
{
}
Paths::Paths( string const & storageDir, Config const & config ):
storageDir( storageDir ), config( config )
{
}
string Paths::getTmpPath()
{
if ( config.runtime.pathsRespectTmp )
{
char * tmpdir;
if ( ( ( tmpdir = getenv( "TMPDIR" ) ) != NULL && *tmpdir != '\0' ) )
return string( tmpdir );
}
return string( Dir::addPath( storageDir, "tmp" ) );
}
string Paths::getBundlesPath()
{
return string( Dir::addPath( storageDir, "bundles" ) );
}
string Paths::getStorageInfoPath()
{
return string( Dir::addPath( storageDir, "info" ) );
}
string Paths::getExtendedStorageInfoPath()
{
return string( Dir::addPath( storageDir, "info_extended" ) );
}
string Paths::getIndexPath()
{
return string( Dir::addPath( storageDir, "index" ) );
}
string Paths::getBackupsPath()
{
return string( Dir::addPath( storageDir, "backups" ) );
}
ZBackupBase::ZBackupBase( string const & storageDir, string const & password ):
Paths( storageDir ), storageInfo( loadStorageInfo() ),
encryptionkey( password, storageInfo.has_encryption_key() ?
&storageInfo.encryption_key() : 0 ),
extendedStorageInfo( loadExtendedStorageInfo( encryptionkey ) ),
tmpMgr( getTmpPath() ),
chunkIndex( encryptionkey, tmpMgr, getIndexPath(), false ),
config( extendedStorageInfo.mutable_config() )
{
propagateUpdate();
dPrintf("%s for %s is instantiated and initialized\n", __CLASS,
storageDir.c_str() );
}
ZBackupBase::ZBackupBase( string const & storageDir, string const & password,
Config & configIn ):
Paths( storageDir, configIn ), storageInfo( loadStorageInfo() ),
encryptionkey( password, storageInfo.has_encryption_key() ?
&storageInfo.encryption_key() : 0 ),
extendedStorageInfo( loadExtendedStorageInfo( encryptionkey ) ),
tmpMgr( getTmpPath() ),
chunkIndex( encryptionkey, tmpMgr, getIndexPath(), false ),
config( configIn, extendedStorageInfo.mutable_config() )
{
propagateUpdate();
dPrintf("%s for %s is instantiated and initialized\n", __CLASS,
storageDir.c_str() );
}
ZBackupBase::ZBackupBase( string const & storageDir, string const & password,
bool prohibitChunkIndexLoading ):
Paths( storageDir ), storageInfo( loadStorageInfo() ),
encryptionkey( password, storageInfo.has_encryption_key() ?
&storageInfo.encryption_key() : 0 ),
extendedStorageInfo( loadExtendedStorageInfo( encryptionkey ) ),
tmpMgr( getTmpPath() ),
chunkIndex( encryptionkey, tmpMgr, getIndexPath(), prohibitChunkIndexLoading ),
config( extendedStorageInfo.mutable_config() )
{
propagateUpdate();
dPrintf("%s for %s is instantiated and initialized\n", __CLASS,
storageDir.c_str() );
}
ZBackupBase::ZBackupBase( string const & storageDir, string const & password,
Config & configIn, bool prohibitChunkIndexLoading ):
Paths( storageDir, configIn ), storageInfo( loadStorageInfo() ),
encryptionkey( password, storageInfo.has_encryption_key() ?
&storageInfo.encryption_key() : 0 ),
extendedStorageInfo( loadExtendedStorageInfo( encryptionkey ) ),
tmpMgr( getTmpPath() ),
chunkIndex( encryptionkey, tmpMgr, getIndexPath(), prohibitChunkIndexLoading ),
config( configIn, extendedStorageInfo.mutable_config() )
{
propagateUpdate();
dPrintf("%s for %s is instantiated and initialized\n", __CLASS,
storageDir.c_str() );
}
// Update all internal variables according to real configuration
// Dunno why someone need to store duplicate information
// in deduplication utility
void ZBackupBase::propagateUpdate()
{
const_sptr< Compression::CompressionMethod > compression =
Compression::CompressionMethod::findCompression(
config.GET_STORABLE( bundle, compression_method ) );
Compression::CompressionMethod::selectedCompression = compression;
}
StorageInfo ZBackupBase::loadStorageInfo()
{
StorageInfo storageInfo;
StorageInfoFile::load( getStorageInfoPath(), storageInfo );
return storageInfo;
}
ExtendedStorageInfo ZBackupBase::loadExtendedStorageInfo(
EncryptionKey const & encryptionkey )
{
try
{
ExtendedStorageInfo extendedStorageInfo;
ExtendedStorageInfoFile::load( getExtendedStorageInfoPath(), encryptionkey,
extendedStorageInfo );
return extendedStorageInfo;
}
catch ( UnbufferedFile::exCantOpen & ex )
{
verbosePrintf( "Can't open extended storage info (info_extended)!\n"
"Attempting to start repo migration.\n" );
if ( !File::exists( getExtendedStorageInfoPath() ) )
{
ExtendedStorageInfo extendedStorageInfo;
Config config( extendedStorageInfo.mutable_config() );
config.reset_storable();
config.SET_STORABLE( chunk, max_size, storageInfo.chunk_max_size() );
config.SET_STORABLE( bundle, max_payload_size,
storageInfo.bundle_max_payload_size() );
config.SET_STORABLE( bundle, compression_method,
storageInfo.default_compression_method() );
ExtendedStorageInfoFile::save( getExtendedStorageInfoPath(), encryptionkey,
extendedStorageInfo );
verbosePrintf( "Done.\n" );
return loadExtendedStorageInfo( encryptionkey );
}
else
{
fprintf( stderr, "info_extended exists but can't be opened!\n"
"Please check file permissions.\n" );
}
}
}
void ZBackupBase::initStorage( string const & storageDir,
string const & password,
bool isEncrypted,
Config const & configIn )
{
StorageInfo storageInfo;
ExtendedStorageInfo extendedStorageInfo;
Config config( extendedStorageInfo.mutable_config() );
config.reset_storable();
config.storable->MergeFrom( *configIn.storable );
EncryptionKey encryptionkey = EncryptionKey::noKey();
if ( isEncrypted )
EncryptionKey::generate( password,
*storageInfo.mutable_encryption_key(),
encryptionkey );
Paths paths( storageDir );
if ( !Dir::exists( storageDir ) )
Dir::create( storageDir );
if ( !Dir::exists( paths.getBundlesPath() ) )
Dir::create( paths.getBundlesPath() );
if ( !Dir::exists( paths.getBackupsPath() ) )
Dir::create( paths.getBackupsPath() );
if ( !Dir::exists( paths.getIndexPath() ) )
Dir::create( paths.getIndexPath() );
string storageInfoPath( paths.getStorageInfoPath() );
string extendedStorageInfoPath( paths.getExtendedStorageInfoPath() );
if ( File::exists( storageInfoPath ) )
throw exWontOverwrite( storageInfoPath );
encryptionkey = EncryptionKey( password, storageInfo.has_encryption_key() ?
&storageInfo.encryption_key() : 0 );
StorageInfoFile::save( storageInfoPath, storageInfo );
ExtendedStorageInfoFile::save( extendedStorageInfoPath, encryptionkey, extendedStorageInfo );
}
string ZBackupBase::deriveStorageDirFromBackupsFile( string const &
backupsFile, bool allowOutside )
{
// TODO: handle cases when there's a backup/ folder within the backup/ folder
// correctly
if ( allowOutside )
return Dir::getRealPath( backupsFile );
string realPath = Dir::getRealPath( Dir::getDirName( backupsFile ) );
size_t pos;
if ( realPath.size() >= 8 && strcmp( realPath.c_str() + realPath.size() - 8,
"/backups") == 0 )
pos = realPath.size() - 8;
else
pos = realPath.rfind( "/backups/" );
if ( pos == string::npos )
throw exCantDeriveStorageDir( backupsFile );
else
return realPath.substr( 0, pos );
}
void ZBackupBase::setPassword( string const & password )
{
EncryptionKey::generate( password,
*storageInfo.mutable_encryption_key(), encryptionkey );
StorageInfoFile::save( getStorageInfoPath(), storageInfo );
EncryptionKey encryptionkey( password, storageInfo.has_encryption_key() ?
&storageInfo.encryption_key() : 0 );
}
void ZBackupBase::saveExtendedStorageInfo()
{
ExtendedStorageInfoFile::save( getExtendedStorageInfoPath(), encryptionkey,
extendedStorageInfo );
}
bool ZBackupBase::spawnEditor( string & data, bool( * validator )
( string const &, string const & ) )
{
// Based on ideas found in cronie-1.4.4-12.el6
// Initially it was just a copy-paste from edit_cmd (crontab.c)
/* Turn off signals. */
(void) signal( SIGHUP, SIG_IGN );
(void) signal( SIGINT, SIG_IGN );
(void) signal( SIGQUIT, SIG_IGN );
sptr< TemporaryFile > tmpFile = tmpMgr.makeTemporaryFile();
const char * tmpFileName = tmpFile->getFileName().c_str();
sptr< File> tmpDataFile = new File( tmpFileName, File::WriteOnly );
tmpDataFile->writeRecords( data.c_str(), data.size(), 1 );
again:
tmpDataFile->rewind();
if ( tmpDataFile->error() )
{
verbosePrintf( "Error while writing data to %s\n", tmpFileName );
fatal:
tmpFile.reset();
exit( EXIT_FAILURE );
}
char * editorEnv;
string editor;
if ( ( ( editorEnv = getenv( "VISUAL" ) ) == NULL || *editorEnv == '\0' ) &&
( ( editorEnv = getenv( "EDITOR" ) ) == NULL || *editorEnv == '\0' ) )
editor.assign( EDITOR );
else
editor.assign( editorEnv );
/* we still have the file open. editors will generally rewrite the
* original file rather than renaming/unlinking it and starting a
* new one; even backup files are supposed to be made by copying
* rather than by renaming. if some editor does not support this,
* then don't use it. the security problems are more severe if we
* close and reopen the file around the edit.
*/
string shellArgs;
shellArgs += editor;
shellArgs += " ";
shellArgs += tmpFileName;
pid_t pid, xpid;
switch ( pid = fork() )
{
case -1:
perror( "fork" );
goto fatal;
case 0:
/* child */
dPrintf( "Spawning editor: %s %s %s %s\n", PATH_BSHELL, PATH_BSHELL,
"-c", shellArgs.c_str() );
execlp( PATH_BSHELL, PATH_BSHELL, "-c", shellArgs.c_str(), (char *) 0 );
perror( editor.c_str() );
exit( EXIT_FAILURE );
/*NOTREACHED*/
default:
/* parent */
break;
}
/* parent */
int waiter;
for ( ; ; )
{
xpid = waitpid( pid, &waiter, 0 );
if ( xpid == -1 )
{
if ( errno != EINTR )
verbosePrintf( "waitpid() failed waiting for PID %ld from \"%s\": %s\n",
(long) pid, editor.c_str(), strerror( errno ) );
}
else
if (xpid != pid)
{
verbosePrintf( "wrong PID (%ld != %ld) from \"%s\"\n",
(long) xpid, (long) pid, editor.c_str() );
goto fatal;
}
else
if ( WIFEXITED( waiter ) && WEXITSTATUS( waiter ) )
{
verbosePrintf( "\"%s\" exited with status %d\n",
editor.c_str(), WEXITSTATUS( waiter ) );
goto fatal;
}
else
if ( WIFSIGNALED( waiter ) )
{
verbosePrintf( "\"%s\" killed; signal %d (%score dumped)\n",
editor.c_str(), WTERMSIG( waiter ),
WCOREDUMP( waiter ) ? "" : "no ");
goto fatal;
}
else
break;
}
(void) signal( SIGHUP, SIG_DFL );
(void) signal( SIGINT, SIG_DFL );
(void) signal( SIGQUIT, SIG_DFL );
tmpDataFile->close();
tmpDataFile = new File( tmpFileName, File::ReadOnly );
string newData;
newData.resize( tmpDataFile->size() );
tmpDataFile->read( &newData[ 0 ], newData.size() );
bool isChanged = false;
bool valid = validator( data, newData );
switch ( valid )
{
case true:
goto success;
case false:
for ( ; ; )
{
fprintf( stderr, "Supplied data is not valid\n" );
fflush( stderr );
printf( "Do you want to retry the same edit? " );
fflush( stdout );
string input;
input.resize( 131072 ); // Should I choose another magic value?
if ( fgets( &input[ 0 ], input.size(), stdin ) == 0L )
continue;
switch ( input[ 0 ] )
{
case 'y':
case 'Y':
goto again;
case 'n':
case 'N':
verbosePrintf( "Data is kept intact\n" );
goto end;
default:
fprintf( stderr, "Enter Y or N\n" );
}
}
}
success:
isChanged = true;
data.assign( newData );
end:
tmpDataFile.reset();
tmpFile.reset();
return isChanged;
}
bool ZBackupBase::editConfigInteractively()
{
string configData( Config::toString( *config.storable ) );
if ( !spawnEditor( configData, &Config::validateProto ) )
return false;
ConfigInfo newConfig;
Config::parseProto( configData, &newConfig );
if ( Config::toString( *config.storable ) ==
Config::toString( newConfig ) )
{
verbosePrintf( "No changes made to config\n" );
return false;
}
verbosePrintf( "Updating configuration...\n" );
config.storable->MergeFrom( newConfig );
verbosePrintf(
"Configuration successfully updated!\n"
"Updated configuration:\n%s", Config::toString( *config.storable ).c_str() );
return true;
}