Source of file BackupManager.php

Size: 33,394 Bytes - Last Modified: 2020-10-24T02:46:31+00:00

/home/travis/build/NextDom/nextdom-core/src/Managers/BackupManager.php

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913
<?php
/* This file is part of NextDom Software.
 *
 * NextDom is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * NextDom Software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with NextDom Software. If not, see <http://www.gnu.org/licenses/>.
 */

namespace NextDom\Managers;

use NextDom\Enums\ConfigKey;
use NextDom\Enums\DateFormat;
use NextDom\Enums\FoldersAndFilesReferential;
use NextDom\Enums\LogLevel;
use NextDom\Enums\LogTarget;
use NextDom\Enums\NextDomFolder;
use NextDom\Enums\NextDomObj;
use NextDom\Enums\NextDomFile;
use NextDom\Exceptions\CoreException;
use NextDom\Helpers\ConsoleHelper;
use NextDom\Helpers\DBHelper;
use NextDom\Helpers\FileSystemHelper;
use NextDom\Helpers\LogHelper;
use NextDom\Helpers\MigrationHelper;
use NextDom\Helpers\NextDomHelper;
use NextDom\Helpers\SystemHelper;
use NextDom\Helpers\Utils;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use splitbrain\PHPArchive\Tar;

/**
 * Class BackupManager
 * @package NextDom\Managers
 */
class BackupManager
{
    /**
     * @var null
     */
    private static $logLevel = null;

    /**
     * Creates a backup archive
     *
     * @param bool $background Lancer la sauvegarde en tâche de fond.
     * @throws \Exception
     */
    public static function backup(bool $background = false)
    {
        if (true === $background) {
            LogHelper::clear(LogTarget::BACKUP);
            $script = sprintf("%s/install/backup.php interactive=false  > %s 2>&1 &",
                NEXTDOM_ROOT,
                LogHelper::getPathToLog(LogTarget::BACKUP));
            SystemHelper::php($script);
        } else {
            self::createBackup();
        }
    }

    /**
     * Runs the backup procedure
     *
     * Last output should not be removed since it act as a marker in ajax calls
     *
     * @return bool true if no error
     * @throws CoreException
     * @throws \Exception
     */
    public static function createBackup()
    {
        $backupDir = self::getBackupDirectory();

        if (FileSystemHelper::getDirectoryFreeSpace($backupDir) < 400000000) {
            throw new CoreException('Not Enough space to create local backup');
        }
        self::$logLevel = ConfigManager::byKey(ConfigKey::LOG_LEVEL, 'core', LogLevel::ERROR);
        $backupName = self::getBackupFilename();
        $backupPath = sprintf("%s/%s", $backupDir, $backupName);
        $sqlPath = sprintf("%s/DB_backup.sql", $backupDir);
        $cachePath = CacheManager::getArchivePath();
        $startTime = strtotime('now');
        $status = "success";
        try {
            ConsoleHelper::title("Create Backup Process", false);
            ConsoleHelper::subTitle("starting backup procedure at " . date(DateFormat::FULL));
            NextDomHelper::event('begin_backup', true);
            ConsoleHelper::step("stopping NextDom (cron & scenario)");
            NextDomHelper::stopSystem(true);
            ConsoleHelper::ok();
            ConsoleHelper::step("starting plugin backup");
            self::backupPlugins();
            ConsoleHelper::ok();
            ConsoleHelper::step("checking database integrity");
            self::repairDB();
            ConsoleHelper::ok();
            ConsoleHelper::step("starting database backup");
            self::createDBBackup($sqlPath);
            ConsoleHelper::ok();
            ConsoleHelper::step("starting cache backup");
            CacheManager::persist();
            ConsoleHelper::ok();
            ConsoleHelper::step("creating backup archive");
            ConsoleHelper::enter();
            self::createBackupArchive($backupPath, $sqlPath, $cachePath, LogTarget::BACKUP);
            ConsoleHelper::ok();
            ConsoleHelper::step("rotating backup archives");
            self::rotateBackups($backupDir);
            ConsoleHelper::ok();
            ConsoleHelper::step("checking remote backup systems");
            self::sendRemoteBackup($backupPath);
            ConsoleHelper::ok();
        } catch (\Exception $e) {
            $status = "error";
            ConsoleHelper::nok();
            ConsoleHelper::error($e);
            LogHelper::addError(LogTarget::BACKUP, $e->getMessage());
        } finally {
            ConsoleHelper::step("starting NextDom (cron & scenario)");
            NextDomHelper::startSystem();
            ConsoleHelper::ok();
            NextDomHelper::event('end_backup');
            ConsoleHelper::subTitle("end of backup procedure at " . date(DateFormat::FULL));
            ConsoleHelper::subTitle("elapsed time " . (strtotime('now') - $startTime));
        }

        // the following line acts as marker used in ajax telling that the procedure is finished
        // it should be me removed
        ConsoleHelper::subTitle("Closing with " . $status);
        ConsoleHelper::title("Create Backup Process", true);
        return ($status == "success");
    }

