Skip to main content
edited tags
Link
Jamal
  • 35.2k
  • 13
  • 134
  • 238
edited tags
Link
200_success
  • 145.7k
  • 22
  • 191
  • 481
fixed syntax highlighting + slight title changes
Source Link
tim
  • 25.3k
  • 3
  • 31
  • 76

php database Database and filesfile backup script

I created a Backup class because I couldn't find one that worked well on Windows and remotely, to do this I have combined a lot of questions on stackoverflow with alota lot of trial and error to get it to work on these two platforms. I'm fairly new to OOP, and I really just want to get some tips on handling passing error messages, and I feel like I'm returning a lot (because some functions shouldn't continue if previous ones have failed), and general thoughts on the class. I have included setup stuff at the top, and some functions the class uses on the bottom (obviously I couldn't include them all). As you may notice the script backs up both user specified tables in a database (and I know I should be using PDO, but ignore that for now) as well as user specified folders and whats cool is the zippy function.

<?php
if (!defined('script_access')) {
    exit('No direct script access allowed');
}



define('APP_PATH', realpath(dirname(__FILE__)) . DIRECTORY_SEPARATOR);                                                      
define('ROOT', realpath(dirname(dirname(dirname(__FILE__)))));
define('CMS_DIR_NAME', 'neou_cms'); 
define('CMS_DIR', ROOT . DIRECTORY_SEPARATOR . CMS_DIR_NAME . DIRECTORY_SEPARATOR);                                                     
define('FRONTEND_IMAGE_UPLOAD_PATH', ROOT . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR);        
define('BACKEND_IMAGE_UPLOAD_PATH', CMS_DIR . 'images' . DIRECTORY_SEPARATOR);              

$backup_core = new Backup();

$backup_core->dir_backup = CMS_DIR . 'backup' . DIRECTORY_SEPARATOR;
$backup_core->dir_sql = $backup_core->dir_backup . 'sql' . DIRECTORY_SEPARATOR;
$backup_core->dir_files = $backup_core->dir_backup . 'files' . DIRECTORY_SEPARATOR;
$backup_core->dir_unpack = ROOT;

$backup_core->folders_to_backup = array(
    FRONTEND_IMAGE_UPLOAD_PATH,
    BACKEND_IMAGE_UPLOAD_PATH . 'users' . DIRECTORY_SEPARATOR
);

class Backup
{
    /************* BACKUP CONFIGURATION ******************/
    // folder locations
    public $dir_backup;
    public $dir_files;
    public $dir_sql;
    // unpack directory, should be at the first writable directory
    public $dir_unpack;
    // array of folders
    public $folders_to_backup;
    public $backup_amt;
    // tables
    public $tables_to_backup = NULL;
    // don't change
    private $sqlDirTemp;
    private $msg;
    private $totalSiteSize;
    /*************** ZIP CONFIGURATION *******************/
    // settings
    public $sqlDirInZip = 'sql_temp'; // sql dir inside zip
    private $overwrite = true; // overwrite file if exists (just incase unlink doesnt work)
    // really imp!
    private $offsetLocal = -1; // local has fagard_designs after doc root remove one directory
    private $offsetRemote = 0; // remote files start in doc root (no offset)
    // files and folders to ignore
    public $ignored_files = array('.DS_STORE');
    public $ignored_folders = array('_notes');
    public $ignored_ext = array('');
    // don't change
    private $zip;
    private $SQLinit = false;
    private $offset;
    
    public function __construct()
    {
        ini_set('upload_max_filesize', '200M');
        
        $this->msg = Messages::getInstance();
        
        $this->totalSiteSize = !Localization::isLocal() ? folderSize(ROOT) : '';
        
        // Note: zip will extract in ROOT and put sql there as well
        $this->sqlDirTemp = ROOT . DIRECTORY_SEPARATOR . $this->sqlDirInZip . DIRECTORY_SEPARATOR;
        
        extension_loaded('zip') ? $this->zip = new ZipArchive() : '';
        
        $this->offset = Localization::isLocal() ? $this->offsetLocal : $this->offsetRemote;
    }
    /*------------------------------------------- BACKUP FN'S ------------------------------------------- */
    /**
     *
     * Backup
     * 
     * Creates SQL, and zip backups
     *
     * @param   string  $filename
     * @param   array   $tables     tables to backup
     * @return          returns     true on success and errors on failure
     *
     */
    private function backupPkg($filename)
    {
        
        $err = '';
        $return = '';

        // get all of the tables
        if (!isset($this->tables_to_backup)) {
            $tables = array();
            $result = Nemesis::query('SHOW TABLES');
            if (!$result) {
                return 'Could not gather table names';
            }
            while ($row = $result->fetch_row()) {
                $tables[] = $row[0];
            }
            // or if the user defines the tables
        } elseif (is_array($this->tables_to_backup)) {
            $tables = explode(',', $this->tables_to_backup);
        } else {
            return 'If you are specifying tables to be backed up, you need to provide function ' . __FUNCTION__ . ' with an array.';
        }
        
        // cycle through each provided table
        foreach ($tables as $table) {
            $result = Nemesis::select("*", $table);
            $result_q = Nemesis::query("SHOW CREATE TABLE {$table}");
            if (!$result && !$result_q) {
                return 'SQL dump was unsuccessful. Could not run queries.';
            }
            $num_fields = $result->field_count;
            // first part of the output - remove the table
            $return .= 'DROP TABLE ' . $table . ';<|||||||>';
            // second part of the output - create table
            $result_q_row = $result_q->fetch_row();
            $return .= "\n\n" . $result_q_row[1] . ";<|||||||>\n\n";
            // third part of the output - insert values into new table
            for ($i = 0; $i < $num_fields; $i++) {
                while ($row = $result->fetch_row()) {
                    $return .= 'INSERT INTO ' . $table . ' VALUES(';
                    for ($j = 0; $j < $num_fields; $j++) {
                        $row[$j] = addslashes($row[$j]);
                        $row[$j] = preg_replace("#\n#", "\\n", $row[$j]);
                        if (isset($row[$j])) {
                            $return .= '"' . $row[$j] . '"';
                        } else {
                            $return .= '""';
                        }
                        if ($j < ($num_fields - 1)) {
                            $return .= ',';
                        }
                    }
                    // add an identifier for explode later
                    $return .= ");<|||||||>\n";
                }
            }
            $return .= "\n\n\n";
        }
        
        if (!is_bool($mkdir = $this->createSaveDir())) {
            return $mkdir;
        }
        $sqlFile = $this->dir_sql . $filename . '.sql';
        $handle = @fopen($sqlFile, 'w+');
        if (!$handle) {
            $err = "Could not open: {$sqlFile}";
        }
        if (empty($err) && @fwrite($handle, $return) === FALSE) {
            $err = "Could write: {$sqlFile}";
        }
        if (empty($err) && $this->zippyConstructor($this->dir_files . $filename . '.zip', $this->folders_to_backup, $sqlFile) == FALSE) {
            $err = "Zip could not complete successfully.";
        }
        @fclose($handle); // we have to close the sql file so we have permission to delete (if required)
        if (!empty($err)) {
            @unlink($this->dir_sql . $filename . '.sql');
            @unlink($this->dir_files . $filename . '.zip');
            return $err;
        } else {
            return true;
        }
    }
    /**
     *
     * Creates necessary dirs for save
     *
     * @returns true on success and errors on failure
     *
     */
    private function createSaveDir()
    {
        
        $dirs = array(
            $this->dir_backup,
            $this->dir_files,
            $this->dir_sql
        );
        
        if (!is_bool($mkdir = mkdirIterator($dirs))) {
            return $mkdir; // errors
        } else {
            return true;
        }
    }
    /**
     *
     * Do Backup
     * 
     * Calls backup
     *
     * @return adds messages
     *
     */
    public function doBackup()
    {
        $filenameFormat = 'dbbackup_' . date(str_replace('-', '.', DATE_FORMAT) . '_H_i_s');
        
        if (!is_bool($output = $this->backupPkg($filenameFormat))) {
            $this->msg->add('e', "Backup failed:<br>{$output}");
        } else {
            $this->msg->add('s', "Backup {$filenameFormat} successfully created.");
        }
    }
    /**
     *
     * Do Restore
     *
     * @param   string  $id         restore id
     * @param   boolean $fromFile   if we are restoring from file this should be true
     *  
     */
    public function doRestore($id, $fromFile = false)
    {
        $err = '';
        
        // remove old folders
        foreach ($this->folders_to_backup as $folder) {
            rmdirRecursive($folder);
        }
        
        if (!$this->zipExtract($this->dir_files . $id . '.zip', $this->dir_unpack)) {
            $err = "An error occurred while extracting backup files from zip for backup {$id}!";
        }
        
        if (empty($err)) {
            if ($fromFile) {
                if (!@copy($this->sqlDirTemp . $id . '.sql', $this->dir_sql . $id . '.sql')) {
                    $err = "Failed to copy SQL file to: {$this->dir_sql}";
                }
            }
            if (empty($err)) {
                $open = $this->dir_sql . $id . '.sql';
                $handle = @fopen($open, "r+");
                if (!$handle) {
                    $err = "Could not open: {$open}";
                }
                $sqlFile = @fread($handle, @filesize($open));
                if (empty($err) && !$sqlFile) {
                    $err = "Could not read: {$open}";
                }
                if (empty($err)) {
                    $sqlArray = explode(';<|||||||>', $sqlFile);
                    // process the sql file by statements
                    foreach ($sqlArray as $stmt) {
                        if (strlen($stmt) > 3) {
                            $result = Nemesis::query($stmt);
                        }
                    }
                }
                @fclose($handle);
                $fromFile ? rmdirRecursive($this->sqlDirTemp) : '';
            }
        }
        
        if (empty($err)) {
            return "Backup {$id} successfully restored.";
        } else {
            return "Backup restore failed:<br>{$err}";
        }
    }
    /**
     *
     * Restores from file upload
     *  Checks file is zip
     *  Moves file to $dir_files directory
     *  Checks zip by extracting to tmp directory
     *  On success, deletes tmp dir, extracts to root
     *
     */
    public function doRestoreFromFile()
    {
        
        $isCorrect = false;
        
        $filename = $_FILES["zip_file"]["name"];
        $source = $_FILES["zip_file"]["tmp_name"];
        $type = $_FILES["zip_file"]["type"];
        $accepted_types = array(
            'application/zip',
            'application/x-zip-compressed',
            'multipart/x-zip',
            'application/x-compressed'
        );
        
        foreach ($accepted_types as $mime_type) {
            if ($mime_type == $type) {
                $isCorrect = true;
                break;
            }
        }
        
        $name = explode(".", $filename);
        $continue = strtolower($name[1]) == 'zip' ? true : false;
        if (!$isCorrect && !$continue && $_FILES['zip_file']['error'] != 1) {
            return 'The file you are trying to upload is not a .zip file. Please try again.';
        }
        
        if (!is_bool($mkdir = $this->createSaveDir())) {
            return $mkdir;
        }
        
        $files = $this->dir_files . $filename;
        // move and begin validating contents for sql file
        if (@move_uploaded_file($source, $files)) {
            
            $filenameNoExt = self::removeExt($filename);
            
            // make a temp dir to extract and test for sql file
            $tmpDir = $this->dir_files . 'temp_' . md5(time()) . DIRECTORY_SEPARATOR;
            @mkdir($tmpDir);
            
            // could not extract
            if (!$this->zipExtract($files, $tmpDir)) {
                return 'An error occurred while extracting files.';
            }
            
            // check for sql file
            $search_sql = $tmpDir . $this->sqlDirInZip . DIRECTORY_SEPARATOR . $filenameNoExt . '.sql';
            if (!@is_file($search_sql) && !@is_readable($search_sql)) {
                // remove bad upload
                @unlink($files);
                // remove temp dir
                rmdirRecursive($tmpDir);
                rmdirRecursive($files);
                return 'There is an error in your file.';
            }
            
            // now the zip file must be valid
            rmdirRecursive($tmpDir);
            if (strpos($output = $this->doRestore($filenameNoExt, true), 'successfully') !== FALSE) {
                return rtrim($output, '.') . ' and uploaded!';
            } else {
                return $output;
            }
            
        } else {
            return 'There was a problem with the upload. Please try again.';
        }
    }
    /**
     *
     * Removes extension from file
     *
     * @param   string  $filename
     * @return  string  $filename without extension
     *
     */
    public static function removeExt($filename)
    {
        return preg_replace('/\\.[^.\\s]{3,4}$/', '', $filename);
    }
    /**
     *
     * Gets extension from file
     *
     * @param   string  $filename
     * @return  string  $filename extension
     *
     */
    public static function getExt($filename)
    {
        return substr(strrchr($filename, '.'), 1);
    }
    /**
     *
     * Maintenance
     *
     * Handles creation (if frontpage = true) in the event that
     * the last backup was over a day old since login and deletion
     * of old backups, as defined by $backup_amt
     *
     * @param   bool    $frontpage  if true then this script can be run on the homepage
     *  
     */
    public function maintenance($frontpage = false)
    {
        if ($frontpage && isAdmin()) {
            // make sure the dirs exists
            if (!is_bool($mkdir = $this->createSaveDir())) {
                return $mkdir;
            }
            // if the dir is empty create a backup
            if (isDirEmpty($this->dir_sql)) {
                $this->doBackup();
                // otherwise, do the normal routine
            } else {
                $path = $this->dir_sql;
                $latest_ctime = 0;
                $latest_filename = '';
                $entry = '';
                $d = dir($path);
                while (false !== ($entry = $d->read())) {
                    $filepath = $path . DIRECTORY_SEPARATOR . $entry;
                    // could do also other checks than just checking whether the entry is a file
                    if (is_file($filepath) && filectime($filepath) > $latest_ctime) {
                        $latest_ctime = filectime($filepath);
                        $latest_filename = $path . $entry;
                    }
                }
                /* check if latest backup is over a day old
                if it is create a new backup and run the regular
                maintenance scripts below */
                if (is_file($latest_filename) && date('d', filectime($latest_filename)) < date('d')) {
                    $this->doBackup();
                }
            }
        }
        $list = listDirDate($this->dir_sql);
        $handle = opendir($this->dir_sql);
        $file = readdir($handle);
        $ct = count($list);
        if (is_numeric($this->backup_amt) && $ct > $this->backup_amt) {
            /* we delete the user specified backup by the backup amt.
            say we have 5, user specified only 2 backups to be kept
            we delete the last 3 backups */
            $list = array_slice($list, 0, $ct - $this->backup_amt);
        foreach ($list as $file => $timestamp) {
            $filenameNoExt = self::removeExt($file);
            @unlink($this->dir_sql . $filenameNoExt . '.sql');
            @unlink($this->dir_files . $filenameNoExt . '.zip');
            $cleanup[] = $filenameNoExt;
        }
        }
        closedir($handle);
        if (!$frontpage && !empty($cleanup)) {
            $passed = implode(', ', $cleanup);
            $message = "Deleted: {$passed}";
            $this->msg->add('w', $message);
        }
    }
    /*------------------------------------------- ZIP ------------------------------------------- */
    /**
     *
     * zippy constructor
     *
     * Called to create zip files
     *
     * @param   string  $destination    where the file goes
     * @param   array   $sources        array
     * @param   string  $SQLfile        if set will include an sql file in the default zip location inside zip
     * @return  boolean                 true or false on success or failure, respectively
     *
     * Note: it is possible to provide sources as $sources = array('ROOT') where ROOT is the root
     * directory of the site
     *
     */
    public function zippyConstructor($destination, $sources = array(), $SQLfile = NULL)
    {
        if (!extension_loaded('zip')) {
            return false;
        }
        if (file_exists($destination)) {
            @unlink($destination);
        }
        if ($this->zip->open($destination, $this->overwrite == true ? ZIPARCHIVE::OVERWRITE : ZIPARCHIVE::CREATE) !== TRUE) {
            return false;
        }
        if (isset($SQLfile) && is_file($SQLfile)) {
            // zip sql file in sql directory
            $this->SQLinit = true;
            $SQLfile = str_replace('\\', DIRECTORY_SEPARATOR, realpath($SQLfile));
            $this->zip->addEmptyDir($this->sqlDirInZip);
            $this->zip->addFromString($this->sqlDirInZip . '/' . basename($SQLfile), file_get_contents($SQLfile));
        }
        foreach ($sources as $source) {
            if (!$this->zippy($source)) {
                $this->zip->close();
                @unlink($destination);
                return false;
            }
        }
        if ($this->SQLinit) {
            if (!$this->verifySQL($destination)) {
                // couldn't verify that sql made it we close dir and remove it, return a false
                $this->zip->close();
                @unlink($destination);
                return false;
            }
        }
        unset($this->SQLinit);
        $this->zip->close();
        return true;
    }
    /**
     *
     * zippy
     *
     * Creates a zip file from a $source directory
     * $include_dir = true means we get the parent directories up until the root directory
     *  otherwise, we just put in the $source dir and its children.
     *
     * @param   string      $source
     * @param   boolean     $include_subs   includes sub directories, limited by offset to root
     * @param   int         $cOffset        custom offset, $include_subs must be false to work
     *                                          default value of -2
     *
     */
    public function zippy($dir, $include_subs = true, $c0ffset = -2)
    {
        if (!extension_loaded('zip')) {
            return false;
        }
        $docRootArr = explode('/', rtrim($_SERVER['DOCUMENT_ROOT'], '/') . '/');
        $docRootCount = count($docRootArr);
        $sOffset = $docRootCount - (1 + $this->offset);
        if ($sOffset < 0) {
            return false;
        }
        $dir = realpath($dir);
        $files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir), RecursiveIteratorIterator::SELF_FIRST);
        foreach ($files as $file) {
            if (is_dir($file)) {
                continue;
            } elseif (is_file($file)) {
                // ignore is in the order of most taxing on the system for efficency
                $filename = basename($file);
                // ignored files
                if (in_array($filename, $this->ignored_files)) {
                    continue;
                    // ignored extensions
                } elseif (in_array(self::getExt($filename), $this->ignored_ext)) {
                    continue;
                    // ignored folders (str_replace removes filename which we filtered first)
                } elseif (self::strposArray(str_replace($filename, '', $file), $this->ignored_folders)) {
                    continue;
                }
                $s1 = str_replace('\\', '/', $file); // normalize slashes
                $s2 = explode('/', $s1); // explode the dir by slashes
                if ($include_subs) {
                    // include directories until root (modified by offset)
                    $s3 = array_slice($s2, $sOffset);
                } elseif (isset($c0ffset)) {
                    // @ -2 just include the file and the directory (go from back of array 2 in (file + dir))
                    $s3 = array_slice($s2, $c0ffset);
                } else {
                    return false;
                }
                $s4 = str_replace('//', '/', implode('/', $s3)); // implode and remove double slashes
                $this->zip->addFromString($s4, file_get_contents($file)); // add file
            }
        }
        return true;
    }
    /**
     *
     * Count Slashes
     *
     * Given a $str will return the amount of slashes in the string
     * We can use this to find the diff (new func needed) between 
     *  /HTML/ and where we are now, and then send that number to minus
     *
     * @param   string  $str
     * @return  int     number of slashes in $str
     *
     */
    public static function countSlashes($str)
    {
        return substr_count($str, '/');
    }
    /**
     *
     * strposArray
     *
     * Array usage of strpos
     * Returns false on first true matching result from the given array
     *
     * @param   string              $haystack
     * @param   string || array     $needle
     * @param   int                 $offset
     * @return  boolean
     *
     */
    public static function strposArray($haystack, $needle, $offset = 0)
    {
        if (!is_array($needle)) {
            $needle = array(
                $needle
            );
        }
        foreach ($needle as $query) {
            if (strpos($haystack, $query, $offset) !== false) {
                return true; // stop on first true result
            }
        }
        return false;
    }
    /**
     *
     * verifySQL
     *
     * Called to check that the $sqlDirInZip exists
     * Returns false on failure to affirm the above
     *
     * @return  boolean
     *
     */
    private function verifySQL()
    {
        // the '/sql' dir exists
        // we want to use '/' instead of directory sep bc windows looks up in zips / instead of \
        if ($this->zip->statName($this->sqlDirInZip . '/') !== false) {
            return true;
        } else {
            return false;
        }
    }
    /**
     *
     * Zip Extract
     *
     * Extracts a ZIP archive to the specified extract path
     *
     * @param   string  $file           The ZIP archive to extract (including the path)    
     * @param   string  $extractPath    The path to extract the ZIP archive to
     * @return  boolean                 True if the ZIP archive is successfully extracted
     *  
     */
    public function zipExtract($file, $extractPath)
    {
        if (!extension_loaded('zip')) {
            return false;
        }
        if ($this->zip->open($file) === TRUE) {
            $this->zip->extractTo($extractPath);
            $this->zip->close();
            return true;
        } else {
            return false;
        }
    }
    /*------------------------------------------- STATS ------------------------------------------- */
    /**
     *
     * Percentage Overflow
     *
     * Child function of findBackupPercentage
     * Determines if the backup dir is too large
     *
     * @return bool true if the backup dir is greater than 25% of the entire site size
     *
     */
    public function percentageOverflow()
    {
        if (!Localization::isLocal()) {
            $stmt = str_replace('%', '', $this->findBackupPercentage());
            // return true if backup folder percentage is higher than 25% (1/4) of the total site
            if ($stmt > 25) {
                return true;
            } else {
                return false;
            }
        } else {
            return 'NaN';
        }
    }
    /**
     *
     * Find Backup Percentage
     *
     * Determines the percentage of the backup dir
     * relative to the entire site size
     *
     * @param   int     $precision (default 2 decimal points)
     * @return          percentage or NaN
     *
     */
    public function findBackupPercentage($precision = 2)
    {
        if (!Localization::isLocal()) {
            $total_backup_folder = folderSize($this->dir_backup);
            $percentage = ($total_backup_folder / $this->total_site_size) * 100;
            return round($percentage, $precision) . '%';
        } else {
            return 'NaN';
        }
    }
    /**
     * Find Backup File Percentage
     *
     * Determines the percentage of a backup file
     * relative to the entire site size
     *
     * @param   int     $precision (default 2 decimal points)
     * @return          percentage or NaN
     *
     */
    public function findBackupFilePercentage($filename, $precision = 2)
    {
        if (!Localization::isLocal()) {
            $size_files = filesize($this->dir_files . $filename . '.zip');
            $size_sql = filesize($this->dir_sql . $filename . '.sql');
            $total_file = $size_files + $size_sql;
            $percentage = ($total_file / $this->total_site_size) * 100;
            return round($percentage, $precision) . '%';
        } else {
            return 'NaN';
        }
    }
}
?>

