-backup-.~" */ class Loco_fs_Revisions implements Countable/*, IteratorAggregate*/ { /** * @var loco_fs_File */ private $master; /** * Sortable list of backed up file paths (not including master) * @var array */ private $paths; /** * Cached regular expression for matching backup file paths * @var string */ private $regex; /** * Cached count of backups + 1 * @var int */ private $length; /** * Paths to delete when object removed from memory * @var array */ private $trash = array(); /** * Construct from master file (current version) */ public function __construct( Loco_fs_File $file ){ $this->master = $file; } /** * @internal * Executes deferred deletions with silent errors */ public function __destruct(){ if( $trash = $this->trash ){ $writer = clone $this->master->getWriteContext(); foreach( $trash as $file ){ if( $file->exists() ){ try { $writer->setFile($file); $writer->delete(false); } catch( Loco_error_WriteException $e ){ // avoiding fatals as pruning is non-critical operation Loco_error_AdminNotices::debug( $e->getMessage() ); } } } } } /** * Check that file permissions allow a new backup to be created * @return bool */ public function writable(){ return $this->master->exists() && $this->master->getParent()->writable(); } /** * Create a new backup of current version * @return Loco_fs_File */ public function create(){ $vers = 0; $date = date('YmdHis'); $ext = $this->master->extension(); $base = $this->master->dirname().'/'.$this->master->filename(); do { $path = sprintf( '%s-backup-%s%u.%s~', $base, $date, $vers++, $ext); } while ( file_exists($path) ); $copy = $this->master->copy( $path ); // invalidate cache so next access reads disk $this->paths = null; $this->length = null; return $copy; } /** * Delete oldest backups until we have maximuim of $num_backups remaining * @return Loco_fs_Revisions */ public function prune( $num_backups ){ $paths = $this->getPaths(); if( isset($paths[$num_backups]) ){ foreach( array_slice( $paths, $num_backups ) as $path ){ $this->unlinkLater($path); } $this->paths = array_slice( $paths, 0, $num_backups ); $this->length = null; } return $this; } /** * build regex for matching backed up revisions of master * @return string */ private function getRegExp(){ $regex = $this->regex; if( is_null($regex) ){ $regex = preg_quote( $this->master->filename(), '/' ).'-backup-(\\d{14,})'; if( $ext = $this->master->extension() ){ $regex .= preg_quote('.'.$ext,'/'); } $regex = '/^'.$regex.'~/'; $this->regex = $regex; } return $regex; } /** * @return array */ public function getPaths(){ if( is_null($this->paths) ){ $this->paths = array(); $regex = $this->getRegExp(); $finder = new Loco_fs_FileFinder( $this->master->dirname() ); /* @var $file Loco_fs_File */ foreach( $finder as $file ){ if( preg_match( $regex, $file->basename(), $r ) ){ $this->paths[] = $file->getPath(); } } // time sort order descending rsort( $this->paths ); } return $this->paths; } /** * Parse a file path into a timestamp * @return int */ public function getTimestamp( $path ){ $name = basename($path); if( preg_match( $this->getRegExp(), $name, $r ) ){ $ymdhis = substr( $r[1], 0, 14 ); return strtotime( $ymdhis ); } throw new Loco_error_Exception('Invalid revision file: '.$name); } /** * Get number of backups plus master * @return int */ public function count(){ if( ! $this->length ){ $this->length = 1 + count( $this->getPaths() ); } return $this->length; } /** * Delete file when object removed from memory. * Previously unlinked on shutdown, but doesn't work with WordPress file system abstraction * @return void */ public function unlinkLater($path){ $this->trash[] = new Loco_fs_File($path); } /** * Test whether at least one backup file exists on disk. * @return bool * public function sniff(){ $found = false; if( $dir = opendir( $this->master->dirname() ) ){ $regex = $this->getRegExp(); while( $f = readdir($dir) ){ if( preg_match($regex,$f) ){ $found = true; break; } } closedir($dir); } return $found; }*/ }