798 lines
24 KiB
Bash
Executable File
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."
|