774 lines
28 KiB
Perl
774 lines
28 KiB
Perl
#!/usr/local/cpanel/3rdparty/bin/perl
|
|
package Cpanel::AdminBin::Script::Call::Gniza4cp::Restore;
|
|
|
|
use strict;
|
|
use warnings;
|
|
use parent 'Cpanel::AdminBin::Script::Call';
|
|
use IPC::Open3;
|
|
use Symbol 'gensym';
|
|
use POSIX qw(setsid);
|
|
|
|
my $GNIZA4CP_BIN = '/usr/local/bin/gniza4cp';
|
|
my $MAIN_CONFIG = '/etc/gniza4cp/gniza4cp.conf';
|
|
my $REMOTES_DIR = '/etc/gniza4cp/remotes.d';
|
|
|
|
# Argument validation patterns (mirrors Gniza4cpWHM::Runner)
|
|
my %OPT_PATTERNS = (
|
|
remote => qr/^[a-zA-Z0-9_,-]+$/,
|
|
timestamp => qr/^\d{4}-\d{2}-\d{2}T\d{6}$/,
|
|
path => qr/^(?!.*\.\.)[a-zA-Z0-9_.\/@ -]+$/,
|
|
exclude => qr/^[a-zA-Z0-9_.,\/@ *?\[\]-]+$/,
|
|
);
|
|
|
|
my $ACCOUNT_RE = qr/^[a-z][a-z0-9_-]*$/;
|
|
my $REMOTE_RE = qr/^[a-zA-Z0-9_-]+$/;
|
|
my $DBNAME_RE = qr/^[a-zA-Z0-9_]+$/;
|
|
my $EMAIL_RE = qr/^[a-zA-Z0-9._+-]+\@[a-zA-Z0-9._-]+$/;
|
|
my $DOMAIN_RE = qr/^[a-zA-Z0-9._-]+$/;
|
|
my $TS_RE = qr/^\d{4}-\d{2}-\d{2}T\d{6}$/;
|
|
|
|
# ── Allowed remotes for user restore ──────────────────────────
|
|
|
|
sub _get_allowed_remotes {
|
|
my $setting = '';
|
|
if (open my $fh, '<', $MAIN_CONFIG) {
|
|
while (my $line = <$fh>) {
|
|
if ($line =~ /^USER_RESTORE_REMOTES=(?:"([^"]*)"|'([^']*)'|(\S*))$/) {
|
|
$setting = defined $1 ? $1 : (defined $2 ? $2 : ($3 // ''));
|
|
}
|
|
}
|
|
close $fh;
|
|
}
|
|
# Default to "all" if not set
|
|
$setting = 'all' if !defined $setting || $setting eq '';
|
|
|
|
return $setting;
|
|
}
|
|
|
|
sub _list_all_remotes {
|
|
my @remotes;
|
|
if (-d $REMOTES_DIR && opendir my $dh, $REMOTES_DIR) {
|
|
while (my $entry = readdir $dh) {
|
|
if ($entry =~ /^([a-zA-Z0-9_-]+)\.conf$/) {
|
|
push @remotes, $1;
|
|
}
|
|
}
|
|
closedir $dh;
|
|
}
|
|
return sort @remotes;
|
|
}
|
|
|
|
sub _is_remote_allowed {
|
|
my ($remote) = @_;
|
|
my $setting = _get_allowed_remotes();
|
|
return 0 if $setting eq ''; # disabled
|
|
|
|
if ($setting eq 'all') {
|
|
# Check it actually exists
|
|
return -f "$REMOTES_DIR/$remote.conf" ? 1 : 0;
|
|
}
|
|
|
|
my %allowed = map { $_ => 1 } split /,/, $setting;
|
|
return $allowed{$remote} ? 1 : 0;
|
|
}
|
|
|
|
sub _get_filtered_remotes {
|
|
my $setting = _get_allowed_remotes();
|
|
return () if $setting eq '';
|
|
|
|
my @all = _list_all_remotes();
|
|
return @all if $setting eq 'all';
|
|
|
|
my %allowed = map { $_ => 1 } split /,/, $setting;
|
|
return grep { $allowed{$_} } @all;
|
|
}
|
|
|
|
# ── Command execution ─────────────────────────────────────────
|
|
|
|
sub _run_gniza4cp {
|
|
my (@args) = @_;
|
|
|
|
my $err_fh = gensym;
|
|
my ($in, $out);
|
|
my $pid = eval { open3($in, $out, $err_fh, $GNIZA4CP_BIN, @args) };
|
|
unless ($pid) {
|
|
return (0, '', "Failed to execute gniza4cp: $@");
|
|
}
|
|
close $in if $in;
|
|
|
|
my $stdout = do { local $/; <$out> } // '';
|
|
my $stderr = do { local $/; <$err_fh> } // '';
|
|
close $out;
|
|
close $err_fh;
|
|
|
|
waitpid($pid, 0);
|
|
my $exit_code = $? >> 8;
|
|
|
|
return ($exit_code == 0, $stdout, $stderr);
|
|
}
|
|
|
|
# ── Action dispatch ───────────────────────────────────────────
|
|
|
|
# ── Per-user activity logging ─────────────────────────────────
|
|
|
|
my $ACTIVITY_ENTRY_RE = qr/^[0-9]+$/;
|
|
|
|
sub _get_log_dir {
|
|
my $log_dir = '/var/log/gniza4cp';
|
|
if (open my $fh, '<', $MAIN_CONFIG) {
|
|
while (my $line = <$fh>) {
|
|
if ($line =~ /^LOG_DIR=(?:"([^"]*)"|'([^']*)'|(\S*))$/) {
|
|
my $val = defined $1 ? $1 : (defined $2 ? $2 : ($3 // ''));
|
|
$log_dir = $val if $val ne '';
|
|
}
|
|
}
|
|
close $fh;
|
|
}
|
|
return $log_dir;
|
|
}
|
|
|
|
sub _activity_log_path {
|
|
my ($user) = @_;
|
|
my $log_dir = _get_log_dir();
|
|
return "$log_dir/cpanel-$user.log";
|
|
}
|
|
|
|
my %ACTION_LABELS = (
|
|
RESTORE_ACCOUNT => 'Full Account',
|
|
RESTORE_FILES => 'Home Directory',
|
|
RESTORE_DATABASE => 'Database',
|
|
RESTORE_MAILBOX => 'Email',
|
|
RESTORE_CRON => 'Cron Jobs',
|
|
RESTORE_DBUSERS => 'DB Users',
|
|
RESTORE_DOMAINS => 'Domains',
|
|
RESTORE_SSL => 'SSL Certificates',
|
|
);
|
|
|
|
sub _log_activity {
|
|
my ($user, $action, $details, $status, $output) = @_;
|
|
my $log_file = _activity_log_path($user);
|
|
my $log_dir = _get_log_dir();
|
|
mkdir $log_dir, 0700 unless -d $log_dir;
|
|
|
|
my @t = gmtime(time);
|
|
my $ts = sprintf('%04d-%02d-%02d %02d:%02d:%02d',
|
|
$t[5]+1900, $t[4]+1, $t[3], $t[2], $t[1], $t[0]);
|
|
my $label = $ACTION_LABELS{$action} // $action;
|
|
|
|
if (open my $fh, '>>', $log_file) {
|
|
print $fh "--- ENTRY ---\n";
|
|
print $fh "Date: $ts\n";
|
|
print $fh "Action: $label\n";
|
|
print $fh "Details: $details\n";
|
|
print $fh "Status: $status\n";
|
|
print $fh $output if defined $output && $output ne '';
|
|
print $fh "--- END ---\n";
|
|
close $fh;
|
|
}
|
|
}
|
|
|
|
sub _actions {
|
|
return qw(
|
|
LIST_ALLOWED_REMOTES
|
|
LIST_SNAPSHOTS
|
|
LIST_DATABASES
|
|
LIST_MAILBOXES
|
|
LIST_FILES
|
|
LIST_DBUSERS
|
|
LIST_CRON
|
|
LIST_DNS
|
|
LIST_SSL
|
|
LIST_LOGS
|
|
GET_LOG
|
|
START_RESTORE
|
|
RESTORE_ACCOUNT
|
|
RESTORE_FILES
|
|
RESTORE_DATABASE
|
|
RESTORE_MAILBOX
|
|
RESTORE_CRON
|
|
RESTORE_DBUSERS
|
|
RESTORE_DOMAINS
|
|
RESTORE_SSL
|
|
);
|
|
}
|
|
|
|
sub LIST_LOGS {
|
|
my ($self) = @_;
|
|
my $user = $self->get_caller_username() // '';
|
|
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
|
|
|
|
my $log_file = _activity_log_path($user);
|
|
return '' unless -f $log_file && !-l $log_file;
|
|
|
|
# Parse entries from the activity log (newest first)
|
|
my @entries;
|
|
if (open my $fh, '<', $log_file) {
|
|
my $in_entry = 0;
|
|
my %cur;
|
|
while (my $line = <$fh>) {
|
|
chomp $line;
|
|
if ($line eq '--- ENTRY ---') {
|
|
$in_entry = 1;
|
|
%cur = ();
|
|
} elsif ($line eq '--- END ---' && $in_entry) {
|
|
push @entries, { %cur } if $cur{date};
|
|
$in_entry = 0;
|
|
} elsif ($in_entry) {
|
|
if ($line =~ /^Date:\s+(.+)$/) { $cur{date} = $1; }
|
|
elsif ($line =~ /^Action:\s+(.+)$/) { $cur{action} = $1; }
|
|
elsif ($line =~ /^Details:\s+(.+)$/) { $cur{details} = $1; }
|
|
elsif ($line =~ /^Status:\s+(.+)$/) { $cur{status} = $1; }
|
|
}
|
|
}
|
|
close $fh;
|
|
}
|
|
|
|
# Return newest first, one line per entry: index\tdate\taction\tdetails\tstatus
|
|
my @lines;
|
|
for my $i (reverse 0 .. $#entries) {
|
|
my $e = $entries[$i];
|
|
push @lines, join("\t", $i, $e->{date} // '', $e->{action} // '',
|
|
$e->{details} // '', $e->{status} // '');
|
|
}
|
|
return join("\n", @lines);
|
|
}
|
|
|
|
sub GET_LOG {
|
|
my ($self, $entry_idx) = @_;
|
|
my $user = $self->get_caller_username() // '';
|
|
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
|
|
return "ERROR: Invalid entry" unless defined $entry_idx && $entry_idx =~ $ACTIVITY_ENTRY_RE;
|
|
|
|
my $log_file = _activity_log_path($user);
|
|
return "ERROR: No activity log" unless -f $log_file && !-l $log_file;
|
|
|
|
# Parse the Nth entry
|
|
my $idx = int($entry_idx);
|
|
my @entries;
|
|
if (open my $fh, '<', $log_file) {
|
|
my $in_entry = 0;
|
|
my @cur_lines;
|
|
while (my $line = <$fh>) {
|
|
chomp $line;
|
|
if ($line eq '--- ENTRY ---') {
|
|
$in_entry = 1;
|
|
@cur_lines = ();
|
|
} elsif ($line eq '--- END ---' && $in_entry) {
|
|
push @entries, join("\n", @cur_lines);
|
|
$in_entry = 0;
|
|
} elsif ($in_entry) {
|
|
push @cur_lines, $line;
|
|
}
|
|
}
|
|
close $fh;
|
|
}
|
|
|
|
return "ERROR: Entry not found" if $idx < 0 || $idx > $#entries;
|
|
return $entries[$idx];
|
|
}
|
|
|
|
sub LIST_ALLOWED_REMOTES {
|
|
my ($self) = @_;
|
|
my @remotes = _get_filtered_remotes();
|
|
return join("\n", @remotes);
|
|
}
|
|
|
|
sub LIST_SNAPSHOTS {
|
|
my ($self, $remote) = @_;
|
|
my $user = $self->get_caller_username() // '';
|
|
|
|
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
|
|
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
|
|
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
|
|
|
|
my ($ok, $stdout, $stderr) = _run_gniza4cp('list', "--remote=$remote", "--account=$user");
|
|
return $ok ? $stdout : "ERROR: $stderr";
|
|
}
|
|
|
|
sub LIST_DATABASES {
|
|
my ($self, $remote, $timestamp) = @_;
|
|
my $user = $self->get_caller_username() // '';
|
|
|
|
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
|
|
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
|
|
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
|
|
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
|
|
|
|
my ($ok, $stdout, $stderr) = _run_gniza4cp('restore', 'list-databases', $user,
|
|
"--remote=$remote", "--timestamp=$timestamp");
|
|
return $ok ? $stdout : "ERROR: $stderr";
|
|
}
|
|
|
|
sub LIST_MAILBOXES {
|
|
my ($self, $remote, $timestamp) = @_;
|
|
my $user = $self->get_caller_username() // '';
|
|
|
|
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
|
|
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
|
|
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
|
|
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
|
|
|
|
my ($ok, $stdout, $stderr) = _run_gniza4cp('restore', 'list-mailboxes', $user,
|
|
"--remote=$remote", "--timestamp=$timestamp");
|
|
return $ok ? $stdout : "ERROR: $stderr";
|
|
}
|
|
|
|
sub LIST_FILES {
|
|
my ($self, $remote, $timestamp, $path) = @_;
|
|
my $user = $self->get_caller_username() // '';
|
|
|
|
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
|
|
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
|
|
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
|
|
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
|
|
|
|
my @opts = ("--remote=$remote", "--timestamp=$timestamp");
|
|
if (defined $path && $path ne '') {
|
|
return "ERROR: Invalid path" unless $path =~ $OPT_PATTERNS{path};
|
|
push @opts, "--path=$path";
|
|
}
|
|
|
|
my ($ok, $stdout, $stderr) = _run_gniza4cp('restore', 'list-files', $user, @opts);
|
|
return $ok ? $stdout : "ERROR: $stderr";
|
|
}
|
|
|
|
sub LIST_DBUSERS {
|
|
my ($self, $remote, $timestamp) = @_;
|
|
my $user = $self->get_caller_username() // '';
|
|
|
|
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
|
|
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
|
|
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
|
|
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
|
|
|
|
my ($ok, $stdout, $stderr) = _run_gniza4cp('restore', 'list-dbusers', $user,
|
|
"--remote=$remote", "--timestamp=$timestamp");
|
|
return $ok ? $stdout : "ERROR: $stderr";
|
|
}
|
|
|
|
sub LIST_CRON {
|
|
my ($self, $remote, $timestamp) = @_;
|
|
my $user = $self->get_caller_username() // '';
|
|
|
|
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
|
|
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
|
|
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
|
|
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
|
|
|
|
my ($ok, $stdout, $stderr) = _run_gniza4cp('restore', 'list-cron', $user,
|
|
"--remote=$remote", "--timestamp=$timestamp");
|
|
return $ok ? $stdout : "ERROR: $stderr";
|
|
}
|
|
|
|
sub LIST_DNS {
|
|
my ($self, $remote, $timestamp) = @_;
|
|
my $user = $self->get_caller_username() // '';
|
|
|
|
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
|
|
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
|
|
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
|
|
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
|
|
|
|
my ($ok, $stdout, $stderr) = _run_gniza4cp('restore', 'list-dns', $user,
|
|
"--remote=$remote", "--timestamp=$timestamp");
|
|
return $ok ? $stdout : "ERROR: $stderr";
|
|
}
|
|
|
|
sub LIST_SSL {
|
|
my ($self, $remote, $timestamp) = @_;
|
|
my $user = $self->get_caller_username() // '';
|
|
|
|
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
|
|
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
|
|
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
|
|
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
|
|
|
|
my ($ok, $stdout, $stderr) = _run_gniza4cp('restore', 'list-ssl', $user,
|
|
"--remote=$remote", "--timestamp=$timestamp");
|
|
return $ok ? $stdout : "ERROR: $stderr";
|
|
}
|
|
|
|
# ── Background restore ────────────────────────────────────────
|
|
#
|
|
# START_RESTORE($remote, $timestamp, $types_str, $path, $exclude_paths)
|
|
#
|
|
# $types_str encodes selected types and items, semicolon-separated:
|
|
# account;files;database:db1,db2;mailbox:a@b.com;cron;dbusers:u1;domains:d1;ssl:d1
|
|
#
|
|
# Validates all inputs, forks a detached child that runs each gniza4cp
|
|
# restore command and logs results via _log_activity(), then returns
|
|
# immediately with "OK".
|
|
|
|
my %TYPE_ITEM_RE = (
|
|
account => undef, # no items
|
|
files => undef, # uses $path arg instead
|
|
cron => undef, # no items
|
|
database => $DBNAME_RE,
|
|
dbusers => $DBNAME_RE,
|
|
mailbox => $EMAIL_RE,
|
|
domains => $DOMAIN_RE,
|
|
ssl => $DOMAIN_RE,
|
|
);
|
|
|
|
# Map type + item to gniza4cp CLI arguments (excluding --remote/--timestamp which are always added)
|
|
sub _build_restore_args {
|
|
my ($type, $user, $item, $path, $exclude) = @_;
|
|
if ($type eq 'account') {
|
|
my @args = ('restore', 'account', $user);
|
|
push @args, "--exclude=$exclude" if defined $exclude && $exclude ne '';
|
|
return @args;
|
|
}
|
|
elsif ($type eq 'files') {
|
|
my @args = ('restore', 'files', $user);
|
|
push @args, "--path=$path" if defined $path && $path ne '';
|
|
push @args, "--exclude=$exclude" if defined $exclude && $exclude ne '';
|
|
return @args;
|
|
}
|
|
elsif ($type eq 'cron') {
|
|
return ('restore', 'cron', $user);
|
|
}
|
|
elsif ($type eq 'database') {
|
|
my @args = ('restore', 'database', $user);
|
|
push @args, $item if defined $item && $item ne '';
|
|
return @args;
|
|
}
|
|
elsif ($type eq 'dbusers') {
|
|
my @args = ('restore', 'dbusers', $user);
|
|
push @args, $item if defined $item && $item ne '';
|
|
return @args;
|
|
}
|
|
elsif ($type eq 'mailbox') {
|
|
my @args = ('restore', 'mailbox', $user);
|
|
push @args, $item if defined $item && $item ne '';
|
|
return @args;
|
|
}
|
|
elsif ($type eq 'domains') {
|
|
my @args = ('restore', 'domains', $user);
|
|
push @args, $item if defined $item && $item ne '';
|
|
return @args;
|
|
}
|
|
elsif ($type eq 'ssl') {
|
|
my @args = ('restore', 'ssl', $user);
|
|
push @args, $item if defined $item && $item ne '';
|
|
return @args;
|
|
}
|
|
return ();
|
|
}
|
|
|
|
# Map type to RESTORE_* action name for _log_activity
|
|
my %TYPE_ACTION_MAP = (
|
|
account => 'RESTORE_ACCOUNT',
|
|
files => 'RESTORE_FILES',
|
|
database => 'RESTORE_DATABASE',
|
|
dbusers => 'RESTORE_DBUSERS',
|
|
mailbox => 'RESTORE_MAILBOX',
|
|
cron => 'RESTORE_CRON',
|
|
domains => 'RESTORE_DOMAINS',
|
|
ssl => 'RESTORE_SSL',
|
|
);
|
|
|
|
sub START_RESTORE {
|
|
my ($self, $remote, $timestamp, $types_str, $path, $exclude_paths) = @_;
|
|
my $user = $self->get_caller_username() // '';
|
|
|
|
# ── Validate common args ──
|
|
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
|
|
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
|
|
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
|
|
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
|
|
|
|
if (defined $path && $path ne '') {
|
|
return "ERROR: Invalid path" unless $path =~ $OPT_PATTERNS{path};
|
|
}
|
|
if (defined $exclude_paths && $exclude_paths ne '') {
|
|
return "ERROR: Invalid exclude" unless $exclude_paths =~ $OPT_PATTERNS{exclude};
|
|
}
|
|
|
|
# ── Parse and validate types_str ──
|
|
$types_str //= '';
|
|
return "ERROR: No restore types specified" if $types_str eq '';
|
|
|
|
my @jobs; # [ { type => ..., items => [...] }, ... ]
|
|
for my $part (split /;/, $types_str) {
|
|
next if $part eq '';
|
|
my ($type, $items_str) = split /:/, $part, 2;
|
|
|
|
return "ERROR: Invalid restore type: $type" unless exists $TYPE_ITEM_RE{$type};
|
|
|
|
my $item_re = $TYPE_ITEM_RE{$type};
|
|
my @items;
|
|
|
|
if (defined $items_str && $items_str ne '') {
|
|
return "ERROR: Type '$type' does not accept items" unless defined $item_re;
|
|
for my $item (split /,/, $items_str) {
|
|
next if $item eq '';
|
|
return "ERROR: Invalid item for $type: $item" unless $item =~ $item_re;
|
|
push @items, $item;
|
|
return "ERROR: Too many items for $type (max 100)" if @items > 100;
|
|
}
|
|
}
|
|
|
|
push @jobs, { type => $type, items => \@items };
|
|
}
|
|
|
|
return "ERROR: No valid restore types parsed" unless @jobs;
|
|
|
|
# ── Pre-build all command arg lists to validate before forking ──
|
|
my @cmd_list; # [ { args => [...], action => ..., details => ... }, ... ]
|
|
for my $job (@jobs) {
|
|
my $type = $job->{type};
|
|
my @items = @{$job->{items}};
|
|
|
|
if (@items) {
|
|
# One command per item
|
|
for my $item (@items) {
|
|
my @args = _build_restore_args($type, $user, $item, $path, $exclude_paths);
|
|
return "ERROR: Failed to build command for $type" unless @args;
|
|
my $details = "remote=$remote snapshot=$timestamp";
|
|
if ($type eq 'database') { $details .= " database=$item"; }
|
|
elsif ($type eq 'dbusers') { $details .= " dbuser=$item"; }
|
|
elsif ($type eq 'mailbox') { $details .= " email=$item"; }
|
|
elsif ($type eq 'domains') { $details .= " domain=$item"; }
|
|
elsif ($type eq 'ssl') { $details .= " domain=$item"; }
|
|
push @cmd_list, { args => \@args, action => $TYPE_ACTION_MAP{$type}, details => $details };
|
|
}
|
|
} else {
|
|
# Single command for this type
|
|
my @args = _build_restore_args($type, $user, '', $path, $exclude_paths);
|
|
return "ERROR: Failed to build command for $type" unless @args;
|
|
my $details = "remote=$remote snapshot=$timestamp";
|
|
$details .= " path=$path" if $type eq 'files' && defined $path && $path ne '';
|
|
$details .= " exclude=$exclude_paths" if defined $exclude_paths && $exclude_paths ne '';
|
|
push @cmd_list, { args => \@args, action => $TYPE_ACTION_MAP{$type}, details => $details };
|
|
}
|
|
}
|
|
|
|
# ── Fork detached child ──
|
|
local $SIG{CHLD} = 'IGNORE';
|
|
my $pid = fork();
|
|
return "ERROR: Fork failed: $!" unless defined $pid;
|
|
|
|
if ($pid == 0) {
|
|
# Child: detach from parent completely
|
|
eval {
|
|
setsid();
|
|
open STDIN, '<', '/dev/null';
|
|
open STDOUT, '>', '/dev/null';
|
|
open STDERR, '>', '/dev/null';
|
|
|
|
for my $cmd (@cmd_list) {
|
|
my @full_args = (@{$cmd->{args}}, "--remote=$remote", "--timestamp=$timestamp");
|
|
my ($ok, $stdout, $stderr) = _run_gniza4cp(@full_args);
|
|
_log_activity($user, $cmd->{action}, $cmd->{details},
|
|
$ok ? 'OK' : 'Error', $ok ? $stdout : $stderr);
|
|
}
|
|
};
|
|
# Ensure child exits even on error
|
|
POSIX::_exit(0);
|
|
}
|
|
|
|
# Parent: return immediately
|
|
return "OK";
|
|
}
|
|
|
|
sub RESTORE_ACCOUNT {
|
|
my ($self, $remote, $timestamp, $exclude) = @_;
|
|
my $user = $self->get_caller_username() // '';
|
|
|
|
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
|
|
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
|
|
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
|
|
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
|
|
|
|
my @opts = ("--remote=$remote", "--timestamp=$timestamp");
|
|
# NOTE: --terminate is NEVER passed for user restore
|
|
if (defined $exclude && $exclude ne '') {
|
|
return "ERROR: Invalid exclude" unless $exclude =~ $OPT_PATTERNS{exclude};
|
|
push @opts, "--exclude=$exclude";
|
|
}
|
|
|
|
my $details = "remote=$remote snapshot=$timestamp";
|
|
$details .= " exclude=$exclude" if defined $exclude && $exclude ne '';
|
|
|
|
my ($ok, $stdout, $stderr) = _run_gniza4cp('restore', 'account', $user, @opts);
|
|
_log_activity($user, 'RESTORE_ACCOUNT', $details,
|
|
$ok ? 'OK' : 'Error', $ok ? $stdout : $stderr);
|
|
return $ok ? "OK\n$stdout" : "ERROR: $stderr";
|
|
}
|
|
|
|
sub RESTORE_FILES {
|
|
my ($self, $remote, $timestamp, $path, $exclude) = @_;
|
|
my $user = $self->get_caller_username() // '';
|
|
|
|
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
|
|
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
|
|
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
|
|
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
|
|
|
|
my @opts = ("--remote=$remote", "--timestamp=$timestamp");
|
|
if (defined $path && $path ne '') {
|
|
return "ERROR: Invalid path" unless $path =~ $OPT_PATTERNS{path};
|
|
push @opts, "--path=$path";
|
|
}
|
|
if (defined $exclude && $exclude ne '') {
|
|
return "ERROR: Invalid exclude" unless $exclude =~ $OPT_PATTERNS{exclude};
|
|
push @opts, "--exclude=$exclude";
|
|
}
|
|
|
|
my $details = "remote=$remote snapshot=$timestamp";
|
|
$details .= " path=$path" if defined $path && $path ne '';
|
|
$details .= " exclude=$exclude" if defined $exclude && $exclude ne '';
|
|
|
|
my ($ok, $stdout, $stderr) = _run_gniza4cp('restore', 'files', $user, @opts);
|
|
_log_activity($user, 'RESTORE_FILES', $details,
|
|
$ok ? 'OK' : 'Error', $ok ? $stdout : $stderr);
|
|
return $ok ? "OK\n$stdout" : "ERROR: $stderr";
|
|
}
|
|
|
|
sub RESTORE_DATABASE {
|
|
my ($self, $remote, $timestamp, $dbname) = @_;
|
|
my $user = $self->get_caller_username() // '';
|
|
|
|
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
|
|
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
|
|
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
|
|
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
|
|
|
|
my @args = ($user);
|
|
if (defined $dbname && $dbname ne '') {
|
|
return "ERROR: Invalid database name" unless $dbname =~ $DBNAME_RE;
|
|
push @args, $dbname;
|
|
}
|
|
|
|
my $details = "remote=$remote snapshot=$timestamp";
|
|
$details .= " database=$dbname" if defined $dbname && $dbname ne '';
|
|
|
|
my ($ok, $stdout, $stderr) = _run_gniza4cp('restore', 'database', @args,
|
|
"--remote=$remote", "--timestamp=$timestamp");
|
|
_log_activity($user, 'RESTORE_DATABASE', $details,
|
|
$ok ? 'OK' : 'Error', $ok ? $stdout : $stderr);
|
|
return $ok ? "OK\n$stdout" : "ERROR: $stderr";
|
|
}
|
|
|
|
sub RESTORE_MAILBOX {
|
|
my ($self, $remote, $timestamp, $email) = @_;
|
|
my $user = $self->get_caller_username() // '';
|
|
|
|
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
|
|
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
|
|
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
|
|
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
|
|
|
|
my @args = ($user);
|
|
if (defined $email && $email ne '') {
|
|
return "ERROR: Invalid email" unless $email =~ $EMAIL_RE;
|
|
push @args, $email;
|
|
}
|
|
|
|
my $details = "remote=$remote snapshot=$timestamp";
|
|
$details .= " email=$email" if defined $email && $email ne '';
|
|
|
|
my ($ok, $stdout, $stderr) = _run_gniza4cp('restore', 'mailbox', @args,
|
|
"--remote=$remote", "--timestamp=$timestamp");
|
|
_log_activity($user, 'RESTORE_MAILBOX', $details,
|
|
$ok ? 'OK' : 'Error', $ok ? $stdout : $stderr);
|
|
return $ok ? "OK\n$stdout" : "ERROR: $stderr";
|
|
}
|
|
|
|
sub RESTORE_CRON {
|
|
my ($self, $remote, $timestamp) = @_;
|
|
my $user = $self->get_caller_username() // '';
|
|
|
|
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
|
|
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
|
|
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
|
|
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
|
|
|
|
my $details = "remote=$remote snapshot=$timestamp";
|
|
|
|
my ($ok, $stdout, $stderr) = _run_gniza4cp('restore', 'cron', $user,
|
|
"--remote=$remote", "--timestamp=$timestamp");
|
|
_log_activity($user, 'RESTORE_CRON', $details,
|
|
$ok ? 'OK' : 'Error', $ok ? $stdout : $stderr);
|
|
return $ok ? "OK\n$stdout" : "ERROR: $stderr";
|
|
}
|
|
|
|
sub RESTORE_DBUSERS {
|
|
my ($self, $remote, $timestamp, $dbuser) = @_;
|
|
my $user = $self->get_caller_username() // '';
|
|
|
|
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
|
|
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
|
|
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
|
|
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
|
|
|
|
my @args = ($user);
|
|
if (defined $dbuser && $dbuser ne '') {
|
|
return "ERROR: Invalid database user" unless $dbuser =~ $DBNAME_RE;
|
|
push @args, $dbuser;
|
|
}
|
|
|
|
my $details = "remote=$remote snapshot=$timestamp";
|
|
$details .= " dbuser=$dbuser" if defined $dbuser && $dbuser ne '';
|
|
|
|
my ($ok, $stdout, $stderr) = _run_gniza4cp('restore', 'dbusers', @args,
|
|
"--remote=$remote", "--timestamp=$timestamp");
|
|
_log_activity($user, 'RESTORE_DBUSERS', $details,
|
|
$ok ? 'OK' : 'Error', $ok ? $stdout : $stderr);
|
|
return $ok ? "OK\n$stdout" : "ERROR: $stderr";
|
|
}
|
|
|
|
sub RESTORE_DOMAINS {
|
|
my ($self, $remote, $timestamp, $domain) = @_;
|
|
my $user = $self->get_caller_username() // '';
|
|
|
|
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
|
|
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
|
|
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
|
|
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
|
|
|
|
my @args = ($user);
|
|
if (defined $domain && $domain ne '') {
|
|
return "ERROR: Invalid domain" unless $domain =~ $DOMAIN_RE;
|
|
push @args, $domain;
|
|
}
|
|
|
|
my $details = "remote=$remote snapshot=$timestamp";
|
|
$details .= " domain=$domain" if defined $domain && $domain ne '';
|
|
|
|
my ($ok, $stdout, $stderr) = _run_gniza4cp('restore', 'domains', @args,
|
|
"--remote=$remote", "--timestamp=$timestamp");
|
|
_log_activity($user, 'RESTORE_DOMAINS', $details,
|
|
$ok ? 'OK' : 'Error', $ok ? $stdout : $stderr);
|
|
return $ok ? "OK\n$stdout" : "ERROR: $stderr";
|
|
}
|
|
|
|
sub RESTORE_SSL {
|
|
my ($self, $remote, $timestamp, $domain) = @_;
|
|
my $user = $self->get_caller_username() // '';
|
|
|
|
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
|
|
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
|
|
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
|
|
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
|
|
|
|
my @args = ($user);
|
|
if (defined $domain && $domain ne '') {
|
|
return "ERROR: Invalid domain" unless $domain =~ $DOMAIN_RE;
|
|
push @args, $domain;
|
|
}
|
|
|
|
my $details = "remote=$remote snapshot=$timestamp";
|
|
$details .= " domain=$domain" if defined $domain && $domain ne '';
|
|
|
|
my ($ok, $stdout, $stderr) = _run_gniza4cp('restore', 'ssl', @args,
|
|
"--remote=$remote", "--timestamp=$timestamp");
|
|
_log_activity($user, 'RESTORE_SSL', $details,
|
|
$ok ? 'OK' : 'Error', $ok ? $stdout : $stderr);
|
|
return $ok ? "OK\n$stdout" : "ERROR: $stderr";
|
|
}
|
|
|
|
__PACKAGE__->run() if !caller;
|
|
|
|
1;
|