<?php

/********************************************
 * FTP & AMAZON S3 SERVER BACKUP SCRIPT
 * By Jordi Romkema (http://www.jor-on.com)
 * Version 1.0: January 7th 2010
 * Original version by Eric Nagel
 * http://www.ericnagel.com/2009/05/ftp-or-amazon-s3-server-backup-php-script.html 
 *******************************************/

set_time_limit(3600);
error_reporting(E_ALL);

/********************************************
 * MySQL variables
 *******************************************/
$backup_mysql true// Do you want to backup MySQL databases?
$mysql "/usr/bin/mysql";
$mysqldump "/usr/bin/mysqldump";

$mysql_server "localhost";
$mysql_username "XXXX";
$mysql_password "XXXX";

/********************************************
 * Httpdocs variables
 *******************************************/
$backup_httpd true// Do you want to backup httpdocs directories?
$vhosts_dir "/var/www/vhosts/"// Your virtual host directory where your domain directories are stored, include trailing slash

/********************************************
 * Utility variables
 *******************************************/
$tar "/bin/tar";
$split "/usr/bin/split";

/********************************************
 * Settings
 *******************************************/
$days 5// How many days of backups to keep?
$maxfilesize 300// What's the maximum file size (in MB) for compressed files? If a compressed file is larger than the maximum file size, the file will be split into smaller pieces with the maximum size.
$interval 24// Every x hours this script will backup the server.
$working_dir "/home/XXXX/backups/"// This is where temporary files are written to, please make sure this script has permissions to write, read and delete in this directory. Include trailing slash
$process_file $working_dir "server_backup.tmp"// This file is used to read/write the state of the script to. If a process for some reason doesn't continue, it can help to delete this file. You don't have to create it yourself, the script will do this for you
$suffix date("Ymd"); // What suffix to add to filenames, if daily then make it the date. If you going to run it hourly, unclude the time aswell or make it unique.
$debug true// Turn on debug, you'll see debug messages in your console
$debug_time_format "d-M-Y H:i:s"// Date/time format for debug messages

/********************************************
 * Amazon S3 setup
 *******************************************/
$backup_s3 true// Do you want to send your backups to your Amazon S3 account?

if ($backup_s3)
{
    
$s3_access_key_id "XXXX";
    
$s3_secret_access_key "XXXX";
    
$s3_bucket_name "XXXX";
    
    
/*
     * Amazon S3 PHP class
     * 
     * @link http://undesigned.org.za/2007/10/22/amazon-s3-php-class
     */
    
    
include ("/path/to/AmazonS3/Class.php");
}

/********************************************
 * FTP setup
 *******************************************/
$backup_ftp false// Do you want to send your backups to an FTP account?

if ($backup_ftp)
{
    
$ftp_server "XXXX";
    
$ftp_username "XXXX";
    
$ftp_password "XXXX";
}

/********************************************
 * You are all set, no need to
 * change anything below this line!
 *******************************************/

function d($message)
{
    global 
$debug$debug_time_format;
    
    if (
$debug)
    {
        echo(
date($debug_time_formattime()) . ": " $message "\n");
    }
}

function 
get_process_data($file$default)
{
    
$data "";
    
    if (
file_exists($file))
    {
        
$handle fopen($file"r");
        
        while (!
feof($handle))
        {
            
$data .= fread($handle8192);
        }
        
        
fclose($handle);
    }
    
    if (empty(
$data))
    {
        
$data $default;
    }
    else
    {
        
$data unserialize($data);
    }
    
    return 
$data;
}

function 
save_process_data($file$data)
{
    
$handle fopen($file"w");
    
    
fwrite($handleserialize($data));
    
fclose($handle);
}

function 
shutdown()
{
    global 
$process_file$process_data;
    
    
$process_data["locked"] = false;
    
    
save_process_data($process_file$process_data);
}

register_shutdown_function("shutdown");

chdir($working_dir);

// get content of the process data file
$process_data get_process_data($process_file, array(
    
"state"        =>    "mysql",
    
"locked"    =>    false,
    
"start"        =>    time()
));

