#!/usr/bin/env bash # Общие функции для скриптов nextcloud_install # Подключение: source "$(dirname "$0")/../lib/common.sh" # Пользователь, от имени которого запущен sudo (или текущий) get_invoke_user() { if [[ -n "${SUDO_USER:-}" && "${SUDO_USER}" != root ]]; then echo "${SUDO_USER}" else id -un fi } # Требует root; если нет — перезапускает скрипт через sudo -E require_root() { if [[ "${EUID}" -ne 0 ]]; then exec sudo -E bash "$(readlink -f "${BASH_SOURCE[1]}")" "$@" fi } # Docker-скрипты не должны работать от root: перезапуск от SUDO_USER drop_root_if_sudo() { local script="${1:-${BASH_SOURCE[1]:-}}" script="$(readlink -f "${script}")" [[ "${EUID}" -ne 0 ]] && return 0 local u="${SUDO_USER:-}" if [[ -z "${u}" || "${u}" == root ]]; then echo "[ERROR] Не запускайте docker-скрипты от root." >&2 echo " Используйте: ./$(basename "${script}")" >&2 exit 1 fi local home home="$(getent passwd "${u}" | cut -d: -f6)" # sudo -u сбрасывает группу docker — перезапуск через sg docker if command -v sg &>/dev/null && id -nG "${u}" | grep -qw docker; then exec sudo -u "${u}" -E env HOME="${home}" USER="${u}" LOGNAME="${u}" \ sg docker -c "bash $(printf '%q' "${script}") $(printf '%q ' "${@:2}")" fi exec sudo -u "${u}" -E env HOME="${home}" USER="${u}" LOGNAME="${u}" \ bash "${script}" "${@:2}" } # docker compose (plugin) или docker-compose (standalone) compose_bin() { if docker compose version &>/dev/null 2>&1; then echo "docker compose" elif command -v docker-compose &>/dev/null && docker-compose version &>/dev/null 2>&1; then echo "docker-compose" else return 1 fi } # Создать каталог на хосте (нужен sudo для /var/nextcloud и т.п.) ensure_host_dir() { local d="$1" [[ -z "${d}" ]] && return 1 if [[ -d "${d}" ]]; then return 0 fi if mkdir -p "${d}" 2>/dev/null; then return 0 fi sudo mkdir -p "${d}" } # Права www-data (uid 33) для каталогов Nextcloud chown_wwwdata_dir() { local d="$1" sudo chown 33:33 "${d}" sudo chmod 0775 "${d}" } # Каталог config.php: всегда www-data, иначе веб-установщик и occ ломаются fix_config_permissions() { local cfg="$1" [[ -d "${cfg}" ]] || return 1 sudo chown -R 33:33 "${cfg}" sudo find "${cfg}" -type d -exec chmod 775 {} \; sudo find "${cfg}" -type f -exec chmod 664 {} \; [[ -f "${cfg}/config.php" ]] && sudo chmod 660 "${cfg}/config.php" } # Запись файла в config (через sudo — каталог принадлежит www-data) config_write_file() { local f="$1" ensure_host_dir "$(dirname "$f")" sudo tee "${f}" >/dev/null sudo chown 33:33 "${f}" sudo chmod 664 "${f}" } # Скопировать config.defaults → NEXTCLOUD_CONFIG_DIR restore_config_templates() { local src="$1" cfg="$2" instanceid="$3" force="${4:-false}" ensure_host_dir "${cfg}" if [[ "${force}" == true ]]; then sudo find "${cfg}" -mindepth 1 -maxdepth 1 -exec rm -rf {} + fi if [[ -d "${src}" ]]; then sudo cp -a "${src}/." "${cfg}/" fi config_write_file "${cfg}/instanceid.config.php" < '${instanceid}', ); EOF config_write_file "${cfg}/datadir.config.php" <<'EOF' false, ); EOF fix_config_permissions "${cfg}" } backup_and_remove_config_php() { local cfg="$1" reason="${2:-reinstall}" [[ -f "${cfg}/config.php" ]] || return 0 local ts ts="$(date +%Y%m%d-%H%M%S)" echo "[WARN] ${reason}: старый config.php сохранён как config.php.bak.${ts}" sudo cp -a "${cfg}/config.php" "${cfg}/config.php.bak.${ts}" 2>/dev/null || true sudo rm -f "${cfg}/config.php" "${cfg}/CAN_INSTALL" } config_php_has_instanceid() { local cfg="$1" instanceid="$2" [[ -f "${cfg}/config.php" ]] || return 1 grep -q "'instanceid' => '${instanceid}'" "${cfg}/config.php" 2>/dev/null } docker_volume_exists() { local volume="$1" if docker info &>/dev/null 2>&1; then docker volume inspect "${volume}" &>/dev/null elif command -v sg &>/dev/null && id -nG | grep -qw docker; then sg docker -c "docker volume inspect $(printf '%q' "${volume}")" &>/dev/null else return 1 fi } prepare_config_for_reinstall() { local src="$1" cfg="$2" instanceid="$3" ensure_host_dir "${cfg}" if [[ -f "${cfg}/config.php" && ! -s "${cfg}/config.php" ]]; then backup_and_remove_config_php "${cfg}" "Пустой config.php" fi if [[ -f "${cfg}/config.php" ]] && ! config_php_has_instanceid "${cfg}" "${instanceid}"; then backup_and_remove_config_php "${cfg}" "instanceid в .env изменился" fi # Если БД volume отсутствует, это новая установка после purge. Старый config.php # приведёт к ошибке Nextcloud про CAN_INSTALL, поэтому убираем его заранее. if [[ -f "${cfg}/config.php" ]] && ! docker_volume_exists nextcloud_db_data; then backup_and_remove_config_php "${cfg}" "БД volume отсутствует после purge" fi restore_config_templates "${src}" "${cfg}" "${instanceid}" false } # occ maintenance:install (нужен запущенный контейнер app) occ_maintenance_install() { # shellcheck disable=SC1091 source "$(pwd)/.env" run_compose exec -T -u www-data app php occ maintenance:install \ --database mysql \ --database-name "${MYSQL_DATABASE}" \ --database-user "${MYSQL_USER}" \ --database-pass "${MYSQL_PASSWORD}" \ --database-host db \ --admin-user "${NEXTCLOUD_ADMIN_USER}" \ --admin-pass "${NEXTCLOUD_ADMIN_PASSWORD}" \ --data-dir /var/www/html/data } # Установить Nextcloud если status.php: installed=false ensure_nextcloud_installed() { local port="${1:-8080}" local installed installed="$(curl -sf "http://127.0.0.1:${port}/status.php" 2>/dev/null | grep -o '"installed":[^,]*' | cut -d: -f2 || echo false)" if [[ "${installed}" == "true" ]]; then return 0 fi # shellcheck disable=SC1091 source "$(pwd)/.env" if [[ -f "${NEXTCLOUD_CONFIG_DIR}/config.php" ]]; then backup_and_remove_config_php "${NEXTCLOUD_CONFIG_DIR}" "Nextcloud не установлен, но config.php уже есть" restore_config_templates "$(pwd)/config.defaults" "${NEXTCLOUD_CONFIG_DIR}" "${NEXTCLOUD_INSTANCEID}" false run_compose restart app sleep 5 fi echo "[INFO] Nextcloud не установлен — occ maintenance:install..." if ! occ_maintenance_install; then echo "[WARN] occ install не удался. Попробуйте: ./restore-config.sh --force" >&2 return 1 fi fix_config_permissions "${NEXTCLOUD_CONFIG_DIR}" return 0 } # Вернуть владельца .env пользователю, вызвавшему sudo fix_env_ownership() { local env_file="${1:-.env}" local user user="$(get_invoke_user)" if [[ -f "${env_file}" && "${user}" != root && "${EUID}" -eq 0 ]]; then chown "${user}:${user}" "${env_file}" fi } # Запуск setup-env от имени обычного пользователя (не root) run_setup_env() { local script_dir="$1" local domain="${2:-}" local user user="$(get_invoke_user)" if [[ "${EUID}" -eq 0 && "${user}" != root ]]; then sudo -u "${user}" bash "${script_dir}/setup-env.sh" ${domain:+"${domain}"} fix_env_ownership "${script_dir}/.env" else bash "${script_dir}/setup-env.sh" ${domain:+"${domain}"} fi } # Проверка: бинарник docker в PATH docker_is_installed() { command -v docker &>/dev/null } # Проверка: docker compose v2 доступен docker_compose_available() { docker compose version &>/dev/null 2>&1 } # Проверка доступности docker (пробует несколько способов) check_docker_access() { if docker info &>/dev/null 2>&1; then return 0 fi # Группа docker ещё не подхватилась в текущей сессии после usermod if command -v sg &>/dev/null && sg docker -c "docker info" &>/dev/null 2>&1; then return 0 fi if sudo docker info &>/dev/null 2>&1; then return 0 fi if [[ "${EUID}" -eq 0 ]]; then echo "[ERROR] Docker недоступен. Не запускайте docker-скрипты через sudo." >&2 echo " Используйте: ./install.sh (без sudo)" >&2 else echo "[ERROR] Docker недоступен." >&2 echo " Добавьте пользователя в группу docker: sudo usermod -aG docker \$USER" >&2 echo " Или перезапустите install.sh — предложит установку Docker." >&2 fi return 1 } # Выполнить docker-команду (docker / sg docker / sudo docker) run_docker() { if docker info &>/dev/null 2>&1; then docker "$@" elif command -v sg &>/dev/null && id -nG | grep -qw docker; then sg docker -c "docker $(printf '%q ' "$@")" elif command -v sg &>/dev/null && sg docker -c "docker $(printf '%q ' "$@")" 2>/dev/null; then : else echo "[ERROR] Docker недоступен" >&2 return 1 fi } # Файлы compose (через COMPOSE_FILE — без -f, чтобы не ломать sudo/sg) compose_env_setup() { local dir dir="$(pwd)" export COMPOSE_FILE="${dir}/docker-compose.yml" if [[ -f "${dir}/.ssl-enabled" ]]; then export COMPOSE_FILE="${COMPOSE_FILE}:${dir}/docker-compose.ssl.yml" fi } run_compose() { if [[ "${EUID}" -eq 0 ]]; then echo "[ERROR] Docker Compose нельзя запускать от root." >&2 echo " Запустите: ./install.sh (без sudo)" >&2 return 1 fi compose_env_setup local dir cb dir="$(pwd)" cb="$(compose_bin)" || { echo "[ERROR] Нужен Docker Compose v2 (docker compose) или docker-compose." >&2 echo " sudo apt install docker-compose-plugin" >&2 return 1 } _compose_run() { # shellcheck disable=SC2086 (cd "${dir}" && ${cb} "$@") } if docker info &>/dev/null 2>&1; then _compose_run "$@" return $? fi if command -v sg &>/dev/null && id -nG | grep -qw docker; then sg docker -c "cd $(printf '%q' "${dir}") && COMPOSE_FILE=$(printf '%q' "${COMPOSE_FILE}") ${cb} $(printf '%q ' "$@")" return $? fi echo "[ERROR] Docker недоступен." >&2 echo " sudo usermod -aG docker $(id -un) && newgrp docker" >&2 return 1 } app_code_ready() { run_compose exec -T app test -f /var/www/html/lib/versioncheck.php &>/dev/null } wait_for_app_code() { local waited=0 limit="${1:-90}" while [[ "${waited}" -lt "${limit}" ]]; do app_code_ready && return 0 sleep 3 waited=$((waited + 3)) done return 1 } repair_app_code_volume() { echo "[WARN] Код Nextcloud в volume nextcloud_app_data неполный." echo "[WARN] Пересоздаю только nextcloud_app_data (config/data/БД не трогаются)." run_compose down --remove-orphans run_docker volume rm nextcloud_app_data >/dev/null 2>&1 || true run_compose up -d wait_for_app_code 120 } ensure_app_code_ready() { if wait_for_app_code 90; then return 0 fi repair_app_code_volume || { echo "[ERROR] Не удалось подготовить код Nextcloud в /var/www/html." >&2 echo " Проверьте: docker logs nextcloud-app" >&2 return 1 } } # Предложить/выполнить установку Docker (вызывается из install.sh) # $1 = путь к install-docker.sh, $2 = auto (yes/no/skip/prompt) ensure_docker() { local installer="$1" local auto="${2:-prompt}" # Docker уже работает (напрямую, через sg или sudo) if docker info &>/dev/null 2>&1 && docker compose version &>/dev/null 2>&1; then return 0 fi if command -v sg &>/dev/null && sg docker -c "docker info" &>/dev/null 2>&1 \ && sg docker -c "docker compose version" &>/dev/null 2>&1; then return 0 fi if sudo docker info &>/dev/null 2>&1 && sudo docker compose version &>/dev/null 2>&1; then return 0 fi if [[ "${auto}" == "skip" ]]; then check_docker_access || return 1 return 0 fi # Docker есть, но нет compose plugin if docker_is_installed && ! docker_compose_available; then echo "[ERROR] Docker установлен, но нет Compose v2." >&2 echo " sudo apt install docker-compose-plugin" >&2 return 1 fi echo "" if docker_is_installed; then echo "Docker установлен, но недоступен без sudo (возможно, нужен перелогин)." echo "Скрипт попробует добавить вас в группу docker." else echo "Docker не найден." echo "Скрипт может установить Docker Engine + Compose v2 (Ubuntu/Debian)." fi echo "Потребуется sudo и доступ к интернету." echo "" local answer="${auto}" if [[ "${answer}" == "prompt" ]]; then read -r -p "Продолжить установку/настройку Docker? (yes/no): " answer fi if [[ "${answer}" != "yes" ]]; then echo "[INFO] Пропущено. Вручную: sudo ${installer} $(id -un)" return 1 fi [[ -f "${installer}" ]] || { echo "[ERROR] Не найден ${installer}" >&2; return 1; } echo "[INFO] Запуск (sudo)..." sudo bash "${installer}" "$(id -un)" sleep 2 if docker info &>/dev/null 2>&1 || sg docker -c "docker info" &>/dev/null 2>&1 \ || sudo docker info &>/dev/null 2>&1; then echo "[INFO] Docker готов." return 0 fi echo "[WARN] Docker установлен. Перелогиньтесь или: newgrp docker" return 1 } # occ от www-data (работает и под sudo, и под root) run_occ() { local nc_dir="$1" shift if [[ "${EUID}" -eq 0 ]]; then sudo -u www-data php "${nc_dir}/occ" "$@" else sudo -u www-data php "${nc_dir}/occ" "$@" fi }