#!/bin/bash # ============================================================= # nextcloud_update_check.sh # Checks for Nextcloud CORE and app updates daily. # Sends an HTML email report with update commands ready to # copy-paste into terminal. # # Schedule: Daily (recommended 7:00 AM) # Install: /usr/scripts/omv/nextcloud_update_check.sh # Log: /var/log/nextcloud-updates/ # ============================================================= # --- Config -------------------------------------------------- CONTAINER="nextcloud" OCC="sudo docker exec -u www-data ${CONTAINER} php occ" NEXTCLOUD_COMPOSE_DIR="/kingdezignsnas/Docker/Compose/nextcloud" NEXTCLOUD_COMPOSE_FILES="-f nextcloud.yml -f compose.override.yml" NEXTCLOUD_FFMPEG_BUILD_DIR="/kingdezignsnas/Docker/Compose/nextcloud-ffmpeg-build" NEXTCLOUD_FFMPEG_BUILD_FILES="-f nextcloud-ffmpeg-build.yml --env-file nextcloud-ffmpeg-build.env" NEXTCLOUD_PULL_SERVICES="db onlyoffice autoheal" SMTP_SERVER="smtppro.zoho.com" SMTP_PORT="465" SMTP_USER="rufus.king@kingdezigns.com" SMTP_PASS_FILE="/etc/nextcloud-smtp-pass" FROM_NAME="NAS08" FROM_EMAIL="rufus.king@kingdezigns.com" TO_EMAIL="rufus.king@kingdezigns.com" HOSTNAME_LABEL="NAS08" LOG_DIR="/var/log/nextcloud-updates" LOG_RETENTION_DAYS=90 # ------------------------------------------------------------- mkdir -p "$LOG_DIR" DATESTAMP=$(date +"%Y%m%d") TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S") LOGFILE="$LOG_DIR/update-check-${DATESTAMP}.log" log() { echo "[$(date +"%H:%M:%S")] $*" | tee -a "$LOGFILE"; } # Returns 0 when $1 is strictly newer than $2 (e.g. 34.0.3 > 34.0.2). version_gt() { local ver1=$1 ver2=$2 [[ -n "$ver1" && -n "$ver2" ]] || return 1 local IFS=. local -a v1=($ver1) v2=($ver2) local i n1 n2 for ((i=0; i<${#v1[@]} || i<${#v2[@]}; i++)); do n1=$((10#${v1[i]:-0})) n2=$((10#${v2[i]:-0})) if (( n1 > n2 )); then return 0; fi if (( n1 < n2 )); then return 1; fi done return 1 } # Adjusted to strip any "v" prefix or extra spaces cleanly fetch_latest_github_release() { curl -sf --max-time 20 "https://api.github.com/repos/nextcloud/server/releases/latest" \ | grep -oE '"tag_name"[[:space:]]*:[[:space:]]*"v[0-9]+\.[0-9]+\.[0-9]+"' \ | head -1 \ | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' } # --- Load SMTP password -------------------------------------- if [[ ! -f "$SMTP_PASS_FILE" ]]; then log "ERROR: SMTP password file not found at $SMTP_PASS_FILE" exit 1 fi SMTP_PASS=$(cat "$SMTP_PASS_FILE") # --- Check container is running ------------------------------ if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER}$"; then log "ERROR: Container '${CONTAINER}' is not running." STATUS_COLOR="#b91c1c" STATUS_LABEL="CRITICAL" STATUS_MSG="โ›”  Nextcloud container is not running" BODY_HTML="

The Nextcloud Docker container was not found running on ${HOSTNAME_LABEL}. No update check could be performed.