    /**
     * Returns backup directory according to backup::path
     *
     * When backup::path is relative, constructs directory from NEXTDOM_DATA root
     *
     * @throws CoreException if backup directory cannot be created
     * @retrun string backup root directory
     */
    public static function getBackupDirectory()
    {
        $dir = ConfigManager::byKey('backup::path');
        if ('/' != substr($dir, 0, 1)) {
            $dir = sprintf("%s/%s", NEXTDOM_DATA, $dir);
        }
        if (!is_dir($dir) && !mkdir($dir, 0775, true)) {
            throw new CoreException("unable to create backup directory " . $dir);
        }
        return $dir;
    }

    /**
     * Computes backup filename from nextdom's name and current datetime
     *
     * @param string $name current nextdom name, default given by ConfigManager
     * @return string
     * @throws \Exception
     */
    public static function getBackupFilename($name = null): string
    {
        $date = date("Y-m-d-H-i-s");
        $version = NextDomHelper::getNextdomVersion();
        $format = "backup-%s-%s-%s.tar.gz";

        if ($name === null) {
            $name = ConfigManager::byKey('name', 'core', 'NextDom');
        }
        $cleanName = str_replace(['&', '#', "'", '"', '+', '-'], '', $name);
        $cleanName = str_replace(' ', '_', $cleanName);

        return sprintf($format, $cleanName, $version, $date);
    }

    /**
     * Runs backup method for each active plugins if available
     *
     * @throws \Exception
     */
    public static function backupPlugins()
    {
        $plugins = PluginManager::listPlugin(true);
        foreach ($plugins as $c_plugin) {
            $pid = $c_plugin->getId();
            if (true === method_exists($pid, 'backup')) {
                $pid::backup();
            }
        }
    }

    /**
     * Checks and repair database
     *
     * @throws CoreException when mysqlcheck exited with error status
     */
    public static function repairDB()
    {
        global $CONFIG;

        $status = 0;
        $format = "mysqlcheck --host='%s' --port='%d' --user='%s' --password='%s' %s --auto-repair --silent 2>/dev/null";
        $command = sprintf($format,
            $CONFIG['db']['host'],
            $CONFIG['db']['port'],
            $CONFIG['db']['username'],
            $CONFIG['db']['password'],
            $CONFIG['db']['dbname']);
        system($command, $status);
        if ($status != 0) {
            throw new CoreException("error while checking database, exited with status " . $status);
        }
    }

    /**
     * Creates a backup of database to given output file path
     *
     * @param $outputFile
     * @throws CoreException true when mysqldump failed
     */
    public static function createDBBackup($outputFile)
    {
        global $CONFIG;

        $status = 0;
        $format = "mysqldump --host='%s' --port='%s' --user='%s' --password='%s' %s  > %s 2>/dev/null";
        $command = sprintf($format,
            $CONFIG['db']['host'],
            $CONFIG['db']['port'],
            $CONFIG['db']['username'],
            $CONFIG['db']['password'],
            $CONFIG['db']['dbname'],
            $outputFile);

        system($command, $status);
        if ($status != 0) {
            throw new CoreException("error while dumping database, exited with status " . $status);
        }
    }

