VvebOIDC/admin/controller/tools/backup.php

414 lines
10 KiB
PHP

<?php
/**
* Vvveb
*
* Copyright (C) 2022 Ziadin Givan
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
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 ?? [];
}
}