File: //var/www/html/install_smartbackup.sh
#!/bin/bash
# ==============================================================================
# SMART BACKUP INSTALLER - AUTOMATED DEPLOYMENT
# Features: Single Job, Auto Config Switch, Telegram Alerts, Weekly Toggle
# Updated: Handle BACKUP_WEEKLY_ENABLE & FORCE CRON EXECUTION
# ==============================================================================
echo ">>> [1/7] Installing Perl Dependencies..."
/usr/local/cpanel/scripts/perlinstaller JSON YAML::Syck LWP::UserAgent
if [ $? -ne 0 ]; then
echo "ERROR: Failed to install Perl modules."
exit 1
fi
echo ">>> [2/7] Creating Directories..."
mkdir -p /usr/local/cpanel/whostmgr/docroot/cgi/smartbackup
mkdir -p /scripts
# ==============================================================================
# FILE 1: WHM PLUGIN APPCONFIG
# ==============================================================================
echo ">>> [3/7] Generating Plugin Config..."
cat << 'EOF' > /var/cpanel/apps/smartbackup.conf
# name: Smart Backup & Alert
name=SmartBackupAlert
service=whostmgr
url=/cgi/smartbackup/index.cgi
acls=all
displayname=Smart Backup & Telegram Alert
entryurl=smartbackup/index.cgi
EOF
# ==============================================================================
# FILE 2: USER INTERFACE (index.cgi) - SINGLE JOB
# ==============================================================================
echo ">>> [4/7] Generating UI Script (index.cgi)..."
cat << 'EOF' > /usr/local/cpanel/whostmgr/docroot/cgi/smartbackup/index.cgi
#!/usr/local/cpanel/3rdparty/bin/perl
use strict;
use warnings;
use CGI;
use JSON;
use YAML::Syck;
use Whostmgr::ACLS ();
my $config_file = '/etc/smartbackup_config.json';
my $cp_dest_dir = -d '/var/cpanel/backups/destinations' ? '/var/cpanel/backups/destinations' : '/var/cpanel/backups';
Whostmgr::ACLS::init_acls();
if (!Whostmgr::ACLS::hasroot()) { print "Content-type: text/html\r\n\r\nAccess Denied"; exit; }
my $q = CGI->new;
if ($q->param('save_config')) {
my %conf = (
telegram_token => $q->param('telegram_token'),
telegram_chat_id => $q->param('telegram_chat_id'),
job_dest_id => $q->param('job_dest_id'),
job_dates => $q->param('job_dates'),
is_active => $q->param('is_active') ? 1 : 0,
);
save_json(\%conf);
print $q->redirect(-uri => '/cgi/smartbackup/index.cgi?saved=1');
exit;
}
my $current_conf = load_json();
my $destinations = get_cpanel_destinations();
my $msg = $q->param('saved') ? '<div class="alert success">Configuration Saved Successfully!</div>' : '';
print "Content-type: text/html; charset=utf-8\r\n\r\n";
print <<HTML;
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Smart Backup Manager</title>
<link rel="stylesheet" href="/cPanel_magic_revision_0/goog-goog-goog/combined_std.css">
<style>
body { background: #fff; font-family: sans-serif; padding: 20px; }
.container { max-width: 650px; margin: 0 auto; }
.card { border: 1px solid #ddd; padding: 20px; border-radius: 5px; background: #f9f9f9; }
.form-group { margin-bottom: 15px; }
label { font-weight: bold; display: block; margin-bottom: 5px; }
input[type="text"], select { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; }
.btn-save { background: #28a745; color: white; padding: 10px 20px; border: none; cursor: pointer; width: 100%; font-weight: bold; }
.alert { padding: 10px; background: #d4edda; color: #155724; margin-bottom: 15px; border-radius: 4px; }
</style>
</head>
<body>
<div class="container">
<h3 style="text-align: center;">Smart Backup Automation</h3>
$msg
<div class="card">
<form method="post" action="index.cgi">
<input type="hidden" name="save_config" value="1">
<h4>1. Telegram Settings</h4>
<div class="form-group"><label>Bot Token</label><input type="text" name="telegram_token" value="$current_conf->{telegram_token}"></div>
<div class="form-group"><label>Chat ID</label><input type="text" name="telegram_chat_id" value="$current_conf->{telegram_chat_id}"></div>
<hr>
<h4>2. Remote Backup Schedule</h4>
<div class="form-group">
<label>Remote Destination:</label>
<select name="job_dest_id"><option value="">-- Select Server --</option>@{[ build_options($destinations, $current_conf->{job_dest_id}) ]}</select>
</div>
<div class="form-group">
<label>Run Dates (comma separated):</label>
<input type="text" name="job_dates" value="$current_conf->{job_dates}" placeholder="e.g.: 15, 30">
</div>
<div class="form-group" style="margin-top:20px">
<label><input type="checkbox" name="is_active" @{[ $current_conf->{is_active} ? 'checked' : '' ]}> Enable Automation</label>
</div>
<button type="submit" class="btn-save">SAVE CONFIG</button>
</form>
</div>
</div>
</body>
</html>
HTML
sub get_cpanel_destinations {
opendir(my $dh, $cp_dest_dir) || return {};
my @files = grep { /(\.yaml|\.backup_destination)$/i } readdir($dh);
closedir $dh;
my %dests;
foreach my $file (@files) {
my $path = "$cp_dest_dir/$file";
my $data = eval { YAML::Syck::LoadFile($path) };
next if !$data;
my $id = $data->{id};
if (!$id) { $id = $file; $id =~ s/(\.yaml|\.backup_destination)$//i; }
$dests{$id} = $data->{name} || $id;
}
return \%dests;
}
sub build_options {
my ($dests, $selected) = @_;
my $html = '';
foreach my $id (sort keys %$dests) {
my $sel = ($selected && $selected eq $id) ? 'selected' : '';
$html .= qq{<option value="$id" $sel>$dests->{$id}</option>};
}
return $html;
}
sub load_json {
if (-e $config_file) { open my $fh, '<', $config_file or return {}; local $/; return decode_json(<$fh>); }
return { is_active => 1 };
}
sub save_json {
my ($data) = @_; open my $fh, '>', $config_file; print $fh encode_json($data); close $fh;
}
EOF
# ==============================================================================
# FILE 3: CRON SCRIPT (The Opener) - Config Switcher & Force Cron
# ==============================================================================
echo ">>> [5/7] Generating Cron Script (smart_backup_config.pl)..."
cat << 'EOF' > /scripts/smart_backup_config.pl
#!/usr/local/cpanel/3rdparty/bin/perl
use strict;
use warnings;
use Cpanel::Config::LoadCpConf ();
use YAML::Syck;
use POSIX qw(strftime);
use JSON;
use LWP::UserAgent;
use Sys::Hostname;
my $ui_config_file = '/etc/smartbackup_config.json';
my $cp_conf_file = '/var/cpanel/backups/config';
my $cp_backup_lock = '/var/cpanel/backups/lock';
my $dest_dir = -d '/var/cpanel/backups/destinations' ? '/var/cpanel/backups/destinations' : '/var/cpanel/backups';
if (-e $cp_backup_lock) { exit 0; } # Safety Lock
if (! -e $ui_config_file) { exit 0; }
my $conf = load_json_config($ui_config_file);
exit 0 unless $conf->{is_active};
my $today_date = int(strftime "%d", localtime);
my $is_remote_day = 0;
my %dests_status;
if ($conf->{job_dest_id}) {
$dests_status{ $conf->{job_dest_id} } = 0; # Default Disable
if (check_date_match($today_date, $conf->{job_dates})) {
$dests_status{ $conf->{job_dest_id} } = 1; # Enable
$is_remote_day = 1;
}
}
apply_destinations_status(\%dests_status);
if ($is_remote_day) {
# MODE REMOTE: Compressed + No Local Retain + No Weekly + FORCE CRON
update_cpanel_config('compressed', 0, 'no');
# Inject --force into root crontab for /usr/local/cpanel/bin/backup
modify_system_cron(1);
send_telegram($conf, "š <b>SmartBackup Notification</b>\nš
Date Match: <b>$today_date</b>\nš Mode: <b>REMOTE (Compressed / No Local)</b>\nš« Weekly Backup: <b>Disabled</b>\nā” System Cron: <b>Added --force</b>\nā
Destination Enabled.");
} else {
# MODE LOCAL: Incremental + Keep Local + Yes Weekly
update_cpanel_config('incremental', 1, 'yes');
# Ensure --force is removed if it stuck
modify_system_cron(0);
}
sub send_telegram {
my ($conf, $msg) = @_;
return unless ($conf->{telegram_token} && $conf->{telegram_chat_id});
$msg .= "\nš„ Server: " . hostname;
my $ua = LWP::UserAgent->new; $ua->timeout(10);
$ua->post("https://api.telegram.org/bot$conf->{telegram_token}/sendMessage", { chat_id => $conf->{telegram_chat_id}, text => $msg, parse_mode => 'HTML' });
}
sub check_date_match {
my ($today, $date_str) = @_; return 0 unless $date_str;
my @dates = split /[\s,]+/, $date_str;
foreach my $d (@dates) { return 1 if ($d == $today); }
return 0;
}
sub apply_destinations_status {
my ($target_map) = @_;
opendir(my $dh, $dest_dir) || die $!;
my @files = grep { /(\.yaml|\.backup_destination)$/i } readdir($dh);
closedir $dh;
foreach my $file (@files) {
my $path = "$dest_dir/$file";
my $data = eval { YAML::Syck::LoadFile($path) };
next if !$data;
my $id = $data->{id};
if (!$id) { $id = $file; $id =~ s/(\.yaml|\.backup_destination)$//i; }
if (exists $target_map->{$id}) {
my $should = $target_map->{$id};
my $is_dis = $data->{disabled} // 0;
if ($should && $is_dis != 0) { $data->{disabled} = 0; YAML::Syck::DumpFile($path, $data); }
elsif (!$should && $is_dis == 0) { $data->{disabled} = 1; YAML::Syck::DumpFile($path, $data); }
}
}
}
sub update_cpanel_config {
my ($type_val, $keep_local_val, $weekly_val) = @_;
local $/; open(my $fh, '<', $cp_conf_file) or return; my $c = <$fh>; close($fh);
my $ch = 0;
# Update BACKUPTYPE
if ($c =~ s/BACKUPTYPE:\s*\w+/BACKUPTYPE: $type_val/) { $ch = 1; }
# Update KEEPLOCAL
if ($c =~ s/KEEPLOCAL:\s*\d/KEEPLOCAL: $keep_local_val/) { $ch = 1; }
# Update BACKUP_WEEKLY_ENABLE
if ($c =~ s/BACKUP_WEEKLY_ENABLE:\s*['"]?\w+['"]?/BACKUP_WEEKLY_ENABLE: '$weekly_val'/) { $ch = 1; }
if ($ch) { open(my $fw, '>', $cp_conf_file); print $fw $c; close($fw); }
}
sub modify_system_cron {
my ($add_force) = @_;
my @lines = `crontab -l 2>/dev/null`;
return unless @lines;
my $changed = 0;
my $output = "";
foreach my $line (@lines) {
# Skip comment lines
if ($line =~ /^\s*#/ ) { $output .= $line; next; }
# Look for cPanel backup line
if ($line =~ /\/usr\/local\/cpanel\/bin\/backup/) {
if ($add_force && $line !~ /--force/) {
chomp($line);
$line .= " --force\n";
$changed = 1;
} elsif (!$add_force && $line =~ /--force/) {
$line =~ s/\s+--force//g;
$changed = 1;
}
}
$output .= $line;
}
if ($changed) {
open(my $fh, "| crontab -") or return;
print $fh $output;
close($fh);
}
}
sub load_json_config {
my ($file) = @_; local $/; open(my $fh, '<', $file) or return {}; return decode_json(<$fh>);
}
EOF
# ==============================================================================
# FILE 4: PRE-HOOK (The Announcer)
# ==============================================================================
echo ">>> [6/7] Generating Hooks..."
cat << 'EOF' > /scripts/smart_backup_pre_hook.pl
#!/usr/local/cpanel/3rdparty/bin/perl
use strict;
use warnings;
use LWP::UserAgent;
use JSON;
use Sys::Hostname;
my $arg = $ARGV[0] // '';
if ($arg eq '--describe') {
print encode_json({ 'category' => 'System', 'event' => 'Backup', 'stage' => 'pre', 'hook' => '/scripts/smart_backup_pre_hook.pl', 'exectype' => 'script' });
exit;
}
my $config_file = '/etc/smartbackup_config.json';
if (-e $config_file) {
local $/; open(my $fh, '<', $config_file); my $conf = decode_json(<$fh>); close($fh);
if ($conf->{is_active} && $conf->{telegram_token}) {
my $ua = LWP::UserAgent->new;
my $msg = "š <b>Backup Process STARTED</b>\nš„ Server: " . hostname . "\nā³ System is now backing up accounts...";
$ua->post("https://api.telegram.org/bot$conf->{telegram_token}/sendMessage", { chat_id => $conf->{telegram_chat_id}, text => $msg, parse_mode => 'HTML' });
}
}
EOF
# ==============================================================================
# FILE 5: POST-HOOK (The Closer & Rollback & Un-Force)
# ==============================================================================
cat << 'EOF' > /scripts/smart_backup_post_hook.pl
#!/usr/local/cpanel/3rdparty/bin/perl
use strict;
use warnings;
use LWP::UserAgent;
use JSON;
use Sys::Hostname;
use YAML::Syck;
my $arg = $ARGV[0] // '';
if ($arg eq '--describe') {
print encode_json({ 'category' => 'System', 'event' => 'Backup', 'stage' => 'post', 'hook' => '/scripts/smart_backup_post_hook.pl', 'exectype' => 'script' });
exit;
}
my $ui_config_file = '/etc/smartbackup_config.json';
my $cp_conf_file = '/var/cpanel/backups/config';
my $dest_dir = -d '/var/cpanel/backups/destinations' ? '/var/cpanel/backups/destinations' : '/var/cpanel/backups';
disable_managed_destination();
# Rollback: Incremental, KeepLocal 1, Weekly Yes
rollback_cpanel_defaults('incremental', 1, 'yes');
# Rollback Cron: REMOVE --force
modify_system_cron(0);
send_finish_report();
sub disable_managed_destination {
return unless -e $ui_config_file;
my $conf = load_json_config($ui_config_file);
return unless $conf->{is_active} && $conf->{job_dest_id};
opendir(my $dh, $dest_dir) || return;
my @files = grep { /(\.yaml|\.backup_destination)$/i } readdir($dh);
closedir $dh;
foreach my $file (@files) {
my $path = "$dest_dir/$file"; my $data = eval { YAML::Syck::LoadFile($path) }; next if !$data;
my $id = $data->{id}; if (!$id) { $id = $file; $id =~ s/(\.yaml|\.backup_destination)$//i; }
if ($id eq $conf->{job_dest_id}) {
if (!defined $data->{disabled} || $data->{disabled} == 0) { $data->{disabled} = 1; YAML::Syck::DumpFile($path, $data); }
}
}
}
sub rollback_cpanel_defaults {
my ($type_val, $keep_local_val, $weekly_val) = @_;
local $/; open(my $fh, '<', $cp_conf_file) or return; my $c = <$fh>; close($fh);
my $ch = 0;
if ($c =~ s/BACKUPTYPE:\s*\w+/BACKUPTYPE: $type_val/) { $ch = 1; }
if ($c =~ s/KEEPLOCAL:\s*\d/KEEPLOCAL: $keep_local_val/) { $ch = 1; }
if ($c =~ s/BACKUP_WEEKLY_ENABLE:\s*['"]?\w+['"]?/BACKUP_WEEKLY_ENABLE: '$weekly_val'/) { $ch = 1; }
if ($ch) { open(my $fw, '>', $cp_conf_file); print $fw $c; close($fw); }
}
sub modify_system_cron {
my ($add_force) = @_;
my @lines = `crontab -l 2>/dev/null`;
return unless @lines;
my $changed = 0;
my $output = "";
foreach my $line (@lines) {
if ($line =~ /^\s*#/ ) { $output .= $line; next; }
# Look for cPanel backup line
if ($line =~ /\/usr\/local\/cpanel\/bin\/backup/) {
if ($add_force && $line !~ /--force/) {
chomp($line);
$line .= " --force\n";
$changed = 1;
} elsif (!$add_force && $line =~ /--force/) {
$line =~ s/\s+--force//g;
$changed = 1;
}
}
$output .= $line;
}
if ($changed) {
open(my $fh, "| crontab -") or return;
print $fh $output;
close($fh);
}
}
sub send_finish_report {
return unless -e $ui_config_file;
my $conf = load_json_config($ui_config_file);
return unless ($conf->{telegram_token} && $conf->{telegram_chat_id});
my $log_dir = '/usr/local/cpanel/logs/cpbackup';
my $latest_log = `ls -t $log_dir/*.log 2>/dev/null | head -1`; chomp($latest_log);
my $icon = "ā
"; my $txt = "Backup & Upload Completed";
if ($latest_log && -e $latest_log) {
my $errs = `grep -E "Error|Failed" $latest_log | wc -l`; chomp($errs);
if ($errs > 0) { $icon = "ā"; $txt = "Completed with errors"; }
}
my $msg = "<b>$icon Backup FINISHED</b>\nš„ " . hostname . "\n----------------\nš $txt\nā©ļø Config: Rolled back to Local/Incremental\nā
Weekly Backup: Re-enabled\nš Cron: <b>Removed --force</b>\nš Remote Destination Disabled.";
my $ua = LWP::UserAgent->new; $ua->post("https://api.telegram.org/bot$conf->{telegram_token}/sendMessage", { chat_id => $conf->{telegram_chat_id}, text => $msg, parse_mode => 'HTML' });
}
sub load_json_config { my ($f)=@_; local $/; open(my $fh,'<',$f) or return {}; return decode_json(<$fh>); }
EOF
# ==============================================================================
# SET PERMISSIONS & REGISTER
# ==============================================================================
echo ">>> [7/7] Finalizing Setup..."
# Set Perms
chmod 755 /usr/local/cpanel/whostmgr/docroot/cgi/smartbackup/index.cgi
chmod 755 /scripts/smart_backup_config.pl
chmod 755 /scripts/smart_backup_pre_hook.pl
chmod 755 /scripts/smart_backup_post_hook.pl
# Register Plugin
/usr/local/cpanel/bin/register_appconfig /var/cpanel/apps/smartbackup.conf
# Clean & Register Hooks
/usr/local/cpanel/bin/manage_hooks delete script /scripts/smart_backup_pre_hook.pl --category System --event Backup --stage pre >/dev/null 2>&1
/usr/local/cpanel/bin/manage_hooks delete script /scripts/smart_backup_post_hook.pl --category System --event Backup --stage post >/dev/null 2>&1
/usr/local/cpanel/bin/manage_hooks add script /scripts/smart_backup_pre_hook.pl --category System --event Backup --stage pre
/usr/local/cpanel/bin/manage_hooks add script /scripts/smart_backup_post_hook.pl --category System --event Backup --stage post
# Setup Cron Job (12:15 AM = 00:15)
CRON_CMD="15 9 * * * /usr/local/cpanel/3rdparty/bin/perl /scripts/smart_backup_config.pl >> /var/log/smart_backup.log 2>&1"
(crontab -l 2>/dev/null | grep -v "/scripts/smart_backup_config.pl"; echo "$CRON_CMD") | crontab -
echo "=========================================================="
echo " INSTALLATION COMPLETED SUCCESSFULLY! "
echo "=========================================================="
echo "1. Plugin UI: WHM -> Plugins -> Smart Backup & Telegram Alert"
echo "2. Cron Job: Set to run at 00:15 Daily."
echo "3. Added logic to INJECT --force into system cron on remote days."
echo "4. Added logic to REMOVE --force from system cron after backup."
echo "=========================================================="