diff --git a/runs/backup.sh b/runs/backup.sh index bb1d9e9d02..48a685a33f 100755 --- a/runs/backup.sh +++ b/runs/backup.sh @@ -5,20 +5,39 @@ OPENWBDIRNAME=${OPENWBDIRNAME:-/} TARBASEDIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd) BACKUPDIR="$OPENWBBASEDIR/data/backup" RAMDISKDIR="$OPENWBBASEDIR/ramdisk" -TEMPDIR="$RAMDISKDIR/temp" +TEMPDIR=$(mktemp -d --tmpdir openwb_backup_XXXXXX) LOGDIR="$OPENWBBASEDIR/data/log" LOGFILE="$LOGDIR/backup.log" +HOMEDIR="/home/openwb" +VAR_LIB="/var/lib" + +# Mosquitto DB files to monitor +DB_FILES=( + "mosquitto/mosquitto.db" + "mosquitto_local/mosquitto.db" +) + +# Timeout for mosquitto DB flush +DB_TIMEOUT=5 useExtendedFilename=$1 -if ((useExtendedFilename == 1)); then - # only use characters supported in most OS! - # for Win see https://learn.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-shares--directories--files--and-metadata - FILENAME="openWB_backup_$(date +"%Y-%m-%d_%H-%M-%S").tar" -else - FILENAME="backup.tar" -fi +FILENAMESUFFIX=".tar.gz" -{ +generate_filename() { + # generate filename + # if useExtendedFilename is 1, use date and version info + # else just use "backup" + if ((useExtendedFilename == 1)); then + # only use characters supported in most OS! + # for Win see https://learn.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-shares--directories--files--and-metadata + FILENAME="openWB_backup_$(date +"%Y-%m-%d_%H-%M-%S")" + else + FILENAME="backup" + fi + BACKUPFILE="$BACKUPDIR/$FILENAME" +} + +log_environment() { echo "starting backup script" echo "environment:" echo " OPENWBBASEDIR: $OPENWBBASEDIR" @@ -30,95 +49,223 @@ fi echo " LOGDIR: $LOGDIR" echo " LOGFILE: $LOGFILE" echo " FILENAME: $FILENAME" +} - echo "deleting old backup files if present in '$BACKUPDIR'" - # remove old backup files - rm -v "$BACKUPDIR/"* - BACKUPFILE="$BACKUPDIR/$FILENAME" +remove_old_backups() { + echo "removing old files in '$BACKUPDIR' if present" + # Delete old backups (robust, no glob issues) + find "$BACKUPDIR" -mindepth 1 -maxdepth 1 -not -name '.donotdelete' -exec rm -vrf {} + +} + +force_mosquitto_write() { + collect_baseline() { + echo "collecting Baseline-mtime before SIGUSR1:" + # Baseline mtimes to avoid race condition (captured before SIGUSR1) + declare -Ag BASELINE_DB_MTIME=() + + for f in "${DB_FILES[@]}"; do + if [ -e "$VAR_LIB/$f" ]; then + BASELINE_DB_MTIME["$f"]=$(stat -c %Y "$VAR_LIB/$f") + echo " $f -> ${BASELINE_DB_MTIME["$f"]}" + else + BASELINE_DB_MTIME["$f"]=0 + echo " $f -> (does not yet exist, setting to 0)" + fi + done + } + + flush_mosquitto() { + # inform mosquitto to flush data to disk + echo "sending 'SIGUSR1' to mosquitto processes to flush data to disk" + sudo pkill -e -SIGUSR1 mosquitto || echo "WARNING: no processes found?" + } + + wait_for_mosquitto_flush() { + # wait for mosquitto to flush db files to disk + # uses inotifywait if available, else falls back to polling + # requires inotify-tools package for inotifywait + local start_ts + start_ts=$(date +%s) + declare -A initial_mtime + + # Use previously captured baseline (recorded before signal) + if ((${#BASELINE_DB_MTIME[@]})); then + for f in "${DB_FILES[@]}"; do + initial_mtime["$VAR_LIB/$f"]=${BASELINE_DB_MTIME["$f"]:-0} + done + else + # Fallback (should not occur) + for f in "${DB_FILES[@]}"; do + [ -e "$VAR_LIB/$f" ] && initial_mtime["$VAR_LIB/$f"]=$(stat -c %Y "$VAR_LIB/$f") || initial_mtime["$VAR_LIB/$f"]=0 + done + fi - # tell mosquitto to store all retained topics in db now - echo "sending 'SIGUSR1' to mosquitto processes" - sudo pkill -e -SIGUSR1 mosquitto - # give mosquitto some time to finish - sleep 1 - - # create backup file - echo "creating new backup file: $BACKUPFILE" - echo "adding openWB files" - tar --verbose --create \ - --file="$BACKUPFILE" \ - --directory="$TARBASEDIR/" \ - "$OPENWBDIRNAME/data/charge_log" \ - "$OPENWBDIRNAME/data/daily_log" \ - "$OPENWBDIRNAME/data/monthly_log" \ - "$OPENWBDIRNAME/data/log/uuid" - echo "adding configuration file" - sudo tar --verbose --append \ - --file="$BACKUPFILE" \ - --directory="/home/openwb/" \ - "configuration.json" - echo "adding mosquitto files" - sudo tar --verbose --append \ - --file="$BACKUPFILE" \ - --directory="/var/lib/" \ - "mosquitto/" "mosquitto_local/" - echo "adding mosquitto configuration" - sudo tar --verbose --append \ - --file="$BACKUPFILE" \ - --directory="/etc/mosquitto/" \ - "conf_local.d/" - echo "adding boot file" - sudo tar --verbose --append \ - --file="$BACKUPFILE" \ - "/boot/config.txt" - echo "adding git information" - git branch --no-color --show-current >"$RAMDISKDIR/GIT_BRANCH" - git log --pretty='format:%H' -n1 >"$RAMDISKDIR/GIT_HASH" - echo "branch: $(<"$RAMDISKDIR/GIT_BRANCH") commit-hash: $(<"$RAMDISKDIR/GIT_HASH")" - tar --verbose --append \ - --file="$BACKUPFILE" \ - --directory="$RAMDISKDIR/" \ - "GIT_BRANCH" "GIT_HASH" - - echo "calculating checksums" - IFS=$'\n' - mapfile -t file_list < <(tar -tf "$BACKUPFILE") - mkdir -p "$TEMPDIR" - # process each file - for file in "${file_list[@]}"; do - # skip directories - if [[ $file =~ /$ ]]; then - echo "skipping directory $file" - continue + echo "waiting for mosquitto to flush db files (timeout ${DB_TIMEOUT}s)..." + + if command -v inotifywait >/dev/null 2>&1; then + echo "using 'inotifywait'" + local pending=() + for f in "${DB_FILES[@]}"; do + pending+=("$VAR_LIB/$f") + done + while ((${#pending[@]})); do + local elapsed=$(( $(date +%s) - start_ts )) + local remain=$(( DB_TIMEOUT - elapsed )) + (( remain <= 0 )) && echo "timeout reached (inotify), continuing." && break + inotifywait -q -t "$remain" -e modify -e close_write -e attrib -e move -e create "${pending[@]}" 2>/dev/null || true + local new_pending=() + for f in "${pending[@]}"; do + if [ -e "$f" ]; then + local mt + mt=$(stat -c %Y "$f") + if (( mt != initial_mtime["$f"] )); then + echo "modified: $f (old: ${initial_mtime["$f"]} -> new: $mt)" + else + new_pending+=("$f") + fi + else + new_pending+=("$f") + fi + done + pending=("${new_pending[@]}") + done + else + echo "'inotifywait' not found, falling back to polling" + while true; do + local all_ok=1 + for f in "${DB_FILES[@]}"; do + if [ -e "$VAR_LIB/$f" ]; then + local mt + mt=$(stat -c %Y "$VAR_LIB/$f") + if (( mt != initial_mtime["$VAR_LIB/$f"] )); then + echo "modified (polling): $f (old: ${initial_mtime["$VAR_LIB/$f"]} -> new: $mt)" + initial_mtime["$VAR_LIB/$f"]=$mt + else + all_ok=0 + fi + else + all_ok=0 + fi + done + local elapsed=$(( $(date +%s) - start_ts )) + (( all_ok == 1 )) && echo "all relevant files are modified after ${elapsed}s" && break + (( elapsed >= DB_TIMEOUT )) && echo "timeout reached (polling), continuing." && break + sleep 0.1 + done fi - # extract the file - tar -xf "$BACKUPFILE" -C "$TEMPDIR" "$file" - # calculate the checksum - sha256sum "$TEMPDIR/$file" | sed -n "s|$TEMPDIR/||p" >> "$RAMDISKDIR/SHA256SUM" - # remove the file - rm -f "$TEMPDIR/$file" - done - tar --verbose --append \ - --file="$BACKUPFILE" \ - --directory="$RAMDISKDIR/" \ - "SHA256SUM" - - # cleanup - echo "removing temporary files" - rm -v "$RAMDISKDIR/GIT_BRANCH" "$RAMDISKDIR/GIT_HASH" "$RAMDISKDIR/SHA256SUM" - rm -rf "${TEMPDIR:?}/" - tar --append \ - --file="$BACKUPFILE" \ - --directory="$LOGDIR/" \ - "backup.log" - echo "zipping archive" - gzip --verbose "$BACKUPFILE" - echo "setting permissions of new backup file" - sudo chown openwb:www-data "$BACKUPFILE.gz" - sudo chmod 664 "$BACKUPFILE.gz" + } + + copy_db_to_temp() { + echo "copying mosquitto db files to temporary location" + for f in "${DB_FILES[@]}"; do + if [ -e "$VAR_LIB/$f" ]; then + local dest_dir + dest_dir="$TEMPDIR/$(dirname "$f")" + mkdir -p "$dest_dir" + sudo cp -v "$VAR_LIB/$f" "$dest_dir/" + else + echo "skipping $f (does not exist)" + fi + done + } + + collect_baseline + flush_mosquitto + wait_for_mosquitto_flush + copy_db_to_temp +} + +collect_git_info() { + echo "collecting git information" + git branch --no-color --show-current >"$TEMPDIR/GIT_BRANCH" + git log --pretty='format:%H' -n1 >"$TEMPDIR/GIT_HASH" + echo "branch: $(<"$TEMPDIR/GIT_BRANCH") commit-hash: $(<"$TEMPDIR/GIT_HASH")" +} + +create_archive() { + create_backup() { + echo "creating new backup file: $BACKUPFILE" + echo "adding files" + + sudo tar --verbose --create \ + --file="$BACKUPFILE" \ + --exclude=".gitignore" \ + --directory="$TEMPDIR/" \ + "${DB_FILES[@]}" \ + "GIT_BRANCH" \ + "GIT_HASH" \ + --directory="/etc/mosquitto/" \ + "conf_local.d/" \ + --directory="$TARBASEDIR/" \ + "$OPENWBDIRNAME/data/charge_log" \ + "$OPENWBDIRNAME/data/daily_log" \ + "$OPENWBDIRNAME/data/monthly_log" \ + "$OPENWBDIRNAME/data/log/uuid" \ + --directory="$HOMEDIR/" \ + "configuration.json" \ + --directory="/" \ + "boot/config.txt" + } + calculate_checksums() { + echo "calculating checksums" + IFS=$'\n' + mapfile -t file_list < <(tar -tf "$BACKUPFILE") + # process each file + for file in "${file_list[@]}"; do + # skip directories + if [[ $file =~ /$ ]]; then + echo "skipping directory $file" + continue + fi + # extract the file + tar -xf "$BACKUPFILE" -C "$TEMPDIR" "$file" + # calculate the checksum + sha256sum "$TEMPDIR/$file" | sed -n "s|$TEMPDIR/||p" >> "$TEMPDIR/SHA256SUM" + # remove the file + rm -f "$TEMPDIR/$file" + done + + echo "adding checksum file to archive" + sudo tar --verbose --append \ + --file="$BACKUPFILE" \ + --directory="$TEMPDIR/" \ + "SHA256SUM" + } + + cleanup_and_compress() { + echo "removing temporary files" + rm -rf "${TEMPDIR:?}/" + echo "adding log file to archive" + sudo tar --append \ + --file="$BACKUPFILE" \ + --directory="$LOGDIR/" \ + "backup.log" + echo "zipping archive" + gzip --verbose --suffix "$FILENAMESUFFIX" "$BACKUPFILE" + } + + fix_permissions() { + echo "setting permissions of new backup file" + sudo chown openwb:www-data "$BACKUPFILE$FILENAMESUFFIX" + sudo chmod 664 "$BACKUPFILE$FILENAMESUFFIX" + } + + create_backup + calculate_checksums + cleanup_and_compress + fix_permissions +} + +{ + generate_filename + log_environment + remove_old_backups + collect_git_info + force_mosquitto_write + create_archive echo "backup finished" } >"$LOGFILE" 2>&1 # return our filename for further processing -echo "$FILENAME.gz" +echo "$FILENAME$FILENAMESUFFIX" diff --git a/runs/install_packages.sh b/runs/install_packages.sh index e796359251..913569bc50 100755 --- a/runs/install_packages.sh +++ b/runs/install_packages.sh @@ -2,7 +2,7 @@ echo "install required packages with 'apt-get'..." sudo apt-get -q update sudo apt-get -q -y install \ - vim bc jq socat sshpass sudo ssl-cert mmc-utils \ + vim bc jq socat sshpass sudo ssl-cert mmc-utils inotify-tools \ apache2 libapache2-mod-php \ php php-gd php-curl php-xml php-json \ git \ diff --git a/web/settings/uploadFile.php b/web/settings/uploadFile.php index fd038b80d5..9f6fa46d9e 100644 --- a/web/settings/uploadFile.php +++ b/web/settings/uploadFile.php @@ -21,11 +21,11 @@ function check_gzip() { // quick check for file contents function check_restore_file_contents() { $output = null; - $result = exec("tar --list --file=\"" . $_FILES["file"]["tmp_name"] . "\" | grep -c \"^\(mosquitto/\|mosquitto_local/\|GIT_HASH\|GIT_BRANCH\|SHA256SUM\|backup.log\)$\"", $output); + $result = exec("tar --list --file=\"" . $_FILES["file"]["tmp_name"] . "\" | egrep -c \"^(mosquitto/.+|mosquitto_local/.+|GIT_HASH|GIT_BRANCH|SHA256SUM|backup.log)$\"", $output); if ($result === false || $result != "6") { exit_with_error("Prüfung des Archivinhalts fehlgeschlagen! Das Archiv ist unvollständig."); } - $result = exec("tar --list --file=\"" . $_FILES["file"]["tmp_name"] . "\" | grep -c \"^openWB/.*\"", $output); + $result = exec("tar --list --file=\"" . $_FILES["file"]["tmp_name"] . "\" | grep -c \"^openWB/\"", $output); if ($result === false || $result <= 4) { exit_with_error("Prüfung des Archivinhalts fehlgeschlagen! Das Archiv ist unvollständig."); }