filebrowser: replace manage_users.sh with additional_directories.sh
Symlinks don't work for giving scoped FileBrowser users access to extra folders — FileBrowser's afero.BasePathFs blocks symlinks that resolve outside the user's scope directory. Switch to bind-mount approach: additional_directories.sh edits docker-compose.yml to add real bind-mount entries for each extra folder, so FileBrowser sees them as actual subdirectories within the user's root. No symlinks, no scope-boundary issues. Features: - Reads FB_PATH from .env to list available source folders on the host - Parses docker-compose.yml to show what's already configured per user - Adds/removes volume entries with a timestamped backup before each edit - Prompts to restart the container after changes - Normalises scope paths from the API (handles missing leading slash) manage_users.sh removed — user CRUD is handled by the FileBrowser web UI. https://claude.ai/code/session_014CCYqVwW6d6f5dw1qRokYt
This commit is contained in:
+13
-6
@@ -222,14 +222,14 @@ FB_SETTINGS
|
||||
|
||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$FB_DIR"
|
||||
|
||||
# Deploy user-management helper script
|
||||
# Deploy extra-directory helper script
|
||||
local _TOOLS_DIR
|
||||
_TOOLS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../tools" 2>/dev/null && pwd)" || true
|
||||
if [ -f "$_TOOLS_DIR/manage_users.sh" ]; then
|
||||
cp "$_TOOLS_DIR/manage_users.sh" "$FB_DIR/manage_users.sh"
|
||||
chmod 750 "$FB_DIR/manage_users.sh"
|
||||
chown "$ACTUAL_USER:$ACTUAL_USER" "$FB_DIR/manage_users.sh"
|
||||
log_success "manage_users.sh installed at $FB_DIR/manage_users.sh"
|
||||
if [ -f "$_TOOLS_DIR/additional_directories.sh" ]; then
|
||||
cp "$_TOOLS_DIR/additional_directories.sh" "$FB_DIR/additional_directories.sh"
|
||||
chmod 750 "$FB_DIR/additional_directories.sh"
|
||||
chown "$ACTUAL_USER:$ACTUAL_USER" "$FB_DIR/additional_directories.sh"
|
||||
log_success "additional_directories.sh installed at $FB_DIR/additional_directories.sh"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
@@ -254,6 +254,13 @@ Web-based file manager. Browse, upload, and download files through a browser.
|
||||
- Database: ./database/filebrowser.db
|
||||
- Settings: ./config/settings.json
|
||||
|
||||
## Users
|
||||
Create and manage users in the FileBrowser web UI.
|
||||
To give a user access to extra folders beyond their root directory:
|
||||
\`\`\`
|
||||
$FB_DIR/additional_directories.sh
|
||||
\`\`\`
|
||||
|
||||
## Manage
|
||||
\`\`\`
|
||||
cd $FB_DIR
|
||||
|
||||
@@ -0,0 +1,341 @@
|
||||
#!/usr/bin/env bash
|
||||
# additional_directories.sh — Give FileBrowser users access to extra folders.
|
||||
#
|
||||
# Placed in ~/docker/filebrowser/ by the installer.
|
||||
# Requires: curl, jq, docker (sudo apt install curl jq)
|
||||
#
|
||||
# FileBrowser only shows each user the one folder set as their root (directory).
|
||||
# This script gives a user access to extra folders by adding bind-mount entries
|
||||
# to docker-compose.yml so FileBrowser sees them as real subdirectories within
|
||||
# the user's root — no symlinks, no scope-boundary issues.
|
||||
#
|
||||
# A container restart is required for changes to take effect.
|
||||
#
|
||||
set -Eeuo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
COMPOSE_FILE="$SCRIPT_DIR/docker-compose.yml"
|
||||
ENV_FILE="$SCRIPT_DIR/.env"
|
||||
FB_URL="${FB_URL:-http://localhost:8085}"
|
||||
TOKEN=""
|
||||
|
||||
# ── Output helpers ────────────────────────────────────────────────────────────
|
||||
if [[ -t 1 ]]; then
|
||||
B=$'\e[1m' R=$'\e[0m' GRN=$'\e[32m' RED=$'\e[31m' YEL=$'\e[33m' DIM=$'\e[2m'
|
||||
else
|
||||
B="" R="" GRN="" RED="" YEL="" DIM=""
|
||||
fi
|
||||
|
||||
die() { echo "${RED}ERROR:${R} $*" >&2; exit 1; }
|
||||
ok() { echo " ${GRN}✓${R} $*"; }
|
||||
warn() { echo " ${YEL}!${R} $*"; }
|
||||
errmsg() { echo " ${RED}✗${R} $*" >&2; }
|
||||
hr() { printf ' %s\n' "────────────────────────────────────────────"; }
|
||||
banner() { echo; hr; printf " ${B}%-44s${R}\n" "$*"; hr; }
|
||||
|
||||
# ── Prerequisites ─────────────────────────────────────────────────────────────
|
||||
require_cmds() {
|
||||
for _c in "$@"; do
|
||||
command -v "$_c" &>/dev/null || die "'$_c' not found — sudo apt install $_c"
|
||||
done
|
||||
}
|
||||
|
||||
[[ -f "$COMPOSE_FILE" ]] || die "docker-compose.yml not found at $COMPOSE_FILE"
|
||||
|
||||
# ── Read FB_PATH from .env ────────────────────────────────────────────────────
|
||||
get_fb_path() {
|
||||
local _p=""
|
||||
if [[ -f "$ENV_FILE" ]]; then
|
||||
_p=$(grep '^FB_PATH=' "$ENV_FILE" 2>/dev/null | head -1 | cut -d= -f2-)
|
||||
fi
|
||||
# Fall back to parsing compose file for the bind-mount line
|
||||
if [[ -z "$_p" ]]; then
|
||||
# Match: - /some/path:/srv or - /some/path:/srv/data
|
||||
_p=$(grep -oP '^\s+-\s+\K[^$][^:]+(?=:/srv(/data)?(\s|$))' "$COMPOSE_FILE" \
|
||||
2>/dev/null | head -1) || true
|
||||
fi
|
||||
[[ -n "$_p" ]] || die "Cannot determine FB_PATH — check $ENV_FILE or $COMPOSE_FILE"
|
||||
echo "$_p"
|
||||
}
|
||||
|
||||
# ── Auth ──────────────────────────────────────────────────────────────────────
|
||||
ensure_token() {
|
||||
[[ -n "$TOKEN" ]] && return 0
|
||||
echo
|
||||
echo " ${B}FileBrowser login${R} ${DIM}(${FB_URL})${R}"
|
||||
local _u _p _payload _tok
|
||||
read -r -p " Admin username [admin]: " _u
|
||||
_u="${_u:-admin}"
|
||||
read -r -s -p " Admin password: " _p; echo
|
||||
|
||||
_payload=$(jq -n --arg u "$_u" --arg p "$_p" '{username:$u,password:$p}')
|
||||
_tok=$(curl -s -X POST "$FB_URL/api/login" \
|
||||
-H "Content-Type: application/json" -d "$_payload") || true
|
||||
|
||||
if [[ -z "$_tok" ]]; then
|
||||
die "No response from FileBrowser at $FB_URL — is it running?"
|
||||
elif [[ "$_tok" == *"."*"."* ]]; then
|
||||
TOKEN="$_tok"
|
||||
ok "Logged in as $_u"
|
||||
else
|
||||
echo " FileBrowser responded: $_tok" >&2
|
||||
die "Login failed — wrong credentials or FileBrowser not reachable"
|
||||
fi
|
||||
}
|
||||
|
||||
api_get() { curl -sf -X GET "$FB_URL$1" -H "X-Auth: $TOKEN"; }
|
||||
|
||||
# ── User selection ────────────────────────────────────────────────────────────
|
||||
CHOSEN_USER=""
|
||||
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
|
||||
# Normalise: ensure leading slash
|
||||
[[ "$_s" == /* ]] || _s="/$_s"
|
||||
_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]}"
|
||||
}
|
||||
|
||||
# ── Parse existing extra mounts for a user ────────────────────────────────────
|
||||
# Extra mounts are any volume line matching HOST_PATH:/srv/USER_DIR/...
|
||||
# (as opposed to base mounts like fb_users:/srv or ./database/... etc.)
|
||||
list_extras_compose() {
|
||||
local _user_dir="$1" # e.g. /jda
|
||||
# Match lines like: - /absolute/path:/srv/jda/something
|
||||
grep -oP "^\s+-\s+\K/.+:/srv${_user_dir}/.+" "$COMPOSE_FILE" 2>/dev/null \
|
||||
| sed 's/.*:\(.*\)/\1/' \
|
||||
| sed "s|/srv${_user_dir}/||" \
|
||||
|| true
|
||||
}
|
||||
|
||||
list_extras_compose_full() {
|
||||
local _user_dir="$1"
|
||||
grep -oP "^\s+-\s+\K/.+:/srv${_user_dir}/.+" "$COMPOSE_FILE" 2>/dev/null \
|
||||
|| true
|
||||
}
|
||||
|
||||
# ── Add a bind-mount entry to docker-compose.yml ─────────────────────────────
|
||||
add_volume_entry() {
|
||||
local _host_path="$1" _container_path="$2"
|
||||
|
||||
# Check not already present
|
||||
if grep -qF "${_host_path}:${_container_path}" "$COMPOSE_FILE" 2>/dev/null; then
|
||||
errmsg "Already in docker-compose.yml."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Back up before editing
|
||||
local _bk="$SCRIPT_DIR/docker-compose.yml.bak.$(date +%Y%m%d-%H%M%S)"
|
||||
cp "$COMPOSE_FILE" "$_bk"
|
||||
|
||||
# Insert after the settings.json volume line (our known anchor)
|
||||
# Works for both new-layout (fb_users) and legacy compose files
|
||||
if grep -q 'settings.json' "$COMPOSE_FILE"; then
|
||||
sed -i "/settings\.json/a\\ - ${_host_path}:${_container_path}" \
|
||||
"$COMPOSE_FILE"
|
||||
else
|
||||
# Fallback: insert before the ports: line
|
||||
sed -i "/^\s*ports:/i\\ - ${_host_path}:${_container_path}" \
|
||||
"$COMPOSE_FILE"
|
||||
fi
|
||||
|
||||
ok "Added to docker-compose.yml: ${_host_path} → ${_container_path}"
|
||||
echo " ${DIM}Backup saved: $(basename "$_bk")${R}"
|
||||
}
|
||||
|
||||
# ── Remove a bind-mount entry from docker-compose.yml ────────────────────────
|
||||
remove_volume_entry() {
|
||||
local _container_path="$1"
|
||||
|
||||
local _bk="$SCRIPT_DIR/docker-compose.yml.bak.$(date +%Y%m%d-%H%M%S)"
|
||||
cp "$COMPOSE_FILE" "$_bk"
|
||||
|
||||
# Escape path for sed pattern
|
||||
local _escaped
|
||||
_escaped=$(printf '%s' "$_container_path" | sed 's|/|\\/|g')
|
||||
sed -i "/[[:space:]]-[[:space:]].*:${_escaped}/d" "$COMPOSE_FILE"
|
||||
|
||||
ok "Removed from docker-compose.yml: ${_container_path}"
|
||||
echo " ${DIM}Backup saved: $(basename "$_bk")${R}"
|
||||
}
|
||||
|
||||
# ── Restart container ─────────────────────────────────────────────────────────
|
||||
restart_container() {
|
||||
echo
|
||||
warn "A container restart is required for changes to take effect."
|
||||
local _r=""
|
||||
read -r -p " Restart FileBrowser now? [y/N]: " _r
|
||||
[[ "${_r,,}" == "y" ]] || { echo " Restart later with: docker compose down && docker compose up -d"; return 0; }
|
||||
|
||||
cd "$SCRIPT_DIR"
|
||||
docker compose down
|
||||
docker compose up -d
|
||||
echo
|
||||
ok "FileBrowser restarted."
|
||||
}
|
||||
|
||||
# ── Add flow ──────────────────────────────────────────────────────────────────
|
||||
do_add() {
|
||||
pick_user || return 1
|
||||
|
||||
local _user_dir="$CHOSEN_DIR"
|
||||
|
||||
if [[ "$_user_dir" == "/" || "$_user_dir" == "/data" ]]; then
|
||||
echo
|
||||
echo " $CHOSEN_USER has full access — no extra directories needed."
|
||||
return 0
|
||||
fi
|
||||
|
||||
local _fb_path
|
||||
_fb_path=$(get_fb_path)
|
||||
|
||||
banner "Add directory — $CHOSEN_USER (${_user_dir})"
|
||||
|
||||
# Show what's already added
|
||||
local _cur
|
||||
_cur=$(list_extras_compose "$_user_dir")
|
||||
if [[ -n "$_cur" ]]; then
|
||||
echo " Already added:"
|
||||
echo "$_cur" | while read -r _n; do printf " - %s\n" "$_n"; done
|
||||
echo
|
||||
fi
|
||||
|
||||
# List available source folders on the host
|
||||
echo " Available folders in ${_fb_path}:"
|
||||
local _avail=()
|
||||
while IFS= read -r -d '' _d; do
|
||||
local _name; _name=$(basename "$_d")
|
||||
[[ "$_name" == .* ]] && continue # skip hidden
|
||||
_avail+=("$_name")
|
||||
printf " %s\n" "$_name"
|
||||
done < <(find "$_fb_path" -maxdepth 1 -mindepth 1 -type d -print0 2>/dev/null | sort -z)
|
||||
|
||||
[[ ${#_avail[@]} -gt 0 ]] || { echo " (none found)"; return 0; }
|
||||
echo
|
||||
|
||||
local _changed=false
|
||||
while true; do
|
||||
local _src=""
|
||||
read -r -p " Folder to add [done]: " _src
|
||||
[[ -n "$_src" ]] || break
|
||||
|
||||
_src="${_src#/}"; _src="${_src%/}"
|
||||
[[ -n "$_src" ]] || continue
|
||||
|
||||
local _host_path="$_fb_path/$_src"
|
||||
local _container_path="/srv${_user_dir}/$_src"
|
||||
|
||||
if [[ ! -d "$_host_path" ]]; then
|
||||
errmsg "'$_src' not found in $_fb_path"
|
||||
continue
|
||||
fi
|
||||
|
||||
add_volume_entry "$_host_path" "$_container_path" && _changed=true || true
|
||||
done
|
||||
|
||||
[[ "$_changed" == true ]] && restart_container || true
|
||||
}
|
||||
|
||||
# ── Remove flow ───────────────────────────────────────────────────────────────
|
||||
do_remove() {
|
||||
pick_user || return 1
|
||||
|
||||
local _user_dir="$CHOSEN_DIR"
|
||||
banner "Remove directory — $CHOSEN_USER (${_user_dir})"
|
||||
|
||||
local _extras
|
||||
_extras=$(list_extras_compose_full "$_user_dir")
|
||||
if [[ -z "$_extras" ]]; then
|
||||
echo " No extra directories configured for $CHOSEN_USER."
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo " Extra directories for $CHOSEN_USER:"
|
||||
echo
|
||||
local -a _names _container_paths
|
||||
local _i=0
|
||||
while IFS= read -r _line; do
|
||||
local _cpath; _cpath="${_line##*:}"
|
||||
local _fname; _fname="${_cpath##*/}"
|
||||
_names+=("$_fname")
|
||||
_container_paths+=("$_cpath")
|
||||
printf " %2d %s\n" "$((_i+1))" "$_fname"
|
||||
((_i++)) || true
|
||||
done <<< "$_extras"
|
||||
echo
|
||||
|
||||
local _pick=""
|
||||
read -r -p " Select entry to remove (number): " _pick
|
||||
[[ "$_pick" =~ ^[0-9]+$ ]] || { errmsg "Enter a number."; return 1; }
|
||||
local _idx=$((_pick-1))
|
||||
[[ $_idx -ge 0 && $_idx -lt ${#_names[@]} ]] || { errmsg "Out of range."; return 1; }
|
||||
|
||||
remove_volume_entry "${_container_paths[$_idx]}"
|
||||
restart_container
|
||||
}
|
||||
|
||||
# ── Show flow ─────────────────────────────────────────────────────────────────
|
||||
do_show() {
|
||||
pick_user || return 1
|
||||
|
||||
banner "Extra directories — $CHOSEN_USER (${CHOSEN_DIR})"
|
||||
|
||||
local _extras
|
||||
_extras=$(list_extras_compose "$CHOSEN_DIR")
|
||||
if [[ -n "$_extras" ]]; then
|
||||
echo "$_extras" | while read -r _n; do printf " - %s\n" "$_n"; done
|
||||
else
|
||||
echo " (none configured)"
|
||||
fi
|
||||
echo
|
||||
}
|
||||
|
||||
# ── Main ──────────────────────────────────────────────────────────────────────
|
||||
require_cmds curl jq docker
|
||||
|
||||
while true; do
|
||||
banner "FileBrowser — Extra Directories"
|
||||
echo " ${DIM}Adds bind-mount entries so users can access extra folders.${R}"
|
||||
echo " ${DIM}Create/delete users via the FileBrowser web UI.${R}"
|
||||
echo
|
||||
echo " 1 Add a directory to a user"
|
||||
echo " 2 Remove a directory from a user"
|
||||
echo " 3 Show a user's extra directories"
|
||||
echo " 0 Exit"
|
||||
echo
|
||||
_ch=""
|
||||
read -r -p " Choice: " _ch
|
||||
|
||||
case "$_ch" in
|
||||
1) do_add || true ;;
|
||||
2) do_remove || true ;;
|
||||
3) do_show || true ;;
|
||||
0) echo; echo " Goodbye."; echo; exit 0 ;;
|
||||
*) errmsg "Invalid choice." ;;
|
||||
esac
|
||||
done
|
||||
@@ -1,276 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# manage_users.sh — Add/remove extra folder access for FileBrowser users.
|
||||
#
|
||||
# Placed in ~/docker/filebrowser/ by the filebrowser installer.
|
||||
# Requires: curl, jq, docker (sudo apt install curl jq)
|
||||
#
|
||||
# FileBrowser only gives each user one root directory. This script adds
|
||||
# shortcuts inside that directory so a user can reach multiple folders
|
||||
# without getting full access to everything.
|
||||
#
|
||||
# Create and manage users through the FileBrowser web UI instead.
|
||||
#
|
||||
set -Eeuo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
FB_URL="${FB_URL:-http://localhost:8085}"
|
||||
TOKEN=""
|
||||
|
||||
# ── Output helpers ────────────────────────────────────────────────────────────
|
||||
if [[ -t 1 ]]; then
|
||||
B=$'\e[1m' R=$'\e[0m' GRN=$'\e[32m' RED=$'\e[31m' DIM=$'\e[2m'
|
||||
else
|
||||
B="" R="" GRN="" RED="" DIM=""
|
||||
fi
|
||||
|
||||
die() { echo "${RED}ERROR:${R} $*" >&2; exit 1; }
|
||||
ok() { echo " ${GRN}✓${R} $*"; }
|
||||
errmsg() { echo " ${RED}✗${R} $*" >&2; }
|
||||
hr() { printf ' %s\n' "────────────────────────────────────────────"; }
|
||||
banner() { echo; hr; printf " ${B}%-44s${R}\n" "$*"; hr; }
|
||||
|
||||
# ── Prerequisites ─────────────────────────────────────────────────────────────
|
||||
require_cmds() {
|
||||
for _c in "$@"; do
|
||||
command -v "$_c" &>/dev/null || die "'$_c' not found — sudo apt install $_c"
|
||||
done
|
||||
}
|
||||
|
||||
# ── Auth ──────────────────────────────────────────────────────────────────────
|
||||
ensure_token() {
|
||||
[[ -n "$TOKEN" ]] && return 0
|
||||
echo
|
||||
echo " ${B}FileBrowser login${R} ${DIM}(${FB_URL})${R}"
|
||||
local _u _p _payload _tok
|
||||
read -r -p " Admin username [admin]: " _u
|
||||
_u="${_u:-admin}"
|
||||
read -r -s -p " Admin password: " _p; echo
|
||||
|
||||
_payload=$(jq -n --arg u "$_u" --arg p "$_p" '{username:$u,password:$p}')
|
||||
_tok=$(curl -s -X POST "$FB_URL/api/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$_payload") || true
|
||||
|
||||
if [[ -z "$_tok" ]]; then
|
||||
die "No response from FileBrowser at $FB_URL — is it running? (docker compose up -d)"
|
||||
elif [[ "$_tok" == *"."*"."* ]]; then
|
||||
TOKEN="$_tok"
|
||||
ok "Logged in as $_u"
|
||||
else
|
||||
echo " FileBrowser responded: $_tok" >&2
|
||||
die "Login failed — wrong credentials or FileBrowser not reachable at $FB_URL"
|
||||
fi
|
||||
}
|
||||
|
||||
api_get() { curl -sf -X GET "$FB_URL$1" -H "X-Auth: $TOKEN"; }
|
||||
|
||||
# ── Docker helpers ────────────────────────────────────────────────────────────
|
||||
get_container_name() {
|
||||
local _compose="$SCRIPT_DIR/docker-compose.yml"
|
||||
if [[ -f "$_compose" ]]; then
|
||||
local _name
|
||||
_name=$(grep 'container_name:' "$_compose" | head -1 | awk '{print $2}')
|
||||
[[ -n "$_name" ]] && { echo "$_name"; return; }
|
||||
fi
|
||||
echo "filebrowser"
|
||||
}
|
||||
|
||||
check_container() {
|
||||
local _running
|
||||
_running=$(docker inspect --format='{{.State.Running}}' "$1" 2>/dev/null || echo "false")
|
||||
[[ "$_running" == "true" ]] \
|
||||
|| die "Container '$1' is not running — start it first: docker compose up -d"
|
||||
}
|
||||
|
||||
# Returns /srv/data (new named-volume layout) or /srv (legacy bind-mount layout).
|
||||
get_data_root() {
|
||||
local _c="$1"
|
||||
if docker exec "$_c" test -d /srv/data 2>/dev/null; then
|
||||
echo "/srv/data"
|
||||
else
|
||||
echo "/srv"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── User selection ────────────────────────────────────────────────────────────
|
||||
# Prints a numbered list and returns chosen username + directory via globals.
|
||||
CHOSEN_USER=""
|
||||
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 '
|
||||
_name=$(basename "$1")
|
||||
_target=$(readlink "$1")
|
||||
_display="${_target#/srv}"
|
||||
[ -z "$_display" ] && _display="/"
|
||||
printf " %-24s→ %s\n" "$_name" "$_display"
|
||||
' _ {} \; 2>/dev/null | sort || true
|
||||
}
|
||||
|
||||
# ── Add extra directories ─────────────────────────────────────────────────────
|
||||
add_extras() {
|
||||
local _dir="$1" _c="$2" _data_root="$3"
|
||||
local _scope_dir="/srv$_dir"
|
||||
|
||||
if ! docker exec "$_c" test -d "$_scope_dir" 2>/dev/null; then
|
||||
echo
|
||||
echo " The directory '$_dir' does not exist in the container yet."
|
||||
local _mk=""
|
||||
read -r -p " Create it now? [y/N]: " _mk
|
||||
if [[ "${_mk,,}" == "y" ]]; then
|
||||
docker exec "$_c" mkdir -p "$_scope_dir" \
|
||||
|| die "Could not create '$_scope_dir' (permission denied?)."
|
||||
ok "Created '$_dir'"
|
||||
else
|
||||
errmsg "Cannot add extras — '$_dir' must exist first."
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo
|
||||
echo " Type a folder name to add — ? to list available, blank when done."
|
||||
echo
|
||||
|
||||
while true; do
|
||||
local _src=""
|
||||
read -r -p " Folder to add [done]: " _src
|
||||
[[ -n "$_src" ]] || break
|
||||
|
||||
if [[ "$_src" == "?" ]]; then
|
||||
local _avail
|
||||
_avail=$(docker exec "$_c" find "$_data_root" -maxdepth 1 -mindepth 1 \
|
||||
\( -type d -o -type l \) -not -name ".*" 2>/dev/null \
|
||||
| sed "s|^${_data_root}/||" | sort | tr '\n' ' ') || true
|
||||
echo " Available: ${_avail:-(none found)}"
|
||||
echo
|
||||
continue
|
||||
fi
|
||||
|
||||
_src="${_src#/}"; _src="${_src%/}"
|
||||
[[ -n "$_src" ]] || continue
|
||||
|
||||
local _target="$_data_root/$_src"
|
||||
local _link_name; _link_name=$(basename "$_src")
|
||||
local _link_path="$_scope_dir/$_link_name"
|
||||
|
||||
if ! docker exec "$_c" test -e "$_target" 2>/dev/null; then
|
||||
errmsg "'$_src' not found — type ? to see available folders."
|
||||
continue
|
||||
fi
|
||||
if docker exec "$_c" test -e "$_link_path" 2>/dev/null; then
|
||||
errmsg "'$_link_name' already added."
|
||||
continue
|
||||
fi
|
||||
if ! docker exec "$_c" ln -s "$_target" "$_link_path" 2>/dev/null; then
|
||||
errmsg "Failed to add '$_link_name' (check: docker logs filebrowser)."
|
||||
continue
|
||||
fi
|
||||
ok "Added '$_link_name'"
|
||||
done
|
||||
}
|
||||
|
||||
# ── Remove extra directory ────────────────────────────────────────────────────
|
||||
remove_extra() {
|
||||
local _dir="$1" _c="$2"
|
||||
local _scope_dir="/srv$_dir"
|
||||
|
||||
echo
|
||||
local _links
|
||||
_links=$(list_extras "$_c" "$_dir")
|
||||
if [[ -z "$_links" ]]; then
|
||||
echo " No extras to remove."
|
||||
return 0
|
||||
fi
|
||||
echo "$_links"
|
||||
echo
|
||||
|
||||
local _name=""
|
||||
read -r -p " Name to remove: " _name
|
||||
[[ -n "$_name" ]] || return 0
|
||||
|
||||
local _link_path="$_scope_dir/$_name"
|
||||
if ! docker exec "$_c" test -L "$_link_path" 2>/dev/null; then
|
||||
errmsg "'$_name' is not an added folder — not removing."
|
||||
return 1
|
||||
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 " 1 Add extra folders to a user"
|
||||
echo " 2 Remove an extra folder from a user"
|
||||
echo " 3 Show a user's extra folders"
|
||||
echo " 0 Exit"
|
||||
echo
|
||||
_ch=""
|
||||
read -r -p " Choice: " _ch
|
||||
|
||||
case "$_ch" in
|
||||
1)
|
||||
pick_user || continue
|
||||
banner "Add extras — $CHOSEN_USER (${CHOSEN_DIR})"
|
||||
if [[ "$CHOSEN_DIR" == "/" || "$CHOSEN_DIR" == "/data" ]]; then
|
||||
echo " This user has full access — no extras needed."
|
||||
else
|
||||
add_extras "$CHOSEN_DIR" "$_c" "$_data_root" || true
|
||||
fi
|
||||
;;
|
||||
2)
|
||||
pick_user || continue
|
||||
banner "Remove extra — $CHOSEN_USER (${CHOSEN_DIR})"
|
||||
remove_extra "$CHOSEN_DIR" "$_c" || true
|
||||
;;
|
||||
3)
|
||||
pick_user || continue
|
||||
banner "Extras — $CHOSEN_USER (${CHOSEN_DIR})"
|
||||
_links=$(list_extras "$_c" "$CHOSEN_DIR")
|
||||
if [[ -n "$_links" ]]; then echo "$_links"; else echo " (none)"; fi
|
||||
echo
|
||||
;;
|
||||
0) echo; echo " Goodbye."; echo; exit 0 ;;
|
||||
*) errmsg "Invalid choice." ;;
|
||||
esac
|
||||
done
|
||||
Reference in New Issue
Block a user