2026-07-26 21:34:22 -04:00
#!/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/
#
# 2026-07-24 fix: previously only checked `occ app:update --all
# --showonly`, which reports APP updates only. It never checked
# for a Nextcloud CORE/server version update, so a core release
# (e.g. Nextcloud 34.0.2) was silently missed and the report
# said "up to date" even when it wasn't. Added a separate core
# check via `occ update:check`.
# =============================================================
# --- Config --------------------------------------------------
CONTAINER = "nextcloud"
OCC = " sudo docker exec -u www-data ${ CONTAINER } php occ "
2026-07-27 20:01:02 -04:00
# OMV Compose project location — NOT ~/docker/nextcloud. Confirmed via:
# docker inspect nextcloud --format '{{ index .Config.Labels "com.docker.compose.project.working_dir" }}'
# Directory is root-only (drwx------ root root), so compose commands
# must run under sudo. OMV names its compose files "<project>.yml" +
# "compose.override.yml" — NOT the default "docker-compose.yml" — so
# both -f flags are required or `docker compose` won't find them.
NEXTCLOUD_COMPOSE_DIR = "/kingdezignsnas/Docker/Compose/nextcloud"
NEXTCLOUD_COMPOSE_FILES = "-f nextcloud.yml -f compose.override.yml"
2026-07-26 21:34:22 -04:00
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 " ; }
# --- 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. "
# Send critical alert
STATUS_COLOR = "#b91c1c"
STATUS_LABEL = "CRITICAL"
STATUS_MSG = "⛔ Nextcloud container is not running"
BODY_HTML = " <p style='color:#b91c1c;font-weight:600;'>The Nextcloud Docker container was not found running on ${ HOSTNAME_LABEL } . No update check could be performed.</p> "
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 -------------------------
# occ app:update only reports app updates, never core/server
# releases. occ update:check is the command that actually
# checks the updater channel for a new Nextcloud version.
log "Checking core (server) update..."
CORE_CHECK_RAW = $( $OCC update:check 2>/dev/null | grep -v '^{' )
CORE_UPDATE_AVAILABLE = false
CORE_NEW_VERSION = ""
if echo " $CORE_CHECK_RAW " | grep -qi "is available" ; then
CORE_UPDATE_AVAILABLE = true
CORE_NEW_VERSION = $( echo " $CORE_CHECK_RAW " | grep -oE 'Nextcloud [0-9]+\.[0-9]+\.[0-9]+' | head -1 | awk '{print $2}' )
log " Core update available: Nextcloud ${ CORE_NEW_VERSION } "
else
log "Core is up to date."
fi
# --- Check for app updates --------------------------------
log "Checking app updates..."
# Filter out admin_audit JSON log lines that appear when admin_audit app is enabled
APP_UPDATE_RAW = $( $OCC app:update --all --showonly 2>/dev/null | grep -v '^{' )
# Parse apps with available updates
# Format from occ app:update --showonly: "appname new version available: X.Y.Z"
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
# --- Maintenance mode color (was previously unset/unused) --
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'|' -f4)
APP_ROWS += " <tr>
<td style = 'padding:9px 14px;border-bottom:1px solid #e5e7eb;font-size:13px;color:#111827;font-family:monospace;' >${ APP_NAME } </td>
<td style = 'padding:9px 14px;border-bottom:1px solid #e5e7eb;font-size:13px;color:#1a7f4b;font-weight:600;' >${ NEW_VER } </td>
</tr>"
done
fi
# --- Build warning rows -----------------------------------
WARN_HTML = ""
for w in " ${ WARNINGS [@] } " ; do
WARN_HTML += " <div style='margin-top:10px;padding:10px 14px;background:#fff1f2;border-left:4px solid #b91c1c;border-radius:4px;'>
<span style = 'font-size:13px;color:#7f1d1d;font-weight:600;' >⚠ ${ w } </span>
</div>"
done
# --- Build copy-paste command block -----------------------
# NOTE: must use $'...' (ANSI-C quoting) below, not "...". Plain
# double quotes do NOT interpret \n as a newline in bash — they
# insert the two literal characters \ and n, which is why the
# previous version of this script emailed out commands with
# literal "\n" text instead of real line breaks.
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
2026-07-27 20:01:02 -04:00
# The nextcloud image's own entrypoint auto-detects the version
# bump and runs the upgrade itself on container start — it does
# NOT need `occ upgrade` run manually. Running `occ upgrade`
# immediately after `up -d` can race the container's own startup
# and falsely report "No upgrade required" if checked too early.
# The 30s sleep gives the entrypoint's internal upgrade time to
# finish before the confirming `occ status` call.
CMD_LINES += " # Update Nextcloud CORE to ${ CORE_NEW_VERSION } (do this before app updates) " $'\n'
CMD_LINES += " sudo bash -c \"cd ${ NEXTCLOUD_COMPOSE_DIR } && docker compose ${ NEXTCLOUD_COMPOSE_FILES } pull && 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'
2026-07-26 21:34:22 -04:00
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 = " <div style='margin-top:16px;'>
<div style = 'font-size:11px;font-weight:700;color:#374151;text-transform:uppercase;letter-spacing:.5px;margin-bottom:8px;' >📋 Commands to run on NAS08</div>
<pre style = 'margin:0;background:#1e293b;color:#e2e8f0;padding:14px;border-radius:6px;font-size:12px;line-height:1.6;overflow-x:auto;white-space:pre;' >${ CMD_LINES } </pre>
</div>"
# --- Maintenance/upgrade block ----------------------------
MAINT_BLOCK = ""
if [ [ -n " $WARN_HTML " ] ] ; then
MAINT_BLOCK = " <div style='margin-top:20px;'>
<div style = 'font-size:11px;font-weight:700;color:#b91c1c;text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px;' >⚠ Issues Detected</div>
${ WARN_HTML }
</div>"
fi
# --- Core update block --------------------------------------
CORE_BLOCK = ""
if [ [ " $CORE_UPDATE_AVAILABLE " = = "true" ] ] ; then
CORE_BLOCK = " <div style='margin-top:20px;background:white;border-radius:10px;border:1px solid #e5e7eb;padding:18px 22px;'>
<div style = 'font-size:13px;font-weight:700;color:#374151;margin-bottom:12px;text-transform:uppercase;letter-spacing:.5px;' >🆙 Core Update Available</div>
<table width = '100%' cellspacing = '0' cellpadding = '0' >
<tr>
<td style = 'font-size:13px;color:#6b7280;padding-bottom:6px;' >Current Version</td>
<td style = 'font-size:13px;color:#111827;font-weight:600;padding-bottom:6px;font-family:monospace;' >${ NC_VERSION } </td>
</tr>
<tr>
<td style = 'font-size:13px;color:#6b7280;' >New Version</td>
<td style = 'font-size:13px;color:#b45309;font-weight:700;font-family:monospace;' >${ CORE_NEW_VERSION } </td>
</tr>
</table>
</div>"
fi
# --- App update table -------------------------------------
APP_TABLE = ""
if [ [ $APP_COUNT -gt 0 ] ] ; then
APP_TABLE = " <div style='margin-top:20px;background:white;border-radius:10px;border:1px solid #e5e7eb;padding:18px 22px;'>
<div style = 'font-size:13px;font-weight:700;color:#374151;margin-bottom:12px;text-transform:uppercase;letter-spacing:.5px;' >🔄 App Updates Available ( ${ APP_COUNT } ) </div>
<table width = '100%' cellspacing = '0' cellpadding = '0' style = 'border-collapse:collapse;font-size:13px;' >
<thead>
<tr>
<th style = 'text-align:left;padding:8px 14px;font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:#9ca3af;background:#f9fafb;border-bottom:1px solid #e5e7eb;font-weight:500;' >App</th>
<th style = 'text-align:left;padding:8px 14px;font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:#9ca3af;background:#f9fafb;border-bottom:1px solid #e5e7eb;font-weight:500;' >New Version</th>
</tr>
</thead>
<tbody>${ APP_ROWS } </tbody>
</table>
</div>"
else
APP_TABLE = " <div style='margin-top:20px;padding:12px 14px;background:#f0fdf4;border-radius:6px;border:1px solid #bbf7d0;'>
<span style = 'font-size:13px;color:#166534;font-weight:600;' >✓ All apps are up to date</span>
</div>"
fi
BODY_HTML = " ${ MAINT_BLOCK }
<!-- NC Version info -->
<div style = 'margin-top:20px;padding:12px 14px;background:#f8fafc;border-radius:6px;border:1px solid #e5e7eb;' >
<span style = 'font-size:12px;font-weight:600;color:#374151;text-transform:uppercase;letter-spacing:.5px;' >Nextcloud Version</span>
<span style = 'margin-left:10px;font-size:13px;color:#374151;font-family:monospace;' >${ NC_VERSION } </span>
<span style = 'margin-left:20px;font-size:12px;font-weight:600;color:#374151;text-transform:uppercase;letter-spacing:.5px;' >Maintenance Mode</span>
<span style = 'margin-left:10px;font-size:13px;color:${MAINTENANCE_COLOR};font-family:monospace;' >${ MAINTENANCE :- false } </span>
</div>
${ CORE_BLOCK }
${ APP_TABLE }
${ CMD_BLOCK } "
fi
# --- Determine email subject ---------------------------------
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 "
# --- Build full HTML email -----------------------------------
HTML_EMAIL = $( cat <<HTML
<!DOCTYPE html>
<html>
<head><meta charset = "UTF-8" ><meta name = "viewport" content = "width=device-width,initial-scale=1.0" ></head>
<body style = "margin:0;padding:0;background:#f1f5f9;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;" >
<table width = "100%" cellspacing = "0" cellpadding = "0" style = "background:#f1f5f9;padding:24px 16px;" >
<tr><td align = "center" >
<table width = "100%" cellspacing = "0" cellpadding = "0" style = "max-width:720px;" >
<!-- HEADER -->
<tr><td style = "background:#0f172a;border-radius:10px 10px 0 0;padding:28px 32px;" >
<table width = "100%" cellspacing = "0" cellpadding = "0" ><tr>
<td>
<div style = "font-size:11px;color:#94a3b8;letter-spacing:1px;text-transform:uppercase;" >Nextcloud · Daily Update Report</div>
<div style = "font-size:26px;font-weight:800;color:white;margin-top:4px;" >☁ ${ HOSTNAME_LABEL } </div>
<div style = "font-size:13px;color:#94a3b8;margin-top:4px;" >${ TIMESTAMP } | cloud.kingdezigns.com</div>
</td>
<td align = "right" >
<div style = " background: ${ STATUS_COLOR } ;color:white;padding:10px 20px;border-radius:8px;font-size:14px;font-weight:700; " >${ STATUS_LABEL } </div>
</td>
</tr></table>
</td></tr>
<!-- STATUS BANNER -->
<tr><td style = " background: ${ STATUS_COLOR } ;padding:12px 32px; " >
<span style = "color:white;font-size:14px;font-weight:600;" >${ STATUS_MSG } </span>
</td></tr>
<!-- BODY -->
<tr><td style = "background:white;border-radius:0 0 10px 10px;padding:28px 32px;" >
${ BODY_HTML }
</td></tr>
<!-- FOOTER -->
<tr><td style = "background:#0f172a;border-radius:0 0 10px 10px;padding:16px 32px;text-align:center;margin-top:8px;" >
<span style = "font-size:11px;color:#64748b;" >Nextcloud Update Check & nbsp; | & nbsp; ${ HOSTNAME_LABEL } & nbsp; | & nbsp; KingDezigns Infrastructure</span>
</td></tr>
</table>
</td></tr>
</table>
</body>
</html>
HTML
)
# --- Send email via curl/SMTP --------------------------------
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 - <<EOF 2>& 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
# --- Rotate old logs -----------------------------------------
find " $LOG_DIR " -name "update-check-*.log" -mtime +${ LOG_RETENTION_DAYS } -delete
log "Done."