- `;
} 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' :
diff --git a/scripts/backup/app/backup_schedule_all.sh b/scripts/backup/app/backup_schedule_all.sh
new file mode 100644
index 0000000..bdccb1e
--- /dev/null
+++ b/scripts/backup/app/backup_schedule_all.sh
@@ -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"
+}
diff --git a/scripts/cli/commands/backup/cli_backup_commands.sh b/scripts/cli/commands/backup/cli_backup_commands.sh
index 2e93182..c93f5e2 100755
--- a/scripts/cli/commands/backup/cli_backup_commands.sh
+++ b/scripts/cli/commands/backup/cli_backup_commands.sh
@@ -54,6 +54,9 @@ cliHandleBackupCommands()
all)
backupAllApps
;;
+ scheduled)
+ backupScheduleEnabledApps
+ ;;
location)
case "$action" in
add)
diff --git a/scripts/cli/commands/backup/cli_backup_header.sh b/scripts/cli/commands/backup/cli_backup_header.sh
index 4b5ad02..95a060a 100755
--- a/scripts/cli/commands/backup/cli_backup_header.sh
+++ b/scripts/cli/commands/backup/cli_backup_header.sh
@@ -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 [type]"
echo " Add a new backup location. Type defaults to 'local'."
echo " Types: local, sftp, rest, s3, b2, gs, azure, rclone"
diff --git a/scripts/config/application/application_menu_apps.sh b/scripts/config/application/application_menu_apps.sh
index 8c7ad7c..f7e5d7f 100755
--- a/scripts/config/application/application_menu_apps.sh
+++ b/scripts/config/application/application_menu_apps.sh
@@ -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))
diff --git a/scripts/config/core/config_manage_menu.sh b/scripts/config/core/config_manage_menu.sh
index e89f029..76317ce 100755
--- a/scripts/config/core/config_manage_menu.sh
+++ b/scripts/config/core/config_manage_menu.sh
@@ -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))
diff --git a/scripts/crontab/app/crontab_backup_all_apps.sh b/scripts/crontab/app/crontab_backup_all_apps.sh
deleted file mode 100755
index 6051aae..0000000
--- a/scripts/crontab/app/crontab_backup_all_apps.sh
+++ /dev/null
@@ -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."
-}
-
diff --git a/scripts/crontab/app/crontab_backup_scheduler.sh b/scripts/crontab/app/crontab_backup_scheduler.sh
new file mode 100644
index 0000000..573d918
--- /dev/null
+++ b/scripts/crontab/app/crontab_backup_scheduler.sh
@@ -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"
+}
diff --git a/scripts/crontab/app/crontab_check_backup_app.sh b/scripts/crontab/app/crontab_check_backup_app.sh
deleted file mode 100755
index 3f59ab7..0000000
--- a/scripts/crontab/app/crontab_check_backup_app.sh
+++ /dev/null
@@ -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
-}
diff --git a/scripts/crontab/app/crontab_remove_backup_app.sh b/scripts/crontab/app/crontab_remove_backup_app.sh
deleted file mode 100755
index 2257d73..0000000
--- a/scripts/crontab/app/crontab_remove_backup_app.sh
+++ /dev/null
@@ -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."
-}
diff --git a/scripts/crontab/app/crontab_remove_folder.sh b/scripts/crontab/app/crontab_remove_folder.sh
deleted file mode 100755
index 283bc71..0000000
--- a/scripts/crontab/app/crontab_remove_folder.sh
+++ /dev/null
@@ -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
-}
diff --git a/scripts/crontab/app/install/crontab_setup.sh b/scripts/crontab/app/install/crontab_setup.sh
deleted file mode 100755
index 72d3392..0000000
--- a/scripts/crontab/app/install/crontab_setup.sh
+++ /dev/null
@@ -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"
-}
diff --git a/scripts/crontab/app/install/crontab_timing.sh b/scripts/crontab/app/install/crontab_timing.sh
deleted file mode 100755
index c815715..0000000
--- a/scripts/crontab/app/install/crontab_timing.sh
+++ /dev/null
@@ -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"
-}
diff --git a/scripts/crontab/crontab_refresh.sh b/scripts/crontab/crontab_refresh.sh
index f2286e7..6115c73 100755
--- a/scripts/crontab/crontab_refresh.sh
+++ b/scripts/crontab/crontab_refresh.sh
@@ -9,7 +9,7 @@ crontabRefresh()
crontabSetup;
# Rebuild from scratch
- crontabSetupAllAppBackups "false";
+ crontabSetupBackupScheduler;
#crontabSetupTaskProcessor # Switched to Systemd
crontabSetupSystemInfoUpdater;
diff --git a/scripts/database/insert/db_insert_cron_jobs.sh b/scripts/database/insert/db_insert_cron_jobs.sh
deleted file mode 100755
index 09b0c5b..0000000
--- a/scripts/database/insert/db_insert_cron_jobs.sh
+++ /dev/null
@@ -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
-}
diff --git a/scripts/database/tables/db_create_tables.sh b/scripts/database/tables/db_create_tables.sh
index 5840dd9..e29d676 100755
--- a/scripts/database/tables/db_create_tables.sh
+++ b/scripts/database/tables/db_create_tables.sh
@@ -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
diff --git a/scripts/menu/tools/manage_crontab.sh b/scripts/menu/tools/manage_crontab.sh
index 45750f0..3127866 100755
--- a/scripts/menu/tools/manage_crontab.sh
+++ b/scripts/menu/tools/manage_crontab.sh
@@ -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 ""
diff --git a/scripts/source/files/arrays/files_backup.sh b/scripts/source/files/arrays/files_backup.sh
index 5428f0a..0b25f76 100755
--- a/scripts/source/files/arrays/files_backup.sh
+++ b/scripts/source/files/arrays/files_backup.sh
@@ -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"
diff --git a/scripts/source/files/arrays/files_crontab.sh b/scripts/source/files/arrays/files_crontab.sh
index 0ed1178..38e1e51 100755
--- a/scripts/source/files/arrays/files_crontab.sh
+++ b/scripts/source/files/arrays/files_crontab.sh
@@ -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"
diff --git a/scripts/source/files/arrays/files_database.sh b/scripts/source/files/arrays/files_database.sh
index 591a11c..899bf0b 100755
--- a/scripts/source/files/arrays/files_database.sh
+++ b/scripts/source/files/arrays/files_database.sh
@@ -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"
diff --git a/scripts/start/start_other.sh b/scripts/start/start_other.sh
index 92a4d87..6b3ef6a 100755
--- a/scripts/start/start_other.sh
+++ b/scripts/start/start_other.sh
@@ -33,7 +33,7 @@ startOther()
fi
if [[ "$toolsstartcrontabsetup" == [yY] ]]; then
- crontabSetupAllAppBackups
+ crontabSetupBackupScheduler
fi
if [[ "$toolinstallcrontab" == [yY] ]]; then
diff --git a/scripts/webui/data/generators/config/webui_update_config.sh b/scripts/webui/data/generators/config/webui_update_config.sh
index 31688a9..25e6487 100755
--- a/scripts/webui/data/generators/config/webui_update_config.sh
+++ b/scripts/webui/data/generators/config/webui_update_config.sh
@@ -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"