functions from other classes / functions

functions from other classes / functions:

    public static function isLocal()
    {
        $whitelist = array(
            'localhost',
            '127.0.0.1'
        );
        if (in_array($_SERVER['HTTP_HOST'], $whitelist)) {
            return TRUE;
        } else {
            return FALSE;
        }
    }
    
    function rmdirRecursive($dir, $empty = false)
    {
        if (substr($dir, -1) == "/") {
            $dir = substr($dir, 0, -1);
        }
        if (!file_exists($dir) || !is_dir($dir)) {
            return false;
        } elseif (!is_readable($dir)) {
            return false;
        } else {
            $handle = opendir($dir);
            while ($contents = readdir($handle)) {
                if ($contents != '.' && $contents != '..') {
                    $path = $dir . "/" . $contents;
                    if (is_dir($path)) {
                        rmdirRecursive($path);
                    } else {
                        unlink($path);
                    }
                }
            }
            closedir($handle);
            if ($empty == false) {
                if (!rmdir($dir)) {
                    return false;
                }
            }
            return true;
        }
    }
    
    function folderSize($dir)
    {
        if (is_dir($dir)) {
            $total_size = 0;
            $files = scandir($dir);
            $cleanDir = rtrim($dir, '/') . '/';
            foreach ($files as $t) {
                if ($t <> "." && $t <> "..") {
                    $currentFile = $cleanDir . $t;
                    if (is_dir($currentFile)) {
                        $size = foldersize($currentFile);
                        $total_size += $size;
                    } else {
                        $size = filesize($currentFile);
                        $total_size += $size;
                    }
                }
            }
            return $total_size;
        } else {
            return false;
        }
    }

    function mkdirIterator($dirs)
    {
        $err = array();
        if (is_array($dirs)) {
            foreach ($dirs as $dir) {
                if (!is_dir($dir)) {
                    if (!mkdir($dir)) {
                        $err[] .= "Directory '{$dir}' could not be created.";
                    }
                }
            }
            // check to see if errors occured in the making of more than one directory
            if (count($err) > 1) {
                $err[] .= "More than one directory could not be made. Please check if you have permission to make folders on the server";
                return implode('<br>', $err);
            // we must have only one error then...
            } elseif (!empty($err)) {
                return $err[0];
            } else {
                return true;
            }
        } elseif (isset($dirs)) {
            if (!is_dir($dirs)) {
                if (!mkdir($dirs)) {
                    return "Directory {$dir} could not be created.";
                }
            }
        }
    }

