Backup Linux server with PHP

You can find a lot of backup solutions for Linux server. From expensive and multi platform software like NetBackup to simple BASH scripts. As a Linux (LAMP) administrator, I decided to write my own backup script in PHP. I wanted to backup important locations like Web root, crontabs, /etc, /root and copy MySQL dump files. My script should be able to copy created tar.gz files and MySQL dumps to the remote server.

In post LAMP setup: Beginning I suggest how to arrange partitions for the LAMP server. Last mentioned partition was /backup and this is the location to save daily backups for the LAMP server – it’s big enough to keep backup for the last three days. I will not explain backup strategy, but in my case, I am satisfied with archived files to the local directory (partition) and remote server.

At the end, you will see complete PHP Backup class. Now I will show example how to save content of the /etc directory to the local directory.

// initialize Backup class and define local directory 
$backup = new Backup('/backup/archive'); 
// make backup of the /etc directory
$backup->tar('/etc');

After backup is done, you will see log file in the /tmp directory and etc_2009-03-26.tar.gz in the /backup/archive directory. You can name the backup file if you like. For example, you can include /var/spool/cron files to the backup, and name it crontabs:

// initialize Backup class and define local directory 
$backup = new Backup('/backup/archive'); 
// backup crontab files
$backup->tar('/var/spool/cron', 'crontabs');

Like in the previous example, crontabs_2009-03-25.tar.gz will be saved to the /backup/archive directory. One of my LAMP servers mounts exported NFS directory to the document root. I have to backup document root, but without mounted directory. So if you have similar situation, you can exclude files from the archive based upon pattern search.

// initialize Backup class and define local directory 
$backup = new Backup('/backup/archive'); 
// backup document root, but without mnt directory
$backup->tar('/var/www', 'www', 'html/mnt/*');

Third parameter of the method “tar” means “exclude”, so if you need to use exclude parameter, you will have to define and the second parameter – name. All this examples save created archive to the local directory. With “tarc” and “rcopy” methods, archives can be copied to the remote server by scp command. Before you start to use “tarc” and “rcopy” method, you will have to exchange keys between LAMP server and remote server. Here is example how to backup Web server to the local directory and to the remote server:

// initialize Backup class, define local directory and remote server
$backup = new Backup('/backup/archive', 'user@server1:/backup/lamp1'); 
// backup document root, etc and crontab directory
$backup->tarc('/var/www');
$backup->tarc('/etc');
$backup->tarc('/var/spool/cron', 'crontabs');

Backup class has option to recursive copy matched files to the remote location. You will have to define start point and the pattern to match files. I have to copy MySQL daily backups to the remote server. Another script – automysqlbackup.sh exports MySQL databases to the file system. Dump files have current date and “sql” in the name, so backup script will copy “sql” files with current date in the name to the user@server1:/backup/lamp1 remote location.

// initialize Backup class, define local directory and remote server
$backup = new Backup('/backup/archive', 'user@server1:/backup/lamp1');
// define current date (needed to find recent MySQL dump files)
$today = date('Y-m-d');
// backup document root, etc, root home and crontab files
$backup->tarc('/var/www');
$backup->tarc('/etc');
$backup->tarc('/root', 'root_home');
$backup->tarc('/var/spool/cron', 'crontabs');
// copy MySQL dump files to the remote server
$backup->rcopy('/usr/mysql/export/daily', "*$today*sql*");

As you can see, Backup class has many options, but it’s not perfect. It can be extended with ftp option, without need for the local directory, option to define path and name of the log file … I tried to write readable and well commented PHP code, so you shouldn’t have any problem to understand, modify or customize Backup class for your needs.

/**
 * Backup class has three public methods
 * tar   - saves archive to the local directory
 * tarc  - saves archive to the local directory and copies to the remote destination
 * rcopy - recursive copy matched files to the remote location
 *
 */
class Backup{
	// define private properties
	private $log;        // pointer to the log object
	private $backup_dir; // local directory
	private $scp_server; // remote destination
	
