Compare commits

...

2 Commits

Author SHA1 Message Date
librelad
a8161f04e5 Merge claude/2 2026-05-22 14:34:35 +01:00
librelad
4ce0340ef8 refactor(backup): replace per-app cron stagger with task-queue scheduler
Application backups were driven by one crontab entry per app, each offset by
id * CFG_BACKUP_CRONTAB_APP_INTERVAL minutes. That minute offset is written
straight into cron's 0-59 minute field, so past ~20 apps it overflowed into
an invalid entry that silently never fired, and the fixed spacing could not
serialize backups that ran longer than the gap.

Replace it with a single daily entry (`libreportal backup scheduled`) that
enqueues a backup task per enabled app. The existing systemd task processor
drains them serially — no minute overflow, real serialization, and backups
are now visible/cancellable in the Tasks UI. Per-app enable is read from
CFG_<APP>_BACKUP at schedule time instead of being mirrored into crontab.

Removes the stagger machinery (timing/setup/check/remove scripts), the
now-unused cron_jobs table + insert, and the CFG_BACKUP_CRONTAB_APP_INTERVAL
config knob and its WebUI field.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-22 14:34:35 +01:00
23 changed files with 77 additions and 252 deletions

View File

@ -2,4 +2,3 @@
# Backup General - Scheduling
# ================================================================================
CFG_BACKUP_CRONTAB_APP="0 5 * * *" # App Backup Schedule - Crontab schedule for application backups
CFG_BACKUP_CRONTAB_APP_INTERVAL=3 # App Backup Interval - Minutes between app backup checks

View File

@ -650,13 +650,6 @@ class ConfigShared {
fieldHTML += `
<input type="url" id="${fieldId}" name="${key}" value="${value}" class="form-control" placeholder="${placeholder}">
`;
} else if (key === 'CFG_BACKUP_CRONTAB_APP_INTERVAL') {
fieldHTML += `
<div class="input-group">
<input type="number" id="${fieldId}" name="${key}" value="${value}" class="form-control" min="1" max="1440" placeholder="3">
<span class="input-group-text">minutes</span>
</div>
`;
} else if (/^CFG_BACKUP(_LOC_[0-9]+)?_KEEP_(LAST|DAILY|WEEKLY|MONTHLY|YEARLY)$/.test(key)) {
const unitLabel = key.endsWith('_KEEP_LAST') ? 'snapshots' :
key.endsWith('_KEEP_DAILY') ? 'days' :

View File

@ -0,0 +1,32 @@
#!/bin/bash
# Enqueue a backup for every installed app that has backups enabled. Invoked
# once daily by the backup scheduler crontab entry. Each app is handed to
# backupAppSchedule, which queues a task for the processor (WebUI installs) or
# runs the backup inline (terminal-only installs).
backupScheduleEnabledApps()
{
isHeader "Scheduling backups for enabled applications"
if [ ! -f "$docker_dir/$db_file" ]; then
isError "Database not found: $docker_dir/$db_file"
return 1
fi
local app_names=()
while IFS= read -r name; do
[[ -z "$name" ]] && continue
app_names+=("$name")
done < <(sudo sqlite3 "$docker_dir/$db_file" "SELECT name FROM apps WHERE status = 1;")
local queued=0
for name in "${app_names[@]}"; do
local backup_flag="CFG_${name^^}_BACKUP"
if [[ "${!backup_flag}" == "true" ]]; then
backupAppSchedule "$name"
((queued++))
fi
done
isSuccessful "Backup scheduling complete — $queued app(s) queued"
}

View File

@ -54,6 +54,9 @@ cliHandleBackupCommands()
all)
backupAllApps
;;
scheduled)
backupScheduleEnabledApps
;;
location)
case "$action" in
add)

View File

@ -21,6 +21,9 @@ cliShowBackupHelp()
echo "backup all"
echo " Snapshot every installed app."
echo ""
echo "backup scheduled"
echo " Queue a backup for every app with backups enabled (daily cron entry)."
echo ""
echo "backup location add <name> [type]"
echo " Add a new backup location. Type defaults to 'local'."
echo " Types: local, sftp, rest, s3, b2, gs, azure, rclone"

