#!/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. # # ============================================================================================== # DESIGN PRINCIPLES # ============================================================================================== # # 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. # # Notification Validated # platform_require_cmd confirms the notify script is present before use. # # 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 platform_require_cmd \ "/usr/local/emhttp/plugins/dynamix/scripts/notify" \ "" "" \ "unRAID notify script" || warn "unRAID notify script not found — native notifications disabled" # 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") ━━━" 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 log "$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