znc-setup.sh 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. #!/bin/bash
  2. # Put converted channel logs into a ZNC account's log folder and let pisg (a different Unix user) read it.
  3. #
  4. # bash scripts/znc-setup.sh show what it found and what it would do (changes nothing)
  5. # bash scripts/znc-setup.sh --apply do it
  6. # Run it as the ZNC account (no sudo needed) or with sudo; with sudo it uses a new group "pisg" instead.
  7. #
  8. # What --apply does, in order (nothing is deleted or overwritten):
  9. # 1. finds ZNC's log folder for the account (the log module's moddata/log ),
  10. # 2. makes a group "pisg" holding the pisg user; the folders leading to the logs get group-execute
  11. # (a path through them, no listing) and ONLY the channel folders being imported get group-read.
  12. # Other networks and private-message logs stay owner-only. Files ZNC adds to those channel
  13. # folders later are world-readable by ZNC's own umask, so they follow the folder's access,
  14. # 3. copies the staged logs ~/znc-import/#channel/*.log into <network>/<channel>/ ("cp -n": an
  15. # existing day file is never replaced),
  16. # 4. links ~/znc-logs/<channel> to those folders, so pisg.cfg never has to know ZNC's layout.
  17. #
  18. # Settings (environment): PISG_USER (required: the unix user that runs pisg), ZNC_HOME (default ~/.znc),
  19. # STAGE (default: ~/znc-import of that user), LINKS (~/znc-logs of that user), NETWORK (only needed if the account has several networks or no log yet).
  20. set -euo pipefail
  21. ZNC_HOME=${ZNC_HOME:-$HOME/.znc}
  22. PISG_USER=${PISG_USER:-}
  23. [ -n "$PISG_USER" ] || { echo "Set PISG_USER to the unix user that runs pisg, e.g. PISG_USER=pisguser bash $0"; exit 1; }
  24. PISG_HOME=$(getent passwd "$PISG_USER" | cut -d: -f6)
  25. STAGE=${STAGE:-$PISG_HOME/znc-import}
  26. LINKS=${LINKS:-$PISG_HOME/znc-logs}
  27. APPLY=0; [ "${1:-}" = "--apply" ] && APPLY=1
  28. # As root: a dedicated group "pisg". As the ZNC account itself (no sudo needed): the existing group
  29. # "users", which both accounts are in (it has to be a group the ZNC account already belongs to).
  30. if [ "$(id -u)" = 0 ]; then ROOTMODE=1; GROUP=${GROUP:-pisg}; else ROOTMODE=0; GROUP=${GROUP:-users}; fi
  31. if [ "$ROOTMODE" = 0 ] && [ "$APPLY" = 1 ] && [ ! -O "$ZNC_HOME" ]; then
  32. echo "Run this as the ZNC account (owner of $ZNC_HOME), or with sudo."; exit 1
  33. fi
  34. say() { printf '%s\n' "$*"; }
  35. run() { if [ "$APPLY" = 1 ]; then "$@"; else say " would run: $*"; fi; }
  36. # 1. where does the log module write?
  37. mapfile -t ROOTS < <(find "$ZNC_HOME" -type d -path '*/moddata/log' 2>/dev/null | sort)
  38. if [ -n "${LOGROOT:-}" ]; then ROOTS=("$LOGROOT"); fi
  39. say "ZNC folder: $ZNC_HOME"
  40. if [ "${#ROOTS[@]}" -ne 1 ]; then
  41. say "Found ${#ROOTS[@]} log folders (need exactly one):"; printf ' %s\n' "${ROOTS[@]:-}"
  42. say "Load the log module first (/msg *status LoadMod log), or set LOGROOT=/path/to/moddata/log"
  43. exit 2
  44. fi
  45. LOGROOT=${ROOTS[0]}
  46. say "log module folder: $LOGROOT"
  47. case "$LOGROOT" in
  48. */networks/*/moddata/log) BASE=$LOGROOT ;; # network scope: $WINDOW/...
  49. */users/*/moddata/log) # user scope: $NETWORK/$WINDOW/...
  50. if [ -z "${NETWORK:-}" ]; then
  51. mapfile -t NETS < <(find "$LOGROOT" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | sort)
  52. if [ "${#NETS[@]}" -eq 1 ]; then NETWORK=${NETS[0]}
  53. else say "Networks found in it: ${NETS[*]:-none}"; say "Pass NETWORK=<name> (say something in a channel first if there is none yet)"; exit 2; fi
  54. fi
  55. BASE=$LOGROOT/$NETWORK ;;
  56. *) say "Global-scope log module (folder holds \$USER/\$NETWORK/...): pass LOGROOT= pointing at the network folder"; exit 2 ;;
  57. esac
  58. say "network folder: $BASE"
  59. say "staged logs: $STAGE"
  60. ls -d "$STAGE"/'#'* >/dev/null 2>&1 || { say "nothing staged in $STAGE"; exit 2; }
  61. # 2. group and permissions
  62. say; say "== access for $PISG_USER"
  63. if [ "$ROOTMODE" = 1 ]; then
  64. getent group "$GROUP" >/dev/null || run groupadd "$GROUP"
  65. id -nG "$PISG_USER" | tr ' ' '\n' | grep -qx "$GROUP" || run usermod -aG "$GROUP" "$PISG_USER"
  66. else
  67. id -nG "$PISG_USER" | tr ' ' '\n' | grep -qx "$GROUP" || { say "$PISG_USER is not in group $GROUP; run with sudo instead"; exit 1; }
  68. fi
  69. say " group used for read access: $GROUP"
  70. d=$LOGROOT
  71. CHAIN=()
  72. while [ "$d" != "/" ] && [ "$d" != "$(dirname "$ZNC_HOME")" ]; do CHAIN+=("$d"); d=$(dirname "$d"); done
  73. CHAIN+=("$(dirname "$ZNC_HOME")")
  74. for d in "${CHAIN[@]}" "$BASE"; do
  75. [ -d "$d" ] || continue
  76. run chgrp "$GROUP" "$d"; run chmod g+x "$d" # a way through, no listing
  77. done
  78. # Only the channel folders named below become readable. Other networks and private-message logs
  79. # under the same log folder keep their owner-only permissions.
  80. # 3. copy the staged logs
  81. say; say "== logs"
  82. ZNC_OWNER=$(stat -c %U "$LOGROOT" 2>/dev/null || id -un)
  83. [ "$APPLY" = 1 ] && mkdir -p "$BASE" && chown "$ZNC_OWNER:$GROUP" "$BASE" && chmod 2710 "$BASE"
  84. for s in "$STAGE"/'#'*; do
  85. name=$(basename "$s")
  86. existing=$(find "$BASE" -mindepth 1 -maxdepth 1 -type d -iname "$name" 2>/dev/null | head -1 || true)
  87. dest=${existing:-$BASE/$name}
  88. n=$(find "$s" -name '*.log' | wc -l)
  89. say " $name: $n day files -> $dest $([ -n "$existing" ] && echo '(folder already there)' || echo '(new folder)')"
  90. if [ "$APPLY" = 1 ]; then
  91. mkdir -p "$dest"; chown "$ZNC_OWNER:$GROUP" "$dest"; chmod 2750 "$dest"
  92. cp --update=none --no-preserve=mode,ownership "$s"/*.log "$dest"/
  93. chown "$ZNC_OWNER:$GROUP" "$dest"/*.log
  94. find "$dest" -type f -name '*.log' -exec chmod 640 {} +
  95. fi
  96. # 4. link for pisg (lower case, no #)
  97. link=$LINKS/$(echo "${name#\#}" | tr 'A-Z' 'a-z')
  98. if [ "$APPLY" = 1 ] && [ "$ROOTMODE" = 1 ]; then
  99. mkdir -p "$LINKS"; chown "$PISG_USER" "$LINKS"
  100. ln -sfn "$dest" "$link" # (no chown -h: on this system it follows the link and changes the target)
  101. elif [ "$APPLY" = 1 ]; then
  102. say " (links are made by $PISG_USER afterwards: $link -> $dest)"
  103. else
  104. say " would link $link -> $dest"
  105. fi
  106. done
  107. if [ "$APPLY" = 1 ] && [ "$ROOTMODE" = 1 ]; then
  108. say; say "== check: what $PISG_USER can now see"
  109. for l in "$LINKS"/*; do
  110. printf ' %s: ' "$l"
  111. setpriv --reuid="$PISG_USER" --regid="$PISG_USER" --groups="$GROUP" ls "$l/" | wc -l | tr '\n' ' '; say "files readable"
  112. done
  113. say; say "Done. New group membership reaches $PISG_USER's cron jobs on their next run."
  114. elif [ "$APPLY" = 1 ]; then
  115. say; say "Done. Tell $PISG_USER it finished: the links and the pisg run come next."
  116. else
  117. say; say "Nothing changed. Run again with --apply to do it."
  118. fi