coolice reset script

Slug: coolice-reset-script

20500 characters 2733 words
#!/usr/bin/env bash # # Revised: stable logging and safe optional tracing. # - Key changes vs. previous iteration: # * Use a stable log file in BASEDIR (not inside ephemeral WORKDIR) for all persistent logs. # * Open a single persistent FD (3) for programmatic logging and (optionally) BASH_XTRACEFD. # * Do NOT use DEBUG traps (they are fragile with process substitution); rely on BASH_XTRACEFD when available. # * Avoid tee pipelines for log writes; use single FD writes to avoid FD lifecycle races. # set -Eeuo pipefail shopt -s inherit_errexit 2>/dev/null || true IFS=$'\n\t' # ----------------------- # Small helpers early (used by arg parsing/sanity checks) # ----------------------- timestamp() { date +'%Y-%m-%d %H:%M:%S'; } abs() { local p="$1" if command -v realpath >/dev/null 2>&1; then realpath -m "$p" 2>/dev/null || printf '%s\n' "$p" elif command -v readlink >/dev/null 2>&1; then readlink -f "$p" 2>/dev/null || printf '%s\n' "$p" else (cd "$(dirname "$p")" 2>/dev/null && printf '%s/%s\n' "$(pwd -P)" "$(basename "$p")") || printf '%s\n' "$p" fi } is_special() { [ -S "$1" ] || [ -p "$1" ] || [ -b "$1" ] || [ -c "$1" ]; } # ----------------------- # Configuration defaults # ----------------------- DEFAULT_BASEDIR="${HOME:-/home/ibbsbbry}" BASEDIR="$DEFAULT_BASEDIR" APPLY=0 USE_ZIP=0 DEBUG=0 TRACE=0 TIMESTAMP="$(date +%Y%m%d_%H%M%S)" MIN_FREE_KB=102400 # ----------------------- # Tool detection (FIX for latent bug) # ----------------------- TAR_GNU=0 if tar --version 2>/dev/null | grep -q 'GNU tar'; then TAR_GNU=1 fi HAS_ZIP=0 if command -v zip >/dev/null 2>&1; then HAS_ZIP=1 fi # ----------------------- # Lightweight logging helpers (they write to FD 3 when available) # ----------------------- _log_to_fd3() { # write a single line to FD3 if it's open; fall back to stdout/stderr local ln="$1" if { true >&3; } 2>/dev/null; then printf '%s\n' "$ln" >&3 2>/dev/null || true else printf '%s\n' "$ln" 2>/dev/null || true fi } log() { local msg="$1" local line line=$(printf '[%s] INFO: %s' "$(timestamp)" "$msg") _log_to_fd3 "$line" printf '%s\n' "$line" } warn() { local msg="$1" local line line=$(printf '[%s] WARN: %s' "$(timestamp)" "$msg") _log_to_fd3 "$line" printf '%s\n' "$line" >&2 } err() { local msg="$1" local line line=$(printf '[%s] ERROR: %s' "$(timestamp)" "$msg") _log_to_fd3 "$line" printf '%s\n' "$line" >&2 } dbg() { [ "${DEBUG:-0}" -ne 0 ] || return 0 local msg="$1" local line line=$(printf '[%s] DEBUG: %s' "$(timestamp)" "$msg") _log_to_fd3 "$line" printf '%s\n' "$line" } trace_log() { [ "${TRACE:-0}" -ne 0 ] || return 0 local msg="$1" local line line=$(printf '[%s] TRACE: %s' "$(timestamp)" "$msg") _log_to_fd3 "$line" printf '%s\n' "$line" } # Capture minimal last/current command for error context CURRENT_COMMAND="" LAST_COMMAND="" # ----------------------- # Argument parsing # ----------------------- usage() { cat <<USAGE Usage: trim_and_fixperms.sh [options] Default: DRY-RUN (no destructive actions). Options: -y, --apply Execute archive + remove + permissions (destructive) --zip Prefer ZIP archive (requires 'zip') --base DIR Base directory (default: \$HOME) --debug Enable verbose debug logging (paths & outcomes) --trace Enable command tracing (command-level trace; requires bash >= 4.1) -h, --help Show this help USAGE } while [ $# -gt 0 ]; do case "$1" in -y|--apply) APPLY=1; shift ;; --zip) USE_ZIP=1; shift ;; --base) shift BASEDIR="${1:-$BASEDIR}" shift ;; --debug) DEBUG=1; shift ;; --trace) TRACE=1; shift ;; -h|--help) usage; exit 0 ;; *) echo "Unknown option: $1" >&2; usage; exit 1 ;; esac done # ----------------------- # Resolve BASEDIR and do early safety checks (we create persistent log in BASEDIR) # ----------------------- BASEDIR="$(abs "$BASEDIR")" case "$BASEDIR" in "/"|"/root"|"/home") echo "Refusing to operate on $BASEDIR" >&2; exit 1 ;; esac [ -d "$BASEDIR" ] || { echo "Base dir not found: $BASEDIR" >&2; exit 1; } FREE_KB=$(df -k "$BASEDIR" 2>/dev/null | awk 'NR==2{print $4}' || echo 0) if [ -z "$FREE_KB" ] || [ "$FREE_KB" -lt "$MIN_FREE_KB" ]; then echo "Low free space on filesystem containing $BASEDIR ($FREE_KB KB < $MIN_FREE_KB KB)" >&2 exit 1 fi # ----------------------- # Prepare stable log (outside WORKDIR) and open FD3 # ----------------------- LOG_STABLE="$BASEDIR/trim_and_fixperms_${TIMESTAMP}.log" : >"$LOG_STABLE" 2>/dev/null || true # Open FD3 for persistent programmatic logging; leave it open until on_exit exec 3>>"$LOG_STABLE" 2>/dev/null || true # If TRACE requested, enable Bash xtrace to FD3 (requires bash >= 4.1) if [ "${TRACE:-0}" -eq 1 ]; then if ((BASH_VERSINFO[0] > 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] >= 1))); then export BASH_XTRACEFD=3 PS4='+[$(date "+%Y-%m-%d %H:%M:%S")] ' set -x trace_log "Enabled per-command xtrace to $LOG_STABLE" else TRACE=0 dbg "TRACE disabled: bash too old to safely redirect xtrace (requires bash >= 4.1)" fi fi log "Script started: $0 (TIMESTAMP=$TIMESTAMP) LOG=$LOG_STABLE" # ----------------------- # Prepare WORKDIR (ephemeral) # ----------------------- WORKDIR="$(mktemp -d "${TMPDIR:-/tmp}/trimwork.${TIMESTAMP}.XXXXXX" 2>/dev/null || true)" if [ -z "${WORKDIR:-}" ] || [ ! -d "$WORKDIR" ] || [ ! -w "$WORKDIR" ]; then WORKDIR="$BASEDIR/.trimwork_${TIMESTAMP}" mkdir -p "$WORKDIR" 2>/dev/null || { err "Cannot create workdir: $WORKDIR"; exit 1; } chmod 700 "$WORKDIR" 2>/dev/null || true fi # local copies of path variables inside workdir if needed DOMAINS_FILE="$WORKDIR/domains_${TIMESTAMP}.lst" CAND_ABS="$WORKDIR/candidates_abs_${TIMESTAMP}.nul" CAND_REL="$WORKDIR/candidates_rel_${TIMESTAMP}.nul" : >"$DOMAINS_FILE" : >"$CAND_ABS" : >"$CAND_REL" dbg "WORKDIR=$WORKDIR" # ----------------------- # Error handler uses stable log (LOG_STABLE). Keep WORKDIR until on_exit. # ----------------------- on_error() { local rc=${1:-$?} LAST_COMMAND=${LAST_COMMAND:-'<none>'} CURRENT_COMMAND=${CURRENT_COMMAND:-"$BASH_COMMAND"} err "Script failed (exit code ${rc})." err "Last command: ${LAST_COMMAND}" err "Current command: ${CURRENT_COMMAND}" err "Location: PWD=$(pwd 2>/dev/null || echo '<pwd?>') USER=${USER:-}" err "Preserved WORKDIR for inspection: ${WORKDIR:-<none>}" KEEP_WORKDIR_ON_EXIT=1 # Ensure stable log has been copied/available; it's already in BASEDIR so no-op most times. cp -f "$LOG_STABLE" "$BASEDIR/trim_and_fixperms_${TIMESTAMP}.log.FAILED" 2>/dev/null || true err "Copied runtime log (best-effort) to $BASEDIR/trim_and_fixperms_${TIMESTAMP}.log.FAILED" # Close FD3 only at the end via on_exit; exit now exit "$rc" } trap 'on_error $?' ERR on_exit() { local rc=${1:-$?} # flush/close FD3 last (best-effort) if [ -n "${WORKDIR:-}" ] && [ -f "$LOG_STABLE" ] && [ "$rc" -eq 0 ]; then cp -f "$LOG_STABLE" "$BASEDIR/trim_and_fixperms_${TIMESTAMP}.log" 2>/dev/null || true dbg "Final log left at $LOG_STABLE" fi if [ "${KEEP_WORKDIR_ON_EXIT:-0}" -eq 1 ]; then dbg "Preserving WORKDIR for inspection: ${WORKDIR:-<none>}" else if [ -n "${WORKDIR:-}" ]; then rm -rf -- "$WORKDIR" 2>/dev/null || true dbg "Removed WORKDIR: $WORKDIR" fi fi # close FD3 exec 3>&- 2>/dev/null || true } trap 'on_exit $?' EXIT # ----------------------- # Portable functions used below # ----------------------- path_size() { local p="$1" [ -n "$p" ] || { printf '0\n'; return; } if command -v stat >/dev/null 2>&1; then if stat --version >/dev/null 2>&1; then stat -c%s -- "$p" 2>/dev/null || du -sb -- "$p" 2>/dev/null | awk '{print $1}' || echo 0 else stat -f%z -- "$p" 2>/dev/null || du -sk -- "$p" 2>/dev/null | awk '{print $1*1024}' || echo 0 fi else du -sb -- "$p" 2>/dev/null | awk '{print $1}' || du -sk -- "$p" 2>/dev/null | awk '{print $1*1024}' || echo 0 fi } human() { local n="${1:-0}" if command -v numfmt >/dev/null 2>&1; then numfmt --to=iec --suffix=B "$n" 2>/dev/null || printf '%sB' "$n" else awk -v b="$n" 'BEGIN{function hr(x){s="B K M G";i=1;while(x>=1024&&i<4){x/=1024;i++}printf "%.1f%s",x,substr(s,i*2-1,1)};hr(b)}' fi } safe_move() { local src="$1" dst="$2" dbg "safe_move: '$src' -> '$dst'" if [ "${APPLY:-0}" -eq 1 ]; then if mv -- "$src" "$dst" 2>>"$LOG_STABLE"; then dbg "mv succeeded: $src -> $dst" return 0 fi dbg "mv failed, attempting cp -a + rm -rf for $src" if cp -a -- "$src" "$dst" 2>>"$LOG_STABLE"; then rm -rf -- "$src" 2>>"$LOG_STABLE" || { err "rm after cp failed for $src"; return 1; } dbg "cp+rm succeeded for $src" return 0 fi err "safe_move failed for $src -> $dst" return 1 else dbg "DRY-RUN safe_move: $src -> $dst" return 0 fi } # ----------------------- # Discover domain roots # ----------------------- log "Discovering domain roots..." [ -d "$BASEDIR/public_html" ] && printf '%s\n' "$BASEDIR" >>"$DOMAINS_FILE" if [ -d "$BASEDIR/domains" ]; then for d in "$BASEDIR/domains"/*; do [ -d "$d/public_html" ] && printf '%s\n' "$d" >>"$DOMAINS_FILE" done fi for d in "$BASEDIR"/*; do [ -d "$d/public_html" ] && printf '%s\n' "$d" >>"$DOMAINS_FILE" done if [ -s "$DOMAINS_FILE" ]; then LC_ALL=C sort -u "$DOMAINS_FILE" -o "$DOMAINS_FILE" fi DOM_COUNT=$(wc -l < "$DOMAINS_FILE" 2>/dev/null || echo 0) log "Detected $DOM_COUNT domain roots" if [ "$DOM_COUNT" -gt 0 ]; then sed 's/^/ /' "$DOMAINS_FILE" >>"$LOG_STABLE" 2>/dev/null || true fi # ----------------------- # Candidate collection helpers # ----------------------- SCRIPT_PATH="$(abs "$0")" SCRIPT_IN_BASE=0 case "$SCRIPT_PATH" in "$BASEDIR"|"$BASEDIR"/*) SCRIPT_IN_BASE=1 ;; esac append_abs() { local p="${1:-}" [ -n "$p" ] || return 0 if [ "${SCRIPT_IN_BASE:-0}" -eq 1 ] && [ "$p" = "$SCRIPT_PATH" ]; then dbg "Skipping script itself: $p" return 0 fi case "$p" in "$WORKDIR"/*) dbg "Skipping workdir path: $p"; return 0 ;; esac if is_special "$p"; then dbg "Skipping special file: $p" return 0 fi printf '%s\0' "$p" >>"$CAND_ABS" dbg "Appended candidate: $p" } # ----------------------- # Collect candidates # ----------------------- log "Collecting candidate files..." if [ "$DOM_COUNT" -gt 0 ]; then while IFS= read -r D; do D="$(abs "$D")" dbg "Processing domain root: $D" mkdir -p "$D/public_html" "$D/cgi-bin" "$D/logs" "$D/stats" "$D/awstats/.data" "$D/public_ftp/incoming" 2>>"$LOG_STABLE" || true # Find all items (files, links, directories) at the top level of the domain root. find "$D" -mindepth 1 -maxdepth 1 -print0 2>>"$LOG_STABLE" | while IFS= read -r -d '' item; do base_item="$(basename "$item")" # Exclude standard directories that are handled by later, specific loops. case "$base_item" in public_html|cgi-bin|logs|stats|awstats|public_ftp|.htpasswd) dbg "Skipping standard path in main loop: $item" continue ;; esac # If the item is a file or symlink at the domain root, add it as a candidate. if [ -f "$item" ] || [ -L "$item" ]; then append_abs "$item" # If the item is a directory not in our exclusion list (e.g., 'data'), # recursively find and add all files within it. elif [ -d "$item" ]; then dbg "Scanning non-standard directory for files: $item" find "$item" -type f -print0 2>>"$LOG_STABLE" | while IFS= read -r -d '' f; do append_abs "$f" done fi done # Now, process the standard directories with their specific rules. PH="$D/public_html" if [ -d "$PH" ]; then find "$PH" -type f -print0 2>>"$LOG_STABLE" | while IFS= read -r -d '' f; do base="$(basename "$f")" case "$base" in index.html|index.htm|index.php) continue ;; esac case "$f" in "$PH/.well-known"|"${PH}/.well-known/"*) continue ;; esac append_abs "$f" done fi if [ -d "$D/logs" ]; then find "$D/logs" -type f -print0 2>>"$LOG_STABLE" | while IFS= read -r -d '' f; do append_abs "$f"; done fi if [ -d "$D/stats" ]; then find "$D/stats" -type f -print0 2>>"$LOG_STABLE" | while IFS= read -r -d '' f; do append_abs "$f"; done fi if [ -d "$D/awstats" ]; then find "$D/awstats" -mindepth 1 -maxdepth 1 -type f -print0 2>>"$LOG_STABLE" | while IFS= read -r -d '' f; do bn="$(basename "$f")" case "$bn" in .data|awstats.pl) continue ;; esac case "$bn" in awstats*.conf) continue ;; esac append_abs "$f" done fi if [ -d "$D/public_ftp" ]; then find "$D/public_ftp" -type f -print0 2>>"$LOG_STABLE" | while IFS= read -r -d '' f; do case "$f" in "$D/public_ftp/incoming"|"${D}/public_ftp/incoming/"*) continue ;; esac append_abs "$f" done fi done < "$DOMAINS_FILE" fi # Top-level under BASEDIR find "$BASEDIR" -mindepth 1 -maxdepth 1 -print0 2>>"$LOG_STABLE" | while IFS= read -r -d '' top; do [ -e "$top" ] || continue case "$top" in "$BASEDIR/.ssh"|"$BASEDIR/public_html"|"$BASEDIR/domains") continue ;; esac skip=0 if [ "$DOM_COUNT" -gt 0 ]; then while IFS= read -r D; do [ "$top" = "$D" ] && { skip=1; break; }; done < "$DOMAINS_FILE" fi [ "$skip" -eq 1 ] && continue if [ -f "$top" ] || [ -L "$top" ]; then append_abs "$top" elif [ -d "$top" ]; then find "$top" -type f -print0 2>>"$LOG_STABLE" | while IFS= read -r -d '' f; do append_abs "$f"; done fi done # If no candidates, exit gracefully if [ ! -s "$CAND_ABS" ]; then log "No candidates found; nothing to do." exit 0 fi # ----------------------- # Build relative list and compute totals # ----------------------- : >"$CAND_REL" while IFS= read -r -d '' abs; do case "$abs" in "$BASEDIR") rel='.' ;; "$BASEDIR"/*) rel="${abs#$BASEDIR/}" ;; *) rel="$abs" ;; esac printf '%s\0' "$rel" >>"$CAND_REL" done <"$CAND_ABS" CAND_COUNT=$(tr -cd '\0' <"$CAND_REL" | wc -c || echo 0) TOTAL_BYTES=0 while IFS= read -r -d '' p; do if [ -e "$p" ]; then bytes=$(path_size "$p" 2>/dev/null || echo 0) case "$bytes" in ''|*[!0-9]*) bytes=0 ;; esac TOTAL_BYTES=$((TOTAL_BYTES + bytes)) fi done <"$CAND_ABS" log "Candidates: $CAND_COUNT (~$(human "$TOTAL_BYTES"))" log "Sample (first 20):" i=0 while IFS= read -r -d '' p && [ $i -lt 20 ]; do printf ' %s\n' "$p" >>"$LOG_STABLE" 2>/dev/null || true i=$((i+1)) done <"$CAND_ABS" # Dry-run: report and exit if [ "$APPLY" -eq 0 ]; then log "DRY-RUN: no changes made. Re-run with --apply to execute." exit 0 fi # Confirm destructive action printf 'Type EXACTLY CONFIRM to proceed: ' read -r CONFIRM [ "$CONFIRM" = "CONFIRM" ] || { err "Aborted by user"; exit 1; } # ----------------------- # Archiving (same behavior) # ----------------------- ARCHIVE_TGZ="$BASEDIR/min_trim_${TIMESTAMP}.tar.gz" ARCHIVE_ZIP="$BASEDIR/min_trim_${TIMESTAMP}.zip" log "Archiving to: $ARCHIVE_TGZ (tar) or $ARCHIVE_ZIP (zip)" if [ "$TAR_GNU" -eq 1 ]; then log "Using GNU tar with --remove-files" set +e (cd "$BASEDIR" && tar --null --ignore-failed-read -czf "$ARCHIVE_TGZ" --remove-files -T "$CAND_REL") 2>>"$LOG_STABLE" TAR_RC=$? set -e if [ $TAR_RC -ne 0 ]; then warn "tar returned $TAR_RC; some files may not have been archived/removed." fi if [ -f "$ARCHIVE_TGZ" ]; then tar -tzf "$ARCHIVE_TGZ" >/dev/null 2>&1 || warn "Archive verification failed: $ARCHIVE_TGZ" else err "Archive was not created: $ARCHIVE_TGZ" fi else STAGE="$WORKDIR/stage" mkdir -p "$STAGE" log "Staging files to $STAGE" while IFS= read -r -d '' rel; do src="$BASEDIR/$rel" dst="$STAGE/$rel" mkdir -p "$(dirname "$dst")" 2>>"$LOG_STABLE" || true if [ -e "$src" ]; then if ! safe_move "$src" "$dst"; then err "Failed to stage $src" fi fi done <"$CAND_REL" if [ "$USE_ZIP" -eq 1 ] && [ "$HAS_ZIP" -eq 1 ]; then (cd "$STAGE" && zip -r -q "$ARCHIVE_ZIP" .) 2>>"$LOG_STABLE" || warn "zip failed" [ -f "$ARCHIVE_ZIP" ] && unzip -t "$ARCHIVE_ZIP" >/dev/null 2>&1 || warn "ZIP verification failed" else (cd "$STAGE" && tar -czf "$ARCHIVE_TGZ" .) 2>>"$LOG_STABLE" || warn "tar failed" [ -f "$ARCHIVE_TGZ" ] && tar -tzf "$ARCHIVE_TGZ" >/dev/null 2>&1 || warn "tar.gz verification failed" fi rm -rf -- "$STAGE" fi # Residual cleanup log "Residual cleanup..." while IFS= read -r -d '' p; do if [ -e "$p" ]; then dbg "Removing residual: $p" rm -rf -- "$p" 2>>"$LOG_STABLE" || warn "Failed to remove residual: $p" fi done <"$CAND_ABS" find "$BASEDIR" -depth -type d -empty -delete 2>>"$LOG_STABLE" || true # ----------------------- # Recreate skeletons # ----------------------- log "Recreating skeletons..." if [ -s "$DOMAINS_FILE" ]; then while IFS= read -r D; do D="$(abs "$D")" dbg "Recreating for domain: $D" mkdir -p "$D/public_html" "$D/cgi-bin" "$D/logs" "$D/stats" "$D/awstats/.data" "$D/public_ftp/incoming" 2>>"$LOG_STABLE" || true PH="$D/public_html" if [ ! -f "$PH/index.html" ] && [ ! -f "$PH/index.php" ]; then cat > "$PH/index.html" <<'HTML' <!doctype html><html><head><meta charset="utf-8"><title>Placeholder</title></head> <body><h1>Placeholder</h1><p>Minimal skeleton active. Upload your site to public_html.</p></body></html> HTML chmod 644 "$PH/index.html" 2>>"$LOG_STABLE" || true fi if [ -d "$D/logs" ]; then find "$D/logs" -mindepth 1 -print0 | xargs -0 -r rm -rf -- 2>>"$LOG_STABLE" || true fi if [ -d "$D/stats" ]; then find "$D/stats" -mindepth 1 -print0 | xargs -0 -r rm -rf -- 2>>"$LOG_STABLE" || true fi if [ -d "$D/awstats" ]; then find "$D/awstats" -mindepth 1 -maxdepth 1 -print0 2>>"$LOG_STABLE" | while IFS= read -r -d '' f; do bn="$(basename "$f")" case "$bn" in .data|awstats.pl|awstats*.conf) continue ;; esac rm -rf -- "$f" 2>>"$LOG_STABLE" || true done fi done <"$DOMAINS_FILE" fi # ----------------------- # Apply permissions # ----------------------- log "Applying permissions..." find "$BASEDIR" -type d -print0 | xargs -0 -r chmod 0755 -- 2>>"$LOG_STABLE" || true find "$BASEDIR" -type f -print0 | xargs -0 -r chmod 0644 -- 2>>"$LOG_STABLE" || true find "$BASEDIR" -type d -name "cgi-bin" -print0 2>>"$LOG_STABLE" | while IFS= read -r -d '' d; do find "$d" -type f -print0 | xargs -0 -r chmod 0755 -- 2>>"$LOG_STABLE" || true done find "$BASEDIR" -type f -name "awstats.pl" -print0 | xargs -0 -r chmod 0755 -- 2>>"$LOG_STABLE" || true find "$BASEDIR" -type d -path "*/public_ftp" -print0 | xargs -0 -r chmod 0711 -- 2>>"$LOG_STABLE" || true find "$BASEDIR" -type d -path "*/public_ftp/incoming" -print0 | xargs -0 -r chmod 0733 -- 2>>"$LOG_STABLE" || true if [ -d "$BASEDIR/.ssh" ]; then chmod 700 "$BASEDIR/.ssh" 2>>"$LOG_STABLE" || true find "$BASEDIR/.ssh" -type f -print0 | xargs -0 -r chmod 0600 -- 2>>"$LOG_STABLE" || true fi find "$BASEDIR" -type f \( -name ".htaccess" -o -name ".htpasswd" \) -print0 | xargs -0 -r chmod 0644 -- 2>>"$LOG_STABLE" || true find "$BASEDIR" -type f \( -iname "wp-config.php" -o -iname "config.php" -o -iname ".env" \) -print0 | xargs -0 -r chmod 0640 -- 2>>"$LOG_STABLE" || true find "$BASEDIR" -type f \( -iname "*token*" -o -iname "*secret*" -o -name "id_*" \) -print0 | xargs -0 -r chmod 0600 -- 2>>"$LOG_STABLE" || true # ----------------------- # Finalization # ----------------------- log "Completed successfully." exit 0
URL: https://ib.bsb.br/coolice-reset-script