443 lines
14 KiB
Bash
443 lines
14 KiB
Bash
#!/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" <<EOF
|
||
<?php
|
||
\$CONFIG = array (
|
||
'instanceid' => '${instanceid}',
|
||
);
|
||
EOF
|
||
|
||
config_write_file "${cfg}/datadir.config.php" <<'EOF'
|
||
<?php
|
||
$CONFIG = array (
|
||
'check_data_directory_permissions' => 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
|
||
}
|