" NEEDS_ACTION=true APP_ROWS="" CMD_BLOCK="" else # --- Get Nextcloud status ---------------------------------- log "Checking Nextcloud status..." NC_STATUS=$($OCC status 2>/dev/null | grep -v '^{') MAINTENANCE=$(echo "$NC_STATUS" | grep "maintenance:" | awk '{print $3}') NEEDS_UPGRADE=$(echo "$NC_STATUS" | grep "needsDbUpgrade:" | awk '{print $3}') NC_VERSION=$(echo "$NC_STATUS" | grep "versionstring:" | awk '{print $3}') log "Nextcloud version: $NC_VERSION | maintenance: $MAINTENANCE | needsDbUpgrade: $NEEDS_UPGRADE" # --- Check for CORE (server) update ------------------------- log "Checking core (server) update via occ update:check..." CORE_CHECK_RAW=$($OCC update:check 2>/dev/null | grep -v '^{') log "occ update:check raw output:" while IFS= read -r line; do [[ -n "$line" ]] && log " $line" done <<< "$CORE_CHECK_RAW" CORE_UPDATE_AVAILABLE=false CORE_NEW_VERSION="" CORE_UPDATE_SOURCE="" if echo "$CORE_CHECK_RAW" | grep -qiE 'Nextcloud [0-9]+\.[0-9]+\.[0-9]+ is available'; then CORE_UPDATE_AVAILABLE=true CORE_NEW_VERSION=$(echo "$CORE_CHECK_RAW" | grep -oiE 'Nextcloud [0-9]+\.[0-9]+\.[0-9]+ is available' | head -1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+') CORE_UPDATE_SOURCE="occ update:check" log "Core update available (occ): Nextcloud ${CORE_NEW_VERSION}" fi log "Checking core update via GitHub latest release fallback..." GITHUB_LATEST=$(fetch_latest_github_release) if [[ -n "$GITHUB_LATEST" ]]; then log "GitHub latest stable release: ${GITHUB_LATEST} (installed: ${NC_VERSION})" if version_gt "$GITHUB_LATEST" "$NC_VERSION"; then if [[ "$CORE_UPDATE_AVAILABLE" != "true" ]] || version_gt "$GITHUB_LATEST" "$CORE_NEW_VERSION"; then CORE_UPDATE_AVAILABLE=true CORE_NEW_VERSION="$GITHUB_LATEST" if [[ -z "$CORE_UPDATE_SOURCE" ]]; then CORE_UPDATE_SOURCE="GitHub latest release (occ update:check lagged)" else CORE_UPDATE_SOURCE="${CORE_UPDATE_SOURCE} + GitHub latest release" fi log "Core update available (GitHub fallback): ${NC_VERSION} โ†’ ${CORE_NEW_VERSION}" fi fi else log "WARN: Could not fetch GitHub latest release for fallback compare." fi # --- Check for app updates -------------------------------- log "Checking app updates..." APP_UPDATE_RAW=$($OCC app:update --all --showonly 2>/dev/null | grep -v '^{' ) UPDATABLE_APPS=() while IFS= read -r line; do if echo "$line" | grep -q "new version available"; then APP_NAME=$(echo "$line" | awk '{print $1}') NEW_VER=$(echo "$line" | awk '{print $NF}') UPDATABLE_APPS+=("${APP_NAME}|${NEW_VER}") log "Update available: $APP_NAME โ†’ $NEW_VER" fi done <<< "$APP_UPDATE_RAW" APP_COUNT=${#UPDATABLE_APPS[@]} # --- Build status ----------------------------------------- NEEDS_ACTION=false WARNINGS=() if [[ "$MAINTENANCE" == "true" ]]; then WARNINGS+=("Nextcloud is currently in maintenance mode") NEEDS_ACTION=true fi if [[ "$NEEDS_UPGRADE" == "true" ]]; then WARNINGS+=("Database upgrade required (needsDbUpgrade: true)") NEEDS_ACTION=true fi if [[ $APP_COUNT -gt 0 ]]; then NEEDS_ACTION=true fi if [[ "$CORE_UPDATE_AVAILABLE" == "true" ]]; then NEEDS_ACTION=true fi # --- Determine overall status color ----------------------- if [[ "$MAINTENANCE" == "true" ]] || [[ "$NEEDS_UPGRADE" == "true" ]]; then STATUS_COLOR="#b91c1c" STATUS_LABEL="CRITICAL" STATUS_MSG="โ›”  Nextcloud requires immediate attention" elif [[ "$CORE_UPDATE_AVAILABLE" == "true" ]] && [[ $APP_COUNT -gt 0 ]]; then STATUS_COLOR="#b45309" STATUS_LABEL="UPDATES AVAILABLE" STATUS_MSG="๐Ÿ”„  Core update to ${CORE_NEW_VERSION} + ${APP_COUNT} app update(s) available" elif [[ "$CORE_UPDATE_AVAILABLE" == "true" ]]; then STATUS_COLOR="#b45309" STATUS_LABEL="UPDATES AVAILABLE" STATUS_MSG="๐Ÿ”„  Nextcloud core update available (${NC_VERSION} โ†’ ${CORE_NEW_VERSION})" elif [[ $APP_COUNT -gt 0 ]]; then STATUS_COLOR="#b45309" STATUS_LABEL="UPDATES AVAILABLE" STATUS_MSG="๐Ÿ”„  ${APP_COUNT} app update(s) available" else STATUS_COLOR="#1a7f4b" STATUS_LABEL="UP TO DATE" STATUS_MSG="โœ…  Nextcloud is fully up to date" fi if [[ "$MAINTENANCE" == "true" ]]; then MAINTENANCE_COLOR="#b91c1c" else MAINTENANCE_COLOR="#1a7f4b" fi # --- Build app update rows -------------------------------- APP_ROWS="" if [[ $APP_COUNT -gt 0 ]]; then for entry in "${UPDATABLE_APPS[@]}"; do APP_NAME=$(echo "$entry" | cut -d'|' -f1) NEW_VER=$(echo "$entry" | cut -d'|' -f2) APP_ROWS+=" ${APP_NAME} ${NEW_VER} " done fi # --- Build warning rows ----------------------------------- WARN_HTML="" for w in "${WARNINGS[@]}"; do WARN_HTML+="
โš  ${w}
" done # --- Build copy-paste command block ----------------------- CMD_LINES="" if [[ "$MAINTENANCE" == "true" ]]; then CMD_LINES+=$'# Disable maintenance mode\nsudo docker exec -u www-data nextcloud php occ maintenance:mode --off\n\n' fi if [[ "$CORE_UPDATE_AVAILABLE" == "true" ]]; then # Corrected step to target the actual service container for your custom image CMD_LINES+="# Update Nextcloud CORE to ${CORE_NEW_VERSION} (do this before app updates)"$'\n' CMD_LINES+="# Step 1: Rebuild nextcloud-ffmpeg from a fresh nextcloud:latest base"$'\n' CMD_LINES+="sudo bash -c \"cd ${NEXTCLOUD_FFMPEG_BUILD_DIR} && docker compose ${NEXTCLOUD_FFMPEG_BUILD_FILES} build --no-cache --pull\""$'\n\n' CMD_LINES+="# Step 2: Pull supporting services only (skip nextcloud/cron โ€” local image)"$'\n' CMD_LINES+="sudo bash -c \"cd ${NEXTCLOUD_COMPOSE_DIR} && docker compose ${NEXTCLOUD_COMPOSE_FILES} pull ${NEXTCLOUD_PULL_SERVICES}\""$'\n\n' CMD_LINES+="# Step 3: Recreate containers to pick up the rebuilt image"$'\n' CMD_LINES+="sudo bash -c \"cd ${NEXTCLOUD_COMPOSE_DIR} && docker compose ${NEXTCLOUD_COMPOSE_FILES} up -d\""$'\n\n' CMD_LINES+="# Give the container's built-in upgrade entrypoint time to finish,"$'\n' CMD_LINES+="# then confirm the new version is active (watch for versionstring: ${CORE_NEW_VERSION})"$'\n' CMD_LINES+="sleep 30"$'\n' CMD_LINES+="sudo docker exec -u www-data nextcloud php occ status"$'\n\n' fi if [[ "$NEEDS_UPGRADE" == "true" ]]; then CMD_LINES+=$'# Run database upgrade\nsudo docker exec -u www-data nextcloud php occ upgrade --no-interaction\n\n' fi if [[ $APP_COUNT -gt 0 ]]; then CMD_LINES+=$'# Update all apps\nsudo docker exec -u www-data nextcloud php occ app:update --all\n\n' fi if [[ -z "$CMD_LINES" ]]; then CMD_LINES="# No action required โ€” everything is up to date"$'\n' fi CMD_LINES+=$'# Verify status after updates\nsudo docker exec -u www-data nextcloud php occ status' CMD_BLOCK="
๐Ÿ“‹ Commands to run on NAS08
${CMD_LINES}
" MAINT_BLOCK="" if [[ -n "$WARN_HTML" ]]; then MAINT_BLOCK="
โš  Issues Detected
${WARN_HTML}
" fi CORE_BLOCK="" if [[ "$CORE_UPDATE_AVAILABLE" == "true" ]]; then CORE_SOURCE_NOTE="" if [[ -n "$CORE_UPDATE_SOURCE" ]]; then CORE_SOURCE_NOTE=" Detected Via ${CORE_UPDATE_SOURCE} " fi CORE_BLOCK="
๐Ÿ†™ Core Update Available
${CORE_SOURCE_NOTE}
Current Version ${NC_VERSION}
New Version ${CORE_NEW_VERSION}
This install uses a custom nextcloud-ffmpeg:latest image. The emailed commands rebuild that image first โ€” do not run a plain docker compose pull in the main project.
" fi APP_TABLE="" if [[ $APP_COUNT -gt 0 ]]; then APP_TABLE="
๐Ÿ”„ App Updates Available (${APP_COUNT})
${APP_ROWS}
App New Version
" else APP_TABLE="
โœ“ All apps are up to date
" fi BODY_HTML="${MAINT_BLOCK}
Nextcloud Version ${NC_VERSION} Maintenance Mode ${MAINTENANCE:-false}
${CORE_BLOCK} ${APP_TABLE} ${CMD_BLOCK}" fi if [[ "$NEEDS_ACTION" == "true" ]]; then SUBJECT="[NAS08] Nextcloud โ€” ${STATUS_LABEL} โ€” Action Required" else SUBJECT="[NAS08] Nextcloud โ€” All Up To Date" fi log "Composing email: $SUBJECT" HTML_EMAIL=$(cat <
Nextcloud ยท Daily Update Report
โ˜ ${HOSTNAME_LABEL}
${TIMESTAMP} | cloud.kingdezigns.com
${STATUS_LABEL}
${STATUS_MSG}
${BODY_HTML}
Nextcloud Update Check  |  ${HOSTNAME_LABEL}  |  KingDezigns Infrastructure
HTML ) log "Sending email to ${TO_EMAIL}..." SEND_RESULT=$(curl --silent --show-error \ --url "smtps://${SMTP_SERVER}:${SMTP_PORT}" \ --ssl-reqd \ --mail-from "${FROM_EMAIL}" \ --mail-rcpt "${TO_EMAIL}" \ --user "${SMTP_USER}:${SMTP_PASS}" \ --upload-file - <&1 From: ${FROM_NAME} <${FROM_EMAIL}> To: ${TO_EMAIL} Subject: ${SUBJECT} MIME-Version: 1.0 Content-Type: text/html; charset=UTF-8 ${HTML_EMAIL} EOF ) if [[ $? -eq 0 ]]; then log "Email sent successfully." else log "ERROR sending email: ${SEND_RESULT}" fi find "$LOG_DIR" -name "update-check-*.log" -mtime +${LOG_RETENTION_DAYS} -delete log "Done."