    /**
     * Creates an archive with all files that should be included in backup
     *
     * We proceed in 4 steps:
     * 1. creates a tar archive with plugins
     * 2. append mysql backup to tar archive
     * 3. append cache to  to tar archive
     * 4. compress tar archive
     *
     * This allows to untie path dependencies between source files while keeping
     * the same directory structure in the tar archive (in order to keep backward
     * compatibility with jeedom archives).
     *
     * @param string $outputPath path of generated backup archive
     * @param string $sqlPath
     * @param string $cachePath
     * @param string $logFile
     *
     * @throws CoreException
     * @throws \splitbrain\PHPArchive\ArchiveCorruptedException
     * @throws \splitbrain\PHPArchive\ArchiveIOException
     * @throws \splitbrain\PHPArchive\ArchiveIllegalCompressionException
     * @throws \splitbrain\PHPArchive\FileInfoException
     * @retrun bool true archive generated successfully
     */
    public static function createBackupArchive(string $outputPath, $sqlPath, $cachePath, $logFile)
    {

        $tar = new Tar();
        $tar->setCompression();
        $tar->create($outputPath);

        // Backup cache and SQL files
        $tar->addFile($cachePath, "var/cache.tar.gz");
        $tar->addFile($sqlPath, "DB_backup.sql");

        // Backup config and data folders
        FileSystemHelper::mkdirIfNotExists(NEXTDOM_DATA . '/data/custom', 0775, true);
        $roots = [NEXTDOM_DATA . '/data/', NEXTDOM_DATA . '/config/'];
        $pattern = NEXTDOM_DATA . '/';
        self::addPathToArchive($roots, $pattern, $tar, $logFile);

        // Backup plugins folder
        $roots = [NEXTDOM_ROOT . '/plugins/'];
        $pattern = NEXTDOM_ROOT . '/';
        self::addPathToArchive($roots, $pattern, $tar, $logFile);

        $dir = new \RecursiveDirectoryIterator(NEXTDOM_ROOT, \FilesystemIterator::SKIP_DOTS);
        // Flatten the recursive iterator, folders come before their files
        $it = new \RecursiveIteratorIterator($dir, \RecursiveIteratorIterator::SELF_FIRST);
        // Maximum depth is 1 level deeper than the base folder
        $it->setMaxDepth(0);


        // Backup all files/folder in root folder added by user
        foreach ($it as $fileInfo) {
            if (($fileInfo->isDir() || $fileInfo->isFile()) && !in_array($fileInfo->getFilename(), FoldersAndFilesReferential::NEXTDOM_ROOT_FOLDERS)
                && !in_array($fileInfo->getFilename(), FoldersAndFilesReferential::NEXTDOM_ROOT_FILES)
                && !is_link($fileInfo->getFilename())) {
                $tar->addFile($fileInfo->getPathname(), $fileInfo->getFilename());
                if ($fileInfo->isDir()) {
                    $roots = [NEXTDOM_ROOT . '/' . $fileInfo->getFilename()];
                    self::addPathToArchive($roots, $pattern, $tar, $logFile);
                }
            }
        }

        $tar->close();
    }

    /**
     * @param array  $roots
     * @param string $pattern
     * @param Tar    $tar
     * @param        $logFile
     * @throws \splitbrain\PHPArchive\ArchiveCorruptedException
     * @throws \splitbrain\PHPArchive\ArchiveIOException
     * @throws \splitbrain\PHPArchive\FileInfoException
     * @throws \Exception
     */
    private static function addPathToArchive($roots, $pattern, $tar, $logFile)
    {
        foreach ($roots as $c_root) {
            $path = $c_root;
            $dirIter = new RecursiveDirectoryIterator($path);
            $riIter = new RecursiveIteratorIterator($dirIter);
            ConsoleHelper::stepLine('Add files of ' . $c_root . ' to archive');
            // iterate on files recursively found
            foreach ($riIter as $c_entry) {
                if (false === $c_entry->isFile()) {
                    continue;
                }
                $message = 'Add folder to archive : ' . $c_entry->getPathname();
                if (self::$logLevel == LogLevel::DEBUG && $logFile === LogTarget::BACKUP) {
                    LogHelper::addDebug($logFile, $message);
                }
                $dest = str_replace($pattern, "", $c_entry->getPathname());
                $tar->addFile($c_entry->getPathname(), $dest);
            }
        }
    }