$ignore_process_lock false;

if (isset(
$argv[1]))
{
    if (
$argv[1] == "ignore-lock")
    {
        
d("Ignoring process lock");
        
$ignore_process_lock true;
    }
}

// is the process locked?
// if it's locked it means that this script was called again too soon, the same script should already be running... this is okay though, don't worry! it should continue normally once the other script has finished doing its job and the process will be unlocked.
if (($process_data["locked"] == true) && ($ignore_process_lock == false))
{
    
// process is locked, the script is already running, no need to continue...
    
d("Process is locked, exiting now");
    exit();
}
else
{
    
// lock current process for now until we're finished
    
$process_data["locked"] = true;
    
d("Process is unlocked, lock current process until we're finished");
    
    
save_process_data($process_file$process_data);
}

// what's the current state of the script?
$current_state $process_data["state"];
$new_state $current_state;

$mysql_backups_dir $working_dir $suffix "_mysql_backups/";
$httpdocs_backups_dir $working_dir $suffix "_httpdocs_backups/";

d("Current state of process is: $current_state");

switch (
$current_state)
{        
    case 
"mysql":
        
// first we do mysql backups
        
        
if ($backup_mysql)
        {
            
d("Trying to get a list of all databases...");
            
            
$mysql_connection mysql_connect($mysql_server$mysql_username$mysql_password);            
            
$db_list mysql_list_dbs($mysql_connection);

            if (!
is_dir($mysql_backups_dir))
            {
                
d("Creating temporary mysql backup directory: " $mysql_backups_dir);
                
mkdir($mysql_backups_dir);
            }
            
            while (
$row mysql_fetch_object($db_list))
            {
                
$database $row->Database;
                
$sql_filename $database "_" $suffix ".sql";
                
$targz_filename $sql_filename ".tar.gz";
                
$targz_filepath $mysql_backups_dir $targz_filename;
                
                
d("Database $database found");
                
d("Dumping database $database to $sql_filename...");
                                
                
$cli_result shell_exec($mysqldump " -u" $mysql_username " -p" $mysql_password " " $database " > " $sql_filename);
                
                
d("Compressing mysql dump to $targz_filename...");
                
                
$cli_result shell_exec($tar " -cpzf " $targz_filepath " " $sql_filename);
                
                
d("Cleaning up mysql dump...");
                
                
unlink($sql_filename);
                
                
$size explode(" "shell_exec("du -b " $targz_filepath));
                
$size = (int) $size[0];
            
                
d("File size of $targz_filepath is $size bytes");
                
                
// check if tar.gz file is larger than $maxfilesize
                
if ($size > ($maxfilesize 1024 1024))
                {
                    
d("File $targz_filepath is larger than maximum allowed size, we need to split this file first...");
                    
                    
shell_exec("split --bytes=" $maxfilesize "m -d " $targz_filepath " " $targz_filepath ".p");
                    
                    
d("File has been split... deleting original file");
                    
                    
unlink($targz_filepath);
                }
                
                
// we're done here... next we need to upload the single archive file or the multiple split files
                
$new_state "mysql_upload";
            }
        }
        else
        {
            
d("Skipping mysql backup, change the config if you would like to execute mysql backups as well...");
            
            
$new_state "httpdocs";
        }
        
        break;
        
    case 
"mysql_upload":
        
// get a list of all the files in the mysql backups directory, we need to upload
        
$files = array();

        
d("Looking for mysql backup files to upload...");
                    
        if (
$handle opendir($mysql_backups_dir))
        {
            while (
false !== ($file readdir($handle)))
            {
                if (!
in_array($file, array(".""..""chroot""default"".skel")))
                {
                    
$files[] = $file;
                }
            }
        }
        
        
closedir($handle);
                
        if (!empty(
$files))
        {
            
$backup_file $mysql_backups_dir $files[0];
            unset(
$files[0]);
            
            
d("Found $backup_file, now uploading...");
            
            if (
$backup_s3)
            {
                
$s3 = new S3($s3_access_key_id$s3_secret_access_key);
                
$s3->useSSL false;
                
$s3->putObjectFile($backup_file$s3_bucket_namebasename($backup_file), S3::ACL_PRIVATE);
            }
            
            if (
$backup_ftp)
            {
                
$ftp_handle ftp_connect($ftp_server);
                
ftp_login($ftp_handle$ftp_username$ftp_password);
                
ftp_put($ftp_handlebasename($backup_file), $backup_fileFTP_BINARY);
                
ftp_close($ftp_handle);
            }
            
            
d("Uploaded $backup_file, now deleting the original backup file");
            
            
unlink($backup_file);
            
            if (
count($files) > 0)
            {
                
// there are still more files to upload, stay in the same state...
                
$new_state "mysql_upload";
            }
            else
            {
                
$new_state "httpdocs";
            }
        }
        else
        {
            
// no more files to upload, go to the next state
            
$new_state "httpdocs";
        }
        
        if (
$new_state == "httpdocs")
        {
            
rmdir($mysql_backups_dir);
        }
        
        break;
        
    case 
"httpdocs":
        
// then we do httpdocs backups
        
        
if ($backup_httpd)
        {
            
chdir($vhosts_dir);
            
            if (!
is_dir($httpdocs_backups_dir))
            {
                
mkdir($httpdocs_backups_dir);
            }
            
            
d("Trying to get a list of all domains...");
            
            if (
$dir_handle opendir($vhosts_dir))
            {
                while (
false !== ($httpd_filename readdir($dir_handle)))
                {
                    if (!
in_array($httpd_filename, array(".""..""chroot""default"".skel")))
                    {
                        
$targz_filename $httpd_filename "_" $suffix ".tar.gz";
                        
$targz_filepath $httpdocs_backups_dir $targz_filename;
                        
                        
d("Domain $httpd_filename found");                        
                        
d("Compressing files to $targz_filename...");
                
                        
$cli_result shell_exec($tar " -cpzf " $targz_filepath " " $httpd_filename);
                        
                        
$size explode(" "shell_exec("du -b " $targz_filepath));
                        
$size = (int) $size[0];
                                    
                        
d("File size of $targz_filepath is $size bytes");
                
                        
// check if tar.gz file is larger than $maxfilesize
                        
if ($size > ($maxfilesize 1024 1024))
                        {
                            
d("File $targz_filepath is larger than maximum allowed size, we need to split this file first...");
                            
                            
shell_exec("split --bytes=" $maxfilesize "m -d " $targz_filepath " " $targz_filepath ".p");
                            
                            
d("File has been split... deleting original file");
                            
                            
unlink($targz_filepath);
                        }
                    }
                }
            }
            
            
closedir($dir_handle);
            
            
// we're done here... next we need to upload the single archive file or the multiple split files
            
$new_state "httpdocs_upload";
        }
        else
        {
            
d("Skipping httpdocs backup, change the config if you would like to execute httpdocs backups as well...");
            
            
$new_state "cleanup";
        }
        
        break;
        
    case 
"httpdocs_upload":        
        
// get a list of all the files in the httpdocs backups directory, we need to upload
        
$files = array();

        
d("Looking for httpdocs backup files to upload...");
                    
        if (
$handle opendir($httpdocs_backups_dir))
        {
            while (
false !== ($file readdir($handle)))
            {
                if (!
in_array($file, array(".""..""chroot""default"".skel")))
                {
                    
$files[] = $file;
                }
            }
        }

        
closedir($handle);
                
        if (!empty(
$files))
        {
            
$backup_file $httpdocs_backups_dir $files[0];
            unset(
$files[0]);
            
            
d("Found $backup_file, now uploading...");
            
            if (
$backup_s3)
            {
                
$s3 = new S3($s3_access_key_id$s3_secret_access_key);
                
$s3->useSSL false;
                
$s3->putObjectFile($backup_file$s3_bucket_namebasename($backup_file), S3::ACL_PRIVATE);
            }
            
            if (
$backup_ftp)
            {
                
$ftp_handle ftp_connect($ftp_server);
                
ftp_login($ftp_handle$ftp_username$ftp_password);
                
ftp_put($ftp_handlebasename($backup_file), $backup_fileFTP_BINARY);
                
ftp_close($ftp_handle);
            }
            
            
d("Uploaded $backup_file, now deleting the original backup file");
            
            
unlink($backup_file);
            
            if (
count($files) > 0)
            {
                
// there are still more files to upload, stay in the same state...
                
$new_state "httpdocs_upload";
            }
            else
            {
                
$new_state "cleanup";
            }
        }
        else
        {
            
// no more files to upload, go to the next state
            
$new_state "cleanup";
        }
        
        if (
$new_state == "cleanup")
        {
            
rmdir($httpdocs_backups_dir);
        }
        
        break;
        
    case 
"cleanup":
        
// clean up old files from the server
        
        
d("Cleaning up old files from the server...");
        
        if (
$backup_s3)
        {
            
$s3 = new S3($s3_access_key_id$s3_secret_access_key);
            
$s3->useSSL false;
            
            
$s3_files $s3->getBucket($s3_bucket_name);
            
            while (list(
$s3_file$s3_file_data) = each($s3_files))
            {
                if (
$s3_file_data["time"] + ($days 24 60 60) < time())
                {
                    
d("Deleting outdated file from S3: " $s3_file);
                    
$s3->deleteObject($s3_bucket_name$s3_file);
                }
            }            
        }
        
        if (
$backup_ftp)
        {
            
$ftp_handle ftp_connect($ftp_server);
            
ftp_login($ftp_handle$ftp_username$ftp_password);
            
            
$list = @ftp_rawlist($ftp_handle".");
            
            
$ftp_files = array();
            
            foreach (
$list as $_)
            {
                
preg_replace(
                    
'`^(.{10}+)(\s*)(\d{1})(\s*)(\d*|\w*)'.
                    
'(\s*)(\d*|\w*)(\s*)(\d*)\s'.
                    
'([a-zA-Z]{3}+)(\s*)([0-9]{1,2}+)'.
                    
'(\s*)([0-9]{2}+):([0-9]{2}+)(\s*)(.*)$`Ue',
                
                    
'$ftp_Files[]=array(
                    "rights"=>"$1",
                    "number"=>"$3",
                    "owner"=>"$5", "group"=>"$7",
                    "file_size"=>"$9",
                    "mod_time"=>"$10 $12 $14:$15",
                    "file"=>"$17",
                    "type"=>print_r((preg_match("/^d/","$1"))?"dir":"file",1));'
,
                
                
$_);
                
                foreach (
$ftp_files as $ftp_file)
                {
                    
$ftp_filename trim($ftp_file["file"]);
                    
$mod_time strtotime($ftp_file["mod_time"]);
                    
                    if (
$mod_time + ($days 24 60 60) < time())
                    {
                        
d("Deleting outdated file from FTP: " $ftp_filename);
                        
                        
ftp_delete($ftp_handle$ftp_filename);
                    }
                }
            }
            
            
ftp_close($ftp_handle);
        }
        
        
d("Finished cleaning up, we're done!");
        
        
$new_state "finished";
        
        break;
        
    case 
"finished":
        
        
d("Previous process has finished, do we need to start a new one already?");
        
        if (
$process_data["start"] + ($interval 60 60) < time())
        {
            
d("Yes, new process should be started on the next run!");
            
            
$new_state "mysql";
        }
        else
        {
            
$minutes_to_go round(($process_data["start"] + ($interval 60 60) - time()) / 600);
            
d("No, only $minutes_to_go minute" . (($minutes_to_go == 1) ? "" "s") . " to go before starting a new process..");
        }        
        
        break;    
}

// write new process data to file and unlock it, we're finished... for now at least...
$process_data["state"] = $new_state;
$process_data["locked"] = false;

d("New state of process is: " $new_state);
d("We're finished, for now... unlock current process");

save_process_data($process_file$process_data);

d("Exiting...");

?>