php database and files backup script

I created a Backup class because I couldn't find one that worked well on Windows and remotely, to do this I have combined a lot of questions on stackoverflow with alot of trial and error to get it to work on these two platforms. I'm fairly new to OOP, and I really just want to get some tips on handling passing error messages, and I feel like I'm returning a lot (because some functions shouldn't continue if previous ones have failed), and general thoughts on the class. I have included setup stuff at the top, and some functions the class uses on the bottom (obviously I couldn't include them all). As you may notice the script backs up both user specified tables in a database (and I know I should be using PDO, but ignore that for now) as well as user specified folders and whats cool is the zippy function.

<?php
if (!defined('script_access')) {
    exit('No direct script access allowed');
}



define('APP_PATH', realpath(dirname(__FILE__)) . DIRECTORY_SEPARATOR);                                                      
define('ROOT', realpath(dirname(dirname(dirname(__FILE__)))));
define('CMS_DIR_NAME', 'neou_cms'); 
define('CMS_DIR', ROOT . DIRECTORY_SEPARATOR . CMS_DIR_NAME . DIRECTORY_SEPARATOR);                                                     
define('FRONTEND_IMAGE_UPLOAD_PATH', ROOT . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR);        
define('BACKEND_IMAGE_UPLOAD_PATH', CMS_DIR . 'images' . DIRECTORY_SEPARATOR);              

$backup_core = new Backup();

$backup_core->dir_backup = CMS_DIR . 'backup' . DIRECTORY_SEPARATOR;
$backup_core->dir_sql = $backup_core->dir_backup . 'sql' . DIRECTORY_SEPARATOR;
$backup_core->dir_files = $backup_core->dir_backup . 'files' . DIRECTORY_SEPARATOR;
$backup_core->dir_unpack = ROOT;

$backup_core->folders_to_backup = array(
    FRONTEND_IMAGE_UPLOAD_PATH,
    BACKEND_IMAGE_UPLOAD_PATH . 'users' . DIRECTORY_SEPARATOR
);

class Backup
{
    /************* BACKUP CONFIGURATION ******************/
    // folder locations
    public $dir_backup;
    public $dir_files;
    public $dir_sql;
    // unpack directory, should be at the first writable directory
    public $dir_unpack;
    // array of folders
    public $folders_to_backup;
    public $backup_amt;
    // tables
    public $tables_to_backup = NULL;
    // don't change
    private $sqlDirTemp;
    private $msg;
    private $totalSiteSize;
    /*************** ZIP CONFIGURATION *******************/
    // settings
    public $sqlDirInZip = 'sql_temp'; // sql dir inside zip
    private $overwrite = true; // overwrite file if exists (just incase unlink doesnt work)
    // really imp!
    private $offsetLocal = -1; // local has fagard_designs after doc root remove one directory
    private $offsetRemote = 0; // remote files start in doc root (no offset)
    // files and folders to ignore
    public $ignored_files = array('.DS_STORE');
    public $ignored_folders = array('_notes');
    public $ignored_ext = array('');
    // don't change
    private $zip;
    private $SQLinit = false;
    private $offset;
    
    public function __construct()
    {
        ini_set('upload_max_filesize', '200M');
        
        $this->msg = Messages::getInstance();
        
        $this->totalSiteSize = !Localization::isLocal() ? folderSize(ROOT) : '';
        
        // Note: zip will extract in ROOT and put sql there as well
        $this->sqlDirTemp = ROOT . DIRECTORY_SEPARATOR . $this->sqlDirInZip . DIRECTORY_SEPARATOR;
        
        extension_loaded('zip') ? $this->zip = new ZipArchive() : '';
        
        $this->offset = Localization::isLocal() ? $this->offsetLocal : $this->offsetRemote;
    }
    /*------------------------------------------- BACKUP FN'S ------------------------------------------- */
    /**
     *
     * Backup
     * 
     * Creates SQL, and zip backups
     *
     * @param   string  $filename
     * @param   array   $tables     tables to backup
     * @return          returns     true on success and errors on failure
     *
     */
    private function backupPkg($filename)
    {
        
        $err = '';
        $return = '';

        // get all of the tables
        if (!isset($this->tables_to_backup)) {
            $tables = array();
            $result = Nemesis::query('SHOW TABLES');
            if (!$result) {
                return 'Could not gather table names';
            }
            while ($row = $result->fetch_row()) {
                $tables[] = $row[0];
            }
            // or if the user defines the tables
        } elseif (is_array($this->tables_to_backup)) {
            $tables = explode(',', $this->tables_to_backup);
        } else {
            return 'If you are specifying tables to be backed up, you need to provide function ' . __FUNCTION__ . ' with an array.';
        }
        
        // cycle through each provided table
        foreach ($tables as $table) {
            $result = Nemesis::select("*", $table);
            $result_q = Nemesis::query("SHOW CREATE TABLE {$table}");
            if (!$result && !$result_q) {
                return 'SQL dump was unsuccessful. Could not run queries.';
            }
            $num_fields = $result->field_count;
            // first part of the output - remove the table
            $return .= 'DROP TABLE ' . $table . ';<|||||||>';
            // second part of the output - create table
            $result_q_row = $result_q->fetch_row();
            $return .= "\n\n" . $result_q_row[1] . ";<|||||||>\n\n";
            // third part of the output - insert values into new table
            for ($i = 0; $i < $num_fields; $i++) {
                while ($row = $result->fetch_row()) {
                    $return .= 'INSERT INTO ' . $table . ' VALUES(';
                    for ($j = 0; $j < $num_fields; $j++) {
                        $row[$j] = addslashes($row[$j]);
                        $row[$j] = preg_replace("#\n#", "\\n", $row[$j]);
                        if (isset($row[$j])) {
                            $return .= '"' . $row[$j] . '"';
                        } else {
                            $return .= '""';
                        }
                        if ($j < ($num_fields - 1)) {
                            $return .= ',';
                        }
                    }
                    // add an identifier for explode later
                    $return .= ");<|||||||>\n";
                }
            }
            $return .= "\n\n\n";
        }
        
        if (!is_bool($mkdir = $this->createSaveDir())) {
            return $mkdir;
        }
        $sqlFile = $this->dir_sql . $filename . '.sql';
        $handle = @fopen($sqlFile, 'w+');
        if (!$handle) {
            $err = "Could not open: {$sqlFile}";
        }
        if (empty($err) && @fwrite($handle, $return) === FALSE) {
            $err = "Could write: {$sqlFile}";
        }
        if (empty($err) && $this->zippyConstructor($this->dir_files . $filename . '.zip', $this->folders_to_backup, $sqlFile) == FALSE) {
            $err = "Zip could not complete successfully.";
        }
        @fclose($handle); // we have to close the sql file so we have permission to delete (if required)
        if (!empty($err)) {
            @unlink($this->dir_sql . $filename . '.sql');
            @unlink($this->dir_files . $filename . '.zip');
            return $err;
        } else {
            return true;
        }
    }
    /**
     *
     * Creates necessary dirs for save
     *
     * @returns true on success and errors on failure
     *
     */
    private function createSaveDir()
    {
        
        $dirs = array(
            $this->dir_backup,
            $this->dir_files,
            $this->dir_sql
        );
        
        if (!is_bool($mkdir = mkdirIterator($dirs))) {
            return $mkdir; // errors
        } else {
            return true;
        }
    }
    /**
     *
     * Do Backup
     * 
     * Calls backup
     *
     * @return adds messages
     *
     */
    public function doBackup()
    {
        $filenameFormat = 'dbbackup_' . date(str_replace('-', '.', DATE_FORMAT) . '_H_i_s');
        
        if (!is_bool($output = $this->backupPkg($filenameFormat))) {
            $this->msg->add('e', "Backup failed:<br>{$output}");
        } else {
            $this->msg->add('s', "Backup {$filenameFormat} successfully created.");
        }
    }
    /**
     *
     * Do Restore
     *
     * @param   string  $id         restore id
     * @param   boolean $fromFile   if we are restoring from file this should be true
     *  
     */
    public function doRestore($id, $fromFile = false)
    {
        $err = '';
        
        // remove old folders
        foreach ($this->folders_to_backup as $folder) {
            rmdirRecursive($folder);
        }
        
        if (!$this->zipExtract($this->dir_files . $id . '.zip', $this->dir_unpack)) {
            $err = "An error occurred while extracting backup files from zip for backup {$id}!";
        }
        
        if (empty($err)) {
            if ($fromFile) {
                if (!@copy($this->sqlDirTemp . $id . '.sql', $this->dir_sql . $id . '.sql')) {
                    $err = "Failed to copy SQL file to: {$this->dir_sql}";
                }
            }
            if (empty($err)) {
                $open = $this->dir_sql . $id . '.sql';
                $handle = @fopen($open, "r+");
                if (!$handle) {
                    $err = "Could not open: {$open}";
                }
                $sqlFile = @fread($handle, @filesize($open));
                if (empty($err) && !$sqlFile) {
                    $err = "Could not read: {$open}";
                }
                if (empty($err)) {
                    $sqlArray = explode(';<|||||||>', $sqlFile);
                    // process the sql file by statements
                    foreach ($sqlArray as $stmt) {
                        if (strlen($stmt) > 3) {
                            $result = Nemesis::query($stmt);
                        }
                    }
                }
                @fclose($handle);
                $fromFile ? rmdirRecursive($this->sqlDirTemp) : '';
            }
        }
        
        if (empty($err)) {
            return "Backup {$id} successfully restored.";
        } else {
            return "Backup restore failed:<br>{$err}";
        }
    }
    /**
     *
     * Restores from file upload
     *  Checks file is zip
     *  Moves file to $dir_files directory
     *  Checks zip by extracting to tmp directory
     *  On success, deletes tmp dir, extracts to root
     *
     */
    public function doRestoreFromFile()
    {
        
        $isCorrect = false;
        
        $filename = $_FILES["zip_file"]["name"];
        $source = $_FILES["zip_file"]["tmp_name"];
        $type = $_FILES["zip_file"]["type"];
        $accepted_types = array(
            'application/zip',
            'application/x-zip-compressed',
            'multipart/x-zip',
            'application/x-compressed'
        );
        
        foreach ($accepted_types as $mime_type) {
            if ($mime_type == $type) {
                $isCorrect = true;
                break;
            }
        }
        
        $name = explode(".", $filename);
        $continue = strtolower($name[1]) == 'zip' ? true : false;
        if (!$isCorrect && !$continue && $_FILES['zip_file']['error'] != 1) {
            return 'The file you are trying to upload is not a .zip file. Please try again.';
        }
        
        if (!is_bool($mkdir = $this->createSaveDir())) {
            return $mkdir;
        }
        
        $files = $this->dir_files . $filename;
        // move and begin validating contents for sql file
        if (@move_uploaded_file($source, $files)) {
            
            $filenameNoExt = self::removeExt($filename);
            
            // make a temp dir to extract and test for sql file
            $tmpDir = $this->dir_files . 'temp_' . md5(time()) . DIRECTORY_SEPARATOR;
            @mkdir($tmpDir);
            
            // could not extract
            if (!$this->zipExtract($files, $tmpDir)) {
                return 'An error occurred while extracting files.';
            }
            
            // check for sql file
            $search_sql = $tmpDir . $this->sqlDirInZip . DIRECTORY_SEPARATOR . $filenameNoExt . '.sql';
            if (!@is_file($search_sql) && !@is_readable($search_sql)) {
                // remove bad upload
                @unlink($files);
                // remove temp dir
                rmdirRecursive($tmpDir);
                rmdirRecursive($files);
                return 'There is an error in your file.';
            }
            
            // now the zip file must be valid
            rmdirRecursive($tmpDir);
            if (strpos($output = $this->doRestore($filenameNoExt, true), 'successfully') !== FALSE) {
                return rtrim($output, '.') . ' and uploaded!';
            } else {
                return $output;
            }
            
        } else {
            return 'There was a problem with the upload. Please try again.';
        }
    }
    /**
     *
     * Removes extension from file
     *
     * @param   string  $filename
     * @return  string  $filename without extension
     *
     */
    public static function removeExt($filename)
    {
        return preg_replace('/\\.[^.\\s]{3,4}$/', '', $filename);
    }
    /**
     *
     * Gets extension from file
     *
     * @param   string  $filename
     * @return  string  $filename extension
     *
     */
    public static function getExt($filename)
    {
        return substr(strrchr($filename, '.'), 1);
    }
    /**
     *
     * Maintenance
     *
     * Handles creation (if frontpage = true) in the event that
     * the last backup was over a day old since login and deletion
     * of old backups, as defined by $backup_amt
     *
     * @param   bool    $frontpage  if true then this script can be run on the homepage
     *  
     */
    public function maintenance($frontpage = false)
    {
        if ($frontpage && isAdmin()) {
            // make sure the dirs exists
            if (!is_bool($mkdir = $this->createSaveDir())) {
                return $mkdir;
            }
            // if the dir is empty create a backup
            if (isDirEmpty($this->dir_sql)) {
                $this->doBackup();
                // otherwise, do the normal routine
            } else {
                $path = $this->dir_sql;
                $latest_ctime = 0;
                $latest_filename = '';
                $entry = '';
                $d = dir($path);
                while (false !== ($entry = $d->read())) {
                    $filepath = $path . DIRECTORY_SEPARATOR . $entry;
                    // could do also other checks than just checking whether the entry is a file
                    if (is_file($filepath) && filectime($filepath) > $latest_ctime) {
                        $latest_ctime = filectime($filepath);
                        $latest_filename = $path . $entry;
                    }
                }
                /* check if latest backup is over a day old
                if it is create a new backup and run the regular
                maintenance scripts below */
                if (is_file($latest_filename) && date('d', filectime($latest_filename)) < date('d')) {
                    $this->doBackup();
                }
            }
        }
        $list = listDirDate($this->dir_sql);
        $handle = opendir($this->dir_sql);
        $file = readdir($handle);
        $ct = count($list);
        if (is_numeric($this->backup_amt) && $ct > $this->backup_amt) {
            /* we delete the user specified backup by the backup amt.
            say we have 5, user specified only 2 backups to be kept
            we delete the last 3 backups */
            $list = array_slice($list, 0, $ct - $this->backup_amt);
        foreach ($list as $file => $timestamp) {
            $filenameNoExt = self::removeExt($file);
            @unlink($this->dir_sql . $filenameNoExt . '.sql');
            @unlink($this->dir_files . $filenameNoExt . '.zip');
            $cleanup[] = $filenameNoExt;
        }
        }
        closedir($handle);
        if (!$frontpage && !empty($cleanup)) {
            $passed = implode(', ', $cleanup);
            $message = "Deleted: {$passed}";
            $this->msg->add('w', $message);
        }
    }
    /*------------------------------------------- ZIP ------------------------------------------- */
    /**
     *
     * zippy constructor
     *
     * Called to create zip files
     *
     * @param   string  $destination    where the file goes
     * @param   array   $sources        array
     * @param   string  $SQLfile        if set will include an sql file in the default zip location inside zip
     * @return  boolean                 true or false on success or failure, respectively
     *
     * Note: it is possible to provide sources as $sources = array('ROOT') where ROOT is the root
     * directory of the site
     *
     */
    public function zippyConstructor($destination, $sources = array(), $SQLfile = NULL)
    {
        if (!extension_loaded('zip')) {
            return false;
        }
        if (file_exists($destination)) {
            @unlink($destination);
        }
        if ($this->zip->open($destination, $this->overwrite == true ? ZIPARCHIVE::OVERWRITE : ZIPARCHIVE::CREATE) !== TRUE) {
            return false;
        }
        if (isset($SQLfile) && is_file($SQLfile)) {
            // zip sql file in sql directory
            $this->SQLinit = true;
            $SQLfile = str_replace('\\', DIRECTORY_SEPARATOR, realpath($SQLfile));
            $this->zip->addEmptyDir($this->sqlDirInZip);
            $this->zip->addFromString($this->sqlDirInZip . '/' . basename($SQLfile), file_get_contents($SQLfile));
        }
        foreach ($sources as $source) {
            if (!$this->zippy($source)) {
                $this->zip->close();
                @unlink($destination);
                return false;
            }
        }
        if ($this->SQLinit) {
            if (!$this->verifySQL($destination)) {
                // couldn't verify that sql made it we close dir and remove it, return a false
                $this->zip->close();
                @unlink($destination);
                return false;
            }
        }
        unset($this->SQLinit);
        $this->zip->close();
        return true;
    }
    /**
     *
     * zippy
     *
     * Creates a zip file from a $source directory
     * $include_dir = true means we get the parent directories up until the root directory
     *  otherwise, we just put in the $source dir and its children.
     *
     * @param   string      $source
     * @param   boolean     $include_subs   includes sub directories, limited by offset to root
     * @param   int         $cOffset        custom offset, $include_subs must be false to work
     *                                          default value of -2
     *
     */
    public function zippy($dir, $include_subs = true, $c0ffset = -2)
    {
        if (!extension_loaded('zip')) {
            return false;
        }
        $docRootArr = explode('/', rtrim($_SERVER['DOCUMENT_ROOT'], '/') . '/');
        $docRootCount = count($docRootArr);
        $sOffset = $docRootCount - (1 + $this->offset);
        if ($sOffset < 0) {
            return false;
        }
        $dir = realpath($dir);
        $files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir), RecursiveIteratorIterator::SELF_FIRST);
        foreach ($files as $file) {
            if (is_dir($file)) {
                continue;
            } elseif (is_file($file)) {
                // ignore is in the order of most taxing on the system for efficency
                $filename = basename($file);
                // ignored files
                if (in_array($filename, $this->ignored_files)) {
                    continue;
                    // ignored extensions
                } elseif (in_array(self::getExt($filename), $this->ignored_ext)) {
                    continue;
                    // ignored folders (str_replace removes filename which we filtered first)
                } elseif (self::strposArray(str_replace($filename, '', $file), $this->ignored_folders)) {
                    continue;
                }
                $s1 = str_replace('\\', '/', $file); // normalize slashes
                $s2 = explode('/', $s1); // explode the dir by slashes
                if ($include_subs) {
                    // include directories until root (modified by offset)
                    $s3 = array_slice($s2, $sOffset);
                } elseif (isset($c0ffset)) {
                    // @ -2 just include the file and the directory (go from back of array 2 in (file + dir))
                    $s3 = array_slice($s2, $c0ffset);
                } else {
                    return false;
                }
                $s4 = str_replace('//', '/', implode('/', $s3)); // implode and remove double slashes
                $this->zip->addFromString($s4, file_get_contents($file)); // add file
            }
        }
        return true;
    }
    /**
     *
     * Count Slashes
     *
     * Given a $str will return the amount of slashes in the string
     * We can use this to find the diff (new func needed) between 
     *  /HTML/ and where we are now, and then send that number to minus
     *
     * @param   string  $str
     * @return  int     number of slashes in $str
     *
     */
    public static function countSlashes($str)
    {
        return substr_count($str, '/');
    }
    /**
     *
     * strposArray
     *
     * Array usage of strpos
     * Returns false on first true matching result from the given array
     *
     * @param   string              $haystack
     * @param   string || array     $needle
     * @param   int                 $offset
     * @return  boolean
     *
     */
    public static function strposArray($haystack, $needle, $offset = 0)
    {
        if (!is_array($needle)) {
            $needle = array(
                $needle
            );
        }
        foreach ($needle as $query) {
            if (strpos($haystack, $query, $offset) !== false) {
                return true; // stop on first true result
            }
        }
        return false;
    }
    /**
     *
     * verifySQL
     *
     * Called to check that the $sqlDirInZip exists
     * Returns false on failure to affirm the above
     *
     * @return  boolean
     *
     */
    private function verifySQL()
    {
        // the '/sql' dir exists
        // we want to use '/' instead of directory sep bc windows looks up in zips / instead of \
        if ($this->zip->statName($this->sqlDirInZip . '/') !== false) {
            return true;
        } else {
            return false;
        }
    }
    /**
     *
     * Zip Extract
     *
     * Extracts a ZIP archive to the specified extract path
     *
     * @param   string  $file           The ZIP archive to extract (including the path)    
     * @param   string  $extractPath    The path to extract the ZIP archive to
     * @return  boolean                 True if the ZIP archive is successfully extracted
     *  
     */
    public function zipExtract($file, $extractPath)
    {
        if (!extension_loaded('zip')) {
            return false;
        }
        if ($this->zip->open($file) === TRUE) {
            $this->zip->extractTo($extractPath);
            $this->zip->close();
            return true;
        } else {
            return false;
        }
    }
    /*------------------------------------------- STATS ------------------------------------------- */
    /**
     *
     * Percentage Overflow
     *
     * Child function of findBackupPercentage
     * Determines if the backup dir is too large
     *
     * @return bool true if the backup dir is greater than 25% of the entire site size
     *
     */
    public function percentageOverflow()
    {
        if (!Localization::isLocal()) {
            $stmt = str_replace('%', '', $this->findBackupPercentage());
            // return true if backup folder percentage is higher than 25% (1/4) of the total site
            if ($stmt > 25) {
                return true;
            } else {
                return false;
            }
        } else {
            return 'NaN';
        }
    }
    /**
     *
     * Find Backup Percentage
     *
     * Determines the percentage of the backup dir
     * relative to the entire site size
     *
     * @param   int     $precision (default 2 decimal points)
     * @return          percentage or NaN
     *
     */
    public function findBackupPercentage($precision = 2)
    {
        if (!Localization::isLocal()) {
            $total_backup_folder = folderSize($this->dir_backup);
            $percentage = ($total_backup_folder / $this->total_site_size) * 100;
            return round($percentage, $precision) . '%';
        } else {
            return 'NaN';
        }
    }
    /**
     * Find Backup File Percentage
     *
     * Determines the percentage of a backup file
     * relative to the entire site size
     *
     * @param   int     $precision (default 2 decimal points)
     * @return          percentage or NaN
     *
     */
    public function findBackupFilePercentage($filename, $precision = 2)
    {
        if (!Localization::isLocal()) {
            $size_files = filesize($this->dir_files . $filename . '.zip');
            $size_sql = filesize($this->dir_sql . $filename . '.sql');
            $total_file = $size_files + $size_sql;
            $percentage = ($total_file / $this->total_site_size) * 100;
            return round($percentage, $precision) . '%';
        } else {
            return 'NaN';
        }
    }
}
?>

