Files

246 lines
7.9 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
# POS: share nfs-server — Manage the NFS kernel server (status, share/unshare exports, enable/disable)
# POS_SUBCMDS: status share unshare list reload enable disable menu
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
source "$(dirname "$0")/../lib/notify.sh" 2>/dev/null || source "$(dirname "$0")/notify.sh"
source "$(dirname "$0")/../lib/share-lib.sh" 2>/dev/null || source "$(dirname "$0")/share-lib.sh"
command -v exportfs &>/dev/null || err "exportfs not found (install nfs-kernel-server)"
# Env seam (testable): which exports file is managed.
EXPORTS_FILE="${EXPORTS_FILE:-/etc/exports}"
usage() {
cat <<EOF
Usage: pos share nfs-server <command> [args]
Manage the NFS kernel server (nfs-kernel-server, /etc/exports).
Commands:
status Show server status + current exports
share <path> [client] Add or update an export and reload
(default client: *(rw,sync,no_subtree_check))
unshare <path> Remove an export and reload
list Show current exports (exportfs -v)
reload Re-apply /etc/exports after hand edits
enable Start nfs-server and enable it on boot
disable Stop nfs-server and disable it on boot
menu Interactive browser (folder → client preset → share)
Run without arguments to open the interactive menu.
Clients are /etc/exports entries — restrict them to your trusted network:
pos share nfs-server share /mnt/hdd '100.64.0.0/10(rw,sync,no_subtree_check)' # Tailscale CGNAT
pos share nfs-server share /mnt/hdd '10.10.0.0/24(rw,sync,no_subtree_check)' # WireGuard
pos share nfs-server share /mnt/backups '192.168.1.0/24(ro,sync,no_subtree_check)'
Examples:
pos share nfs-server status
pos share nfs-server share /mnt/hdd
pos share nfs-server list
pos share nfs-server unshare /mnt/hdd
pos share nfs-server menu
EOF
exit 0
}
cmd="${1:-}"
case "$cmd" in
-h|--help) usage ;;
""|menu|status|share|unshare|list|reload|enable|disable) ;;
*) err "Unknown command '$cmd' (see --help)" ;;
esac
require_root_dir() {
local path="$1"
case "$path" in
/*) ;;
*) err "Path must be absolute: $path" ;;
esac
[ -d "$path" ] || err "Path not found: $path"
}
cmd_status() {
if systemctl is-active --quiet nfs-server 2>/dev/null; then
ok "nfs-server: running"
else
warn "nfs-server: not running (enable with 'pos share nfs-server enable')"
fi
echo
section "Exports"
exportfs -v 2>/dev/null || echo " (none)"
}
cmd_share() {
local path="$1" client="${2:-*(rw,sync,no_subtree_check)}"
require_root_dir "$path"
if [ "$client" = "*(rw,sync,no_subtree_check)" ]; then
warn "Generic export '$client' — ANY client can mount $path. Restrict it, e.g.:"
echo " pos share nfs-server share $path '100.64.0.0/10(rw,sync,no_subtree_check)'"
echo " pos share nfs-server share $path '10.10.0.0/24(rw,sync,no_subtree_check)'"
echo
fi
local tmp
tmp="$(mktemp)"
awk -v p="$path" '$1 != p' "$EXPORTS_FILE" > "$tmp"
echo "$path $client" >> "$tmp"
sudo cp "$tmp" "$EXPORTS_FILE"
rm -f "$tmp"
sudo exportfs -ra
log "Exported: $path $client"
notify_send "NFS share added: $path $client"
# Advisory post-checks (never abort the share operation).
if ! share_service_active nfs-server; then
share_offer_fix "The nfs-server service is not running" \
sudo systemctl enable --now nfs-server
fi
if share_ufw_blocks_ports '2049|111|\bnfs\b'; then
warn "ufw is active but has no NFS rule — clients will be blocked."
share_offer_fix "Allow NFS through ufw" sudo ufw allow 2049/tcp
fi
}
cmd_unshare() {
local path="$1"
require_root_dir "$path"
if ! awk -v p="$path" '$1 == p {found=1} END {exit !found}' "$EXPORTS_FILE"; then
warn "No export for $path in $EXPORTS_FILE"
exit 0
fi
local tmp
tmp="$(mktemp)"
awk -v p="$path" '$1 != p' "$EXPORTS_FILE" > "$tmp"
sudo cp "$tmp" "$EXPORTS_FILE"
rm -f "$tmp"
sudo exportfs -ra
log "Removed export: $path"
notify_send "NFS share removed: $path"
}
cmd_list() {
exportfs -v 2>/dev/null || echo "No exports"
}
cmd_reload() {
sudo exportfs -ra
log "NFS exports reloaded"
}
cmd_enable() {
sudo systemctl enable --now nfs-server
log "nfs-server enabled (starts on boot)"
notify_send "NFS server enabled"
}
cmd_disable() {
sudo systemctl disable --now nfs-server
log "nfs-server disabled (will not start on boot)"
notify_send "NFS server disabled"
}
# ── Interactive menu flows ─────────────────────────────────────
menu_pick_folder() { # stdout: folder path · rc 1 cancelled
local idx dir cand
local -a cands=()
if mapfile -t cands < <(share_folder_candidates) && [ "${#cands[@]}" -gt 0 ]; then
if idx="$(share_pick "Share which folder?" "${cands[@]}")"; then
cand="${cands[$((idx - 1))]}"
dir="${cand%% (*}" # strip "(mounted fstype)" annotation
[ -d "$dir" ] || { warn "Folder vanished: $dir"; return 1; }
echo "$dir"
return 0
fi
return 1
fi
dir="$(share_ask_value "Folder to share (absolute path)")" || return 1
[ -n "$dir" ] || { warn "No folder given"; return 1; }
echo "$dir"
}
menu_share() {
local dir idx spec
dir="$(menu_pick_folder)" || return 1
local -a specs=(
"100.64.0.0/10(rw,sync,no_subtree_check) — Tailscale CGNAT range"
"10.10.0.0/24(rw,sync,no_subtree_check) — WireGuard subnet"
"192.168.1.0/24(rw,sync,no_subtree_check) — LAN subnet"
"192.168.1.0/24(ro,sync,no_subtree_check) — LAN read-only"
"* (rw,sync,no_subtree_check) — ANY client (unsafe)"
)
if idx="$(share_pick "Client access for ${dir}" "${specs[@]}")"; then
spec="$(sed 's/[[:space:]]*—.*//' <<<"${specs[$((idx - 1))]}")"
[ "$spec" = "*" ] && spec="*(rw,sync,no_subtree_check)"
else
spec="$(share_ask_value "Client spec (e.g. 10.10.0.0/24(rw,sync))" "")" || return 1
[ -n "$spec" ] || spec="*(rw,sync,no_subtree_check)"
fi
cmd_share "$dir" "$spec"
}
menu_unshare() {
local idx path
local -a paths=()
if mapfile -t paths < <(awk 'NF > 0 && $1 !~ /^#/ {print $1}' "$EXPORTS_FILE" 2>/dev/null) &&
[ "${#paths[@]}" -gt 0 ]; then
idx="$(share_pick "Remove which export?" "${paths[@]}")" || return 1
path="${paths[$((idx - 1))]}"
else
path="$(share_ask_value "Exported path to remove")" || return 1
[ -n "$path" ] || return 1
fi
cmd_unshare "$path"
}
run_menu() {
share_menu_guard || exit 1
while true; do
local choice
choice="$(share_menu_run "NFS server" \
"Show status (service + exports)" \
"Share a folder" \
"Remove an export" \
"List current exports" \
"Reload exports after hand edits" \
"Enable service on boot" \
"Disable service")" || return 0
case "$choice" in
1) cmd_status ;;
2) menu_share ;;
3) menu_unshare ;;
4) cmd_list ;;
5) cmd_reload ;;
6) cmd_enable ;;
7) cmd_disable ;;
esac
done
}
case "$cmd" in
""|menu)
run_menu
exit 0
;;
status) cmd_status ;;
share)
[ $# -ge 2 ] || err "Usage: pos share nfs-server share <path> [client]"
cmd_share "$2" "${3:-*(rw,sync,no_subtree_check)}"
;;
unshare)
[ $# -ge 2 ] || err "Usage: pos share nfs-server unshare <path>"
cmd_unshare "$2"
;;
list) cmd_list ;;
reload) cmd_reload ;;
enable) cmd_enable ;;
disable) cmd_disable ;;
esac