414 lines
10 KiB
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 ?? [];
|
|
}
|
|
}
|