    /**
     * Removes backup archives according to backup::keepDays and backup::maxSize
     *
     * 1. since files are sorted, we can sum-up bytes until limit is reached
     *    from that point, remove all other files
     *
     * @param string $backupDir backup root directory
     * @throws \Exception
     */
    public static function rotateBackups(string $backupDir)
    {
        $maxDays = ConfigManager::byKey('backup::keepDays');
        $maxSizeInBytes = ConfigManager::byKey('backup::maxSize') * 1024 * 1024;
        $minMtime = time() - ($maxDays * 60 * 60 * 24);
        $totalBytes = 0;
        $files = self::getBackupFileInfo($backupDir, "newest");

        // 1.
        foreach ($files as $c_entry) {
            if (($c_entry["mtime"] < $minMtime) ||
                ($totalBytes > $maxSizeInBytes)) {
                unlink($c_entry["file"]);
                continue;
            }
            $totalBytes += $c_entry["size"];
        }
    }

    /**
     * Creates an array with information about existing backup files
     *
     * Available fields:
     * - file: path to backup archive
     * - mtime: current file modification timestamp
     * - size: file size in bytes
     *
     * 1. get file list from globing
     * 2. generate array of object from file list
     *
     * @param string $backupDir backup root directory
     * @param string $order sort result by 'newest' or 'oldest' first
     * @return array of file object
     * @throws CoreException
     */
    public static function getBackupFileInfo($backupDir, $order = "newest")
    {
        $pattern = sprintf("%s/*.gz", $backupDir);
        // 1.
        $entries = glob($pattern);
        if (false === $entries) {
            return [];
        }

        // 2.
        $array_map = [];
        foreach ($entries as $key => $c_file) {
            $stat = stat($c_file);
            if (false === $stat) {
                throw new CoreException("unable to stat file " . $c_file);
            }
            $array_map[$key] = ["file" => $c_file, "mtime" => $stat[9], "size" => $stat[7]];
        }
        $files = $array_map;

        if ($order == "newest") {
            usort($files, function ($x, $y) {
                return -1 * ($x["mtime"] - $y["mtime"]);
            });
        } else {
            usort($files, function ($x, $y) {
                return ($x["mtime"] - $y["mtime"]);
            });
        }

        return $files;
    }

    /**
     * Trigger remote upload for all available repos
     *
     * @param string $path path to backup archive
     * @return bool
     * @throws \Exception
     * @retrun bool true is everything went fine
     */
    public static function sendRemoteBackup(string $path)
    {
        $repos = UpdateManager::listRepo();
        foreach ($repos as $c_key => $c_val) {
            if (($c_val['scope']['backup'] === false) ||
                (ConfigManager::byKey($c_key . '::enable') == 0) ||
                (ConfigManager::byKey($c_key . '::cloudUpload') == 0)) {
                continue;
            }
            LogHelper::addInfo("system", $c_val['class']);
            try {
                $c_val['class']::backup_send($path);
            } catch (\Exception $e) {
                // Even if we have a samba exception, the backup should be available
            }
        }
        return true;
    }

    /**
     * Restore a backup from file
     *
     * @param string $file Backup file path
     * @param bool $background Start backup task in background
     * @throws \Exception
     */
    public static function restore(string $file = '', bool $background = false)
    {
        if (true === $background) {
            LogHelper::clear("restore");
            $script = sprintf("%s/install/restore.php file=%s interactive=false > %s 2>&1 &",
                NEXTDOM_ROOT,
                $file,
                LogHelper::getPathToLog(LogTarget::RESTORE));
            SystemHelper::php($script);
        } else {
            self::restoreBackup($file);
        }
    }