functions from other classes / functions

    public static function isLocal()
    {
        $whitelist = array(
            'localhost',
            '127.0.0.1'
        );
        if (in_array($_SERVER['HTTP_HOST'], $whitelist)) {
            return TRUE;
        } else {
            return FALSE;
        }
    }
    
    function rmdirRecursive($dir, $empty = false)
    {
        if (substr($dir, -1) == "/") {
            $dir = substr($dir, 0, -1);
        }
        if (!file_exists($dir) || !is_dir($dir)) {
            return false;
        } elseif (!is_readable($dir)) {
            return false;
        } else {
            $handle = opendir($dir);
            while ($contents = readdir($handle)) {
                if ($contents != '.' && $contents != '..') {
                    $path = $dir . "/" . $contents;
                    if (is_dir($path)) {
                        rmdirRecursive($path);
                    } else {
                        unlink($path);
                    }
                }
            }
            closedir($handle);
            if ($empty == false) {
                if (!rmdir($dir)) {
                    return false;
                }
            }
            return true;
        }
    }
    
    function folderSize($dir)
    {
        if (is_dir($dir)) {
            $total_size = 0;
            $files = scandir($dir);
            $cleanDir = rtrim($dir, '/') . '/';
            foreach ($files as $t) {
                if ($t <> "." && $t <> "..") {
                    $currentFile = $cleanDir . $t;
                    if (is_dir($currentFile)) {
                        $size = foldersize($currentFile);
                        $total_size += $size;
                    } else {
                        $size = filesize($currentFile);
                        $total_size += $size;
                    }
                }
            }
            return $total_size;
        } else {
            return false;
        }
    }

    function mkdirIterator($dirs)
    {
        $err = array();
        if (is_array($dirs)) {
            foreach ($dirs as $dir) {
                if (!is_dir($dir)) {
                    if (!mkdir($dir)) {
                        $err[] .= "Directory '{$dir}' could not be created.";
                    }
                }
            }
            // check to see if errors occured in the making of more than one directory
            if (count($err) > 1) {
                $err[] .= "More than one directory could not be made. Please check if you have permission to make folders on the server";
                return implode('<br>', $err);
            // we must have only one error then...
            } elseif (!empty($err)) {
                return $err[0];
            } else {
                return true;
            }
        } elseif (isset($dirs)) {
            if (!is_dir($dirs)) {
                if (!mkdir($dirs)) {
                    return "Directory {$dir} could not be created.";
                }
            }
        }
    }

