Files
Qortal-Nextcloud-Integration/upgrade-devprod-nextcloud.sh

798 lines
24 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
env_file="${repo_root}/.env.devprod"
source "${repo_root}/scripts/lib-compose-project.sh"
mode="ssl"
nextcloud_image_override=""
nextcloud_major_shortcut=""
pull_image="1"
do_backup="1"
skip_precheck="0"
skip_image_check="0"
backup_dir=""
db_engine_override=""
usage() {
cat <<'USAGE'
Usage: ./upgrade-devprod-nextcloud.sh [major] [options]
Safely upgrades the Nextcloud app/cron containers in devprod mode.
Options:
major Resolve latest official nextcloud:<major>.*-apache image
--ssl Use docker-compose.devprod.yml (default)
--nossl Use docker-compose.devprod.nossl.yml
--env-file <path> Override env file (default: ./.env.devprod)
--nc34 Shortcut for --nextcloud-image nextcloud:34-apache
--nc33 Legacy shortcut for --nextcloud-image nextcloud:33-apache
--nextcloud-image <image> Target image; persisted to env after success
--backup-dir <dir> Backup output dir
--db-engine <mysql|postgres> Force DB engine for backup (default: auto-detect from env)
--skip-backup Skip backup step
--skip-precheck Skip status precheck (not recommended)
--skip-image-check Skip Docker Hub/running image update notice
--no-pull Skip docker pull for selected image
-h, --help Show this help
Examples:
./upgrade-devprod-nextcloud.sh 34
./upgrade-devprod-nextcloud.sh --nc34
./upgrade-devprod-nextcloud.sh --nossl --nextcloud-image nextcloud:34.0.0-apache
USAGE
}
read_kv() {
local key="$1"
local line
line="$(grep -m1 -E "^${key}=" "${env_file}" || true)"
if [[ -z "${line}" ]]; then
return 1
fi
echo "${line#*=}"
}
set_kv() {
local key="$1"
local value="$2"
local tmp_file=""
tmp_file="$(mktemp)"
awk -v key="${key}" -v value="${value}" '
BEGIN { found = 0 }
$0 ~ "^" key "=" {
print key "=" value
found = 1
next
}
{ print }
END {
if (!found) {
print key "=" value
}
}
' "${env_file}" > "${tmp_file}"
mv "${tmp_file}" "${env_file}"
}
resolve_path_from_env() {
local key="$1"
local default_path="$2"
local value
value="$(read_kv "${key}" || true)"
value="${value:-${default_path}}"
if [[ "${value}" == /* ]]; then
echo "${value}"
else
echo "${repo_root}/${value}"
fi
}
compose() {
docker compose -f "${compose_file}" --env-file "${env_file}" "$@"
}
resolve_latest_nextcloud_major_image() {
local major="$1"
local timeout_seconds="${2:-8}"
if ! command -v python3 >/dev/null 2>&1; then
echo "python3 is required to resolve the latest Nextcloud major image." >&2
return 1
fi
python3 - "${major}" "${timeout_seconds}" <<'PY'
import json
import re
import sys
import urllib.error
import urllib.request
major = int(sys.argv[1])
timeout = int(sys.argv[2])
best = None
best_tag = None
url = "https://hub.docker.com/v2/repositories/library/nextcloud/tags?page_size=100"
try:
while url:
with urllib.request.urlopen(url, timeout=timeout) as response:
payload = json.load(response)
for entry in payload.get("results", []):
name = str(entry.get("name", "")).strip()
match = re.fullmatch(r"(\d+)(?:\.(\d+))?(?:\.(\d+))?-apache", name)
if not match or int(match.group(1)) != major:
continue
minor = int(match.group(2)) if match.group(2) is not None else -1
patch = int(match.group(3)) if match.group(3) is not None else -1
version_key = (major, minor, patch)
if best is None or version_key > best:
best = version_key
best_tag = name
url = payload.get("next")
except urllib.error.URLError as exc:
print(f"Docker Hub lookup failed: {getattr(exc, 'reason', exc)}", file=sys.stderr)
sys.exit(1)
except Exception as exc:
print(f"Docker Hub lookup failed: {exc}", file=sys.stderr)
sys.exit(1)
if not best_tag:
print(f"No official nextcloud:{major}.*-apache image tag found on Docker Hub", file=sys.stderr)
sys.exit(1)
print(f"nextcloud:{best_tag}")
PY
}
ensure_target_image_ready() {
if [[ "${pull_image}" == "1" ]]; then
echo "Pulling image: ${NEXTCLOUD_IMAGE}"
docker pull "${NEXTCLOUD_IMAGE}"
return 0
fi
if docker image inspect "${NEXTCLOUD_IMAGE}" >/dev/null 2>&1; then
echo "Using existing local image: ${NEXTCLOUD_IMAGE}"
return 0
fi
echo "Target image is not present locally and --no-pull was requested: ${NEXTCLOUD_IMAGE}"
echo "Either remove --no-pull or pre-pull the image first."
exit 1
}
occ_exec() {
compose exec -T --user www-data app php occ "$@"
}
dump_mariadb() {
local dump_user="$1"
local dump_password="$2"
local dump_db="$3"
local dump_out="$4"
local dump_err="$5"
local dump_cmd='if command -v mysqldump >/dev/null 2>&1; then
exec mysqldump --single-transaction --skip-lock-tables -u"$DUMP_USER" "$DUMP_DB"
elif command -v mariadb-dump >/dev/null 2>&1; then
exec mariadb-dump --single-transaction --skip-lock-tables -u"$DUMP_USER" "$DUMP_DB"
else
echo "Neither mysqldump nor mariadb-dump is available in db container." >&2
exit 127
fi'
compose exec -T \
-e DUMP_USER="${dump_user}" \
-e DUMP_DB="${dump_db}" \
-e MYSQL_PWD="${dump_password}" \
db sh -lc "${dump_cmd}" \
> "${dump_out}" 2> "${dump_err}"
}
dump_postgres() {
local dump_user="$1"
local dump_password="$2"
local dump_db="$3"
local dump_out="$4"
local dump_err="$5"
local dump_cmd='if command -v pg_dump >/dev/null 2>&1; then
exec pg_dump -U "$DUMP_USER" -d "$DUMP_DB"
else
echo "pg_dump is not available in db container." >&2
exit 127
fi'
compose exec -T \
-e DUMP_USER="${dump_user}" \
-e DUMP_DB="${dump_db}" \
-e PGPASSWORD="${dump_password}" \
db sh -lc "${dump_cmd}" \
> "${dump_out}" 2> "${dump_err}"
}
create_backup_archive() {
local archive_file="$1"
local base_dir="$2"
local source_path="$3"
local error_file="${archive_file}.stderr.log"
local tar_rc="0"
set +e
tar -czf "${archive_file}" -C "${base_dir}" "${source_path}" 2> "${error_file}"
tar_rc="$?"
set -e
if [[ "${tar_rc}" -eq 0 ]]; then
rm -f "${error_file}"
return 0
fi
if [[ "${tar_rc}" -eq 1 ]] && grep -q "file changed as we read it" "${error_file}"; then
echo "Archive completed with live-file warnings: ${archive_file}"
echo "See: ${error_file}"
return 0
fi
echo "Archive failed: ${archive_file}"
echo "See: ${error_file}"
return "${tar_rc}"
}
status_has_true() {
local status_json="$1"
local key="$2"
echo "${status_json}" | grep -q "\"${key}\":[[:space:]]*true"
}
refresh_bundled_app_from_image() {
local app_id="$1"
if [[ ! "${app_id}" =~ ^[A-Za-z0-9_-]+$ ]]; then
echo "Refusing to refresh invalid bundled app id: ${app_id}"
return 1
fi
compose exec -T app sh -lc '
set -eu
app_id="$1"
src="/usr/src/nextcloud/apps/${app_id}"
dst="/var/www/html/apps/${app_id}"
if [ ! -d "${src}" ]; then
echo "Bundled app source not found in target image: ${src}" >&2
exit 1
fi
rm -rf "${dst}"
mkdir -p /var/www/html/apps
cp -a "${src}" "${dst}"
chown -R www-data:www-data "${dst}"
' sh "${app_id}"
}
replace_custom_app_with_bundled_app_if_available() {
local app_id="$1"
if [[ ! "${app_id}" =~ ^[A-Za-z0-9_-]+$ ]]; then
echo "Refusing to replace invalid app id: ${app_id}"
return 1
fi
compose exec -T app sh -lc '
set -eu
app_id="$1"
src="/usr/src/nextcloud/apps/${app_id}"
custom="/var/www/html/custom_apps/${app_id}"
bundled="/var/www/html/apps/${app_id}"
backup_dir="/var/www/html/data/app-backups/stale-custom-apps"
if [ ! -d "${src}" ] || [ ! -d "${custom}" ]; then
exit 0
fi
stamp="$(date +%Y%m%d-%H%M%S)"
mkdir -p "${backup_dir}"
mv "${custom}" "${backup_dir}/${app_id}-${stamp}"
rm -rf "${bundled}"
cp -a "${src}" "${bundled}"
chown -R www-data:www-data "${bundled}" "${backup_dir}/${app_id}-${stamp}"
echo "Replaced stale custom app ${app_id} with bundled target-image copy."
echo "Stale custom copy moved to data/app-backups/stale-custom-apps/${app_id}-${stamp}"
' sh "${app_id}"
}
refresh_nextcloud_core_from_image() {
compose exec -T app sh -lc '
set -eu
cd /var/www/html
for path in 3rdparty apps core lib ocs ocs-provider resources; do
rm -rf "${path}"
cp -a "/usr/src/nextcloud/${path}" "${path}"
done
find /usr/src/nextcloud -maxdepth 1 -type f -exec cp -a {} /var/www/html/ \;
chown -R www-data:www-data 3rdparty apps core lib ocs ocs-provider resources
find /var/www/html -maxdepth 1 -type f -exec chown www-data:www-data {} \;
'
}
run_occ_upgrade_with_bundled_app_repair() {
local upgrade_output=""
local upgrade_rc="0"
local app_id=""
set +e
upgrade_output="$(occ_exec upgrade 2>&1)"
upgrade_rc="$?"
set -e
printf '%s\n' "${upgrade_output}"
if [[ "${upgrade_rc}" -eq 0 ]]; then
return 0
fi
if printf '%s\n' "${upgrade_output}" | grep -q 'CleanPreviews::__construct().*IAppConfig.*OC\\AllConfig'; then
echo "Detected mixed Nextcloud core files; refreshing image-owned core files from target image."
refresh_nextcloud_core_from_image
echo "Retrying Nextcloud upgrade after core refresh..."
occ_exec upgrade
return
fi
app_id="$(printf '%s\n' "${upgrade_output}" | sed -nE 's/.*app "([^"]+)".*not correctly replaced.*/\1/p' | head -n1)"
if [[ -z "${app_id}" ]]; then
return "${upgrade_rc}"
fi
echo "Refreshing bundled app files from target image: ${app_id}"
refresh_bundled_app_from_image "${app_id}"
echo "Retrying Nextcloud upgrade after bundled app refresh..."
occ_exec upgrade
}
detect_db_engine() {
if [[ -n "${db_engine_override}" ]]; then
echo "${db_engine_override}"
return 0
fi
local nc_db_engine
nc_db_engine="$(read_kv "NEXTCLOUD_DB_ENGINE" || true)"
case "${nc_db_engine}" in
mysql|mariadb)
echo "mysql"
return 0
;;
postgres|postgresql)
echo "postgres"
return 0
;;
esac
local pg_db pg_user
pg_db="$(read_kv "POSTGRES_DB" || true)"
pg_user="$(read_kv "POSTGRES_USER" || true)"
if [[ -n "${pg_db}" && -n "${pg_user}" ]]; then
echo "postgres"
return 0
fi
echo "mysql"
}
wait_occ_ready() {
local attempts="${1:-24}"
local sleep_seconds="${2:-5}"
local i
for i in $(seq 1 "${attempts}"); do
if occ_exec status >/dev/null 2>&1; then
return 0
fi
sleep "${sleep_seconds}"
done
return 1
}
read_status_json() {
occ_exec status --output=json 2>/dev/null || true
}
ensure_bundled_custom_app_compatibility() {
local compat_script="${repo_root}/scripts/check-nextcloud-custom-app-compat.sh"
if [[ ! -x "${compat_script}" ]]; then
return 0
fi
"${compat_script}" --image "${NEXTCLOUD_IMAGE}" --tracked-only
}
update_appstore_app_compatibility() {
local compat_script="${repo_root}/scripts/check-nextcloud-custom-app-compat.sh"
local incompatible_apps=()
local app_id=""
if [[ ! -x "${compat_script}" ]]; then
return 0
fi
if "${compat_script}" --image "${NEXTCLOUD_IMAGE}" --untracked-only; then
return 0
fi
echo "Some app-store custom apps do not currently declare compatibility with ${NEXTCLOUD_IMAGE}."
echo "Attempting app-store updates for the blocking app(s), then re-checking compatibility..."
mapfile -t incompatible_apps < <("${compat_script}" --image "${NEXTCLOUD_IMAGE}" --untracked-only --list-blocking-apps || true)
if [[ "${#incompatible_apps[@]}" -eq 0 ]]; then
echo "Unable to determine blocking app IDs."
return 1
fi
for app_id in "${incompatible_apps[@]}"; do
if [[ -z "${app_id}" ]]; then
continue
fi
echo "Updating app after core upgrade: ${app_id}"
if ! occ_exec app:update "${app_id}"; then
echo "App update did not complete for ${app_id}; continuing to final compatibility check."
fi
replace_custom_app_with_bundled_app_if_available "${app_id}"
occ_exec app:enable "${app_id}" >/dev/null 2>&1 || true
done
"${compat_script}" --image "${NEXTCLOUD_IMAGE}" --untracked-only
}
next_arg_is_value=""
for arg in "$@"; do
if [[ -n "${next_arg_is_value}" ]]; then
case "${next_arg_is_value}" in
env_file) env_file="${arg}" ;;
nextcloud_image_override) nextcloud_image_override="${arg}" ;;
backup_dir) backup_dir="${arg}" ;;
esac
next_arg_is_value=""
continue
fi
case "${arg}" in
--ssl)
mode="ssl"
;;
--nossl)
mode="nossl"
;;
--env-file)
next_arg_is_value="env_file"
;;
--env-file=*)
env_file="${arg#*=}"
;;
--nc34)
nextcloud_image_override="nextcloud:34-apache"
;;
--nc33)
nextcloud_image_override="nextcloud:33-apache"
;;
--nextcloud-image)
next_arg_is_value="nextcloud_image_override"
;;
--nextcloud-image=*)
nextcloud_image_override="${arg#*=}"
;;
--backup-dir)
next_arg_is_value="backup_dir"
;;
--backup-dir=*)
backup_dir="${arg#*=}"
;;
--db-engine)
next_arg_is_value="db_engine_override"
;;
--db-engine=*)
db_engine_override="${arg#*=}"
;;
--skip-backup)
do_backup="0"
;;
--skip-precheck)
skip_precheck="1"
;;
--skip-image-check)
skip_image_check="1"
;;
--no-pull)
pull_image="0"
;;
-h|--help)
usage
exit 0
;;
*)
if [[ "${arg}" =~ ^[0-9]+$ ]]; then
if [[ -n "${nextcloud_major_shortcut}" ]]; then
echo "Only one Nextcloud major shortcut may be provided."
usage
exit 1
fi
nextcloud_major_shortcut="${arg}"
else
echo "Unknown option: ${arg}"
usage
exit 1
fi
;;
esac
done
if [[ -n "${next_arg_is_value}" ]]; then
echo "Missing value for option: --${next_arg_is_value//_/-}"
exit 1
fi
if [[ -n "${nextcloud_major_shortcut}" && -n "${nextcloud_image_override}" ]]; then
echo "Use either a major shortcut or --nextcloud-image, not both."
exit 1
fi
if [[ "${env_file}" != /* ]]; then
env_file="${repo_root}/${env_file}"
fi
if [[ -f "${env_file}" ]]; then
if ! export_compose_project_name_from_env "${env_file}"; then
warn_missing_compose_project_name "${env_file}"
fi
fi
if [[ -n "${db_engine_override}" ]]; then
case "${db_engine_override}" in
mysql|postgres)
;;
*)
echo "Invalid --db-engine value: ${db_engine_override} (expected mysql or postgres)"
exit 1
;;
esac
fi
if [[ ! -f "${env_file}" ]]; then
echo "Missing env file: ${env_file}"
exit 1
fi
if [[ "${mode}" == "nossl" ]]; then
compose_file="${repo_root}/docker-compose.devprod.nossl.yml"
else
compose_file="${repo_root}/docker-compose.devprod.yml"
fi
target_image="${nextcloud_image_override}"
if [[ -n "${nextcloud_major_shortcut}" ]]; then
target_image="$(resolve_latest_nextcloud_major_image "${nextcloud_major_shortcut}")"
echo "Resolved Nextcloud ${nextcloud_major_shortcut} shortcut to ${target_image}"
fi
if [[ -z "${target_image}" ]]; then
target_image="$(read_kv "NEXTCLOUD_IMAGE" || true)"
fi
target_image="${target_image:-nextcloud:34-apache}"
export NEXTCLOUD_IMAGE="${target_image}"
echo "Using compose file: ${compose_file}"
echo "Using env file: ${env_file}"
echo "Target Nextcloud image: ${NEXTCLOUD_IMAGE}"
if [[ "${skip_image_check}" != "1" && -x "${repo_root}/scripts/check-nextcloud-image-update.sh" ]]; then
"${repo_root}/scripts/check-nextcloud-image-update.sh" \
--compose-file "${compose_file}" \
--env-file "${env_file}" \
--image "${NEXTCLOUD_IMAGE}" \
--include-newer-major || true
fi
if ! wait_occ_ready 24 5; then
echo "Nextcloud OCC is not ready; aborting."
exit 1
fi
if [[ "${skip_precheck}" != "1" ]]; then
echo "Running pre-upgrade status check..."
status_json="$(read_status_json)"
if [[ -z "${status_json}" ]]; then
echo "Unable to read OCC status in JSON form. Run manually: docker compose ... exec -T --user www-data app php occ status"
exit 1
fi
if status_has_true "${status_json}" "maintenance"; then
echo "Instance is already in maintenance mode; continuing so this script can finish or settle the upgrade."
fi
if status_has_true "${status_json}" "needsDbUpgrade"; then
echo "Instance already reports a pending database/core upgrade; continuing so this script can finish it."
fi
fi
if [[ "${do_backup}" == "1" ]]; then
timestamp="$(date +%Y%m%d-%H%M%S)"
if [[ -z "${backup_dir}" ]]; then
backup_dir="${repo_root}/backups/nextcloud-upgrade-${timestamp}"
fi
mkdir -p "${backup_dir}"
echo "Creating backup in: ${backup_dir}"
cp -f "${env_file}" "${backup_dir}/env.snapshot"
cp -f "${compose_file}" "${backup_dir}/compose.snapshot.yml"
db_dump_file="${backup_dir}/nextcloud-db.sql"
db_dump_err_file="${backup_dir}/nextcloud-db.stderr.log"
db_engine="$(detect_db_engine)"
echo "Detected DB engine for backup: ${db_engine}"
db_dump_ok="0"
if [[ "${db_engine}" == "postgres" ]]; then
db_name="$(read_kv "POSTGRES_DB" || true)"
db_user="$(read_kv "POSTGRES_USER" || true)"
db_password="$(read_kv "POSTGRES_PASSWORD" || true)"
db_name="${db_name:-nextcloud}"
if [[ -n "${db_user}" && -n "${db_password}" ]]; then
if dump_postgres "${db_user}" "${db_password}" "${db_name}" "${db_dump_file}" "${db_dump_err_file}"; then
db_dump_ok="1"
fi
fi
if [[ "${db_dump_ok}" != "1" ]]; then
echo "PostgreSQL backup failed."
echo "See: ${db_dump_err_file}"
echo "Quick check:"
echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T db sh -lc 'command -v pg_dump || true'"
exit 1
fi
else
db_name="$(read_kv "MYSQL_DATABASE" || true)"
db_user="$(read_kv "MYSQL_USER" || true)"
db_password="$(read_kv "MYSQL_PASSWORD" || true)"
db_root_password="$(read_kv "MYSQL_ROOT_PASSWORD" || true)"
db_name="${db_name:-nextcloud}"
if [[ -n "${db_user}" && -n "${db_password}" ]]; then
if dump_mariadb "${db_user}" "${db_password}" "${db_name}" "${db_dump_file}" "${db_dump_err_file}"; then
db_dump_ok="1"
fi
fi
if [[ "${db_dump_ok}" != "1" && -n "${db_root_password}" ]]; then
if dump_mariadb "root" "${db_root_password}" "${db_name}" "${db_dump_file}" "${db_dump_err_file}"; then
db_dump_ok="1"
fi
fi
if [[ "${db_dump_ok}" != "1" ]]; then
echo "MySQL/MariaDB backup failed."
echo "See: ${db_dump_err_file}"
echo "Quick check:"
echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T db sh -lc 'command -v mysqldump || command -v mariadb-dump || true'"
exit 1
fi
fi
create_backup_archive "${backup_dir}/nextcloud-html.tar.gz" "${repo_root}" nextcloud/html
create_backup_archive "${backup_dir}/nextcloud-data.tar.gz" "${repo_root}" nextcloud/data
custom_apps_path="$(resolve_path_from_env "NEXTCLOUD_CUSTOM_APPS_PATH" "./nextcloud/custom_apps")"
if [[ -d "${custom_apps_path}" ]]; then
create_backup_archive "${backup_dir}/nextcloud-custom-apps.tar.gz" "$(dirname "${custom_apps_path}")" "$(basename "${custom_apps_path}")"
fi
fi
ensure_bundled_custom_app_compatibility
ensure_target_image_ready
echo "Recreating app/cron containers on target image..."
compose up -d --force-recreate --no-deps app cron
if ! wait_occ_ready 36 5; then
echo "Nextcloud OCC did not come back after container recreate."
echo "Fix container health and rerun:"
echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ upgrade"
exit 1
fi
echo "Checking post-recreate upgrade status..."
status_json="$(read_status_json)"
if [[ -z "${status_json}" ]]; then
echo "Unable to read OCC status after container recreate."
echo "Run manually:"
echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ status"
exit 1
fi
if status_has_true "${status_json}" "maintenance"; then
echo "Instance is in maintenance mode after recreate; continuing to the upgrade/settle step."
fi
echo "Running Nextcloud upgrade/settle step..."
run_occ_upgrade_with_bundled_app_repair
status_json="$(read_status_json)"
if [[ -z "${status_json}" ]]; then
echo "Unable to read OCC status after upgrade step."
echo "Run manually:"
echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ status"
exit 1
fi
if echo "${status_json}" | grep -q '"needsDbUpgrade":[[:space:]]*true'; then
echo "Nextcloud still requires a database upgrade after occ upgrade."
echo "Run manually:"
echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ upgrade"
echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ status"
exit 1
fi
update_appstore_app_compatibility
if [[ -x "${repo_root}/scripts/check-nextcloud-custom-app-compat.sh" ]]; then
"${repo_root}/scripts/check-nextcloud-custom-app-compat.sh" --image "${NEXTCLOUD_IMAGE}"
fi
echo "Running post-upgrade repair tasks..."
occ_exec maintenance:repair
occ_exec db:add-missing-indices || true
occ_exec maintenance:mimetype:update-js || true
echo "Reapplying Nextcloud defaults and NuQloud skeleton..."
"${repo_root}/scripts/configure-nextcloud-defaults.sh" \
--compose-file "${compose_file}" \
--env-file "${env_file}" \
--restore-core-skeleton
echo "Restarting app container to clear stale opcache..."
compose restart app
if ! wait_occ_ready 36 5; then
echo "Nextcloud OCC did not come back after post-upgrade restart."
echo "Fix container health and rerun:"
echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ status"
exit 1
fi
final_status_json="$(read_status_json)"
if [[ -z "${final_status_json}" ]]; then
echo "Unable to read OCC status after post-upgrade restart."
echo "Run manually:"
echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ status"
exit 1
fi
if echo "${final_status_json}" | grep -q '"needsDbUpgrade":[[:space:]]*true'; then
echo "Nextcloud still requires a database upgrade after the upgrade run."
echo "Run manually:"
echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ upgrade"
echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ status"
exit 1
fi
if echo "${final_status_json}" | grep -q '"maintenance":[[:space:]]*true'; then
echo "Maintenance mode is still enabled after upgrade; disabling it..."
if ! occ_exec maintenance:mode --off >/dev/null 2>&1; then
echo "Unable to disable maintenance mode automatically."
echo "Run manually:"
echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ maintenance:mode --off"
echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ status"
exit 1
fi
final_status_json="$(read_status_json)"
if [[ -z "${final_status_json}" ]]; then
echo "Unable to re-read OCC status after disabling maintenance mode."
echo "Run manually:"
echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ status"
exit 1
fi
if echo "${final_status_json}" | grep -q '"maintenance":[[:space:]]*true'; then
echo "Maintenance mode is still enabled after attempting to disable it."
echo "Run manually:"
echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ maintenance:mode --off"
echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ status"
exit 1
fi
fi
echo "Final status:"
occ_exec status
persisted_nextcloud_image="$(read_kv "NEXTCLOUD_IMAGE" || true)"
if [[ "${persisted_nextcloud_image}" != "${NEXTCLOUD_IMAGE}" ]]; then
set_kv "NEXTCLOUD_IMAGE" "${NEXTCLOUD_IMAGE}"
echo "Persisted NEXTCLOUD_IMAGE=${NEXTCLOUD_IMAGE} to ${env_file}"
fi
echo "Upgrade complete."