Add background job execution for restore and live status dashboard
- Runner.pm: extract _validate()/_build_cmd_line(), add run_async() that forks a detached child via setsid() to run commands in background - restore.cgi: handle_step4() builds commands array and uses run_async() instead of blocking synchronous execution, redirects to logs.cgi - logs.cgi: add auto-refresh JS (10s list view, 5s file view with auto-scroll) that polls index.cgi?action=status while gniza is running - index.cgi: add live status card with AJAX polling and JSON endpoint - Cron/schedule: redirect cron output to /dev/null (gniza has own logs) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -144,7 +144,7 @@ build_cron_line() {
|
|||||||
extra_flags+=" --sysbackup"
|
extra_flags+=" --sysbackup"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "$cron_expr /usr/local/bin/gniza backup${extra_flags} >> /var/log/gniza/cron-${name}.log 2>&1"
|
echo "$cron_expr /usr/local/bin/gniza backup${extra_flags} >/dev/null 2>&1"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Crontab Management ────────────────────────────────────────
|
# ── Crontab Management ────────────────────────────────────────
|
||||||
|
|||||||
@@ -11,13 +11,26 @@ use GnizaWHM::Config;
|
|||||||
use GnizaWHM::Cron;
|
use GnizaWHM::Cron;
|
||||||
use GnizaWHM::UI;
|
use GnizaWHM::UI;
|
||||||
|
|
||||||
|
my $form = Cpanel::Form::parseform();
|
||||||
|
|
||||||
# Redirect to setup wizard if gniza is not configured
|
# Redirect to setup wizard if gniza is not configured
|
||||||
unless (GnizaWHM::UI::is_configured()) {
|
unless (GnizaWHM::UI::is_configured()) {
|
||||||
|
if (($form->{'action'} // '') eq 'status') {
|
||||||
|
print "Content-Type: application/json\r\n\r\n";
|
||||||
|
print '{"running":false,"pid":"","log_file":"","lines":[]}';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
print "Status: 302 Found\r\n";
|
print "Status: 302 Found\r\n";
|
||||||
print "Location: setup.cgi\r\n\r\n";
|
print "Location: setup.cgi\r\n\r\n";
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# JSON status endpoint for AJAX polling
|
||||||
|
if (($form->{'action'} // '') eq 'status') {
|
||||||
|
handle_status_json();
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
print "Content-Type: text/html\r\n\r\n";
|
print "Content-Type: text/html\r\n\r\n";
|
||||||
|
|
||||||
Whostmgr::HTMLInterface::defheader('GNIZA Backup Manager — Dashboard', '', '/cgi/gniza-whm/index.cgi');
|
Whostmgr::HTMLInterface::defheader('GNIZA Backup Manager — Dashboard', '', '/cgi/gniza-whm/index.cgi');
|
||||||
@@ -26,6 +39,17 @@ print GnizaWHM::UI::page_header('GNIZA Backup Manager');
|
|||||||
print GnizaWHM::UI::render_nav('index.cgi');
|
print GnizaWHM::UI::render_nav('index.cgi');
|
||||||
print GnizaWHM::UI::render_flash();
|
print GnizaWHM::UI::render_flash();
|
||||||
|
|
||||||
|
# Live Status card
|
||||||
|
print qq{<div id="gniza-live-status" class="card bg-white shadow-sm border border-base-300 mb-6">\n<div class="card-body">\n};
|
||||||
|
print qq{<h2 class="card-title text-sm">Live Status</h2>\n};
|
||||||
|
print qq{<div id="gniza-status-content">\n};
|
||||||
|
print qq{ <div class="flex items-center gap-2">\n};
|
||||||
|
print qq{ <span class="loading loading-spinner loading-xs"></span>\n};
|
||||||
|
print qq{ <span class="text-sm text-base-content/60">Checking...</span>\n};
|
||||||
|
print qq{ </div>\n};
|
||||||
|
print qq{</div>\n};
|
||||||
|
print qq{</div>\n</div>\n};
|
||||||
|
|
||||||
# Quick links
|
# Quick links
|
||||||
print qq{<div class="flex gap-3 mb-5">\n};
|
print qq{<div class="flex gap-3 mb-5">\n};
|
||||||
print qq{ <button type="button" class="btn btn-primary btn-sm" onclick="location.href='setup.cgi'">Run Setup Wizard</button>\n};
|
print qq{ <button type="button" class="btn btn-primary btn-sm" onclick="location.href='setup.cgi'">Run Setup Wizard</button>\n};
|
||||||
@@ -83,5 +107,154 @@ print qq{<tr><td class="font-semibold w-44">gniza version</td><td>} . GnizaWHM::
|
|||||||
print qq{</table></div>\n};
|
print qq{</table></div>\n};
|
||||||
print qq{</div>\n</div>\n};
|
print qq{</div>\n</div>\n};
|
||||||
|
|
||||||
|
# Inline JavaScript for live status polling
|
||||||
|
print qq{<script>
|
||||||
|
(function() {
|
||||||
|
var interval = null;
|
||||||
|
var wasRunning = false;
|
||||||
|
var el = document.getElementById('gniza-status-content');
|
||||||
|
|
||||||
|
function escHtml(s) {
|
||||||
|
var d = document.createElement('div');
|
||||||
|
d.appendChild(document.createTextNode(s));
|
||||||
|
return d.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
function levelColor(level) {
|
||||||
|
if (level === 'ERROR') return 'text-error';
|
||||||
|
if (level === 'WARN') return 'text-warning';
|
||||||
|
if (level === 'DEBUG') return 'text-base-content/60';
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function update(data) {
|
||||||
|
var html = '';
|
||||||
|
if (data.running) {
|
||||||
|
html += '<div class="flex items-center gap-2 mb-3">';
|
||||||
|
html += '<span class="badge badge-warning badge-sm animate-pulse">Running</span>';
|
||||||
|
html += '<span class="text-sm">PID ' + escHtml(data.pid);
|
||||||
|
if (data.log_file) html += ' — ' + escHtml(data.log_file);
|
||||||
|
html += '</span></div>';
|
||||||
|
if (data.lines && data.lines.length) {
|
||||||
|
html += '<pre class="font-mono text-xs bg-base-200 rounded-lg p-3 overflow-x-auto whitespace-pre-wrap max-h-48 overflow-y-auto">';
|
||||||
|
for (var i = 0; i < data.lines.length; i++) {
|
||||||
|
var cls = levelColor(data.lines[i].level);
|
||||||
|
html += '<span class="' + cls + '">' + escHtml(data.lines[i].text) + '</span>\\n';
|
||||||
|
}
|
||||||
|
html += '</pre>';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
html += '<div class="flex items-center gap-2">';
|
||||||
|
html += '<span class="badge badge-success badge-sm">Idle</span>';
|
||||||
|
html += '<span class="text-sm">No backup or restore running.</span>';
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
|
el.innerHTML = html;
|
||||||
|
|
||||||
|
if (data.running !== wasRunning) {
|
||||||
|
clearInterval(interval);
|
||||||
|
interval = setInterval(poll, data.running ? 10000 : 60000);
|
||||||
|
wasRunning = data.running;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function poll() {
|
||||||
|
fetch('index.cgi?action=status')
|
||||||
|
.then(function(r) { return r.json(); })
|
||||||
|
.then(update)
|
||||||
|
.catch(function() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
interval = setInterval(poll, 10000);
|
||||||
|
poll();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
};
|
||||||
|
|
||||||
print GnizaWHM::UI::page_footer();
|
print GnizaWHM::UI::page_footer();
|
||||||
Whostmgr::HTMLInterface::footer();
|
Whostmgr::HTMLInterface::footer();
|
||||||
|
|
||||||
|
# --- JSON status endpoint ---
|
||||||
|
sub handle_status_json {
|
||||||
|
print "Content-Type: application/json\r\n\r\n";
|
||||||
|
|
||||||
|
my $lock_file = '/var/run/gniza.lock';
|
||||||
|
my $running = 0;
|
||||||
|
my $pid = '';
|
||||||
|
|
||||||
|
# Check lock file for running PID
|
||||||
|
if (-f $lock_file) {
|
||||||
|
if (open my $fh, '<', $lock_file) {
|
||||||
|
$pid = <$fh> // '';
|
||||||
|
chomp $pid;
|
||||||
|
close $fh;
|
||||||
|
if ($pid =~ /^\d+$/ && kill(0, $pid)) {
|
||||||
|
$running = 1;
|
||||||
|
} else {
|
||||||
|
$pid = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Find LOG_DIR from main config
|
||||||
|
my $log_dir = '/var/log/gniza';
|
||||||
|
my $main_conf_file = '/etc/gniza/gniza.conf';
|
||||||
|
if (-f $main_conf_file) {
|
||||||
|
my $cfg = GnizaWHM::Config::parse($main_conf_file, 'main');
|
||||||
|
$log_dir = $cfg->{LOG_DIR} if $cfg->{LOG_DIR} && $cfg->{LOG_DIR} ne '';
|
||||||
|
}
|
||||||
|
|
||||||
|
# Find most recent gniza-*.log by mtime
|
||||||
|
my $log_file = '';
|
||||||
|
my @lines;
|
||||||
|
if (opendir my $dh, $log_dir) {
|
||||||
|
my @logs = sort {
|
||||||
|
(stat("$log_dir/$b"))[9] <=> (stat("$log_dir/$a"))[9]
|
||||||
|
} grep { /^gniza-\d{8}-\d{6}\.log$/ } readdir($dh);
|
||||||
|
closedir $dh;
|
||||||
|
$log_file = $logs[0] // '';
|
||||||
|
}
|
||||||
|
|
||||||
|
# Read last 15 lines if running and log exists
|
||||||
|
if ($log_file && $running) {
|
||||||
|
my $path = "$log_dir/$log_file";
|
||||||
|
if (open my $fh, '<', $path) {
|
||||||
|
my @all = <$fh>;
|
||||||
|
close $fh;
|
||||||
|
my $start = @all > 15 ? @all - 15 : 0;
|
||||||
|
@lines = @all[$start .. $#all];
|
||||||
|
chomp @lines;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build JSON manually (no JSON module dependency)
|
||||||
|
my $json = '{"running":' . ($running ? 'true' : 'false');
|
||||||
|
$json .= ',"pid":"' . _json_esc($pid) . '"';
|
||||||
|
$json .= ',"log_file":"' . _json_esc($log_file) . '"';
|
||||||
|
$json .= ',"lines":[';
|
||||||
|
my @json_lines;
|
||||||
|
for my $line (@lines) {
|
||||||
|
my $level = 'INFO';
|
||||||
|
if ($line =~ /\]\s+\[(\w+)\]/) {
|
||||||
|
$level = uc($1);
|
||||||
|
}
|
||||||
|
push @json_lines, '{"level":"' . _json_esc($level) . '","text":"' . _json_esc($line) . '"}';
|
||||||
|
}
|
||||||
|
$json .= join(',', @json_lines);
|
||||||
|
$json .= ']}';
|
||||||
|
|
||||||
|
print $json;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub _json_esc {
|
||||||
|
my ($s) = @_;
|
||||||
|
$s //= '';
|
||||||
|
$s =~ s/\\/\\\\/g;
|
||||||
|
$s =~ s/"/\\"/g;
|
||||||
|
$s =~ s/\n/\\n/g;
|
||||||
|
$s =~ s/\r/\\r/g;
|
||||||
|
$s =~ s/\t/\\t/g;
|
||||||
|
# Escape control characters
|
||||||
|
$s =~ s/([\x00-\x1f])/sprintf("\\u%04x", ord($1))/ge;
|
||||||
|
return $s;
|
||||||
|
}
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ sub install_schedule {
|
|||||||
if (($conf->{SYSBACKUP} // '') eq 'yes') {
|
if (($conf->{SYSBACKUP} // '') eq 'yes') {
|
||||||
$extra_flags .= " --sysbackup";
|
$extra_flags .= " --sysbackup";
|
||||||
}
|
}
|
||||||
my $cmd_line = "$cron_expr $GNIZA_BIN backup${extra_flags} >> /var/log/gniza/cron-${name}.log 2>&1";
|
my $cmd_line = "$cron_expr $GNIZA_BIN backup${extra_flags} >/dev/null 2>&1";
|
||||||
|
|
||||||
# Read current crontab, strip existing entry for this schedule, append new
|
# Read current crontab, strip existing entry for this schedule, append new
|
||||||
my $crontab = _read_crontab();
|
my $crontab = _read_crontab();
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ use strict;
|
|||||||
use warnings;
|
use warnings;
|
||||||
use IPC::Open3;
|
use IPC::Open3;
|
||||||
use Symbol 'gensym';
|
use Symbol 'gensym';
|
||||||
|
use POSIX qw(setsid);
|
||||||
|
use File::Temp qw(tempfile);
|
||||||
|
|
||||||
my $GNIZA_BIN = '/usr/local/bin/gniza';
|
my $GNIZA_BIN = '/usr/local/bin/gniza';
|
||||||
|
|
||||||
@@ -47,9 +49,9 @@ my %OPT_PATTERNS = (
|
|||||||
account => qr/^[a-z][a-z0-9_-]*$/,
|
account => qr/^[a-z][a-z0-9_-]*$/,
|
||||||
);
|
);
|
||||||
|
|
||||||
# run($cmd, $subcmd, \@args, \%opts)
|
# _validate($cmd, $subcmd, \@args, \%opts)
|
||||||
# Returns ($success, $stdout, $stderr).
|
# Returns (1, undef) on success or (0, $error_msg) on failure.
|
||||||
sub run {
|
sub _validate {
|
||||||
my ($cmd, $subcmd, $args, $opts) = @_;
|
my ($cmd, $subcmd, $args, $opts) = @_;
|
||||||
$args //= [];
|
$args //= [];
|
||||||
$opts //= {};
|
$opts //= {};
|
||||||
@@ -81,21 +83,61 @@ sub run {
|
|||||||
|
|
||||||
unless ($matched) {
|
unless ($matched) {
|
||||||
my $desc = "gniza $cmd" . (defined $subcmd ? " $subcmd" : "") . " " . join(" ", @$args);
|
my $desc = "gniza $cmd" . (defined $subcmd ? " $subcmd" : "") . " " . join(" ", @$args);
|
||||||
return (0, '', "Command not allowed: $desc");
|
return (0, "Command not allowed: $desc");
|
||||||
}
|
}
|
||||||
|
|
||||||
# Validate options
|
# Validate options
|
||||||
for my $key (keys %$opts) {
|
for my $key (keys %$opts) {
|
||||||
my $pat = $OPT_PATTERNS{$key};
|
my $pat = $OPT_PATTERNS{$key};
|
||||||
unless ($pat) {
|
unless ($pat) {
|
||||||
return (0, '', "Unknown option: --$key");
|
return (0, "Unknown option: --$key");
|
||||||
}
|
}
|
||||||
unless ($opts->{$key} =~ $pat) {
|
unless ($opts->{$key} =~ $pat) {
|
||||||
return (0, '', "Invalid value for --$key: $opts->{$key}");
|
return (0, "Invalid value for --$key: $opts->{$key}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Build command
|
return (1, undef);
|
||||||
|
}
|
||||||
|
|
||||||
|
# _shell_quote($str)
|
||||||
|
# Single-quote a string for safe shell embedding.
|
||||||
|
sub _shell_quote {
|
||||||
|
my ($str) = @_;
|
||||||
|
$str =~ s/'/'\\''/g;
|
||||||
|
return "'$str'";
|
||||||
|
}
|
||||||
|
|
||||||
|
# _build_cmd_line($cmd, $subcmd, \@args, \%opts)
|
||||||
|
# Returns a shell command string.
|
||||||
|
sub _build_cmd_line {
|
||||||
|
my ($cmd, $subcmd, $args, $opts) = @_;
|
||||||
|
$args //= [];
|
||||||
|
$opts //= {};
|
||||||
|
|
||||||
|
my @parts = (_shell_quote($GNIZA_BIN), _shell_quote($cmd));
|
||||||
|
push @parts, _shell_quote($subcmd) if defined $subcmd;
|
||||||
|
push @parts, _shell_quote($_) for @$args;
|
||||||
|
for my $key (sort keys %$opts) {
|
||||||
|
push @parts, _shell_quote("--$key=$opts->{$key}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return join(' ', @parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
# run($cmd, $subcmd, \@args, \%opts)
|
||||||
|
# Returns ($success, $stdout, $stderr).
|
||||||
|
sub run {
|
||||||
|
my ($cmd, $subcmd, $args, $opts) = @_;
|
||||||
|
$args //= [];
|
||||||
|
$opts //= {};
|
||||||
|
|
||||||
|
my ($valid, $err) = _validate($cmd, $subcmd, $args, $opts);
|
||||||
|
unless ($valid) {
|
||||||
|
return (0, '', $err);
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build exec args
|
||||||
my @exec_args = ($cmd);
|
my @exec_args = ($cmd);
|
||||||
push @exec_args, $subcmd if defined $subcmd;
|
push @exec_args, $subcmd if defined $subcmd;
|
||||||
push @exec_args, @$args;
|
push @exec_args, @$args;
|
||||||
@@ -106,6 +148,57 @@ sub run {
|
|||||||
return _exec(@exec_args);
|
return _exec(@exec_args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# run_async(\@commands)
|
||||||
|
# Each command: [$cmd, $subcmd, \@args, \%opts]
|
||||||
|
# Validates all commands upfront, then forks a detached child to run them.
|
||||||
|
# Returns ($success, $error_msg).
|
||||||
|
sub run_async {
|
||||||
|
my ($commands) = @_;
|
||||||
|
|
||||||
|
return (0, 'No commands provided') unless $commands && @$commands;
|
||||||
|
|
||||||
|
# Validate all commands upfront
|
||||||
|
my @cmd_lines;
|
||||||
|
for my $c (@$commands) {
|
||||||
|
my ($cmd, $subcmd, $args, $opts) = @$c;
|
||||||
|
my ($valid, $err) = _validate($cmd, $subcmd, $args, $opts);
|
||||||
|
unless ($valid) {
|
||||||
|
return (0, $err);
|
||||||
|
}
|
||||||
|
push @cmd_lines, _build_cmd_line($cmd, $subcmd, $args, $opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
# Write bash wrapper script
|
||||||
|
my ($fh, $tmpfile) = tempfile('gniza-whm-job-XXXXXXXX', DIR => '/tmp', SUFFIX => '.sh');
|
||||||
|
print $fh "#!/bin/bash\n";
|
||||||
|
print $fh "set -uo pipefail\n";
|
||||||
|
for my $line (@cmd_lines) {
|
||||||
|
print $fh "$line\n";
|
||||||
|
}
|
||||||
|
print $fh "rm -f \"\$0\"\n";
|
||||||
|
close $fh;
|
||||||
|
chmod 0700, $tmpfile;
|
||||||
|
|
||||||
|
# Fork and detach
|
||||||
|
my $pid = fork();
|
||||||
|
if (!defined $pid) {
|
||||||
|
unlink $tmpfile;
|
||||||
|
return (0, "Fork failed: $!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($pid == 0) {
|
||||||
|
# Child: detach from parent
|
||||||
|
setsid();
|
||||||
|
open STDIN, '<', '/dev/null';
|
||||||
|
open STDOUT, '>', '/dev/null';
|
||||||
|
open STDERR, '>', '/dev/null';
|
||||||
|
exec('/bin/bash', $tmpfile) or exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parent: don't wait for child
|
||||||
|
return (1, '');
|
||||||
|
}
|
||||||
|
|
||||||
sub _exec {
|
sub _exec {
|
||||||
my (@args) = @_;
|
my (@args) = @_;
|
||||||
|
|
||||||
|
|||||||
@@ -125,6 +125,25 @@ sub show_list {
|
|||||||
print qq{</div>\n};
|
print qq{</div>\n};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Auto-refresh while gniza is running
|
||||||
|
print qq{<script>
|
||||||
|
(function() {
|
||||||
|
var key = 'gniza_wasRunning';
|
||||||
|
fetch('index.cgi?action=status')
|
||||||
|
.then(function(r) { return r.json(); })
|
||||||
|
.then(function(d) {
|
||||||
|
if (d.running) {
|
||||||
|
sessionStorage.setItem(key, '1');
|
||||||
|
setTimeout(function() { location.reload(); }, 10000);
|
||||||
|
} else if (sessionStorage.getItem(key) === '1') {
|
||||||
|
sessionStorage.removeItem(key);
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function() {});
|
||||||
|
})();
|
||||||
|
</script>\n};
|
||||||
|
|
||||||
print GnizaWHM::UI::page_footer();
|
print GnizaWHM::UI::page_footer();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,6 +282,26 @@ sub show_file {
|
|||||||
print qq{</pre>\n};
|
print qq{</pre>\n};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Auto-refresh + auto-scroll while gniza is running
|
||||||
|
print qq{<script>
|
||||||
|
(function() {
|
||||||
|
var key = 'gniza_wasRunning_file';
|
||||||
|
fetch('index.cgi?action=status')
|
||||||
|
.then(function(r) { return r.json(); })
|
||||||
|
.then(function(d) {
|
||||||
|
if (d.running) {
|
||||||
|
sessionStorage.setItem(key, '1');
|
||||||
|
window.scrollTo(0, document.body.scrollHeight);
|
||||||
|
setTimeout(function() { location.reload(); }, 5000);
|
||||||
|
} else if (sessionStorage.getItem(key) === '1') {
|
||||||
|
sessionStorage.removeItem(key);
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function() {});
|
||||||
|
})();
|
||||||
|
</script>\n};
|
||||||
|
|
||||||
print GnizaWHM::UI::page_footer();
|
print GnizaWHM::UI::page_footer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -890,19 +890,17 @@ sub handle_step4 {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Execute each type sequentially
|
# Build commands for async execution
|
||||||
my @results;
|
my @commands;
|
||||||
for my $type (@selected_types) {
|
for my $type (@selected_types) {
|
||||||
my %opts = (remote => $remote);
|
my %opts = (remote => $remote);
|
||||||
$opts{timestamp} = $timestamp if $timestamp ne '';
|
$opts{timestamp} = $timestamp if $timestamp ne '';
|
||||||
|
|
||||||
if ($SIMPLE_TYPES{$type}) {
|
if ($SIMPLE_TYPES{$type}) {
|
||||||
my ($ok, $stdout, $stderr) = GnizaWHM::Runner::run('restore', $type, [$account], \%opts);
|
push @commands, ['restore', $type, [$account], {%opts}];
|
||||||
push @results, { type => $type, label => $TYPE_LABELS{$type} // $type, ok => $ok, stdout => $stdout // '', stderr => $stderr // '' };
|
|
||||||
} elsif ($type eq 'files') {
|
} elsif ($type eq 'files') {
|
||||||
$opts{path} = $path if $path ne '';
|
$opts{path} = $path if $path ne '';
|
||||||
my ($ok, $stdout, $stderr) = GnizaWHM::Runner::run('restore', 'files', [$account], \%opts);
|
push @commands, ['restore', 'files', [$account], {%opts}];
|
||||||
push @results, { type => $type, label => $TYPE_LABELS{$type} // $type, ok => $ok, stdout => $stdout // '', stderr => $stderr // '' };
|
|
||||||
} elsif ($type eq 'database') {
|
} elsif ($type eq 'database') {
|
||||||
my @dbs;
|
my @dbs;
|
||||||
if ($dbnames ne '' && $dbnames ne '__ALL__') {
|
if ($dbnames ne '' && $dbnames ne '__ALL__') {
|
||||||
@@ -910,12 +908,10 @@ sub handle_step4 {
|
|||||||
}
|
}
|
||||||
if (@dbs) {
|
if (@dbs) {
|
||||||
for my $db (@dbs) {
|
for my $db (@dbs) {
|
||||||
my ($ok, $stdout, $stderr) = GnizaWHM::Runner::run('restore', 'database', [$account, $db], \%opts);
|
push @commands, ['restore', 'database', [$account, $db], {%opts}];
|
||||||
push @results, { type => $type, label => "Database: $db", ok => $ok, stdout => $stdout // '', stderr => $stderr // '' };
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
my ($ok, $stdout, $stderr) = GnizaWHM::Runner::run('restore', 'database', [$account], \%opts);
|
push @commands, ['restore', 'database', [$account], {%opts}];
|
||||||
push @results, { type => $type, label => 'Database: All', ok => $ok, stdout => $stdout // '', stderr => $stderr // '' };
|
|
||||||
}
|
}
|
||||||
} elsif ($type eq 'dbusers') {
|
} elsif ($type eq 'dbusers') {
|
||||||
my @dbus;
|
my @dbus;
|
||||||
@@ -924,12 +920,10 @@ sub handle_step4 {
|
|||||||
}
|
}
|
||||||
if (@dbus) {
|
if (@dbus) {
|
||||||
for my $dbu (@dbus) {
|
for my $dbu (@dbus) {
|
||||||
my ($ok, $stdout, $stderr) = GnizaWHM::Runner::run('restore', 'dbusers', [$account, $dbu], \%opts);
|
push @commands, ['restore', 'dbusers', [$account, $dbu], {%opts}];
|
||||||
push @results, { type => $type, label => "DB User: $dbu", ok => $ok, stdout => $stdout // '', stderr => $stderr // '' };
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
my ($ok, $stdout, $stderr) = GnizaWHM::Runner::run('restore', 'dbusers', [$account], \%opts);
|
push @commands, ['restore', 'dbusers', [$account], {%opts}];
|
||||||
push @results, { type => $type, label => 'Database Users: All', ok => $ok, stdout => $stdout // '', stderr => $stderr // '' };
|
|
||||||
}
|
}
|
||||||
} elsif ($type eq 'mailbox') {
|
} elsif ($type eq 'mailbox') {
|
||||||
my @mbs;
|
my @mbs;
|
||||||
@@ -938,12 +932,10 @@ sub handle_step4 {
|
|||||||
}
|
}
|
||||||
if (@mbs) {
|
if (@mbs) {
|
||||||
for my $mb (@mbs) {
|
for my $mb (@mbs) {
|
||||||
my ($ok, $stdout, $stderr) = GnizaWHM::Runner::run('restore', 'mailbox', [$account, $mb], \%opts);
|
push @commands, ['restore', 'mailbox', [$account, $mb], {%opts}];
|
||||||
push @results, { type => $type, label => "Mailbox: $mb", ok => $ok, stdout => $stdout // '', stderr => $stderr // '' };
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
my ($ok, $stdout, $stderr) = GnizaWHM::Runner::run('restore', 'mailbox', [$account], \%opts);
|
push @commands, ['restore', 'mailbox', [$account], {%opts}];
|
||||||
push @results, { type => $type, label => 'Mailbox: All', ok => $ok, stdout => $stdout // '', stderr => $stderr // '' };
|
|
||||||
}
|
}
|
||||||
} elsif ($type eq 'domains') {
|
} elsif ($type eq 'domains') {
|
||||||
my @doms;
|
my @doms;
|
||||||
@@ -952,12 +944,10 @@ sub handle_step4 {
|
|||||||
}
|
}
|
||||||
if (@doms) {
|
if (@doms) {
|
||||||
for my $dom (@doms) {
|
for my $dom (@doms) {
|
||||||
my ($ok, $stdout, $stderr) = GnizaWHM::Runner::run('restore', 'domains', [$account, $dom], \%opts);
|
push @commands, ['restore', 'domains', [$account, $dom], {%opts}];
|
||||||
push @results, { type => $type, label => "Domain: $dom", ok => $ok, stdout => $stdout // '', stderr => $stderr // '' };
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
my ($ok, $stdout, $stderr) = GnizaWHM::Runner::run('restore', 'domains', [$account], \%opts);
|
push @commands, ['restore', 'domains', [$account], {%opts}];
|
||||||
push @results, { type => $type, label => 'Domains: All', ok => $ok, stdout => $stdout // '', stderr => $stderr // '' };
|
|
||||||
}
|
}
|
||||||
} elsif ($type eq 'ssl') {
|
} elsif ($type eq 'ssl') {
|
||||||
my @certs;
|
my @certs;
|
||||||
@@ -966,52 +956,22 @@ sub handle_step4 {
|
|||||||
}
|
}
|
||||||
if (@certs) {
|
if (@certs) {
|
||||||
for my $cert (@certs) {
|
for my $cert (@certs) {
|
||||||
my ($ok, $stdout, $stderr) = GnizaWHM::Runner::run('restore', 'ssl', [$account, $cert], \%opts);
|
push @commands, ['restore', 'ssl', [$account, $cert], {%opts}];
|
||||||
push @results, { type => $type, label => "SSL: $cert", ok => $ok, stdout => $stdout // '', stderr => $stderr // '' };
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
my ($ok, $stdout, $stderr) = GnizaWHM::Runner::run('restore', 'ssl', [$account], \%opts);
|
push @commands, ['restore', 'ssl', [$account], {%opts}];
|
||||||
push @results, { type => $type, label => 'SSL: All', ok => $ok, stdout => $stdout // '', stderr => $stderr // '' };
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
push @results, { type => $type, label => $TYPE_LABELS{$type} // $type, ok => 0, stdout => '', stderr => 'Invalid restore type' };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
print "Content-Type: text/html\r\n\r\n";
|
my ($ok, $err) = GnizaWHM::Runner::run_async(\@commands);
|
||||||
Whostmgr::HTMLInterface::defheader('GNIZA Backup Manager — Restore', '', '/cgi/gniza-whm/restore.cgi');
|
if ($ok) {
|
||||||
|
GnizaWHM::UI::set_flash('success', 'Restore started in background. Watch progress below.');
|
||||||
print GnizaWHM::UI::page_header('Restore from Backup');
|
print "Status: 302 Found\r\n";
|
||||||
print GnizaWHM::UI::render_nav('restore.cgi');
|
print "Location: logs.cgi\r\n\r\n";
|
||||||
|
|
||||||
# Overall status
|
|
||||||
my $all_ok = !grep { !$_->{ok} } @results;
|
|
||||||
my $any_ok = grep { $_->{ok} } @results;
|
|
||||||
if ($all_ok) {
|
|
||||||
print qq{<div class="alert alert-success mb-4">All restore operations completed successfully.</div>\n};
|
|
||||||
} elsif ($any_ok) {
|
|
||||||
print qq{<div class="alert alert-warning mb-4">Some restore operations failed. See details below.</div>\n};
|
|
||||||
} else {
|
} else {
|
||||||
print qq{<div class="alert alert-error mb-4">All restore operations failed.</div>\n};
|
GnizaWHM::UI::set_flash('error', "Failed to start restore: $err");
|
||||||
|
print "Status: 302 Found\r\n";
|
||||||
|
print "Location: restore.cgi\r\n\r\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
# Per-type output blocks
|
|
||||||
for my $r (@results) {
|
|
||||||
my $esc_label = GnizaWHM::UI::esc($r->{label});
|
|
||||||
my $badge = $r->{ok}
|
|
||||||
? '<span class="badge badge-success badge-sm">OK</span>'
|
|
||||||
: '<span class="badge badge-error badge-sm">Failed</span>';
|
|
||||||
|
|
||||||
print qq{<div class="card bg-white shadow-sm border border-base-300 mb-4">\n<div class="card-body">\n};
|
|
||||||
print qq{<h2 class="card-title text-sm">$esc_label $badge</h2>\n};
|
|
||||||
my $output = $r->{stdout} . ($r->{stderr} =~ /\S/ ? "\n$r->{stderr}" : '');
|
|
||||||
$output = '(no output)' unless $output =~ /\S/;
|
|
||||||
print qq{<pre class="bg-neutral text-neutral-content p-3 rounded-lg text-sm font-mono overflow-x-auto whitespace-pre-wrap">} . GnizaWHM::UI::esc($output) . qq{</pre>\n};
|
|
||||||
print qq{</div>\n</div>\n};
|
|
||||||
}
|
|
||||||
|
|
||||||
print qq{<button type="button" class="btn btn-info btn-sm" onclick="location.href='restore.cgi'">Start New Restore</button>\n};
|
|
||||||
|
|
||||||
print GnizaWHM::UI::page_footer();
|
|
||||||
Whostmgr::HTMLInterface::footer();
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user