Merge pull request #63 from outis1one/claude/zealous-feynman-odDrh
Claude/zealous feynman od drh
This commit is contained in:
@@ -104,6 +104,17 @@ CADDY_COMPOSE
|
|||||||
# Example configuration - edit this for your services
|
# Example configuration - edit this for your services
|
||||||
# Uncomment and modify these examples:
|
# Uncomment and modify these examples:
|
||||||
|
|
||||||
|
# ── Port reference ────────────────────────────────────────────────────────────
|
||||||
|
# Services on caddy_net → use the container name and INTERNAL port:
|
||||||
|
# reverse_proxy myservice:80 (what the container listens on inside Docker)
|
||||||
|
#
|
||||||
|
# Accessing a service directly from another machine → use the HOST port:
|
||||||
|
# http://server-ip:8085 (the left side of ports: "8085:80" in compose)
|
||||||
|
#
|
||||||
|
# The ports: mapping is only for direct host access.
|
||||||
|
# Caddy bypasses it entirely and talks container-to-container.
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
# ── Authelia SSO snippet (auto-added by installer if Authelia is installed) ───
|
# ── Authelia SSO snippet (auto-added by installer if Authelia is installed) ───
|
||||||
# (authelia) {
|
# (authelia) {
|
||||||
# forward_auth authelia:9091 {
|
# forward_auth authelia:9091 {
|
||||||
@@ -180,6 +191,11 @@ myservice.example.com {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Port rule:** use the container's *internal* port, not the host-mapped port.
|
||||||
|
If a service has `ports: "8085:80"` in its compose file, Caddy uses `:80`
|
||||||
|
(container-to-container on `caddy_net`). The `8085` is only for direct
|
||||||
|
host access from another machine.
|
||||||
|
|
||||||
## Reloading after edits
|
## Reloading after edits
|
||||||
|
|
||||||
Apply Caddyfile changes without downtime:
|
Apply Caddyfile changes without downtime:
|
||||||
|
|||||||
+131
-558
@@ -1,38 +1,14 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# manage_users.sh — FileBrowser user management via the REST API.
|
# manage_users.sh — Add/remove extra folder access for FileBrowser users.
|
||||||
#
|
#
|
||||||
# Placed in ~/docker/filebrowser/ by the filebrowser installer.
|
# Placed in ~/docker/filebrowser/ by the filebrowser installer.
|
||||||
# Requires: curl, jq (sudo apt install curl jq)
|
# Requires: curl, jq, docker (sudo apt install curl jq)
|
||||||
#
|
#
|
||||||
# Run with no arguments for the interactive menu.
|
# FileBrowser only gives each user one root directory. This script adds
|
||||||
# Pass a command for one-shot use (see --help).
|
# shortcuts inside that directory so a user can reach multiple folders
|
||||||
|
# without getting full access to everything.
|
||||||
#
|
#
|
||||||
# ── Username rules ────────────────────────────────────────────────────────────
|
# Create and manage users through the FileBrowser web UI instead.
|
||||||
# Letters, numbers, hyphens, underscores only. No spaces, dots, or @.
|
|
||||||
# Examples: alice bob-smith data_user2
|
|
||||||
#
|
|
||||||
# ── Password rules ────────────────────────────────────────────────────────────
|
|
||||||
# Minimum 8 characters. No maximum.
|
|
||||||
# Must contain at least one letter and one number.
|
|
||||||
#
|
|
||||||
# ── Directory ─────────────────────────────────────────────────────────────────
|
|
||||||
# Each user has a starting directory — the root they see when they log in.
|
|
||||||
# It is a path inside the container relative to /srv.
|
|
||||||
#
|
|
||||||
# New layout (fb_users named volume + /srv/data bind mount):
|
|
||||||
# /data → full access to all files (FB_PATH on the host)
|
|
||||||
# /alice → alice's folder (must already exist, or be created separately)
|
|
||||||
#
|
|
||||||
# Legacy layout (FB_PATH mounted directly at /srv):
|
|
||||||
# / → full access
|
|
||||||
# /alice → alice's subfolder inside FB_PATH on the host
|
|
||||||
#
|
|
||||||
# ── Additional directories ────────────────────────────────────────────────────
|
|
||||||
# Each user has exactly one starting directory, but you can give them access
|
|
||||||
# to more folders by adding shortcuts inside it.
|
|
||||||
# FileBrowser follows symlinks, so a shortcut placed in /alice pointing to
|
|
||||||
# /data/music lets alice browse "music" alongside her own files.
|
|
||||||
# Shortcuts live in the Docker named volume — no clutter on the host.
|
|
||||||
#
|
#
|
||||||
set -Eeuo pipefail
|
set -Eeuo pipefail
|
||||||
|
|
||||||
@@ -60,42 +36,7 @@ require_cmds() {
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Validation ────────────────────────────────────────────────────────────────
|
# ── Auth ──────────────────────────────────────────────────────────────────────
|
||||||
validate_username() {
|
|
||||||
[[ -n "$1" ]] || { errmsg "Username cannot be empty."; return 1; }
|
|
||||||
[[ "$1" =~ ^[a-zA-Z0-9_-]+$ ]] || {
|
|
||||||
errmsg "Invalid username '$1'. Use only letters, numbers, hyphens, underscores."
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
validate_password() {
|
|
||||||
[[ ${#1} -ge 8 ]] || { errmsg "Password too short (minimum 8 characters)."; return 1; }
|
|
||||||
[[ "$1" =~ [a-zA-Z] ]] || { errmsg "Password must contain at least one letter."; return 1; }
|
|
||||||
[[ "$1" =~ [0-9] ]] || { errmsg "Password must contain at least one number."; return 1; }
|
|
||||||
}
|
|
||||||
|
|
||||||
validate_scope() {
|
|
||||||
[[ "$1" == /* ]] || { errmsg "Directory must start with / (e.g. / or /alice or /music)"; return 1; }
|
|
||||||
}
|
|
||||||
|
|
||||||
# prompt_password VARNAME [label]
|
|
||||||
# Uses nameref (bash 4.3+) so the caller's local variable is set correctly.
|
|
||||||
prompt_password() {
|
|
||||||
local -n _pp_ref="$1"
|
|
||||||
local _label="${2:-New password}"
|
|
||||||
local _p1 _p2
|
|
||||||
while true; do
|
|
||||||
read -r -s -p " $_label: " _p1; echo
|
|
||||||
validate_password "$_p1" || continue
|
|
||||||
read -r -s -p " Confirm: " _p2; echo
|
|
||||||
[[ "$_p1" == "$_p2" ]] || { errmsg "Passwords do not match. Try again."; continue; }
|
|
||||||
_pp_ref="$_p1"
|
|
||||||
break
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Auth — login once, reuse token ────────────────────────────────────────────
|
|
||||||
ensure_token() {
|
ensure_token() {
|
||||||
[[ -n "$TOKEN" ]] && return 0
|
[[ -n "$TOKEN" ]] && return 0
|
||||||
echo
|
echo
|
||||||
@@ -105,10 +46,7 @@ ensure_token() {
|
|||||||
_u="${_u:-admin}"
|
_u="${_u:-admin}"
|
||||||
read -r -s -p " Admin password: " _p; echo
|
read -r -s -p " Admin password: " _p; echo
|
||||||
|
|
||||||
# Build JSON with jq so special characters in passwords don't break the payload
|
|
||||||
_payload=$(jq -n --arg u "$_u" --arg p "$_p" '{username:$u,password:$p}')
|
_payload=$(jq -n --arg u "$_u" --arg p "$_p" '{username:$u,password:$p}')
|
||||||
|
|
||||||
# || true prevents set -e from exiting silently on connection refused
|
|
||||||
_tok=$(curl -s -X POST "$FB_URL/api/login" \
|
_tok=$(curl -s -X POST "$FB_URL/api/login" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d "$_payload") || true
|
-d "$_payload") || true
|
||||||
@@ -120,36 +58,13 @@ ensure_token() {
|
|||||||
ok "Logged in as $_u"
|
ok "Logged in as $_u"
|
||||||
else
|
else
|
||||||
echo " FileBrowser responded: $_tok" >&2
|
echo " FileBrowser responded: $_tok" >&2
|
||||||
die "Login failed — wrong credentials, or FileBrowser is not reachable at $FB_URL"
|
die "Login failed — wrong credentials or FileBrowser not reachable at $FB_URL"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── REST wrappers ─────────────────────────────────────────────────────────────
|
api_get() { curl -sf -X GET "$FB_URL$1" -H "X-Auth: $TOKEN"; }
|
||||||
api_get() { curl -sf -X GET "$FB_URL$1" -H "X-Auth: $TOKEN"; }
|
|
||||||
api_post() { curl -sf -X POST "$FB_URL$1" -H "X-Auth: $TOKEN" \
|
|
||||||
-H "Content-Type: application/json" -d "$2"; }
|
|
||||||
api_put() { curl -sf -X PUT "$FB_URL$1" -H "X-Auth: $TOKEN" \
|
|
||||||
-H "Content-Type: application/json" -d "$2"; }
|
|
||||||
api_delete() { curl -sf -X DELETE "$FB_URL$1" -H "X-Auth: $TOKEN"; }
|
|
||||||
|
|
||||||
find_user() {
|
|
||||||
api_get "/api/users" | jq -r --arg u "$1" '.[] | select(.username==$u)'
|
|
||||||
}
|
|
||||||
|
|
||||||
get_user_id() {
|
|
||||||
local _j
|
|
||||||
_j=$(find_user "$1")
|
|
||||||
[[ -n "$_j" ]] || { errmsg "User '$1' not found."; return 1; }
|
|
||||||
echo "$_j" | jq -r '.id'
|
|
||||||
}
|
|
||||||
|
|
||||||
default_perms() {
|
|
||||||
echo '{"admin":false,"execute":false,"create":true,"rename":true,
|
|
||||||
"modify":true,"delete":true,"share":false,"download":true}'
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Docker helpers ─────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
|
# ── Docker helpers ────────────────────────────────────────────────────────────
|
||||||
get_container_name() {
|
get_container_name() {
|
||||||
local _compose="$SCRIPT_DIR/docker-compose.yml"
|
local _compose="$SCRIPT_DIR/docker-compose.yml"
|
||||||
if [[ -f "$_compose" ]]; then
|
if [[ -f "$_compose" ]]; then
|
||||||
@@ -161,14 +76,13 @@ get_container_name() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
check_container() {
|
check_container() {
|
||||||
require_cmds docker
|
|
||||||
local _running
|
local _running
|
||||||
_running=$(docker inspect --format='{{.State.Running}}' "$1" 2>/dev/null || echo "false")
|
_running=$(docker inspect --format='{{.State.Running}}' "$1" 2>/dev/null || echo "false")
|
||||||
[[ "$_running" == "true" ]] \
|
[[ "$_running" == "true" ]] \
|
||||||
|| { errmsg "Container '$1' is not running. Start it: docker compose up -d"; return 1; }
|
|| die "Container '$1' is not running — start it first: docker compose up -d"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Returns /srv/data if the new named-volume layout is in use, else /srv (legacy).
|
# Returns /srv/data (new named-volume layout) or /srv (legacy bind-mount layout).
|
||||||
get_data_root() {
|
get_data_root() {
|
||||||
local _c="$1"
|
local _c="$1"
|
||||||
if docker exec "$_c" test -d /srv/data 2>/dev/null; then
|
if docker exec "$_c" test -d /srv/data 2>/dev/null; then
|
||||||
@@ -178,10 +92,43 @@ get_data_root() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Print additional directories inside /srv<scope>, one per line.
|
# ── User selection ────────────────────────────────────────────────────────────
|
||||||
list_links() {
|
# Prints a numbered list and returns chosen username + directory via globals.
|
||||||
local _c="$1" _scope="$2"
|
CHOSEN_USER=""
|
||||||
docker exec "$_c" find "/srv$_scope" -maxdepth 1 -type l \
|
CHOSEN_DIR=""
|
||||||
|
|
||||||
|
pick_user() {
|
||||||
|
ensure_token
|
||||||
|
echo
|
||||||
|
|
||||||
|
local _raw
|
||||||
|
_raw=$(api_get "/api/users" | jq -r '.[] | [.username, .scope] | @tsv') || true
|
||||||
|
[[ -n "$_raw" ]] || die "No users returned from FileBrowser."
|
||||||
|
|
||||||
|
local -a _users _dirs
|
||||||
|
local _i=0
|
||||||
|
while IFS=$'\t' read -r _u _s; do
|
||||||
|
_users+=("$_u")
|
||||||
|
_dirs+=("$_s")
|
||||||
|
printf " %2d %-20s %s\n" "$((_i+1))" "$_u" "$_s"
|
||||||
|
((_i++)) || true
|
||||||
|
done <<< "$_raw"
|
||||||
|
echo
|
||||||
|
|
||||||
|
local _pick=""
|
||||||
|
read -r -p " Select user (number): " _pick
|
||||||
|
[[ "$_pick" =~ ^[0-9]+$ ]] || { errmsg "Enter a number."; return 1; }
|
||||||
|
local _idx=$((_pick-1))
|
||||||
|
[[ $_idx -ge 0 && $_idx -lt ${#_users[@]} ]] || { errmsg "Out of range."; return 1; }
|
||||||
|
|
||||||
|
CHOSEN_USER="${_users[$_idx]}"
|
||||||
|
CHOSEN_DIR="${_dirs[$_idx]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Directory listing ─────────────────────────────────────────────────────────
|
||||||
|
list_extras() {
|
||||||
|
local _c="$1" _dir="$2"
|
||||||
|
docker exec "$_c" find "/srv$_dir" -maxdepth 1 -type l \
|
||||||
-exec sh -c '
|
-exec sh -c '
|
||||||
_name=$(basename "$1")
|
_name=$(basename "$1")
|
||||||
_target=$(readlink "$1")
|
_target=$(readlink "$1")
|
||||||
@@ -191,513 +138,139 @@ list_links() {
|
|||||||
' _ {} \; 2>/dev/null | sort || true
|
' _ {} \; 2>/dev/null | sort || true
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── prompt_add_dirs SCOPE ─────────────────────────────────────────────────────
|
# ── Add extra directories ─────────────────────────────────────────────────────
|
||||||
# Loop: user types folder names as they appear in FileBrowser — blank to finish.
|
add_extras() {
|
||||||
# Shared by cmd_add (offered inline) and menu_add_dirs (add option).
|
local _dir="$1" _c="$2" _data_root="$3"
|
||||||
prompt_add_dirs() {
|
local _scope_dir="/srv$_dir"
|
||||||
local _scope="$1"
|
|
||||||
local _c _data_root
|
|
||||||
_c=$(get_container_name)
|
|
||||||
check_container "$_c" || return 1
|
|
||||||
_data_root=$(get_data_root "$_c")
|
|
||||||
|
|
||||||
local _scope_dir="/srv$_scope"
|
|
||||||
if ! docker exec "$_c" test -d "$_scope_dir" 2>/dev/null; then
|
if ! docker exec "$_c" test -d "$_scope_dir" 2>/dev/null; then
|
||||||
echo
|
echo
|
||||||
echo " The directory '$_scope' does not exist in the container yet."
|
echo " The directory '$_dir' does not exist in the container yet."
|
||||||
local _mk=""
|
local _mk=""
|
||||||
read -r -p " Create it now? [y/N]: " _mk
|
read -r -p " Create it now? [y/N]: " _mk
|
||||||
if [[ "${_mk,,}" == "y" ]]; then
|
if [[ "${_mk,,}" == "y" ]]; then
|
||||||
if ! docker exec "$_c" mkdir -p "$_scope_dir" 2>/dev/null; then
|
docker exec "$_c" mkdir -p "$_scope_dir" \
|
||||||
errmsg "Could not create '$_scope_dir' in container (permission denied?)."
|
|| die "Could not create '$_scope_dir' (permission denied?)."
|
||||||
return 1
|
ok "Created '$_dir'"
|
||||||
fi
|
|
||||||
ok "Created '$_scope'"
|
|
||||||
else
|
else
|
||||||
errmsg "Cannot add directories — '$_scope' must exist first."
|
errmsg "Cannot add extras — '$_dir' must exist first."
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo " Type a folder name to add — ? to list available folders, blank when done."
|
echo " Type a folder name to add — ? to list available, blank when done."
|
||||||
echo
|
echo
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
local _src=""
|
local _src=""
|
||||||
read -r -p " Directory to add [done]: " _src
|
read -r -p " Folder to add [done]: " _src
|
||||||
[[ -n "$_src" ]] || break
|
[[ -n "$_src" ]] || break
|
||||||
|
|
||||||
if [[ "$_src" == "?" ]]; then
|
if [[ "$_src" == "?" ]]; then
|
||||||
local _avail
|
local _avail
|
||||||
_avail=$(docker exec "$_c" find "$_data_root" -maxdepth 1 -mindepth 1 \
|
_avail=$(docker exec "$_c" find "$_data_root" -maxdepth 1 -mindepth 1 \
|
||||||
\( -type d -o -type l \) \
|
\( -type d -o -type l \) -not -name ".*" 2>/dev/null \
|
||||||
-not -name ".*" 2>/dev/null \
|
|
||||||
| sed "s|^${_data_root}/||" | sort | tr '\n' ' ') || true
|
| sed "s|^${_data_root}/||" | sort | tr '\n' ' ') || true
|
||||||
echo " Available: ${_avail:-(none — no subdirectories found)}"
|
echo " Available: ${_avail:-(none found)}"
|
||||||
echo
|
echo
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Normalise: strip surrounding slashes
|
_src="${_src#/}"; _src="${_src%/}"
|
||||||
_src="${_src#/}"
|
|
||||||
_src="${_src%/}"
|
|
||||||
[[ -n "$_src" ]] || continue
|
[[ -n "$_src" ]] || continue
|
||||||
|
|
||||||
local _target="$_data_root/$_src"
|
local _target="$_data_root/$_src"
|
||||||
local _link_name
|
local _link_name; _link_name=$(basename "$_src")
|
||||||
_link_name=$(basename "$_src")
|
local _link_path="$_scope_dir/$_link_name"
|
||||||
|
|
||||||
if ! docker exec "$_c" test -e "$_target" 2>/dev/null; then
|
if ! docker exec "$_c" test -e "$_target" 2>/dev/null; then
|
||||||
errmsg "'$_src' not found — type ? to see available folders."
|
errmsg "'$_src' not found — type ? to see available folders."
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local _link_path="$_scope_dir/$_link_name"
|
|
||||||
if docker exec "$_c" test -e "$_link_path" 2>/dev/null; then
|
if docker exec "$_c" test -e "$_link_path" 2>/dev/null; then
|
||||||
errmsg "'$_link_name' already added — use Remove to clear it first."
|
errmsg "'$_link_name' already added."
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! docker exec "$_c" ln -s "$_target" "$_link_path" 2>/dev/null; then
|
if ! docker exec "$_c" ln -s "$_target" "$_link_path" 2>/dev/null; then
|
||||||
errmsg "Failed to add directory (check: docker logs filebrowser)."
|
errmsg "Failed to add '$_link_name' (check: docker logs filebrowser)."
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
if [[ "$_src" == "$_link_name" ]]; then
|
ok "Added '$_link_name'"
|
||||||
ok "Added '$_link_name' to user's view"
|
|
||||||
else
|
|
||||||
ok "Added '$_link_name' ${DIM}(from $_src)${R}"
|
|
||||||
fi
|
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Additional directories submenu (shown from Modify option 5) ──────────────
|
# ── Remove extra directory ────────────────────────────────────────────────────
|
||||||
menu_add_dirs() {
|
remove_extra() {
|
||||||
local _scope="$1"
|
local _dir="$1" _c="$2"
|
||||||
local _c
|
local _scope_dir="/srv$_dir"
|
||||||
_c=$(get_container_name)
|
|
||||||
check_container "$_c" || return 1
|
|
||||||
|
|
||||||
while true; do
|
|
||||||
banner "Additional directories ($_scope)"
|
|
||||||
echo " Extra folders this user can access:"
|
|
||||||
echo
|
|
||||||
local _links
|
|
||||||
_links=$(list_links "$_c" "$_scope")
|
|
||||||
if [[ -n "$_links" ]]; then
|
|
||||||
echo "$_links"
|
|
||||||
else
|
|
||||||
echo " (none)"
|
|
||||||
fi
|
|
||||||
echo
|
|
||||||
echo " 1 Add a directory"
|
|
||||||
echo " 2 Remove a directory"
|
|
||||||
echo " 0 Back"
|
|
||||||
echo
|
|
||||||
local _ch=""
|
|
||||||
read -r -p " Choice: " _ch
|
|
||||||
case "$_ch" in
|
|
||||||
1) prompt_add_dirs "$_scope" || true ;;
|
|
||||||
2)
|
|
||||||
local _link_name=""
|
|
||||||
echo
|
|
||||||
read -r -p " Directory 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 an added directory — refusing to delete."
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
docker exec "$_c" rm "$_link_path"
|
|
||||||
ok "'$_link_name' removed."
|
|
||||||
;;
|
|
||||||
0) break ;;
|
|
||||||
*) errmsg "Invalid choice." ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── cmd: list ─────────────────────────────────────────────────────────────────
|
|
||||||
cmd_list() {
|
|
||||||
ensure_token
|
|
||||||
echo
|
echo
|
||||||
printf " ${B}%-22s %-5s %s${R}\n" "USERNAME" "ADMIN" "DIRECTORY"
|
local _links
|
||||||
printf " %-22s %-5s %s\n" "--------" "-----" "---------"
|
_links=$(list_extras "$_c" "$_dir")
|
||||||
api_get "/api/users" | \
|
if [[ -z "$_links" ]]; then
|
||||||
jq -r '.[] | [.username, (if .perm.admin then "yes" else "no" end), .scope] | @tsv' | \
|
echo " No extras to remove."
|
||||||
while IFS=$'\t' read -r _u _a _s; do
|
return 0
|
||||||
printf " %-22s %-5s %s\n" "$_u" "$_a" "$_s"
|
fi
|
||||||
done
|
echo "$_links"
|
||||||
echo
|
echo
|
||||||
}
|
|
||||||
|
|
||||||
# ── cmd: add ─────────────────────────────────────────────────────────────────
|
local _name=""
|
||||||
cmd_add() {
|
read -r -p " Name to remove: " _name
|
||||||
local _username="${1:-}" _scope="${2:-}" _is_admin="false"
|
[[ -n "$_name" ]] || return 0
|
||||||
[[ "${3:-}" == "--admin" ]] && _is_admin="true"
|
|
||||||
|
|
||||||
ensure_token
|
local _link_path="$_scope_dir/$_name"
|
||||||
|
if ! docker exec "$_c" test -L "$_link_path" 2>/dev/null; then
|
||||||
if [[ -z "$_username" ]]; then
|
errmsg "'$_name' is not an added folder — not removing."
|
||||||
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}Directory:${R} the folder the user sees as their root — add extras after."
|
|
||||||
echo
|
|
||||||
read -r -p " Username: " _username
|
|
||||||
local _adm=""
|
|
||||||
read -r -p " Admin? [y/N]: " _adm
|
|
||||||
[[ "${_adm,,}" == "y" ]] && _is_admin="true"
|
|
||||||
fi
|
|
||||||
|
|
||||||
validate_username "$_username" || return 1
|
|
||||||
|
|
||||||
if [[ -z "$_scope" ]]; then
|
|
||||||
echo
|
|
||||||
echo " Starting directory — what the user sees as their root when they log in:"
|
|
||||||
echo " /data full access to all files (new layout)"
|
|
||||||
echo " / full access to all files (legacy layout)"
|
|
||||||
echo " /$_username this user's own folder (must already exist)"
|
|
||||||
echo " /music music folder only"
|
|
||||||
echo
|
|
||||||
read -r -p " Directory for '$_username': " _scope
|
|
||||||
_scope="/${_scope#/}" # ensure leading /
|
|
||||||
fi
|
|
||||||
|
|
||||||
validate_scope "$_scope" || return 1
|
|
||||||
|
|
||||||
local _ex
|
|
||||||
_ex=$(find_user "$_username")
|
|
||||||
if [[ -n "$_ex" ]]; then
|
|
||||||
errmsg "User '$_username' already exists. Use Modify to change it."
|
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
docker exec "$_c" rm "$_link_path"
|
||||||
|
ok "'$_name' removed."
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Main ──────────────────────────────────────────────────────────────────────
|
||||||
|
require_cmds curl jq docker
|
||||||
|
|
||||||
|
_c=$(get_container_name)
|
||||||
|
check_container "$_c"
|
||||||
|
_data_root=$(get_data_root "$_c")
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
banner "FileBrowser — Extra Folder Access"
|
||||||
|
echo " ${DIM}Manage additional folders for users beyond their root directory.${R}"
|
||||||
|
echo " ${DIM}Create/delete users in the FileBrowser web UI.${R}"
|
||||||
echo
|
echo
|
||||||
local password=""
|
echo " 1 Add extra folders to a user"
|
||||||
prompt_password password "Password for $_username"
|
echo " 2 Remove an extra folder from a user"
|
||||||
|
echo " 3 Show a user's extra folders"
|
||||||
local _perms _body
|
echo " 0 Exit"
|
||||||
_perms=$(default_perms)
|
|
||||||
[[ "$_is_admin" == "true" ]] && _perms=$(echo "$_perms" | jq '.admin = true')
|
|
||||||
_body=$(jq -n \
|
|
||||||
--arg u "$_username" --arg p "$password" --arg s "$_scope" \
|
|
||||||
--argjson perms "$_perms" \
|
|
||||||
'{username:$u, password:$p, scope:$s, locale:"en", viewMode:"list",
|
|
||||||
perm:$perms, commands:[], lockPassword:false}')
|
|
||||||
|
|
||||||
local _resp _http_code
|
|
||||||
_resp=$(curl -s -w '\n%{http_code}' -X POST "$FB_URL/api/users" \
|
|
||||||
-H "X-Auth: $TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "$_body" 2>&1) || true
|
|
||||||
_http_code=$(printf '%s' "$_resp" | tail -1)
|
|
||||||
_resp=$(printf '%s' "$_resp" | head -n -1)
|
|
||||||
|
|
||||||
if [[ "$_http_code" != "200" && "$_http_code" != "201" ]]; then
|
|
||||||
local _msg
|
|
||||||
_msg=$(printf '%s' "$_resp" | jq -r '.message // .error // empty' 2>/dev/null) || true
|
|
||||||
[[ -z "$_msg" ]] && _msg="$_resp"
|
|
||||||
[[ -z "$_msg" ]] && _msg="HTTP $_http_code"
|
|
||||||
errmsg "Failed to create user '$_username': $_msg"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo
|
echo
|
||||||
ok "User '$_username' created | directory: $_scope | admin: $_is_admin"
|
_ch=""
|
||||||
|
read -r -p " Choice: " _ch
|
||||||
|
|
||||||
# Offer additional directories when user has a private home (not full access)
|
case "$_ch" in
|
||||||
if command -v docker &>/dev/null && [[ "$_scope" != "/" && "$_scope" != "/data" ]]; then
|
1)
|
||||||
local _do_dirs=""
|
pick_user || continue
|
||||||
read -r -p " Add additional directories for '$_username'? [y/N]: " _do_dirs
|
banner "Add extras — $CHOSEN_USER (${CHOSEN_DIR})"
|
||||||
if [[ "${_do_dirs,,}" == "y" ]]; then
|
if [[ "$CHOSEN_DIR" == "/" || "$CHOSEN_DIR" == "/data" ]]; then
|
||||||
prompt_add_dirs "$_scope" || true
|
echo " This user has full access — no extras needed."
|
||||||
fi
|
else
|
||||||
fi
|
add_extras "$CHOSEN_DIR" "$_c" "$_data_root" || true
|
||||||
}
|
fi
|
||||||
|
;;
|
||||||
# ── cmd: delete ───────────────────────────────────────────────────────────────
|
2)
|
||||||
cmd_delete() {
|
pick_user || continue
|
||||||
local _username="${1:-}"
|
banner "Remove extra — $CHOSEN_USER (${CHOSEN_DIR})"
|
||||||
ensure_token
|
remove_extra "$CHOSEN_DIR" "$_c" || true
|
||||||
|
;;
|
||||||
if [[ -z "$_username" ]]; then
|
3)
|
||||||
cmd_list
|
pick_user || continue
|
||||||
read -r -p " Username to delete: " _username
|
banner "Extras — $CHOSEN_USER (${CHOSEN_DIR})"
|
||||||
fi
|
_links=$(list_extras "$_c" "$CHOSEN_DIR")
|
||||||
validate_username "$_username" || return 1
|
if [[ -n "$_links" ]]; then echo "$_links"; else echo " (none)"; fi
|
||||||
|
echo
|
||||||
local _uid
|
;;
|
||||||
_uid=$(get_user_id "$_username") || return 1
|
0) echo; echo " Goodbye."; echo; exit 0 ;;
|
||||||
|
*) errmsg "Invalid choice." ;;
|
||||||
local _c=""
|
esac
|
||||||
read -r -p " Delete '$_username' (id $_uid)? [y/N]: " _c
|
done
|
||||||
[[ "${_c,,}" == "y" ]] || { echo " Aborted."; return 0; }
|
|
||||||
api_delete "/api/users/$_uid" >/dev/null
|
|
||||||
ok "User '$_username' deleted."
|
|
||||||
echo " ${DIM}Note: their directory and any added extras still exist in the Docker volume.${R}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── cmd: passwd ───────────────────────────────────────────────────────────────
|
|
||||||
cmd_passwd() {
|
|
||||||
local _username="${1:-}"
|
|
||||||
ensure_token
|
|
||||||
|
|
||||||
if [[ -z "$_username" ]]; then
|
|
||||||
cmd_list
|
|
||||||
read -r -p " Username: " _username
|
|
||||||
fi
|
|
||||||
validate_username "$_username" || return 1
|
|
||||||
|
|
||||||
local _uid _user
|
|
||||||
_uid=$(get_user_id "$_username") || return 1
|
|
||||||
_user=$(find_user "$_username")
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo " ${B}Password rules:${R} min 8 chars, at least 1 letter and 1 number."
|
|
||||||
echo
|
|
||||||
local password=""
|
|
||||||
prompt_password password "New password for $_username"
|
|
||||||
|
|
||||||
local _body
|
|
||||||
_body=$(echo "$_user" | jq --arg p "$password" '. + {password: $p}')
|
|
||||||
api_put "/api/users/$_uid" "$_body" >/dev/null
|
|
||||||
echo
|
|
||||||
ok "Password updated for '$_username'."
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── cmd: scope ────────────────────────────────────────────────────────────────
|
|
||||||
cmd_scope() {
|
|
||||||
local _username="${1:-}" _new_scope="${2:-}"
|
|
||||||
ensure_token
|
|
||||||
|
|
||||||
if [[ -z "$_username" ]]; then
|
|
||||||
cmd_list
|
|
||||||
read -r -p " Username: " _username
|
|
||||||
fi
|
|
||||||
validate_username "$_username" || return 1
|
|
||||||
|
|
||||||
local _uid _user _old_scope
|
|
||||||
_uid=$(get_user_id "$_username") || return 1
|
|
||||||
_user=$(find_user "$_username")
|
|
||||||
_old_scope=$(echo "$_user" | jq -r '.scope')
|
|
||||||
|
|
||||||
if [[ -z "$_new_scope" ]]; then
|
|
||||||
echo
|
|
||||||
echo " Current directory: $_old_scope"
|
|
||||||
echo " ${DIM}Additional directories from the old location are not moved automatically.${R}"
|
|
||||||
echo
|
|
||||||
read -r -p " New directory: " _new_scope
|
|
||||||
_new_scope="/${_new_scope#/}"
|
|
||||||
fi
|
|
||||||
validate_scope "$_new_scope" || return 1
|
|
||||||
|
|
||||||
local _body
|
|
||||||
_body=$(echo "$_user" | jq --arg s "$_new_scope" '. + {scope: $s}')
|
|
||||||
api_put "/api/users/$_uid" "$_body" >/dev/null
|
|
||||||
echo
|
|
||||||
ok "Directory updated for '$_username': $_old_scope → $_new_scope"
|
|
||||||
|
|
||||||
# Offer to add directories into the new scope
|
|
||||||
if command -v docker &>/dev/null && [[ "$_new_scope" != "/" && "$_new_scope" != "/data" ]]; then
|
|
||||||
local _do_dirs=""
|
|
||||||
read -r -p " Add additional directories into '$_new_scope'? [y/N]: " _do_dirs
|
|
||||||
if [[ "${_do_dirs,,}" == "y" ]]; then
|
|
||||||
prompt_add_dirs "$_new_scope" || true
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── cmd: rename ───────────────────────────────────────────────────────────────
|
|
||||||
cmd_rename() {
|
|
||||||
local _username="${1:-}" _new_username="${2:-}"
|
|
||||||
ensure_token
|
|
||||||
|
|
||||||
if [[ -z "$_username" ]]; then
|
|
||||||
cmd_list
|
|
||||||
read -r -p " Username to rename: " _username
|
|
||||||
fi
|
|
||||||
validate_username "$_username" || return 1
|
|
||||||
|
|
||||||
if [[ -z "$_new_username" ]]; then
|
|
||||||
read -r -p " New username: " _new_username
|
|
||||||
fi
|
|
||||||
validate_username "$_new_username" || return 1
|
|
||||||
|
|
||||||
local _uid _user _body
|
|
||||||
_uid=$(get_user_id "$_username") || return 1
|
|
||||||
_user=$(find_user "$_username")
|
|
||||||
_body=$(echo "$_user" | jq --arg u "$_new_username" '. + {username: $u}')
|
|
||||||
api_put "/api/users/$_uid" "$_body" >/dev/null
|
|
||||||
ok "Renamed: '$_username' → '$_new_username'"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── cmd: info ─────────────────────────────────────────────────────────────────
|
|
||||||
cmd_info() {
|
|
||||||
local _username="${1:-}"
|
|
||||||
ensure_token
|
|
||||||
|
|
||||||
if [[ -z "$_username" ]]; then
|
|
||||||
cmd_list
|
|
||||||
read -r -p " Username: " _username
|
|
||||||
fi
|
|
||||||
validate_username "$_username" || return 1
|
|
||||||
|
|
||||||
local _user
|
|
||||||
_user=$(find_user "$_username")
|
|
||||||
[[ -n "$_user" ]] || { errmsg "User '$_username' not found."; return 1; }
|
|
||||||
echo
|
|
||||||
echo "$_user" | jq '{username, scope,
|
|
||||||
admin: .perm.admin,
|
|
||||||
create: .perm.create,
|
|
||||||
modify: .perm.modify,
|
|
||||||
delete: .perm.delete,
|
|
||||||
download: .perm.download,
|
|
||||||
execute: .perm.execute}'
|
|
||||||
echo
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Modify submenu ────────────────────────────────────────────────────────────
|
|
||||||
menu_modify() {
|
|
||||||
ensure_token
|
|
||||||
cmd_list
|
|
||||||
|
|
||||||
local _cur=""
|
|
||||||
read -r -p " Username to modify: " _cur
|
|
||||||
validate_username "$_cur" || return 1
|
|
||||||
get_user_id "$_cur" >/dev/null || return 1
|
|
||||||
|
|
||||||
while true; do
|
|
||||||
local _user _scope _admin
|
|
||||||
_user=$(find_user "$_cur") || { errmsg "User '$_cur' no longer exists."; break; }
|
|
||||||
[[ -n "$_user" ]] || { errmsg "User '$_cur' no longer exists."; break; }
|
|
||||||
_scope=$(echo "$_user" | jq -r '.scope')
|
|
||||||
_admin=$(echo "$_user" | jq -r 'if .perm.admin then "yes" else "no" end')
|
|
||||||
|
|
||||||
banner "Modify: $_cur"
|
|
||||||
echo " ${B}Directory:${R} $_scope"
|
|
||||||
echo " ${B}Admin:${R} $_admin"
|
|
||||||
echo
|
|
||||||
echo " 1 Change username"
|
|
||||||
echo " 2 Change password"
|
|
||||||
echo " 3 Change directory"
|
|
||||||
echo " 4 Toggle admin status"
|
|
||||||
echo " 5 Additional directories ${DIM}(add/remove extra folder access)${R}"
|
|
||||||
echo " 0 Back"
|
|
||||||
echo
|
|
||||||
local _ch=""
|
|
||||||
read -r -p " Choice: " _ch
|
|
||||||
|
|
||||||
case "$_ch" in
|
|
||||||
1)
|
|
||||||
local _new_u=""
|
|
||||||
echo
|
|
||||||
read -r -p " New username: " _new_u
|
|
||||||
validate_username "$_new_u" || continue
|
|
||||||
local _uid1 _body1
|
|
||||||
_uid1=$(get_user_id "$_cur") || continue
|
|
||||||
_body1=$(echo "$_user" | jq --arg u "$_new_u" '. + {username: $u}')
|
|
||||||
if api_put "/api/users/$_uid1" "$_body1" >/dev/null; then
|
|
||||||
ok "Renamed: '$_cur' → '$_new_u'"
|
|
||||||
_cur="$_new_u"
|
|
||||||
else
|
|
||||||
errmsg "Rename failed."
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
2) cmd_passwd "$_cur" || true ;;
|
|
||||||
3) cmd_scope "$_cur" || true ;;
|
|
||||||
4)
|
|
||||||
local _uid4
|
|
||||||
_uid4=$(get_user_id "$_cur") || continue
|
|
||||||
local _toggled
|
|
||||||
_toggled=$(echo "$_user" | jq '.perm.admin = (.perm.admin | not)')
|
|
||||||
if api_put "/api/users/$_uid4" "$_toggled" >/dev/null; then
|
|
||||||
local _new_admin
|
|
||||||
_new_admin=$(echo "$_toggled" | jq -r 'if .perm.admin then "yes" else "no" end')
|
|
||||||
ok "Admin for '$_cur' is now: $_new_admin"
|
|
||||||
else
|
|
||||||
errmsg "Toggle failed."
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
5) menu_add_dirs "$_scope" || true ;;
|
|
||||||
0) break ;;
|
|
||||||
*) errmsg "Invalid choice." ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── usage ─────────────────────────────────────────────────────────────────────
|
|
||||||
usage() {
|
|
||||||
cat <<'EOF'
|
|
||||||
FileBrowser user management
|
|
||||||
|
|
||||||
Run with no arguments for the interactive menu.
|
|
||||||
|
|
||||||
One-shot usage:
|
|
||||||
manage_users.sh list
|
|
||||||
manage_users.sh add <username> <directory> [--admin]
|
|
||||||
manage_users.sh delete <username>
|
|
||||||
manage_users.sh passwd <username>
|
|
||||||
manage_users.sh scope <username> <new-directory>
|
|
||||||
manage_users.sh rename <username> <new-username>
|
|
||||||
manage_users.sh info <username>
|
|
||||||
|
|
||||||
Directory is a path relative to /srv inside the container.
|
|
||||||
New layout: /data = full access, /alice = alice's folder
|
|
||||||
Legacy: / = full access, /alice = alice's folder
|
|
||||||
Username: letters, numbers, hyphens, underscores only. No dots or @.
|
|
||||||
Password: min 8 chars, at least one letter and one number.
|
|
||||||
|
|
||||||
Additional directories: use the interactive menu — extras
|
|
||||||
are offered automatically when you add a user or change their directory.
|
|
||||||
|
|
||||||
Override URL: FB_URL=http://localhost:8085 ./manage_users.sh
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Main interactive menu ─────────────────────────────────────────────────────
|
|
||||||
run_interactive() {
|
|
||||||
ensure_token
|
|
||||||
while true; do
|
|
||||||
banner "FileBrowser User Manager"
|
|
||||||
echo " ${DIM}${FB_URL}${R}"
|
|
||||||
echo
|
|
||||||
echo " 1 List users"
|
|
||||||
echo " 2 Add user"
|
|
||||||
echo " 3 Delete user"
|
|
||||||
echo " 4 Modify user (username / password / directory / admin / extras)"
|
|
||||||
echo " 5 View user details"
|
|
||||||
echo " 0 Exit"
|
|
||||||
echo
|
|
||||||
local _ch=""
|
|
||||||
read -r -p " Choice: " _ch
|
|
||||||
case "$_ch" in
|
|
||||||
1) cmd_list || true ;;
|
|
||||||
2) cmd_add || true ;;
|
|
||||||
3) cmd_delete || true ;;
|
|
||||||
4) menu_modify || true ;;
|
|
||||||
5) cmd_info || true ;;
|
|
||||||
0) echo; echo " Goodbye."; echo; exit 0 ;;
|
|
||||||
*) errmsg "Invalid choice." ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Entry point ───────────────────────────────────────────────────────────────
|
|
||||||
require_cmds curl jq
|
|
||||||
|
|
||||||
_cmd="${1:-}"
|
|
||||||
shift || true
|
|
||||||
|
|
||||||
case "$_cmd" in
|
|
||||||
"") run_interactive ;;
|
|
||||||
list) ensure_token; cmd_list ;;
|
|
||||||
add) ensure_token; cmd_add "$@" ;;
|
|
||||||
delete|del) ensure_token; cmd_delete "${1:-}" ;;
|
|
||||||
passwd|pw) ensure_token; cmd_passwd "${1:-}" ;;
|
|
||||||
scope) ensure_token; cmd_scope "${1:-}" "${2:-}" ;;
|
|
||||||
rename) ensure_token; cmd_rename "${1:-}" "${2:-}" ;;
|
|
||||||
info) ensure_token; cmd_info "${1:-}" ;;
|
|
||||||
help|--help|-h) usage ;;
|
|
||||||
*) errmsg "Unknown command: $_cmd"; echo; usage; exit 2 ;;
|
|
||||||
esac
|
|
||||||
|
|||||||
Reference in New Issue
Block a user