#!/bin/bash # ============================================================================================== # ============================= share_setup.sh ================================================= # ============================================================================================== # # PURPOSE # ───────────────────────────────────────────────────────────────────────────── # Creates missing Unraid shares on the mirror during partnership onboarding. # Safe to re-run — existing shares are never modified, only missing ones created. # # For each path in the owner's sync share lists (daily, weekly, critical, # intermediate): # - Extracts the top-level Unraid share name # - Skips if the remote already has a .cfg for it # - Uses the local share .cfg as a template: substitutes the remote's detected # appdata pool, clears disk include/exclude (disk layouts differ per server) # - Writes the .cfg to remote /boot/config/shares/ and mkdir -p the directory # - For sub-paths (e.g. appdata-Fallback/Critical-Data), ensures the subdir # exists after the top-level share is in place # # Pool detection: reads the remote's appdata.cfg to find its cache pool name, # so appdata-type shares land on the right pool without hardcoding. # # Media shares (shareUseCache=no in local .cfg) are created as array-only. # Remote admin assigns disk sets from the Unraid UI after onboarding. # # ============================================================================================== # USAGE # ============================================================================================== # # share_setup.sh # Create missing shares on the configured mirror. Skips existing. # # share_setup.sh --dry-run # Show what would be created without writing anything. # # ============================================================================================== set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/../load_config.sh" detect_hosts parse_args "$@" # ── Resolve mirror ──────────────────────────────────────────────────────────────────────────── [[ -z "${REMOTE_SERVER_NAME:-}" ]] && { error "Cannot determine mirror hostname — check HOST* in master.conf"; exit 1; } MIRROR="$REMOTE_SERVER_NAME" MIRROR_IP=$(resolve_tailscale_ip "$MIRROR") [[ -z "$MIRROR_IP" ]] && { error "Cannot resolve $MIRROR Tailscale IP — is Tailscale running?"; exit 1; } _ssh() { ssh -i "$SSH_KEY" -o ConnectTimeout=10 -o BatchMode=yes -o StrictHostKeyChecking=no root@"$MIRROR_IP" "$@"; } _scp() { scp -i "$SSH_KEY" -o ConnectTimeout=10 -o StrictHostKeyChecking=no "$@"; } # ── Detect remote appdata pool ──────────────────────────────────────────────────────────────── REMOTE_POOL=$(_ssh "grep -oP '(?<=shareCachePool=\")[^\"]+' /boot/config/shares/appdata.cfg 2>/dev/null | head -1" 2>/dev/null || true) REMOTE_POOL="${REMOTE_POOL:-cache}" log "Remote appdata pool: $REMOTE_POOL" # ── Collect paths from all sync share arrays ────────────────────────────────────────────────── declare -a ALL_PATHS=() _add_array() { local varname="$1" local -n _ref="$varname" 2>/dev/null || return 0 for entry in "${_ref[@]+"${_ref[@]}"}"; do [[ -n "$entry" ]] && ALL_PATHS+=("${entry%%|*}") done } _add_array "${MY_ID}_DAILY_SYNC_SHARES" _add_array "${MY_ID}_WEEKLY_SYNC_SHARES" _add_array "${MY_ID}_CRITICAL_SYNC_SHARES" _add_array "${MY_ID}_INTERMEDIATE_SYNC_SHARES" # ── Separate top-level shares from sub-paths ────────────────────────────────────────────────── declare -A SEEN_TOP=() declare -A SEEN_SUB=() declare -a TOP_SHARES=() declare -a SUB_PATHS=() for path in "${ALL_PATHS[@]+"${ALL_PATHS[@]}"}"; do [[ "$path" != /mnt/user/* ]] && continue rel="${path#/mnt/user/}" top="${rel%%/*}" [[ -z "${SEEN_TOP[$top]+_}" ]] && { TOP_SHARES+=("$top"); SEEN_TOP["$top"]=1; } if [[ "$rel" == */* && -z "${SEEN_SUB[$path]+_}" ]]; then SUB_PATHS+=("$path"); SEEN_SUB["$path"]=1 fi done [[ ${#TOP_SHARES[@]} -eq 0 ]] && { echo "No shares to process."; exit 0; } # ── Build .cfg for a share ──────────────────────────────────────────────────────────────────── # Uses local .cfg as template: substitutes pool, clears disk sets, drops comment. # Falls back to a minimal cfg inferred from share name when no local .cfg exists. _make_cfg() { local share_name="$1" local local_cfg="/boot/config/shares/${share_name}.cfg" if [[ -f "$local_cfg" ]]; then sed \ -e "s|^shareCachePool=\"[^\"]*\"|shareCachePool=\"${REMOTE_POOL}\"|" \ -e 's|^shareInclude="[^"]*"|shareInclude=""|' \ -e 's|^shareExclude="[^"]*"|shareExclude=""|' \ -e '/^shareComment=/d' \ "$local_cfg" else local use_cache="no" pool="" allocator="mostfree" case "$share_name" in appdata*|Media_Server*) use_cache="yes"; pool="$REMOTE_POOL"; allocator="highwater" ;; esac printf 'shareUseCache="%s"\nshareCachePool="%s"\nshareCachePool2=""\n' "$use_cache" "$pool" printf 'shareCOW="auto"\nshareAllocator="%s"\nshareInclude=""\nshareExclude=""\n' "$allocator" printf 'shareFloor="0"\nshareExport="e"\nshareCaseSensitive="auto"\n' printf 'shareSecurity="private"\nshareReadList=""\nshareWriteList=""\n' printf 'shareVolsizelimit=""\nshareExportNFS="-"\nshareExportNFSFsid="0"\n' printf 'shareSecurityNFS="public"\nshareHostListNFS=""\n' fi } # ── Process each top-level share ────────────────────────────────────────────────────────────── TMPFILE=$(mktemp) trap 'rm -f "$TMPFILE"' EXIT CREATED=0 SKIPPED=0 for share in "${TOP_SHARES[@]}"; do existing=$(_ssh "[[ -f '/boot/config/shares/${share}.cfg' ]] && echo yes || echo no" 2>/dev/null) if [[ "$existing" == "yes" ]]; then log " exists: $share" (( SKIPPED++ )) || true continue fi if [[ "$DRY_RUN" == true ]]; then local_cfg="/boot/config/shares/${share}.cfg" if [[ -f "$local_cfg" ]]; then use_cache=$(grep -oP '(?<=shareUseCache=")[^"]+' "$local_cfg" || echo "?") echo " [dry-run] CREATE $share (useCache=$use_cache → pool: $REMOTE_POOL)" else echo " [dry-run] CREATE $share (no local cfg — minimal fallback)" fi (( CREATED++ )) || true continue fi _make_cfg "$share" > "$TMPFILE" _scp "$TMPFILE" "root@${MIRROR_IP}:/boot/config/shares/${share}.cfg" >/dev/null _ssh "mkdir -p /mnt/user/${share}" >/dev/null echo " Created: $share ✅" (( CREATED++ )) || true done # ── Ensure sub-path directories exist ──────────────────────────────────────────────────────── for path in "${SUB_PATHS[@]+"${SUB_PATHS[@]}"}"; do if [[ "$DRY_RUN" == true ]]; then echo " [dry-run] MKDIR $path" continue fi _ssh "mkdir -p '$path'" 2>/dev/null && log " mkdir: $path" || true done # ── Summary ─────────────────────────────────────────────────────────────────────────────────── echo "" echo " Shares: $CREATED created, $SKIPPED already existed on $MIRROR" [[ "$DRY_RUN" == true ]] && echo " (dry-run — nothing written)"