Database and file backup script

I created a Backup class because I couldn't find one that worked well on Windows and remotely, to do this I have combined a lot of questions on stackoverflow with a lot of trial and error to get it to work on these two platforms. I'm fairly new to OOP, and I really just want to get some tips on handling passing error messages, and I feel like I'm returning a lot (because some functions shouldn't continue if previous ones have failed), and general thoughts on the class. I have included setup stuff at the top, and some functions the class uses on the bottom (obviously I couldn't include them all). As you may notice the script backs up both user specified tables in a database (and I know I should be using PDO, but ignore that for now) as well as user specified folders and whats cool is the zippy function.

<?php
if (!defined('script_access')) {
    exit('No direct script access allowed');
}



define('APP_PATH', realpath(dirname(__FILE__)) . DIRECTORY_SEPARATOR);                                                      
define('ROOT', realpath(dirname(dirname(dirname(__FILE__)))));
define('CMS_DIR_NAME', 'neou_cms'); 
define('CMS_DIR', ROOT . DIRECTORY_SEPARATOR . CMS_DIR_NAME . DIRECTORY_SEPARATOR);                                                     
define('FRONTEND_IMAGE_UPLOAD_PATH', ROOT . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR);        
define('BACKEND_IMAGE_UPLOAD_PATH', CMS_DIR . 'images' . DIRECTORY_SEPARATOR);              

$backup_core = new Backup();

$backup_core->dir_backup = CMS_DIR . 'backup' . DIRECTORY_SEPARATOR;
$backup_core->dir_sql = $backup_core->dir_backup . 'sql' . DIRECTORY_SEPARATOR;
$backup_core->dir_files = $backup_core->dir_backup . 'files' . DIRECTORY_SEPARATOR;
$backup_core->dir_unpack = ROOT;

$backup_core->folders_to_backup = array(
    FRONTEND_IMAGE_UPLOAD_PATH,
    BACKEND_IMAGE_UPLOAD_PATH . 'users' . DIRECTORY_SEPARATOR
);

class Backup
{
    /************* BACKUP CONFIGURATION ******************/
    // folder locations
    public $dir_backup;
    public $dir_files;
    public $dir_sql;
    // unpack directory, should be at the first writable directory
    public $dir_unpack;
    // array of folders
    public $folders_to_backup;
    public $backup_amt;
    // tables
    public $tables_to_backup = NULL;
    // don't change
    private $sqlDirTemp;
    private $msg;
    private $totalSiteSize;
    /*************** ZIP CONFIGURATION *******************/
    // settings
    public $sqlDirInZip = 'sql_temp'; // sql dir inside zip
    private $overwrite = true; // overwrite file if exists (just incase unlink doesnt work)
    // really imp!
    private $offsetLocal = -1; // local has fagard_designs after doc root remove one directory
    private $offsetRemote = 0; // remote files start in doc root (no offset)
    // files and folders to ignore
    public $ignored_files = array('.DS_STORE');
    public $ignored_folders = array('_notes');
    public $ignored_ext = array('');
    // don't change
    private $zip;
    private $SQLinit = false;
    private $offset;
    
    public function __construct()
    {
        ini_set('upload_max_filesize', '200M');
        
        $this->msg = Messages::getInstance();
        
        $this->totalSiteSize = !Localization::isLocal() ? folderSize(ROOT) : '';
        
        // Note: zip will extract in ROOT and put sql there as well
        $this->sqlDirTemp = ROOT . DIRECTORY_SEPARATOR . $this->sqlDirInZip . DIRECTORY_SEPARATOR;
        
        extension_loaded('zip') ? $this->zip = new ZipArchive() : '';
        
        $this->offset = Localization::isLocal() ? $this->offsetLocal : $this->offsetRemote;
    }
    /*------------------------------------------- BACKUP FN'S ------------------------------------------- */
    /**
     *
     * Backup
     * 
     * Creates SQL, and zip backups
     *
     * @param   string  $filename
     * @param   array   $tables     tables to backup
     * @return          returns     true on success and errors on failure
     *
     */
    private function backupPkg($filename)
    {
        
        $err = '';
        $return = '';

        // get all of the tables
        if (!isset($this->tables_to_backup)) {
            $tables = array();
            $result = Nemesis::query('SHOW TABLES');
            if (!$result) {
                return 'Could not gather table names';
            }
            while ($row = $result->fetch_row()) {
                $tables[] = $row[0];
            }
            // or if the user defines the tables
        } elseif (is_array($this->tables_to_backup)) {
            $tables = explode(',', $this->tables_to_backup);
        } else {
            return 'If you are specifying tables to be backed up, you need to provide function ' . __FUNCTION__ . ' with an array.';
        }
        
        // cycle through each provided table
        foreach ($tables as $table) {
            $result = Nemesis::select("*", $table);
            $result_q = Nemesis::query("SHOW CREATE TABLE {$table}");
            if (!$result && !$result_q) {
                return 'SQL dump was unsuccessful. Could not run queries.';
            }
            $num_fields = $result->field_count;
            // first part of the output - remove the table
            $return .= 'DROP TABLE ' . $table . ';<|||||||>';
            // second part of the output - create table
            $result_q_row = $result_q->fetch_row();
            $return .= "\n\n" . $result_q_row[1] . ";<|||||||>\n\n";
            // third part of the output - insert values into new table
            for ($i = 0; $i < $num_fields; $i++) {
                while ($row = $result->fetch_row()) {
                    $return .= 'INSERT INTO ' . $table . ' VALUES(';
                    for ($j = 0; $j < $num_fields; $j++) {
                        $row[$j] = addslashes($row[$j]);
                        $row[$j] = preg_replace("#\n#", "\\n", $row[$j]);
                        if (isset($row[$j])) {
                            $return .= '"' . $row[$j] . '"';
                        } else {
                            $return .= '""';
                        }
                        if ($j < ($num_fields - 1)) {
                            $return .= ',';
                        }
                    }
                    // add an identifier for explode later
                    $return .= ");<|||||||>\n";
                }
            }
            $return .= "\n\n\n";
        }
        
        if (!is_bool($mkdir = $this->createSaveDir())) {
            return $mkdir;
        }
        $sqlFile = $this->dir_sql . $filename . '.sql';
        $handle = @fopen($sqlFile, 'w+');
        if (!$handle) {
            $err = "Could not open: {$sqlFile}";
        }
        if (empty($err) && @fwrite($handle, $return) === FALSE) {
            $err = "Could write: {$sqlFile}";
        }
        if (empty($err) && $this->zippyConstructor($this->dir_files . $filename . '.zip', $this->folders_to_backup, $sqlFile) == FALSE) {
            $err = "Zip could not complete successfully.";
        }
        @fclose($handle); // we have to close the sql file so we have permission to delete (if required)
        if (!empty($err)) {
            @unlink($this->dir_sql . $filename . '.sql');
            @unlink($this->dir_files . $filename . '.zip');
            return $err;
        } else {
            return true;
        }
    }
    /**
     *
     * Creates necessary dirs for save
     *
     * @returns true on success and errors on failure
     *
     */
    private function createSaveDir()
    {
        
        $dirs = array(
            $this->dir_backup,
            $this->dir_files,
            $this->dir_sql
        );
        
        if (!is_bool($mkdir = mkdirIterator($dirs))) {
            return $mkdir; // errors
        } else {
            return true;
        }
    }
    /**
     *
     * Do Backup
     * 
     * Calls backup
     *
     * @return adds messages
     *
     */
    public function doBackup()
    {
        $filenameFormat = 'dbbackup_' . date(str_replace('-', '.', DATE_FORMAT) . '_H_i_s');
        
        if (!is_bool($output = $this->backupPkg($filenameFormat))) {
            $this->msg->add('e', "Backup failed:<br>{$output}");
        } else {
            $this->msg->add('s', "Backup {$filenameFormat} successfully created.");
        }
    }
    /**
     *
     * Do Restore
     *
     * @param   string  $id         restore id
     * @param   boolean $fromFile   if we are restoring from file this should be true
     *  
     */
    public function doRestore($id, $fromFile = false)
    {
        $err = '';
        
        // remove old folders
        foreach ($this->folders_to_backup as $folder) {
            rmdirRecursive($folder);
        }
        
        if (!$this->zipExtract($this->dir_files . $id . '.zip', $this->dir_unpack)) {
            $err = "An error occurred while extracting backup files from zip for backup {$id}!";
        }
        
        if (empty($err)) {
            if ($fromFile) {
                if (!@copy($this->sqlDirTemp . $id . '.sql', $this->dir_sql . $id . '.sql')) {
                    $err = "Failed to copy SQL file to: {$this->dir_sql}";
                }
            }
            if (empty($err)) {
                $open = $this->dir_sql . $id . '.sql';
                $handle = @fopen($open, "r+");
                if (!$handle) {
                    $err = "Could not open: {$open}";
                }
                $sqlFile = @fread($handle, @filesize($open));
                if (empty($err) && !$sqlFile) {
                    $err = "Could not read: {$open}";
                }
                if (empty($err)) {
                    $sqlArray = explode(';<|||||||>', $sqlFile);
                    // process the sql file by statements
                    foreach ($sqlArray as $stmt) {
                        if (strlen($stmt) > 3) {
                            $result = Nemesis::query($stmt);
                        }
                    }
                }
                @fclose($handle);
                $fromFile ? rmdirRecursive($this->sqlDirTemp) : '';
            }
        }
        
        if (empty($err)) {
            return "Backup {$id} successfully restored.";
        } else {
            return "Backup restore failed:<br>{$err}";
        }
    }
    /**
     *
     * Restores from file upload
     *  Checks file is zip
     *  Moves file to $dir_files directory
     *  Checks zip by extracting to tmp directory
     *  On success, deletes tmp dir, extracts to root
     *
     */
    public function doRestoreFromFile()
    {
        
        $isCorrect = false;
        
        $filename = $_FILES["zip_file"]["name"];
        $source = $_FILES["zip_file"]["tmp_name"];
        $type = $_FILES["zip_file"]["type"];
        $accepted_types = array(
            'application/zip',
            'application/x-zip-compressed',
            'multipart/x-zip',
            'application/x-compressed'
        );
        
        foreach ($accepted_types as $mime_type) {
            if ($mime_type == $type) {
                $isCorrect = true;
                break;
            }
        }
        
        $name = explode(".", $filename);
        $continue = strtolower($name[1]) == 'zip' ? true : false;
        if (!$isCorrect && !$continue && $_FILES['zip_file']['error'] != 1) {
            return 'The file you are trying to upload is not a .zip file. Please try again.';
        }
        
        if (!is_bool($mkdir = $this->createSaveDir())) {
            return $mkdir;
        }
        
        $files = $this->dir_files . $filename;
        // move and begin validating contents for sql file
        if (@move_uploaded_file($source, $files)) {
            
            $filenameNoExt = self::removeExt($filename);
            
            // make a temp dir to extract and test for sql file
            $tmpDir = $this->dir_files . 'temp_' . md5(time()) . DIRECTORY_SEPARATOR;
            @mkdir($tmpDir);
            
            // could not extract
            if (!$this->zipExtract($files, $tmpDir)) {
                return 'An error occurred while extracting files.';
            }
            
            // check for sql file
            $search_sql = $tmpDir . $this->sqlDirInZip . DIRECTORY_SEPARATOR . $filenameNoExt . '.sql';
            if (!@is_file($search_sql) && !@is_readable($search_sql)) {
                // remove bad upload
                @unlink($files);
                // remove temp dir
                rmdirRecursive($tmpDir);
                rmdirRecursive($files);
                return 'There is an error in your file.';
            }
            
            // now the zip file must be valid
            rmdirRecursive($tmpDir);
            if (strpos($output = $this->doRestore($filenameNoExt, true), 'successfully') !== FALSE) {
                return rtrim($output, '.') . ' and uploaded!';
            } else {
                return $output;
            }
            
        } else {
            return 'There was a problem with the upload. Please try again.';
        }
    }
    /**
     *
     * Removes extension from file
     *
     * @param   string  $filename
     * @return  string  $filename without extension
     *
     */
    public static function removeExt($filename)
    {
        return preg_replace('/\\.[^.\\s]{3,4}$/', '', $filename);
    }
    /**
     *
     * Gets extension from file
     *
     * @param   string  $filename
     * @return  string  $filename extension
     *
     */
    public static function getExt($filename)
    {
        return substr(strrchr($filename, '.'), 1);
    }
    /**
     *
     * Maintenance
     *
     * Handles creation (if frontpage = true) in the event that
     * the last backup was over a day old since login and deletion
     * of old backups, as defined by $backup_amt
     *
     * @param   bool    $frontpage  if true then this script can be run on the homepage
     *  
     */
    public function maintenance($frontpage = false)
    {
        if ($frontpage && isAdmin()) {
            // make sure the dirs exists
            if (!is_bool($mkdir = $this->createSaveDir())) {
                return $mkdir;
            }
            // if the dir is empty create a backup
            if (isDirEmpty($this->dir_sql)) {
                $this->doBackup();
                // otherwise, do the normal routine
            } else {
                $path = $this->dir_sql;
                $latest_ctime = 0;
                $latest_filename = '';
                $entry = '';
                $d = dir($path);
                while (false !== ($entry = $d->read())) {
                    $filepath = $path . DIRECTORY_SEPARATOR . $entry;
                    // could do also other checks than just checking whether the entry is a file
                    if (is_file($filepath) && filectime($filepath) > $latest_ctime) {
                        $latest_ctime = filectime($filepath);
                        $latest_filename = $path . $entry;
                    }
                }
                /* check if latest backup is over a day old
                if it is create a new backup and run the regular
                maintenance scripts below */
                if (is_file($latest_filename) && date('d', filectime($latest_filename)) < date('d')) {
                    $this->doBackup();
                }
            }
        }
        $list = listDirDate($this->dir_sql);
        $handle = opendir($this->dir_sql);
        $file = readdir($handle);
        $ct = count($list);
        if (is_numeric($this->backup_amt) && $ct > $this->backup_amt) {
            /* we delete the user specified backup by the backup amt.
            say we have 5, user specified only 2 backups to be kept
            we delete the last 3 backups */
            $list = array_slice($list, 0, $ct - $this->backup_amt);
        foreach ($list as $file => $timestamp) {
            $filenameNoExt = self::removeExt($file);
            @unlink($this->dir_sql . $filenameNoExt . '.sql');
            @unlink($this->dir_files . $filenameNoExt . '.zip');
            $cleanup[] = $filenameNoExt;
        }
        }
        closedir($handle);
        if (!$frontpage && !empty($cleanup)) {
            $passed = implode(', ', $cleanup);
            $message = "Deleted: {$passed}";
            $this->msg->add('w', $message);
        }
    }
    /*------------------------------------------- ZIP ------------------------------------------- */
    /**
     *
     * zippy constructor
     *
     * Called to create zip files
     *
     * @param   string  $destination    where the file goes
     * @param   array   $sources        array
     * @param   string  $SQLfile        if set will include an sql file in the default zip location inside zip
     * @return  boolean                 true or false on success or failure, respectively
     *
     * Note: it is possible to provide sources as $sources = array('ROOT') where ROOT is the root
     * directory of the site
     *
     */
    public function zippyConstructor($destination, $sources = array(), $SQLfile = NULL)
    {
        if (!extension_loaded('zip')) {
            return false;
        }
        if (file_exists($destination)) {
            @unlink($destination);
        }
        if ($this->zip->open($destination, $this->overwrite == true ? ZIPARCHIVE::OVERWRITE : ZIPARCHIVE::CREATE) !== TRUE) {
            return false;
        }
        if (isset($SQLfile) && is_file($SQLfile)) {
            // zip sql file in sql directory
            $this->SQLinit = true;
            $SQLfile = str_replace('\\', DIRECTORY_SEPARATOR, realpath($SQLfile));
            $this->zip->addEmptyDir($this->sqlDirInZip);
            $this->zip->addFromString($this->sqlDirInZip . '/' . basename($SQLfile), file_get_contents($SQLfile));
        }
        foreach ($sources as $source) {
            if (!$this->zippy($source)) {
                $this->zip->close();
                @unlink($destination);
                return false;
            }
        }
        if ($this->SQLinit) {
            if (!$this->verifySQL($destination)) {
                // couldn't verify that sql made it we close dir and remove it, return a false
                $this->zip->close();
                @unlink($destination);
                return false;
            }
        }
        unset($this->SQLinit);
        $this->zip->close();
        return true;
    }
    /**
     *
     * zippy
     *
     * Creates a zip file from a $source directory
     * $include_dir = true means we get the parent directories up until the root directory
     *  otherwise, we just put in the $source dir and its children.
     *
     * @param   string      $source
     * @param   boolean     $include_subs   includes sub directories, limited by offset to root
     * @param   int         $cOffset        custom offset, $include_subs must be false to work
     *                                          default value of -2
     *
     */
    public function zippy($dir, $include_subs = true, $c0ffset = -2)
    {
        if (!extension_loaded('zip')) {
            return false;
        }
        $docRootArr = explode('/', rtrim($_SERVER['DOCUMENT_ROOT'], '/') . '/');
        $docRootCount = count($docRootArr);
        $sOffset = $docRootCount - (1 + $this->offset);
        if ($sOffset < 0) {
            return false;
        }
        $dir = realpath($dir);
        $files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir), RecursiveIteratorIterator::SELF_FIRST);
        foreach ($files as $file) {
            if (is_dir($file)) {
                continue;
            } elseif (is_file($file)) {
                // ignore is in the order of most taxing on the system for efficency
                $filename = basename($file);
                // ignored files
                if (in_array($filename, $this->ignored_files)) {
                    continue;
                    // ignored extensions
                } elseif (in_array(self::getExt($filename), $this->ignored_ext)) {
                    continue;
                    // ignored folders (str_replace removes filename which we filtered first)
                } elseif (self::strposArray(str_replace($filename, '', $file), $this->ignored_folders)) {
                    continue;
                }
                $s1 = str_replace('\\', '/', $file); // normalize slashes
                $s2 = explode('/', $s1); // explode the dir by slashes
                if ($include_subs) {
                    // include directories until root (modified by offset)
                    $s3 = array_slice($s2, $sOffset);
                } elseif (isset($c0ffset)) {
                    // @ -2 just include the file and the directory (go from back of array 2 in (file + dir))
                    $s3 = array_slice($s2, $c0ffset);
                } else {
                    return false;
                }
                $s4 = str_replace('//', '/', implode('/', $s3)); // implode and remove double slashes
                $this->zip->addFromString($s4, file_get_contents($file)); // add file
            }
        }
        return true;
    }
    /**
     *
     * Count Slashes
     *
     * Given a $str will return the amount of slashes in the string
     * We can use this to find the diff (new func needed) between 
     *  /HTML/ and where we are now, and then send that number to minus
     *
     * @param   string  $str
     * @return  int     number of slashes in $str
     *
     */
    public static function countSlashes($str)
    {
        return substr_count($str, '/');
    }
    /**
     *
     * strposArray
     *
     * Array usage of strpos
     * Returns false on first true matching result from the given array
     *
     * @param   string              $haystack
     * @param   string || array     $needle
     * @param   int                 $offset
     * @return  boolean
     *
     */
    public static function strposArray($haystack, $needle, $offset = 0)
    {
        if (!is_array($needle)) {
            $needle = array(
                $needle
            );
        }
        foreach ($needle as $query) {
            if (strpos($haystack, $query, $offset) !== false) {
                return true; // stop on first true result
            }
        }
        return false;
    }
    /**
     *
     * verifySQL
     *
     * Called to check that the $sqlDirInZip exists
     * Returns false on failure to affirm the above
     *
     * @return  boolean
     *
     */
    private function verifySQL()
    {
        // the '/sql' dir exists
        // we want to use '/' instead of directory sep bc windows looks up in zips / instead of \
        if ($this->zip->statName($this->sqlDirInZip . '/') !== false) {
            return true;
        } else {
            return false;
        }
    }
    /**
     *
     * Zip Extract
     *
     * Extracts a ZIP archive to the specified extract path
     *
     * @param   string  $file           The ZIP archive to extract (including the path)    
     * @param   string  $extractPath    The path to extract the ZIP archive to
     * @return  boolean                 True if the ZIP archive is successfully extracted
     *  
     */
    public function zipExtract($file, $extractPath)
    {
        if (!extension_loaded('zip')) {
            return false;
        }
        if ($this->zip->open($file) === TRUE) {
            $this->zip->extractTo($extractPath);
            $this->zip->close();
            return true;
        } else {
            return false;
        }
    }
    /*------------------------------------------- STATS ------------------------------------------- */
    /**
     *
     * Percentage Overflow
     *
     * Child function of findBackupPercentage
     * Determines if the backup dir is too large
     *
     * @return bool true if the backup dir is greater than 25% of the entire site size
     *
     */
    public function percentageOverflow()
    {
        if (!Localization::isLocal()) {
            $stmt = str_replace('%', '', $this->findBackupPercentage());
            // return true if backup folder percentage is higher than 25% (1/4) of the total site
            if ($stmt > 25) {
                return true;
            } else {
                return false;
            }
        } else {
            return 'NaN';
        }
    }
    /**
     *
     * Find Backup Percentage
     *
     * Determines the percentage of the backup dir
     * relative to the entire site size
     *
     * @param   int     $precision (default 2 decimal points)
     * @return          percentage or NaN
     *
     */
    public function findBackupPercentage($precision = 2)
    {
        if (!Localization::isLocal()) {
            $total_backup_folder = folderSize($this->dir_backup);
            $percentage = ($total_backup_folder / $this->total_site_size) * 100;
            return round($percentage, $precision) . '%';
        } else {
            return 'NaN';
        }
    }
    /**
     * Find Backup File Percentage
     *
     * Determines the percentage of a backup file
     * relative to the entire site size
     *
     * @param   int     $precision (default 2 decimal points)
     * @return          percentage or NaN
     *
     */
    public function findBackupFilePercentage($filename, $precision = 2)
    {
        if (!Localization::isLocal()) {
            $size_files = filesize($this->dir_files . $filename . '.zip');
            $size_sql = filesize($this->dir_sql . $filename . '.sql');
            $total_file = $size_files + $size_sql;
            $percentage = ($total_file / $this->total_site_size) * 100;
            return round($percentage, $precision) . '%';
        } else {
            return 'NaN';
        }
    }
}
?>

