#!/bin/bash # ============================================================================================== # ============================= Recreate Shares ================================================ # ============================================================================================== # # PURPOSE # ───────────────────────────────────────────────────────────────────────────── # Creates share directories on the correct disks after a fresh unRAID install # or disk rebuild. Reads all .cfg files from /boot/config/shares/ and creates # the corresponding directories on each disk listed in the shareInclude setting. # The array must be started before running — /mnt/user must be mounted. # # Typically run on HOST2 after a full disk replacement or fresh install where # share folders were lost but /boot/config/shares/*.cfg files were restored. # # ============================================================================================== # OPERATIONAL MODEL # ============================================================================================== # # For each share .cfg file: # 1. Reads shareInclude= to determine which disks own this share # 2. Creates /mnt/diskN/ShareName/ on each listed disk if it doesn't exist # 3. Places a .recovery marker file in /mnt/user/ShareName/ via the union filesystem # # .recovery Marker # Signals to rsync.sh that this is a fresh share with no existing data. # rsync.sh checks for .recovery before running with --delete: # .recovery present → rsync WITHOUT --delete (new files only, nothing removed) # .recovery absent → rsync WITH --delete (normal mirror mode) # # Self-cleaning: after the first successful rsync the source side has no .recovery # file, so the second nightly run deletes it from the mirror, restoring normal # --delete behaviour automatically. No manual cleanup needed. # # ============================================================================================== # OPERATIONAL SAFEGUARDS # ============================================================================================== # # Single Instance Lock # acquire_lock prevents duplicate runs placing duplicate .recovery markers. # # Root Required # mkdir on /mnt/diskN requires root. # # Array Mount Check # Exits cleanly if the array is not started — /mnt/user not mounted means # all share operations would fail silently. # # Per-Disk Guards # Missing disks are skipped with a warning and the rest continue — a single # offline disk does not abort the full run. # # Empty Config Guard # Warns if no share .cfg files are found — catches the case where # /boot/config/shares/ was not restored. # # Notification Validated # validate_unraid_cmd confirms the notify script is present before use. # # ============================================================================================== # RUNTIME MODES # ============================================================================================== # # recreate_shares.sh # Read all .cfg files, create share directories, place .recovery markers. # # recreate_shares.sh --dry-run # Show what directories and markers would be created. No changes. # # recreate_shares.sh --log # Verbose output per share and per disk. # # recreate_shares.sh --status # Show which shares exist in config and which directories exist on disk. # # ============================================================================================== SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/../load_config.sh" parse_args "$@" SHARE_CFG_DIR="/boot/config/shares" MARKER_FILE=".recovery" CREATED=() SKIPPED=() FAILED=() # ============================================================================================== # ━━━ Setup ━━━ # ============================================================================================== if [[ "$EUID" -ne 0 ]]; then error "Must be run as root — mkdir on /mnt/diskN requires root" exit 1 fi validate_unraid_cmd \ "/usr/local/emhttp/plugins/dynamix/scripts/notify" \ "" "" \ "unRAID notify script" || warn "unRAID notify script not found — native notifications disabled" acquire_lock # detect_hosts() sets MY_ID — used in summary detect_hosts [[ "$DRY_RUN" == true ]] && warn "DRY RUN — no directories or markers will be created" # ============================================================================================== # ━━━ Status ━━━ # ============================================================================================== if [[ "$SHOW_STATUS" == true ]]; then echo "" echo "━━━━━ $ICON_SUMMARY STATUS ━━━━━" echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)" echo "$ICON_DISK Cfg dir: $SHARE_CFG_DIR" echo "$ICON_DISK Marker: $MARKER_FILE" echo "" if ! mountpoint -q /mnt/user; then warn "Array: NOT STARTED — /mnt/user not mounted" else echo " Array: started ✅" fi echo "" echo "━━━ Share Config Files ━━━" CFG_COUNT=0 for cfg in "$SHARE_CFG_DIR"/*.cfg; do [[ ! -f "$cfg" ]] && continue (( CFG_COUNT++ )) SHARE_NAME=$(basename "$cfg" .cfg) INCLUDE=$(grep '^shareInclude=' "$cfg" 2>/dev/null | cut -d'"' -f2) MARKER_EXISTS="no" [[ -f "/mnt/user/${SHARE_NAME}/${MARKER_FILE}" ]] && MARKER_EXISTS="yes" echo " $ICON_DISK $SHARE_NAME — disks: ${INCLUDE:-none} — recovery marker: $MARKER_EXISTS" done [[ "$CFG_COUNT" -eq 0 ]] && warn "No .cfg files found in $SHARE_CFG_DIR" echo "━━━━━━━━━━━━━━━━━━━━━━━" exit 0 fi # ============================================================================================== # ━━━ Pre-flight ━━━ # ============================================================================================== # Array must be started — /mnt/user must be mounted if ! mountpoint -q /mnt/user; then error "Array is not started — /mnt/user is not mounted" warn "Start the array in the unRAID UI before running this script" notify "Recreate shares failed on $(hostname) — array is not started" \ "Recreate Shares" "warning" exit 1 fi log "Array is started — /mnt/user is mounted ✅" # Check share cfg directory exists and has files if [[ ! -d "$SHARE_CFG_DIR" ]]; then error "Share config directory not found: $SHARE_CFG_DIR" error "Is /boot mounted? Is this the correct server?" exit 1 fi CFG_FILES=("$SHARE_CFG_DIR"/*.cfg) if [[ ! -f "${CFG_FILES[0]}" ]]; then warn "No share .cfg files found in $SHARE_CFG_DIR" warn "Nothing to recreate — are share configs present on /boot?" exit 0 fi log "Found ${#CFG_FILES[@]} share .cfg file(s) in $SHARE_CFG_DIR" # ============================================================================================== # ━━━ Recreate Shares ━━━ # ============================================================================================== echo "" echo "━━━ $ICON_DISK Recreate Shares — $MY_ID ━━━" echo "" for cfg in "${CFG_FILES[@]}"; do [[ ! -f "$cfg" ]] && continue SHARE_NAME=$(basename "$cfg" .cfg) INCLUDE=$(grep '^shareInclude=' "$cfg" 2>/dev/null | cut -d'"' -f2) echo "━━━ $ICON_DISK $SHARE_NAME ━━━" if [[ -z "$INCLUDE" ]]; then warn "$SHARE_NAME — no shareInclude in .cfg — skipping" SKIPPED+=("$SHARE_NAME") echo "" continue fi log "$SHARE_NAME — disks: $INCLUDE" SHARE_OK=true DIRS_CREATED=0 DIRS_EXISTED=0 # Create directory on each listed disk IFS=',' read -ra DISKS <<< "$INCLUDE" for disk in "${DISKS[@]}"; do disk="${disk// /}" # trim whitespace [[ -z "$disk" ]] && continue DISK_MOUNT="/mnt/${disk}" DISK_PATH="${DISK_MOUNT}/${SHARE_NAME}" # Verify disk is mounted if ! mountpoint -q "$DISK_MOUNT" 2>/dev/null; then warn "$disk not mounted — skipping $DISK_PATH" continue fi if [[ -d "$DISK_PATH" ]]; then log "$disk/$SHARE_NAME already exists — skipping" (( DIRS_EXISTED++ )) else if [[ "$DRY_RUN" == true ]]; then warn "DRY RUN — would create: $DISK_PATH" (( DIRS_CREATED++ )) elif mkdir -p "$DISK_PATH"; then log "Created: $DISK_PATH ✅" (( DIRS_CREATED++ )) else error "Failed to create: $DISK_PATH" SHARE_OK=false fi fi done # Place .recovery marker via /mnt/user (union filesystem) MARKER_PATH="/mnt/user/${SHARE_NAME}/${MARKER_FILE}" if [[ "$SHARE_OK" == true ]]; then if [[ -f "$MARKER_PATH" ]]; then log ".recovery marker already exists in $SHARE_NAME" CREATED+=("$SHARE_NAME") elif [[ "$DRY_RUN" == true ]]; then warn "DRY RUN — would place marker: $MARKER_PATH" CREATED+=("$SHARE_NAME") elif touch "$MARKER_PATH" 2>/dev/null; then log "Marker placed: $MARKER_PATH ✅" CREATED+=("$SHARE_NAME") else warn "$SHARE_NAME — could not place .recovery marker" warn "Share directory may not be visible via /mnt/user yet" warn "Try: touch /mnt/user/${SHARE_NAME}/.recovery manually after verifying share" SKIPPED+=("$SHARE_NAME") fi else FAILED+=("$SHARE_NAME") fi [[ "$DIRS_CREATED" -gt 0 ]] && warn "$SHARE_NAME — created $DIRS_CREATED dir(s) on disk" [[ "$DIRS_EXISTED" -gt 0 ]] && log "$SHARE_NAME — $DIRS_EXISTED dir(s) already existed" echo "" done # ============================================================================================== # ━━━ Summary ━━━ # ============================================================================================== echo "━━━━━ $ICON_SUMMARY RECREATE SHARES SUMMARY ━━━━━" echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)" echo "" [[ ${#CREATED[@]} -gt 0 ]] && warn "Created + marked: ${CREATED[*]}" [[ ${#SKIPPED[@]} -gt 0 ]] && warn "Skipped: ${SKIPPED[*]}" [[ ${#FAILED[@]} -gt 0 ]] && echo "$ICON_ERROR Failed: ${FAILED[*]}" echo "" echo " Created: ${#CREATED[@]}" echo " Skipped: ${#SKIPPED[@]}" echo " Failed: ${#FAILED[@]}" if [[ "$DRY_RUN" == false && ${#CREATED[@]} -gt 0 ]]; then echo "" echo "━━━ Next Steps ━━━" echo " 1. $ICON_HEALTH Verify shares are visible in unRAID UI" echo " 2. $ICON_SYNC Run initial rsync push from HOST1 → HOST2" echo " rsync.sh will detect .recovery markers and skip --delete" echo " Normal --delete mode restores automatically on second nightly run" echo " 3. $ICON_GEAR No manual config changes needed — markers self-clean ✅" fi echo "" if [[ "$DRY_RUN" == true ]]; then warn "DRY RUN — no changes made" elif [[ ${#FAILED[@]} -gt 0 ]]; then echo "$ICON_ERROR Status: completed with failures" notify "Recreate shares failed on $(hostname) ($MY_ID) — failed: ${FAILED[*]}" \ "Recreate Shares" "warning" exit 1 else echo "$ICON_DONE Status: done — ${#CREATED[@]} created, ${#SKIPPED[@]} skipped" fi echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"