HEX
Server: Microsoft-IIS/8.5
System: Windows NT YDAWBH120 6.3 build 9600 (Windows Server 2012 R2 Standard Edition) AMD64
User: tentjecom_web (0)
PHP: 7.4.14
Disabled: NONE
Upload Files
File: D:/HostingSpaces/Eurotools/euro-tools.nl/kommaterug.php
<?php
/**
 * Class KommaTerug
 *
 * Backup script for any website
 */
Class KommaTerug
{
    /** Terminal colors */
    const COLOR_BLACK = '0;30';
    const COLOR_BLUE = '0;34';
    const COLOR_GREEN = '0;32';
    const COLOR_CYAN = '0;36';
    const COLOR_RED = '0;31';
    const COLOR_PURPLE = '0;35';
    const COLOR_BROWN = '0;33';
    const COLOR_LIGHT_GRAY = '0;37';
    const COLOR_DARK_GRAY = '1;30';
    const COLOR_LIGHT_BLUE = '1;34';
    const COLOR_LIGHT_GREEN = '1;32';
    const COLOR_LIGHT_CYAN = '1;36';
    const COLOR_LIGHT_RED = '1;31';
    const COLOR_LIGHT_PURPLE = '1;35';
    const COLOR_YELLOW = '1;33';
    const COLOR_WHITE = '1;37';

    /** Script version number.  */
    const version = 1;

    /** @var string $folder The working folder */
    private static $folder = __DIR__.DIRECTORY_SEPARATOR.'kommaterug' . DIRECTORY_SEPARATOR;

    /** @var string $logFolderName */
    private static $logFolderName = 'logs' . DIRECTORY_SEPARATOR;

    /** @var string $scriptConfigFileName The config file name for the script */
    private static $scriptConfigFileName = 'config.php';

    /** @var string $projectConfigFileName The config file name for the project that you want to backup */
    private $projectConfigFileName;

    /** @var int $maxBackupsSize in bytes */
    private $maxBackupsSize;

    /** @var PDO $PDORead used as a database abstraction layer */
    private $PDORead;

    /** @var PDO $PDORead used as a database abstraction layer */
    private $PDOWrite;

    /** @var string $dbHostRead */
    private $dbHostRead;

    /** @var string $dbPortRead */
    private $dbPortRead;

    /** @var string $dbDatabaseRead */
    private $dbDatabaseRead;

    /** @var string $dbUsernameRead */
    private $dbUsernameRead;

    /** @var string $dbPasswordRead */
    private $dbPasswordRead;

    /** @var string $dbHostWrite */
    private $dbHostWrite;

    /** @var string $dbPortWrite */
    private $dbPortWrite;

    /** @var string $dbDatabaseWrite */
    private $dbDatabaseWrite;

    /** @var string $dbUsernameWrite */
    private $dbUsernameWrite;

    /** @var string $dbPasswordWrite */
    private $dbPasswordWrite;

    /** @var array $config */
    private $config;

    /** @var bool $constructedSuccessfully */
    private $constructedSuccessfully;

    /**
     * KommaTerug constructor.
     */
    public function __construct()
    {
        self::sendHeadersToPreventCaching();

        $this->projectConfigFileName = '.env';
        $this->maxBackupsSize = 3500000000; //Approx 3.5 GB

        if (!self::initializeFilesAndFolders()) {
            $this->constructedSuccessfully = false;
            return false;
        }
        if (!$this->parseScriptConfigFile()) {
            $this->constructedSuccessfully = false;
            return false;
        }

        $this->constructedSuccessfully = true;
    }

    /**
     * KommaTerug destructor
     */
    public function __destruct()
    {
        if(!$this->constructedSuccessfully) return false;

        if ($this->PDORead) {
            $this->PDORead = null;
        }
        if ($this->PDOWrite) {
            $this->PDOWrite = null;
        }

        $this->cleanup();
    }

    /**
     * With this command you run the app. This is where it all starts after the instantiation.
     * Called manually
     */
    public function run()
    {
        if(!$this->constructedSuccessfully) return false;

        self::handleExceptionsIfAny(function () {
            if ($this->runsFromCommandLine()) {
                $this->runFromCommandLine();
            } else {
                $this->runFromServerOrCron();
            }
        });
    }

    /**
     * Runs main program from server or cron job
     */
    private function runFromServerOrCron()
    {
        self::writeOutput('Running the KommaTerug backup program v.'.self::version, self::COLOR_GREEN);
        if (!$this->parseConfigFileFromLaravel($this->projectConfigFileName)) {
            self::writeOutput('Could not parse laravel\'s config file');
            return false;
        }

        self::writeOutput('Lets create a back-up.', self::COLOR_GREEN);
        $backupFolder = $this->createBackupFolderIfNotExists();
        if (!$backupFolder) return false;

        if(!$this->backupDatabase($backupFolder)) return false;
        self::writeOutput('Backup was created successfully!', self::COLOR_GREEN);
        return true;
    }

    /**
     * Sends headers to prevent caching.
     */
    private static function sendHeadersToPreventCaching()
    {
        header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
        header("Cache-Control: post-check=0, pre-check=0", false);
        header("Pragma: no-cache");
    }

    /**
     * Runs main program from command line. Allowing user interaction
     */
    private function runFromCommandLine()
    {
        self::writeOutput('Thanks for using the KommaTerug backup program.', self::COLOR_GREEN);

        $this->setConfigFileNameFromCommandLine();
        if (!$this->parseConfigFileFromLaravel($this->projectConfigFileName)) {
            return false;
        }

        if (!$this->letUserChooseAnAction()) {
            return false;
        }

        return true;
    }

    /**
     * Lets the user choose what to do. Like backing up, restoring etc.
     *
     * @return boolean
     */
    private function letUserChooseAnAction()
    {
        self::writeOutput('What is it what you want to do now? (nothing)', self::COLOR_GREEN);
        self::writeOutput('Choose between: "Backup", "Restore"', self::COLOR_GREEN);
        switch (strtolower(self::readInput())) {
            case 'backup':
                self::writeOutput('Smart choice! Lets create a back-up.', self::COLOR_GREEN);
                $backupFolder = $this->createBackupFolderIfNotExists();
                if (!$backupFolder) return false;

                if(!$this->backupDatabase($backupFolder)) return false;
                self::writeOutput('Backup was created successfully! Now, go do something awesome.', self::COLOR_GREEN);
                return true;
            case 'restore';
                self::writeOutput('Have faith my young Padawan. I am going to help you to restore the database.', self::COLOR_GREEN);
                $this->chooseAndRunDatabaseRestoreStrategy();
                return true;
            default:
                self::writeOutput('You choose to do nothing. Have a nice day.', self::COLOR_GREEN);
                return true;
        }
    }

    /**
     * Creates an SQL file representing a backup from the database
     */
    private function backupDatabase($backupFolder)
    {
        return $this->chooseAndRunDatabaseBackupStrategy($backupFolder);
    }

    /**
     * Creates a backup folder and returns its path.
     * It wil be created in the folder specified in the folder variable
     */
    private function createBackupFolderIfNotExists()
    {
        $name = "backup_".date('d-m-Y_H.i.s');
        $folder = self::$folder.$name;
        if(!self::createFolderIfNotExists($folder)) return false;
        self::writeOutput('Created backup folder: '.$folder, self::COLOR_GREEN);
        return $folder;
    }

    /**
     * Count the amount of backup folders / files
     *
     * @return int
     */
    private static function countBackups()
    {
        $found = scandir(self::$folder);
        $count = 0;
        foreach($found as $value)
        {
            if(strpos($value, 'backup') !== false) $count++;
        }
        return $count;
    }

    /**
     * Let the system choose one method to backup the database
     */
    private function chooseAndRunDatabaseBackupStrategy($backupFolder)
    {
        $mysqldumpExecutable = self::getConfig('mysqldump_executable', 'mysqldump');
        if(self::systemCommandExists($mysqldumpExecutable)) {
            self::writeOutput("Using the mysqldump system command to backup your database...", self::COLOR_GREEN);
            return $this->backupDatabaseUsingMySQLDump($backupFolder);
        } else {
//            if (!$this->initializeMySQLDatabaseConnection()) return false; //TODO implement way to backup via php
        }

        self::writeOutput('Could not backup your database since no suitable backup strategy could be found');
        $this->showMysqlToolLocations();
        return false;
    }

    /**
     * Let the system choose a method to restore the database
     *
     * @return bool
     */
    private function chooseAndRunDatabaseRestoreStrategy()
    {
        $mysqlImportExecutable = self::getConfig('mysqlimport_executable', 'mysqlimport');
        $mysqlExecutable = self::getConfig('mysql_executable', 'mysql');
        if(self::systemCommandExists($mysqlExecutable)) {
            self::writeOutput("Using the mysql system command to restore your database...", self::COLOR_GREEN);
            return $this->restoreDatabaseUsingMySQL();
        }
        else if(self::systemCommandExists($mysqlImportExecutable)) {
            self::writeOutput("Using the mysqlimport system command to restore your database...", self::COLOR_GREEN);
            return $this->restoreDatabaseUsingMySQLImport();
        } else {
//            if (!$this->initializeMySQLDatabaseConnection()) return false; //TODO implement way to restore via php
        }

        self::writeOutput('Could not restore your database since no suitable restore strategy could be found');
        return false;
    }

    /**
     * Determines if a command exists on the current environment
     *
     * @param string $command The command to check
     * @return bool true if the command has been found. false otherwise
     */
    private static function systemCommandExists ($command) {
        $whereIsCommand = (PHP_OS == 'WINNT') ? 'where' : 'which';

        $process = proc_open(
            $whereIsCommand.' '.$command,
            [
                ["pipe", "r"], //STDIN
                ["pipe", "w"], //STDOUT
                ["pipe", "w"]  //STDERR
            ],
            $pipes
        );
        if ($process !== false) {
            $stdout = stream_get_contents($pipes[1]);
            $stderr = stream_get_contents($pipes[2]);
            fclose($pipes[1]);
            fclose($pipes[2]);
            proc_close($process);

            return $stdout != '';
        }

        return false;
    }

    /**
     * executes a system command
     *
     * @param string $command The command to execute
     * @param bool $showCommandBeforeExecuting
     * @return bool true if the command was executed successfully. false otherwise
     */
    private static function executeSystemCommand ($command, $showCommandBeforeExecuting = false) {
        if($showCommandBeforeExecuting) self::writeOutput('Running system command: '.$command);

        $process = proc_open(
            $command,
            [
                ["pipe", "r"], //STDIN
                ["pipe", "w"], //STDOUT
                ["pipe", "w"]  //STDERR
            ],
            $pipes
        );
        if ($process !== false) {
            $stdout = stream_get_contents($pipes[1]);
            $stderr = stream_get_contents($pipes[2]);
            fclose($pipes[1]);
            fclose($pipes[2]);
            proc_close($process);

            if ($stderr && strpos($stderr, 'Warning') === false) {
                self::writeOutput($stderr, self::COLOR_LIGHT_GRAY);
                return false;
            }

            if($stdout) self::writeOutput($stdout, self::COLOR_LIGHT_GRAY);
            return true;
        }

        return false;
    }

    /**
     * Opens a MySQL database connection.
     *
     * @return bool
     */
    private function initializeMySQLDatabaseConnection()
    {
        if(
            !$this->dbHostWrite || !$this->dbDatabaseWrite || !$this->dbUsernameWrite || !$this->dbPasswordWrite ||
            !$this->dbHostRead || !$this->dbDatabaseRead || !$this->dbUsernameRead || !$this->dbPasswordRead
        ) {
            self::writeOutput('Error. please make sure you set your database host, database, username and password in your projects config (not the script config)');
            return false;
        }

        $dsnWriteParts = [
            'host='.$this->dbHostWrite,
            'dbname='.$this->dbDatabaseWrite,
        ];
        if($this->dbPortWrite) $dsnWriteParts[] = 'port='.intval($this->dbPortWrite);
        $dsnWrite = 'mysql:'.implode(';', $dsnWriteParts);

        $dsnReadParts = [
            'host='.$this->dbHostRead,
            'dbname='.$this->dbDatabaseRead,
        ];
        if($this->dbPortRead) $dsnReadParts[] = 'port='.intval($this->dbPortRead);
        $dsnRead = 'mysql:'.implode(';', $dsnReadParts);

        try {
            $this->PDOWrite = new PDO($dsnWrite, $this->dbUsernameWrite, $this->dbPasswordWrite);
        }
        catch (Exception $exception)
        {
            self::writeOutput('Could not connect to MySQL database for writing with the current settings.');
            self::writeOutput($exception->getMessage());
            return false;
        }

        try {
            $this->PDORead = new PDO($dsnRead, $this->dbUsernameRead, $this->dbPasswordRead);
        }
        catch (Exception $exception)
        {
            self::writeOutput('Could not connect to MySQL database for reading with the current settings.');
            self::writeOutput($exception->getMessage());
            return false;
        }

        return true;
    }

    /**
     * Backup database using MySQL dump
     *
     * @return bool
     */
    private function backupDatabaseUsingMySQLDump($folder)
    {
        $mysqldumpExecutable = $this->getConfig('mysqldump_executable', 'mysqldump');
        $commandDisplay = $mysqldumpExecutable.' --databases '.$this->dbDatabaseRead.' > '.$folder.DIRECTORY_SEPARATOR.'database.sql';
        $command = $mysqldumpExecutable.' -u '.$this->dbUsernameRead.' -p'.$this->dbPasswordRead.' --databases '.$this->dbDatabaseRead.' > '.$folder.DIRECTORY_SEPARATOR.'database.sql';
        self::writeOutput('executing mysql command like this (credentials excluded): "'.$commandDisplay.'"', self::COLOR_GREEN);
        self::writeOutput('Backing up database "'.$this->dbDatabaseRead.'". Please standby...', self::COLOR_GREEN);
        if(!self::executeSystemCommand($command)) {
            self::writeOutput('Well...that did not work. I don\'t know what to do next, so I\'ll stop.', self::COLOR_GREEN);
            return false;
        }
        self::writeOutput('Database "'.$this->dbDatabaseRead.'" backupped! It\'s here: '.$folder.DIRECTORY_SEPARATOR.'database.sql', self::COLOR_GREEN);
        return true;
    }

    /**
     * Let the user select a backup
     *
     * @return string The name of the backup relative to the folder
     */
    private function selectBackup()
    {
        $backupNames = $this->listBackups();
        if(count($backupNames) == 0) {
            self::writeOutput("There are no backups. So i will stop.", self::COLOR_GREEN);
            return false;
        }

        self::writeOutput('Type the number of a backup to select it: ', self::COLOR_GREEN);
        $backupNumber = self::readInput();
        if($backupNumber == "") $this->selectBackup();
        if(!in_array($backupNumber, array_keys($backupNames))) {
            self::writeOutput('There is no backup number '.$backupNumber.'.', self::COLOR_GREEN);
            return $this->selectBackup();
        }
        return $backupNames[$backupNumber];
    }

    /**
     * Asks the user a couple of questions to restore a database and then restores it using the mysqlimport programm
     *
     * @return bool true if restored successfully. false if not.
     */
    private function restoreDatabaseUsingMySQLImport()
    {
        $backupName = $this->selectBackup();
        if(!$backupName) return false;

        $mysqlImportExecutable = $this->getConfig('mysqlimport_executable', 'mysqlimport');
        $systemCommand = $mysqlImportExecutable.' -u '.$this->dbUsernameWrite.' -p'.$this->dbPasswordWrite.' '.$this->dbDatabaseWrite.' '.self::$folder.$backupName.DIRECTORY_SEPARATOR.'database.sql';
        $systemCommandDisplay = $mysqlImportExecutable.' '.$this->dbDatabaseWrite.' '.self::$folder.$backupName.DIRECTORY_SEPARATOR.'database.sql';

        self::writeOutput('Restoring database with name: '.$backupName.' into configured database "'.$this->dbDatabaseWrite.'", using command (credentials excluded): ', self::COLOR_GREEN);
        self::writeOutput($systemCommand, self::COLOR_GREEN);

        if(!self::executeSystemCommand($systemCommand)) {
            self::writeOutput('Well...that did not work. I don\'t know what to do next, so I\'ll stop.', self::COLOR_GREEN);
            return false;
        }
        self::writeOutput('Database restored successfully', self::COLOR_GREEN);

        return true;
    }

    /**
     * Asks the user a couple of questions to restore a database and then restores it using the mysql programm
     *
     * @return bool true if restored successfully. false if not.
     */
    private function restoreDatabaseUsingMySQL()
    {
        $backupName = self::selectBackup();
        if(!$backupName) return false;

        $mysqlExecutable = $this->getConfig('mysql_executable', 'mysql');
        $systemCommand = $mysqlExecutable.' -u '.$this->dbUsernameWrite.' -p'.$this->dbPasswordWrite.' '.$this->dbDatabaseWrite.' < '.self::$folder.$backupName.DIRECTORY_SEPARATOR.'database.sql';
        $systemCommandDisplay = $mysqlExecutable.' '.$this->dbDatabaseWrite.'  < '.self::$folder.$backupName.DIRECTORY_SEPARATOR.'database.sql';

        self::writeOutput('Restoring database backup '.$backupName.' into configured database "'.$this->dbDatabaseWrite.'", using command (credentials excluded): ', self::COLOR_GREEN);
        self::writeOutput($systemCommandDisplay, self::COLOR_GREEN);

        if(!self::executeSystemCommand($systemCommand)) {
            self::writeOutput('Well...that did not work. I don\'t know what to do next, so I\'ll stop.', self::COLOR_GREEN);
            return false;
        }
        self::writeOutput('Database restored successfully!', self::COLOR_GREEN);

        return true;
    }

    /**
     * Returns backup folder / file names and shows them in a nicely formatted way so that a user can choose one.
     *
     * @param bool $showToUser
     * @return array
     */
    private function listBackups($showToUser = true)
    {
        $found = scandir(self::$folder, 0);
        $count = 1;

        $backupNames = [];
        foreach($found as $value)
        {
            if(strpos($value, 'backup') !== false) {
                $dirSizeBytes = self::directorySize(self::$folder.DIRECTORY_SEPARATOR.$value);
                $dirSizeInMegaBytes = round($dirSizeBytes / 1024 / 1024, 2);

                $backupNames[$count] = $value;
                if($showToUser) self::writeOutput($count.') '.$backupNames[$count].' ('.$dirSizeInMegaBytes.' MB)', self::COLOR_LIGHT_GRAY);
                $count++;
            }
        }
        return $backupNames;
    }

    /**
     * Returns log file names and shows them in a nicely formatted way so that a user can choose one.
     *
     * @param bool $showToUser
     * @return array
     */
    private function listLogFiles($showToUser = true)
    {
        $found = scandir(self::$folder.self::$logFolderName, 0);
        $count = 1;

        $logNames = [];
        foreach($found as $value)
        {
            if($value == '.' || $value == '..') continue;

            $dirSizeBytes = self::directorySize(self::$folder.self::$logFolderName.DIRECTORY_SEPARATOR.$value);
            $dirSizeInMegaBytes = round($dirSizeBytes / 1024 / 1024, 2);

            $logNames[$count] = $value;
            if($showToUser) self::writeOutput($count.') '.$logNames[$count].' ('.$dirSizeInMegaBytes.' MB)', self::COLOR_LIGHT_GRAY);
            $count++;
        }
        return $logNames;
    }

    /**
     * Initializes files and folders this script needs.
     *
     * Returns true if successful. false if not
     */
    private static function initializeFilesAndFolders()
    {
        if(!self::createFolderIfNotExists(self::$folder)) return false;
        if(!self::createScriptConfigFileIfNotExists(self::$folder.self::$scriptConfigFileName)) return false;
        if(!self::createFolderIfNotExists(self::$folder. self::$logFolderName)) return false;

        return true;
    }

    /**
     * Creates a folder if it does not exist.
     * Returns true if the folder was created or when it exists. false otherwise
     *
     * @param $folder
     * @param $mode
     * @return bool
     */
    private static function createFolderIfNotExists($folder, $mode = 0777)
    {
        if(!file_exists($folder))
        {
            if(!@mkdir($folder, $mode, false)) {
                self::writeOutput('Could not create folder: '.$folder.'');
                return false;
            }
        }

        return true;
    }

    /**
     * Create a config file for the script. Resides inside the folder
     * @param $configFile
     * @return bool
     */
    private static function createScriptConfigFileIfNotExists($configFile)
    {
        if(!file_exists($configFile))
        {
            if(!@touch($configFile, false)) {
                self::writeOutput('Could not create config file: '.$configFile.'');
                return false;
            }

            file_put_contents($configFile, [
                '<?php'.PHP_EOL,
                'return ['.PHP_EOL,
                '    "max_backup_size" => 1000, //Megabytes'.PHP_EOL,
                '    "max_log_size" => 10, //Megabytes for all log files together'.PHP_EOL,
                '    "mysqldump_executable" => "/usr/local/bin/mysqldump", //Path to mysqldump executable. The one for Mamp is /Applications/MAMP/Library/bin/mysqldump'.PHP_EOL,
                '    "mysqlimport_executable" => "/usr/local/bin/mysqlimport", //Path to mysqldump executable. The one for Mamp is /Applications/MAMP/Library/bin/mysqlimport'.PHP_EOL,
                '    "mysql_executable" => "/usr/local/bin/mysql", //Path to mysql executable. The one for Mamp is /Applications/MAMP/Library/bin/mysql'.PHP_EOL,
                '];'.PHP_EOL
            ]);
        }

        return true;
    }

    /**
     * Sets the project config file name by prompting the user for its name or using a default one.
     */
    private function setConfigFileNameFromCommandLine()
    {
        self::writeOutput('What is the name of your projects config file you want to use: ('.$this->projectConfigFileName.'): ', self::COLOR_GREEN);
        self::writeOutput('Notice that this is not the config file from the KommaTerug script.', self::COLOR_GREEN);
        $input = self::readInput();
        if($input != "") $this->projectConfigFileName = $input;
        if(file_exists($this->projectConfigFileName)) {
            self::writeOutput('Ok. We will use the file "'.$this->projectConfigFileName.'" as the config file', self::COLOR_GREEN);
        } else {
            self::writeOutput('The file "'.$this->projectConfigFileName.'" does not exist. Please try again', self::COLOR_RED);
            $this->setConfigFileNameFromCommandLine();
        }
    }

    /**
     * Reads configuration data from the configuration file
     */
    private function parseScriptConfigFile()
    {
        $path = self::$folder.self::$scriptConfigFileName;
        $this->config = require($path);
        return true;
    }

    /**
     * Gets a config value OR the specified default value
     *
     * @param $key
     * @param null|mixed $default A default value to return if the key could not be found in the configuration
     * @return mixed|null
     */
    private function getConfig($key, $default = null)
    {
        $keys = explode('.', $key);
        if($key == $keys)
        {
            //no array. Just return the value or default
            return (isset($this->config[$key])) ? $this->config[$key] : $default;
        } else {
            $currentConfig = $this->config;
            while($keys)
            {
                $currentKey = array_shift($keys);
                if(!isset($currentConfig[$currentKey])) return $default;
                if(count($keys) == 0) return isset($currentConfig[$currentKey]) ? $currentConfig[$currentKey] : $default;
                $currentConfig = $currentConfig[$currentKey];
            }
        }

        return $default;
    }


    /**
     * Reads a laravel environment file to array
     *
     * @param $configFileName
     * @return bool
     */
    private function parseConfigFileFromLaravel($configFileName)
    {
        $configData = file_get_contents(__DIR__.DIRECTORY_SEPARATOR.$configFileName);
        $configDataLines = explode(PHP_EOL, $configData);
        foreach($configDataLines as $line)
        {
            if(trim($line) == "") continue;
            $parts = explode('=', $line);
            $option = $parts[0];
            $value = $parts[1];


            switch (strtolower($option))
            {
                case "db_host";
                    $parts = explode(':', $value);
                    if($parts !== $value) {
                        $this->dbHostRead = $this->dbHostWrite = $parts[0];
                        $this->dbPortRead = $this->dbPortWrite = $parts[1];
                    } else {
                        $this->dbHostRead = $this->dbHostWrite = $value;
                    }
                    break;
                case "db_database";
                    $this->dbDatabaseRead = $this->dbDatabaseWrite = $value;
                    break;
                case "db_port";
                    $this->dbPortRead = $this->dbPortWrite = $value;
                    break;
                case "db_username";
                    $this->dbUsernameRead = $this->dbUsernameWrite = $value;
                    break;
                case "db_password";
                    $this->dbPasswordRead = $this->dbPasswordWrite = $value;
                    break;
            }
        }

        return true;
    }

    /**
     * Read input from the command line
     *
     * @param string $default
     * @return string
     */
    private static function readInput($default = '')
    {
        $line = trim(fgets(STDIN));
        return ($line != "") ? $line : $default;
    }

    /**
     * Colors a string using one of the COLOR_ constants on the top of this class
     *
     * @param $string
     * @param $color
     * @return string
     */
    private static function color($string, $color)
    {
        return "\033[".$color."m".$string."\033[0m";
    }

    /**
     * @param string $string
     * @param bool $isError
     * @param null|int $color One of the COLOR_ constants on the top of this class
     */
    private static function writeOutput($string, $color = null, $isError = false)
    {
        if(self::runsFromCommandLine()) {
            $string = $string.PHP_EOL;
            if($color) $string = self::color($string, $color);

            fwrite(STDOUT, $string);
            if ($isError) {
                fwrite(STDERR, $string);
            }
        }
        else
        {
            echo $string.'<br>';
        }

        self::log($string);
    }

    /**
     * Log string to a file of today
     *
     * @param $string
     * @return bool true on success, false on failure
     */
    private static function log($string)
    {
        $string = self::getTimeLog().$string.PHP_EOL;
        $logFile = self::$folder.self::$logFolderName.self::getTodaysLogFileName();
        if(file_put_contents($logFile, $string, FILE_APPEND) === false) return false;
        return true;
    }

    /**
     * Get today's log file name. Relative to the logs folder
     */
    private static function getTodaysLogFileName()
    {
        $name  = 'log_';
        $name .= date('d-m-Y');
        $name .= '.txt';
        return $name;
    }

    /**
     * Returns true if php is ran from the command line. false if not.
     *
     * @return bool
     */
    private static function runsFromCommandLine()
    {
        if(php_sapi_name() == 'cli') {
            return true;
        }
        else {
            return false;
        }
    }

    /**
     * Dump data
     *
     * @param $data
     * @param bool $verbose
     */
    private static function dd($data, $verbose = false)
    {
        if($verbose) {
            var_dump($data);
        } else {
            print_r($data);
        }
        die(PHP_EOL);
    }

    /**
     * Convert an arguments array to a string
     * @param $arguments
     * @return string
     */
    private static function argumentsArrayToString(...$arguments)
    {
        if(!is_array($arguments) && count($arguments) == 0) return '';
        return implode(',', $arguments);
    }

    /**
     * Returns a time log string. Example. A string like this: "[24-07-2018 10:15:01]"
     */
    private static function getTimeLog()
    {
        $string = date('d/m/Y H:i:s');
        $string = '['.$string.'] ';
        return $string;
    }

    /**
     * Do some cleaning up. Like removing old log files and backups
     */
    private function cleanup()
    {
        self::writeOutput('Cleaning up old stuff', self::COLOR_GREEN);
        $this->deleteOldBackups();
        $this->deleteOldLogs();
        self::writeOutput('Done cleaning up!', self::COLOR_GREEN);

    }

    /**
     * Deletes old backups if the size of the backups exceeds the config's max_backup_size variable
     */
    private function deleteOldBackups()
    {
        $backupsToDelete = $this->findOldBackups();
        foreach($backupsToDelete as $backupName)
        {
            self::writeOutput('Deleting old backup: '.$backupName);
            $backupPath = self::$folder.$backupName;
            if(is_dir($backupPath)) {
                self::deleteDir($backupPath);
            }
        }
    }

    /**
     * Deletes old logs if the size of the logs exceeds the config's max_log_size variable
     */
    private function deleteOldLogs()
    {
        $logsToDelete = $this->findOldLogs();
        foreach($logsToDelete as $logToDelete)
        {
            self::writeOutput('Deleting old log: '.$logToDelete);
            $logPath = self::$folder.self::$logFolderName.$logToDelete;
            if(!unlink($logPath)) {
                self::writeOutput('Could not delete log: '.$logPath);
            }
        }
    }

    /**
     * Finds old backups when the size of the backups in total exceed the config's max_backup_size variable
     */
    private function findOldBackups()
    {
        $maxBackupSizeInBytes = self::getConfig('max_backup_size', 500) * 1024 * 1024;
        $backupNames = array_reverse($this->listBackups(false)); //First one is newest

        $currentSize = 0; //In bytes
        $backupsPathsToKeep = [];
        $backupPathsToDelete = [];
        foreach($backupNames as $index => $backupName)
        {
            $backupPath = self::$folder.$backupName;
            $currentSize += self::directorySize($backupPath);

            if($currentSize < $maxBackupSizeInBytes) {
                $backupsPathsToKeep[] = $backupName;
            } else {
                $backupPathsToDelete[] = $backupName;
            }
        }

        return $backupPathsToDelete;
    }

    /**
     * Finds old backups when the size of the backups in total exceed the config's max_backup_size variable
     */
    private function findOldLogs()
    {
        $maxLogSizeInBytes = self::getConfig('max_log_size', 50) * 1024 * 1024;
        $logNames = array_reverse($this->listLogFiles(false)); //First one is newest

        $currentSize = 0; //In bytes
        $logPathsToKeep = [];
        $logPathsToDelete = [];
        foreach($logNames as $index => $logName)
        {
            $logPath = self::$folder.self::$logFolderName.$logName;
            $currentSize += filesize($logPath);

            if($currentSize < $maxLogSizeInBytes) {
                $logPathsToKeep[] = $logName;
            } else {
                $logPathsToDelete[] = $logName;
            }
        }

        return $logPathsToDelete;
    }

    /**
     * Executes the callback and handles its exceptions
     *
     * @param $callback
     */
    private static function handleExceptionsIfAny($callback)
    {
        try {
            $callback();
        }
        catch (Exception $exception)
        {
            self::writeOutput(self::getTimeLog().'Exception on line: '.$exception->getLine().' "'.$exception->getMessage().'"');
            self::writeOutput('Tracelog: ');
            $trace = $exception->getTrace();
            foreach($trace as $traceLine)
            {
                self::writeOutput($traceLine['file'].':'.$traceLine['line'].'@'.$traceLine['function'].' arguments: '.self::argumentsArrayToString($traceLine['args']));
            }
        }
    }

    /**
     * Deletes a directory with it's contents if any.
     *
     * @param $dirPath
     */
    public static function deleteDir($dirPath) {
        if (! is_dir($dirPath)) {
            throw new InvalidArgumentException("$dirPath must be a directory");
        }
        if (substr($dirPath, strlen($dirPath) - 1, 1) != '/') {
            $dirPath .= '/';
        }
        $files = glob($dirPath . '*', GLOB_MARK);
        foreach ($files as $file) {
            if (is_dir($file)) {
                self::deleteDir($file);
            } else {
                unlink($file);
            }
        }
        rmdir($dirPath);
    }

    /**
     * Show MySQL tool locations
     */
    public function showMysqlToolLocations()
    {
        $whereIsCommand = (PHP_OS == 'WINNT') ? 'where' : 'which';

        self::writeOutput('Trying to show you the mysql tool locations:');
        self::executeSystemCommand($whereIsCommand." mysql", true);
        self::executeSystemCommand($whereIsCommand." mysqlimport", true);
        self::executeSystemCommand($whereIsCommand." mysqldump", true);
        self::executeSystemCommand($whereIsCommand." mysql.exe", true);
        self::executeSystemCommand($whereIsCommand." mysqlimport.exe", true);
        self::executeSystemCommand($whereIsCommand." mysqldump.exe", true);
    }

    /**
     * Returns the folder size in bytes
     *
     * @param $dir
     * @return int
     */
    private static function directorySize($dir)
    {
        $size = 0;
        foreach (glob(rtrim($dir, '/').'/*', GLOB_NOSORT) as $each) {
            $size += is_file($each) ? filesize($each) : self::directorySize($each);
        }
        return $size;
    }
}

(new KommaTerug())->run();