	/**
	 * constructor
	 *
	 * @param string $backup_dir - local directory where tar.gz files should be saved
	 * @param string $scp_server - (optional) remote location to save archives
	 */
	function __construct($backup_dir, $scp_server=null){
		$this->log = new Logging();      // initialize Logging class
		$this->backup_dir = $backup_dir; // set local backup directory
		$this->scp_server = $scp_server; // set remote location
	}

	/**
	 * method saves archive to the local directory and copies to the remote destination
	 *
	 * @param string $dir     - directory you want to archive
	 * @param string $name    - (optional) name of the created archive
	 * @param string $exclude - (optional) excluded files from archive based upon pattern
	 */
	public function tarc($dir, $name=null, $exclude=null){
		// call tar method to create archive in the local directory
		$tar = $this->tar($dir, $name, $exclude);
		// after archive is created, copy archive to the remote location
		$this->scp($tar);		
	}

	/**
	 * method saves archive to the local directory
	 *
	 * @param string $dir     - directory you want to archive
	 * @param string $name    - (optional) name of the created archive
	 * @param string $exclude - (optional) files / subtree to exclude from tar
	 */
	public function tar($dir, $name=null, $exclude=null){
		// get local backup directory from the class property
		$backup_dir = $this->backup_dir;
		// define current date
		$today = date('Y-m-d');
		// if tar name is not defined, use base name of the given path
		if (!$name) $name = basename($dir);
		// create complete tar name (path and name) 
		$tar = "$backup_dir/$name"."_$today.tar.gz";
		// define excluded files based upon pattern (if needed)
		if ($exclude) $exclude = "--exclude=\"$exclude\"";
		// remove leading '/' from the path
		$dir = substr($dir, 1);
		// execute tar command relative from / directory to
		// avoid "tar: Removing leading `/' from member names" warning
		$this->run("tar zcf $tar $exclude -C / $dir");
		// return tar name (needed for tarc method)
		return $tar;
	}

	/**
	 * recursive copy matched files to the remote location 
	 *
	 * @param string $dir     - directory to start with
	 * @param string $pattern - pattern to search
	 */
	public function rcopy($dir, $pattern){
		// find files matching pattern
		$files = $this->find($dir, $pattern);
		// loop through found files and call scp method
		foreach ($files as $file) $this->scp($file);
	}	

	/**
	 * create scp command and call run method
	 *
	 * @param string $file - absolute path to the source file
	 */
	private function scp($file){
		// get remote location from the class property
		$scp_server = $this->scp_server;
		// execute and log scp command if scp server is defined
		if ($scp_server) $this->run("/usr/bin/scp -pq $file $scp_server/");
		else $this->log->lwrite("Warning: scp server is not defined for $file!");
	}

	/**
	 * method executes given command and logs to the log file
	 *
	 * @param string $command - command to execute
	 */
	private function run($command){
		// execute command and redirect STDERR to the STDOUT
		$output = shell_exec("$command 2>&1");
		// log command and command output if exists to the log file
		$this->log->lwrite($command);
		if ($output) $this->log->lwrite($output);
	}

	/**
	 * find files matching a pattern
	 * using unix "find" command
	 *
	 * @return array containing all pattern-matched files or empty array
	 *
	 * @param string $dir     - directory to start with
	 * @param string $pattern - pattern to search
	 */
	private function find($dir, $pattern){
		// escape any character in a string that might be used to trick
		// a shell command into executing arbitrary commands
		$dir = escapeshellcmd($dir);
		// if directory exists, then execute find command
		if (file_exists($dir)){
			// execute "find" and return string with found files
			$files = shell_exec("find $dir -name '$pattern' -print");
			// create array from the returned string (trim will strip the last newline)
			$files = explode("\n", trim($files));
			// return array
			return $files;
		}
		// else write warning to the log and return empty array
		else{
			$this->log->lwrite("Warning: $dir does not exist (find)!");
			return Array();
		}
	}
}

You have probably noticed initialization of the Logging class in the constructor, but you can’t find the code. Logging class is described in Write to a log file with PHP, so you only need to place Logging and Backup class to the same PHP file. I also used code from my previous post Find files with PHP for private method “find”. The only modification is in checking if directory exists.

Leave a Comment