From 3c0639f9406cad80474af25b956d4426dd481166 Mon Sep 17 00:00:00 2001 From: Charlie Silverman Date: Sat, 31 Oct 2026 13:55:28 -0700 Subject: [PATCH] Initial upload first build --- openwrtupload.sh | 322 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 322 insertions(+) create mode 100644 openwrtupload.sh diff --git a/openwrtupload.sh b/openwrtupload.sh new file mode 100644 index 0000000..29dacc0 --- /dev/null +++ b/openwrtupload.sh @@ -0,0 +1,322 @@ +#!/bin/bash +# +# Script Name: openwrtupload.sh +# Description: +# Backs up OpenWrt configuration archives to a Backblaze B2 repository +# using Restic. Each .gz archive found in the source directory is +# extracted to a staging directory, backed up with Restic, and then +# removed along with the staging directory. +# +# Usage: +# openwrtupload.sh +# +# Author: Charlie Silverman +# Version: 20261031-01 +# +# Requirements: +# restic, jq, awk, tar, numfmt (optional, for human-readable sizes). +# Restic environment file: /root/.restic-env +# +# Versioning: +# Versions use the format YYYYMMDD-xx, where YYYYMMDD is the release date +# and xx is the release number for that day, starting at 01. +# +# Revision History: +# 20261031-01 - Added script header and YYYYMMDD-xx versioning. +# Added strict error handling with error/interrupt logging; +# an archive is no longer deleted if its backup fails. +# Staging directory is cleaned up on failure or interrupt. +# Restic snapshot details (ID, parent, tags, file/dir counts, +# data added, snapshot size, duration) are now written to the +# log. restic backup now runs with --json and the details are +# read from its backup summary. Added totals at end of run. +# Added required-command check. +# + +set -o errexit +set -o nounset +set -o pipefail + +SCRIPT_VERSION="20261031-01" + +#------------------------------------------------------------------------------ +# Configuration +#------------------------------------------------------------------------------ + +LOG_FILE="/home/glibackup/openwrt-backup.log" +SOURCE_DIR="/home/glibackup/openwrt" +STAGING_DIR="/home/Backups/openwrt" +TIMESTAMP="$(date +%Y%m%d-%H%M%S)" +HOSTNAME_FQDN="$(hostname -f)" + +RESTIC_TAGS=(openwrt backup) + +# Restic repository configuration +export RESTIC_REPOSITORY="s3:https://s3.us-west-004.backblazeb2.com/ackitsbucket/openwrt" +export RESTIC_ENV_FILE="/root/.restic-env" + +RESTIC_OUTPUT="" +STAGING_IN_USE="" + +#------------------------------------------------------------------------------ +# Logging Function +#------------------------------------------------------------------------------ + +log() { + + local message + + message="[$(date '+%F %T')] $*" + + echo "$message" + echo "$message" >> "$LOG_FILE" +} + +#------------------------------------------------------------------------------ +# Convert a byte count to a human-readable size (e.g. 1.23GiB) +#------------------------------------------------------------------------------ + +human_bytes() { + + local bytes="${1:-0}" + + if command -v numfmt >/dev/null 2>&1; then + numfmt --to=iec-i --suffix=B --format="%.2f" "$bytes" 2>/dev/null \ + || echo "${bytes} B" + else + echo "${bytes} B" + fi +} + +#------------------------------------------------------------------------------ +# Write Restic snapshot details from a backup summary (JSON) to the log +#------------------------------------------------------------------------------ + +log_snapshot_info() { + + local summary="$1" + + local snapshot_id parent_id + local files_new files_changed files_unmodified + local dirs_new dirs_changed dirs_unmodified + local data_added data_added_packed + local total_files total_bytes total_duration + local tags + + snapshot_id=$(jq -r '.snapshot_id // "none"' <<< "$summary") + parent_id=$(restic snapshots "$snapshot_id" --json 2>/dev/null \ + | jq -r '.[0].parent // "none"' 2>/dev/null || echo "unknown") + + files_new=$(jq -r '.files_new // 0' <<< "$summary") + files_changed=$(jq -r '.files_changed // 0' <<< "$summary") + files_unmodified=$(jq -r '.files_unmodified // 0' <<< "$summary") + dirs_new=$(jq -r '.dirs_new // 0' <<< "$summary") + dirs_changed=$(jq -r '.dirs_changed // 0' <<< "$summary") + dirs_unmodified=$(jq -r '.dirs_unmodified // 0' <<< "$summary") + data_added=$(jq -r '.data_added // 0' <<< "$summary") + data_added_packed=$(jq -r '.data_added_packed // empty' <<< "$summary") + total_files=$(jq -r '.total_files_processed // 0' <<< "$summary") + total_bytes=$(jq -r '.total_bytes_processed // 0' <<< "$summary") + total_duration=$(jq -r '.total_duration // 0' <<< "$summary") + + tags=$(IFS=,; echo "${RESTIC_TAGS[*]}" | sed 's/,/, /g') + + log "---------------- Restic Snapshot -----------------" + log "Snapshot ID: ${snapshot_id:0:8} (${snapshot_id})" + log "Parent snapshot: ${parent_id:0:8}" + log "Tags: ${tags}" + log "Files: ${files_new} new, ${files_changed} changed, ${files_unmodified} unmodified" + log "Directories: ${dirs_new} new, ${dirs_changed} changed, ${dirs_unmodified} unmodified" + if [[ -n "$data_added_packed" ]]; then + log "Data added: $(human_bytes "$data_added") ($(human_bytes "$data_added_packed") stored)" + else + log "Data added: $(human_bytes "$data_added")" + fi + log "Snapshot size: $(human_bytes "$total_bytes") in ${total_files} files" + log "Duration: $(awk -v d="$total_duration" 'BEGIN{printf "%.1f", d}') seconds" + log "--------------------------------------------------" +} + +#------------------------------------------------------------------------------ +# Cleanup Function +#------------------------------------------------------------------------------ + +cleanup() { + + if [[ -n "${STAGING_IN_USE:-}" && -d "$STAGING_DIR" ]]; then + log "Cleaning up staging directory ${STAGING_DIR}" + rm -rf "$STAGING_DIR" || log "WARNING: Failed to remove ${STAGING_DIR}" + STAGING_IN_USE="" + fi + + if [[ -n "${RESTIC_OUTPUT:-}" ]]; then + rm -f "$RESTIC_OUTPUT" + RESTIC_OUTPUT="" + fi +} + +#------------------------------------------------------------------------------ +# Error Handling +#------------------------------------------------------------------------------ + +error_handler() { + + local exit_code=$? + local line_number=$1 + + trap - ERR + + log "ERROR: Backup failed on line ${line_number} with exit code ${exit_code}" + + cleanup + + log "----------------------------------------------------" + + exit "${exit_code}" +} + +interrupt_handler() { + + trap - SIGINT SIGTERM + + log "ERROR: Backup interrupted by signal" + + cleanup + + log "----------------------------------------------------" + + exit 1 +} + +trap 'error_handler $LINENO' ERR +trap interrupt_handler SIGINT SIGTERM + +#------------------------------------------------------------------------------ +# Startup Logging +#------------------------------------------------------------------------------ + +log "====================================================" +log "OpenWrt Backup Starting (openwrtupload.sh v${SCRIPT_VERSION})" +log "Host: ${HOSTNAME_FQDN}" +log "Source Directory: ${SOURCE_DIR}" +log "Log File: ${LOG_FILE}" +log "====================================================" + +#------------------------------------------------------------------------------ +# Verify Requirements +#------------------------------------------------------------------------------ + +for cmd in restic jq awk tar; do + + if ! command -v "$cmd" >/dev/null 2>&1; then + log "ERROR: Missing required command: $cmd" + exit 1 + fi + +done + +#------------------------------------------------------------------------------ +# Load Restic Environment +#------------------------------------------------------------------------------ + +# shellcheck source=/dev/null +source "$RESTIC_ENV_FILE" + +#------------------------------------------------------------------------------ +# Locate Archives +#------------------------------------------------------------------------------ + +shopt -s nullglob +ARCHIVES=("$SOURCE_DIR"/*.gz) +shopt -u nullglob + +if [[ ${#ARCHIVES[@]} -eq 0 ]]; then + log "No backup files found." + log "----------------------------------------------------" + exit 0 +fi + +log "Found ${#ARCHIVES[@]} archive(s) to back up" + +#------------------------------------------------------------------------------ +# Process Each Archive +#------------------------------------------------------------------------------ + +TAG_ARGS=() +for tag in "${RESTIC_TAGS[@]}"; do + TAG_ARGS+=(--tag "$tag") +done + +total_bytes_all=0 +total_added_all=0 +archive_count=0 + +for FILE in "${ARCHIVES[@]}"; do + + log "Backing up archive ${FILE}..." + + # Start from an empty staging directory + rm -rf "$STAGING_DIR" + mkdir -p "$STAGING_DIR" + STAGING_IN_USE="yes" + + log "Extracting archive ${FILE} to ${STAGING_DIR}" + + tar xzf "$FILE" -C "$STAGING_DIR" + + log "Backing up ${STAGING_DIR} with restic" + + # Capture restic's JSON output in a temporary file. Errors and + # warnings still go to the log via stderr. + RESTIC_OUTPUT="$(mktemp /tmp/restic-backup-XXXXXX.json)" + + restic backup \ + "$STAGING_DIR" \ + --json \ + "${TAG_ARGS[@]}" \ + > "$RESTIC_OUTPUT" 2>> "$LOG_FILE" + + summary=$(jq -cR 'fromjson? | select(.message_type == "summary")' \ + "$RESTIC_OUTPUT" | tail -n 1) + + rm -f "$RESTIC_OUTPUT" + RESTIC_OUTPUT="" + + if [[ -n "$summary" ]]; then + log_snapshot_info "$summary" + snap_bytes=$(jq -r '.total_bytes_processed // 0' <<< "$summary") + added_bytes=$(jq -r '.data_added // 0' <<< "$summary") + else + log "WARNING: Restic returned no snapshot summary for ${FILE}" + snap_bytes=0 + added_bytes=0 + fi + + total_bytes_all=$(awk -v a="$total_bytes_all" -v b="$snap_bytes" 'BEGIN{printf "%.0f", a + b}') + total_added_all=$(awk -v a="$total_added_all" -v b="$added_bytes" 'BEGIN{printf "%.0f", a + b}') + archive_count=$((archive_count + 1)) + + log "Cleaning up staging directory ${STAGING_DIR}" + rm -rf "$STAGING_DIR" + STAGING_IN_USE="" + + log "Backup Complete." + + log "Removing archive file ${FILE}" + rm -f "$FILE" + + log "----------------------------------------------------" + +done + +#------------------------------------------------------------------------------ +# Completion +#------------------------------------------------------------------------------ + +log "All archives backed up successfully." +log "Archives Processed: ${archive_count}" +log "Total Backup Size: $(human_bytes "$total_bytes_all")" +log "Total New Data Uploaded: $(human_bytes "$total_added_all")" +log "====================================================" + +exit 0