diff --git a/services/filebrowser.sh b/services/filebrowser.sh index 3cff91a..02e4e42 100644 --- a/services/filebrowser.sh +++ b/services/filebrowser.sh @@ -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 diff --git a/tools/additional_directories.sh b/tools/additional_directories.sh new file mode 100644 index 0000000..be5ae32 --- /dev/null +++ b/tools/additional_directories.sh @@ -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 diff --git a/tools/manage_users.sh b/tools/manage_users.sh deleted file mode 100644 index 9d62128..0000000 --- a/tools/manage_users.sh +++ /dev/null @@ -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