    /**
     * Runs the restore procedure
     *
     * Last output should not be removed since it act as a marker in ajax calls
     *
     * @param string $file path to backup archive, when empty, use last available backup
     * @return bool false when error occurs
     * @throws CoreException
     */
    public static function restoreBackup($file = '')
    {
        $backupDir = self::getBackupDirectory();
        $startTime = strtotime('now');
        $status = "success";
        $tmpDir = "";

        try {
            ConsoleHelper::title("Restore Backup Process", false);
            ConsoleHelper::subTitle("starting restore procedure at " . date(DateFormat::FULL));
            NextDomHelper::event('begin_restore', true);

            if (($file === null) || ("" === $file)) {
                $file = self::getLastBackupFilePath($backupDir, "newest");
            }
            ConsoleHelper::process("file used for restoration: " . $file);
            ConsoleHelper::ok();
            ConsoleHelper::step("stopping Nextdom system...");
            NextDomHelper::stopSystem(false);
            ConsoleHelper::ok();
            ConsoleHelper::step("extracting backup archive...");
            $tmpDir = self::extractArchive($file);
            ConsoleHelper::ok();
            ConsoleHelper::step("restoring plugins...");
            self::restorePlugins($tmpDir);
            ConsoleHelper::ok();
            ConsoleHelper::step("restoring mysql database...");
            self::restoreDatabase($tmpDir);
            ConsoleHelper::ok();
            ConsoleHelper::step("importing Jeedom configuration...");
            self::restoreJeedomConfig($tmpDir);
            ConsoleHelper::ok();
            ConsoleHelper::step("restoring custom data...\n");
            self::restoreCustomData($tmpDir, LogTarget::RESTORE);
            ConsoleHelper::ok();
            ConsoleHelper::step("migrating data...");
            MigrationHelper::migrate(LogTarget::RESTORE);
            ConsoleHelper::ok();
            ConsoleHelper::step("starting nextdom system...");
            NextDomHelper::startSystem();
            ConsoleHelper::ok();
            ConsoleHelper::step("updating system configuration...");
            self::updateConfig();
            ConsoleHelper::ok();
            ConsoleHelper::step("checking system consistency...");
            ConsistencyManager::checkConsistency();
            ConsoleHelper::ok();
            ConsoleHelper::step("init values...");
            self::initValues();
            ConsoleHelper::ok();
            ConsoleHelper::step("clearing cache...");
            self::clearCache();
            ConsoleHelper::ok();
            ConsoleHelper::step("restoring cache...");
            self::restoreCache($tmpDir,LogTarget::RESTORE);
            ConsoleHelper::ok();
            FileSystemHelper::rrmdir($tmpDir);
            NextDomHelper::event("end_restore");
            ConsoleHelper::subTitle("end of restore procedure at " . date(DateFormat::FULL));
            ConsoleHelper::subTitle("elapsed time " . (strtotime('now') - $startTime));
        } catch (\Exception $e) {
            $status = "error";
            ConsoleHelper::nok();
            ConsoleHelper::error($e);
            LogHelper::addError(LogTarget::RESTORE, $e->getMessage());
            if (true === is_dir($tmpDir)) {
                FileSystemHelper::rrmdir($tmpDir);
            }
            ConsoleHelper::step('starting Nextdom system...');
            NextDomHelper::startSystem();
            ConsoleHelper::ok();
        }
        // the following line acts as marker used in ajax telling that the procedure is finished
        // it should be me removed
        ConsoleHelper::subTitle('Closing with ' . $status);
        ConsoleHelper::title('Restore Backup Process', true);
        return ($status == 'success');
    }

    /**
     * Returns path to last available backup archive
     *
     * @param $backupDir
     * @param string $order sort result by 'newest' or 'oldest' first
     * @return string archive file path
     * @throws CoreException when no archive is found
     */
    public static function getLastBackupFilePath($backupDir, $order = "newest")
    {
        $files = self::getBackupFileInfo($backupDir, $order);

        if (empty($files)) {
            throw new CoreException('unable to find any backup file');
        }
        return $files[0]["file"];
    }

