From 5fdeff3f9aec28a32a4d9addabff5c00120af906 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 7 Jun 2026 23:15:12 +0000 Subject: [PATCH] manage_users.sh: inline link prompts on add/scope-change - prompt_add_links: shared helper loops asking for /srv paths until blank Enter, creates symlinks via docker exec, skips bad paths - cmd_add: offers linked-dir prompt right after user creation - cmd_scope: offers linked-dir prompt after a scope change - menu_links: tighter submenu (list + add loop + remove) replacing the old menu_symlinks; called from Modify option 5 - Scope prompts now show the leading / so the user only types the rest - Note on delete: symlinks on disk survive user deletion (by design) https://claude.ai/code/session_01UZus2Q9gNTfUdqSMrhuX29 --- tools/manage_users.sh | 257 ++++++++++++++++++++---------------------- 1 file changed, 125 insertions(+), 132 deletions(-) diff --git a/tools/manage_users.sh b/tools/manage_users.sh index c6e8f83..e6c4697 100644 --- a/tools/manage_users.sh +++ b/tools/manage_users.sh @@ -21,17 +21,19 @@ # # If FB_PATH=~/drives/data1: # / → full access (all of ~/drives/data1) -# /music → ~/drives/data1/music only -# /docs/bob → ~/drives/data1/docs/bob only +# /alice → alice's own subdir (pair with linked dirs below) +# /music → music subdir only # -# ── Multi-directory access via symlinks ─────────────────────────────────────── -# FileBrowser follows symlinks inside a user's scope dir. -# Use "Manage linked directories" in the Modify menu to create symlinks -# inside the user's scope that point to other directories under /srv. +# ── Multi-directory access via linked directories ──────────────────────────── +# FileBrowser follows symlinks inside the scope dir. +# Give a user scope=/alice, then link extra folders into /alice: # -# Example: user alice has scope /alice -# link /alice/music → /srv/music (alice sees a "music" folder in her root) -# link /alice/photos → /srv/photos (alice sees a "photos" folder too) +# /alice/music → /srv/music (alice sees "music" in her root) +# /alice/photos → /srv/photos (alice sees "photos" too) +# +# This script creates those symlinks via "docker exec", so they live in +# the bind-mount on the host as well (target is /srv/... so it appears +# broken from the host, but resolves correctly inside the container). # set -Eeuo pipefail @@ -75,7 +77,7 @@ validate_password() { } validate_scope() { - [[ "$1" == /* ]] || { errmsg "Scope must start with / (e.g. / or /music or /docs/bob)"; return 1; } + [[ "$1" == /* ]] || { errmsg "Scope must start with / (e.g. / or /alice or /music)"; return 1; } } # prompt_password VARNAME [label] @@ -136,9 +138,8 @@ default_perms() { "modify":true,"delete":true,"share":false,"download":true}' } -# ── Docker helpers for symlink management ───────────────────────────────────── +# ── Docker helpers ───────────────────────────────────────────────────────────── -# Read container name from docker-compose.yml next to this script, or default. get_container_name() { local _compose="$SCRIPT_DIR/docker-compose.yml" if [[ -f "$_compose" ]]; then @@ -150,135 +151,115 @@ get_container_name() { } check_container() { - local _c="$1" require_cmds docker local _running - _running=$(docker inspect --format='{{.State.Running}}' "$_c" 2>/dev/null || echo "false") + _running=$(docker inspect --format='{{.State.Running}}' "$1" 2>/dev/null || echo "false") [[ "$_running" == "true" ]] \ - || { errmsg "Container '$_c' is not running. Start it: docker compose up -d"; return 1; } + || { errmsg "Container '$1' is not running. Start it: docker compose up -d"; return 1; } } -# List symlinks in /srv inside the container. -list_scope_symlinks() { +# Print symlinks inside /srv, one per line. +list_links() { local _c="$1" _scope="$2" - local _dir="/srv$_scope" - local _out - _out=$(docker exec "$_c" find "$_dir" -maxdepth 1 -type l \ + docker exec "$_c" find "/srv$_scope" -maxdepth 1 -type l \ -exec sh -c 'printf " %-24s→ %s\n" "$(basename "$1")" "$(readlink "$1")"' _ {} \; \ - 2>/dev/null | sort) || true - if [[ -n "$_out" ]]; then - echo "$_out" - else - echo " (none)" - fi + 2>/dev/null | sort || true } -# Interactively add a symlink inside a user's scope dir. -add_scope_symlink() { - local _c="$1" _scope="$2" +# ── prompt_add_links SCOPE ──────────────────────────────────────────────────── +# Loop: ask for /srv source paths, create symlinks, blank line to finish. +# Shared by cmd_add (offered inline) and menu_links (add option). +prompt_add_links() { + local _scope="$1" + local _c + _c=$(get_container_name) + check_container "$_c" || return 1 + local _scope_dir="/srv$_scope" - - echo - echo " Add a directory link inside this user's scope." - echo " Both paths are inside /srv (the FileBrowser root)." - echo - echo " Example: source=/music → user sees a 'music' folder in their root." - echo " source=/shared/reports name=reports" - echo - - local _src="" - read -r -p " Source path in /srv (e.g. /music): " _src - _src="/${_src#/}" # ensure leading / - _src="${_src%/}" # strip trailing / - validate_scope "$_src" || return 1 - - # Check that target actually exists inside the container - if ! docker exec "$_c" test -e "/srv$_src" 2>/dev/null; then - errmsg "/srv$_src does not exist inside the container." - local _anyway="" - read -r -p " Create the link anyway? (it will appear broken until the dir exists) [y/N]: " _anyway - [[ "${_anyway,,}" == "y" ]] || { echo " Aborted."; return 0; } - fi - - local _default_name - _default_name=$(basename "$_src") - local _link_name="" - read -r -p " Name shown to user in their folder [$_default_name]: " _link_name - _link_name="${_link_name:-$_default_name}" - [[ "$_link_name" =~ ^[a-zA-Z0-9._-]+$ ]] \ - || { errmsg "Invalid name '$_link_name'. Use letters, numbers, dots, hyphens, underscores."; return 1; } - - local _link_path="$_scope_dir/$_link_name" - local _target="/srv$_src" - - # Ensure the scope dir exists inside the container docker exec "$_c" mkdir -p "$_scope_dir" >/dev/null 2>&1 || true - # Handle existing entry at link path - if docker exec "$_c" test -e "$_link_path" 2>/dev/null; then - local _ov="" - echo - errmsg "'$_link_name' already exists in $_scope_dir." - read -r -p " Overwrite? [y/N]: " _ov - [[ "${_ov,,}" == "y" ]] || { echo " Aborted."; return 0; } - docker exec "$_c" rm -rf "$_link_path" - fi - echo - echo " Creating: $_link_path → $_target" - docker exec "$_c" ln -s "$_target" "$_link_path" - ok "'$_link_name' linked to $_target" - echo " User will see '$_link_name' as a folder inside their scope." + echo " Link directories into '$_scope_dir'." + echo " Enter a /srv path for each directory to add — blank line when done." + echo + + while true; do + local _src="" + read -r -p " /srv path [done]: /" _src + [[ -n "$_src" ]] || break + + _src="/${_src#/}" # normalise leading slash + _src="${_src%/}" # strip trailing slash + + if ! docker exec "$_c" test -e "/srv$_src" 2>/dev/null; then + errmsg "/srv$_src does not exist inside the container. Skipping." + continue + fi + + local _default_name + _default_name=$(basename "$_src") + local _link_name="" + read -r -p " Name in user's folder [$_default_name]: " _link_name + _link_name="${_link_name:-$_default_name}" + + if ! [[ "$_link_name" =~ ^[a-zA-Z0-9._-]+$ ]]; then + errmsg "Invalid name. Use letters, numbers, dots, hyphens, underscores." + continue + fi + + local _link_path="$_scope_dir/$_link_name" + local _target="/srv$_src" + + if docker exec "$_c" test -e "$_link_path" 2>/dev/null; then + errmsg "'$_link_name' already exists at $_link_path — skipping. Use Remove to clear it first." + continue + fi + + docker exec "$_c" ln -s "$_target" "$_link_path" + ok "'$_link_name' → $_target" + done } -# Interactively remove a symlink from a user's scope dir. -remove_scope_symlink() { - local _c="$1" _scope="$2" - local _scope_dir="/srv$_scope" - - echo - echo " Current links in $_scope_dir:" - list_scope_symlinks "$_c" "$_scope" - echo - local _link_name="" - read -r -p " Link name to remove (Enter to cancel): " _link_name - [[ -n "$_link_name" ]] || { echo " Cancelled."; return 0; } - - local _link_path="$_scope_dir/$_link_name" - - # Only remove symlinks — refuse to delete regular files/dirs - if ! docker exec "$_c" test -L "$_link_path" 2>/dev/null; then - errmsg "'$_link_name' is not a symlink. Refusing to delete." - return 1 - fi - - docker exec "$_c" rm "$_link_path" - ok "Link '$_link_name' removed from $_scope_dir." -} - -# Submenu for managing symlinks for one user. -menu_symlinks() { - local _username="$1" _scope="$2" +# ── Linked-directory submenu (shown from Modify option 5) ──────────────────── +menu_links() { + local _scope="$1" local _c _c=$(get_container_name) check_container "$_c" || return 1 while true; do - banner "Linked dirs: $_username (scope: $_scope)" - echo " Container: ${DIM}$_c${R} Root: ${DIM}/srv${R}" + banner "Linked directories (scope: $_scope)" + echo " ${DIM}Symlinks inside /srv${_scope} — visible as folders in FileBrowser${R}" echo - echo " Symlinks visible to $_username in /srv$_scope:" - list_scope_symlinks "$_c" "$_scope" + local _links + _links=$(list_links "$_c" "$_scope") + if [[ -n "$_links" ]]; then + echo "$_links" + else + echo " (none)" + fi echo - echo " 1 Add a linked directory" + echo " 1 Add linked directories" echo " 2 Remove a linked directory" echo " 0 Back" echo local _ch="" read -r -p " Choice: " _ch case "$_ch" in - 1) add_scope_symlink "$_c" "$_scope" || true ;; - 2) remove_scope_symlink "$_c" "$_scope" || true ;; + 1) prompt_add_links "$_scope" || true ;; + 2) + local _link_name="" + echo + read -r -p " Link name to remove: " _link_name + [[ -n "$_link_name" ]] || continue + local _link_path="/srv${_scope}/${_link_name}" + if ! docker exec "$_c" test -L "$_link_path" 2>/dev/null; then + errmsg "'$_link_name' is not a symlink — refusing to delete." + continue + fi + docker exec "$_c" rm "$_link_path" + ok "'$_link_name' removed." + ;; 0) break ;; *) errmsg "Invalid choice." ;; esac @@ -310,8 +291,7 @@ cmd_add() { echo echo " ${B}Username:${R} letters, numbers, hyphens, underscores only. No dots or @." echo " ${B}Password:${R} min 8 chars, at least 1 letter and 1 number." - echo " ${B}Scope:${R} FileBrowser supports ONE path per user." - echo " For multi-dir access use Modify → Linked directories." + echo " ${B}Scope:${R} one path per user — use linked dirs for multi-folder access." echo read -r -p " Username: " _username local _adm="" @@ -325,10 +305,11 @@ cmd_add() { echo echo " Scope examples:" echo " / full access (all of FB_PATH)" - echo " /alice alice's private subdir" + echo " /$_username user's own private subdir (recommended with linked dirs)" echo " /music music subdir only" echo - read -r -p " Scope for '$_username': " _scope + read -r -p " Scope for '$_username': /" _scope + _scope="/${_scope#/}" fi validate_scope "$_scope" || return 1 @@ -356,7 +337,15 @@ cmd_add() { api_post "/api/users" "$_body" >/dev/null echo ok "User '$_username' created | scope: $_scope | admin: $_is_admin" - echo " Tip: use Modify → Linked directories to give access to more folders." + + # Offer to add linked directories inline + if command -v docker &>/dev/null; then + local _do_links="" + read -r -p " Add linked directories for '$_username'? [y/N]: " _do_links + if [[ "${_do_links,,}" == "y" ]]; then + prompt_add_links "$_scope" || true + fi + fi } # ── cmd: delete ─────────────────────────────────────────────────────────────── @@ -378,7 +367,7 @@ cmd_delete() { [[ "${_c,,}" == "y" ]] || { echo " Aborted."; return 0; } api_delete "/api/users/$_uid" >/dev/null ok "User '$_username' deleted." - echo " Note: any symlinks created in their scope dir still exist on disk." + echo " ${DIM}Note: symlinks in their scope dir still exist on disk if you want to reuse them.${R}" } # ── cmd: passwd ─────────────────────────────────────────────────────────────── @@ -428,10 +417,10 @@ cmd_scope() { if [[ -z "$_new_scope" ]]; then echo echo " Current scope: $_old_scope" - echo " Note: existing symlinks in the old scope dir are not moved automatically." - echo " Examples: / /music /docs/bob /alice" + echo " ${DIM}Existing symlinks in the old scope dir are not moved automatically.${R}" echo - read -r -p " New scope: " _new_scope + read -r -p " New scope: /" _new_scope + _new_scope="/${_new_scope#/}" fi validate_scope "$_new_scope" || return 1 @@ -440,6 +429,15 @@ cmd_scope() { api_put "/api/users/$_uid" "$_body" >/dev/null echo ok "Scope updated for '$_username': $_old_scope → $_new_scope" + + # Offer to add links into the new scope + if command -v docker &>/dev/null && [[ "$_new_scope" != "/" ]]; then + local _do_links="" + read -r -p " Add linked directories into '$_new_scope'? [y/N]: " _do_links + if [[ "${_do_links,,}" == "y" ]]; then + prompt_add_links "$_new_scope" || true + fi + fi } # ── cmd: rename ─────────────────────────────────────────────────────────────── @@ -516,7 +514,7 @@ menu_modify() { echo " 2 Change password" echo " 3 Change scope (file path)" echo " 4 Toggle admin status" - echo " 5 Manage linked directories ${DIM}(symlinks for multi-dir access)${R}" + echo " 5 Linked directories ${DIM}(add/remove multi-folder symlinks)${R}" echo " 0 Back" echo local _ch="" @@ -553,7 +551,7 @@ menu_modify() { errmsg "Toggle failed." fi ;; - 5) menu_symlinks "$_cur" "$_scope" || true ;; + 5) menu_links "$_scope" || true ;; 0) break ;; *) errmsg "Invalid choice." ;; esac @@ -576,17 +574,12 @@ One-shot usage: manage_users.sh rename manage_users.sh info -Scope is relative to /srv inside the container (= FB_PATH on the host): - / full access to everything under FB_PATH - /alice alice's own subdir - /music music subdir only - +Scope is relative to /srv inside the container (= FB_PATH on the host). Username: letters, numbers, hyphens, underscores only. No dots or @. Password: min 8 chars, at least one letter and one number. -Multi-directory access: use the interactive menu → Modify → Linked -directories. This creates symlinks inside the user's scope dir so they -see multiple folders without scope being set to /. +Multi-directory access: use the interactive menu — linked directories +are offered automatically when you add a user or change their scope. Override URL: FB_URL=http://localhost:8085 ./manage_users.sh EOF