+ add backup/restore feature as extension

This commit is contained in:
maxpozdeev 2023-09-10 23:31:40 +03:00
parent 149b554676
commit 1a75eec5dc
10 changed files with 970 additions and 0 deletions

1
src/ext/backup/.htaccess Normal file
View file

@ -0,0 +1 @@
deny from all

View file

@ -0,0 +1,190 @@
<?php declare(strict_types=1);
/*
This file is a part of myTinyTodo.
(C) Copyright 2023 Max Pozdeev <maxpozdeev@gmail.com>
Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details.
*/
namespace BackupExtension;
use Exception;
class Backup
{
public $lastErrorString = null;
public $filename;
private $fh;
private $level = 0;
private $tagClosed = true;
function __construct(?string $filename)
{
$this->filename = is_null($filename) ? MTTPATH. 'db/backup.xml' : $filename;
}
function isFileWritable()
{
if (!file_exists($this->filename)) {
@touch($this->filename);
}
if (!is_writable($this->filename)) {
return false;
}
return true;
}
function makeBackup()
{
if (!$this->isFileWritable()) {
$this->lastErrorString = __('backup.not_writable');
return false;
}
$this->fh = fopen($this->filename, 'w');
if ($this->fh === false) {
$ea = error_get_last();
$this->lastErrorString = $ea['message'] ?? "Failed to open file for writing";
return false;
}
$db = \DBConnection::instance();
fwrite($this->fh, "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
$this->writeOpeningTag('mttdb', [
'version' => 1,
'appversion' => \mytinytodo\Version::VERSION,
'dbversion' => \mytinytodo\Version::DB_VERSION,
'dbtype' => $db::DBTYPE,
'created' => date(DATE_ATOM)
]);
$this->level = 0;
$this->writeTable($db->prefix.'lists', 'lists', 'list');
$this->writeTable($db->prefix.'todolist', 'tasks', 'task');
$this->writeTable($db->prefix.'tags', 'tags', 'tag');
$this->writeTable($db->prefix.'tag2task', 'tag2task', 'item');
$this->writeTable($db->prefix.'settings', 'settings', 'item');
$this->writeClosingTag('mttdb');
fwrite($this->fh, "\n");
if (!fclose($this->fh)) {
$ea = error_get_last();
$this->lastErrorString = $ea['message'] ?? "Failed to close file";
return false;
}
return true;
}
function writeTable(string $table, string $group, string $itemName)
{
if (!preg_match("/^[\\w:]+$/", $table)) {
throw new Exception("Malformed table name: $table");
}
$db = \DBConnection::instance();
$props = null;
if ($db::DBTYPE == \DBConnection::DBTYPE_MYSQL) {
$autoinc = $this->getMysqlTableAutoIncrement($table);
if ($autoinc != '') {
$props = ['auto_increment' => $autoinc];
}
}
$this->writeOpeningTag($group, $props);
$q = $db->dq("SELECT * FROM $table");
while ($r = $q->fetchAssoc()) {
$this->writeItem($itemName, $r);
}
$this->writeClosingTag($group);
}
function writeItem(string $entity, $r)
{
$tagAttrs = null;
if (isset($r['id'])) {
$tagAttrs = ['id' => $r['id']];
unset($r['id']);
}
$this->writeOpeningTag($entity, $tagAttrs);
foreach ($r as $field => $value) {
$props = null;
if (is_null($value)) {
$props['isnull'] = 'yes';
}
$this->writeOpeningTag($field, $props);
$this->writeTagContent((string)$value);
$this->writeClosingTag($field);
}
$this->writeClosingTag($entity);
}
function getMysqlTableAutoIncrement(string $table): string
{
$db = \DBConnection::instance();
$r = $db->sqa("SHOW TABLE STATUS WHERE Name=?", [$table]);
return (string)$r['Auto_increment'] ?? '';
}
function writeOpeningTag(string $tag, ?array $attrs = null)
{
if (!preg_match("/^[\\w:]+$/", $tag)) {
throw new Exception("Malformed tag: $tag");
}
$data = "<$tag";
if ($attrs !== null) {
$a = [];
foreach ($attrs as $k => $v) {
if (!preg_match("/^[\\w:-]+$/", $k)) {
throw new Exception("Malformed attribute name: $k");
}
$v = (string)$v;
if (preg_match("/[\\r\\n]+/", $v)) {
throw new Exception("Malformed attribute value: $v");
}
$a[] = "$k=\"". htmlspecialchars($v). "\"";
}
if (count($a) > 0) {
$data .= " ". implode(" ", $a);
}
}
$data .= ">";
$this->write( ($this->tagClosed ? "" : "\n"). str_repeat(' ', $this->level) . $data );
$this->level += 1;
$this->tagClosed = false;
}
function writeClosingTag(string $tag)
{
if (!preg_match("/^[\\w:]+$/", $tag)) {
throw new Exception("Malformed tag: $tag");
}
$this->level -= 1;
if ($this->level < 0) $this->level = 0;
$padding = '';
if ($this->tagClosed) {
$padding = str_repeat(' ', $this->level);
}
$this->write( $padding . "</$tag>\n" );
$this->tagClosed = true;
}
function writeTagContent(?string $content)
{
if ($content !== null) {
$this->write( htmlspecialchars($content, ENT_XML1, 'UTF-8') ); //TODO: make xml compliant?
}
}
function write(string $data)
{
if (false === @fwrite($this->fh, $data)) {
$ea = error_get_last();
throw new Exception("Failed to write to file: ". ($ea['message'] ?? "unknown reason"));
}
}
}

View file

@ -0,0 +1,126 @@
<?php declare(strict_types=1);
/*
This file is a part of myTinyTodo.
(C) Copyright 2023 Max Pozdeev <maxpozdeev@gmail.com>
Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details.
*/
namespace BackupExtension;
use DBConnection;
class Check
{
public $lastErrorString = null;
public $report = '';
function check(): bool
{
$db = DBConnection::instance();
$msg = [];
// Task without list
$count = $db->sq("SELECT COUNT(*) FROM {$db->prefix}todolist WHERE list_id NOT IN (SELECT id FROM {$db->prefix}lists)");
if ($count) {
$msg[] = "Tasks without list: $count";
}
// Tag without task (not a broblem)
$count = $db->sq("SELECT COUNT(*) FROM {$db->prefix}tags WHERE id NOT IN (SELECT tag_id FROM {$db->prefix}tag2task)");
if ($count) {
$msg[] = "Tags without task: $count";
}
// tag2task no list
$count = $db->sq("SELECT COUNT(*) FROM {$db->prefix}tag2task WHERE list_id NOT IN (SELECT id FROM {$db->prefix}lists)");
if ($count) {
$msg[] = "tag2task no list: $count";
}
// tag2task no tag
$count = $db->sq("SELECT COUNT(*) FROM {$db->prefix}tag2task WHERE tag_id NOT IN (SELECT id FROM {$db->prefix}tags)");
if ($count) {
$msg[] = "tag2task no tag: $count";
}
// tag2task no task
$count = $db->sq("SELECT COUNT(*) FROM {$db->prefix}tag2task WHERE task_id NOT IN (SELECT id FROM {$db->prefix}todolist)");
if ($count) {
$msg[] = "tag2task no task: $count";
}
$count = 0;
$uniqTag = []; // lowerTag => [id, tag]
$nonuniqTag = []; // id => [tag, lowerTag, uniqId, uniqTag, taskCount]
$q = $db->dq("SELECT id,name,COUNT(task_id) c FROM {$db->prefix}tags t LEFT JOIN {$db->prefix}tag2task tt ON t.id=tt.tag_id GROUP BY id ORDER BY id");
while ($r = $q->fetchAssoc()) {
$v = mb_strtolower((string)$r['name'], 'UTF-8');
if (!isset($uniqTag[$v])) {
$uniqTag[$v] = [$r['id'], $r['name']];
}
else {
$count++;
$nonuniqTag[$r['id']] = [$r['name'], $v, $uniqTag[$v][0], $uniqTag[$v][1], $r['c']];
}
}
if ($count > 0) {
$msg[] = "Non-unique tags: $count";
foreach ($nonuniqTag as $id => $a) {
$msg[] = " ID:{$id} Tag:{$a[0]} (tasks: {$a[4]}) same as ID:{$a[2]} Tag:{$a[3]}";
}
}
if (count($msg) == 0) {
$msg[] = "OK";
}
$this->report = implode("\n", $msg);
return true;
}
function repair(): bool
{
$db = DBConnection::instance();
$db->ex("BEGIN");
// Task without list
$count = (int)$db->sq("SELECT COUNT(*) FROM {$db->prefix}todolist WHERE list_id NOT IN (SELECT id FROM {$db->prefix}lists)");
if ($count > 0) {
// Move to new list
$listID = \DBCore::default()->createListWithName("Restored tasks");
$db->ex("UPDATE {$db->prefix}todolist SET list_id=? WHERE list_id NOT IN (SELECT id FROM {$db->prefix}lists)", [$listID]);
}
//Tags
$db->ex("DELETE FROM {$db->prefix}tags WHERE id NOT IN (SELECT tag_id FROM {$db->prefix}tag2task)");
$db->ex("DELETE FROM {$db->prefix}tag2task WHERE task_id NOT IN (SELECT id FROM {$db->prefix}todolist)");
$db->ex("DELETE FROM {$db->prefix}tag2task WHERE tag_id NOT IN (SELECT id FROM {$db->prefix}tags)");
//Non-unique tags replace with first unique
$uniqTag = [];
$replace = [];
$q = $db->dq("SELECT id,name FROM {$db->prefix}tags t LEFT JOIN {$db->prefix}tag2task tt ON t.id=tt.tag_id GROUP BY id ORDER BY id");
while ($r = $q->fetchAssoc()) {
$v = mb_strtolower((string)$r['name'], 'UTF-8');
if (!isset($uniqTag[$v])) {
$uniqTag[$v] = $r['id'];
}
else {
$replace[$r['id']] = $uniqTag[$v];
}
}
foreach ($replace as $id => $newId) {
$db->ex("UPDATE {$db->prefix}tag2task SET tag_id=? WHERE tag_id=?", [$newId, $id]);
}
$db->ex("DELETE FROM {$db->prefix}tags WHERE id NOT IN (SELECT tag_id FROM {$db->prefix}tag2task)");
// TODO: tag2task no list ?
$db->ex("COMMIT");
return true;
}
}

View file

@ -0,0 +1,148 @@
<?php declare(strict_types=1);
/*
This file is a part of myTinyTodo.
(C) Copyright 2023 Max Pozdeev <maxpozdeev@gmail.com>
Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details.
*/
namespace BackupExtension;
use BackupExtension;
use BackupExtension\Backup;
use BackupExtension\Download;
use BackupExtension\Check;
class Controller extends \ApiController
{
function postMakeBackup()
{
require_once('class.backup.php');
$filename = BackupExtension::backupFilePath();
$backup = new Backup($filename);
if (!$backup->makeBackup()) {
$this->response->data = [
'total' => 0,
'msg' => __("error"),
'details' => $backup->lastErrorString ?? '',
];
}
$this->response->data = [
'total' => 1,
'msg' => __("backup.done"),
'details' => ''
];
}
function postDownload()
{
require_once('class.download.php');
$filename = BackupExtension::backupFilePath();
$download = new Download($filename);
if (!$download->checkFileAccess()) {
$this->response->data = [
'total' => 0,
'msg' => __("error"),
'details' => $download->lastErrorString ?? '',
];
return;
}
$this->response->data = [
'total' => 1,
'redirect' => $download->downloadUrl()
];
}
function getDownload()
{
require_once('class.download.php');
$filename = BackupExtension::backupFilePath();
$download = new Download($filename);
$ott = (string)_get('t');
if (!$download->checkFileAccess($ott)) {
$this->response->data = [
'total' => 0,
'msg' => __("error"),
'details' => $download->lastErrorString ?? '',
];
return;
}
$download->printFile();
exit();
}
function postRestore()
{
require_once('class.restore.php');
$restore = new Restore();
if (!$restore->isUploaded()) {
$this->response->data = [
'total' => 0,
'msg' => __("error"),
'details' => $restore->lastErrorString ?? '',
];
return;
}
if (!$restore->restore()) {
$this->response->data = [
'total' => 0,
'msg' => __("error"),
'details' => $restore->lastErrorString ?? '',
];
return;
}
$this->response->data = [
'total' => 1,
'msg' => __("backup.done"),
'redirect' => get_mttinfo('url'),
];
}
function postCheckInconsistency()
{
require_once('class.check.php');
$check = new Check();
if (!$check->check()) {
$this->response->data = [
'total' => 0,
'msg' => __("error"),
'details' => $check->lastErrorString ?? '',
];
return;
}
$html = "<pre>". htmlspecialchars($check->report). "</pre>";
$this->response->data = [
'total' => 1,
'msg' => __("backup.done"),
'html' => $html,
];
}
function postRepairInconsistency()
{
require_once('class.check.php');
$check = new Check();
if (!$check->repair()) {
$this->response->data = [
'total' => 0,
'msg' => __("error"),
'details' => $check->lastErrorString ?? '',
];
return;
}
$this->response->data = [
'total' => 1,
'msg' => __("backup.done"),
];
}
}

View file

@ -0,0 +1,74 @@
<?php declare(strict_types=1);
/*
This file is a part of myTinyTodo.
(C) Copyright 2023 Max Pozdeev <maxpozdeev@gmail.com>
Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details.
*/
namespace BackupExtension;
use BackupExtension;
class Download
{
public $filename;
public $lastErrorString = null;
private $token = '';
function __construct(?string $filename)
{
$this->filename = is_null($filename) ? MTTPATH. 'db/backup.xml' : $filename;
}
function checkFileAccess(?string $tokenHash = null): bool
{
if (!file_exists($this->filename)) {
$this->lastErrorString = "Backup file not found";
return false;
}
$this->token = access_token();
if ($this->token == '') {
$this->lastErrorString = "No token provided";
return false;
}
if (!is_null($tokenHash)) {
$a = explode(':', $tokenHash, 2);
$rnd = $a[0] ?? '';
$hash = $a[1] ?? '';
if (!hash_equals(hash_hmac('sha256', $rnd, $this->token), $hash)) {
$this->lastErrorString = "No temp token provided";
return false;
}
}
return true;
}
function downloadUrl()
{
$rnd = randomString();
$hash = $rnd. ':'. hash_hmac('sha256', $rnd, $this->token);
$url = BackupExtension::extApiActionUrl("download", "t=$hash");
return $url;
}
function printFile()
{
header('Content-type: application/xml; charset=utf-8');
header('Content-disposition: attachment; filename=backup.xml');
$fh = fopen($this->filename, "r") or die("Couldn't open file");
if ($fh) {
while (!feof($fh)) {
$buffer = fgets($fh, 4096);
print($buffer);
}
fclose($fh);
}
exit();
}
}

View file

@ -0,0 +1,251 @@
<?php declare(strict_types=1);
/*
This file is a part of myTinyTodo.
(C) Copyright 2023 Max Pozdeev <maxpozdeev@gmail.com>
Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details.
*/
namespace BackupExtension;
use XMLReader;
use DBConnection;
use Exception;
class Restore
{
public $lastErrorString = null;
private $filename;
/** @var XMLReader */
private $reader;
private $tableItem;
function __construct()
{
// xml table => [ db table, xml item ]
$this->tableItem = [
'lists' => ['lists', 'list'],
'tasks' => ['todolist', 'task'],
'tags' => ['tags', 'tag'],
'tag2task' => ['tag2task', 'item'],
'settings' => ['settings', 'item'],
];
}
function isUploaded(): bool
{
if (!isset($_FILES['file']) || !isset($_FILES['file']['name']) || !isset($_FILES['file']['tmp_name'])) {
$this->lastErrorString = "Not uploaded";
return false;
}
$this->filename = $_FILES['file']['tmp_name'];
if (!file_exists($this->filename) || !is_readable($this->filename)) {
$this->lastErrorString = "Can't open file";
return false;
}
return true;
}
function restore(): bool
{
$this->reader = $reader = new XMLReader();
$reader->open($this->filename);
// root element
$reader->next('mttdb');
if ($reader->name != 'mttdb') {
$this->lastErrorString = "Incorrect format: missing 'mttdb'.";
return false;
}
if (!$this->moveNextElement()) {
$this->lastErrorString = "Incorrect format: tables not found.";
return false;
}
$this->beginRestore();
$tables = array_keys($this->tableItem);
// Enumerate tables
do {
if ($reader->nodeType != XMLReader::ELEMENT) {
error_log("Unexpected element '{$reader->name}' of type: {$reader->nodeType}");
break;
}
//error_log("Found table '{$reader->name}'");
$result = null;
if (in_array($reader->name, $tables)) {
$result = $this->readTable($this->tableItem[$reader->name][0], $this->tableItem[$reader->name][1]);
}
else {
continue; // Unexpected table, just skip
}
if (is_null($result)) {
return false; // Incorrect format, error is set, stop
}
} while ($this->moveNextElementSameLevel());
$this->endRestore();
$reader->close();
return true;
}
function moveNextElement(?string $el = null): ?bool
{
while ($this->reader->read()) {
if ($this->reader->nodeType == XMLReader::ELEMENT) {
if (!is_null($el) && $this->reader->name != $el) {
return false;
}
return true;
}
else if ($this->reader->nodeType == XMLReader::END_ELEMENT) {
return null;
}
}
return null;
}
function moveNextElementSameLevel(?string $el = null)
{
return $this->reader->next() && ($this->reader->nodeType == XMLReader::ELEMENT || $this->moveNextElement($el));
}
function readTable(string $table, string $itemName): ?int
{
$autoinc = $this->reader->getAttribute("auto_increment");
$maxId = 0;
$count = 0;
// find first item
$found = $this->moveNextElement($itemName);
if ($found === false) {
$this->lastErrorString = "Incorrect item name {$this->reader->name}, expected '{$itemName}'";
return null; // Error
}
else if (is_null($found)) {
return 0; // No items found
}
do {
$count++;
$id = $this->reader->getAttribute("id");
if (!is_null($id)) {
$maxId = max($maxId, (int)$id);
}
// error_log("# $count: found $itemName with id $id");
$itemXml = $this->reader->readOuterXml();
$xml = simplexml_load_string($itemXml); //SimpleXMLElement
if ($xml === false) {
error_log("Incorrect format of $itemName");
continue;
}
if (!$this->insertToTable($table, $xml)) {
return null; // Error
}
} while ($this->moveNextElementSameLevel($itemName));
// restore table last auto_increment (mysql)
if (!is_null($autoinc)) {
$autoinc = max((int)$autoinc, $maxId);
}
else {
$autoinc = $maxId + 1;
}
if ($autoinc > 1) {
$this->updateAutoinc($table, $autoinc);
}
return $count;
}
private function insertToTable(string $table, \SimpleXMLElement $xml): bool
{
if (!preg_match("/^[\\w]+$/", $table)) {
throw new Exception("Malformed table name: $table");
}
$fields = [];
$values = [];
$attrsXml = $xml->attributes();
if (isset($attrsXml['id']) && $attrsXml['id'] != '') {
$fields[] = 'id';
$values[] = (string)$attrsXml['id'];
}
foreach ($xml->children() as $item) {
$field = $item->getName();
$value = (string)$item;
if (!preg_match("/^[\\w]+$/", $field)) {
throw new Exception("Malformed field name: $field");
}
$attrsXml = $item->attributes();
if (isset($attrsXml['isnull']) && $attrsXml['isnull'] == 'yes') { //$attrsXml['isnull']->__toString()
$value = null;
}
$fields[] = $field;
$values[] = $value;
}
$fieldsStr = implode(',', $fields); // id,name,title ...
$subsStr = implode(',', array_fill(0, count($fields), '?')); // ?,?,? ...
$db = DBConnection::instance();
try {
$db->ex("INSERT INTO {$db->prefix}{$table} ($fieldsStr) VALUES ($subsStr)", $values);
}
catch (Exception $e) {
error_log("Failed query: {$db->lastQuery}");
$this->lastErrorString = "Failed to add data to table '{$db->prefix}$table'. Database error (see query in error log): ". $e->getMessage();
return false;
}
return true;
}
private function updateAutoinc(string $table, int $autoinc)
{
$db = DBConnection::instance();
switch ($db::DBTYPE) {
case DBConnection::DBTYPE_MYSQL:
$db->ex("ALTER TABLE {$db->prefix}$table AUTO_INCREMENT = ". (int)$autoinc);
break;
case DBConnection::DBTYPE_POSTGRES:
$db->ex("ALTER TABLE {$db->prefix}$table ALTER COLUMN id RESTART WITH ". (int)$autoinc);
break;
default:
break;
}
}
private function beginRestore()
{
$db = DBConnection::instance();
$db->ex("BEGIN");
foreach ($this->tableItem as $a) {
$table = $db->prefix. $a[0];
if ($db::DBTYPE == DBConnection::DBTYPE_POSTGRES) {
$db->ex("TRUNCATE TABLE $table RESTART IDENTITY");
}
else {
// we do not use TRUNCATE on mysql due to autocommit
// sqlite has truncate optimizer
$db->ex("DELETE FROM $table");
}
}
$db->ex("DELETE FROM {$db->prefix}sessions");
}
private function endRestore()
{
$db = DBConnection::instance();
$db->ex("COMMIT");
// vacuum?
}
}

View file

@ -0,0 +1,6 @@
{
"bundleId": "backup",
"name": "Backup",
"version": "0.9",
"description": "Backup"
}

View file

@ -0,0 +1,17 @@
{
"ext.backup.name": "Backup",
"backup.h_make": "Make backup",
"backup.d_make": "Will create backup file in '%s' folder.",
"backup.make": "Make",
"backup.download": "Download",
"backup.done": "Done",
"backup.not_writable": "Backup file is not writable, check permissions for db/backup.xml",
"backup.last_backup": "Last backup: %s",
"backup.h_inconsistency": "Check for inconsistency",
"backup.d_inconsistency": "If you want to restore the backup to another database version or type, it's better to fix inconsistency before making backup.",
"backup.check": "Check",
"backup.repair": "Repair",
"backup.h_restore": "Restore from backup",
"backup.d_restore": "Will delete all records in existing database before restoring.",
"backup.restore": "Restore…"
}

View file

@ -0,0 +1,17 @@
{
"ext.backup.name": "Резервное копирование",
"backup.h_make": "Сделать копию",
"backup.d_make": "Сохранит файл в папке '%s'.",
"backup.make": "Создать",
"backup.download": "Скачать",
"backup.done": "Выполнено",
"backup.not_writable": "Ошибка записи в файл, проверьте права доступа к db/backup.xml",
"backup.last_backup": "Резервная копия: %s",
"backup.h_inconsistency": "Проверка базы данных",
"backup.d_inconsistency": "Если планируете восстановление из резервной копии в другую базу данных, то лучше исправить возможные ошибки перед резервным копированием.",
"backup.check": "Проверить",
"backup.repair": "Исправить",
"backup.h_restore": "Восстановить из резервной копии",
"backup.d_restore": "Удалит все существующие записи во время восстановления.",
"backup.restore": "Восстановить…"
}

140
src/ext/backup/loader.php Normal file
View file

@ -0,0 +1,140 @@
<?php declare(strict_types=1);
/*
This file is a part of myTinyTodo.
(C) Copyright 2023 Max Pozdeev <maxpozdeev@gmail.com>
Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details.
*/
if (!defined('MTTPATH')) {
die("Unexpected usage.");
}
require_once('class.controller.php');
function mtt_ext_backup_instance(): MTTExtension
{
return new BackupExtension();
}
use BackupExtension\Controller;
class BackupExtension extends MTTExtension implements MTTExtensionSettingsInterface, MTTHttpApiExtender
{
//the same as dir name
const bundleId = 'backup';
// settings domain
const domain = "ext.backup.json";
function init() {
}
// MTTHttpApiExtender
function extendHttpApi(): array
{
return array(
'/makeBackup' => [
'POST' => [ Controller::class , 'postMakeBackup' ],
],
'/download' => [
'POST' => [ Controller::class , 'postDownload' ],
'GET' => [ Controller::class , 'getDownload', true ], // doesn't check auth token
],
'/restore' => [
'POST' => [ Controller::class , 'postRestore' ],
],
'/checkInconsistency' => [
'POST' => [ Controller::class , 'postCheckInconsistency' ],
],
'/repairInconsistency' => [
'POST' => [ Controller::class , 'postRepairInconsistency' ],
],
);
}
function settingsPage(): string
{
$warning = '';
$e = function($s, $arg=null) { return __($s, true, $arg); };
$ext = htmlspecialchars(self::bundleId);
$downloadDisabled = '';
$lastBackup = '';
$filename = MTTPATH. 'db/backup.xml';
if (file_exists($filename)) {
$time = filemtime($filename);
$lastBackup = htmlspecialchars( sprintf($e('backup.last_backup'), formatTime(Config::get('dateformat'). " H:m:s", $time)) );
}
else {
$downloadDisabled = 'disabled';
}
return <<<EOD
$warning
<script>
function onBackupFileChange(el) {
const fd = new FormData();
fd.append('file', el.files[0]);
mytinytodo.extensionSettingsAction(el.dataset.extSettingsAction, el.dataset.ext, fd);
}
</script>
<div class="tr">
<div class="th"> {$e('backup.h_make')}
<div class="descr">{$e('backup.d_make', 'db')}</div>
</div>
<div class="td">
<button type=button data-ext-settings-action="post:makeBackup" data-ext="$ext"> {$e('backup.make')} </button> <br>
<br> $lastBackup &nbsp;
<button type=button data-ext-settings-action="post:download" data-ext="$ext" $downloadDisabled> {$e('backup.download')} </button>
</div>
</div>
<div class="tr">
<div class="th"> {$e('backup.h_inconsistency')}
<div class="descr">{$e('backup.d_inconsistency')}</div>
</div>
<div class="td">
<button type=button data-ext-settings-action="post:checkInconsistency:file" data-ext="$ext"> {$e('backup.check')} </button> &nbsp;
<button type=button data-ext-settings-action="post:repairInconsistency" data-ext="$ext"> {$e('backup.repair')} </button> <br>
</div>
</div>
<div class="tr">
<div class="th"> {$e('backup.h_restore')}
<div class="descr"> {$e('backup.d_restore')} </div>
</div>
<div class="td">
<label class="mtt-settings-upload-button">
<input type="file" name="file" onchange="return onBackupFileChange(this)" data-ext-settings-action="post:restore" data-ext="$ext">
{$e('backup.restore')}
</label>
</div>
</div>
EOD;
}
function settingsPageType(): int
{
return 1; //no form buttons
}
function saveSettings(array $params, ?string &$outMessage): bool
{
return false;
}
/*
static function preferences(): array
{
return [
'backupFilePath' => MTTPATH. 'db/backup.xml'
];
}
*/
static function backupFilePath()
{
//return self::preferences()['backupFilePath'];
return MTTPATH. 'db/backup.xml';
}
}