    /**
     * Extracts backup archive to a temporary folder
     *
     * @param string $file path to backup archive
     * @return string path to generated temporary directory
     * @throws CoreException
     * @throws \splitbrain\PHPArchive\ArchiveCorruptedException
     * @throws \splitbrain\PHPArchive\ArchiveIOException
     * @throws \splitbrain\PHPArchive\ArchiveIllegalCompressionException
     * @throw CoreException when error on reading archive or creating temporary dir
     */
    private static function extractArchive($file)
    {
        $excludeDirs = ["AlternativeMarketForJeedom", "musicast"];
        $exclude = sprintf("/^(%s)$/", join("|", $excludeDirs));
        $tmpDir = sprintf("%s-restore-%s", NEXTDOM_TMP, date('Y-m-d-H:i:s'));
        if (false === mkdir($tmpDir, 0775, true)) {
            throw new CoreException("unable to create tmp directory " . $tmpDir);
        }
        if (FileSystemHelper::getDirectoryFreeSpace($tmpDir) < 400000000) {
            throw new CoreException('Not enough space to extract archive');
        }
        $tar = new Tar();
        $tar->open($file);
        $tar->extract($tmpDir, "", $exclude);
        return $tmpDir;
    }

    /**
     * Restore plugins from backup archive
     *
     * @param string $tmpDir extracted backup root directory
     * @throws CoreException
     * @throws \Exception
     */
    private static function restorePlugins($tmpDir)
    {
        $pluginDirs = glob(sprintf("%s/plugins/*", $tmpDir), GLOB_ONLYDIR);
        $pluginRoot = sprintf("%s/plugins", NEXTDOM_ROOT);

        FileSystemHelper::rrmdir($pluginRoot);
        FileSystemHelper::mkdirIfNotExists($pluginRoot, 0775, true);
        foreach ($pluginDirs as $c_dir) {
            if (false === FileSystemHelper::mv($c_dir, $pluginRoot)) {
                // should probably fail, keeping behavior prior to install/restore.php refactoring
            }
        }
        self::restorePublicPerms($pluginRoot);

        $plugins = PluginManager::listPlugin(true);
        foreach ($plugins as $c_plugin) {
            // call plugin restore hook, if any
            $pluginID = $c_plugin->getId();
            if (method_exists($pluginID, 'restore')) {
                $pluginID::restore();
            }
            // reset plugin dependencies
            $cache = CacheManager::byKey('dependancy' . $c_plugin->getId());
            $cache->remove();
            CacheManager::set('dependancy' . $c_plugin->getId(), "nok");
        }
    }

    /**
     * Restore www-data owner and 775 permissions on directory
     *
     * @param $folderRoot
     * @throws CoreException on permission error
     * @throws \Exception
     */
    private static function restorePublicPerms($folderRoot)
    {
        $status = SystemHelper::vsystem("%s chown %s:%s -R %s",
            SystemHelper::getCmdSudo(),
            SystemHelper::getWWWUid(),
            SystemHelper::getWWWGid(),
            $folderRoot);
        if (0 != $status) {
            throw new CoreException("unable to restore filesystem owner on " . $folderRoot);
        }

        SystemHelper::vsystem("%s chmod 775 -R %s",
            SystemHelper::getCmdSudo(),
            $folderRoot);
        if (0 != $status) {
            throw new CoreException("unable to restore filesystem rights" . $folderRoot);
        }
    }