functions from other classes / functions:

    public static function isLocal()
    {
        $whitelist = array(
            'localhost',
            '127.0.0.1'
        );
        if (in_array($_SERVER['HTTP_HOST'], $whitelist)) {
            return TRUE;
        } else {
            return FALSE;
        }
    }
    
    function rmdirRecursive($dir, $empty = false)
    {
        if (substr($dir, -1) == "/") {
            $dir = substr($dir, 0, -1);
        }
        if (!file_exists($dir) || !is_dir($dir)) {
            return false;
        } elseif (!is_readable($dir)) {
            return false;
        } else {
            $handle = opendir($dir);
            while ($contents = readdir($handle)) {
                if ($contents != '.' && $contents != '..') {
                    $path = $dir . "/" . $contents;
                    if (is_dir($path)) {
                        rmdirRecursive($path);
                    } else {
                        unlink($path);
                    }
                }
            }
            closedir($handle);
            if ($empty == false) {
                if (!rmdir($dir)) {
                    return false;
                }
            }
            return true;
        }
    }
    
    function folderSize($dir)
    {
        if (is_dir($dir)) {
            $total_size = 0;
            $files = scandir($dir);
            $cleanDir = rtrim($dir, '/') . '/';
            foreach ($files as $t) {
                if ($t <> "." && $t <> "..") {
                    $currentFile = $cleanDir . $t;
                    if (is_dir($currentFile)) {
                        $size = foldersize($currentFile);
                        $total_size += $size;
                    } else {
                        $size = filesize($currentFile);
                        $total_size += $size;
                    }
                }
            }
            return $total_size;
        } else {
            return false;
        }
    }

    function mkdirIterator($dirs)
    {
        $err = array();
        if (is_array($dirs)) {
            foreach ($dirs as $dir) {
                if (!is_dir($dir)) {
                    if (!mkdir($dir)) {
                        $err[] .= "Directory '{$dir}' could not be created.";
                    }
                }
            }
            // check to see if errors occured in the making of more than one directory
            if (count($err) > 1) {
                $err[] .= "More than one directory could not be made. Please check if you have permission to make folders on the server";
                return implode('<br>', $err);
            // we must have only one error then...
            } elseif (!empty($err)) {
                return $err[0];
            } else {
                return true;
            }
        } elseif (isset($dirs)) {
            if (!is_dir($dirs)) {
                if (!mkdir($dirs)) {
                    return "Directory {$dir} could not be created.";
                }
            }
        }
    }
deleted 30 characters in body
Source Link
Alex
  • 223
  • 1
  • 6
Loading
Source Link
Alex
  • 223
  • 1
  • 6
Loading