#!/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
| Current Version |
${NC_VERSION} |
| New Version |
${CORE_NEW_VERSION} |
${CORE_SOURCE_NOTE}
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 |
New Version |
${APP_ROWS}
"
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."