<?php
/**
 * Backup script for IceBear.
 *
 * Even if you are running some enterprise-level whole-disk backup system (NetWorker, CommServe, etc.), you
 * MUST take additional steps to ensure that you have a viable database backup. Simply backing up the MySQL
 * data files is unreliable.
 *
 * This script will dump the database to a .sql file, then gzip that and www-root into the local image store.
 * A small number of older copies is kept.
 *
 * Alternatively, specify -dbonly when running this script. In this case, only the .sql file will be saved in
 * the local image store.
 *
 * From this point, there are two choices:
 *
 * 1. Configure (at http(s)://your.icebear.install/config) both a remote backup path and number of remote
 *    www-root/database backups to keep, minimum 1.
 *    - Imported drop images will be written to both the local and backup image store as they are imported.
 *    - Users' uploaded files will be rsynced to remote backup when this script runs.
 *    - The gzipped www/database backup will be copied from local to remote, with these being rotated and
 *      the specified number of backups kept.
 *
 * 2. Don't configure them. You must have another backup strategy in place.
 *    - Drop images will be written to the local store only.
 *    - www-root/database dumps will be written to the local store only.
 *    - Users' uploaded files will be written to the local store only.
 *
 * The database will be effectively read-only for the duration of its backup, so we write a small file
 * before starting the database backup and remove it afterward. Thus, imager importers can check for
 * the existence of this file and abort if found.
 *
 * Should the database dump fail, this lock file will NOT be removed, and imager importers SHOULD complain
 * about this and stop. A lack of new plate inspections in IceBear could mean a failed backup - but backups can
 * fail in other ways. You should satisfy yourself that your backups are being taken properly and continue
 * to monitor this.
 *
 */


//If called from web, bail - command line/cron only. .htaccess should do this but be safe.
if('cli'!==php_sapi_name()){
    die("Backup script can only be called from command line or cron");
}

set_time_limit(0);
setUpClassAutoLoader();

$logLevel=Log::LOGLEVEL_INFO; //may be overridden by argument
$dbOnly=false;

for($i=1;$i<count($argv);$i++) {
    $arg = $argv[$i];
    if ($arg == '-h') {
        showHelp();
    } else if ($arg == '-dbonly') {
        $dbOnly = true;
    } else if (preg_match('/^-l[1-4]$/', $arg)) {
        $logLevel = (int)substr($arg, 2);
    }
}

Log::init($logLevel);
Log::write(Log::LOGLEVEL_INFO,'Beginning backup');
if($dbOnly){
    Log::write(Log::LOGLEVEL_INFO,'Beginning backup of IceBear database only. NOT BACKING UP WWW DIRECTORY');
} else {
    Log::write(Log::LOGLEVEL_INFO,'Beginning backup of IceBear database and www directory');
}

