. * */ namespace Vvveb\Controller\Tools; use function Vvveb\__; use Vvveb\Controller\Base; use function Vvveb\formatBytes; use function Vvveb\pregMatch; use function Vvveb\sanitizeFileName; use Vvveb\System\Db; use function Vvveb\url; class Backup extends Base { private $tables = []; private $position = 0; private $db; function __construct() { $this->db = Db::getInstance(); } public function getTableNames() { return $this->db->getTableNames(); } public function truncateTableSQL($sql) { if (DB_ENGINE == 'sqlite') { $tableName = pregMatch('/TRUNCATE TABLE [`"\']?([^`"\';]+)[`"\']?;?/ms', $sql, 1); $sql = preg_replace('/TRUNCATE TABLE [`"\']?([^`"\';]+)[`"\']?;?/ms', "DELETE FROM '\\1';\nDELETE FROM SQLITE_SEQUENCE WHERE name='\\1';", $sql); if ($tableName == 'post_content') { //$sql .= ';DELETE FROM \'post_content_search\';DELETE FROM SQLITE_SEQUENCE WHERE name=\'post_content_search\';'; $sql = trim($sql); $sql .= "\nDELETE FROM 'post_content_search';\n"; } if ($tableName == 'product_content') { //$sql .= ';DELETE FROM \'product_content_search\';DELETE FROM SQLITE_SEQUENCE WHERE name=\'product_content_search\';'; $sql = trim($sql); $sql .= "\nDELETE FROM 'product_content_search';\n"; } return $sql; } else { if (DB_ENGINE == 'pgsql') { $sql = preg_replace('/TRUNCATE TABLE [`"\']?([^`"\';]+)[`"\']?;?/ms', 'TRUNCATE TABLE "\1";', $sql); } } return $sql; } public function insertSQL($sql) { if (DB_ENGINE == 'pgsql') { $sql = preg_replace_callback('/INSERT INTO .+?\) VALUES \(/ms', function ($matches) { return str_replace('`','"', $matches[0]); }, $sql); } return $sql; } function getTableDump($tableName, $page = 1, $limit = 1000) { $output = ''; if ($page == 1) { $output = "TRUNCATE TABLE `$tableName`;\n\n"; } //don't dump sqlite virtual tables used for full text search if (substr($tableName, -7) == '_search') { return $output; } $start = (($page - 1) * $limit); $sql = "SELECT * FROM `$tableName` LIMIT $start, $limit"; $stmt = $this->db->execute($sql, [], []); $rows = $this->db->fetchAll($stmt); if ($rows) { $columns = $this->db->getColumnsMeta($tableName); array_walk($columns, fn (&$v) => $v = $v['name']); //$output .= "INSERT INTO `$tableName` "; //$output .= '(`' . implode('`,`', $columns) . "`) VALUES \n"; $len = count($rows); $i = 0; foreach ($rows as $row) { array_walk($row, function (&$v) { if (is_null($v)) { $v = 'null'; } else { if (is_numeric($v)) { } else { if (is_string($v)) { $v = '\'' . $this->db->escape($v) . '\''; } } } }); $output .= "INSERT INTO `$tableName` "; $output .= '(`' . implode('`,`', $columns) . '`) VALUES '; $output .= '(' . implode(',', $row) . ')'; if (++$i < $len) { //$output .= ",\n"; $output .= ";\n"; } else { $output .= ";\n"; } } $output .= "\n"; } return $output; } function nextBackup() { $page = $this->request->get['page'] ?? 1; $table = $this->request->get['table'] ?? false; $position = $this->request->get['position'] ?? 1; $file = $this->request->get['file'] ?? date('Y-m-d H:i:s'); $tables = $this->getTableNames(); $filename = DIR_BACKUP . $file . '.sql'; $count = count($tables); $position = (int)array_search($table, $tables); $start = microtime(true); $elapsed = 0; $tableName = 0; $handle = fopen($filename, 'a'); $rows = ''; //if takes longer than 8 seconds start a new process to avoid php timeout while ($elapsed < 8 && ($position < $count)) { $tableName = $tables[$position]; $output = $this->getTableDump($tableName, $page++); fwrite($handle, $output); $end = microtime(true); $elapsed = $end - $start; //usleep(100000); if (! $output) { $page = 1; $position++; } } fclose($handle); $tableName = $tables[$position] ?? false; if ($tableName) { $url = ['module'=>'tools/backup', 'action' => 'nextBackup', 'file' => $file, 'table' => $tableName, 'page' => $page, 'position' => $position, 'count' => $count]; } else { $message = __('Backup finished!'); $url = ['module'=>'tools/backup', 'success' => $message]; } if ($this->request->isAjax()) { die(json_encode($url + ['url' => url($url), 'page' => $page, 'position' => $position, 'count' => $count])); } else { $this->redirect($url); return $this->index(); } } function nextRestore() { $position = $this->request->get['position'] ?? 0; $file = $this->request->get['file']; $filename = DIR_BACKUP . $file; $size = filesize($filename); $time = microtime(true); $elapsed = 0; $i = 0; $handle = fopen($filename, 'r'); fseek($handle, $position, SEEK_SET); $start = false; $isInsert = false; $sql = ''; while (! feof($handle) && ($i < 10000 || $start) && ($elapsed < 8 || $start)) { $line = fgets($handle, 1000000); if (substr($line, 0, 14) == 'TRUNCATE TABLE') { $sql = ''; $line = $this->truncateTableSQL($line); $start = true; $isInsert = false; } if (substr($line, 0, 11) == 'INSERT INTO') { $sql = ''; $line = $this->insertSQL($line); $isInsert = true; $start = true; } if ($start) { $sql .= $line; } $end = substr($line, -2); if ($start && $end == ";\n") { //$sql = substr($sql, 0, strlen($sql) - 2); if ($isInsert) { $current = ftell($handle); while (($peek = fgets($handle, 8)) && empty(trim($peek))) { } if (feof($handle)) { $isInsert = false; $peek = ''; } fseek($handle, $current); $next = substr($peek, 0, 6); if ($next == 'TRUNCA' || $next == 'INSERT') { $isInsert = false; } } if (! $isInsert) { $this->db->query($sql); $sql = ''; $start = false; } } $i++; $timeEnd = microtime(true); $elapsed = $timeEnd - $time; } $position = ftell($handle); $inProgress = $position && ! feof($handle); fclose($handle); if ($inProgress) { $url = ['module'=>'tools/backup', 'action' => 'nextRestore', 'file' => $file, 'table' => formatBytes($position) . ' - ' . formatBytes($size), 'position' => $position, 'count' => $size]; if ($this->request->isAjax()) { die(json_encode($url + ['url' => url($url)])); } else { $this->redirect($url); die('Processing'); } } else { $message = __('Restore finished!'); $url = ['module'=>'tools/backup', 'success' => $message, 'table' => $message, 'position' => $position, 'count' => $size]; if ($this->request->isAjax()) { die(json_encode($url + ['url' => url($url)])); } else { $this->redirect($url); $this->view->info[] = $message; $this->index(); } } } function delete() { $file = sanitizeFileName($this->request->get['file'] ?? ''); if ($file) { $file = DIR_BACKUP . $file; if (file_exists($file)) { if (unlink($file)) { $this->view->success[] = __('Backup deleted!'); } else { $this->view->errors[] = __('Error deleting backup!'); } } else { $this->view->errors[] = __('Backup does not exist!'); } } $this->index(); } function restore() { $file = sanitizeFileName($this->request->post['file'] ?? $this->request->get['file'] ?? ''); $url = ['module'=>'tools/backup', 'action' => 'nextRestore', 'file' => $file]; if ($file) { if (file_exists(DIR_BACKUP . $file)) { if ($this->request->isAjax()) { die(json_encode($url + ['url' => url($url)])); } else { $this->redirect($url); return $this->index(); } } else { $error = __('Backup does not exist!'); if ($this->request->isAjax()) { die(json_encode($url + ['error' => $error])); } else { $this->view->errors[] = $error; return $this->index(); } } } } function download() { $filename = sanitizeFileName($this->request->get['file'] ?? ''); if ($filename) { $file = DIR_BACKUP . $filename; if (file_exists($file)) { $fp = fopen($file, 'rb'); header('Content-Type: text/plain'); header('Content-Length: ' . filesize($file)); header('Content-Disposition: attachment; filename="' . $filename . '"'); fpassthru($fp); exit(0); } else { $this->view->errors[] = __('Backup does not exist!'); } } return $this->index(); } function save() { $url = ['module'=>'tools/backup', 'action' => 'nextBackup']; if ($this->request->isAjax()) { die(json_encode($url + ['url' => url($url)])); } else { $this->redirect($url); return $this->index(); } } function index() { $view = $this->view; $backupFiles = glob(DIR_BACKUP . '*'); $tableNames = $this->db->getTableNames(); foreach ($backupFiles as $index => $file) { $name = basename($file); $size = filesize($file); $backups[] = [ 'name' => $name, 'key' => $index, 'file' => $file, 'size_bytes' => $size, 'size' => formatBytes($size), 'created_at' => date('Y/m/d H:i:s', filemtime($file)), 'restore-url' => url(['module'=>'tools/backup', 'action' => 'restore', 'file' => $name]), 'download-url' => url(['module'=>'tools/backup', 'action' => 'download', 'file' => $name]), 'delete-url' => url(['module'=>'tools/backup', 'action' => 'delete', 'file' => $name]), ]; } $view->backupUrl = url(['module'=>'tools/backup', 'action' => 'save']); $view->backups = $backups ?? []; } }