Import alt-mysql plugin

This commit is contained in:
Marek Miklewicz
2026-07-04 15:48:19 +02:00
commit d71fcf9ace
101 changed files with 18635 additions and 0 deletions
+238
View File
@@ -0,0 +1,238 @@
#!/bin/bash
set -Eeuo pipefail
ERROR_MSG=""
BACKUP_OK=0
DA_USER=""
DB_NAME=""
JOB_ID=""
LOG_FILE=""
JOB_FILE="${1:-}"
if [ -z "$JOB_FILE" ] || [ ! -f "$JOB_FILE" ]; then
echo "mysql: missing job file" >&2
exit 1
fi
PLUGIN_DIR="$(cd "$(dirname "$0")/.." && pwd)"
CONFIG_FILE="${PLUGIN_DIR}/plugin-settings.conf"
LOG_DIR="${PLUGIN_DIR}/data/logs"
COMMON_FILE="${PLUGIN_DIR}/scripts/setup/mysql_common.sh"
mkdir -p "$LOG_DIR"
urlencode() {
local s="$1"
local out=""
local i c
for ((i=0; i<${#s}; i++)); do
c="${s:i:1}"
case "$c" in
[a-zA-Z0-9.~_-]) out+="$c" ;;
' ') out+='%20' ;;
*) printf -v c '%%%02X' "'$c"; out+="$c" ;;
esac
done
printf '%s' "$out"
}
notify_user() {
local code="$1"
if [ -z "${DA_USER:-}" ]; then
return 0
fi
local subject message
if [ "$code" -eq 0 ] && [ "$BACKUP_OK" -eq 1 ]; then
subject="Backup bazy MySQL zakończony pomyślnie"
message="Backup bazy ${DB_NAME} został wykonany. Plik: ${TARGET_FILE:-nieznany}."
else
subject="Błąd wykonywania backupu bazy MySQL"
if [ -n "$ERROR_MSG" ]; then
message="Backup bazy ${DB_NAME} nie powiódł się: ${ERROR_MSG}"
else
message="Backup bazy ${DB_NAME} nie powiódł się. Sprawdź log zadania."
fi
fi
local task_queue="/usr/local/directadmin/data/task.queue"
local users="select1%3D$(urlencode "$DA_USER")"
local line="action=notify&value=users&subject=$(urlencode "$subject")&message=$(urlencode "$message")&users=${users}"
printf "%s\n" "$line" >> "$task_queue"
}
fail() {
local msg="$1"
ERROR_MSG="$msg"
if [ -n "${LOG_FILE:-}" ]; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: ${msg}" >> "$LOG_FILE"
fi
}
log_unexpected_error() {
local line_no="$1"
local command="$2"
local code="$3"
if [ -z "${LOG_FILE:-}" ]; then
return 0
fi
if [ -n "${ERROR_MSG:-}" ]; then
return 0
fi
ERROR_MSG="Nieoczekiwany błąd wykonywania backupu (kod ${code}, linia ${line_no})."
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: ${ERROR_MSG} CMD: ${command}" >> "$LOG_FILE"
}
load_plugin_settings() {
if [ ! -f "$CONFIG_FILE" ]; then
fail "Brak pliku konfiguracyjnego pluginu: $CONFIG_FILE"
exit 1
fi
# shellcheck disable=SC1090
source "$CONFIG_FILE"
}
quote_sh_value() {
local value="$1"
value="${value//\'/\'\\\'\'}"
printf "'%s'" "$value"
}
set_job_var() {
local key="$1"
local value="$2"
local tmp
local owner_group=""
if [ -f "$JOB_FILE" ]; then
owner_group="$(stat -c '%u:%g' "$JOB_FILE" 2>/dev/null || stat -f '%u:%g' "$JOB_FILE" 2>/dev/null || true)"
fi
tmp="$(mktemp)"
grep -v "^${key}=" "$JOB_FILE" > "$tmp" 2>/dev/null || true
printf "%s=%s\n" "$key" "$(quote_sh_value "$value")" >> "$tmp"
mv "$tmp" "$JOB_FILE"
chmod 600 "$JOB_FILE" 2>/dev/null || true
if [ "$(id -u)" -eq 0 ] && [ -n "$owner_group" ]; then
chown "$owner_group" "$JOB_FILE" 2>/dev/null || true
fi
}
on_exit() {
local code="$1"
if [ "$code" -ne 0 ] && [ -z "${ERROR_MSG:-}" ] && [ -n "${LOG_FILE:-}" ]; then
ERROR_MSG="Zadanie backupu zakończyło się błędem (kod ${code}) bez zarejestrowanego szczegółu."
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: ${ERROR_MSG}" >> "$LOG_FILE"
fi
notify_user "$code"
}
trap 'log_unexpected_error "$LINENO" "$BASH_COMMAND" "$?"' ERR
trap 'on_exit $?' EXIT
[ -f "$COMMON_FILE" ] || {
echo "Brak helpera MySQL: $COMMON_FILE" >&2
exit 1
}
# shellcheck disable=SC1090
source "$COMMON_FILE"
# shellcheck disable=SC1090
source "$JOB_FILE"
JOB_ID="${JOB_ID:-$(basename "$JOB_FILE" .env)}"
DA_USER="${DA_USER:-}"
DB_NAME="${DB_NAME:-}"
DATE_DIR="${DATE_DIR:-$(date +%Y-%m-%d)}"
COMPRESS_BACKUP="${COMPRESS_BACKUP:-1}"
USER_BASE_BACKUP_DIR="${USER_BASE_BACKUP_DIR:-/home/${DA_USER}/mysql_backup}"
JOB_COMPRESS_BACKUP="${COMPRESS_BACKUP}"
JOB_USER_BASE_BACKUP_DIR="${USER_BASE_BACKUP_DIR}"
LOG_FILE="${LOG_DIR}/${JOB_ID}.log"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Start backup job ${JOB_ID}" > "$LOG_FILE"
if [ -z "$DA_USER" ] || [ -z "$DB_NAME" ]; then
fail "Brak DA_USER lub DB_NAME w pliku zadania."
exit 1
fi
if [[ "$DB_NAME" != "${DA_USER}_"* ]]; then
fail "Baza ${DB_NAME} nie należy do użytkownika ${DA_USER}."
exit 1
fi
load_plugin_settings
COMPRESS_BACKUP="${JOB_COMPRESS_BACKUP:-$COMPRESS_BACKUP}"
USER_BASE_BACKUP_DIR="${JOB_USER_BASE_BACKUP_DIR:-$USER_BASE_BACKUP_DIR}"
mysql_load_alt_credentials
MYSQL_BIN="$(mysql_alt_binary mysql)" || {
fail "Brak binarium mysql."
exit 1
}
MYSQLDUMP_BIN="$(mysql_alt_binary mysqldump)" || {
fail "Brak binarium mysqldump."
exit 1
}
mapfile -t MYSQL_ARGS < <(mysql_base_args)
if [[ "$COMPRESS_BACKUP" =~ ^(true|yes|on)$ ]]; then
COMPRESS_BACKUP="1"
fi
if [[ "$COMPRESS_BACKUP" =~ ^(false|no|off)$ ]]; then
COMPRESS_BACKUP="0"
fi
TARGET_DIR="${USER_BASE_BACKUP_DIR%/}/${DATE_DIR}"
mkdir -p "$TARGET_DIR"
chmod 700 "$TARGET_DIR" 2>/dev/null || true
if [ "$COMPRESS_BACKUP" = "1" ]; then
TARGET_FILE="${TARGET_DIR}/${DB_NAME}.sql.gz"
else
TARGET_FILE="${TARGET_DIR}/${DB_NAME}.sql"
fi
TMP_FILE="${TARGET_FILE}.tmp.$$"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Backup source DB: ${DB_NAME}" >> "$LOG_FILE"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Backup target file: ${TARGET_FILE}" >> "$LOG_FILE"
set +e
if [ "$COMPRESS_BACKUP" = "1" ]; then
MYSQL_PWD="$MYSQL_PASSWORD" "$MYSQLDUMP_BIN" \
"${MYSQL_ARGS[@]}" \
--single-transaction \
--quick \
--skip-lock-tables \
--routines \
--triggers \
--events \
--default-character-set=utf8mb4 \
"$DB_NAME" 2>>"$LOG_FILE" | gzip -c > "$TMP_FILE"
RC=${PIPESTATUS[0]}
else
MYSQL_PWD="$MYSQL_PASSWORD" "$MYSQLDUMP_BIN" \
"${MYSQL_ARGS[@]}" \
--single-transaction \
--quick \
--skip-lock-tables \
--routines \
--triggers \
--events \
--default-character-set=utf8mb4 \
"$DB_NAME" > "$TMP_FILE" 2>>"$LOG_FILE"
RC=$?
fi
set -e
if [ "$RC" -ne 0 ]; then
rm -f "$TMP_FILE"
fail "mysqldump zakończył się błędem dla bazy ${DB_NAME}."
exit 1
fi
mv "$TMP_FILE" "$TARGET_FILE"
chmod 600 "$TARGET_FILE" 2>/dev/null || true
set_job_var "TARGET_FILE" "$TARGET_FILE"
set_job_var "END_TS" "$(date +%s)"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Backup completed successfully." >> "$LOG_FILE"
BACKUP_OK=1
exit 0
@@ -0,0 +1,293 @@
#!/bin/bash
set -Eeuo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PLUGIN_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
COMMON_FILE="$PLUGIN_DIR/scripts/setup/mysql_common.sh"
SETTINGS_FILE="$PLUGIN_DIR/plugin-settings.conf"
LOG_FILE="${DA_ALT_MYSQL_RESTORE_LOG:-$PLUGIN_DIR/data/logs/da-restore.log}"
DA_USER=""
PAYLOAD_DIR=""
OVERWRITE_POLICY=""
RESTORE_ROLLBACK_FILE=""
RESTORE_ROLLBACK_CREATED=0
mkdir -p "$(dirname "$LOG_FILE")"
log() {
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >> "$LOG_FILE"
}
die() {
log "ERROR: $*"
printf 'alt-mysql restore payload: %s\n' "$*" >&2
exit 1
}
usage() {
cat >&2 <<'EOF'
Usage: alt_mysql_restore_payload.sh --user DA_USER --payload /path/to/backup/alt_mysql [--overwrite allow|deny]
EOF
exit 2
}
while [ "$#" -gt 0 ]; do
case "$1" in
--user) DA_USER="${2:-}"; shift 2 ;;
--payload) PAYLOAD_DIR="${2:-}"; shift 2 ;;
--overwrite) OVERWRITE_POLICY="${2:-}"; shift 2 ;;
*) usage ;;
esac
done
[ -n "$DA_USER" ] || usage
[ -n "$PAYLOAD_DIR" ] || usage
[[ "$DA_USER" =~ ^[A-Za-z0-9_][A-Za-z0-9_.-]*$ ]] || die "unsafe DirectAdmin username: $DA_USER"
[ -d "$PAYLOAD_DIR" ] || die "payload directory does not exist: $PAYLOAD_DIR"
[ -r "$COMMON_FILE" ] || die "missing helper: $COMMON_FILE"
# shellcheck source=../setup/mysql_common.sh
source "$COMMON_FILE"
mysql_load_plugin_settings_file "$SETTINGS_FILE"
read_plugin_setting() {
local key="$1"
local default="$2"
local line value
[ -r "$SETTINGS_FILE" ] || {
printf '%s\n' "$default"
return 0
}
line="$(grep -E "^${key}=" "$SETTINGS_FILE" | tail -n 1 || true)"
[ -n "$line" ] || {
printf '%s\n' "$default"
return 0
}
value="${line#*=}"
value="${value%%#*}"
value="${value%\"}"
value="${value#\"}"
value="${value%\'}"
value="${value#\'}"
value="$(printf '%s' "$value" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
printf '%s\n' "${value:-$default}"
}
sha256_file() {
local file="$1"
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$file" | awk '{print $1}'
return 0
fi
if command -v shasum >/dev/null 2>&1; then
shasum -a 256 "$file" | awk '{print $1}'
return 0
fi
die "missing sha256sum/shasum"
}
require_php_cli() {
command -v php >/dev/null 2>&1 || die "php CLI is required to parse manifest.json"
}
manifest_database_rows() {
local manifest="$1"
php -r '
$manifest = $argv[1];
$data = json_decode(file_get_contents($manifest), true);
if (!is_array($data) || ($data["format"] ?? "") !== "da-alt-mysql-v1") {
fwrite(STDERR, "invalid manifest\n");
exit(3);
}
foreach (($data["databases"] ?? []) as $db) {
$name = (string)($db["name"] ?? "");
$file = (string)($db["file"] ?? "");
$sha = (string)($db["sha256"] ?? "");
if ($name === "" || $file === "" || $sha === "") {
fwrite(STDERR, "invalid database entry\n");
exit(4);
}
echo $name, "\t", $file, "\t", $sha, "\n";
}
' "$manifest"
}
safe_database_name() {
local db="$1"
[[ "$db" =~ ^[A-Za-z0-9_]+$ ]] || die "unsafe database name in payload: $db"
printf '%s\n' "$db"
}
database_allowed_for_user() {
local db="$1"
[[ "$db" == "${DA_USER}_"* ]]
}
import_gzip_sql() {
local database="$1"
local file="$2"
if [[ "$file" == *.gz ]]; then
gunzip -c "$file" | mysql_exec "$database"
return "${PIPESTATUS[1]}"
fi
mysql_exec "$database" < "$file"
}
cleanup_restore_rollback() {
if [ -n "${RESTORE_ROLLBACK_FILE:-}" ] && [ -f "$RESTORE_ROLLBACK_FILE" ]; then
rm -f "$RESTORE_ROLLBACK_FILE"
fi
RESTORE_ROLLBACK_FILE=""
RESTORE_ROLLBACK_CREATED=0
}
backup_alt_database_for_rollback() {
local db="$1"
cleanup_restore_rollback
RESTORE_ROLLBACK_FILE="$(mktemp)"
if [ -z "$(mysql_exec mysql -Nse "SELECT 1 FROM information_schema.SCHEMATA WHERE SCHEMA_NAME='$(mysql_escape_literal "$db")' LIMIT 1" 2>>"$LOG_FILE" || true)" ]; then
cleanup_restore_rollback
return 0
fi
log "creating rollback dump: $db"
if mysqldump_exec \
--single-transaction \
--quick \
--skip-lock-tables \
--routines \
--triggers \
--events \
--default-character-set=utf8mb4 \
"$db" > "$RESTORE_ROLLBACK_FILE" 2>>"$LOG_FILE"; then
RESTORE_ROLLBACK_CREATED=1
return 0
fi
cleanup_restore_rollback
return 1
}
restore_alt_database_from_rollback() {
local db="$1"
if [ "$RESTORE_ROLLBACK_CREATED" -ne 1 ] || [ -z "${RESTORE_ROLLBACK_FILE:-}" ] || [ ! -r "$RESTORE_ROLLBACK_FILE" ]; then
return 0
fi
log "restoring rollback dump: $db"
mysql_exec mysql -e "DROP DATABASE IF EXISTS $(mysql_quote_identifier "$db");" >> "$LOG_FILE" 2>&1 || true
mysql_exec mysql -e "CREATE DATABASE $(mysql_quote_identifier "$db") CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" >> "$LOG_FILE" 2>&1 || true
mysql_exec "$db" < "$RESTORE_ROLLBACK_FILE" >> "$LOG_FILE" 2>&1 || {
log "ERROR: rollback restore failed for $db"
return 1
}
}
trap cleanup_restore_rollback INT TERM EXIT
metadata_cleanup_for_user() {
local escaped_user
escaped_user="$(mysql_escape_literal "$DA_USER")"
mysql_exec mysql -e "DELETE FROM da_plugin_access_hosts WHERE da_user='${escaped_user}' OR role_name LIKE '${escaped_user}\\_%';"
mysql_exec mysql -e "DELETE FROM da_plugin_grant_profiles WHERE db_name LIKE '${escaped_user}\\_%' OR role_name LIKE '${escaped_user}\\_%';"
mysql_exec mysql -e "DELETE FROM da_plugin_database_owners WHERE db_name LIKE '${escaped_user}\\_%' OR role_name LIKE '${escaped_user}\\_%';"
}
verify_database_exists() {
local db="$1"
local escaped_db
escaped_db="$(mysql_escape_literal "$db")"
[ -n "$(mysql_exec mysql -Nse "SELECT 1 FROM information_schema.SCHEMATA WHERE SCHEMA_NAME='${escaped_db}' LIMIT 1")" ] \
|| die "database was not restored: $db"
}
sync_remote_hosts_if_enabled() {
local allow sync_script
allow="$(read_plugin_setting allow_remote_hosts 0)"
[ "$allow" = "1" ] || return 0
sync_script="$(read_plugin_setting MYSQL_PLUGIN_SUDO_SYNC_HOSTS /usr/local/directadmin/plugins/alt-mysql/scripts/setup/mysql_host_sync.sh)"
if [ -x "$sync_script" ]; then
"$sync_script" >> "$LOG_FILE" 2>&1 || log "WARNING: host sync failed after restore"
fi
}
main() {
local manifest users_file grants_file metadata_file db file rel_file expected_sha actual_sha overwrite
manifest="$PAYLOAD_DIR/manifest.json"
users_file="$PAYLOAD_DIR/grants/users.sql.gz"
grants_file="$PAYLOAD_DIR/grants/grants.sql.gz"
[ -r "$manifest" ] || die "missing manifest: $manifest"
[ -r "$users_file" ] || die "missing users payload: $users_file"
[ -r "$grants_file" ] || die "missing grants payload: $grants_file"
require_php_cli
mysql_load_alt_credentials
mysql_ensure_metadata_schema
overwrite="${OVERWRITE_POLICY:-$(read_plugin_setting DA_ALT_MYSQL_RESTORE_OVERWRITE allow)}"
case "$overwrite" in
allow|deny) ;;
prompt) overwrite="deny" ;;
*) overwrite="deny" ;;
esac
log "restore payload start: user=$DA_USER payload=$PAYLOAD_DIR overwrite=$overwrite"
manifest_database_rows "$manifest" >/dev/null
while IFS=$'\t' read -r db rel_file expected_sha; do
db="$(safe_database_name "$db")"
database_allowed_for_user "$db" || die "database $db does not match target user prefix $DA_USER"
file="$PAYLOAD_DIR/$rel_file"
[ -r "$file" ] || die "missing database dump: $file"
actual_sha="$(sha256_file "$file")"
[ "$actual_sha" = "$expected_sha" ] || die "checksum mismatch for $db"
if [ "$overwrite" = "deny" ] && [ -n "$(mysql_exec mysql -Nse "SELECT 1 FROM information_schema.SCHEMATA WHERE SCHEMA_NAME='$(mysql_escape_literal "$db")' LIMIT 1")" ]; then
die "target database exists and overwrite is denied: $db"
fi
done < <(manifest_database_rows "$manifest")
log "restoring user definitions"
import_gzip_sql mysql "$users_file" >> "$LOG_FILE" 2>&1 || die "cannot restore MySQL users from payload"
metadata_cleanup_for_user
while IFS=$'\t' read -r db rel_file expected_sha; do
db="$(safe_database_name "$db")"
file="$PAYLOAD_DIR/$rel_file"
log "restoring database: $db"
backup_alt_database_for_rollback "$db" || die "cannot create rollback dump before restoring database: $db"
if ! mysql_exec mysql -e "DROP DATABASE IF EXISTS $(mysql_quote_identifier "$db");" >> "$LOG_FILE" 2>&1; then
restore_alt_database_from_rollback "$db" || true
die "cannot drop database before restore: $db"
fi
if ! mysql_exec mysql -e "CREATE DATABASE $(mysql_quote_identifier "$db") CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" >> "$LOG_FILE" 2>&1; then
restore_alt_database_from_rollback "$db" || true
die "cannot create database before restore: $db"
fi
if ! import_gzip_sql "$db" "$file" >> "$LOG_FILE" 2>&1; then
restore_alt_database_from_rollback "$db" || true
die "cannot import database dump: $db"
fi
cleanup_restore_rollback
verify_database_exists "$db"
done < <(manifest_database_rows "$manifest")
for metadata_file in \
"$PAYLOAD_DIR/metadata/da_plugin_database_owners.sql.gz" \
"$PAYLOAD_DIR/metadata/da_plugin_grant_profiles.sql.gz" \
"$PAYLOAD_DIR/metadata/da_plugin_access_hosts.sql.gz"; do
if [ -r "$metadata_file" ]; then
log "restoring metadata: ${metadata_file##*/}"
import_gzip_sql mysql "$metadata_file" >> "$LOG_FILE" 2>&1 || die "cannot restore metadata: $metadata_file"
fi
done
log "restoring grants"
import_gzip_sql mysql "$grants_file" >> "$LOG_FILE" 2>&1 || die "cannot restore grants from payload"
mysql_exec mysql -e "FLUSH PRIVILEGES;" >> "$LOG_FILE" 2>&1 || true
sync_remote_hosts_if_enabled
log "restore payload completed: user=$DA_USER"
}
main "$@"
@@ -0,0 +1,312 @@
#!/bin/bash
set -Eeuo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PLUGIN_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
COMMON_FILE="$PLUGIN_DIR/scripts/setup/mysql_common.sh"
LOG_FILE="${DA_ALT_MYSQL_BACKUP_LOG:-$PLUGIN_DIR/data/logs/da-backup.log}"
DRY_RUN=0
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=1 ;;
esac
done
mkdir -p "$(dirname "$LOG_FILE")"
log() {
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >> "$LOG_FILE"
}
die() {
log "ERROR: $*"
printf 'alt-mysql backup hook: %s\n' "$*" >&2
exit 1
}
[ -r "$COMMON_FILE" ] || die "missing helper: $COMMON_FILE"
# shellcheck source=../setup/mysql_common.sh
source "$COMMON_FILE"
mysql_load_plugin_settings_file "$PLUGIN_DIR/plugin-settings.conf"
json_escape() {
printf '%s' "${1:-}" | sed 's/\\/\\\\/g; s/"/\\"/g'
}
json_string() {
printf '"%s"' "$(json_escape "${1:-}")"
}
sha256_file() {
local file="$1"
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$file" | awk '{print $1}'
return 0
fi
if command -v shasum >/dev/null 2>&1; then
shasum -a 256 "$file" | awk '{print $1}'
return 0
fi
die "missing sha256sum/shasum"
}
detect_da_user() {
local value="${DA_USER:-${username:-${user:-${USERNAME:-}}}}"
if [ -z "$value" ] && [ "${1:-}" != "--dry-run" ]; then
value="${1:-}"
fi
if [[ ! "$value" =~ ^[A-Za-z0-9_][A-Za-z0-9_.-]*$ ]]; then
die "cannot detect safe DirectAdmin username"
fi
printf '%s\n' "$value"
}
candidate_dir_if_valid() {
local candidate="${1:-}"
[ -n "$candidate" ] || return 1
[ -d "$candidate" ] || return 1
printf '%s\n' "$(cd "$candidate" && pwd -P)"
}
candidate_backup_root_if_valid() {
local candidate="${1:-}"
local dir=""
if ! dir="$(candidate_dir_if_valid "$candidate")"; then
return 1
fi
if [ "$DRY_RUN" -eq 1 ] || [ -d "$dir/backup" ]; then
printf '%s\n' "$dir"
return 0
fi
return 1
}
detect_backup_root() {
local current candidate dir file_candidate
current="$(pwd -P)"
for candidate in \
"${DA_ALT_MYSQL_BACKUP_ROOT:-}" \
"${backup_path:-}" \
"${backupdir:-}" \
"${backup_dir:-}" \
"${tmpdir:-}" \
"$current"; do
if dir="$(candidate_backup_root_if_valid "$candidate")"; then
printf '%s\n' "$dir"
return 0
fi
done
for file_candidate in "${file:-}" "${filename:-}" "${backup_file:-}"; do
[ -n "$file_candidate" ] || continue
candidate="$(dirname "$file_candidate")"
if dir="$(candidate_backup_root_if_valid "$candidate")"; then
printf '%s\n' "$dir"
return 0
fi
done
die "cannot detect DirectAdmin backup assembly directory"
}
list_user_databases() {
local da_user="$1"
local escaped_user
escaped_user="$(mysql_escape_literal "$da_user")"
mysql_exec mysql -Nse "
SELECT SCHEMA_NAME
FROM information_schema.SCHEMATA
WHERE SCHEMA_NAME LIKE '${escaped_user}\\_%' ESCAPE '\\'
ORDER BY SCHEMA_NAME
" | grep -Ev '^(information_schema|performance_schema|mysql|sys)$' || true
}
list_user_roles() {
local da_user="$1"
local escaped_user
escaped_user="$(mysql_escape_literal "$da_user")"
mysql_exec mysql -Nse "
SELECT DISTINCT User
FROM mysql.user
WHERE User LIKE '${escaped_user}\\_%' ESCAPE '\\'
AND User <> ''
ORDER BY User
" || true
}
dump_users_file() {
local da_user="$1"
local output_file="$2"
local tmp_file user host q_user q_host create_sql
tmp_file="$(mktemp)"
{
printf '%s\n' "-- alt-mysql user definitions for $da_user"
printf '%s\n' "SET sql_log_bin=0;"
} > "$tmp_file"
while IFS=$'\t' read -r user host; do
[ -n "${user:-}" ] || continue
[ -n "${host:-}" ] || continue
q_user="$(mysql_escape_literal "$user")"
q_host="$(mysql_escape_literal "$host")"
create_sql="$(mysql_exec mysql -Nse "SHOW CREATE USER '${q_user}'@'${q_host}'" | awk -F'\t' '{print $NF}' || true)"
[ -n "$create_sql" ] || continue
printf "DROP USER IF EXISTS '%s'@'%s';\n" "$q_user" "$q_host" >> "$tmp_file"
printf '%s;\n' "$create_sql" >> "$tmp_file"
done < <(
mysql_exec mysql -Nse "
SELECT User, Host
FROM mysql.user
WHERE User LIKE '$(mysql_escape_literal "$da_user")\\_%' ESCAPE '\\'
AND User <> ''
ORDER BY User, Host
" || true
)
printf '%s\n' "SET sql_log_bin=1;" >> "$tmp_file"
gzip -9 < "$tmp_file" > "$output_file"
rm -f "$tmp_file"
}
dump_grants_file() {
local da_user="$1"
local output_file="$2"
local tmp_file user host q_user q_host grant_line
tmp_file="$(mktemp)"
printf '%s\n' "-- alt-mysql grants for $da_user" > "$tmp_file"
while IFS=$'\t' read -r user host; do
[ -n "${user:-}" ] || continue
[ -n "${host:-}" ] || continue
q_user="$(mysql_escape_literal "$user")"
q_host="$(mysql_escape_literal "$host")"
while IFS= read -r grant_line; do
[ -n "$grant_line" ] || continue
printf '%s;\n' "$grant_line" >> "$tmp_file"
done < <(mysql_exec mysql -Nse "SHOW GRANTS FOR '${q_user}'@'${q_host}'" || true)
printf '\n' >> "$tmp_file"
done < <(
mysql_exec mysql -Nse "
SELECT User, Host
FROM mysql.user
WHERE User LIKE '$(mysql_escape_literal "$da_user")\\_%' ESCAPE '\\'
AND User <> ''
ORDER BY User, Host
" || true
)
gzip -9 < "$tmp_file" > "$output_file"
rm -f "$tmp_file"
}
dump_metadata_table() {
local table="$1"
local where="$2"
local output_file="$3"
mysqldump_exec \
--single-transaction \
--skip-lock-tables \
--no-create-info \
--compact \
--where="$where" \
mysql "$table" | gzip -9 > "$output_file"
}
write_manifest() {
local da_user="$1"
local payload_dir="$2"
shift 2
local db_json="$1"
local manifest="$payload_dir/manifest.json"
local version
version="$(mysql_alt_version)"
mysql_load_alt_runtime
cat > "$manifest" <<JSON
{
"format": "da-alt-mysql-v1",
"created_at": "$(date -u '+%Y-%m-%dT%H:%M:%SZ')",
"da_user": $(json_string "$da_user"),
"mariadb_version": $(json_string "$version"),
"source": "alt-mariadb",
"source_instance": {
"service": $(json_string "$MYSQL_ALT_SERVICE_NAME"),
"basedir": $(json_string "$MYSQL_ALT_BASEDIR"),
"datadir": $(json_string "$MYSQL_ALT_DATADIR"),
"port": $(json_string "$MYSQL_ALT_PORT"),
"socket": $(json_string "$MYSQL_ALT_SOCKET")
},
"databases": [
$db_json
]
}
JSON
}
main() {
local da_user backup_root payload_dir db_dir grants_dir metadata_dir
local db db_file rel_file sha db_entries entry prefix where
da_user="$(detect_da_user "${1:-}")"
backup_root="$(detect_backup_root)"
payload_dir="$backup_root/backup/alt_mysql"
db_dir="$payload_dir/databases"
grants_dir="$payload_dir/grants"
metadata_dir="$payload_dir/metadata"
log "backup hook env: user=$da_user pwd=$(pwd -P) file=${file:-} filename=${filename:-} root=$backup_root"
mysql_load_alt_credentials
mysql_ensure_metadata_schema
if [ "$DRY_RUN" -eq 1 ]; then
printf 'Would write alt-mysql payload for %s into %s\n' "$da_user" "$payload_dir"
list_user_databases "$da_user"
return 0
fi
rm -rf "$payload_dir"
mkdir -p "$db_dir" "$grants_dir" "$metadata_dir"
db_entries=""
while IFS= read -r db; do
[ -n "$db" ] || continue
db_file="$db_dir/${db}.sql.gz"
mysqldump_exec \
--single-transaction \
--quick \
--skip-lock-tables \
--routines \
--triggers \
--events \
--default-character-set=utf8mb4 \
"$db" | gzip -9 > "$db_file"
rel_file="databases/${db}.sql.gz"
sha="$(sha256_file "$db_file")"
entry=" { \"name\": $(json_string "$db"), \"file\": $(json_string "$rel_file"), \"sha256\": $(json_string "$sha") }"
if [ -n "$db_entries" ]; then
db_entries+=$',\n'
fi
db_entries+="$entry"
log "added database dump: $db"
done < <(list_user_databases "$da_user")
dump_users_file "$da_user" "$grants_dir/users.sql.gz"
dump_grants_file "$da_user" "$grants_dir/grants.sql.gz"
prefix="$(mysql_escape_literal "$da_user")"
where="db_name LIKE '${prefix}\\_%'"
dump_metadata_table da_plugin_database_owners "$where" "$metadata_dir/da_plugin_database_owners.sql.gz"
dump_metadata_table da_plugin_grant_profiles "$where" "$metadata_dir/da_plugin_grant_profiles.sql.gz"
where="da_user = '${prefix}' OR role_name LIKE '${prefix}\\_%'"
dump_metadata_table da_plugin_access_hosts "$where" "$metadata_dir/da_plugin_access_hosts.sql.gz"
write_manifest "$da_user" "$payload_dir" "$db_entries"
chmod -R go-rwx "$payload_dir" 2>/dev/null || true
log "backup payload completed: $payload_dir"
}
main "$@"
@@ -0,0 +1,165 @@
#!/bin/bash
set -Eeuo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PLUGIN_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
SETTINGS_FILE="$PLUGIN_DIR/plugin-settings.conf"
LOG_FILE="${DA_ALT_MYSQL_RESTORE_LOG:-$PLUGIN_DIR/data/logs/da-restore.log}"
RESTORE_PAYLOAD_SCRIPT="$SCRIPT_DIR/alt_mysql_restore_payload.sh"
NATIVE_MIGRATE_SCRIPT="$SCRIPT_DIR/da_restore_native_staging_to_alt_mysql.sh"
mkdir -p "$(dirname "$LOG_FILE")"
log() {
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >> "$LOG_FILE"
}
die() {
log "ERROR: $*"
printf 'alt-mysql restore hook: %s\n' "$*" >&2
exit 1
}
read_plugin_setting() {
local key="$1"
local default="$2"
local line value
[ -r "$SETTINGS_FILE" ] || {
printf '%s\n' "$default"
return 0
}
line="$(grep -E "^${key}=" "$SETTINGS_FILE" | tail -n 1 || true)"
[ -n "$line" ] || {
printf '%s\n' "$default"
return 0
}
value="${line#*=}"
value="${value%%#*}"
value="${value%\"}"
value="${value#\"}"
value="${value%\'}"
value="${value#\'}"
value="$(printf '%s' "$value" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
printf '%s\n' "${value:-$default}"
}
detect_da_user() {
local value="${DA_USER:-${username:-${user:-${USERNAME:-}}}}"
if [ -z "$value" ]; then
value="${1:-}"
fi
if [[ ! "$value" =~ ^[A-Za-z0-9_][A-Za-z0-9_.-]*$ ]]; then
die "cannot detect safe DirectAdmin username"
fi
printf '%s\n' "$value"
}
candidate_dir_if_valid() {
local candidate="${1:-}"
[ -n "$candidate" ] || return 1
[ -d "$candidate" ] || return 1
printf '%s\n' "$(cd "$candidate" && pwd -P)"
}
find_payload_in_dir() {
local root="$1"
local payload=""
[ -d "$root" ] || return 1
for payload in \
"$root/backup/alt_mysql" \
"$root/alt_mysql" \
"$root/mysql/backup/alt_mysql"; do
if [ -r "$payload/manifest.json" ]; then
printf '%s\n' "$payload"
return 0
fi
done
payload="$(find "$root" -maxdepth 5 -type f -path '*/backup/alt_mysql/manifest.json' -print -quit 2>/dev/null || true)"
if [ -n "$payload" ]; then
dirname "$payload"
return 0
fi
return 1
}
extract_payload_from_archive() {
local archive="$1"
local tmp_dir payload_member
[ -r "$archive" ] || return 1
command -v tar >/dev/null 2>&1 || return 1
payload_member="$(tar -tf "$archive" 2>/dev/null | grep -E '(^|/)backup/alt_mysql/manifest\.json$' | head -n 1 || true)"
[ -n "$payload_member" ] || return 1
tmp_dir="$(mktemp -d)"
tar -xf "$archive" -C "$tmp_dir" --wildcards '*/backup/alt_mysql/*' 'backup/alt_mysql/*' 2>/dev/null \
|| tar -xf "$archive" -C "$tmp_dir" 2>/dev/null
find_payload_in_dir "$tmp_dir"
}
detect_payload_dir() {
local current candidate dir archive payload
current="$(pwd -P)"
for candidate in \
"${DA_ALT_MYSQL_RESTORE_ROOT:-}" \
"${restore_path:-}" \
"${restore_dir:-}" \
"${tmpdir:-}" \
"${backup_path:-}" \
"$current"; do
if dir="$(candidate_dir_if_valid "$candidate")"; then
if payload="$(find_payload_in_dir "$dir")"; then
printf '%s\n' "$payload"
return 0
fi
fi
done
for archive in "${filename:-}" "${file:-}" "${backup_file:-}"; do
[ -n "$archive" ] || continue
if payload="$(extract_payload_from_archive "$archive")"; then
printf '%s\n' "$payload"
return 0
fi
done
return 1
}
main() {
local da_user enabled overwrite payload native_mode
da_user="$(detect_da_user "${1:-}")"
enabled="$(read_plugin_setting DA_ALT_MYSQL_RESTORE_ENABLED true)"
native_mode="$(read_plugin_setting DA_ALT_MYSQL_NATIVE_STAGING_MIGRATE true)"
log "restore hook env: user=$da_user pwd=$(pwd -P) file=${file:-} filename=${filename:-}"
case "$enabled" in
true|yes|on|1) ;;
*) log "restore hook disabled by DA_ALT_MYSQL_RESTORE_ENABLED=$enabled"; return 0 ;;
esac
overwrite="$(read_plugin_setting DA_ALT_MYSQL_RESTORE_OVERWRITE allow)"
if payload="$(detect_payload_dir)"; then
[ -x "$RESTORE_PAYLOAD_SCRIPT" ] || die "restore payload script is not executable: $RESTORE_PAYLOAD_SCRIPT"
log "plugin payload detected: $payload"
"$RESTORE_PAYLOAD_SCRIPT" --user "$da_user" --payload "$payload" --overwrite "$overwrite"
return 0
fi
log "plugin payload not found; considering native staging migration"
case "$native_mode" in
true|yes|on|1)
[ -x "$NATIVE_MIGRATE_SCRIPT" ] || die "native staging migration script is not executable: $NATIVE_MIGRATE_SCRIPT"
"$NATIVE_MIGRATE_SCRIPT" --user "$da_user"
;;
*)
log "native staging migration disabled; account restore continues without alt-mysql DB migration"
;;
esac
}
main "$@"
@@ -0,0 +1,68 @@
#!/bin/bash
set -Eeuo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PLUGIN_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
SETTINGS_FILE="$PLUGIN_DIR/plugin-settings.conf"
LOG_FILE="${DA_ALT_MYSQL_DB_HOOK_LOG:-$PLUGIN_DIR/data/logs/da-db-hooks.log}"
HOOK_NAME="${DA_HOOK_NAME:-$(basename "$0")}"
mkdir -p "$(dirname "$LOG_FILE")"
read_plugin_setting() {
local key="$1"
local default="$2"
local line value
[ -r "$SETTINGS_FILE" ] || {
printf '%s\n' "$default"
return 0
}
line="$(grep -E "^${key}=" "$SETTINGS_FILE" | tail -n 1 || true)"
[ -n "$line" ] || {
printf '%s\n' "$default"
return 0
}
value="${line#*=}"
value="${value%%#*}"
value="${value%\"}"
value="${value#\"}"
value="${value%\'}"
value="${value#\'}"
value="$(printf '%s' "$value" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
printf '%s\n' "${value:-$default}"
}
log() {
printf '[%s] hook=%s username=%s database=%s user=%s %s\n' \
"$(date '+%Y-%m-%d %H:%M:%S')" \
"$HOOK_NAME" \
"${username:-}" \
"${database:-${name:-}}" \
"${user:-}" \
"$*" >> "$LOG_FILE"
}
enabled="$(read_plugin_setting DA_ALT_MYSQL_BLOCK_NATIVE_DB_HOOKS true)"
case "$enabled" in
true|yes|on|1) ;;
*) log "native DB hook blocker disabled"; exit 0 ;;
esac
if [ "${DA_ALT_MYSQL_ALLOW_NATIVE_DB_ACTION:-0}" = "1" ]; then
log "native DB action explicitly allowed by environment"
exit 0
fi
case "$HOOK_NAME" in
*_post.sh|*_post)
log "native DirectAdmin database action observed after completion"
exit 0
;;
esac
log "blocked native DirectAdmin database action"
cat >&2 <<'EOF'
Databases on this server are managed by the MySQL plugin and the alt-mariadb instance.
Use the MySQL plugin instead of DirectAdmin native database actions.
EOF
exit 1
@@ -0,0 +1,448 @@
#!/bin/bash
set -Eeuo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PLUGIN_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
COMMON_FILE="$PLUGIN_DIR/scripts/setup/mysql_common.sh"
SETTINGS_FILE="$PLUGIN_DIR/plugin-settings.conf"
LOG_FILE="${DA_ALT_MYSQL_RESTORE_LOG:-$PLUGIN_DIR/data/logs/da-restore.log}"
DA_USER=""
NATIVE_MY_CNF="${NATIVE_MY_CNF:-/usr/local/directadmin/conf/my.cnf}"
OVERWRITE_POLICY=""
CLEANUP_POLICY=""
RESTORE_ROLLBACK_FILE=""
RESTORE_ROLLBACK_CREATED=0
mkdir -p "$(dirname "$LOG_FILE")"
log() {
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >> "$LOG_FILE"
}
die() {
log "ERROR: $*"
printf 'native staging to alt-mysql: %s\n' "$*" >&2
exit 1
}
usage() {
cat >&2 <<'EOF'
Usage: da_restore_native_staging_to_alt_mysql.sh --user DA_USER [--native-my-cnf /path] [--overwrite allow|deny] [--cleanup after_success|keep]
EOF
exit 2
}
while [ "$#" -gt 0 ]; do
case "$1" in
--user) DA_USER="${2:-}"; shift 2 ;;
--native-my-cnf) NATIVE_MY_CNF="${2:-}"; shift 2 ;;
--overwrite) OVERWRITE_POLICY="${2:-}"; shift 2 ;;
--cleanup) CLEANUP_POLICY="${2:-}"; shift 2 ;;
*) usage ;;
esac
done
[ -n "$DA_USER" ] || usage
[[ "$DA_USER" =~ ^[A-Za-z0-9_][A-Za-z0-9_.-]*$ ]] || die "unsafe DirectAdmin username: $DA_USER"
[ -r "$COMMON_FILE" ] || die "missing helper: $COMMON_FILE"
# shellcheck source=../setup/mysql_common.sh
source "$COMMON_FILE"
mysql_load_plugin_settings_file "$SETTINGS_FILE"
read_plugin_setting() {
local key="$1"
local default="$2"
local line value
[ -r "$SETTINGS_FILE" ] || {
printf '%s\n' "$default"
return 0
}
line="$(grep -E "^${key}=" "$SETTINGS_FILE" | tail -n 1 || true)"
[ -n "$line" ] || {
printf '%s\n' "$default"
return 0
}
value="${line#*=}"
value="${value%%#*}"
value="${value%\"}"
value="${value#\"}"
value="${value%\'}"
value="${value#\'}"
value="$(printf '%s' "$value" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
printf '%s\n' "${value:-$default}"
}
native_mysql() {
"$NATIVE_MYSQL_BIN" --defaults-extra-file="$NATIVE_MY_CNF" "$@"
}
native_query() {
local database="$1"
local sql="$2"
native_mysql --batch --skip-column-names "$database" -e "$sql"
}
safe_identifier() {
local value="$1"
[[ "$value" =~ ^[A-Za-z0-9_]+$ ]] || die "unsafe MySQL identifier: $value"
printf '%s\n' "$value"
}
list_native_databases() {
native_query information_schema "
SELECT SCHEMA_NAME
FROM SCHEMATA
WHERE SCHEMA_NAME LIKE '$(mysql_escape_literal "$DA_USER")\\_%' ESCAPE '\\'
ORDER BY SCHEMA_NAME
" | grep -Ev '^(information_schema|performance_schema|mysql|sys)$' || true
}
list_native_user_hosts() {
native_query mysql "
SELECT User, Host
FROM user
WHERE User LIKE '$(mysql_escape_literal "$DA_USER")\\_%' ESCAPE '\\'
AND User <> ''
ORDER BY User, Host
" || true
}
list_native_roles_for_db() {
local db="$1"
native_query mysql "
SELECT DISTINCT User
FROM db
WHERE Db = '$(mysql_escape_literal "$db")'
AND User LIKE '$(mysql_escape_literal "$DA_USER")\\_%' ESCAPE '\\'
AND User <> ''
ORDER BY User
" || true
}
native_show_create_user() {
local user="$1"
local host="$2"
native_query mysql "SHOW CREATE USER '$(mysql_escape_literal "$user")'@'$(mysql_escape_literal "$host")'" | awk -F'\t' '{print $NF}'
}
native_show_grants() {
local user="$1"
local host="$2"
native_query mysql "SHOW GRANTS FOR '$(mysql_escape_literal "$user")'@'$(mysql_escape_literal "$host")'" || true
}
apply_user_definition_to_alt() {
local user="$1"
local host="$2"
local create_sql
create_sql="$(native_show_create_user "$user" "$host" || true)"
[ -n "$create_sql" ] || die "cannot recover password/authentication definition for ${user}@${host}"
{
printf "DROP USER IF EXISTS '%s'@'%s';\n" "$(mysql_escape_literal "$user")" "$(mysql_escape_literal "$host")"
printf '%s;\n' "$create_sql"
} | mysql_exec mysql >> "$LOG_FILE" 2>&1 || die "cannot recreate user with preserved password: ${user}@${host}"
}
apply_grants_to_alt() {
local user="$1"
local host="$2"
local grant_line
while IFS= read -r grant_line; do
[ -n "$grant_line" ] || continue
printf '%s;\n' "$grant_line" | mysql_exec mysql >> "$LOG_FILE" 2>&1 \
|| die "cannot apply grant for ${user}@${host}"
done < <(native_show_grants "$user" "$host")
}
cleanup_restore_rollback() {
if [ -n "${RESTORE_ROLLBACK_FILE:-}" ] && [ -f "$RESTORE_ROLLBACK_FILE" ]; then
rm -f "$RESTORE_ROLLBACK_FILE"
fi
RESTORE_ROLLBACK_FILE=""
RESTORE_ROLLBACK_CREATED=0
}
backup_alt_database_for_rollback() {
local db="$1"
cleanup_restore_rollback
RESTORE_ROLLBACK_FILE="$(mktemp)"
if [ -z "$(mysql_exec mysql -Nse "SELECT 1 FROM information_schema.SCHEMATA WHERE SCHEMA_NAME='$(mysql_escape_literal "$db")' LIMIT 1" 2>>"$LOG_FILE" || true)" ]; then
cleanup_restore_rollback
return 0
fi
log "creating rollback dump: $db"
if mysqldump_exec \
--single-transaction \
--quick \
--skip-lock-tables \
--routines \
--triggers \
--events \
--default-character-set=utf8mb4 \
"$db" > "$RESTORE_ROLLBACK_FILE" 2>>"$LOG_FILE"; then
RESTORE_ROLLBACK_CREATED=1
return 0
fi
cleanup_restore_rollback
return 1
}
restore_alt_database_from_rollback() {
local db="$1"
if [ "$RESTORE_ROLLBACK_CREATED" -ne 1 ] || [ -z "${RESTORE_ROLLBACK_FILE:-}" ] || [ ! -r "$RESTORE_ROLLBACK_FILE" ]; then
return 0
fi
log "restoring rollback dump: $db"
mysql_exec mysql -e "DROP DATABASE IF EXISTS $(mysql_quote_identifier "$db");" >> "$LOG_FILE" 2>&1 || true
mysql_exec mysql -e "CREATE DATABASE $(mysql_quote_identifier "$db") CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" >> "$LOG_FILE" 2>&1 || true
mysql_exec "$db" < "$RESTORE_ROLLBACK_FILE" >> "$LOG_FILE" 2>&1 || {
log "ERROR: rollback restore failed for $db"
return 1
}
}
trap cleanup_restore_rollback INT TERM EXIT
db_privilege_profile() {
local db="$1"
local role="$2"
local row privs=()
row="$(native_query mysql "
SELECT Select_priv, Insert_priv, Update_priv, Delete_priv, Create_priv, Drop_priv, Index_priv, Alter_priv,
Create_tmp_table_priv, Lock_tables_priv, Create_view_priv, Show_view_priv, Create_routine_priv,
Alter_routine_priv, Execute_priv, Event_priv, Trigger_priv
FROM db
WHERE Db='$(mysql_escape_literal "$db")'
AND User='$(mysql_escape_literal "$role")'
LIMIT 1
" | head -n 1 || true)"
if [ -z "$row" ]; then
printf 'ALL\n'
return 0
fi
IFS=$'\t' read -r select_p insert_p update_p delete_p create_p drop_p index_p alter_p tmp_p lock_p create_view_p show_view_p create_routine_p alter_routine_p execute_p event_p trigger_p <<< "$row"
[ "${select_p:-N}" = "Y" ] && privs+=("SELECT")
[ "${insert_p:-N}" = "Y" ] && privs+=("INSERT")
[ "${update_p:-N}" = "Y" ] && privs+=("UPDATE")
[ "${delete_p:-N}" = "Y" ] && privs+=("DELETE")
[ "${create_p:-N}" = "Y" ] && privs+=("CREATE")
[ "${drop_p:-N}" = "Y" ] && privs+=("DROP")
[ "${index_p:-N}" = "Y" ] && privs+=("INDEX")
[ "${alter_p:-N}" = "Y" ] && privs+=("ALTER")
[ "${tmp_p:-N}" = "Y" ] && privs+=("CREATE TEMPORARY TABLES")
[ "${lock_p:-N}" = "Y" ] && privs+=("LOCK TABLES")
[ "${create_view_p:-N}" = "Y" ] && privs+=("CREATE VIEW")
[ "${show_view_p:-N}" = "Y" ] && privs+=("SHOW VIEW")
[ "${create_routine_p:-N}" = "Y" ] && privs+=("CREATE ROUTINE")
[ "${alter_routine_p:-N}" = "Y" ] && privs+=("ALTER ROUTINE")
[ "${execute_p:-N}" = "Y" ] && privs+=("EXECUTE")
[ "${event_p:-N}" = "Y" ] && privs+=("EVENT")
[ "${trigger_p:-N}" = "Y" ] && privs+=("TRIGGER")
if [ "${#privs[@]}" -eq 0 ]; then
printf 'ALL\n'
else
local IFS=,
printf '%s\n' "${privs[*]}"
fi
}
metadata_cleanup_for_user() {
local escaped_user
escaped_user="$(mysql_escape_literal "$DA_USER")"
mysql_exec mysql -e "DELETE FROM da_plugin_access_hosts WHERE da_user='${escaped_user}' OR role_name LIKE '${escaped_user}\\_%';"
mysql_exec mysql -e "DELETE FROM da_plugin_grant_profiles WHERE db_name LIKE '${escaped_user}\\_%' OR role_name LIKE '${escaped_user}\\_%';"
mysql_exec mysql -e "DELETE FROM da_plugin_database_owners WHERE db_name LIKE '${escaped_user}\\_%' OR role_name LIKE '${escaped_user}\\_%';"
}
rebuild_metadata_for_db() {
local db="$1"
local owner="" role profile host
while IFS= read -r role; do
[ -n "$role" ] || continue
if [ -z "$owner" ]; then
owner="$role"
fi
profile="$(db_privilege_profile "$db" "$role")"
mysql_exec mysql -e "
INSERT INTO da_plugin_grant_profiles (db_name, role_name, privileges)
VALUES ('$(mysql_escape_literal "$db")', '$(mysql_escape_literal "$role")', '$(mysql_escape_literal "$profile")')
ON DUPLICATE KEY UPDATE privileges=VALUES(privileges), updated_at=CURRENT_TIMESTAMP;
"
while IFS=$'\t' read -r _user host; do
[ -n "${host:-}" ] || continue
mysql_exec mysql -e "
INSERT INTO da_plugin_access_hosts (da_user, role_name, host_pattern, note)
VALUES ('$(mysql_escape_literal "$DA_USER")', '$(mysql_escape_literal "$role")', '$(mysql_escape_literal "$host")', 'migrated from native DirectAdmin restore')
ON DUPLICATE KEY UPDATE da_user=VALUES(da_user), note=VALUES(note), updated_at=CURRENT_TIMESTAMP;
"
done < <(list_native_user_hosts | awk -F'\t' -v role="$role" '$1 == role { print $0 }')
done < <(list_native_roles_for_db "$db")
if [ -n "$owner" ]; then
mysql_exec mysql -e "
INSERT INTO da_plugin_database_owners (db_name, da_user, role_name)
VALUES ('$(mysql_escape_literal "$db")', '$(mysql_escape_literal "$DA_USER")', '$(mysql_escape_literal "$owner")')
ON DUPLICATE KEY UPDATE da_user=VALUES(da_user), role_name=VALUES(role_name), updated_at=CURRENT_TIMESTAMP;
"
fi
}
verify_alt_database() {
local db="$1"
[ -n "$(mysql_exec mysql -Nse "SELECT 1 FROM information_schema.SCHEMATA WHERE SCHEMA_NAME='$(mysql_escape_literal "$db")' LIMIT 1")" ] \
|| die "alt database missing after import: $db"
}
drop_native_staging() {
local db user host remaining
log "cleanup native staging for $DA_USER"
while IFS= read -r db; do
[ -n "$db" ] || continue
native_query mysql "DROP DATABASE IF EXISTS $(mysql_quote_identifier "$db")" >> "$LOG_FILE" 2>&1 || die "cannot drop native staged database: $db"
done < <(list_native_databases)
while IFS=$'\t' read -r user host; do
[ -n "${user:-}" ] || continue
[ -n "${host:-}" ] || continue
remaining="$(native_query mysql "
SELECT COUNT(*)
FROM db
WHERE User='$(mysql_escape_literal "$user")'
AND Db NOT LIKE '$(mysql_escape_literal "$DA_USER")\\_%' ESCAPE '\\'
" | tail -n 1)"
if [ "${remaining:-0}" = "0" ]; then
native_query mysql "DROP USER IF EXISTS '$(mysql_escape_literal "$user")'@'$(mysql_escape_literal "$host")'" >> "$LOG_FILE" 2>&1 || true
else
log "keeping native user ${user}@${host}; it still has non-staging grants"
fi
done < <(list_native_user_hosts)
}
sync_remote_hosts_if_enabled() {
local allow sync_script
allow="$(read_plugin_setting allow_remote_hosts 0)"
[ "$allow" = "1" ] || return 0
sync_script="$(read_plugin_setting MYSQL_PLUGIN_SUDO_SYNC_HOSTS /usr/local/directadmin/plugins/alt-mysql/scripts/setup/mysql_host_sync.sh)"
if [ -x "$sync_script" ]; then
"$sync_script" >> "$LOG_FILE" 2>&1 || log "WARNING: host sync failed after native staging migration"
fi
}
main() {
local db tmp_dump user host overwrite cleanup native_count
[ -r "$NATIVE_MY_CNF" ] || die "native DirectAdmin MySQL client config is not readable: $NATIVE_MY_CNF"
NATIVE_MYSQL_BIN="$(mysql_native_binary mysql)" || die "native mysql/mariadb client not found"
NATIVE_MYSQLDUMP_BIN="$(mysql_native_binary mysqldump)" || die "native mysqldump/mariadb-dump client not found"
mysql_load_alt_credentials
mysql_ensure_metadata_schema
native_mysql mysql -e "SELECT 1" >/dev/null 2>&1 || die "native DirectAdmin MySQL staging is not reachable"
mysql_exec mysql -e "SELECT 1" >/dev/null 2>&1 || die "alt-mariadb is not reachable"
overwrite="${OVERWRITE_POLICY:-$(read_plugin_setting DA_ALT_MYSQL_RESTORE_OVERWRITE allow)}"
cleanup="${CLEANUP_POLICY:-$(read_plugin_setting DA_ALT_MYSQL_NATIVE_CLEANUP after_success)}"
case "$overwrite" in allow|deny) ;; prompt) overwrite="deny" ;; *) overwrite="deny" ;; esac
case "$cleanup" in after_success|keep) ;; *) cleanup="keep" ;; esac
native_count="$(list_native_databases | wc -l | tr -d '[:space:]')"
if [ "${native_count:-0}" = "0" ]; then
log "no native staged databases for $DA_USER"
return 0
fi
log "native staging migration start: user=$DA_USER db_count=$native_count overwrite=$overwrite cleanup=$cleanup"
if [ "$overwrite" = "deny" ]; then
while IFS= read -r db; do
[ -n "$db" ] || continue
if [ -n "$(mysql_exec mysql -Nse "SELECT 1 FROM information_schema.SCHEMATA WHERE SCHEMA_NAME='$(mysql_escape_literal "$db")' LIMIT 1")" ]; then
die "target database exists and overwrite is denied: $db"
fi
done < <(list_native_databases)
fi
while IFS=$'\t' read -r user host; do
[ -n "${user:-}" ] || continue
[ -n "${host:-}" ] || continue
log "preserving native user definition: ${user}@${host}"
apply_user_definition_to_alt "$user" "$host"
done < <(list_native_user_hosts)
metadata_cleanup_for_user
while IFS= read -r db; do
[ -n "$db" ] || continue
db="$(safe_identifier "$db")"
if [ "$overwrite" = "deny" ] && [ -n "$(mysql_exec mysql -Nse "SELECT 1 FROM information_schema.SCHEMATA WHERE SCHEMA_NAME='$(mysql_escape_literal "$db")' LIMIT 1")" ]; then
die "target database exists and overwrite is denied: $db"
fi
tmp_dump="$(mktemp)"
log "dumping native staged database: $db"
"$NATIVE_MYSQLDUMP_BIN" --defaults-extra-file="$NATIVE_MY_CNF" \
--single-transaction \
--quick \
--skip-lock-tables \
--routines \
--triggers \
--events \
--default-character-set=utf8mb4 \
"$db" > "$tmp_dump" 2>>"$LOG_FILE" || {
rm -f "$tmp_dump"
die "cannot dump native staged database: $db"
}
log "importing database into alt-mariadb: $db"
backup_alt_database_for_rollback "$db" || {
rm -f "$tmp_dump"
die "cannot create rollback dump before importing native staged database: $db"
}
if ! mysql_exec mysql -e "DROP DATABASE IF EXISTS $(mysql_quote_identifier "$db");" >> "$LOG_FILE" 2>&1; then
restore_alt_database_from_rollback "$db" || true
rm -f "$tmp_dump"
die "cannot drop target database before importing native staged database: $db"
fi
if ! mysql_exec mysql -e "CREATE DATABASE $(mysql_quote_identifier "$db") CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" >> "$LOG_FILE" 2>&1; then
restore_alt_database_from_rollback "$db" || true
rm -f "$tmp_dump"
die "cannot create target database before importing native staged database: $db"
fi
mysql_exec "$db" < "$tmp_dump" >> "$LOG_FILE" 2>&1 || {
restore_alt_database_from_rollback "$db" || true
rm -f "$tmp_dump"
die "cannot import native staged database into alt-mariadb: $db"
}
rm -f "$tmp_dump"
cleanup_restore_rollback
verify_alt_database "$db"
rebuild_metadata_for_db "$db"
done < <(list_native_databases)
while IFS=$'\t' read -r user host; do
[ -n "${user:-}" ] || continue
[ -n "${host:-}" ] || continue
log "applying preserved grants: ${user}@${host}"
apply_grants_to_alt "$user" "$host"
done < <(list_native_user_hosts)
mysql_exec mysql -e "FLUSH PRIVILEGES;" >> "$LOG_FILE" 2>&1 || true
sync_remote_hosts_if_enabled
if [ "$cleanup" = "after_success" ]; then
drop_native_staging
else
log "native staging cleanup disabled by policy"
fi
log "native staging migration completed: user=$DA_USER"
}
main "$@"
+249
View File
@@ -0,0 +1,249 @@
#!/bin/bash
set -euo pipefail
PLUGIN_DIR="$(cd "$(dirname "$0")/.." && pwd)"
SETTINGS_FILE="${ALT_MYSQL_SETTINGS_FILE:-$PLUGIN_DIR/plugin-settings.conf}"
CPIF="${ALT_MYSQL_CUSTOM_PACKAGE_ITEMS_CONF:-/usr/local/directadmin/data/admin/custom_package_items.conf}"
SUDOERS_FILE="/etc/sudoers.d/directadmin-alt-mysql-plugin"
CRON_FILE="/etc/cron.d/alt-mysql-jobs"
IS_ROOT=0
if [ "${ALT_MYSQL_TEST_ASSUME_ROOT:-0}" = "1" ] || [ "$(id -u)" -eq 0 ]; then
IS_ROOT=1
fi
read_plugin_setting() {
local key="$1"
local default="$2"
local line value
[ -r "$SETTINGS_FILE" ] || {
printf '%s\n' "$default"
return 0
}
line="$(grep -E "^${key}=" "$SETTINGS_FILE" | tail -n 1 || true)"
if [ -z "$line" ]; then
printf '%s\n' "$default"
return 0
fi
value="${line#*=}"
value="${value%%#*}"
value="${value%\"}"
value="${value#\"}"
value="${value%\'}"
value="${value#\'}"
value="$(printf '%s' "$value" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
printf '%s\n' "${value:-$default}"
}
setting_enabled() {
case "$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')" in
1|yes|true|on) return 0 ;;
*) return 1 ;;
esac
}
always_enabled() {
setting_enabled "$(read_plugin_setting always_enabled 1)"
}
remove_custom_package_item() {
local key="$1"
local tmp
[ -f "$CPIF" ] || return 0
tmp="$(mktemp)"
grep -Ev "^${key}=" "$CPIF" > "$tmp" || true
cat "$tmp" > "$CPIF"
rm -f "$tmp"
}
set_permissions() {
local owner="$1"
local mode="$2"
local path="$3"
if [ -e "$path" ]; then
chown "$owner" "$path" 2>/dev/null || true
chmod "$mode" "$path" 2>/dev/null || true
fi
}
set_mode_if_exists() {
local mode="$1"
local path="$2"
if [ -e "$path" ]; then
chmod "$mode" "$path" 2>/dev/null || true
fi
}
bootstrap_custom_package_items() {
if [ "$IS_ROOT" -ne 1 ]; then
echo "mysql: warning: uruchom jako root, aby zaktualizować $CPIF" >&2
return 0
fi
touch "$CPIF"
remove_custom_package_item mysql_enabled
remove_custom_package_item mysql
remove_custom_package_item umysql
remove_custom_package_item mysql_max_databases
remove_custom_package_item mysql_unlimited
remove_custom_package_item umysql_max_databases
if always_enabled; then
set_permissions diradmin:diradmin 640 "$CPIF"
return 0
fi
cat >> "$CPIF" <<'EOF'
mysql_enabled=type=checkbox&string=Enable mysql&desc=Zezwól na dostęp do baz danych MySQL&default=no
mysql=type=text&string=Bazy MySQL&desc=&default=5
umysql=type=checkbox&string=Bez ograniczeń&desc=&default=no&custom=autofocus%20onfocus%3D%22%28function%28c%29%7Bif%28c.dataset.pgInit%3D%3D%3D%271%27%29%7Breturn%3B%7Dc.dataset.pgInit%3D%271%27%3Bvar%20l%3Ddocument.querySelector%28%22input%5Bname%3D%27mysql%27%5D%22%29%3Bif%28%21l%29%7Breturn%3B%7Dvar%20r%3Dc.closest%28%27tr%27%29%3Bvar%20td%3Dl.closest%28%27td%27%29%3Bif%28r%26%26td%29%7Bvar%20w%3Ddocument.getElementById%28%27da-pg-unlim-wrap%27%29%3Bif%28%21w%29%7Bw%3Ddocument.createElement%28%27label%27%29%3Bw.id%3D%27da-pg-unlim-wrap%27%3Bw.style.display%3D%27inline-flex%27%3Bw.style.alignItems%3D%27center%27%3Bw.style.gap%3D%278px%27%3Bw.style.marginLeft%3D%2712px%27%3Bw.appendChild%28c%29%3Bw.appendChild%28document.createTextNode%28%27Bez%20ogranicze%C5%84%27%29%29%3Btd.style.display%3D%27flex%27%3Btd.style.alignItems%3D%27center%27%3Btd.style.justifyContent%3D%27space-between%27%3Btd.appendChild%28w%29%3B%7Dr.style.display%3D%27none%27%3B%7Dvar%20s%3Dfunction%28%29%7Bl.disabled%3D%21%21c.checked%3B%7D%3Bc.addEventListener%28%27change%27%2Cs%29%3Bs%28%29%3B%7D%29%28this%29%22%20onchange%3D%22var%20l%3Ddocument.querySelector%28%22input%5Bname%3D%27mysql%27%5D%22%29%3Bif%28l%29%7Bl.disabled%3D%21%21this.checked%3B%7D%22
EOF
set_permissions diradmin:diradmin 640 "$CPIF"
}
bootstrap_sudoers() {
if [ "$IS_ROOT" -ne 1 ]; then
echo "mysql: warning: uruchom jako root, aby dodać sudoers dla synchronizacji hostów MySQL" >&2
return 0
fi
cat > "$SUDOERS_FILE" <<SUDO
# DirectAdmin alt-mysql plugin
diradmin ALL=(root) NOPASSWD: /usr/local/directadmin/plugins/alt-mysql/scripts/setup/mysql_host_sync.sh
SUDO
chmod 440 "$SUDOERS_FILE"
}
bootstrap_worker_cron() {
if [ "$IS_ROOT" -ne 1 ]; then
echo "mysql: warning: uruchom jako root, aby dodać cron workera backup/restore" >&2
return 0
fi
touch "$CRON_FILE"
if ! grep -Fq '/usr/local/directadmin/plugins/alt-mysql/scripts/worker.sh' "$CRON_FILE"; then
echo '* * * * * root /usr/local/directadmin/plugins/alt-mysql/scripts/worker.sh >/dev/null 2>&1' >> "$CRON_FILE"
fi
chmod 644 "$CRON_FILE" 2>/dev/null || true
}
bootstrap_phpmyadmin() {
local phpmyadmin_setup="$PLUGIN_DIR/scripts/setup/phpmyadmin_private_install.sh"
if [ "$IS_ROOT" -ne 1 ]; then
echo "mysql: warning: uruchom jako root, aby przygotować integrację phpMyAdmin." >&2
return 0
fi
if [ ! -f "$phpmyadmin_setup" ]; then
echo "mysql: error: brak skryptu integracji phpMyAdmin: $phpmyadmin_setup" >&2
return 1
fi
bash "$phpmyadmin_setup"
}
bootstrap_da_integration() {
local da_setup="$PLUGIN_DIR/scripts/setup/da_integration_install.sh"
if [ "$IS_ROOT" -ne 1 ]; then
echo "mysql: warning: uruchom jako root, aby zainstalować hooki integracji DirectAdmin backup/restore." >&2
return 0
fi
if [ ! -f "$da_setup" ]; then
echo "mysql: error: brak skryptu integracji DirectAdmin: $da_setup" >&2
return 1
fi
bash "$da_setup" install
}
bootstrap_custombuild_hooks() {
local custombuild_setup="$PLUGIN_DIR/scripts/setup/custombuild_hooks_install.sh"
if [ "$IS_ROOT" -ne 1 ]; then
echo "mysql: warning: uruchom jako root, aby zainstalować hooki CustomBuild po aktualizacjach Roundcube/phpMyAdmin." >&2
return 0
fi
if [ ! -f "$custombuild_setup" ]; then
echo "mysql: error: brak skryptu hooków CustomBuild: $custombuild_setup" >&2
return 1
fi
bash "$custombuild_setup" install
}
activate_plugin() {
local conf="$PLUGIN_DIR/plugin.conf"
if [ -f "$conf" ]; then
sed -i 's/^active=.*/active=yes/' "$conf" || true
sed -i 's/^installed=.*/installed=yes/' "$conf" || true
fi
}
if [ "${ALT_MYSQL_TEST_ONLY_CUSTOM_PACKAGE_ITEMS:-0}" = "1" ]; then
bootstrap_custom_package_items
exit 0
fi
mkdir -p "$PLUGIN_DIR/data"
mkdir -p "$PLUGIN_DIR/exec" "$PLUGIN_DIR/exec/lib" "$PLUGIN_DIR/exec/handlers"
mkdir -p "$PLUGIN_DIR/data/jobs/pending" "$PLUGIN_DIR/data/jobs/processing" "$PLUGIN_DIR/data/jobs/done"
mkdir -p "$PLUGIN_DIR/data/logs" "$PLUGIN_DIR/data/lock" "$PLUGIN_DIR/data/pids" "$PLUGIN_DIR/data/cancel"
mkdir -p "$PLUGIN_DIR/data/uploads"
if [ "$IS_ROOT" -eq 1 ]; then
chown -R diradmin:diradmin "$PLUGIN_DIR" 2>/dev/null || true
else
echo "mysql: warning: uruchom jako root, aby ustawić właściciela plików pluginu (w przeciwnym razie może wystąpić biała strona)." >&2
fi
find "$PLUGIN_DIR" -type d -exec chmod 755 {} + 2>/dev/null || true
set_mode_if_exists 700 "$PLUGIN_DIR/data"
set_mode_if_exists 700 "$PLUGIN_DIR/data/jobs"
set_mode_if_exists 700 "$PLUGIN_DIR/data/jobs/pending"
set_mode_if_exists 700 "$PLUGIN_DIR/data/jobs/processing"
set_mode_if_exists 700 "$PLUGIN_DIR/data/jobs/done"
set_mode_if_exists 700 "$PLUGIN_DIR/data/logs"
set_mode_if_exists 700 "$PLUGIN_DIR/data/lock"
set_mode_if_exists 700 "$PLUGIN_DIR/data/pids"
set_mode_if_exists 700 "$PLUGIN_DIR/data/cancel"
set_mode_if_exists 700 "$PLUGIN_DIR/data/uploads"
find "$PLUGIN_DIR" -type f -name '*.html' -exec chmod 755 {} + 2>/dev/null || true
find "$PLUGIN_DIR" -type f -name '*.raw' -exec chmod 755 {} + 2>/dev/null || true
find "$PLUGIN_DIR" -type f -name '*.css' -exec chmod 644 {} + 2>/dev/null || true
find "$PLUGIN_DIR/scripts" -type f -name '*.sh' -exec chmod 755 {} + 2>/dev/null || true
find "$PLUGIN_DIR" -type f -name '*.php' -exec chmod 640 {} + 2>/dev/null || true
set_mode_if_exists 644 "$PLUGIN_DIR/plugin.conf"
set_mode_if_exists 644 "$PLUGIN_DIR/plugin-settings.conf"
set_mode_if_exists 644 "$PLUGIN_DIR/php.ini"
if [ ! -f "$PLUGIN_DIR/data/secret.key" ]; then
tr -dc 'a-f0-9' </dev/urandom | head -c 64 > "$PLUGIN_DIR/data/secret.key" || true
fi
set_mode_if_exists 600 "$PLUGIN_DIR/data/secret.key"
touch "$PLUGIN_DIR/error.log" 2>/dev/null || true
if [ "$IS_ROOT" -eq 1 ]; then
chown diradmin:diradmin "$PLUGIN_DIR/error.log" 2>/dev/null || true
fi
set_mode_if_exists 640 "$PLUGIN_DIR/error.log"
bootstrap_custom_package_items
bootstrap_sudoers
bootstrap_worker_cron
bootstrap_phpmyadmin
bootstrap_da_integration
bootstrap_custombuild_hooks
activate_plugin
echo "mysql: instalacja zakończona."
echo "Przypomnienie: zapisz pakiety i user.conf w DirectAdmin, aby odświeżyć custom package items."
+872
View File
@@ -0,0 +1,872 @@
#!/usr/bin/env bash
set -euo pipefail
# ==============================================================================
# Konfiguracja instalacji alternatywnej instancji MariaDB dla serwera DirectAdmin
# ==============================================================================
MARIADB_VERSION="10.11"
MARIADB_DIR="/usr/local/mariadb-$MARIADB_VERSION"
DATADIR="/var/lib/mariadb_data"
PORT="33033"
SOCKET="/var/lib/mariadb_data/mariadb.sock"
MYSQL_USER_SOURCE_FILE="/usr/local/directadmin/conf/mysql.conf"
MYSQL_CLIENT_SOURCE_FILE="/usr/local/directadmin/conf/my.cnf"
ALT_MYSQL_CONF="/usr/local/directadmin/conf/alt-mysql.conf"
ALT_MY_CNF="/usr/local/directadmin/conf/alt-my.cnf"
ALT_METADATA_ENV="/usr/local/directadmin/conf/alt-mariadb.env"
INSTANCE_CNF="/etc/alt-mariadb.cnf"
SERVICE_NAME="alt-mariadb"
SERVICE_FILE="/etc/systemd/system/alt-mariadb.service"
LOG_DIR="/var/log/alt-mariadb"
RUN_DIR="/run/alt-mariadb"
SYSTEM_USER="mysql"
SYSTEM_GROUP="mysql"
BIND_ADDRESS="127.0.0.1"
FORCE_INSTALL="no"
FORCE_REINIT="no"
FORCE_SERVICE_REWRITE="yes"
# Jeżeli automatyczne wykrywanie adresu tarballa zawiedzie, ustaw pełny oficjalny URL ręcznie.
MARIADB_TARBALL_URL=""
# Katalog roboczy pobierania. Skrypt nie używa pakietów RPM/DNF MariaDB ani MySQL.
DOWNLOAD_DIR="/usr/local/src"
ALT_ADMIN_USER=""
ALT_ADMIN_PASS=""
DA_PORT=""
DA_SOCKET=""
DA_HOST=""
DA_DATADIR=""
TARBALL_PATH=""
TMP_DIR=""
RESOLVED_MARIADB_RELEASE=""
DOWNLOAD_URL=""
log() {
printf '[INFO] %s\n' "$*"
}
warn() {
printf '[UWAGA] %s\n' "$*" >&2
}
die() {
printf '[BŁĄD] %s\n' "$*" >&2
exit 1
}
cleanup() {
if [[ -n "${TMP_DIR:-}" && -d "$TMP_DIR" ]]; then
rm -rf "$TMP_DIR"
fi
}
trap cleanup EXIT
usage() {
cat <<EOF
Użycie:
$0 <MARIADB_VERSION> <PORT>
Przykłady:
$0 10.11 33033
$0 11.4 3020
$0 11.8 3021
Dozwolone gałęzie MariaDB: 10.11, 11.4, 11.8.
EOF
}
usage_error() {
usage >&2
printf '[BŁĄD] %s\n' "$*" >&2
exit 2
}
apply_cli_args() {
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
usage
exit 0
fi
if [[ $# -eq 0 ]]; then
usage
exit 2
fi
if [[ $# -gt 2 ]]; then
usage_error "Podano zbyt wiele argumentów. Oczekiwano: MARIADB_VERSION PORT."
fi
if [[ $# -lt 2 ]]; then
usage_error "Podaj wersję MariaDB i port TCP, np. 11.4 3020."
fi
if [[ $# -ge 1 && -n "${1:-}" ]]; then
MARIADB_VERSION="$1"
fi
if [[ $# -ge 2 && -n "${2:-}" ]]; then
PORT="$2"
fi
MARIADB_DIR="/usr/local/mariadb-$MARIADB_VERSION"
}
require_root() {
if [[ "$(id -u)" -ne 0 ]]; then
die "Ten skrypt musi zostać uruchomiony jako root."
fi
}
detect_os() {
[[ -r /etc/os-release ]] || die "Nie można odczytać /etc/os-release."
. /etc/os-release
local os_id="${ID:-}"
local os_like="${ID_LIKE:-}"
case "$os_id" in
almalinux|rhel|rocky|centos|ol)
log "Wykryto zgodny system: ${PRETTY_NAME:-$os_id}."
;;
*)
if [[ "$os_like" == *"rhel"* || "$os_like" == *"fedora"* ]]; then
warn "System wygląda na zgodny z RHEL: ${PRETTY_NAME:-$os_id}."
else
die "Ten skrypt jest przeznaczony dla AlmaLinux lub systemów zgodnych z RHEL."
fi
;;
esac
}
read_kv_file_value() {
local file="$1"
local key="$2"
awk -F= -v key="$key" '
$1 == key {
sub(/^[^=]*=/, "")
print
exit
}
' "$file"
}
load_da_credentials() {
[[ -d /usr/local/directadmin ]] || die "Nie znaleziono katalogu /usr/local/directadmin."
[[ -f "$MYSQL_USER_SOURCE_FILE" ]] || die "Nie znaleziono pliku $MYSQL_USER_SOURCE_FILE."
[[ -f "$MYSQL_CLIENT_SOURCE_FILE" ]] || die "Nie znaleziono pliku $MYSQL_CLIENT_SOURCE_FILE."
ALT_ADMIN_USER="$(read_kv_file_value "$MYSQL_USER_SOURCE_FILE" "user")"
ALT_ADMIN_PASS="$(read_kv_file_value "$MYSQL_USER_SOURCE_FILE" "passwd")"
DA_PORT="$(read_kv_file_value "$MYSQL_USER_SOURCE_FILE" "port")"
DA_SOCKET="$(read_kv_file_value "$MYSQL_USER_SOURCE_FILE" "socket")"
DA_HOST="$(read_kv_file_value "$MYSQL_USER_SOURCE_FILE" "host")"
[[ -n "$ALT_ADMIN_USER" ]] || die "Nie udało się odczytać pola user z $MYSQL_USER_SOURCE_FILE."
[[ -n "$ALT_ADMIN_PASS" ]] || die "Nie udało się odczytać pola passwd z $MYSQL_USER_SOURCE_FILE."
DA_DATADIR="$(detect_da_datadir || true)"
log "Odczytano dane administratora DirectAdmin bez wyświetlania hasła."
}
detect_da_datadir() {
local candidate_files=(
"/etc/my.cnf"
"/etc/mysql/my.cnf"
)
local file=""
for file in "${candidate_files[@]}"; do
[[ -r "$file" ]] || continue
awk -F= '
/^[[:space:]]*datadir[[:space:]]*=/ {
value=$2
gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
gsub(/^"|"$/, "", value)
print value
exit
}
' "$file"
return 0
done
if [[ -d /var/lib/mysql ]]; then
printf '/var/lib/mysql\n'
fi
}
validate_version() {
case "$MARIADB_VERSION" in
10.11|11.4|11.8)
log "Wybrano obsługiwaną wersję MariaDB: $MARIADB_VERSION."
;;
*)
die "Nieobsługiwana wersja MariaDB: $MARIADB_VERSION. Dozwolone wartości: 10.11, 11.4, 11.8."
;;
esac
}
validate_port() {
local numeric_port=""
[[ "$PORT" =~ ^[0-9]+$ ]] || die "Nieprawidłowy port MariaDB: $PORT. Podaj liczbę od 1 do 65535."
numeric_port=$((10#$PORT))
(( numeric_port >= 1 && numeric_port <= 65535 )) || die "Nieprawidłowy port MariaDB: $PORT. Podaj liczbę od 1 do 65535."
log "Wybrano port TCP alternatywnej instancji MariaDB: $PORT."
}
canonical_path() {
local path="$1"
if command -v realpath >/dev/null 2>&1; then
realpath -m "$path"
else
printf '%s\n' "$path"
fi
}
port_is_listening() {
local port="$1"
if command -v ss >/dev/null 2>&1; then
ss -ltn "( sport = :$port )" 2>/dev/null | awk 'NR > 1 { found=1 } END { exit !found }'
return $?
fi
if command -v lsof >/dev/null 2>&1; then
lsof -nP -iTCP:"$port" -sTCP:LISTEN >/dev/null 2>&1
return $?
fi
warn "Nie znaleziono narzędzia ss ani lsof, więc nie można pewnie sprawdzić zajętości portu."
return 1
}
validate_paths() {
local alt_socket_canon da_socket_canon alt_datadir_canon da_datadir_canon
alt_socket_canon="$(canonical_path "$SOCKET")"
da_socket_canon="$(canonical_path "${DA_SOCKET:-}")"
alt_datadir_canon="$(canonical_path "$DATADIR")"
da_datadir_canon="$(canonical_path "${DA_DATADIR:-}")"
[[ "$alt_socket_canon" != "$da_socket_canon" ]] || die "Alternatywny socket jest taki sam jak socket DirectAdmin: $SOCKET."
[[ "$alt_datadir_canon" != "$(canonical_path /var/lib/mysql)" ]] || die "Alternatywny katalog danych nie może wskazywać na /var/lib/mysql."
if [[ -n "${DA_DATADIR:-}" ]]; then
[[ "$alt_datadir_canon" != "$da_datadir_canon" ]] || die "Alternatywny katalog danych jest taki sam jak wykryty katalog danych DirectAdmin: $DATADIR."
fi
if [[ -n "${DA_PORT:-}" ]]; then
[[ "$PORT" != "$DA_PORT" ]] || die "Alternatywny port $PORT jest taki sam jak port MariaDB DirectAdmin."
fi
if port_is_listening "$PORT"; then
die "Port $PORT jest już zajęty. Zmień PORT w sekcji konfiguracji."
fi
if [[ -S "$SOCKET" ]] && ! systemctl is-active --quiet "$SERVICE_NAME" 2>/dev/null; then
die "Socket $SOCKET już istnieje, ale usługa $SERVICE_NAME nie jest aktywna. Przerwano dla bezpieczeństwa."
fi
[[ "$SOCKET" == "$DATADIR"* || "$(dirname "$SOCKET")" != "/var/lib/mysql" ]] || die "Socket nie może znajdować się w katalogu DirectAdmin MariaDB."
log "Sprawdzono rozdzielenie portu, socketu i katalogu danych od instancji DirectAdmin."
}
install_dependencies() {
local deps=(curl tar gzip xz libaio ncurses-compat-libs)
local dep=""
local safe_deps=()
command -v dnf >/dev/null 2>&1 || die "Nie znaleziono dnf. Ten skrypt instaluje neutralne zależności wyłącznie przez dnf."
for dep in "${deps[@]}"; do
if [[ "$dep" == *mariadb* || "$dep" == *mysql* || "$dep" == *MariaDB* || "$dep" == *MySQL* ]]; then
die "Wewnętrzny błąd bezpieczeństwa: próba instalacji niedozwolonego pakietu $dep."
fi
safe_deps+=("$dep")
done
log "Instaluję neutralne zależności systemowe bez pakietów MariaDB/MySQL."
dnf install -y "${safe_deps[@]}"
}
detect_architecture() {
local machine
machine="$(uname -m)"
case "$machine" in
x86_64|amd64)
printf 'x86_64\n'
;;
*)
die "Nieobsługiwana architektura: $machine. Skrypt obsługuje automatycznie tylko x86_64/amd64."
;;
esac
}
resolve_latest_release_from_archive() {
local series="$1"
local listing=""
local release=""
listing="$(curl -fsSL "https://archive.mariadb.org/" || true)"
release="$(printf '%s\n' "$listing" \
| grep -Eo "mariadb-${series//./\\.}\.[0-9]+/" \
| sed 's#^mariadb-##; s#/$##' \
| sort -V \
| tail -n 1)"
[[ -n "$release" ]] || return 1
printf '%s\n' "$release"
}
url_exists() {
local url="$1"
curl -fsIL --retry 2 --connect-timeout 15 "$url" >/dev/null 2>&1
}
resolve_tarball_from_directory() {
local release="$1"
local arch="$2"
local dir_url="https://archive.mariadb.org/mariadb-${release}/bintar-linux-systemd-${arch}/"
local listing=""
local file=""
listing="$(curl -fsSL "$dir_url" || true)"
file="$(printf '%s\n' "$listing" \
| grep -Eo "mariadb-${release}-linux-systemd-${arch}\.tar\.(gz|xz)" \
| head -n 1)"
[[ -n "$file" ]] || return 1
printf '%s%s\n' "$dir_url" "$file"
}
resolve_download_url() {
local arch=""
local candidate=""
local candidates=()
arch="$(detect_architecture)"
if [[ -n "$MARIADB_TARBALL_URL" ]]; then
DOWNLOAD_URL="$MARIADB_TARBALL_URL"
log "Używam ręcznie ustawionego adresu tarballa MariaDB."
return 0
fi
log "Wyszukuję oficjalny tarball MariaDB w archiwum MariaDB dla gałęzi $MARIADB_VERSION."
warn "Nazwy tarballi mogą zależeć od schematu mirrorów MariaDB; w razie błędu ustaw MARIADB_TARBALL_URL ręcznie."
RESOLVED_MARIADB_RELEASE="$(resolve_latest_release_from_archive "$MARIADB_VERSION" || true)"
[[ -n "$RESOLVED_MARIADB_RELEASE" ]] || die "Nie udało się wykryć najnowszego wydania dla gałęzi $MARIADB_VERSION."
if candidate="$(resolve_tarball_from_directory "$RESOLVED_MARIADB_RELEASE" "$arch" || true)"; then
DOWNLOAD_URL="$candidate"
log "Wykryto tarball MariaDB $RESOLVED_MARIADB_RELEASE."
return 0
fi
candidates=(
"https://archive.mariadb.org/mariadb-${RESOLVED_MARIADB_RELEASE}/bintar-linux-systemd-${arch}/mariadb-${RESOLVED_MARIADB_RELEASE}-linux-systemd-${arch}.tar.gz"
"https://archive.mariadb.org/mariadb-${RESOLVED_MARIADB_RELEASE}/bintar-linux-systemd-${arch}/mariadb-${RESOLVED_MARIADB_RELEASE}-linux-systemd-${arch}.tar.xz"
"https://downloads.mariadb.org/f/mariadb-${RESOLVED_MARIADB_RELEASE}/bintar-linux-systemd-${arch}/mariadb-${RESOLVED_MARIADB_RELEASE}-linux-systemd-${arch}.tar.gz/from/https%3A//archive.mariadb.org/"
)
for candidate in "${candidates[@]}"; do
if url_exists "$candidate"; then
DOWNLOAD_URL="$candidate"
log "Wykryto działający adres tarballa MariaDB."
return 0
fi
done
die "Nie udało się automatycznie ustalić URL tarballa. Ustaw MARIADB_TARBALL_URL w sekcji konfiguracji."
}
download_tarball() {
local filename=""
[[ -n "$DOWNLOAD_URL" ]] || die "Brak adresu pobierania tarballa."
install -d -m 0755 "$DOWNLOAD_DIR"
filename="$(basename "${DOWNLOAD_URL%%\?*}")"
[[ "$filename" == *.tar.gz || "$filename" == *.tar.xz || "$filename" == *.tgz ]] || filename="mariadb-${RESOLVED_MARIADB_RELEASE:-$MARIADB_VERSION}-linux-systemd.tar.gz"
TARBALL_PATH="$DOWNLOAD_DIR/$filename"
log "Pobieram oficjalny tarball MariaDB do $TARBALL_PATH."
curl -fL --retry 3 --connect-timeout 20 -o "$TARBALL_PATH" "$DOWNLOAD_URL"
[[ -s "$TARBALL_PATH" ]] || die "Pobrany tarball jest pusty albo nie istnieje: $TARBALL_PATH."
}
extract_tarball() {
local parent_dir=""
local extracted_dir=""
local backup_dir=""
[[ -s "$TARBALL_PATH" ]] || die "Nie znaleziono tarballa do rozpakowania."
if [[ -d "$MARIADB_DIR" ]] && [[ -n "$(find "$MARIADB_DIR" -mindepth 1 -maxdepth 1 -print -quit)" ]]; then
if [[ -x "$MARIADB_DIR/bin/mariadbd" && "$FORCE_INSTALL" != "yes" ]]; then
log "Katalog $MARIADB_DIR już zawiera binaria MariaDB, używam istniejącej instalacji programu."
return 0
fi
if [[ "$FORCE_INSTALL" != "yes" ]]; then
die "Katalog $MARIADB_DIR już istnieje i nie jest pusty. Ustaw FORCE_INSTALL=\"yes\", aby pozwolić na bezpieczne zastąpienie."
fi
backup_dir="${MARIADB_DIR}.backup.$(date +%Y%m%d%H%M%S)"
log "Tworzę kopię istniejącego katalogu programu: $backup_dir."
mv "$MARIADB_DIR" "$backup_dir"
fi
parent_dir="$(dirname "$MARIADB_DIR")"
install -d -m 0755 "$parent_dir"
TMP_DIR="$(mktemp -d)"
log "Rozpakowuję tarball MariaDB do katalogu tymczasowego."
tar -xf "$TARBALL_PATH" -C "$TMP_DIR"
extracted_dir="$(find "$TMP_DIR" -mindepth 1 -maxdepth 1 -type d -print -quit)"
[[ -n "$extracted_dir" ]] || die "Tarball nie zawiera oczekiwanego katalogu głównego MariaDB."
rm -rf "$MARIADB_DIR"
mv "$extracted_dir" "$MARIADB_DIR"
chown -R root:root "$MARIADB_DIR"
log "MariaDB została rozpakowana do $MARIADB_DIR."
}
prepare_user_and_dirs() {
local socket_dir=""
if getent passwd "$SYSTEM_USER" >/dev/null 2>&1 && getent group "$SYSTEM_GROUP" >/dev/null 2>&1; then
log "Używam istniejącego użytkownika systemowego $SYSTEM_USER:$SYSTEM_GROUP."
elif getent passwd mysql >/dev/null 2>&1 && getent group mysql >/dev/null 2>&1; then
SYSTEM_USER="mysql"
SYSTEM_GROUP="mysql"
log "Używam istniejącego użytkownika systemowego mysql:mysql."
else
SYSTEM_USER="altmysql"
SYSTEM_GROUP="altmysql"
if ! getent group "$SYSTEM_GROUP" >/dev/null 2>&1; then
groupadd --system "$SYSTEM_GROUP"
fi
if ! getent passwd "$SYSTEM_USER" >/dev/null 2>&1; then
useradd --system --gid "$SYSTEM_GROUP" --home-dir "$DATADIR" --shell /sbin/nologin "$SYSTEM_USER"
fi
log "Utworzono dedykowanego użytkownika systemowego $SYSTEM_USER:$SYSTEM_GROUP."
fi
socket_dir="$(dirname "$SOCKET")"
install -d -m 0750 -o "$SYSTEM_USER" -g "$SYSTEM_GROUP" "$DATADIR"
install -d -m 0750 -o "$SYSTEM_USER" -g "$SYSTEM_GROUP" "$LOG_DIR"
install -d -m 0750 -o "$SYSTEM_USER" -g "$SYSTEM_GROUP" "$socket_dir"
install -d -m 0750 -o "$SYSTEM_USER" -g "$SYSTEM_GROUP" "$RUN_DIR"
chown -R "$SYSTEM_USER:$SYSTEM_GROUP" "$DATADIR" "$LOG_DIR" "$socket_dir"
}
backup_existing_file() {
local file="$1"
local backup=""
if [[ -e "$file" ]]; then
backup="${file}.backup.$(date +%Y%m%d%H%M%S)"
cp -a "$file" "$backup"
log "Utworzono kopię pliku $file jako $backup."
fi
}
write_file_atomic() {
local target="$1"
local mode="$2"
local owner="$3"
local group="$4"
local tmp_file=""
tmp_file="$(mktemp)"
cat > "$tmp_file"
install -m "$mode" -o "$owner" -g "$group" "$tmp_file" "$target"
rm -f "$tmp_file"
}
cnf_double_quote() {
local value="$1"
value="${value//\\/\\\\}"
value="${value//\"/\\\"}"
printf '%s' "$value"
}
write_instance_config() {
backup_existing_file "$INSTANCE_CNF"
write_file_atomic "$INSTANCE_CNF" 0640 root "$SYSTEM_GROUP" <<EOF
[client]
port=$PORT
socket=$SOCKET
[mariadb]
user=$SYSTEM_USER
basedir=$MARIADB_DIR
datadir=$DATADIR
port=$PORT
socket=$SOCKET
pid-file=$RUN_DIR/alt-mariadb.pid
bind-address=$BIND_ADDRESS
skip-name-resolve
log-error=$LOG_DIR/alt-mariadb.log
plugin-dir=$MARIADB_DIR/lib/plugin
tmpdir=/tmp
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci
max_connections=100
innodb_buffer_pool_size=512M
innodb_log_file_size=256M
local-infile=0
symbolic-links=0
EOF
log "Zapisano konfigurację alternatywnej instancji w $INSTANCE_CNF z prawem odczytu dla grupy $SYSTEM_GROUP."
}
credential_group() {
if getent group diradmin >/dev/null 2>&1; then
printf 'diradmin\n'
else
printf 'root\n'
fi
}
write_da_style_credential_files() {
local group_name=""
local cnf_user=""
local cnf_pass=""
local cnf_socket=""
local credential_mode="0600"
group_name="$(credential_group)"
if [[ "$group_name" == "diradmin" ]]; then
credential_mode="0640"
fi
cnf_user="$(cnf_double_quote "$ALT_ADMIN_USER")"
cnf_pass="$(cnf_double_quote "$ALT_ADMIN_PASS")"
cnf_socket="$(cnf_double_quote "$SOCKET")"
backup_existing_file "$ALT_MYSQL_CONF"
backup_existing_file "$ALT_MY_CNF"
umask 077
write_file_atomic "$ALT_MYSQL_CONF" "$credential_mode" root "$group_name" <<EOF
host=
passwd=$ALT_ADMIN_PASS
port=$PORT
socket=$SOCKET
user=$ALT_ADMIN_USER
EOF
write_file_atomic "$ALT_MY_CNF" "$credential_mode" root "$group_name" <<EOF
[client]
user="$cnf_user"
password="$cnf_pass"
protocol="socket"
socket="$cnf_socket"
EOF
log "Zapisano pliki poświadczeń DirectAdmin dla alternatywnej instancji bez wyświetlania hasła."
}
sh_single_quote() {
local value="$1"
value="${value//\'/\'\\\'\'}"
printf "'%s'" "$value"
}
write_alt_metadata_env() {
local group_name=""
local metadata_mode="0600"
group_name="$(credential_group)"
if [[ "$group_name" == "diradmin" ]]; then
metadata_mode="0640"
fi
backup_existing_file "$ALT_METADATA_ENV"
write_file_atomic "$ALT_METADATA_ENV" "$metadata_mode" root "$group_name" <<EOF
# Metadata alternatywnej instancji MariaDB zarządzanej przez install_db.sh.
# Ten plik jest kontraktem dla pluginu DirectAdmin i nie jest natywną konfiguracją MySQL DirectAdmina.
SERVICE_NAME=$(sh_single_quote "$SERVICE_NAME")
MARIADB_VERSION=$(sh_single_quote "$MARIADB_VERSION")
MARIADB_RELEASE=$(sh_single_quote "${RESOLVED_MARIADB_RELEASE:-$MARIADB_VERSION}")
MARIADB_DIR=$(sh_single_quote "$MARIADB_DIR")
DATADIR=$(sh_single_quote "$DATADIR")
PORT=$(sh_single_quote "$PORT")
SOCKET=$(sh_single_quote "$SOCKET")
ALT_MYSQL_CONF=$(sh_single_quote "$ALT_MYSQL_CONF")
ALT_MY_CNF=$(sh_single_quote "$ALT_MY_CNF")
INSTANCE_CNF=$(sh_single_quote "$INSTANCE_CNF")
SERVICE_FILE=$(sh_single_quote "$SERVICE_FILE")
LOG_DIR=$(sh_single_quote "$LOG_DIR")
RUN_DIR=$(sh_single_quote "$RUN_DIR")
SYSTEM_USER=$(sh_single_quote "$SYSTEM_USER")
SYSTEM_GROUP=$(sh_single_quote "$SYSTEM_GROUP")
BIND_ADDRESS=$(sh_single_quote "$BIND_ADDRESS")
EOF
log "Zapisano metadane alternatywnej instancji w $ALT_METADATA_ENV."
}
datadir_is_initialized() {
[[ -d "$DATADIR/mysql" ]] && [[ -f "$DATADIR/mysql/user.frm" || -f "$DATADIR/mysql/global_priv.frm" || -f "$DATADIR/mysql/global_priv.ibd" ]]
}
initialize_datadir() {
local install_db=""
if datadir_is_initialized; then
if [[ "$FORCE_REINIT" != "yes" ]]; then
log "Katalog danych wygląda na zainicjalizowany, pomijam inicjalizację."
return 0
fi
die "FORCE_REINIT=\"yes\" wymaga ręcznego opróżnienia lub zabezpieczenia katalogu $DATADIR. Skrypt nie usuwa danych automatycznie."
fi
if [[ -n "$(find "$DATADIR" -mindepth 1 -maxdepth 1 -print -quit)" ]] && [[ "$FORCE_REINIT" != "yes" ]]; then
die "Katalog $DATADIR zawiera pliki, ale nie wygląda na zainicjalizowany katalog MariaDB. Przerwano dla bezpieczeństwa."
fi
if [[ -x "$MARIADB_DIR/scripts/mariadb-install-db" ]]; then
install_db="$MARIADB_DIR/scripts/mariadb-install-db"
elif [[ -x "$MARIADB_DIR/bin/mariadb-install-db" ]]; then
install_db="$MARIADB_DIR/bin/mariadb-install-db"
else
die "Nie znaleziono mariadb-install-db w rozpakowanym katalogu MariaDB."
fi
log "Inicjalizuję katalog danych alternatywnej instancji MariaDB."
if ! "$install_db" \
--defaults-file="$INSTANCE_CNF" \
--basedir="$MARIADB_DIR" \
--datadir="$DATADIR" \
--user="$SYSTEM_USER" \
--auth-root-authentication-method=normal; then
warn "Inicjalizacja z auth-root-authentication-method=normal nie powiodła się, próbuję wariantu zgodności."
"$install_db" \
--defaults-file="$INSTANCE_CNF" \
--basedir="$MARIADB_DIR" \
--datadir="$DATADIR" \
--user="$SYSTEM_USER"
fi
chown -R "$SYSTEM_USER:$SYSTEM_GROUP" "$DATADIR"
}
write_systemd_service() {
if [[ -e "$SERVICE_FILE" && "$FORCE_SERVICE_REWRITE" != "yes" ]]; then
die "Plik usługi $SERVICE_FILE już istnieje. Ustaw FORCE_SERVICE_REWRITE=\"yes\", aby go odtworzyć po wykonaniu kopii."
fi
backup_existing_file "$SERVICE_FILE"
write_file_atomic "$SERVICE_FILE" 0644 root root <<EOF
[Unit]
Description=Alternatywna instancja MariaDB
After=network.target
[Service]
Type=simple
User=$SYSTEM_USER
Group=$SYSTEM_GROUP
RuntimeDirectory=alt-mariadb
RuntimeDirectoryMode=0750
ExecStart=$MARIADB_DIR/bin/mariadbd --defaults-file=$INSTANCE_CNF
Restart=on-failure
RestartSec=5
LimitNOFILE=65535
PrivateTmp=false
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
log "Zapisano i przeładowano usługę systemd $SERVICE_NAME."
}
print_service_diagnostics() {
warn "Usługa $SERVICE_NAME nie uruchomiła się poprawnie. Poniżej ostatnie wpisy diagnostyczne."
if [[ -f "$LOG_DIR/alt-mariadb.log" ]]; then
warn "Ostatnie linie z $LOG_DIR/alt-mariadb.log:"
tail -n 80 "$LOG_DIR/alt-mariadb.log" >&2 || true
else
warn "Plik logu $LOG_DIR/alt-mariadb.log jeszcze nie istnieje."
fi
if command -v journalctl >/dev/null 2>&1; then
warn "Ostatnie wpisy journalctl dla $SERVICE_NAME:"
journalctl -u "$SERVICE_NAME" -n 80 --no-pager >&2 || true
fi
}
start_service() {
local i=""
log "Włączam i uruchamiam usługę $SERVICE_NAME."
systemctl enable "$SERVICE_NAME"
systemctl restart "$SERVICE_NAME"
for i in $(seq 1 60); do
if [[ -S "$SOCKET" ]]; then
log "Socket alternatywnej instancji jest dostępny."
return 0
fi
sleep 1
done
systemctl status "$SERVICE_NAME" --no-pager || true
print_service_diagnostics
die "Socket $SOCKET nie pojawił się w ciągu 60 sekund."
}
sql_quote() {
local value="$1"
value="${value//\\/\\\\}"
value="${value//\'/\'\'}"
printf "'%s'" "$value"
}
run_sql_as_root_or_admin() {
local sql="$1"
if "$MARIADB_DIR/bin/mariadb" --defaults-extra-file="$ALT_MY_CNF" -e "SELECT 1;" >/dev/null 2>&1; then
printf '%s\n' "$sql" | "$MARIADB_DIR/bin/mariadb" --defaults-extra-file="$ALT_MY_CNF"
return 0
fi
if "$MARIADB_DIR/bin/mariadb" --protocol=socket --socket="$SOCKET" -uroot -e "SELECT 1;" >/dev/null 2>&1; then
printf '%s\n' "$sql" | "$MARIADB_DIR/bin/mariadb" --protocol=socket --socket="$SOCKET" -uroot
return 0
fi
die "Nie można zalogować się do alternatywnej instancji jako root bez hasła ani przez plik $ALT_MY_CNF."
}
create_admin_user() {
local quoted_user=""
local quoted_pass=""
local sql=""
quoted_user="$(sql_quote "$ALT_ADMIN_USER")"
quoted_pass="$(sql_quote "$ALT_ADMIN_PASS")"
sql="
CREATE USER IF NOT EXISTS ${quoted_user}@'localhost' IDENTIFIED BY ${quoted_pass};
ALTER USER ${quoted_user}@'localhost' IDENTIFIED BY ${quoted_pass};
GRANT ALL PRIVILEGES ON *.* TO ${quoted_user}@'localhost' WITH GRANT OPTION;
CREATE USER IF NOT EXISTS ${quoted_user}@'127.0.0.1' IDENTIFIED BY ${quoted_pass};
ALTER USER ${quoted_user}@'127.0.0.1' IDENTIFIED BY ${quoted_pass};
GRANT ALL PRIVILEGES ON *.* TO ${quoted_user}@'127.0.0.1' WITH GRANT OPTION;
FLUSH PRIVILEGES;
"
log "Tworzę lub aktualizuję konto administratora alternatywnej instancji bez wyświetlania hasła."
run_sql_as_root_or_admin "$sql"
}
run_final_tests() {
log "Wykonuję test połączenia przez socket."
"$MARIADB_DIR/bin/mariadb" --defaults-extra-file="$ALT_MY_CNF" -e "SELECT VERSION();"
log "Wykonuję test połączenia TCP do 127.0.0.1 bez wyświetlania hasła w komunikacie."
"$MARIADB_DIR/bin/mariadb" \
--defaults-extra-file="$ALT_MY_CNF" \
--protocol=TCP \
--host=127.0.0.1 \
--port="$PORT" \
-e "SELECT VERSION();"
}
rerun_plugin_install_if_available() {
local script_dir=""
local plugin_install=""
if [[ "${SKIP_PLUGIN_INSTALL_REFRESH:-0}" == "1" ]]; then
warn "Pominięto odświeżenie install.sh pluginu, bo SKIP_PLUGIN_INSTALL_REFRESH=1."
return 0
fi
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
plugin_install="$script_dir/install.sh"
if [[ ! -f "$plugin_install" ]]; then
warn "Nie znaleziono install.sh pluginu przy $plugin_install; pomijam odświeżenie pluginu."
return 0
fi
log "Odświeżam instalację pluginu alt-mysql przez $plugin_install."
if [[ -x "$plugin_install" ]]; then
"$plugin_install"
else
bash "$plugin_install"
fi
}
print_summary() {
cat <<EOF
Instalacja alternatywnej instancji MariaDB zakończona.
Wersja MariaDB: ${RESOLVED_MARIADB_RELEASE:-$MARIADB_VERSION}
Katalog programu: $MARIADB_DIR
Katalog danych: $DATADIR
Port TCP: $PORT
Socket: $SOCKET
Konfiguracja instancji: $INSTANCE_CNF
Usługa systemd: $SERVICE_NAME
Plik usługi: $SERVICE_FILE
Logi: $LOG_DIR/alt-mariadb.log
Plik DirectAdmin alt-mysql.conf: $ALT_MYSQL_CONF
Plik klienta alt-my.cnf: $ALT_MY_CNF
Plik metadanych pluginu: $ALT_METADATA_ENV
Przykładowe polecenia testowe:
systemctl status $SERVICE_NAME --no-pager
$MARIADB_DIR/bin/mariadb --defaults-extra-file=$ALT_MY_CNF -e "SELECT VERSION();"
$MARIADB_DIR/bin/mariadb --defaults-extra-file=$ALT_MY_CNF --protocol=TCP --host=127.0.0.1 --port=$PORT -e "SELECT VERSION();"
Hasło administratora nie zostało wypisane. Znajduje się wyłącznie w chronionych plikach poświadczeń.
EOF
}
main() {
apply_cli_args "$@"
validate_version
validate_port
require_root
detect_os
load_da_credentials
validate_paths
install_dependencies
resolve_download_url
download_tarball
extract_tarball
prepare_user_and_dirs
write_instance_config
write_da_style_credential_files
write_alt_metadata_env
initialize_datadir
write_systemd_service
start_service
create_admin_user
run_final_tests
rerun_plugin_install_if_available
print_summary
}
main "$@"
+300
View File
@@ -0,0 +1,300 @@
#!/bin/bash
set -Eeuo pipefail
ERROR_MSG=""
RESTORE_OK=0
DA_USER=""
DB_NAME=""
JOB_ID=""
LOG_FILE=""
RESTORE_MODE=""
SOURCE_DB_NAME=""
SOURCE_FILE=""
RESTORE_ROLLBACK_FILE=""
RESTORE_ROLLBACK_CREATED=0
JOB_FILE="${1:-}"
if [ -z "$JOB_FILE" ] || [ ! -f "$JOB_FILE" ]; then
echo "mysql: missing job file" >&2
exit 1
fi
PLUGIN_DIR="$(cd "$(dirname "$0")/.." && pwd)"
CONFIG_FILE="${PLUGIN_DIR}/plugin-settings.conf"
LOG_DIR="${PLUGIN_DIR}/data/logs"
COMMON_FILE="${PLUGIN_DIR}/scripts/setup/mysql_common.sh"
mkdir -p "$LOG_DIR"
urlencode() {
local s="$1"
local out=""
local i c
for ((i=0; i<${#s}; i++)); do
c="${s:i:1}"
case "$c" in
[a-zA-Z0-9.~_-]) out+="$c" ;;
' ') out+='%20' ;;
*) printf -v c '%%%02X' "'$c"; out+="$c" ;;
esac
done
printf '%s' "$out"
}
notify_user() {
local code="$1"
if [ -z "${DA_USER:-}" ]; then
return 0
fi
local subject message
if [ "$code" -eq 0 ] && [ "$RESTORE_OK" -eq 1 ]; then
subject="Przywracanie bazy MySQL zakończone pomyślnie"
message="Przywracanie bazy ${DB_NAME} z pliku ${SOURCE_FILE:-nieznany} zakończone."
else
subject="Błąd przywracania bazy MySQL"
if [ -n "$ERROR_MSG" ]; then
message="Przywracanie bazy ${DB_NAME} nie powiodło się: ${ERROR_MSG}"
else
message="Przywracanie bazy ${DB_NAME} nie powiodło się. Sprawdź log zadania."
fi
fi
local task_queue="/usr/local/directadmin/data/task.queue"
local users="select1%3D$(urlencode "$DA_USER")"
local line="action=notify&value=users&subject=$(urlencode "$subject")&message=$(urlencode "$message")&users=${users}"
printf "%s\n" "$line" >> "$task_queue"
}
fail() {
local msg="$1"
ERROR_MSG="$msg"
if [ -n "${LOG_FILE:-}" ]; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: ${msg}" >> "$LOG_FILE"
fi
}
log_unexpected_error() {
local line_no="$1"
local command="$2"
local code="$3"
if [ -z "${LOG_FILE:-}" ]; then
return 0
fi
if [ -n "${ERROR_MSG:-}" ]; then
return 0
fi
ERROR_MSG="Nieoczekiwany błąd wykonywania przywracania (kod ${code}, linia ${line_no})."
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: ${ERROR_MSG} CMD: ${command}" >> "$LOG_FILE"
}
load_plugin_settings() {
if [ ! -f "$CONFIG_FILE" ]; then
fail "Brak pliku konfiguracyjnego pluginu: $CONFIG_FILE"
exit 1
fi
# shellcheck disable=SC1090
source "$CONFIG_FILE"
}
sanitize_mysql_dump() {
awk '
function lower(s) { return tolower(s) }
{
line = $0
l = lower(line)
if (l ~ /^[[:space:]]*create[[:space:]]+database[[:space:]]+/) { next }
if (l ~ /^[[:space:]]*drop[[:space:]]+database[[:space:]]+/) { next }
if (l ~ /^[[:space:]]*use[[:space:]]+[`"]/ ) { next }
if (l ~ /^[[:space:]]*use[[:space:]]+[a-z0-9_]+[[:space:]]*;/) { next }
print line
}
'
}
preclean_database() {
local db_name="$1"
local current_charset=""
local current_collation=""
current_charset="$(mysql_exec mysql -Nse "SELECT DEFAULT_CHARACTER_SET_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = '${db_name}'" 2>>"$LOG_FILE" || true)"
current_collation="$(mysql_exec mysql -Nse "SELECT DEFAULT_COLLATION_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = '${db_name}'" 2>>"$LOG_FILE" || true)"
[ -n "$current_charset" ] || current_charset="utf8mb4"
[ -n "$current_collation" ] || current_collation="utf8mb4_unicode_ci"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Recreating database ${db_name} before restore" >> "$LOG_FILE"
mysql_exec mysql -e "DROP DATABASE IF EXISTS $(mysql_quote_identifier "$db_name");" >> "$LOG_FILE" 2>&1
mysql_exec mysql -e "CREATE DATABASE $(mysql_quote_identifier "$db_name") CHARACTER SET ${current_charset} COLLATE ${current_collation};" >> "$LOG_FILE" 2>&1
}
cleanup_restore_rollback() {
if [ -n "${RESTORE_ROLLBACK_FILE:-}" ] && [ -f "$RESTORE_ROLLBACK_FILE" ]; then
rm -f "$RESTORE_ROLLBACK_FILE"
fi
}
backup_alt_database_for_rollback() {
local db_name="$1"
RESTORE_ROLLBACK_FILE="$(mktemp)"
RESTORE_ROLLBACK_CREATED=0
if [ -z "$(mysql_exec mysql -Nse "SELECT 1 FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = '$(mysql_escape_literal "$db_name")' LIMIT 1" 2>>"$LOG_FILE" || true)" ]; then
rm -f "$RESTORE_ROLLBACK_FILE"
RESTORE_ROLLBACK_FILE=""
return 0
fi
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Creating rollback dump for ${db_name}" >> "$LOG_FILE"
if mysqldump_exec \
--single-transaction \
--quick \
--skip-lock-tables \
--routines \
--triggers \
--events \
--default-character-set=utf8mb4 \
"$db_name" > "$RESTORE_ROLLBACK_FILE" 2>>"$LOG_FILE"; then
RESTORE_ROLLBACK_CREATED=1
return 0
fi
rm -f "$RESTORE_ROLLBACK_FILE"
RESTORE_ROLLBACK_FILE=""
return 1
}
restore_alt_database_from_rollback() {
local db_name="$1"
if [ "$RESTORE_ROLLBACK_CREATED" -ne 1 ] || [ -z "${RESTORE_ROLLBACK_FILE:-}" ] || [ ! -r "$RESTORE_ROLLBACK_FILE" ]; then
return 0
fi
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Restoring rollback dump for ${db_name}" >> "$LOG_FILE"
mysql_exec mysql -e "DROP DATABASE IF EXISTS $(mysql_quote_identifier "$db_name");" >> "$LOG_FILE" 2>&1 || true
mysql_exec mysql -e "CREATE DATABASE $(mysql_quote_identifier "$db_name") CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" >> "$LOG_FILE" 2>&1 || true
mysql_exec "$db_name" < "$RESTORE_ROLLBACK_FILE" >> "$LOG_FILE" 2>&1 || {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: rollback restore failed for ${db_name}" >> "$LOG_FILE"
return 1
}
}
quote_sh_value() {
local value="$1"
value="${value//\'/\'\\\'\'}"
printf "'%s'" "$value"
}
set_job_var() {
local key="$1"
local value="$2"
local tmp
local owner_group=""
if [ -f "$JOB_FILE" ]; then
owner_group="$(stat -c '%u:%g' "$JOB_FILE" 2>/dev/null || stat -f '%u:%g' "$JOB_FILE" 2>/dev/null || true)"
fi
tmp="$(mktemp)"
grep -v "^${key}=" "$JOB_FILE" > "$tmp" 2>/dev/null || true
printf "%s=%s\n" "$key" "$(quote_sh_value "$value")" >> "$tmp"
mv "$tmp" "$JOB_FILE"
chmod 600 "$JOB_FILE" 2>/dev/null || true
if [ "$(id -u)" -eq 0 ] && [ -n "$owner_group" ]; then
chown "$owner_group" "$JOB_FILE" 2>/dev/null || true
fi
}
on_exit() {
local code="$1"
if [ "$code" -ne 0 ] && [ -z "${ERROR_MSG:-}" ] && [ -n "${LOG_FILE:-}" ]; then
ERROR_MSG="Zadanie przywracania zakończyło się błędem (kod ${code}) bez zarejestrowanego szczegółu."
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: ${ERROR_MSG}" >> "$LOG_FILE"
fi
notify_user "$code"
}
trap 'log_unexpected_error "$LINENO" "$BASH_COMMAND" "$?"' ERR
trap cleanup_restore_rollback INT TERM
trap 'code=$?; cleanup_restore_rollback; on_exit "$code"' EXIT
[ -f "$COMMON_FILE" ] || {
echo "Brak helpera MySQL: $COMMON_FILE" >&2
exit 1
}
# shellcheck disable=SC1090
source "$COMMON_FILE"
# shellcheck disable=SC1090
source "$JOB_FILE"
JOB_ID="${JOB_ID:-$(basename "$JOB_FILE" .env)}"
DA_USER="${DA_USER:-}"
DB_NAME="${DB_NAME:-}"
RESTORE_MODE="${RESTORE_MODE:-overwrite}"
SOURCE_DB_NAME="${SOURCE_DB_NAME:-}"
SOURCE_FILE="${SOURCE_FILE:-}"
LOG_FILE="${LOG_DIR}/${JOB_ID}.log"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Start restore job ${JOB_ID}" > "$LOG_FILE"
if [ -z "$DA_USER" ] || [ -z "$DB_NAME" ] || [ -z "$SOURCE_FILE" ]; then
fail "Brak DA_USER, DB_NAME lub SOURCE_FILE w pliku zadania."
exit 1
fi
if [[ "$DB_NAME" != "${DA_USER}_"* ]]; then
fail "Baza ${DB_NAME} nie należy do użytkownika ${DA_USER}."
exit 1
fi
if [ ! -r "$SOURCE_FILE" ]; then
fail "Nie można odczytać pliku źródłowego: ${SOURCE_FILE}"
exit 1
fi
load_plugin_settings
mysql_load_alt_credentials
MYSQL_BIN="$(mysql_alt_binary mysql)" || {
fail "Brak binarium mysql."
exit 1
}
if [ -z "$(mysql_exec mysql -Nse "SELECT 1 FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = '${DB_NAME}'" 2>>"$LOG_FILE" || true)" ]; then
fail "Docelowa baza danych nie istnieje: ${DB_NAME}"
exit 1
fi
if ! backup_alt_database_for_rollback "$DB_NAME"; then
fail "Nie udało się utworzyć kopii rollback przed przywracaniem bazy ${DB_NAME}."
exit 1
fi
if ! preclean_database "$DB_NAME"; then
restore_alt_database_from_rollback "$DB_NAME" || true
fail "Nie udało się przygotować bazy ${DB_NAME} do importu; przywrócono kopię rollback, jeśli była dostępna."
exit 1
fi
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Restore source file: ${SOURCE_FILE}" >> "$LOG_FILE"
if [ -n "$SOURCE_DB_NAME" ]; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Restore source DB label: ${SOURCE_DB_NAME}" >> "$LOG_FILE"
fi
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Restore target DB: ${DB_NAME}" >> "$LOG_FILE"
set +e
if [[ "$SOURCE_FILE" == *.gz ]]; then
gunzip -c "$SOURCE_FILE" | sanitize_mysql_dump | mysql_exec "$DB_NAME" >> "$LOG_FILE" 2>&1
RC=${PIPESTATUS[2]}
else
sanitize_mysql_dump < "$SOURCE_FILE" | mysql_exec "$DB_NAME" >> "$LOG_FILE" 2>&1
RC=${PIPESTATUS[1]}
fi
set -e
if [ "$RC" -ne 0 ]; then
restore_alt_database_from_rollback "$DB_NAME" || true
fail "Import pliku ${SOURCE_FILE} do bazy ${DB_NAME} zakończył się błędem."
exit 1
fi
set_job_var "END_TS" "$(date +%s)"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Restore completed successfully." >> "$LOG_FILE"
RESTORE_OK=1
exit 0
+32
View File
@@ -0,0 +1,32 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=./mysql_common.sh
source "$SCRIPT_DIR/mysql_common.sh"
fail() {
echo "alt-mysql health: $*" >&2
exit 1
}
mysql_load_alt_runtime
mysql_load_alt_credentials
if command -v systemctl >/dev/null 2>&1; then
systemctl is-active --quiet "$MYSQL_ALT_SERVICE_NAME" || fail "service is not active: $MYSQL_ALT_SERVICE_NAME"
fi
[ -r "$MYSQL_ALT_MY_CNF" ] || fail "client defaults file is not readable: $MYSQL_ALT_MY_CNF"
[ -r "$MYSQL_ALT_MYSQL_CONF" ] || fail "credentials file is not readable: $MYSQL_ALT_MYSQL_CONF"
[ -n "$MYSQL_ALT_PORT" ] || fail "alt port was not discovered"
[ -n "$MYSQL_ALT_SOCKET" ] || fail "alt socket was not discovered"
"$(mysql_alt_binary mysql)" --defaults-extra-file="$MYSQL_ALT_MY_CNF" -e "SELECT 1" >/dev/null \
|| fail "cannot connect through $MYSQL_ALT_MY_CNF"
mysql_ensure_metadata_schema
mysql_exec mysql -e "SELECT COUNT(*) FROM da_plugin_database_owners" >/dev/null \
|| fail "metadata schema is not usable"
echo "alt-mysql health: OK"
+126
View File
@@ -0,0 +1,126 @@
#!/bin/bash
set -euo pipefail
PLUGIN_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
CUSTOMBUILD_ROOT="${CUSTOMBUILD_ROOT:-/usr/local/directadmin/custombuild}"
ACTION="${1:-install}"
SETTINGS_FILE="${MYSQL_PLUGIN_SETTINGS_FILE:-$PLUGIN_DIR/plugin-settings.conf}"
MARKER="managed by DirectAdmin MySQL plugin CustomBuild hook"
log() {
printf 'mysql custombuild-hooks: %s\n' "$*"
}
die() {
printf 'mysql custombuild-hooks: %s\n' "$*" >&2
exit 1
}
read_setting() {
local key="$1"
local default="$2"
local line value
[ -r "$SETTINGS_FILE" ] || {
printf '%s\n' "$default"
return 0
}
line="$(grep -E "^${key}=" "$SETTINGS_FILE" | tail -n 1 || true)"
[ -n "$line" ] || {
printf '%s\n' "$default"
return 0
}
value="${line#*=}"
value="${value%%#*}"
value="${value%\"}"
value="${value#\"}"
value="${value%\'}"
value="${value#\'}"
value="$(printf '%s' "$value" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
printf '%s\n' "${value:-$default}"
}
custombuild_hooks_enabled() {
case "$(read_setting CUSTOMBUILD_HOOKS_ENABLED true)" in
true|yes|on|1) return 0 ;;
*) return 1 ;;
esac
}
write_hook() {
local target="$1"
local app="$2"
local repair_script="$3"
local health_script="$4"
local repair_name health_name tmp
[ -f "$repair_script" ] || die "missing repair script: $repair_script"
[ -f "$health_script" ] || die "missing health check script: $health_script"
repair_name="$(basename "$repair_script")"
health_name="$(basename "$health_script")"
mkdir -p "$(dirname "$target")"
tmp="$(mktemp)"
cat > "$tmp" <<EOF
#!/bin/bash
# $MARKER
set -euo pipefail
PLUGIN_DIR='$PLUGIN_DIR'
LOG_DIR="\$PLUGIN_DIR/data/logs"
LOG_FILE="\$LOG_DIR/custombuild-${app}-repair.log"
mkdir -p "\$LOG_DIR" 2>/dev/null || true
{
printf '%s\\n' "=== \$(date '+%Y-%m-%d %H:%M:%S') ${app} CustomBuild post hook ==="
"\$PLUGIN_DIR/scripts/setup/${repair_name}"
if ! "\$PLUGIN_DIR/scripts/setup/${health_name}"; then
printf '%s\\n' "${app}: warning: health check failed after repair"
fi
} >> "\$LOG_FILE" 2>&1
EOF
install -m 0755 "$tmp" "$target"
rm -f "$tmp"
log "installed CustomBuild post hook: $target"
}
install_all() {
custombuild_hooks_enabled || {
log "disabled by CUSTOMBUILD_HOOKS_ENABLED"
return 0
}
write_hook \
"$CUSTOMBUILD_ROOT/custom/hooks/roundcube/post/90-alt-mysql-repair.sh" \
"roundcube" \
"$PLUGIN_DIR/scripts/setup/roundcube_repair_config.sh" \
"$PLUGIN_DIR/scripts/setup/roundcube_health_check.sh"
write_hook \
"$CUSTOMBUILD_ROOT/custom/hooks/phpmyadmin/post/90-alt-mysql-repair.sh" \
"phpmyadmin" \
"$PLUGIN_DIR/scripts/setup/phpmyadmin_repair.sh" \
"$PLUGIN_DIR/scripts/setup/phpmyadmin_health_check.sh"
}
uninstall_all() {
local hook
for hook in \
"$CUSTOMBUILD_ROOT/custom/hooks/roundcube/post/90-alt-mysql-repair.sh" \
"$CUSTOMBUILD_ROOT/custom/hooks/phpmyadmin/post/90-alt-mysql-repair.sh"
do
if [ -f "$hook" ] && grep -Fq "$MARKER" "$hook"; then
rm -f "$hook"
log "removed CustomBuild post hook: $hook"
fi
done
}
case "$ACTION" in
install) install_all ;;
uninstall) uninstall_all ;;
*) die "unknown action: $ACTION" ;;
esac
+124
View File
@@ -0,0 +1,124 @@
#!/bin/bash
set -Eeuo pipefail
PLUGIN_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
HOOK_ROOT="${DA_CUSTOM_HOOK_DIR:-/usr/local/directadmin/scripts/custom}"
ACTION="${1:-install}"
MARKER="managed by DirectAdmin MySQL plugin hook dispatcher"
log() {
printf 'mysql da-integration: %s\n' "$*"
}
die() {
printf 'mysql da-integration: %s\n' "$*" >&2
exit 1
}
write_dispatcher() {
local target="$1"
local tmp
tmp="$(mktemp)"
cat > "$tmp" <<'EOF'
#!/bin/bash
# managed by DirectAdmin MySQL plugin hook dispatcher
set -u
hook_name="$(basename "$0")"
hook_dir="${0}.d"
status=0
if [ ! -d "$hook_dir" ]; then
exit 0
fi
for hook in "$hook_dir"/*.sh; do
[ -e "$hook" ] || continue
[ -x "$hook" ] || continue
DA_HOOK_NAME="$hook_name" "$hook" "$@"
status=$?
if [ "$status" -ne 0 ]; then
exit "$status"
fi
done
exit 0
EOF
install -m 0755 "$tmp" "$target"
rm -f "$tmp"
}
install_hook() {
local hook_name="$1"
local source_script="$2"
local link_name="${3:-10-alt-mysql.sh}"
local target="$HOOK_ROOT/$hook_name"
local hook_dir="$HOOK_ROOT/${hook_name}.d"
local preserved=""
[ -f "$source_script" ] || die "missing hook source: $source_script"
chmod 0755 "$source_script" 2>/dev/null || true
mkdir -p "$hook_dir"
if [ -e "$target" ] && ! grep -Fq "$MARKER" "$target" 2>/dev/null; then
if [ -d "$target" ]; then
die "cannot preserve hook because target is a directory: $target"
fi
preserved="$hook_dir/00-existing.sh"
if [ -e "$preserved" ]; then
preserved="$hook_dir/00-existing.$(date +%Y%m%d%H%M%S).sh"
fi
mv "$target" "$preserved"
chmod 0755 "$preserved" 2>/dev/null || true
log "preserved existing hook $hook_name as $preserved"
fi
write_dispatcher "$target"
ln -sfn "$source_script" "$hook_dir/$link_name"
chmod 0755 "$target" "$source_script" 2>/dev/null || true
log "installed hook $hook_name -> $source_script"
}
remove_hook_link() {
local hook_name="$1"
local link_name="${2:-10-alt-mysql.sh}"
local target="$HOOK_ROOT/$hook_name"
local hook_dir="$HOOK_ROOT/${hook_name}.d"
rm -f "$hook_dir/$link_name"
if [ -d "$hook_dir" ] && ! find "$hook_dir" -mindepth 1 -maxdepth 1 -print -quit | grep -q .; then
rmdir "$hook_dir" 2>/dev/null || true
if [ -f "$target" ] && grep -Fq "$MARKER" "$target" 2>/dev/null; then
rm -f "$target"
fi
fi
}
install_all() {
mkdir -p "$HOOK_ROOT"
install_hook "user_backup_compress_pre.sh" "$PLUGIN_DIR/scripts/da-integration/alt_mysql_user_backup_compress_pre.sh"
install_hook "user_restore_post_pre_cleanup.sh" "$PLUGIN_DIR/scripts/da-integration/alt_mysql_user_restore_post_pre_cleanup.sh"
install_hook "database_create_pre.sh" "$PLUGIN_DIR/scripts/da-integration/block_native_database_action.sh" "20-alt-mysql-block-native-db.sh"
install_hook "database_delete_pre.sh" "$PLUGIN_DIR/scripts/da-integration/block_native_database_action.sh" "20-alt-mysql-block-native-db.sh"
install_hook "database_user_password_change_pre.sh" "$PLUGIN_DIR/scripts/da-integration/block_native_database_action.sh" "20-alt-mysql-block-native-db.sh"
install_hook "database_user_create_post.sh" "$PLUGIN_DIR/scripts/da-integration/block_native_database_action.sh" "20-alt-mysql-observe-native-db.sh"
install_hook "database_destroy_user_post.sh" "$PLUGIN_DIR/scripts/da-integration/block_native_database_action.sh" "20-alt-mysql-observe-native-db.sh"
}
uninstall_all() {
remove_hook_link "user_backup_compress_pre.sh"
remove_hook_link "user_restore_post_pre_cleanup.sh"
remove_hook_link "database_create_pre.sh" "20-alt-mysql-block-native-db.sh"
remove_hook_link "database_delete_pre.sh" "20-alt-mysql-block-native-db.sh"
remove_hook_link "database_user_password_change_pre.sh" "20-alt-mysql-block-native-db.sh"
remove_hook_link "database_user_create_post.sh" "20-alt-mysql-observe-native-db.sh"
remove_hook_link "database_destroy_user_post.sh" "20-alt-mysql-observe-native-db.sh"
}
case "$ACTION" in
install) install_all ;;
uninstall) uninstall_all ;;
*) die "unknown action: $ACTION" ;;
esac
+52
View File
@@ -0,0 +1,52 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=./mysql_common.sh
source "$SCRIPT_DIR/mysql_common.sh"
usage() {
cat <<'EOF'
Użycie:
dbrestore.sh <plik.sql|plik.sql.gz> [nazwa_bazy]
Opis:
Przywraca pojedynczą bazę MySQL z pliku dumpa. Gdy nazwa bazy nie zostanie
podana, skrypt wyprowadza ją z nazwy pliku.
EOF
}
[ "${1:-}" = "--help" ] && {
usage
exit 0
}
[ $# -ge 1 ] || {
usage
exit 1
}
FILE="$1"
[ -f "$FILE" ] || {
echo "Plik nie istnieje: $FILE" >&2
exit 1
}
case "$FILE" in
*.sql|*.sql.gz|*.gz) ;;
*)
echo "Dozwolone są pliki .sql, .sql.gz lub .gz." >&2
exit 1
;;
esac
TARGET_DB="${2:-$(basename "$FILE" | sed -E 's/\.sql(\.gz)?$//; s/\.gz$//')}"
TARGET_DB="$(mysql_safe_identifier "$TARGET_DB")"
mysql_load_alt_credentials
mysql_exec mysql -e "DROP DATABASE IF EXISTS $(mysql_quote_identifier "$TARGET_DB");"
mysql_exec mysql -e "CREATE DATABASE $(mysql_quote_identifier "$TARGET_DB") CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
mysql_restore_stream "$TARGET_DB" "$FILE"
echo "Przywracanie bazy ${TARGET_DB} zakończone."
+99
View File
@@ -0,0 +1,99 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=./mysql_common.sh
source "$SCRIPT_DIR/mysql_common.sh"
BASE_DIR="${BASE_DIR:-/home/admin/mysql_backup}"
DATE_FORMAT="${DATE_FORMAT:-%d-%m-%Y}"
ADD_TIME="${ADD_TIME:-0}"
RETENTION="${RETENTION:-7}"
ENABLE_COMPRESSION="${ENABLE_COMPRESSION:-1}"
LOG_FILE="${LOG_FILE:-/var/log/mysql_backup.log}"
timestamp() {
date '+%Y-%m-%d %H:%M:%S'
}
log() {
printf '[%s] %s\n' "$(timestamp)" "$*" | tee -a "$LOG_FILE"
}
dump_globals_file() {
local output_file="$1"
local tmp_file
tmp_file="$(mktemp)"
{
echo "-- Global users and grants for MySQL"
echo "-- Generated at $(timestamp)"
} > "$tmp_file"
while IFS=$'\t' read -r db_user db_host; do
[ -n "$db_user" ] || continue
[ -n "$db_host" ] || continue
printf "DROP USER IF EXISTS '%s'@'%s';\n" "$db_user" "$db_host" >> "$tmp_file"
mysql_exec mysql -Nse "SHOW CREATE USER '${db_user}'@'${db_host}'" >> "$tmp_file" || true
echo ";" >> "$tmp_file"
while IFS= read -r grant_line; do
[ -n "$grant_line" ] || continue
printf "%s;\n" "$grant_line" >> "$tmp_file"
done < <(mysql_exec mysql -Nse "SHOW GRANTS FOR '${db_user}'@'${db_host}'" || true)
echo >> "$tmp_file"
done < <(
mysql_exec mysql -Nse "
SELECT user, host
FROM mysql.user
WHERE user <> ''
AND user NOT IN ('mysql.sys', 'mysql.session', 'mysql.infoschema', 'mariadb.sys')
ORDER BY user, host
" || true
)
if [ "$ENABLE_COMPRESSION" = "1" ]; then
gzip -9 < "$tmp_file" > "$output_file"
else
mv "$tmp_file" "$output_file"
return 0
fi
rm -f "$tmp_file"
}
mysql_load_alt_credentials
mkdir -p "$BASE_DIR"
if [ "$ADD_TIME" = "1" ]; then
DATE_STR="$(date +"${DATE_FORMAT}_%H-%M")"
else
DATE_STR="$(date +"${DATE_FORMAT}")"
fi
BACKUP_DIR="${BASE_DIR}/${DATE_STR}"
mkdir -p "$BACKUP_DIR"
log "Start backupu MySQL do ${BACKUP_DIR}"
GLOBALS_FILE="${BACKUP_DIR}/_globals.sql"
if [ "$ENABLE_COMPRESSION" = "1" ]; then
GLOBALS_FILE="${GLOBALS_FILE}.gz"
fi
dump_globals_file "$GLOBALS_FILE"
log "Zapisano ${GLOBALS_FILE##*/}"
while IFS= read -r db_name; do
[ -n "$db_name" ] || continue
target_file="${BACKUP_DIR}/${db_name}.sql"
if [ "$ENABLE_COMPRESSION" = "1" ]; then
mysqldump_exec --single-transaction --skip-lock-tables --routines --triggers --events --databases "$db_name" | gzip -9 > "${target_file}.gz"
log "Backup ${db_name} -> ${target_file##*/}.gz"
else
mysqldump_exec --single-transaction --skip-lock-tables --routines --triggers --events --databases "$db_name" > "$target_file"
log "Backup ${db_name} -> ${target_file##*/}"
fi
done < <(mysql_non_system_databases)
if [ "$RETENTION" -gt 0 ]; then
find "$BASE_DIR" -mindepth 1 -maxdepth 1 -type d -mtime +"$RETENTION" -exec rm -rf {} +
fi
log "Backup MySQL zakończony."
+489
View File
@@ -0,0 +1,489 @@
#!/bin/bash
set -euo pipefail
MYSQL_PLUGIN_CREDENTIALS_FILE="${MYSQL_PLUGIN_CREDENTIALS_FILE:-/usr/local/directadmin/conf/alt-mysql.conf}"
MYSQL_PLUGIN_METADATA_FILE="${MYSQL_PLUGIN_METADATA_FILE:-/usr/local/directadmin/conf/alt-mariadb.env}"
MYSQL_PLUGIN_ALT_MY_CNF="${MYSQL_PLUGIN_ALT_MY_CNF:-/usr/local/directadmin/conf/alt-my.cnf}"
MYSQL_PLUGIN_ALT_INSTANCE_CNF="${MYSQL_PLUGIN_ALT_INSTANCE_CNF:-/etc/alt-mariadb.cnf}"
trim_value() {
local value="${1:-}"
value="${value#"${value%%[![:space:]]*}"}"
value="${value%"${value##*[![:space:]]}"}"
printf '%s' "$value"
}
strip_inline_comment() {
local value
value="$(trim_value "${1:-}")"
value="${value%%#*}"
printf '%s' "$(trim_value "$value")"
}
unquote_value() {
local value
value="$(trim_value "${1:-}")"
if [ "${#value}" -ge 2 ] && [ "${value:0:1}" = "'" ] && [ "${value: -1}" = "'" ]; then
value="${value:1:${#value}-2}"
elif [ "${#value}" -ge 2 ] && [ "${value:0:1}" = '"' ] && [ "${value: -1}" = '"' ]; then
value="${value:1:${#value}-2}"
fi
printf '%s' "$value"
}
mysql_require_root() {
if [ "${EUID:-$(id -u)}" -ne 0 ]; then
echo "Ten skrypt musi być uruchomiony jako root." >&2
exit 1
fi
}
mysql_detect_binary() {
local bin_name="$1"
local candidate=""
local alt_name=""
if [ -n "${MYSQL_PLUGIN_CLIENT_BIN_DIR:-}" ]; then
for alt_name in "$bin_name" "$(mysql_binary_alias "$bin_name")"; do
[ -n "$alt_name" ] || continue
candidate="${MYSQL_PLUGIN_CLIENT_BIN_DIR%/}/${alt_name}"
if [ -x "$candidate" ]; then
printf '%s\n' "$candidate"
return 0
fi
done
fi
for alt_name in "$bin_name" "$(mysql_binary_alias "$bin_name")"; do
[ -n "$alt_name" ] || continue
candidate="$(command -v "$alt_name" 2>/dev/null || true)"
if [ -n "$candidate" ]; then
printf '%s\n' "$candidate"
return 0
fi
done
for alt_name in "$bin_name" "$(mysql_binary_alias "$bin_name")"; do
[ -n "$alt_name" ] || continue
while IFS= read -r candidate; do
if [ -x "$candidate" ]; then
printf '%s\n' "$candidate"
return 0
fi
done < <(find /usr/local -maxdepth 3 -type f -path '/usr/local/mariadb-*/bin/'"$alt_name" 2>/dev/null | sort -V -r)
for candidate in /usr/bin/"$alt_name" /usr/local/bin/"$alt_name"; do
if [ -x "$candidate" ]; then
printf '%s\n' "$candidate"
return 0
fi
done
done
return 1
}
mysql_binary_alias() {
case "$1" in
mysql) printf 'mariadb\n' ;;
mysqldump) printf 'mariadb-dump\n' ;;
mysqladmin) printf 'mariadb-admin\n' ;;
mysql_upgrade) printf 'mariadb-upgrade\n' ;;
*) printf '\n' ;;
esac
}
mysql_load_plugin_settings_file() {
local file="${1:-}"
local raw_line key value
[ -n "$file" ] || return 0
[ -r "$file" ] || return 0
while IFS= read -r raw_line || [ -n "$raw_line" ]; do
raw_line="$(trim_value "$raw_line")"
[ -n "$raw_line" ] || continue
case "$raw_line" in
\#*) continue ;;
esac
[[ "$raw_line" == *=* ]] || continue
key="$(trim_value "${raw_line%%=*}")"
value="$(unquote_value "$(strip_inline_comment "${raw_line#*=}")")"
case "$key" in
MYSQL_PLUGIN_CREDENTIALS_FILE) MYSQL_PLUGIN_CREDENTIALS_FILE="$value" ;;
MYSQL_PLUGIN_METADATA_FILE) MYSQL_PLUGIN_METADATA_FILE="$value" ;;
MYSQL_PLUGIN_ALT_MY_CNF) MYSQL_PLUGIN_ALT_MY_CNF="$value" ;;
MYSQL_PLUGIN_ALT_MARIADB_CNF) MYSQL_PLUGIN_ALT_INSTANCE_CNF="$value" ;;
MYSQL_PLUGIN_CLIENT_BIN_DIR) MYSQL_PLUGIN_CLIENT_BIN_DIR="$value" ;;
esac
done < "$file"
}
mysql_read_key_value() {
local file="$1"
local key="$2"
[ -r "$file" ] || return 1
awk -F= -v key="$key" '
function trim(s) {
sub(/^[[:space:]]+/, "", s)
sub(/[[:space:]]+$/, "", s)
return s
}
$0 ~ /^[[:space:]]*#/ || $0 !~ /=/ { next }
{
k = trim($1)
if (k != key) { next }
sub(/^[^=]*=/, "")
v = trim($0)
if (v ~ /^'\''.*'\''$/ || v ~ /^".*"$/) {
v = substr(v, 2, length(v) - 2)
}
print v
exit
}
' "$file"
}
mysql_read_cnf_value() {
local file="$1"
local section="$2"
local key="$3"
[ -r "$file" ] || return 1
awk -F= -v section="$section" -v key="$key" '
function trim(s) {
sub(/^[[:space:]]+/, "", s)
sub(/[[:space:]]+$/, "", s)
return s
}
/^[[:space:]]*#/ || /^[[:space:]]*;/ { next }
/^\[[^]]+\]/ {
current=$0
gsub(/^\[[[:space:]]*/, "", current)
gsub(/[[:space:]]*\]$/, "", current)
current=trim(current)
next
}
current == section && $0 ~ /=/ {
k=trim($1)
if (k != key) { next }
sub(/^[^=]*=/, "")
v=trim($0)
if (v ~ /^'\''.*'\''$/ || v ~ /^".*"$/) {
v=substr(v, 2, length(v)-2)
}
print v
exit
}
' "$file"
}
mysql_load_alt_runtime() {
MYSQL_ALT_METADATA_FILE="${MYSQL_ALT_METADATA_FILE:-$MYSQL_PLUGIN_METADATA_FILE}"
MYSQL_ALT_MYSQL_CONF="${MYSQL_ALT_MYSQL_CONF:-$MYSQL_PLUGIN_CREDENTIALS_FILE}"
MYSQL_ALT_MY_CNF="${MYSQL_ALT_MY_CNF:-$MYSQL_PLUGIN_ALT_MY_CNF}"
MYSQL_ALT_INSTANCE_CNF="${MYSQL_ALT_INSTANCE_CNF:-$MYSQL_PLUGIN_ALT_INSTANCE_CNF}"
MYSQL_ALT_SERVICE_NAME="${MYSQL_ALT_SERVICE_NAME:-alt-mariadb}"
MYSQL_ALT_BASEDIR="${MYSQL_ALT_BASEDIR:-}"
MYSQL_ALT_DATADIR="${MYSQL_ALT_DATADIR:-}"
MYSQL_ALT_PORT="${MYSQL_ALT_PORT:-}"
MYSQL_ALT_SOCKET="${MYSQL_ALT_SOCKET:-}"
if [ -r "$MYSQL_ALT_METADATA_FILE" ]; then
MYSQL_ALT_SERVICE_NAME="$(mysql_read_key_value "$MYSQL_ALT_METADATA_FILE" SERVICE_NAME || printf '%s' "$MYSQL_ALT_SERVICE_NAME")"
MYSQL_ALT_BASEDIR="$(mysql_read_key_value "$MYSQL_ALT_METADATA_FILE" MARIADB_DIR || printf '%s' "$MYSQL_ALT_BASEDIR")"
MYSQL_ALT_DATADIR="$(mysql_read_key_value "$MYSQL_ALT_METADATA_FILE" DATADIR || printf '%s' "$MYSQL_ALT_DATADIR")"
MYSQL_ALT_PORT="$(mysql_read_key_value "$MYSQL_ALT_METADATA_FILE" PORT || printf '%s' "$MYSQL_ALT_PORT")"
MYSQL_ALT_SOCKET="$(mysql_read_key_value "$MYSQL_ALT_METADATA_FILE" SOCKET || printf '%s' "$MYSQL_ALT_SOCKET")"
MYSQL_ALT_MYSQL_CONF="$(mysql_read_key_value "$MYSQL_ALT_METADATA_FILE" ALT_MYSQL_CONF || printf '%s' "$MYSQL_ALT_MYSQL_CONF")"
MYSQL_ALT_MY_CNF="$(mysql_read_key_value "$MYSQL_ALT_METADATA_FILE" ALT_MY_CNF || printf '%s' "$MYSQL_ALT_MY_CNF")"
MYSQL_ALT_INSTANCE_CNF="$(mysql_read_key_value "$MYSQL_ALT_METADATA_FILE" INSTANCE_CNF || printf '%s' "$MYSQL_ALT_INSTANCE_CNF")"
fi
if [ -r "$MYSQL_ALT_MYSQL_CONF" ]; then
[ -n "$MYSQL_ALT_PORT" ] || MYSQL_ALT_PORT="$(mysql_read_key_value "$MYSQL_ALT_MYSQL_CONF" port || true)"
[ -n "$MYSQL_ALT_SOCKET" ] || MYSQL_ALT_SOCKET="$(mysql_read_key_value "$MYSQL_ALT_MYSQL_CONF" socket || true)"
fi
if [ -r "$MYSQL_ALT_INSTANCE_CNF" ]; then
[ -n "$MYSQL_ALT_BASEDIR" ] || MYSQL_ALT_BASEDIR="$(mysql_read_cnf_value "$MYSQL_ALT_INSTANCE_CNF" mariadb basedir || true)"
[ -n "$MYSQL_ALT_DATADIR" ] || MYSQL_ALT_DATADIR="$(mysql_read_cnf_value "$MYSQL_ALT_INSTANCE_CNF" mariadb datadir || true)"
[ -n "$MYSQL_ALT_PORT" ] || MYSQL_ALT_PORT="$(mysql_read_cnf_value "$MYSQL_ALT_INSTANCE_CNF" mariadb port || true)"
[ -n "$MYSQL_ALT_SOCKET" ] || MYSQL_ALT_SOCKET="$(mysql_read_cnf_value "$MYSQL_ALT_INSTANCE_CNF" mariadb socket || true)"
fi
if [ -n "$MYSQL_ALT_BASEDIR" ] && [ -d "$MYSQL_ALT_BASEDIR/bin" ]; then
MYSQL_PLUGIN_CLIENT_BIN_DIR="${MYSQL_PLUGIN_CLIENT_BIN_DIR:-$MYSQL_ALT_BASEDIR/bin}"
fi
[ -n "$MYSQL_ALT_PORT" ] || MYSQL_ALT_PORT="3306"
[ -n "$MYSQL_ALT_MYSQL_CONF" ] || MYSQL_ALT_MYSQL_CONF="/usr/local/directadmin/conf/alt-mysql.conf"
[ -n "$MYSQL_ALT_MY_CNF" ] || MYSQL_ALT_MY_CNF="/usr/local/directadmin/conf/alt-my.cnf"
[ -n "$MYSQL_ALT_INSTANCE_CNF" ] || MYSQL_ALT_INSTANCE_CNF="/etc/alt-mariadb.cnf"
[ -n "$MYSQL_ALT_SERVICE_NAME" ] || MYSQL_ALT_SERVICE_NAME="alt-mariadb"
}
mysql_alt_service_name() {
mysql_load_alt_runtime
printf '%s\n' "$MYSQL_ALT_SERVICE_NAME"
}
mysql_alt_basedir() {
mysql_load_alt_runtime
printf '%s\n' "$MYSQL_ALT_BASEDIR"
}
mysql_alt_datadir() {
mysql_load_alt_runtime
printf '%s\n' "$MYSQL_ALT_DATADIR"
}
mysql_alt_port() {
mysql_load_alt_runtime
printf '%s\n' "$MYSQL_ALT_PORT"
}
mysql_alt_socket() {
mysql_load_alt_runtime
printf '%s\n' "$MYSQL_ALT_SOCKET"
}
mysql_alt_client_defaults() {
mysql_load_alt_runtime
printf '%s\n' "$MYSQL_ALT_MY_CNF"
}
mysql_alt_binary() {
local bin_name="$1"
mysql_load_alt_runtime
mysql_detect_binary "$bin_name"
}
mysql_native_binary() {
local bin_name="$1"
local alt_name candidate
for alt_name in "$bin_name" "$(mysql_binary_alias "$bin_name")"; do
[ -n "$alt_name" ] || continue
for candidate in /usr/bin/"$alt_name" /usr/local/bin/"$alt_name"; do
if [ -x "$candidate" ]; then
printf '%s\n' "$candidate"
return 0
fi
done
candidate="$(command -v "$alt_name" 2>/dev/null || true)"
if [ -n "$candidate" ]; then
printf '%s\n' "$candidate"
return 0
fi
done
return 1
}
mysql_load_credentials() {
local file="${1:-$MYSQL_PLUGIN_CREDENTIALS_FILE}"
[ -r "$file" ] || {
echo "Brak pliku poświadczeń MySQL: $file" >&2
exit 1
}
MYSQL_HOST=""
MYSQL_PORT=""
MYSQL_USER=""
MYSQL_PASSWORD=""
MYSQL_DATABASE=""
MYSQL_SOCKET=""
while IFS= read -r raw_line || [ -n "$raw_line" ]; do
raw_line="$(trim_value "$raw_line")"
[ -n "$raw_line" ] || continue
case "$raw_line" in
\#*) continue ;;
esac
if [[ "$raw_line" == *=* ]]; then
local key="${raw_line%%=*}"
local value="${raw_line#*=}"
key="$(printf '%s' "$key" | tr '[:upper:]' '[:lower:]')"
key="$(trim_value "$key")"
value="$(unquote_value "$(strip_inline_comment "$value")")"
case "$key" in
host|mysql_host) MYSQL_HOST="$value" ;;
port|mysql_port) MYSQL_PORT="$value" ;;
user|username|mysql_user) MYSQL_USER="$value" ;;
password|passwd|pass|mysql_password) MYSQL_PASSWORD="$value" ;;
database|dbname|mysql_database) MYSQL_DATABASE="$value" ;;
socket|mysql_socket) MYSQL_SOCKET="$value" ;;
esac
continue
fi
local host_part port_part db_part user_part pass_part rest
host_part="${raw_line%%:*}"
rest="${raw_line#*:}"
port_part="${rest%%:*}"
rest="${rest#*:}"
db_part="${rest%%:*}"
rest="${rest#*:}"
user_part="${rest%%:*}"
pass_part="${rest#*:}"
MYSQL_HOST="$host_part"
MYSQL_PORT="$port_part"
MYSQL_DATABASE="$db_part"
MYSQL_USER="$user_part"
MYSQL_PASSWORD="$pass_part"
break
done < "$file"
MYSQL_HOST="$(trim_value "${MYSQL_HOST:-}")"
MYSQL_PORT="$(trim_value "${MYSQL_PORT:-}")"
MYSQL_USER="$(trim_value "${MYSQL_USER:-}")"
MYSQL_PASSWORD="${MYSQL_PASSWORD:-}"
MYSQL_DATABASE="$(trim_value "${MYSQL_DATABASE:-}")"
MYSQL_SOCKET="$(trim_value "${MYSQL_SOCKET:-}")"
[ -n "$MYSQL_HOST" ] || MYSQL_HOST="localhost"
[ -n "$MYSQL_PORT" ] || MYSQL_PORT="3306"
[ -n "$MYSQL_USER" ] || MYSQL_USER="root"
[ -n "$MYSQL_DATABASE" ] || MYSQL_DATABASE="mysql"
}
mysql_load_alt_credentials() {
mysql_load_alt_runtime
mysql_load_credentials "$MYSQL_ALT_MYSQL_CONF"
}
mysql_base_args() {
local args=("--user=$MYSQL_USER")
if [ "$MYSQL_HOST" = "localhost" ]; then
if [ -n "$MYSQL_SOCKET" ]; then
args+=("--socket=$MYSQL_SOCKET")
else
args+=("--host=localhost")
fi
else
args+=("--protocol=TCP" "--host=$MYSQL_HOST" "--port=$MYSQL_PORT")
fi
printf '%s\n' "${args[@]}"
}
mysql_exec() {
local database="$1"
shift
local -a args=()
while IFS= read -r arg; do
args+=("$arg")
done < <(mysql_base_args)
MYSQL_PWD="$MYSQL_PASSWORD" "$(mysql_detect_binary mysql)" "${args[@]}" --database="$database" "$@"
}
mysql_exec_sql() {
local database="$1"
local sql="$2"
mysql_exec "$database" -e "$sql"
}
mysqldump_exec() {
local -a args=()
while IFS= read -r arg; do
args+=("$arg")
done < <(mysql_base_args)
MYSQL_PWD="$MYSQL_PASSWORD" "$(mysql_detect_binary mysqldump)" "${args[@]}" "$@"
}
mysql_alt_version() {
mysql_load_alt_credentials
mysql_exec mysql -Nse "SELECT VERSION()" 2>/dev/null || true
}
mysql_ensure_metadata_schema() {
mysql_exec mysql -e "
CREATE TABLE IF NOT EXISTS da_plugin_access_hosts (
da_user VARCHAR(255) NOT NULL,
role_name VARCHAR(255) NOT NULL,
host_pattern VARCHAR(255) NOT NULL,
note VARCHAR(180) NOT NULL DEFAULT '',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (role_name, host_pattern),
KEY access_hosts_da_user_idx (da_user)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS da_plugin_grant_profiles (
db_name VARCHAR(255) NOT NULL,
role_name VARCHAR(255) NOT NULL,
privileges TEXT NOT NULL,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (db_name, role_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS da_plugin_database_owners (
db_name VARCHAR(255) NOT NULL,
da_user VARCHAR(255) NOT NULL,
role_name VARCHAR(255) NOT NULL,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (db_name),
KEY database_owners_da_user_idx (da_user),
KEY database_owners_role_idx (role_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS da_plugin_sso_accounts (
role_name VARCHAR(255) NOT NULL,
host_pattern VARCHAR(255) NOT NULL,
expires_at INT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (role_name, host_pattern),
KEY sso_accounts_expires_idx (expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS da_plugin_system_databases (
service_name VARCHAR(64) NOT NULL,
db_name VARCHAR(255) NOT NULL,
db_user VARCHAR(255) NOT NULL,
endpoint_mode VARCHAR(32) NOT NULL DEFAULT 'tcp',
config_path VARCHAR(512) NOT NULL DEFAULT '',
enabled TINYINT(1) NOT NULL DEFAULT 1,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (service_name),
KEY system_databases_db_idx (db_name),
KEY system_databases_user_idx (db_user)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
"
}
mysql_service_name() {
local service
for service in mysqld mariadb mysql; do
if systemctl list-unit-files "${service}.service" >/dev/null 2>&1; then
printf '%s\n' "$service"
return 0
fi
done
printf '%s\n' "mysqld"
}
mysql_safe_identifier() {
local value="${1:-}"
if [[ ! "$value" =~ ^[A-Za-z0-9_]+$ ]]; then
echo "Nieprawidłowy identyfikator MySQL: $value" >&2
exit 1
fi
printf '%s' "$value"
}
mysql_quote_identifier() {
local value
value="$(mysql_safe_identifier "$1")"
printf '`%s`' "$value"
}
mysql_escape_literal() {
printf "%s" "${1:-}" | sed "s/'/''/g"
}
mysql_non_system_databases() {
mysql_exec mysql -Nse "SHOW DATABASES" | grep -Ev '^(information_schema|performance_schema|mysql|sys)$' || true
}
mysql_restore_stream() {
local database="$1"
local file="$2"
if [[ "$file" == *.gz ]]; then
gunzip -c "$file" | mysql_exec "$database"
return ${PIPESTATUS[1]}
fi
mysql_exec "$database" < "$file"
}
+376
View File
@@ -0,0 +1,376 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PLUGIN_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
PLUGIN_SETTINGS_FILE="${MYSQL_HOST_SYNC_PLUGIN_SETTINGS:-$PLUGIN_DIR/plugin-settings.conf}"
# shellcheck source=./mysql_common.sh
source "$SCRIPT_DIR/mysql_common.sh"
load_plugin_settings() {
local file="$1"
local raw_line key value
[ -r "$file" ] || return 0
while IFS= read -r raw_line || [ -n "$raw_line" ]; do
raw_line="$(trim_value "$raw_line")"
[ -n "$raw_line" ] || continue
case "$raw_line" in
\#*) continue ;;
esac
[[ "$raw_line" == *=* ]] || continue
key="$(trim_value "${raw_line%%=*}")"
value="$(unquote_value "$(strip_inline_comment "${raw_line#*=}")")"
case "$key" in
MYSQL_PLUGIN_CREDENTIALS_FILE) MYSQL_PLUGIN_CREDENTIALS_FILE="$value" ;;
MYSQL_PLUGIN_METADATA_FILE) MYSQL_PLUGIN_METADATA_FILE="$value" ;;
MYSQL_PLUGIN_ALT_MY_CNF) MYSQL_PLUGIN_ALT_MY_CNF="$value" ;;
MYSQL_PLUGIN_CLIENT_BIN_DIR) MYSQL_PLUGIN_CLIENT_BIN_DIR="$value" ;;
MYSQL_PLUGIN_REMOTE_BIND_ADDRESS) MYSQL_PLUGIN_REMOTE_BIND_ADDRESS="$value" ;;
MYSQL_PLUGIN_CSF_ALLOW_FILE) MYSQL_HOST_SYNC_CSF_ALLOW="$value" ;;
MYSQL_PLUGIN_CSF_CONF_FILE) MYSQL_HOST_SYNC_CSF_CONF="$value" ;;
MYSQL_PLUGIN_CSF_MANAGED_ALLOW_FILE) MYSQL_HOST_SYNC_MANAGED_ALLOW="$value" ;;
MYSQL_PLUGIN_ALT_MARIADB_CNF) MYSQL_HOST_SYNC_INSTANCE_CNF="$value" ;;
MYSQL_PLUGIN_ALT_MARIADB_SERVICE) MYSQL_HOST_SYNC_SERVICE="$value" ;;
esac
done < "$file"
}
require_root_or_sudo() {
if [ "${MYSQL_HOST_SYNC_ASSUME_ROOT:-0}" = "1" ]; then
return 0
fi
if [ "${EUID:-$(id -u)}" -eq 0 ]; then
return 0
fi
if command -v sudo >/dev/null 2>&1; then
exec sudo -n "$0" "$@"
fi
echo "mysql: synchronizacja hostów wymaga uprawnień root." >&2
exit 1
}
is_ipv4() {
local ip="${1:-}"
local IFS=.
local -a octets
[[ "$ip" =~ ^[0-9]+(\.[0-9]+){3}$ ]] || return 1
read -r -a octets <<< "$ip"
[ "${#octets[@]}" -eq 4 ] || return 1
local octet
for octet in "${octets[@]}"; do
[[ "$octet" =~ ^[0-9]+$ ]] || return 1
[ "$octet" -ge 0 ] && [ "$octet" -le 255 ] || return 1
done
}
is_local_host() {
case "$(printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]')" in
""|localhost|127.0.0.1|::1) return 0 ;;
*) return 1 ;;
esac
}
collect_remote_hosts() {
local sql output host
sql="SELECT DISTINCT host_pattern FROM da_plugin_access_hosts WHERE host_pattern <> '' ORDER BY host_pattern"
if ! output="$(mysql_exec mysql --batch --skip-column-names --raw -e "$sql")"; then
echo "mysql: nie można odczytać listy hostów zdalnych z da_plugin_access_hosts." >&2
return 1
fi
while IFS= read -r host || [ -n "$host" ]; do
host="$(trim_value "$host")"
[ -n "$host" ] || continue
is_local_host "$host" && continue
is_ipv4 "$host" || continue
printf '%s\n' "$host"
done <<< "$output" | sort -u
}
write_managed_allow_file() {
local target="$1"
local port="$2"
local tmp
shift 2
local -a hosts=("$@")
install -d -m 0755 "$(dirname "$target")"
tmp="$(mktemp)"
{
echo "# Managed by DirectAdmin MySQL plugin. Do not edit manually."
echo "# Remote MariaDB access for port $port."
echo "# Source: da_plugin_access_hosts in the plugin metadata schema."
local host
for host in "${hosts[@]}"; do
echo "tcp|in|d=$port|s=$host"
done
} > "$tmp"
if [ ! -f "$target" ] || ! cmp -s "$tmp" "$target"; then
if [ -f "$target" ]; then
cat "$tmp" > "$target"
chmod 0644 "$target"
else
install -m 0644 "$tmp" "$target"
fi
changed_csf=1
fi
rm -f "$tmp"
}
ensure_csf_include() {
local csf_allow="$1"
local managed_allow="$2"
local include_line="Include $managed_allow"
[ -f "$csf_allow" ] || {
echo "mysql: brak pliku CSF allow: $csf_allow" >&2
return 1
}
if grep -Fxq "$include_line" "$csf_allow"; then
return 0
fi
printf '\n%s\n' "$include_line" >> "$csf_allow"
changed_csf=1
}
remove_port_from_csv() {
local csv="$1"
local port="$2"
local old_ifs="$IFS"
local item out="" start end left_start left_end right_start right_end
IFS=','
for item in $csv; do
item="$(trim_value "$item")"
[ -n "$item" ] || continue
if [ "$item" = "$port" ]; then
continue
fi
if [[ "$item" =~ ^([0-9]+):([0-9]+)$ ]]; then
start="${BASH_REMATCH[1]}"
end="${BASH_REMATCH[2]}"
if [ "$start" -le "$port" ] && [ "$port" -le "$end" ]; then
left_start="$start"
left_end=$((port - 1))
right_start=$((port + 1))
right_end="$end"
if [ "$left_start" -le "$left_end" ]; then
if [ -z "$out" ]; then
out="$left_start"
else
out="$out,$left_start"
fi
if [ "$left_start" -ne "$left_end" ]; then
out="$out:$left_end"
fi
fi
if [ "$right_start" -le "$right_end" ]; then
if [ -z "$out" ]; then
out="$right_start"
else
out="$out,$right_start"
fi
if [ "$right_start" -ne "$right_end" ]; then
out="$out:$right_end"
fi
fi
continue
fi
fi
if [ -z "$out" ]; then
out="$item"
else
out="$out,$item"
fi
done
IFS="$old_ifs"
printf '%s' "$out"
}
remove_port_from_global_csf_lists() {
local csf_conf="$1"
local port="$2"
local tmp changed=0 line new_list new_line
[ -f "$csf_conf" ] || {
echo "mysql: brak pliku CSF config: $csf_conf" >&2
return 1
}
tmp="$(mktemp)"
while IFS= read -r line || [ -n "$line" ]; do
if [[ "$line" =~ ^([[:space:]]*)(TCP6?_IN)([[:space:]]*=[[:space:]]*)\"([^\"]*)\"(.*)$ ]]; then
new_list="$(remove_port_from_csv "${BASH_REMATCH[4]}" "$port")"
new_line="${BASH_REMATCH[1]}${BASH_REMATCH[2]}${BASH_REMATCH[3]}\"$new_list\"${BASH_REMATCH[5]}"
if [ "$new_line" != "$line" ]; then
changed=1
fi
printf '%s\n' "$new_line" >> "$tmp"
else
printf '%s\n' "$line" >> "$tmp"
fi
done < "$csf_conf"
if [ "$changed" -eq 1 ]; then
cat "$tmp" > "$csf_conf"
chmod 0600 "$csf_conf"
changed_csf=1
fi
rm -f "$tmp"
}
ensure_remote_bind_address() {
local cnf="$1"
local desired="$2"
local tmp changed=0 inserted=0 in_mariadb=0 line new_line
[ -f "$cnf" ] || {
echo "mysql: brak pliku konfiguracji MariaDB: $cnf" >&2
return 1
}
is_ipv4 "$desired" || {
echo "mysql: nieprawidłowy bind-address dla zdalnego MySQL: $desired" >&2
return 1
}
tmp="$(mktemp)"
while IFS= read -r line || [ -n "$line" ]; do
if [[ "$line" =~ ^[[:space:]]*\[mariadb\][[:space:]]*$ ]]; then
in_mariadb=1
printf '%s\n' "$line" >> "$tmp"
continue
fi
if [ "$in_mariadb" -eq 1 ] && [[ "$line" =~ ^[[:space:]]*\[.*\][[:space:]]*$ ]]; then
if [ "$inserted" -eq 0 ]; then
printf 'bind-address=%s\n' "$desired" >> "$tmp"
inserted=1
changed=1
fi
in_mariadb=0
fi
if [ "$in_mariadb" -eq 1 ] && [[ "$line" =~ ^[[:space:]]*bind-address[[:space:]]*= ]]; then
new_line="bind-address=$desired"
if [ "$line" != "$new_line" ]; then
changed=1
fi
printf '%s\n' "$new_line" >> "$tmp"
inserted=1
continue
fi
printf '%s\n' "$line" >> "$tmp"
done < "$cnf"
if [ "$inserted" -eq 0 ]; then
printf '\n[mariadb]\nbind-address=%s\n' "$desired" >> "$tmp"
changed=1
fi
if [ "$changed" -eq 1 ]; then
cat "$tmp" > "$cnf"
chmod 0640 "$cnf"
changed_bind=1
fi
rm -f "$tmp"
}
reload_csf() {
if [ "${MYSQL_HOST_SYNC_SKIP_CSF_RELOAD:-0}" = "1" ]; then
return 0
fi
if command -v csf >/dev/null 2>&1; then
csf -r
return 0
fi
echo "mysql: nie znaleziono polecenia csf, nie można przeładować firewalla." >&2
return 1
}
restart_mariadb_service() {
local service="$1"
if [ "${MYSQL_HOST_SYNC_SKIP_SERVICE_RESTART:-0}" = "1" ]; then
return 0
fi
if command -v systemctl >/dev/null 2>&1; then
systemctl restart "$service"
return 0
fi
if command -v service >/dev/null 2>&1; then
service "$service" restart
return 0
fi
echo "mysql: nie znaleziono systemctl/service, nie można zrestartować $service." >&2
return 1
}
main() {
require_root_or_sudo "$@"
load_plugin_settings "$PLUGIN_SETTINGS_FILE"
mysql_load_alt_credentials
mysql_ensure_metadata_schema
local csf_dir="${MYSQL_HOST_SYNC_CSF_DIR:-/etc/csf}"
local csf_allow="${MYSQL_HOST_SYNC_CSF_ALLOW:-$csf_dir/csf.allow}"
local csf_conf="${MYSQL_HOST_SYNC_CSF_CONF:-$csf_dir/csf.conf}"
local managed_allow="${MYSQL_HOST_SYNC_MANAGED_ALLOW:-$csf_dir/alt-mysql-plugin.allow}"
local instance_cnf="${MYSQL_HOST_SYNC_INSTANCE_CNF:-$MYSQL_ALT_INSTANCE_CNF}"
local bind_address="${MYSQL_REMOTE_BIND_ADDRESS:-${MYSQL_PLUGIN_REMOTE_BIND_ADDRESS:-0.0.0.0}}"
local service="${MYSQL_HOST_SYNC_SERVICE:-$MYSQL_ALT_SERVICE_NAME}"
local changed_csf=0 changed_bind=0 host_output
local -a hosts=()
host_output="$(collect_remote_hosts)"
while IFS= read -r host || [ -n "$host" ]; do
[ -n "$host" ] || continue
hosts+=("$host")
done <<< "$host_output"
write_managed_allow_file "$managed_allow" "$MYSQL_PORT" "${hosts[@]}"
ensure_csf_include "$csf_allow" "$managed_allow"
remove_port_from_global_csf_lists "$csf_conf" "$MYSQL_PORT"
ensure_remote_bind_address "$instance_cnf" "$bind_address"
if [ "$changed_bind" -eq 1 ]; then
restart_mariadb_service "$service"
fi
if [ "$changed_csf" -eq 1 ]; then
reload_csf
fi
cat <<EOF
mysql: zsynchronizowano zdalne hosty MariaDB.
Port: $MYSQL_PORT
Bind address: $bind_address
Plik CSF include: $managed_allow
Liczba hostów: ${#hosts[@]}
EOF
}
main "$@"
+365
View File
@@ -0,0 +1,365 @@
#!/bin/bash
set -euo pipefail
# Upgrade alternatywnej instancji MariaDB instalowanej z oficjalnego tarballa.
TARGET_MARIADB_VERSION="${TARGET_MARIADB_VERSION:-11.8}"
MARIADB_DIR="${MARIADB_DIR:-/usr/local/mariadb-$TARGET_MARIADB_VERSION}"
DATADIR="${DATADIR:-/var/lib/mariadb_data}"
SERVICE_NAME="${SERVICE_NAME:-alt-mariadb}"
INSTANCE_CNF="${INSTANCE_CNF:-/etc/alt-mariadb.cnf}"
ALT_MY_CNF="${ALT_MY_CNF:-/usr/local/directadmin/conf/alt-my.cnf}"
ALT_MYSQL_CONF="${ALT_MYSQL_CONF:-/usr/local/directadmin/conf/alt-mysql.conf}"
ALT_METADATA_ENV="${ALT_METADATA_ENV:-/usr/local/directadmin/conf/alt-mariadb.env}"
DOWNLOAD_DIR="${DOWNLOAD_DIR:-/usr/local/src}"
MARIADB_TARBALL_URL="${MARIADB_TARBALL_URL:-}"
FORCE_UPGRADE="${FORCE_UPGRADE:-no}"
PORT="${PORT:-}"
SOCKET="${SOCKET:-}"
CURRENT_MARIADB_DIR=""
RESOLVED_MARIADB_RELEASE=""
DOWNLOAD_URL=""
TARBALL_PATH=""
TMP_DIR=""
TARGET_BACKUP_DIR=""
INSTANCE_CNF_BACKUP=""
UPGRADE_EXTRACTED=0
UPGRADE_COMMITTED=0
SERVICE_WAS_ACTIVE=0
log() {
printf '[INFO] %s\n' "$*"
}
warn() {
printf '[UWAGA] %s\n' "$*" >&2
}
die() {
printf '[BŁĄD] %s\n' "$*" >&2
exit 1
}
cleanup() {
if [ -n "${TMP_DIR:-}" ] && [ -d "$TMP_DIR" ]; then
rm -rf "$TMP_DIR"
fi
}
trap cleanup EXIT
rollback_upgrade() {
local code="$1"
if [ "$code" -eq 0 ] || [ "$UPGRADE_COMMITTED" -eq 1 ]; then
cleanup
return 0
fi
warn "Upgrade nie zakończył się poprawnie, uruchamiam rollback."
if [ -n "$INSTANCE_CNF_BACKUP" ] && [ -r "$INSTANCE_CNF_BACKUP" ]; then
cp -a "$INSTANCE_CNF_BACKUP" "$INSTANCE_CNF" || true
warn "RESTORED_INSTANCE_CNF: $INSTANCE_CNF"
fi
if [ "$UPGRADE_EXTRACTED" -eq 1 ] && [ -d "$MARIADB_DIR" ]; then
if [ -n "$TARGET_BACKUP_DIR" ] && [ -d "$TARGET_BACKUP_DIR" ]; then
rm -rf "$MARIADB_DIR" || true
elif [ "$MARIADB_DIR" != "$CURRENT_MARIADB_DIR" ]; then
rm -rf "$MARIADB_DIR" || true
fi
fi
if [ -n "$TARGET_BACKUP_DIR" ] && [ -d "$TARGET_BACKUP_DIR" ] && [ ! -e "$MARIADB_DIR" ]; then
mv "$TARGET_BACKUP_DIR" "$MARIADB_DIR" || true
warn "RESTORED_PREVIOUS_DIR: $MARIADB_DIR"
fi
if [ "$SERVICE_WAS_ACTIVE" -eq 1 ]; then
if command -v systemctl >/dev/null 2>&1; then
systemctl daemon-reload || true
systemctl restart "$SERVICE_NAME" || true
fi
fi
cleanup
}
trap 'code=$?; rollback_upgrade "$code"' EXIT
require_root() {
if [ "${EUID:-$(id -u)}" -ne 0 ]; then
die "Ten skrypt musi być uruchomiony jako root."
fi
}
validate_version() {
case "$TARGET_MARIADB_VERSION" in
10.11|11.4|11.8)
log "Wybrano docelową wersję MariaDB: $TARGET_MARIADB_VERSION."
;;
*)
die "Nieobsługiwana wersja MariaDB: $TARGET_MARIADB_VERSION. Dozwolone wartości: 10.11, 11.4, 11.8."
;;
esac
}
detect_architecture() {
case "$(uname -m)" in
x86_64|amd64) printf 'x86_64\n' ;;
*) die "Nieobsługiwana architektura: $(uname -m). Skrypt obsługuje automatycznie tylko x86_64/amd64." ;;
esac
}
detect_current_mariadb_dir() {
if [ -r "$INSTANCE_CNF" ]; then
CURRENT_MARIADB_DIR="$(awk -F= '
/^[[:space:]]*basedir[[:space:]]*=/ {
value=$2
gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
print value
exit
}
' "$INSTANCE_CNF")"
DATADIR="$(awk -F= '
/^[[:space:]]*datadir[[:space:]]*=/ {
value=$2
gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
print value
exit
}
' "$INSTANCE_CNF")"
PORT="$(awk -F= '
/^[[:space:]]*port[[:space:]]*=/ {
value=$2
gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
print value
exit
}
' "$INSTANCE_CNF")"
SOCKET="$(awk -F= '
/^[[:space:]]*socket[[:space:]]*=/ {
value=$2
gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
print value
exit
}
' "$INSTANCE_CNF")"
fi
if [ -z "$CURRENT_MARIADB_DIR" ] && systemctl cat "$SERVICE_NAME" >/dev/null 2>&1; then
CURRENT_MARIADB_DIR="$(systemctl cat "$SERVICE_NAME" \
| awk '/ExecStart=/ {
sub(/^.*ExecStart=/, "")
sub(/\/bin\/mariadbd.*$/, "")
print
exit
}')"
fi
[ -n "$CURRENT_MARIADB_DIR" ] || die "Nie udało się wykryć bieżącego katalogu MariaDB."
[ -x "$CURRENT_MARIADB_DIR/bin/mariadbd" ] || die "Bieżący katalog MariaDB nie zawiera bin/mariadbd: $CURRENT_MARIADB_DIR."
[ -n "$DATADIR" ] || die "Nie udało się wykryć katalogu danych z $INSTANCE_CNF."
log "Wykryto bieżący katalog MariaDB: $CURRENT_MARIADB_DIR."
}
resolve_latest_release_from_archive() {
local series="$1"
local listing=""
local release=""
listing="$(curl -fsSL "https://archive.mariadb.org/" || true)"
release="$(printf '%s\n' "$listing" \
| grep -Eo "mariadb-${series//./\\.}\.[0-9]+/" \
| sed 's#^mariadb-##; s#/$##' \
| sort -V \
| tail -n 1)"
[ -n "$release" ] || return 1
printf '%s\n' "$release"
}
resolve_tarball_from_directory() {
local release="$1"
local arch="$2"
local dir_url="https://archive.mariadb.org/mariadb-${release}/bintar-linux-systemd-${arch}/"
local listing=""
local file=""
listing="$(curl -fsSL "$dir_url" || true)"
file="$(printf '%s\n' "$listing" \
| grep -Eo "mariadb-${release}-linux-systemd-${arch}\.tar\.(gz|xz)" \
| head -n 1)"
[ -n "$file" ] || return 1
printf '%s%s\n' "$dir_url" "$file"
}
resolve_download_url() {
local arch=""
arch="$(detect_architecture)"
if [ -n "$MARIADB_TARBALL_URL" ]; then
DOWNLOAD_URL="$MARIADB_TARBALL_URL"
log "Używam ręcznie ustawionego adresu tarballa MariaDB."
return 0
fi
RESOLVED_MARIADB_RELEASE="$(resolve_latest_release_from_archive "$TARGET_MARIADB_VERSION" || true)"
[ -n "$RESOLVED_MARIADB_RELEASE" ] || die "Nie udało się wykryć najnowszego wydania dla gałęzi $TARGET_MARIADB_VERSION."
DOWNLOAD_URL="$(resolve_tarball_from_directory "$RESOLVED_MARIADB_RELEASE" "$arch" || true)"
[ -n "$DOWNLOAD_URL" ] || die "Nie udało się automatycznie ustalić URL tarballa. Ustaw MARIADB_TARBALL_URL."
log "Wykryto tarball MariaDB $RESOLVED_MARIADB_RELEASE."
}
download_tarball() {
local filename=""
install -d -m 0755 "$DOWNLOAD_DIR"
filename="$(basename "${DOWNLOAD_URL%%\?*}")"
TARBALL_PATH="$DOWNLOAD_DIR/$filename"
log "Pobieram tarball MariaDB do $TARBALL_PATH."
curl -fL --retry 3 --connect-timeout 20 -o "$TARBALL_PATH" "$DOWNLOAD_URL"
[ -s "$TARBALL_PATH" ] || die "Pobrany tarball jest pusty albo nie istnieje: $TARBALL_PATH."
}
extract_tarball() {
local extracted_dir=""
local backup_dir=""
if [ -d "$MARIADB_DIR" ] && [ -n "$(find "$MARIADB_DIR" -mindepth 1 -maxdepth 1 -print -quit)" ]; then
if [ "$FORCE_UPGRADE" != "yes" ]; then
die "Katalog docelowy $MARIADB_DIR już istnieje. Ustaw FORCE_UPGRADE=yes, aby zastąpić go po kopii."
fi
TARGET_BACKUP_DIR="${MARIADB_DIR}.pre-upgrade.$(date +%Y%m%d%H%M%S)"
log "Tworzę kopię istniejącego katalogu docelowego: $TARGET_BACKUP_DIR."
mv "$MARIADB_DIR" "$TARGET_BACKUP_DIR"
fi
TMP_DIR="$(mktemp -d)"
tar -xf "$TARBALL_PATH" -C "$TMP_DIR"
extracted_dir="$(find "$TMP_DIR" -mindepth 1 -maxdepth 1 -type d -print -quit)"
[ -n "$extracted_dir" ] || die "Tarball nie zawiera oczekiwanego katalogu MariaDB."
mv "$extracted_dir" "$MARIADB_DIR"
chown -R root:root "$MARIADB_DIR"
UPGRADE_EXTRACTED=1
log "Nowe binaria MariaDB są w $MARIADB_DIR."
}
rewrite_instance_basedir() {
local backup=""
[ -r "$INSTANCE_CNF" ] || die "Brak konfiguracji instancji: $INSTANCE_CNF."
backup="${INSTANCE_CNF}.pre-upgrade.$(date +%Y%m%d%H%M%S)"
cp -a "$INSTANCE_CNF" "$backup"
INSTANCE_CNF_BACKUP="$backup"
sed -i "s#^[[:space:]]*basedir[[:space:]]*=.*#basedir=$MARIADB_DIR#" "$INSTANCE_CNF"
sed -i "s#^[[:space:]]*plugin-dir[[:space:]]*=.*#plugin-dir=$MARIADB_DIR/lib/plugin#" "$INSTANCE_CNF"
log "Zaktualizowano basedir i plugin-dir w $INSTANCE_CNF. Kopia: $backup."
}
restart_service() {
log "Restartuję usługę $SERVICE_NAME."
systemctl daemon-reload
systemctl restart "$SERVICE_NAME"
sleep 5
systemctl is-active --quiet "$SERVICE_NAME" || {
systemctl status "$SERVICE_NAME" --no-pager || true
die "Usługa $SERVICE_NAME nie uruchomiła się po upgrade."
}
}
run_mariadb_upgrade() {
local upgrade_bin=""
if [ -x "$MARIADB_DIR/bin/mariadb-upgrade" ]; then
upgrade_bin="$MARIADB_DIR/bin/mariadb-upgrade"
elif [ -x "$MARIADB_DIR/bin/mysql_upgrade" ]; then
upgrade_bin="$MARIADB_DIR/bin/mysql_upgrade"
else
warn "Nie znaleziono mariadb-upgrade ani mysql_upgrade, pomijam etap aktualizacji tabel systemowych."
return 0
fi
[ -r "$ALT_MY_CNF" ] || die "Brak pliku klienta alternatywnej instancji: $ALT_MY_CNF."
log "Uruchamiam aktualizację tabel systemowych MariaDB."
"$upgrade_bin" --defaults-extra-file="$ALT_MY_CNF"
}
credential_group() {
if getent group diradmin >/dev/null 2>&1; then
printf 'diradmin\n'
else
printf 'root\n'
fi
}
sh_single_quote() {
local value="$1"
value="${value//\'/\'\\\'\'}"
printf "'%s'" "$value"
}
write_alt_metadata_env() {
local group_name metadata_mode tmp_file
group_name="$(credential_group)"
metadata_mode="0600"
if [ "$group_name" = "diradmin" ]; then
metadata_mode="0640"
fi
tmp_file="$(mktemp)"
cat > "$tmp_file" <<EOF
# Metadata alternatywnej instancji MariaDB zarządzanej przez mysql_upgrade.sh.
# Ten plik jest kontraktem dla pluginu DirectAdmin i nie jest natywną konfiguracją MySQL DirectAdmina.
SERVICE_NAME=$(sh_single_quote "$SERVICE_NAME")
MARIADB_VERSION=$(sh_single_quote "$TARGET_MARIADB_VERSION")
MARIADB_RELEASE=$(sh_single_quote "${RESOLVED_MARIADB_RELEASE:-$TARGET_MARIADB_VERSION}")
MARIADB_DIR=$(sh_single_quote "$MARIADB_DIR")
DATADIR=$(sh_single_quote "$DATADIR")
PORT=$(sh_single_quote "$PORT")
SOCKET=$(sh_single_quote "$SOCKET")
ALT_MYSQL_CONF=$(sh_single_quote "$ALT_MYSQL_CONF")
ALT_MY_CNF=$(sh_single_quote "$ALT_MY_CNF")
INSTANCE_CNF=$(sh_single_quote "$INSTANCE_CNF")
EOF
install -m "$metadata_mode" -o root -g "$group_name" "$tmp_file" "$ALT_METADATA_ENV"
rm -f "$tmp_file"
log "Zaktualizowano metadane alternatywnej instancji: $ALT_METADATA_ENV."
}
print_summary() {
cat <<EOF
Upgrade alternatywnej instancji MariaDB zakończony.
Poprzedni katalog programu: $CURRENT_MARIADB_DIR
Nowy katalog programu: $MARIADB_DIR
Katalog danych: $DATADIR
Konfiguracja instancji: $INSTANCE_CNF
Usługa systemd: $SERVICE_NAME
Polecenie testowe:
$MARIADB_DIR/bin/mariadb --defaults-extra-file=$ALT_MY_CNF -e "SELECT VERSION();"
EOF
}
main() {
require_root
validate_version
detect_current_mariadb_dir
resolve_download_url
download_tarball
if systemctl is-active --quiet "$SERVICE_NAME"; then
SERVICE_WAS_ACTIVE=1
fi
systemctl stop "$SERVICE_NAME"
extract_tarball
rewrite_instance_basedir
restart_service
run_mariadb_upgrade
write_alt_metadata_env
UPGRADE_COMMITTED=1
print_summary
}
main "$@"
+42
View File
@@ -0,0 +1,42 @@
#!/bin/bash
set -euo pipefail
PHPMYADMIN_DIR="${PHPMYADMIN_PRIVATE_DIR:-${PHPMYADMIN_DIR:-/var/www/html/alt-mysql-phpmyadmin}}"
CONFIG_INC="${PHPMYADMIN_DIR}/config.inc.php"
SIGNON_SCRIPT="${PHPMYADMIN_DIR}/da_signon.php"
SIGNON_URL_PAGE="${PHPMYADMIN_DIR}/da_login.php"
RUNTIME_CONFIG="${PHPMYADMIN_DIR}/runtime/config.json"
fail() {
printf 'phpMyAdmin alt-mysql health: %s\n' "$*" >&2
exit 1
}
[ -d "$PHPMYADMIN_DIR" ] || fail "phpMyAdmin directory not found: $PHPMYADMIN_DIR"
[ -r "$PHPMYADMIN_DIR/index.php" ] || fail "phpMyAdmin index.php is not readable: $PHPMYADMIN_DIR/index.php"
[ -r "$CONFIG_INC" ] || fail "config.inc.php is not readable: $CONFIG_INC"
[ -r "$SIGNON_SCRIPT" ] || fail "signon script is not readable: $SIGNON_SCRIPT"
[ -r "$SIGNON_URL_PAGE" ] || fail "SignonURL page is not readable: $SIGNON_URL_PAGE"
grep -Fq "DA_MYSQL_PMA_DEFAULT_CONF" "$CONFIG_INC" || fail "config.inc.php does not configure alt-mysql endpoint"
grep -Fq "auth_type'] = 'signon" "$CONFIG_INC" || fail "config.inc.php does not configure signon auth"
grep -Fq "SignonURL'] = da_mysql_phpmyadmin_signon_url" "$CONFIG_INC" || fail "config.inc.php does not configure dynamic SignonURL"
grep -Fq "da_mysql_login_consume_ticket" "$SIGNON_URL_PAGE" || fail "da_login.php does not consume SSO tickets"
grep -Fq "DA_MYSQL_DEFAULT_CONF = '/usr/local/directadmin/conf/alt-mysql.conf'" "$SIGNON_SCRIPT" \
|| fail "signon script does not default to alt-mysql.conf"
if [ -r "$RUNTIME_CONFIG" ] && command -v php >/dev/null 2>&1; then
php -r '
$path = $argv[1];
$data = json_decode(file_get_contents($path), true);
if (!is_array($data)) {
fwrite(STDERR, "runtime config is not valid JSON\n");
exit(2);
}
if (($data["credentials_file"] ?? "") === "/usr/local/directadmin/conf/mysql.conf") {
fwrite(STDERR, "runtime config points to native DirectAdmin mysql.conf\n");
exit(3);
}
' "$RUNTIME_CONFIG" || fail "runtime config validation failed"
fi
echo "phpMyAdmin alt-mysql health: OK"
+486
View File
@@ -0,0 +1,486 @@
#!/bin/bash
set -euo pipefail
PHPMYADMIN_PRIVATE_DIR="${PHPMYADMIN_PRIVATE_DIR:-${PHPMYADMIN_DIR:-/var/www/html/alt-mysql-phpmyadmin}}"
PHPMYADMIN_VERSION="${PHPMYADMIN_VERSION:-5.2.3}"
PHPMYADMIN_TARBALL_URL="${PHPMYADMIN_TARBALL_URL:-https://files.phpmyadmin.net/phpMyAdmin/5.2.3/phpMyAdmin-5.2.3-all-languages.tar.gz}"
PHPMYADMIN_DOWNLOAD_DIR="${PHPMYADMIN_DOWNLOAD_DIR:-/usr/local/src}"
PHPMYADMIN_SOURCE_DIR="${PHPMYADMIN_SOURCE_DIR:-}"
RUNTIME_DIR="${PHPMYADMIN_PRIVATE_DIR}/runtime"
TICKETS_DIR="${RUNTIME_DIR}/tickets"
CONFIG_INC="${PHPMYADMIN_PRIVATE_DIR}/config.inc.php"
SIGNON_SCRIPT="${PHPMYADMIN_PRIVATE_DIR}/da_signon.php"
SIGNON_URL_PAGE="${PHPMYADMIN_PRIVATE_DIR}/da_login.php"
BLOWFISH_SECRET_FILE="${RUNTIME_DIR}/blowfish_secret"
if [ "${PHPMYADMIN_INSTALL_ASSUME_ROOT:-0}" != "1" ] && [ "${EUID:-$(id -u)}" -ne 0 ]; then
echo "Skrypt musi być uruchomiony jako root." >&2
exit 1
fi
log() {
printf 'phpMyAdmin alt-mysql: %s\n' "$*"
}
die() {
printf 'phpMyAdmin alt-mysql: %s\n' "$*" >&2
exit 1
}
install_private_copy_from_source() {
local source_dir="$1"
[ -d "$source_dir" ] || die "Katalog źródłowy phpMyAdmin nie istnieje: $source_dir"
[ -f "$source_dir/index.php" ] || die "Katalog źródłowy nie wygląda jak phpMyAdmin: $source_dir"
mkdir -p "$PHPMYADMIN_PRIVATE_DIR"
cp -a "$source_dir"/. "$PHPMYADMIN_PRIVATE_DIR"/
}
download_private_copy() {
local tarball extracted tmp_dir
command -v curl >/dev/null 2>&1 || die "Brak curl; ustaw PHPMYADMIN_SOURCE_DIR albo doinstaluj curl."
command -v tar >/dev/null 2>&1 || die "Brak tar."
mkdir -p "$PHPMYADMIN_DOWNLOAD_DIR"
tarball="$PHPMYADMIN_DOWNLOAD_DIR/phpMyAdmin-${PHPMYADMIN_VERSION}-all-languages.tar.gz"
if [ ! -s "$tarball" ]; then
log "Pobieram prywatny phpMyAdmin ${PHPMYADMIN_VERSION}."
curl -fL --retry 3 --connect-timeout 20 -o "$tarball" "$PHPMYADMIN_TARBALL_URL"
fi
tmp_dir="$(mktemp -d)"
tar -xzf "$tarball" -C "$tmp_dir"
extracted="$(find "$tmp_dir" -mindepth 1 -maxdepth 1 -type d -name 'phpMyAdmin-*' -print -quit)"
[ -n "$extracted" ] || {
rm -rf "$tmp_dir"
die "Tarball phpMyAdmin nie zawiera oczekiwanego katalogu."
}
install_private_copy_from_source "$extracted"
rm -rf "$tmp_dir"
}
ensure_private_copy() {
if [ -f "$PHPMYADMIN_PRIVATE_DIR/index.php" ]; then
return 0
fi
rm -rf "$PHPMYADMIN_PRIVATE_DIR"
if [ -n "$PHPMYADMIN_SOURCE_DIR" ]; then
install_private_copy_from_source "$PHPMYADMIN_SOURCE_DIR"
else
download_private_copy
fi
}
generate_secret() {
if [ -r "$BLOWFISH_SECRET_FILE" ]; then
local existing
existing="$(tr -d '\r\n' < "$BLOWFISH_SECRET_FILE")"
if [ -n "$existing" ]; then
printf '%s\n' "$existing"
return 0
fi
fi
if command -v openssl >/dev/null 2>&1; then
openssl rand -hex 32
return 0
fi
tr -dc 'a-f0-9' </dev/urandom | head -c 64
printf '\n'
}
write_private_config() {
local secret
mkdir -p "$RUNTIME_DIR" "$TICKETS_DIR"
secret="$(generate_secret)"
printf '%s\n' "$secret" > "$BLOWFISH_SECRET_FILE"
chmod 600 "$BLOWFISH_SECRET_FILE" 2>/dev/null || true
cat > "$RUNTIME_DIR/.htaccess" <<'EOF'
Require all denied
Deny from all
EOF
cat > "$TICKETS_DIR/.htaccess" <<'EOF'
Require all denied
Deny from all
EOF
printf '%s\n' '<!doctype html><title>Forbidden</title>' > "$RUNTIME_DIR/index.html"
printf '%s\n' '<!doctype html><title>Forbidden</title>' > "$TICKETS_DIR/index.html"
cat > "$CONFIG_INC" <<'PHP'
<?php
declare(strict_types=1);
const DA_MYSQL_PMA_DEFAULT_CONF = '/usr/local/directadmin/conf/alt-mysql.conf';
const DA_MYSQL_PMA_RUNTIME_CONFIG = __DIR__ . '/runtime/config.json';
const DA_MYSQL_PMA_BLOWFISH_SECRET_FILE = __DIR__ . '/runtime/blowfish_secret';
function da_mysql_phpmyadmin_runtime_config(): array
{
if (!is_readable(DA_MYSQL_PMA_RUNTIME_CONFIG)) {
return [];
}
$raw = file_get_contents(DA_MYSQL_PMA_RUNTIME_CONFIG);
if (!is_string($raw) || $raw === '') {
return [];
}
$decoded = json_decode($raw, true);
return is_array($decoded) ? $decoded : [];
}
function da_mysql_phpmyadmin_parse_conf(string $path): array
{
$data = [
'host' => '127.0.0.1',
'port' => '33033',
'socket' => '',
];
if (!is_readable($path)) {
return $data;
}
$lines = file($path, FILE_IGNORE_NEW_LINES);
if ($lines === false) {
return $data;
}
foreach ($lines as $line) {
$line = trim($line);
if ($line === '' || $line[0] === '#' || strpos($line, '=') === false) {
continue;
}
[$key, $value] = explode('=', $line, 2);
$key = strtolower(trim($key));
$value = trim(trim($value), "\"'");
if ($key === 'host' && $value !== '') {
$data['host'] = $value;
} elseif (($key === 'port' || $key === 'mysql_port') && ctype_digit($value)) {
$data['port'] = $value;
} elseif (($key === 'socket' || $key === 'mysql_socket') && $value !== '') {
$data['socket'] = $value;
}
}
if ($data['host'] === '' || $data['host'] === 'localhost') {
$data['host'] = '127.0.0.1';
}
return $data;
}
function da_mysql_phpmyadmin_blowfish_secret(): string
{
if (is_readable(DA_MYSQL_PMA_BLOWFISH_SECRET_FILE)) {
$secret = trim((string)file_get_contents(DA_MYSQL_PMA_BLOWFISH_SECRET_FILE));
if ($secret !== '') {
return $secret;
}
}
return 'alt-mysql-phpmyadmin-secret-change-me';
}
function da_mysql_phpmyadmin_url_path(array $runtimeConfig): string
{
$path = trim((string)($runtimeConfig['phpmyadmin_url_path'] ?? '/alt-mysql-phpmyadmin'));
if ($path === '') {
return '/alt-mysql-phpmyadmin';
}
if ($path[0] !== '/') {
$path = '/' . $path;
}
$path = rtrim($path, '/');
return $path !== '' ? $path : '/alt-mysql-phpmyadmin';
}
function da_mysql_phpmyadmin_public_base_url(array $runtimeConfig): string
{
$configured = trim((string)($runtimeConfig['phpmyadmin_public_base_url'] ?? ''));
if (preg_match('#^https?://#i', $configured) === 1) {
return rtrim($configured, '/');
}
$scheme = 'http';
$https = strtolower((string)($_SERVER['HTTPS'] ?? ''));
if ($https !== '' && $https !== 'off' && $https !== '0') {
$scheme = 'https';
} elseif (strtolower((string)($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '')) === 'https') {
$scheme = 'https';
} elseif (strtolower((string)($_SERVER['REQUEST_SCHEME'] ?? '')) === 'https') {
$scheme = 'https';
}
$host = trim((string)($_SERVER['HTTP_HOST'] ?? ''));
if ($host === '') {
$host = trim((string)($_SERVER['SERVER_NAME'] ?? 'localhost'));
}
if ($host === '') {
$host = 'localhost';
}
return $scheme . '://' . $host;
}
function da_mysql_phpmyadmin_signon_url(array $runtimeConfig): string
{
return da_mysql_phpmyadmin_public_base_url($runtimeConfig)
. da_mysql_phpmyadmin_url_path($runtimeConfig)
. '/da_login.php';
}
$runtimeConfig = da_mysql_phpmyadmin_runtime_config();
$credentialsFile = (string)($runtimeConfig['credentials_file'] ?? DA_MYSQL_PMA_DEFAULT_CONF);
if ($credentialsFile === '') {
$credentialsFile = DA_MYSQL_PMA_DEFAULT_CONF;
}
$daMysqlServer = da_mysql_phpmyadmin_parse_conf($credentialsFile);
$cfg = [];
$cfg['blowfish_secret'] = da_mysql_phpmyadmin_blowfish_secret();
$cfg['TempDir'] = __DIR__ . '/runtime/tmp';
$cfg['LoginCookieValidity'] = 900;
$i = 0;
$i++;
$cfg['Servers'][$i]['host'] = $daMysqlServer['host'];
$cfg['Servers'][$i]['port'] = $daMysqlServer['port'];
$cfg['Servers'][$i]['socket'] = $daMysqlServer['host'] === 'localhost' ? $daMysqlServer['socket'] : '';
$cfg['Servers'][$i]['auth_type'] = 'signon';
$cfg['Servers'][$i]['SignonScript'] = __DIR__ . '/da_signon.php';
$cfg['Servers'][$i]['SignonSession'] = 'DA_MYSQL_PHPMYADMIN';
$cfg['Servers'][$i]['SignonURL'] = da_mysql_phpmyadmin_signon_url($runtimeConfig);
$cfg['Servers'][$i]['LogoutURL'] = da_mysql_phpmyadmin_signon_url($runtimeConfig);
$cfg['Servers'][$i]['AllowNoPassword'] = false;
$cfg['Servers'][$i]['verbose'] = 'DirectAdmin alt-mysql';
$cfg['Servers'][$i]['only_db'] = '';
$cfg['ServerDefault'] = 1;
$cfg['AllowArbitraryServer'] = false;
PHP
cat > "$SIGNON_URL_PAGE" <<'PHP'
<?php
declare(strict_types=1);
const DA_MYSQL_SESSION = 'DA_MYSQL_PHPMYADMIN';
const DA_MYSQL_RUNTIME_CONFIG = __DIR__ . '/runtime/config.json';
function da_mysql_login_runtime_config(): array
{
if (!is_readable(DA_MYSQL_RUNTIME_CONFIG)) {
return [];
}
$raw = file_get_contents(DA_MYSQL_RUNTIME_CONFIG);
if (!is_string($raw) || $raw === '') {
return [];
}
$decoded = json_decode($raw, true);
return is_array($decoded) ? $decoded : [];
}
function da_mysql_login_url_path(array $runtimeConfig): string
{
$path = trim((string)($runtimeConfig['phpmyadmin_url_path'] ?? '/alt-mysql-phpmyadmin'));
if ($path === '') {
return '/alt-mysql-phpmyadmin';
}
if ($path[0] !== '/') {
$path = '/' . $path;
}
$path = rtrim($path, '/');
return $path !== '' ? $path : '/alt-mysql-phpmyadmin';
}
function da_mysql_login_ticket_dir(array $runtimeConfig): string
{
$dir = trim((string)($runtimeConfig['ticket_dir'] ?? ''));
if ($dir !== '') {
return rtrim($dir, '/');
}
return __DIR__ . '/runtime/tickets';
}
function da_mysql_login_error(string $message): void
{
http_response_code(400);
header('Content-Type: text/html; charset=UTF-8');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Pragma: no-cache');
$safe = htmlspecialchars($message, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
echo '<!doctype html><html lang="en"><head><meta charset="utf-8"><title>alt-mysql phpMyAdmin</title></head><body>';
echo '<h1>alt-mysql phpMyAdmin</h1><p>' . $safe . '</p>';
echo '</body></html>';
exit;
}
function da_mysql_login_consume_ticket(array $runtimeConfig): array
{
$token = strtolower((string)($_GET['ticket'] ?? ''));
if (preg_match('/\A[a-f0-9]{64}\z/', $token) !== 1) {
da_mysql_login_error('Missing or invalid phpMyAdmin login ticket.');
}
$path = da_mysql_login_ticket_dir($runtimeConfig) . '/' . $token . '.json';
if (!is_readable($path)) {
da_mysql_login_error('The phpMyAdmin login ticket is not available or has already been used.');
}
$raw = file_get_contents($path);
@unlink($path);
$ticket = is_string($raw) ? json_decode($raw, true) : null;
if (!is_array($ticket)) {
da_mysql_login_error('The phpMyAdmin login ticket is invalid.');
}
if ((int)($ticket['expires_at'] ?? 0) < time()) {
da_mysql_login_error('The phpMyAdmin login ticket has expired.');
}
$auth = $ticket['auth'] ?? [];
if (!is_array($auth) || (string)($auth['user'] ?? '') === '' || (string)($auth['password'] ?? '') === '') {
da_mysql_login_error('The phpMyAdmin login ticket does not contain credentials.');
}
return $ticket;
}
function da_mysql_login_redirect_target(array $ticket): string
{
$route = (string)($ticket['route'] ?? '/database/structure');
if (preg_match('#^/[A-Za-z0-9_./-]+$#', $route) !== 1 || strpos($route, '//') !== false) {
$route = '/';
}
$query = ['route' => $route];
$db = (string)($ticket['db'] ?? ($ticket['auth']['db'] ?? ''));
if ($db !== '') {
$query['db'] = $db;
}
return 'index.php?' . http_build_query($query);
}
$runtimeConfig = da_mysql_login_runtime_config();
$ticket = da_mysql_login_consume_ticket($runtimeConfig);
$secure = false;
$https = strtolower((string)($_SERVER['HTTPS'] ?? ''));
if ($https !== '' && $https !== 'off' && $https !== '0') {
$secure = true;
} elseif (strtolower((string)($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '')) === 'https') {
$secure = true;
}
session_name(DA_MYSQL_SESSION);
session_set_cookie_params([
'lifetime' => 0,
'path' => da_mysql_login_url_path($runtimeConfig),
'secure' => $secure,
'httponly' => true,
'samesite' => 'Lax',
]);
session_start();
$_SESSION['DA_MYSQL_PHPMYADMIN_AUTH'] = $ticket['auth'];
session_write_close();
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Pragma: no-cache');
header('Location: ' . da_mysql_login_redirect_target($ticket), true, 302);
exit;
PHP
cat > "$SIGNON_SCRIPT" <<'PHP'
<?php
declare(strict_types=1);
const DA_MYSQL_SESSION = 'DA_MYSQL_PHPMYADMIN';
const DA_MYSQL_DEFAULT_CONF = '/usr/local/directadmin/conf/alt-mysql.conf';
const DA_MYSQL_RUNTIME_CONFIG = __DIR__ . '/runtime/config.json';
function da_mysql_runtime_config(): array
{
if (!is_readable(DA_MYSQL_RUNTIME_CONFIG)) {
return [];
}
$raw = file_get_contents(DA_MYSQL_RUNTIME_CONFIG);
if (!is_string($raw) || $raw === '') {
return [];
}
$decoded = json_decode($raw, true);
return is_array($decoded) ? $decoded : [];
}
function da_mysql_signon_session_auth(): array
{
$payload = $_SESSION['DA_MYSQL_PHPMYADMIN_AUTH'] ?? [];
return is_array($payload) ? $payload : [];
}
function da_mysql_signon_read_auth(): array
{
$previousActive = session_status() === PHP_SESSION_ACTIVE;
$previousName = session_name();
$previousId = $previousActive ? session_id() : '';
if ($previousActive && $previousName === DA_MYSQL_SESSION) {
return da_mysql_signon_session_auth();
}
if ($previousActive) {
session_write_close();
}
session_name(DA_MYSQL_SESSION);
session_id('');
$ssoSessionId = (string)($_COOKIE[DA_MYSQL_SESSION] ?? '');
if ($ssoSessionId !== '' && preg_match('/\A[A-Za-z0-9,-]+\z/', $ssoSessionId) === 1) {
session_id($ssoSessionId);
}
session_start();
$payload = da_mysql_signon_session_auth();
session_write_close();
if ($previousActive) {
session_name($previousName);
if ($previousId !== '') {
session_id($previousId);
}
session_start();
} elseif ($previousName !== '') {
session_name($previousName);
}
return $payload;
}
function get_login_credentials($user = ''): array
{
$payload = da_mysql_signon_read_auth();
$username = (string)($payload['user'] ?? '');
$password = (string)($payload['password'] ?? '');
return [$username, $password];
}
PHP
mkdir -p "$RUNTIME_DIR/tmp"
chown -R diradmin:diradmin "$RUNTIME_DIR" "$CONFIG_INC" "$SIGNON_SCRIPT" "$SIGNON_URL_PAGE" 2>/dev/null || true
chmod 755 "$RUNTIME_DIR" "$TICKETS_DIR" "$RUNTIME_DIR/tmp" 2>/dev/null || true
chmod 644 "$CONFIG_INC" "$SIGNON_SCRIPT" "$SIGNON_URL_PAGE" "$RUNTIME_DIR/.htaccess" "$TICKETS_DIR/.htaccess" "$RUNTIME_DIR/index.html" "$TICKETS_DIR/index.html" 2>/dev/null || true
}
ensure_private_copy
write_private_config
log "Prywatna kopia phpMyAdmin dla alt-mysql jest gotowa w ${PHPMYADMIN_PRIVATE_DIR}."
@@ -0,0 +1,5 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
exec "$SCRIPT_DIR/phpmyadmin_install.sh" "$@"
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
exec "$SCRIPT_DIR/phpmyadmin_install.sh"
+36
View File
@@ -0,0 +1,36 @@
#!/bin/bash
set -euo pipefail
CB_DIR="/usr/local/directadmin/custombuild"
[ "${EUID:-$(id -u)}" -eq 0 ] || {
echo "Skrypt musi być uruchomiony jako root." >&2
exit 1
}
[ -x "${CB_DIR}/build" ] || {
echo "Nie znaleziono DirectAdmin CustomBuild w ${CB_DIR}." >&2
exit 1
}
php_slots=()
for slot in 1 2 3 4 5 6 7 8 9; do
version="$(awk -F= -v key="php${slot}_release" '$1 == key {print $2}' "${CB_DIR}/options.conf" 2>/dev/null | tr -d '[:space:]')"
case "${version,,}" in
""|no|off) continue ;;
esac
php_slots+=("$version")
done
[ ${#php_slots[@]} -gt 0 ] || {
echo "Nie wykryto aktywnych wersji PHP w CustomBuild." >&2
exit 1
}
for version in "${php_slots[@]}"; do
echo "Rebuild PHP ${version}"
"${CB_DIR}/build" php "$version"
done
"${CB_DIR}/build" rewrite_confs || true
echo "Rekompilacja PHP zakończona. Rozszerzenia mysqli i pdo_mysql są dostarczane przez standardowy build DirectAdmin."
+68
View File
@@ -0,0 +1,68 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=./mysql_common.sh
source "$SCRIPT_DIR/mysql_common.sh"
usage() {
cat <<'EOF'
Użycie:
restore_all.sh <katalog_backupu> [--restore-users]
Opis:
Przywraca wszystkie bazy MySQL z katalogu backupu. Jeśli obecny jest plik
`_globals.sql` lub `_globals.sql.gz`, można go odtworzyć flagą
`--restore-users`.
EOF
}
BACKUP_DIR=""
RESTORE_USERS="0"
for arg in "$@"; do
case "$arg" in
--help|-h)
usage
exit 0
;;
--restore-users)
RESTORE_USERS="1"
;;
*)
if [ -n "$BACKUP_DIR" ]; then
echo "Podano więcej niż jeden katalog backupu." >&2
exit 1
fi
BACKUP_DIR="$arg"
;;
esac
done
[ -n "$BACKUP_DIR" ] || {
usage
exit 1
}
[ -d "$BACKUP_DIR" ] || {
echo "Katalog nie istnieje: $BACKUP_DIR" >&2
exit 1
}
mysql_load_alt_credentials
if [ "$RESTORE_USERS" = "1" ]; then
for globals_file in "$BACKUP_DIR/_globals.sql.gz" "$BACKUP_DIR/_globals.sql"; do
[ -f "$globals_file" ] || continue
mysql_restore_stream mysql "$globals_file"
break
done
fi
find "$BACKUP_DIR" -maxdepth 1 -type f \( -name '*.sql' -o -name '*.sql.gz' \) ! -name '_globals.*' | sort | while IFS= read -r file; do
[ -n "$file" ] || continue
db_name="$(basename "$file" | sed -E 's/\.sql(\.gz)?$//')"
db_name="$(mysql_safe_identifier "$db_name")"
mysql_exec mysql -e "DROP DATABASE IF EXISTS $(mysql_quote_identifier "$db_name");"
mysql_exec mysql -e "CREATE DATABASE $(mysql_quote_identifier "$db_name") CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
mysql_restore_stream "$db_name" "$file"
echo "Przywrócono ${db_name}"
done
+186
View File
@@ -0,0 +1,186 @@
#!/bin/bash
set -euo pipefail
ROUNDCUBE_PLUGIN_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
ROUNDCUBE_SETTINGS_FILE="${ROUNDCUBE_SETTINGS_FILE:-$ROUNDCUBE_PLUGIN_DIR/plugin-settings.conf}"
ROUNDCUBE_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=./mysql_common.sh
source "$ROUNDCUBE_SCRIPT_DIR/mysql_common.sh"
mysql_load_plugin_settings_file "$ROUNDCUBE_SETTINGS_FILE"
roundcube_read_setting() {
local key="$1"
local default="$2"
local line value
[ -r "$ROUNDCUBE_SETTINGS_FILE" ] || {
printf '%s\n' "$default"
return 0
}
line="$(grep -E "^${key}=" "$ROUNDCUBE_SETTINGS_FILE" | tail -n 1 || true)"
[ -n "$line" ] || {
printf '%s\n' "$default"
return 0
}
value="${line#*=}"
value="${value%%#*}"
value="${value%\"}"
value="${value#\"}"
value="${value%\'}"
value="${value#\'}"
value="$(printf '%s' "$value" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
printf '%s\n' "${value:-$default}"
}
roundcube_enabled() {
case "$(roundcube_read_setting ROUNDCUBE_ENABLED true)" in
true|yes|on|1) return 0 ;;
*) return 1 ;;
esac
}
roundcube_config_file() {
roundcube_read_setting ROUNDCUBE_CONFIG_FILE /var/www/html/roundcube/config/config.inc.php
}
roundcube_custom_config_file() {
roundcube_read_setting ROUNDCUBE_CUSTOM_CONFIG_FILE /usr/local/directadmin/custombuild/custom/roundcube/config.inc.php
}
roundcube_write_custombuild_config() {
case "$(roundcube_read_setting ROUNDCUBE_WRITE_CUSTOMBUILD_CONFIG true)" in
true|yes|on|1) return 0 ;;
*) return 1 ;;
esac
}
roundcube_db_name() {
roundcube_read_setting ROUNDCUBE_DB_NAME roundcube
}
roundcube_db_user() {
roundcube_read_setting ROUNDCUBE_DB_USER roundcube
}
roundcube_password_file() {
roundcube_read_setting ROUNDCUBE_DB_PASSWORD_FILE /usr/local/directadmin/conf/alt-roundcube.conf
}
roundcube_native_my_cnf() {
roundcube_read_setting ROUNDCUBE_NATIVE_MY_CNF /usr/local/directadmin/conf/my.cnf
}
roundcube_endpoint_mode() {
roundcube_read_setting ROUNDCUBE_ALT_DSN_MODE tcp
}
roundcube_generate_secret() {
if command -v openssl >/dev/null 2>&1; then
openssl rand -base64 36 | tr -d '\n'
return 0
fi
tr -dc 'A-Za-z0-9_@%+=' </dev/urandom | head -c 48
}
roundcube_load_or_create_password() {
local file user pass group_name tmp
file="$(roundcube_password_file)"
user="$(roundcube_db_user)"
if [ -r "$file" ]; then
pass="$(awk -F= '$1 == "password" || $1 == "passwd" { sub(/^[^=]*=/, ""); print; exit }' "$file")"
if [ -n "$pass" ]; then
printf '%s\n' "$pass"
return 0
fi
fi
pass="$(roundcube_generate_secret)"
group_name=""
if command -v getent >/dev/null 2>&1 && getent group diradmin >/dev/null 2>&1; then
group_name="diradmin"
elif command -v getent >/dev/null 2>&1 && getent group root >/dev/null 2>&1; then
group_name="root"
fi
tmp="$(mktemp)"
{
printf 'user=%s\n' "$user"
printf 'password=%s\n' "$pass"
} > "$tmp"
if [ "${EUID:-$(id -u)}" -eq 0 ] && [ -n "$group_name" ]; then
install -m 0640 -o root -g "$group_name" "$tmp" "$file"
else
install -m 0600 "$tmp" "$file"
fi
rm -f "$tmp"
printf '%s\n' "$pass"
}
roundcube_load_password() {
local file pass
file="$(roundcube_password_file)"
[ -r "$file" ] || return 1
pass="$(awk -F= '$1 == "password" || $1 == "passwd" { sub(/^[^=]*=/, ""); print; exit }' "$file")"
[ -n "$pass" ] || return 1
printf '%s\n' "$pass"
}
roundcube_urlencode() {
php -r 'echo rawurlencode($argv[1]);' "$1"
}
roundcube_alt_dsn() {
local db user pass host port mode encoded_user encoded_pass
mysql_load_alt_runtime
db="$(roundcube_db_name)"
user="$(roundcube_db_user)"
pass="$(roundcube_load_or_create_password)"
mode="$(roundcube_endpoint_mode)"
encoded_user="$(roundcube_urlencode "$user")"
encoded_pass="$(roundcube_urlencode "$pass")"
case "$mode" in
socket)
printf 'mysql://%s:%s@unix(%s)/%s\n' "$encoded_user" "$encoded_pass" "$MYSQL_ALT_SOCKET" "$db"
;;
*)
host="127.0.0.1"
port="$MYSQL_ALT_PORT"
printf 'mysql://%s:%s@%s:%s/%s\n' "$encoded_user" "$encoded_pass" "$host" "$port" "$db"
;;
esac
}
roundcube_sql_quote() {
local value="$1"
value="${value//\\/\\\\}"
value="${value//\'/\'\'}"
printf "'%s'" "$value"
}
roundcube_create_alt_database_and_user() {
local db user pass q_user q_pass
mysql_load_alt_credentials
mysql_ensure_metadata_schema
db="$(roundcube_db_name)"
user="$(roundcube_db_user)"
pass="$(roundcube_load_or_create_password)"
q_user="$(roundcube_sql_quote "$user")"
q_pass="$(roundcube_sql_quote "$pass")"
mysql_safe_identifier "$db" >/dev/null
mysql_safe_identifier "$user" >/dev/null
mysql_exec mysql -e "CREATE DATABASE IF NOT EXISTS $(mysql_quote_identifier "$db") CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
mysql_exec mysql -e "CREATE USER IF NOT EXISTS ${q_user}@'localhost' IDENTIFIED BY ${q_pass};"
mysql_exec mysql -e "ALTER USER ${q_user}@'localhost' IDENTIFIED BY ${q_pass};"
mysql_exec mysql -e "CREATE USER IF NOT EXISTS ${q_user}@'127.0.0.1' IDENTIFIED BY ${q_pass};"
mysql_exec mysql -e "ALTER USER ${q_user}@'127.0.0.1' IDENTIFIED BY ${q_pass};"
mysql_exec mysql -e "GRANT ALL PRIVILEGES ON $(mysql_quote_identifier "$db").* TO ${q_user}@'localhost';"
mysql_exec mysql -e "GRANT ALL PRIVILEGES ON $(mysql_quote_identifier "$db").* TO ${q_user}@'127.0.0.1';"
mysql_exec mysql -e "
INSERT INTO da_plugin_system_databases (service_name, db_name, db_user, endpoint_mode, config_path, enabled)
VALUES ('roundcube', '$(mysql_escape_literal "$db")', '$(mysql_escape_literal "$user")', '$(mysql_escape_literal "$(roundcube_endpoint_mode)")', '$(mysql_escape_literal "$(roundcube_config_file)")', 1)
ON DUPLICATE KEY UPDATE db_name=VALUES(db_name), db_user=VALUES(db_user), endpoint_mode=VALUES(endpoint_mode), config_path=VALUES(config_path), enabled=VALUES(enabled), updated_at=CURRENT_TIMESTAMP;
"
}
+38
View File
@@ -0,0 +1,38 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=./roundcube_common.sh
source "$SCRIPT_DIR/roundcube_common.sh"
fail() {
echo "roundcube alt-mysql health: $*" >&2
exit 1
}
roundcube_enabled || {
echo "roundcube alt-mysql health: disabled"
exit 0
}
CONFIG_FILE="$(roundcube_config_file)"
DB_NAME="$(roundcube_db_name)"
DB_USER="$(roundcube_db_user)"
PASS="$(roundcube_load_password)" || fail "Roundcube password file is missing or invalid: $(roundcube_password_file)"
mysql_load_alt_credentials
mysql_load_alt_runtime
[ -r "$CONFIG_FILE" ] || fail "config file is not readable: $CONFIG_FILE"
grep -Fq "BEGIN DIRECTADMIN MYSQL PLUGIN ROUNDCUBE" "$CONFIG_FILE" || fail "managed Roundcube DB block is missing"
grep -Fq "$DB_NAME" "$CONFIG_FILE" || fail "Roundcube config does not reference expected database"
if grep -Fq "/var/lib/mysql/mysql.sock" "$CONFIG_FILE"; then
fail "Roundcube config references native DirectAdmin socket"
fi
if grep -Fq "/usr/local/directadmin/conf/mysql.conf" "$CONFIG_FILE"; then
fail "Roundcube config references native DirectAdmin mysql.conf"
fi
MYSQL_PWD="$PASS" "$(mysql_alt_binary mysql)" --protocol=TCP --host=127.0.0.1 --port="$MYSQL_ALT_PORT" --user="$DB_USER" --database="$DB_NAME" -e "SELECT 1" >/dev/null \
|| fail "cannot connect to Roundcube database on alt-mariadb as $DB_USER"
echo "roundcube alt-mysql health: OK"
@@ -0,0 +1,65 @@
#!/bin/bash
set -Eeuo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=./roundcube_common.sh
source "$SCRIPT_DIR/roundcube_common.sh"
FORCE="${FORCE:-0}"
SKIP_NATIVE_DUMP="${SKIP_NATIVE_DUMP:-0}"
LOG_FILE="${LOG_FILE:-$ROUNDCUBE_PLUGIN_DIR/data/logs/roundcube-migrate.log}"
mkdir -p "$(dirname "$LOG_FILE")"
log() {
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" | tee -a "$LOG_FILE"
}
die() {
log "ERROR: $*"
exit 1
}
roundcube_enabled || die "Roundcube integration is disabled by ROUNDCUBE_ENABLED."
DB_NAME="$(roundcube_db_name)"
NATIVE_MY_CNF="$(roundcube_native_my_cnf)"
TMP_DUMP="$(mktemp)"
trap 'rm -f "$TMP_DUMP"' EXIT
roundcube_create_alt_database_and_user
if [ "$SKIP_NATIVE_DUMP" != "1" ]; then
[ -r "$NATIVE_MY_CNF" ] || die "Native DirectAdmin MySQL config not readable: $NATIVE_MY_CNF"
NATIVE_MYSQL_BIN="$(mysql_native_binary mysql)" || die "native mysql/mariadb client not found"
NATIVE_MYSQLDUMP_BIN="$(mysql_native_binary mysqldump)" || die "native mysqldump/mariadb-dump client not found"
if ! "$NATIVE_MYSQL_BIN" --defaults-extra-file="$NATIVE_MY_CNF" --batch --skip-column-names information_schema \
-e "SELECT 1 FROM SCHEMATA WHERE SCHEMA_NAME='$(mysql_escape_literal "$DB_NAME")' LIMIT 1" | grep -q 1; then
if [ "$FORCE" != "1" ]; then
die "Native Roundcube database $DB_NAME not found. Use SKIP_NATIVE_DUMP=1 if this is a new Roundcube install."
fi
log "Native Roundcube database $DB_NAME not found; continuing because FORCE=1."
else
log "Dumping native Roundcube database: $DB_NAME"
"$NATIVE_MYSQLDUMP_BIN" --defaults-extra-file="$NATIVE_MY_CNF" \
--single-transaction \
--quick \
--skip-lock-tables \
--routines \
--triggers \
--events \
--default-character-set=utf8mb4 \
"$DB_NAME" > "$TMP_DUMP" 2>>"$LOG_FILE" || die "native Roundcube dump failed"
log "Importing Roundcube database into alt-mariadb: $DB_NAME"
mysql_exec mysql -e "DROP DATABASE IF EXISTS $(mysql_quote_identifier "$DB_NAME");"
mysql_exec mysql -e "CREATE DATABASE $(mysql_quote_identifier "$DB_NAME") CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
mysql_exec "$DB_NAME" < "$TMP_DUMP" >> "$LOG_FILE" 2>&1 || die "Roundcube import into alt-mariadb failed"
roundcube_create_alt_database_and_user
fi
fi
"$SCRIPT_DIR/roundcube_repair_config.sh"
"$SCRIPT_DIR/roundcube_health_check.sh"
log "Roundcube migration to alt-mariadb completed."
+79
View File
@@ -0,0 +1,79 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=./roundcube_common.sh
source "$SCRIPT_DIR/roundcube_common.sh"
rewrite_roundcube_config() {
local source_file="$1"
local target_file="$2"
local dsn="$3"
local tmp parent
[ -f "$source_file" ] || {
echo "roundcube: source config file not found: $source_file" >&2
return 1
}
if [ -e "$target_file" ]; then
[ -w "$target_file" ] || {
echo "roundcube: config file is not writable: $target_file" >&2
return 1
}
else
parent="$(dirname "$target_file")"
mkdir -p "$parent"
fi
tmp="$(mktemp)"
awk '
/BEGIN DIRECTADMIN MYSQL PLUGIN ROUNDCUBE/ { skip=1; next }
/END DIRECTADMIN MYSQL PLUGIN ROUNDCUBE/ { skip=0; next }
skip != 1 { print }
' "$source_file" > "$tmp"
cat >> "$tmp" <<PHP
// BEGIN DIRECTADMIN MYSQL PLUGIN ROUNDCUBE
\$config['db_dsnw'] = '$dsn';
// END DIRECTADMIN MYSQL PLUGIN ROUNDCUBE
PHP
if [ -e "$target_file" ]; then
cat "$tmp" > "$target_file"
else
install -m 0644 "$tmp" "$target_file"
fi
rm -f "$tmp"
}
roundcube_enabled || {
echo "roundcube: integration disabled by ROUNDCUBE_ENABLED."
exit 0
}
CONFIG_FILE="$(roundcube_config_file)"
[ -f "$CONFIG_FILE" ] || {
echo "roundcube: config file not found: $CONFIG_FILE" >&2
exit 1
}
[ -w "$CONFIG_FILE" ] || {
echo "roundcube: config file is not writable: $CONFIG_FILE" >&2
exit 1
}
DSN="$(roundcube_alt_dsn)"
rewrite_roundcube_config "$CONFIG_FILE" "$CONFIG_FILE" "$DSN"
echo "roundcube: config repaired: $CONFIG_FILE"
if roundcube_write_custombuild_config; then
CUSTOM_CONFIG_FILE="$(roundcube_custom_config_file)"
if [ -f "$CUSTOM_CONFIG_FILE" ]; then
rewrite_roundcube_config "$CUSTOM_CONFIG_FILE" "$CUSTOM_CONFIG_FILE" "$DSN"
else
rewrite_roundcube_config "$CONFIG_FILE" "$CUSTOM_CONFIG_FILE" "$DSN"
fi
echo "roundcube: CustomBuild config repaired: $CUSTOM_CONFIG_FILE"
fi
+106
View File
@@ -0,0 +1,106 @@
#!/bin/bash
set -euo pipefail
PLUGIN_DIR="$(cd "$(dirname "$0")/.." && pwd)"
CPIF="/usr/local/directadmin/data/admin/custom_package_items.conf"
SUDOERS_FILE="/etc/sudoers.d/directadmin-alt-mysql-plugin"
CRON_FILE="/etc/cron.d/alt-mysql-jobs"
CSF_ALLOW_FILE="/etc/csf/csf.allow"
CSF_MANAGED_ALLOW_FILE="/etc/csf/alt-mysql-plugin.allow"
DA_INTEGRATION_INSTALLER="$(cd "$(dirname "$0")/.." && pwd)/scripts/setup/da_integration_install.sh"
CUSTOMBUILD_HOOKS_INSTALLER="$(cd "$(dirname "$0")/.." && pwd)/scripts/setup/custombuild_hooks_install.sh"
read_plugin_setting() {
local key="$1"
local default="$2"
local file="$PLUGIN_DIR/plugin-settings.conf"
local line value
[ -r "$file" ] || {
printf '%s\n' "$default"
return 0
}
line="$(grep -E "^${key}=" "$file" | tail -n 1 || true)"
if [ -z "$line" ]; then
printf '%s\n' "$default"
return 0
fi
value="${line#*=}"
value="${value%%#*}"
value="${value%\"}"
value="${value#\"}"
value="${value%\'}"
value="${value#\'}"
value="$(printf '%s' "$value" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
printf '%s\n' "${value:-$default}"
}
cleanup_csf_managed_file() {
local managed_file="$1"
local tmp include_line changed=0
include_line="Include $managed_file"
if [ -f "$CSF_ALLOW_FILE" ]; then
tmp="$(mktemp)"
grep -Fxv "$include_line" "$CSF_ALLOW_FILE" > "$tmp" || true
if ! cmp -s "$tmp" "$CSF_ALLOW_FILE"; then
cat "$tmp" > "$CSF_ALLOW_FILE"
changed=1
fi
rm -f "$tmp"
fi
if [ -f "$managed_file" ]; then
rm -f "$managed_file"
changed=1
fi
return "$changed"
}
cleanup_csf_remote_mysql() {
local changed=0 configured_managed_allow
CSF_ALLOW_FILE="$(read_plugin_setting MYSQL_PLUGIN_CSF_ALLOW_FILE "$CSF_ALLOW_FILE")"
configured_managed_allow="$(read_plugin_setting MYSQL_PLUGIN_CSF_MANAGED_ALLOW_FILE "$CSF_MANAGED_ALLOW_FILE")"
cleanup_csf_managed_file "$configured_managed_allow" || changed=1
if [ "$changed" -eq 1 ] && command -v csf >/dev/null 2>&1; then
csf -r >/dev/null 2>&1 || true
fi
}
if [ "$(id -u)" -eq 0 ]; then
if [ -f "$CPIF" ]; then
sed -i '/^mysql_enabled=/d' "$CPIF" || true
sed -i '/^mysql=/d' "$CPIF" || true
sed -i '/^umysql=/d' "$CPIF" || true
sed -i '/^mysql_max_databases=/d' "$CPIF" || true
sed -i '/^mysql_unlimited=/d' "$CPIF" || true
sed -i '/^umysql_max_databases=/d' "$CPIF" || true
chown diradmin:diradmin "$CPIF" 2>/dev/null || true
chmod 640 "$CPIF" 2>/dev/null || true
fi
rm -f "$SUDOERS_FILE" "$CRON_FILE" || true
cleanup_csf_remote_mysql
if [ -x "$DA_INTEGRATION_INSTALLER" ]; then
"$DA_INTEGRATION_INSTALLER" uninstall || true
fi
if [ -x "$CUSTOMBUILD_HOOKS_INSTALLER" ]; then
"$CUSTOMBUILD_HOOKS_INSTALLER" uninstall || true
fi
else
echo "mysql: warning: uruchom jako root, aby usunąć wpisy z custom_package_items i sudoers" >&2
fi
if [ -f "$PLUGIN_DIR/plugin.conf" ]; then
sed -i 's/^active=.*/active=no/' "$PLUGIN_DIR/plugin.conf" || true
sed -i 's/^installed=.*/installed=no/' "$PLUGIN_DIR/plugin.conf" || true
fi
echo "mysql: deaktywowany."
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
set -euo pipefail
"$(cd "$(dirname "$0")" && pwd)/install.sh"
+104
View File
@@ -0,0 +1,104 @@
#!/bin/bash
set -euo pipefail
PLUGIN_DIR="$(cd "$(dirname "$0")/.." && pwd)"
DATA_DIR="$PLUGIN_DIR/data"
JOB_DIR="$DATA_DIR/jobs/pending"
PROCESSING_DIR="$DATA_DIR/jobs/processing"
DONE_DIR="$DATA_DIR/jobs/done"
WORKER_LOCK_DIR="$DATA_DIR/worker.lock"
DB_LOCK_DIR="$DATA_DIR/lock"
PID_DIR="$DATA_DIR/pids"
CANCEL_DIR="$DATA_DIR/cancel"
mkdir -p "$JOB_DIR" "$PROCESSING_DIR" "$DONE_DIR" "$PID_DIR" "$CANCEL_DIR" "$DB_LOCK_DIR"
if ! mkdir "$WORKER_LOCK_DIR" 2>/dev/null; then
exit 0
fi
cleanup() {
rmdir "$WORKER_LOCK_DIR" 2>/dev/null || true
}
trap cleanup EXIT
db_lock_path_for_db() {
local db_name="$1"
local safe
safe="$(printf '%s' "$db_name" | tr -c 'A-Za-z0-9_.-' '_')"
printf '%s/%s.lock' "$DB_LOCK_DIR" "$safe"
}
release_db_lock_later() {
local lock_path="$1"
if [ -z "$lock_path" ]; then
return 0
fi
(
sleep 60
rm -f "$lock_path" 2>/dev/null || true
) >/dev/null 2>&1 &
}
while true; do
JOB_FILE="$(ls -1 "$JOB_DIR"/*.env 2>/dev/null | head -n 1 || true)"
if [ -z "${JOB_FILE:-}" ]; then
break
fi
BASE_NAME="$(basename "$JOB_FILE")"
RUN_FILE="$PROCESSING_DIR/$BASE_NAME"
mv "$JOB_FILE" "$RUN_FILE"
JOB_ID="${BASE_NAME%.env}"
PID_FILE="$PID_DIR/${JOB_ID}.pid"
JOB_TYPE="restore"
DB_NAME=""
DB_LOCK_FILE=""
# shellcheck disable=SC1090
source "$RUN_FILE" || true
JOB_TYPE="${JOB_TYPE:-restore}"
DB_NAME="${DB_NAME:-}"
DB_LOCK_FILE="${DB_LOCK_FILE:-}"
LOCK_PATH=""
if [ -n "$DB_LOCK_FILE" ]; then
case "$DB_LOCK_FILE" in
"$DB_LOCK_DIR"/*.lock) LOCK_PATH="$DB_LOCK_FILE" ;;
*) LOCK_PATH="" ;;
esac
fi
if [ -z "$LOCK_PATH" ] && [ -n "$DB_NAME" ]; then
LOCK_PATH="$(db_lock_path_for_db "$DB_NAME")"
fi
RUNNER="$PLUGIN_DIR/scripts/restore_job.sh"
if [ "$JOB_TYPE" = "backup" ]; then
RUNNER="$PLUGIN_DIR/scripts/backup_job.sh"
fi
set +e
"$RUNNER" "$RUN_FILE" &
RUN_PID=$!
echo "PID=${RUN_PID}" >> "$RUN_FILE"
echo "${RUN_PID}" > "$PID_FILE"
wait "$RUN_PID"
EXIT_CODE=$?
set -e
rm -f "$PID_FILE"
CANCEL_FLAG="${CANCEL_DIR}/${JOB_ID}.flag"
if [ -f "$CANCEL_FLAG" ]; then
rm -f "$CANCEL_FLAG"
mv "$RUN_FILE" "$DONE_DIR/${JOB_ID}.cancel"
elif [ "$EXIT_CODE" -eq 0 ]; then
mv "$RUN_FILE" "$DONE_DIR/${JOB_ID}.ok"
else
mv "$RUN_FILE" "$DONE_DIR/${JOB_ID}.fail"
fi
release_db_lock_later "$LOCK_PATH"
done
exit 0