Headers claimed protections the code never had, and several destructive paths had no guard against a collapsed config value.
268 lines
12 KiB
Bash
Executable File
268 lines
12 KiB
Bash
Executable File
#!/bin/bash
|
||
# ==============================================================================================
|
||
# ============================= Bulk Permissions Repair ========================================
|
||
# ==============================================================================================
|
||
#
|
||
# PURPOSE
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
# Applies correct ownership and permissions to one or more specific paths.
|
||
# Targeted repair — faster than media_shares_permissions.sh, which processes
|
||
# every configured share. Use after failed transfers that left root:root ownership,
|
||
# containers writing as root before PUID/PGID was fixed, manual file copies, or
|
||
# new shares that need permissions applied before the next nightly run.
|
||
#
|
||
# Counts files with wrong ownership before fixing. A high count on a recently
|
||
# written share means a container has wrong PUID/PGID — add PUID=99 PGID=100
|
||
# to its Docker template. Common culprits: SABnzbd, qBittorrent, slskd.
|
||
#
|
||
# Side effect worth knowing: this applies owner/mode unconditionally, and chown/chmod
|
||
# restamp an inode's ctime even when the value doesn't change. sonarr/radarr/lidarr_cleanup.sh
|
||
# gate orphan deletion on ctime, so running this over a whole media root resets that clock
|
||
# and pauses orphan collection there for *_ORPHAN_AGE days. That is why the nightly
|
||
# media_shares_permissions.sh applies its passes conditionally. Harmless for the targeted
|
||
# repairs this tool is meant for — worth remembering before pointing it at an entire share.
|
||
#
|
||
# ==============================================================================================
|
||
# OPERATIONAL MODEL
|
||
# ==============================================================================================
|
||
#
|
||
# For each path given on the command line:
|
||
#
|
||
# 1. Path guard — refuse anything shallower than two components
|
||
# 2. Existence check — a missing path is a failure for that entry, not the run
|
||
# 3. Report scale — file count, dir count, total size, so the operator sees the job size
|
||
# 4. Count entries with wrong ownership (the diagnostic number in the summary)
|
||
# 5. chown -R PERMISSIONS_OWNER across the path
|
||
# 6. find -type d → chmod PERMISSIONS_DIR_MODE
|
||
# 7. find -type f → chmod PERMISSIONS_FILE_MODE
|
||
#
|
||
# Unlike the nightly media_shares_permissions.sh, steps 5–7 are unconditional — see the
|
||
# ctime note in PURPOSE above for why that distinction matters.
|
||
#
|
||
# ==============================================================================================
|
||
# DESIGN PRINCIPLES
|
||
# ==============================================================================================
|
||
#
|
||
# Unconditional by Design, Unlike the Nightly Job
|
||
# media_shares_permissions.sh applies its passes conditionally to protect ctime as an
|
||
# age signal for arr orphan collection. This tool deliberately does not: it exists to
|
||
# repair paths that are known-wrong, where correctness matters more than preserving a
|
||
# clock. That is exactly why it is a targeted manual tool and not a scheduled one.
|
||
#
|
||
# Permissions Model
|
||
# Directories (PERMISSIONS_DIR_MODE, default 755):
|
||
# Owner (nobody) — rwx enter, list, create files
|
||
# Group (users) — r-x enter and list
|
||
# Others — r-x Samba guests can browse
|
||
# Files (PERMISSIONS_FILE_MODE, default 664):
|
||
# Owner (nobody) — rw read + write
|
||
# Group (users) — rw arrs can import and rename
|
||
# Others — r Samba guests can read
|
||
# No execute bit — media files are never executable
|
||
#
|
||
# Separate Passes
|
||
# Directories and files are chmod'd in separate find passes. A combined pass
|
||
# with mode 664 would wrongly strip the execute bit from directories, making
|
||
# them untraversable.
|
||
#
|
||
# ==============================================================================================
|
||
# OPERATIONAL SAFEGUARDS
|
||
# ==============================================================================================
|
||
#
|
||
# Root Required
|
||
# chown requires root — exits immediately if not running as root.
|
||
#
|
||
# Path Existence Check
|
||
# Each path is verified before processing — missing paths log an error and
|
||
# are skipped rather than silently passing.
|
||
#
|
||
# Silent on Success
|
||
# Only failures and the wrong-owner diagnostic produce visible output.
|
||
#
|
||
# ==============================================================================================
|
||
# CONFIGURATION
|
||
# ==============================================================================================
|
||
#
|
||
# master.conf
|
||
#
|
||
# PERMISSIONS_OWNER
|
||
# Owner applied to all paths. (default: nobody:users)
|
||
#
|
||
# PERMISSIONS_DIR_MODE
|
||
# chmod mode for directories. (default: 755)
|
||
#
|
||
# PERMISSIONS_FILE_MODE
|
||
# chmod mode for files. (default: 664)
|
||
#
|
||
# ==============================================================================================
|
||
# RUNTIME MODES
|
||
# ==============================================================================================
|
||
#
|
||
# bulk_permissions_repair.sh /path/to/share [/another/path ...]
|
||
# Apply ownership and permissions to each specified path.
|
||
#
|
||
# bulk_permissions_repair.sh /path/to/share --dry-run
|
||
# Show wrong-owner count per path. No chown or chmod applied.
|
||
#
|
||
# bulk_permissions_repair.sh /path/to/share --log
|
||
# Verbose output including per-path file counts and modes applied.
|
||
#
|
||
# ==============================================================================================
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
|
||
source "$SCRIPT_DIR/../load_config.sh"
|
||
|
||
parse_args "$@"
|
||
|
||
# ==============================================================================================
|
||
# ━━━ Setup ━━━
|
||
# ==============================================================================================
|
||
if [[ "$EUID" -ne 0 ]]; then
|
||
error "Must be run as root — chown requires root"
|
||
exit 1
|
||
fi
|
||
|
||
acquire_lock
|
||
|
||
|
||
# detect_hosts() sets MY_ID — used in summary
|
||
detect_hosts
|
||
|
||
if [[ ${#PARSED_ARGS[@]} -eq 0 ]]; then
|
||
error "No paths specified"
|
||
error "Usage: bulk_permissions_repair.sh /path/to/share [/another/path] [--dry-run]"
|
||
exit 1
|
||
fi
|
||
|
||
log "Dir mode: ${PERMISSIONS_DIR_MODE:-755}"
|
||
log "File mode: ${PERMISSIONS_FILE_MODE:-664}"
|
||
log "Owner: $PERMISSIONS_OWNER"
|
||
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no permissions will be changed"
|
||
|
||
# ==============================================================================================
|
||
# ━━━ Apply Permissions ━━━
|
||
# ==============================================================================================
|
||
echo ""
|
||
echo "━━━ $ICON_PERMS Permissions Repair — $MY_ID ━━━"
|
||
|
||
START=$(date +%s)
|
||
PASS=()
|
||
FAIL=()
|
||
TOTAL_WRONG_OWNER=0
|
||
|
||
for share_path in "${PARSED_ARGS[@]}"; do
|
||
[[ -z "$share_path" ]] && continue
|
||
|
||
echo ""
|
||
echo "━━━ $ICON_PERMS $(basename "$share_path") ━━━"
|
||
|
||
# chown -R below. Paths come straight from the command line, so a spacing typo
|
||
# ("/mnt/user /Movies" instead of "/mnt/user/Movies") would hand this a bare top-level
|
||
# directory — and chown -R nobody:users on / or /etc breaks the system outright.
|
||
# Require at least two path components; that still allows a deliberate whole-share
|
||
# repair like /mnt/user while refusing /, /mnt, /etc, /boot and friends.
|
||
_bpr_slashes="${share_path//[^\/]/}"
|
||
if [[ "$share_path" != /* || "${#_bpr_slashes}" -lt 2 ]]; then
|
||
error "$share_path — refusing: expected an absolute path at least 2 levels deep"
|
||
FAIL+=("$share_path")
|
||
continue
|
||
fi
|
||
|
||
if [[ ! -d "$share_path" ]]; then
|
||
error "$share_path — not found"
|
||
FAIL+=("$share_path")
|
||
continue
|
||
fi
|
||
|
||
# Count files for context — warn level so user knows what they're in for on large shares
|
||
FILE_COUNT=$(find "$share_path" -type f 2>/dev/null | wc -l)
|
||
DIR_COUNT=$(find "$share_path" -type d 2>/dev/null | wc -l)
|
||
SIZE=$(du -sh "$share_path" 2>/dev/null | cut -f1)
|
||
warn "$share_path — $FILE_COUNT files, $DIR_COUNT dirs ($SIZE)"
|
||
|
||
# Count files with wrong ownership before fixing — diagnostic
|
||
WRONG_OWNER=$(find "$share_path" \( ! -user nobody -o ! -group users \) \
|
||
2>/dev/null | wc -l)
|
||
if [[ "$WRONG_OWNER" -gt 0 ]]; then
|
||
warn "$WRONG_OWNER file(s) with wrong ownership — fixing..."
|
||
if [[ "$WRONG_OWNER" -gt 500 ]]; then
|
||
warn "High wrong-owner count — check container PUID/PGID settings (should be PUID=99 PGID=100)"
|
||
warn "Common culprits: SABnzbd, qBittorrent, slskd"
|
||
fi
|
||
TOTAL_WRONG_OWNER=$(( TOTAL_WRONG_OWNER + WRONG_OWNER ))
|
||
else
|
||
log "Ownership already correct — applying mode only"
|
||
fi
|
||
|
||
if [[ "$DRY_RUN" == true ]]; then
|
||
warn "DRY RUN — would apply:"
|
||
warn " chown -R $PERMISSIONS_OWNER $share_path"
|
||
warn " find -type d → chmod ${PERMISSIONS_DIR_MODE:-755}"
|
||
warn " find -type f → chmod ${PERMISSIONS_FILE_MODE:-664}"
|
||
PASS+=("$(basename "$share_path")")
|
||
continue
|
||
fi
|
||
|
||
CHOWN_OK=true
|
||
CHMOD_DIR_OK=true
|
||
CHMOD_FILE_OK=true
|
||
|
||
# Apply ownership first
|
||
log "Applying ownership: $PERMISSIONS_OWNER..."
|
||
chown -R "$PERMISSIONS_OWNER" "$share_path" 2>/dev/null || CHOWN_OK=false
|
||
|
||
# Apply directory permissions — separate pass (dirs need execute bit)
|
||
log "Applying directory permissions: ${PERMISSIONS_DIR_MODE:-755}..."
|
||
find "$share_path" -type d \
|
||
-exec chmod "${PERMISSIONS_DIR_MODE:-755}" {} + 2>/dev/null || CHMOD_DIR_OK=false
|
||
|
||
# Apply file permissions — no execute bit on media files
|
||
log "Applying file permissions: ${PERMISSIONS_FILE_MODE:-664}..."
|
||
find "$share_path" -type f \
|
||
-exec chmod "${PERMISSIONS_FILE_MODE:-664}" {} + 2>/dev/null || CHMOD_FILE_OK=false
|
||
|
||
if [[ "$CHOWN_OK" == true && "$CHMOD_DIR_OK" == true && "$CHMOD_FILE_OK" == true ]]; then
|
||
echo "$ICON_UNLOCKED $(basename "$share_path") — permissions applied ✅"
|
||
PASS+=("$(basename "$share_path")")
|
||
else
|
||
error "$(basename "$share_path") — repair failed"
|
||
error " chown: $CHOWN_OK chmod dirs: $CHMOD_DIR_OK chmod files: $CHMOD_FILE_OK"
|
||
FAIL+=("$(basename "$share_path")")
|
||
fi
|
||
done
|
||
|
||
END=$(date +%s)
|
||
|
||
# ==============================================================================================
|
||
# ━━━ Summary ━━━
|
||
# ==============================================================================================
|
||
echo ""
|
||
echo "━━━━━ $ICON_SUMMARY PERMISSIONS REPAIR SUMMARY ━━━━━"
|
||
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
|
||
echo "$ICON_PERMS Dir mode: ${PERMISSIONS_DIR_MODE:-755}"
|
||
echo "$ICON_PERMS File mode: ${PERMISSIONS_FILE_MODE:-664}"
|
||
echo "$ICON_PERMS Owner: $PERMISSIONS_OWNER"
|
||
echo "$ICON_TIME Duration: $(format_duration $(( END - START )))"
|
||
echo ""
|
||
[[ ${#PASS[@]} -gt 0 ]] && log "Pass: ${PASS[*]}"
|
||
[[ ${#FAIL[@]} -gt 0 ]] && echo "$ICON_ERROR Fail: ${FAIL[*]}"
|
||
|
||
if [[ "$TOTAL_WRONG_OWNER" -gt 0 ]]; then
|
||
warn "Total wrong-owner files fixed: $TOTAL_WRONG_OWNER"
|
||
fi
|
||
echo ""
|
||
|
||
if [[ "$DRY_RUN" == true ]]; then
|
||
warn "DRY RUN — no changes made"
|
||
elif [[ ${#FAIL[@]} -gt 0 ]]; then
|
||
echo "$ICON_ERROR Status: SOME REPAIRS FAILED"
|
||
notify "Permissions repair failed on $(hostname) — ${FAIL[*]}" \
|
||
"Permissions Repair" "warning"
|
||
else
|
||
echo "$ICON_DONE Status: done — ${#PASS[@]} path(s) repaired"
|
||
fi
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
|
||
[[ ${#FAIL[@]} -gt 0 ]] && exit 1
|
||
exit 0 |