    /**
     * Loads mysql dump from backup archive into database
     *
     * @param string $tmpDir extracted backup root directory
     * @throws CoreException when error occurs
     */
    private static function restoreDatabase($tmpDir)
    {
        $backupFile = sprintf("%s/DB_backup.sql", $tmpDir);

        //Just database comment changes, rest done in migrationHelper
        if (0 != SystemHelper::vsystem("sed -i -e 's/Database: jeedom/Database: nextdom/g' '%s'", $backupFile)) {
            throw new CoreException("unable to modify content of backup file " . $backupFile);
        }
        if (0 != SystemHelper::vsystem("sed -i -e 's/Definer=`jeedom`/Definer=`nextdom`/g' '%s'", $backupFile)) {
            throw new CoreException("unable to modify content of backup file " . $backupFile);
        }
        if (0 != SystemHelper::vsystem("sed -i -e 's/varchar(255) /varchar(191) /g' '%s'", $backupFile)) {
            throw new CoreException("unable to modify varchar(255) to varchar(191) of backup file " . $backupFile);
        }

        DBHelper::exec("SET foreign_key_checks = 0");
        $tables = DBHelper::getAll("SHOW TABLES");
        foreach ($tables as $table) {
            $table = array_values($table);
            $table = $table[0];
            $statement = sprintf("DROP TABLE IF EXISTS `%s`", $table);
            DBHelper::exec($statement);
        }
        self::loadSQLFromFile($backupFile);
        DBHelper::exec("SET foreign_key_checks = 1");
    }

    /**
     * Load given file in mysql database
     *
     * @param string $file path to file to load
     * @throws CoreException
     * @throw CoreException when a mysql error occurs
     */
    public static function loadSQLFromFile($file)
    {
        global $CONFIG;

        $format = "mysql --host='%s' --port='%s' --user='%s' --password='%s' --force %s < %s";
        $status = SystemHelper::vsystem($format,
            $CONFIG['db']['host'],
            $CONFIG['db']['port'],
            $CONFIG['db']['username'],
            $CONFIG['db']['password'],
            $CONFIG['db']['dbname'],
            $file);
        if ($status !== 0) {
            throw new CoreException("error loading sql file " . $file);
        }
    }

    /**
     * Import common config if not already exists
     *
     * @param string $tmpDir extracted backup root directory
     */
    private static function restoreJeedomConfig(string $tmpDir)
    {
        $commonBackup = $tmpDir . '/common.config.php';
        $commonConfig = NEXTDOM_DATA . '/config/common.config.php';
        $jeedomConfig = NEXTDOM_DATA . '/config/jeedom.config.php';

        if (true == file_exists($jeedomConfig)) {
            @unlink($jeedomConfig);
        }
        if (!file_exists($commonConfig) && file_exists($commonBackup)) {
            if (false === FileSystemHelper::mv($commonBackup, $commonConfig)) {
                // should at least warn, silent fail kept from install/restore.php refactoring
            }
        }
    }