View File

@ -40,7 +40,7 @@ viewAppConfigs()
isNotice "Exiting..."
echo ""
checkConfigFilesMissingVariables true
crontabSetupAllAppBackups true
crontabSetupBackupScheduler
fi
elif [[ "$selected_app_number" =~ ^[0-9]+$ ]] && [ "$selected_app_number" -ge 1 ] && [ "$selected_app_number" -le ${#installed_apps[@]} ]; then
local index=$((selected_app_number - 1))

View File

@ -102,7 +102,7 @@ viewLibrePortalConfigs()
isNotice "Exiting..."
echo ""
checkConfigFilesMissingVariables true;
crontabSetupAllAppBackups true;
crontabSetupBackupScheduler;
fi
elif [[ "$selected_number" =~ ^[0-9]+$ ]] && [ "$selected_number" -ge 1 ] && [ "$selected_number" -le ${#config_files[@]} ]; then
local index=$((selected_number - 1))

View File

@ -1,54 +0,0 @@
#!/bin/bash
crontabSetupAllAppBackups()
{
local show_header=$1
local ISCRON=$( (sudo -u $sudo_user_name crontab -l) 2>/dev/null )
# Check to see if installed
if [[ "$ISCRON" == *"command not found"* ]]; then
isNotice "Crontab is not found. Unable to set up backups."
fi
# Check to see if crontab is not installed
if ! sudo -u $sudo_user_name crontab -l 2>/dev/null | grep -q "cron is set up for $sudo_user_name" > /dev/null 2>&1; then
isNotice "Crontab is not set up, skipping until it's found."
fi
# Check if the database file exists
if [ ! -f "$docker_dir/$db_file" ]; then
isNotice "Database file not found: $docker_dir/$db_file"
fi
if [[ $show_header != "false" ]]; then
isHeader "Backup Crontab Install"
fi
local app_names=()
while IFS= read -r name; do
local app_names+=("$name")
done < <(sudo sqlite3 "$docker_dir/$db_file" "SELECT name FROM apps WHERE status = 1;")
# Check if sqlite3 is available
if ! command -v sudo sqlite3 &> /dev/null; then
isNotice "sqlite3 command not found. Make sure it's installed."
fi
# Remove crontab entries for applications with status = 0 (uninstalled)
while IFS= read -r name; do
local uninstalled_apps+=("$name")
done < <(sudo sqlite3 "$docker_dir/$db_file" "SELECT name FROM apps WHERE status = 0;")
for name in "${uninstalled_apps[@]}"; do
removeBackupCrontabAppFolderRemoved $name
done
# Setup crontab entries for installed applications
for name in "${app_names[@]}"; do
checkBackupCrontabApp $name
done
crontabClean;
isSuccessful "Setting up Crontab backups for application(s) completed."
}

View File

@ -0,0 +1,31 @@
#!/bin/bash
# Install (or refresh) the single daily crontab entry that drives application
# backups. The entry runs `libreportal backup scheduled`, which enqueues a
# backup task per enabled app for the processor to drain serially.
crontabSetupBackupScheduler()
{
local ISCRON=$( (sudo -u $sudo_user_name crontab -l) 2>/dev/null )
if [[ "$ISCRON" == *"command not found"* ]]; then
isNotice "Crontab is not found. Unable to set up the backup scheduler."
return 0
fi
if ! sudo -u $sudo_user_name crontab -l 2>/dev/null | grep -q "cron is set up for $sudo_user_name"; then
isNotice "Crontab is not set up, skipping backup scheduler until it's found."
return 0
fi
local marker="# CRONTAB BACKUP SCHEDULER"
local scheduler_entry="$CFG_BACKUP_CRONTAB_APP libreportal backup scheduled $marker"
# Drop any previous scheduler entry, then re-add the current one so a
# changed schedule (CFG_BACKUP_CRONTAB_APP) always takes effect.
local result=$(sudo -u $sudo_user_name crontab -l 2>/dev/null | grep -v "$marker" | sudo -u $sudo_user_name crontab -)
local result=$( (sudo -u $sudo_user_name crontab -l 2>/dev/null; echo "$scheduler_entry") | sudo -u $sudo_user_name crontab - )
checkSuccess "Installing the daily backup scheduler entry"
local schedule_time=$(echo "$CFG_BACKUP_CRONTAB_APP" | cut -d' ' -f2)
isSuccessful "Enabled apps will be queued for backup daily at ${schedule_time}:00"
}

View File

@ -1,21 +0,0 @@
#!/bin/bash
checkBackupCrontabApp()
{
local name="$1"
local config_variable="CFG_${name^^}_BACKUP"
local desired="${!config_variable}"
local has_cron=0
if sudo -u $sudo_user_name crontab -l 2>/dev/null | grep -q "$name"; then
has_cron=1
fi
if [[ "$desired" == "true" && $has_cron -eq 0 ]]; then
installSetupCrontab "$name"
databaseCronJobsInsert "$name"
isSuccessful "Backup crontab enabled for $name."
elif [[ "$desired" == "false" && $has_cron -eq 1 ]]; then
removeBackupCrontabApp "$name"
isSuccessful "Backup crontab removed for $name (disabled in config)."
fi
}

View File

@ -1,9 +0,0 @@
#!/bin/bash
removeBackupCrontabApp()
{
local name="$1"
# Remove the crontab entry for the specified application
sudo -u $sudo_user_name crontab -l 2>/dev/null | grep -v "$name" | sudo -u $sudo_user_name crontab -
isSuccessful "Automatic backups for $name have been removed."
}

View File

@ -1,23 +0,0 @@
#!/bin/bash
removeBackupCrontabAppFolderRemoved()
{
local name="$1"
# Check if the crontab entry exists for the specified application
if sudo -u $sudo_user_name crontab -l 2>/dev/null | grep -q "$name"; then
echo ""
isNotice "Application $name is no longer installed."
while true; do
isQuestion "Do you want to remove automatic backups for $name (y/n): "
read -rp "" removecrontab
if [[ "$removecrontab" =~ ^[yYnN]$ ]]; then
break
fi
isNotice "Please provide a valid input (y/n)."
done
if [[ "$removecrontab" =~ ^[yY]$ ]]; then
removeBackupCrontabApp $name;
fi
fi
}

View File

@ -1,33 +0,0 @@
#!/bin/bash
# Function to set up the backup entry in crontab
installSetupCrontab()
{
local entry_name="$1"
isHeader "Adding $entry_name to Crontab"
# Check to see if already instealled
if ! sudo -u $sudo_user_name crontab -l 2>/dev/null | grep -q "cron is set up for $sudo_user_name"; then
isError "Crontab is not setup"
fi
local crontab_entry="$CFG_BACKUP_CRONTAB_APP libreportal backup app schedule $entry_name"
local apps_comment="# CRONTAB BACKUP APPS"
local existing_crontab=$(sudo -u $sudo_user_name crontab -l 2>/dev/null)
# Check if the apps comment exists in the crontab
if ! echo "$existing_crontab" | grep -q "$apps_comment"; then
existing_crontab=$(echo -e "$existing_crontab\n$apps_comment")
checkSuccess "Insert the apps comment header"
fi
existing_crontab=$(echo "$existing_crontab" | sed "/$apps_comment/a\\
$crontab_entry")
checkSuccess "Insert the backup entry after the apps comment"
local result=$(echo "$existing_crontab" | sudo -u $sudo_user_name crontab -)
checkSuccess "Set the updated crontab"
crontab_full_value=$(echo "$CFG_BACKUP_CRONTAB_APP" | cut -d' ' -f2)
isSuccessful "$entry_name will be backed up every day at $crontab_full_value:am"
}

View File

@ -1,65 +0,0 @@
#!/bin/bash
# Function to update a specific line in the crontab
installSetupCrontabTiming()
{
local entry_name=$1
ISCRON=$( (sudo -u $sudo_user_name crontab -l) 2>/dev/null )
# Check to see if installed
if [[ "$ISCRON" == *"command not found"* ]]; then
isError "Cron is not installed."
fi
# Check to see if already setup
if ! sudo -u $sudo_user_name crontab -l 2>/dev/null | grep -q "cron is set up for $sudo_user_name"; then
isError "Crontab is not setup"
fi
# Check if sqlite3 is available
if ! command -v sqlite3 &> /dev/null; then
isNotice "sqlite3 command not found. Make sure it's installed."
fi
# Ensure the database file exists
if [ ! -f "$docker_dir/$db_file" ]; then
isNotice "Database file not found: $docker_dir/$db_file"
fi
# Step 1: Retrieve the necessary information from the database
db_entry=$(sqlite3 "$docker_dir/$db_file" "SELECT id, name FROM cron_jobs WHERE name='$entry_name';")
IFS='|' read -r id name <<< "$db_entry"
# Check if the entry exists in the database
if [[ -z "$id" ]]; then
isNotice "Entry '$entry_name' not found in the database."
fi
# Calculate the new minute value based on the ID
new_minute_value=$((id * $CFG_BACKUP_CRONTAB_APP_INTERVAL))
# Step 2: Locate the existing crontab entry in the crontab file
crontab_entry_to_update=$(sudo -u $sudo_user_name crontab -l 2>/dev/null | grep "$entry_name")
# Check if the entry exists in the crontab
if [[ -z "$crontab_entry_to_update" ]]; then
isError "Entry '$entry_name' not found in the crontab."
fi
# Extract the existing minute value from the current crontab entry
current_minute_value=$(echo "$crontab_entry_to_update" | awk '{print $1}')
# Step 3: Update the minute value in the identified crontab entry
updated_crontab_entry="${crontab_entry_to_update/$current_minute_value/$new_minute_value}"
# Assuming CFG_BACKUP_CRONTAB_APP is set to "0 5 * * *"
crontab_app_value=$(echo "$CFG_BACKUP_CRONTAB_APP" | cut -d' ' -f2)
local result=$(sudo -u $sudo_user_name crontab -l 2>/dev/null | grep -v "$entry_name" | sudo -u $sudo_user_name crontab - )
checkSuccess "Remove the existing crontab entry"
local result=$( (sudo -u $sudo_user_name crontab -l 2>/dev/null; echo "$updated_crontab_entry") | sudo -u $sudo_user_name crontab - )
checkSuccess "Add the updated crontab entry"
isSuccessful "Crontab entry for '$entry_name' updated successfully."
isSuccessful "$entry_name will be backed up every day at $crontab_app_value:${new_minute_value}am"
}

View File

@ -9,7 +9,7 @@ crontabRefresh()
crontabSetup;
# Rebuild from scratch
crontabSetupAllAppBackups "false";
crontabSetupBackupScheduler;
#crontabSetupTaskProcessor # Switched to Systemd
crontabSetupSystemInfoUpdater;

View File

@ -1,19 +0,0 @@
#!/bin/bash
databaseCronJobsInsert()
{
local app_name="$1"
local table_name=cron_jobs
local key_in_db=$(sudo sqlite3 "$docker_dir/$db_file" "SELECT COUNT(*) FROM $table_name WHERE name = '$app_name';")
if [ "$key_in_db" != "" ]; then
if [ "$key_in_db" -eq 0 ]; then
local result=$(sudo sqlite3 "$docker_dir/$db_file" "INSERT INTO $table_name (name, date, time) VALUES ('$app_name', '$current_date', '$current_time');")
checkSuccess "Adding $app_name to the $table_name table."
else
local result=$(sudo sqlite3 "$docker_dir/$db_file" "UPDATE $table_name SET name = '$app_name', date = '$current_date', time = '$current_time' WHERE name = '$app_name';")
checkSuccess "$app_name already added to the $table_name table. Updating date/time."
fi
#isNotice "app_name is empty, unable to insert"
fi
}

View File

@ -67,13 +67,6 @@ databaseCreateTables()
checkSuccess "Creating $setup_table_name table"
fi
setup_table_name=cron_jobs
if ! sqlite3 "$docker_dir/$db_file" ".tables" | grep -q "\b$setup_table_name\b"; then
# Table info here
local result=$(sqlite3 $docker_dir/$db_file "CREATE TABLE IF NOT EXISTS $setup_table_name (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE, date DATE, time TIME);")
checkSuccess "Creating $setup_table_name table"
fi
setup_table_name=network_resources
if ! sqlite3 "$docker_dir/$db_file" ".tables" | grep -q "\b$setup_table_name\b"; then
# Simple unified network resources table - replaces all complex network tables

View File

@ -7,7 +7,7 @@ crontabToolsMenu()
while true; do
isHeader "Crontab Menu"
isOption "1. Scan apps for Crontab Backup"
isOption "1. Set up Backup Scheduler"
isOption "2. Force Crontab Reinstall"
isOption "x. Exit to Main Menu"
echo ""

View File

@ -9,6 +9,7 @@ backup_scripts=(
"backup/app/backup_app_hooks.sh"
"backup/app/backup_app_schedule.sh"
"backup/app/backup_app_start.sh"
"backup/app/backup_schedule_all.sh"
"backup/engine/backup_ssh.sh"
"backup/engine/borg_backup.sh"
"backup/engine/borg_check.sh"

View File

@ -4,12 +4,7 @@
# Do not edit manually - run './scripts/source/files/generate_arrays.sh run' to regenerate
crontab_scripts=(
"crontab/app/crontab_backup_all_apps.sh"
"crontab/app/crontab_check_backup_app.sh"
"crontab/app/crontab_remove_backup_app.sh"
"crontab/app/crontab_remove_folder.sh"
"crontab/app/install/crontab_setup.sh"
"crontab/app/install/crontab_timing.sh"
"crontab/app/crontab_backup_scheduler.sh"
"crontab/crontab_clean.sh"
"crontab/crontab_clear.sh"
"crontab/crontab_install.sh"

View File

@ -14,7 +14,6 @@ database_scripts=(
"database/check_os_update.sh"
"database/delete_db_file.sh"
"database/insert/db_insert_backups.sh"
"database/insert/db_insert_cron_jobs.sh"
"database/insert/db_insert_option.sh"
"database/insert/db_insert_port_open.sh"
"database/insert/db_insert_port_used.sh"

View File

@ -33,7 +33,7 @@ startOther()
fi
if [[ "$toolsstartcrontabsetup" == [yY] ]]; then
crontabSetupAllAppBackups
crontabSetupBackupScheduler
fi
if [[ "$toolinstallcrontab" == [yY] ]]; then

View File

@ -72,7 +72,7 @@ webuiValidateConfigValue() {
isError " Invalid crontab format for $var_name"
fi
;;
CFG_BACKUP_KEEP_LAST|CFG_BACKUP_KEEP_DAILY|CFG_BACKUP_KEEP_WEEKLY|CFG_BACKUP_KEEP_MONTHLY|CFG_BACKUP_KEEP_YEARLY|CFG_BACKUP_VERIFY_DATA_PERCENT|CFG_BACKUP_CRONTAB_APP_INTERVAL|CFG_UPDATER_CHECK|CFG_SWAPFILE_SIZE|CFG_GENERATED_PASS_LENGTH|CFG_WEBUI_LOG_STREAM_IDLE_TIMEOUT_MINUTES|CFG_WEBUI_LOG_STREAM_MAX_DURATION_MINUTES|CFG_WEBUI_LOG_STREAM_MAX_LINES_PER_SEC)
CFG_BACKUP_KEEP_LAST|CFG_BACKUP_KEEP_DAILY|CFG_BACKUP_KEEP_WEEKLY|CFG_BACKUP_KEEP_MONTHLY|CFG_BACKUP_KEEP_YEARLY|CFG_BACKUP_VERIFY_DATA_PERCENT|CFG_UPDATER_CHECK|CFG_SWAPFILE_SIZE|CFG_GENERATED_PASS_LENGTH|CFG_WEBUI_LOG_STREAM_IDLE_TIMEOUT_MINUTES|CFG_WEBUI_LOG_STREAM_MAX_DURATION_MINUTES|CFG_WEBUI_LOG_STREAM_MAX_LINES_PER_SEC)
# Validate numeric values
if ! echo "$var_value" | grep -qE '^[0-9]+$'; then
isError " $var_name must be a positive integer"