try{

    //get db connection details from conf/config.ini
    $dbDetails=parse_ini_file(realpath(__DIR__).'/../conf/config.ini');
    if(!$dbDetails){
        throw new Exception('Could not read database connection details from config file');
    }
    $db = new PDO('mysql:host='.$dbDetails['dbHost'].';dbname='.$dbDetails['dbName'], $dbDetails['dbUser'], $dbDetails['dbPass'], array(PDO::MYSQL_ATTR_FOUND_ROWS => true));
    $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);


    //connect to database
    Log::write(Log::LOGLEVEL_DEBUG,'Attempting to get LIMS DB connection... ');
    database::connect();
    Log::write(Log::LOGLEVEL_DEBUG,'...got LIMS DB connection');

    Log::write(Log::LOGLEVEL_DEBUG,'Attempting to get LIMS session... ');
    session::init(new DummySession());
    session::set('isAdmin',true);
    Log::write(Log::LOGLEVEL_DEBUG,'...got LIMS session');

    //get config items
    $imageStoreRoot=rtrim(config::get('core_imagestore'),'/\\').'/';
    $localBackupStorage=$imageStoreRoot.'dbbackups/';
    $localBackupsToKeep=3;

    $remoteBackupStorage=rtrim(config::get('core_databasebackup'),'/\\').'/';
    $remoteBackupsToKeep=config::get('core_databasebackupstokeep');
    $remoteBackupsToKeep=(int)$remoteBackupsToKeep;
    $remoteBackupsToKeep=max($remoteBackupsToKeep,0);

    $remoteFilesBackup=config::get('core_filestorebackup');
    $remoteFilesBackup=rtrim($remoteFilesBackup,'/').'/';
    $localFileStore=config::get('core_filestore');
    $localFileStore=rtrim($localFileStore,'/').'/';

    //destroy LIMS session, no longer needed
    session::destroy();

    //verify that local backup dir exists; attempt to create it otherwise
    if(!@file_exists($localBackupStorage)){
        @mkdir($localBackupStorage);
        if(!@file_exists($localBackupStorage)){
            throw new Exception("Directory for local database backups does not exist: ".$localBackupStorage);
        }
    }

    //touch .dbbackuprunning - importer should check for existence and bail if found
    Log::write(Log::LOGLEVEL_DEBUG, 'Writing lock file before database backup...');
    if(!@touch($imageStoreRoot.'dbbackuprunning')){
        throw new Exception("Could not create lock file before database backup");
    }
    Log::write(Log::LOGLEVEL_DEBUG, '...Done.');

    //dump the database to .sql file in backup storage root
    Log::write(Log::LOGLEVEL_DEBUG, 'Commencing database dump...');
    $sqlFilename='icebear.sql';
    $cmd='mysqldump --no-tablespaces '.$dbDetails['dbName'].' --password=\''.$dbDetails['dbPass'].'\' --user=\''.$dbDetails['dbUser'].'\' --no-tablespaces --single-transaction >'.$localBackupStorage.$sqlFilename;
    $result=exec($cmd,$output);
    Log::write(Log::LOGLEVEL_DEBUG, '...Database dump done.');

    //remove lock file
    Log::write(Log::LOGLEVEL_DEBUG, 'Removing lock file after database backup...');
    if(!@unlink($imageStoreRoot.'dbbackuprunning')){
        throw new Exception("Could not remove lock file after database backup");
    }
    Log::write(Log::LOGLEVEL_DEBUG, '...Done.');

    //Bail if database dump failed
    if(!empty($output)){
		foreach ($output as $line) {
			Log::error($line);
		}
        throw new Exception('Database dump failed');
    }

    if($dbOnly){

        Log::write(Log::LOGLEVEL_INFO, 'Database-only backup - not tarring with application code');

    } else {

        Log::write(Log::LOGLEVEL_INFO, 'Tarring up database dump with application code...');

        //make .tar.gz from .sql and /var/www - assume backup script is located at [wwwroot]/backup/backup.php
        $parts=explode('/backup', __DIR__);
        $wwwroot=$parts[0];
        $result=system('tar -czf '.$localBackupStorage.'icebear_inprogress.tar.gz '.$localBackupStorage.$sqlFilename.' '.$wwwroot);

        //remove the .sql file
        @unlink($localBackupStorage.$sqlFilename);

        //rotate older local backups
        for($i=$localBackupsToKeep-1;$i>0;$i--){
            @rename($localBackupStorage.'icebear.tar.gz.'.$i, $localBackupStorage.'icebear.tar.gz.'.($i+1));
        }
        @rename($localBackupStorage.'icebear.tar.gz', $localBackupStorage.'icebear.tar.gz.1');
        //Rename new backup into place
        @rename($localBackupStorage.'icebear_inprogress.tar.gz', $localBackupStorage.'icebear.tar.gz');

        Log::write(Log::LOGLEVEL_INFO, '...done');
    }


    /* At this point, we have local backups of the database and www-root. If we're using an enterprise-grade whole-disk backup system,
     * we probably don't need to do anything else. Otherwise, we need to have BOTH the remote backup path AND the number of db/www
     * backups to keep.
     */

    if(0==$remoteBackupsToKeep || empty(rtrim($remoteBackupStorage,'/\\'))){

        Log::write(Log::LOGLEVEL_WARN, 'Not backing up remotely because configuration says not to.');
        Log::write(Log::LOGLEVEL_WARN, 'Any existing remote backups will remain in place.');

    } else {

        //verify that core_icebearbackups exists and can be written
        if(!@file_exists($remoteBackupStorage)){
            throw new Exception("Directory for remote backups does not exist: ".$remoteBackupStorage);
        }

        if($dbOnly){

            //rotate older backups on core_icebearbackups
            //Note that we DO NOT remove older backups beyond the specified period. A malicious IceBear admin could have reduced the
            //backup period before causing mischief.
            for($i=$remoteBackupsToKeep-1;$i>0;$i--){
                @rename($remoteBackupStorage.$sqlFilename.'.'.$i, $remoteBackupStorage.$sqlFilename.'.'.($i+1));
            }
            @rename($remoteBackupStorage.$sqlFilename, $remoteBackupStorage.$sqlFilename.'.1');

            //Copy the latest local backup to remote
            @copy($localBackupStorage.$sqlFilename, $remoteBackupStorage.$sqlFilename);

        } else {

            //rotate older backups on core_icebearbackups
            //Note that we DO NOT remove older backups beyond the specified period. A malicious IceBear admin could have reduced the
            //backup period before causing mischief.
            for($i=$remoteBackupsToKeep-1;$i>0;$i--){
                @rename($remoteBackupStorage.'icebear.tar.gz.'.$i, $remoteBackupStorage.'icebear.tar.gz.'.($i+1));
            }
            @rename($remoteBackupStorage.'icebear.tar.gz', $remoteBackupStorage.'icebear.tar.gz.1');

            //Copy the latest local backup to remote
            @copy($localBackupStorage.'icebear.tar.gz', $remoteBackupStorage.'icebear.tar.gz');

        }

        /* Back up users' downloaded files, if configured to do so.
         * We use rsync for this.
         */
        Log::write(Log::LOGLEVEL_DEBUG, 'File uploads: Synching to backup...');
        if(empty(rtrim($remoteFilesBackup,'/\\'))){
            Log::write(Log::LOGLEVEL_INFO, 'core_filestorebackup not set - not backing up file uploads');
        } else if($dbOnly){
            Log::write(Log::LOGLEVEL_INFO, 'Database-only backup - not backing up file uploads');
        } else {
            $cmd='rsync -qa '.$localFileStore.' '.$remoteFilesBackup;
            Log::write(Log::LOGLEVEL_DEBUG, 'Source: '.$localFileStore);
            Log::write(Log::LOGLEVEL_DEBUG, 'Dest: '.$remoteFilesBackup);
            Log::write(Log::LOGLEVEL_DEBUG, 'Command: '.$cmd);
            $output='';
            exec($cmd,$output);
            if(!empty($output)){
				foreach ($output as $line){
					Log::error($line);
				}
                throw new Exception('Could not rsync file uploads backup');
            }
        }
        Log::write(Log::LOGLEVEL_DEBUG, '...Done');
    }

} catch(Exception $e){
    Log::write(Log::LOGLEVEL_ERROR, get_class($e).' caught: '.$e->getMessage());
    $trace=$e->getTrace();
    foreach($trace as $t){
        Log::write(Log::LOGLEVEL_ERROR, ': '.$t['file']);
        if(isset($t['type']) && isset($t['class'])){
            Log::write(Log::LOGLEVEL_ERROR, ':    Line '.$t['line'].', Function '.$t['class'].$t['type'].$t['function']);
        } else {
            Log::write(Log::LOGLEVEL_ERROR, ':    Line '.$t['line'].', Function '.$t['function']);
        }
        //TODO $t['args']
    }
    Log::write(Log::LOGLEVEL_ERROR, 'Backup threw exception. Ending.');
}


Log::write(Log::LOGLEVEL_INFO,'Backup script finished.');
Log::write(Log::LOGLEVEL_INFO,'=================================================');

Log::end();

exit; //functions below

function setUpClassAutoLoader(){
    spl_autoload_register(function($className){
        $paths=array(
            '../classes/',
            '../classes/core/',
            '../classes/core/exception/',
            '../classes/core/authentication/',
            '../classes/core/interface/',
            '../classes/model/',
        );
        foreach($paths as $path){
            if(file_exists(__DIR__.'/'.$path.$className.'.class.php')){
                include_once(__DIR__.'/'.$path.$className.'.class.php');
            }
        }
    });
}

function showHelp(){
    echo "Backs up the IceBear database and the web application files\n";
    echo "Backups will be placed in the directory specified in IceBear config : Storage and backup : Directory where database backups are stored\n";
    echo "Call with -dbonly for an SQL dump of the database only, with no webapp files\n";
    echo "Call with -l1 for full debug info, 2 is info (default), 3 only warnings and errors, 4 errors only\n";
}