    /**
     * Restore custom data from backup archive
     *
     * @param string $tmpDir extracted backup root directory
     * @throws CoreException
     */
    private static function restoreCustomData($tmpDir, $logFile)
    {
        $rootCustomDataDirs = glob(sprintf("%s/*", $tmpDir), GLOB_ONLYDIR);

        foreach ($rootCustomDataDirs as $c_dir) {
            $name = basename($c_dir);
            if (!in_array($name, FoldersAndFilesReferential::NEXTDOM_ROOT_FOLDERS)
                && !in_array($name, FoldersAndFilesReferential::NEXTDOM_ROOT_FILES)
                && !in_array($name, FoldersAndFilesReferential::JEEDOM_BACKUP_FOLDERS)
                && !in_array($name, FoldersAndFilesReferential::JEEDOM_BACKUP_FILES)) {
                $message = 'Restoring folder: ' . $name;
                if ($logFile == LogTarget::MIGRATION) {
                    LogHelper::addInfo($logFile, $message, '');
                } else {
                    ConsoleHelper::process($message);
                }
                if (true === FileSystemHelper::mv($c_dir, sprintf("%s/%s", NEXTDOM_ROOT, $name))) {
                    self::restorePublicPerms(NEXTDOM_ROOT);
                }
                if ($logFile != LogTarget::MIGRATION) {
                    ConsoleHelper::ok();
                }
            }
        }


        $customDataDirs = glob(sprintf("%s/data/*", $tmpDir), GLOB_ONLYDIR);
        $customDataRoot = sprintf("%s/data", NEXTDOM_DATA);

        FileSystemHelper::rrmdir($customDataRoot . "/");
        FileSystemHelper::mkdirIfNotExists($customDataRoot, 0775, true);
        foreach ($customDataDirs as $c_dir) {
            $name = basename($c_dir);
            $message = 'Restoring folder :' . $name;
            if ($logFile == LogTarget::MIGRATION) {
                LogHelper::addInfo($logFile, $message, '');
            } else {
                ConsoleHelper::process($message);
            }
            if (true === FileSystemHelper::mv($c_dir, sprintf("%s/%s", $customDataRoot, $name))) {
                self::restorePublicPerms($customDataRoot);
            }
            if ($logFile != LogTarget::MIGRATION) {
                ConsoleHelper::ok();
            }
        }

        $customPlanDirs = glob(sprintf("%s/core/img/*", $tmpDir), GLOB_ONLYDIR);
        $customPlanRoot = sprintf("%s/data/plan", NEXTDOM_DATA);

        FileSystemHelper::mkdirIfNotExists($customPlanRoot, 0775, true);
        foreach ($customPlanDirs as $c_dir) {
            $name = basename($c_dir);
            if (Utils::startsWith($name, NextDomObj::PLAN)) {
                $message = 'Restoring folder :' . $name;
                if ($logFile == LogTarget::MIGRATION) {
                    LogHelper::addInfo($logFile, $message, '');
                } else {
                    ConsoleHelper::process($message);
                }
                if (true === FileSystemHelper::mv($c_dir, sprintf("%s/%s", $customPlanRoot, $name))) {
                    self::restorePublicPerms($customPlanRoot);
                }
                if ($logFile != LogTarget::MIGRATION) {
                    ConsoleHelper::ok();
                }
            }
        }


    }

    /**
     * Restore cache from backup archive
     *
     * @param string $tmpDir extracted backup root directory
     * @param string logFile logging file name
     *
     * @throws \Exception
     */
    private static function restoreCache($tmpDir, $logFile)
    {
        LogHelper::addInfo($logFile, 'Restore cache', '');
        FileSystemHelper::rrmfile(CacheManager::getArchivePath());
        FileSystemHelper::rmove($tmpDir . '/' . NextDomFolder::VAR . '/' . NextDomFile::CACHE_TAR_GZ, CacheManager::getArchivePath());
        CacheManager::restore();
    }
    private static function updateConfig()
    {
        ConfigManager::save('hardware_name', '');
        $cache = CacheManager::byKey('nextdom::isCapable::sudo');
        $cache->remove();
    }

    /**
     * Init default values
     *
     * @throws \Exception
     */
    private static function initValues()
    {
        ConfigManager::save('nextdom::firstUse', 0);
    }

    private static function clearCache()
    {
        CacheManager::flush();
        exec('sh ' . NEXTDOM_ROOT . '/scripts/clear_cache.sh');
    }

    /**
     * Get the list of backups
     *
     * @return array list of backups
     * @throws \Exception
     */
    public static function listBackup(): array
    {
        $backupDir = self::getBackupDirectory();
        $backups = self::getBackupFileInfo($backupDir, 'newest');
        $results = [];
        foreach ($backups as $c_backup) {
            $results[] = basename($c_backup['file']);
        }
        return $results;
    }

    /**
     * Remove a backup file
     *
     * @param string $backupFilePath Backup file path
     *
     * @throws CoreException
     * @throws \Exception
     */
    public static function removeBackup(string $backupFilePath)
    {
        if (Utils::checkPath($backupFilePath) && is_file($backupFilePath)) {
            unlink($backupFilePath);
        } else {
            throw new CoreException(__('Impossible de trouver le fichier : ') . $backupFilePath);
        }
    }

    /**
     * Loads migrate script into mysql database
     *
     * @throws CoreException from RestoreManager::loadSQLFromFile
     */
    private static function loadSQLMigrateScript()
    {
        $migrateFile = sprintf("%s/install/migrate/migrate_0_0_0.sql", NEXTDOM_ROOT);

        self::loadSQLFromFile($migrateFile);
    }
}