Finishes the pass: every script now documents its safeguards, and the deliberate absences in the sourced libraries are recorded so they are not "corrected" later.
334 lines
13 KiB
Bash
Executable File
334 lines
13 KiB
Bash
Executable File
#!/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.
|
|
#
|
|
# ==============================================================================================
|
|
# DESIGN PRINCIPLES
|
|
# ==============================================================================================
|
|
#
|
|
# The .cfg Files Are the Source of Truth
|
|
# Directories are recreated from /boot/config/shares/*.cfg rather than from a list in
|
|
# Varaverk's conf. Those files are Unraid's own record of what a share is and which disks
|
|
# it spans — anything Varaverk maintained separately would be a second copy free to drift.
|
|
#
|
|
# Create Only, Never Delete
|
|
# Missing directories are created and existing ones left alone. This runs after a rebuild,
|
|
# when the operator's mental model of what should exist may be out of date; removing
|
|
# anything on that basis is how a recovery step becomes a data loss step.
|
|
#
|
|
# Array Must Be Started
|
|
# Refuses to run without /mnt/user mounted. Creating share directories against an
|
|
# unmounted array writes them into the underlying root filesystem, which then shadows the
|
|
# real shares once the array does mount.
|
|
#
|
|
# ==============================================================================================
|
|
# 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
|
|
# platform_require_cmd confirms the notify script is present before use.
|
|
#
|
|
# ==============================================================================================
|
|
# CONFIGURATION
|
|
# ==============================================================================================
|
|
#
|
|
# No Varaverk config vars. Everything is read from Unraid's own share definitions:
|
|
#
|
|
# /boot/config/shares/*.cfg
|
|
# One file per share. shareInclude names the disks the share spans; the directory is
|
|
# created on each of them. A share with no shareInclude spans all array disks.
|
|
#
|
|
# /mnt/user
|
|
# Must be mounted — see Array Must Be Started above.
|
|
#
|
|
# Deliberately not driven by HOST*_*_SYNC_SHARES: this recreates every share the server
|
|
# knows about, not only the ones Varaverk syncs.
|
|
#
|
|
# ==============================================================================================
|
|
# 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
|
|
|
|
platform_require_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
|
|
|
|
log "$ICON_GEAR Config: cfg-dir=${SHARE_CFG_DIR} marker=${MARKER_FILE}"
|
|
[[ "$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
|
|
echo "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
|
|
echo "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
|
|
echo "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 "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" |