From 7379f0b62f689aec7094bdd3d97a625bb2cb9d8b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 8 Jun 2026 04:59:11 +0000 Subject: [PATCH 1/2] tools: add fbq-add-source.sh for FileBrowser Quantum source management Interactive script that keeps docker-compose.yml and config.yaml in sync when adding or removing file sources in a FileBrowser Quantum installation. - Prompts for host path, source name, defaultEnabled, readOnly - Validates against duplicate volume mounts and source entries - Backs up both files before editing - Uses yq v4 for safe YAML editing (no fragile sed/awk on YAML) - Offers container restart after changes - Works standalone; no post-install framework dependency https://claude.ai/code/session_014CCYqVwW6d6f5dw1qRokYt --- tools/fbq-add-source.sh | 372 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 372 insertions(+) create mode 100644 tools/fbq-add-source.sh diff --git a/tools/fbq-add-source.sh b/tools/fbq-add-source.sh new file mode 100644 index 0000000..789981e --- /dev/null +++ b/tools/fbq-add-source.sh @@ -0,0 +1,372 @@ +#!/usr/bin/env bash +# fbq-add-source.sh — Add file sources to a FileBrowser Quantum installation. +# +# Edits docker-compose.yml and config.yaml together so they stay in sync: +# - Adds a volume mount to the filebrowser service +# - Adds a matching source entry under server.sources +# - Optionally restarts the container to apply changes +# +# Usage: +# bash fbq-add-source.sh [--dir /path/to/compose] +# +# Requirements: +# - yq v4+ (https://github.com/mikefarah/yq) +# Install: sudo wget -qO /usr/local/bin/yq \ +# https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 \ +# && sudo chmod +x /usr/local/bin/yq +# - docker compose v2 + +set -euo pipefail + +# ── Colour helpers ──────────────────────────────────────────────────────────── +_blue() { printf '\033[0;34m[INFO]\033[0m %s\n' "$*"; } +_green() { printf '\033[0;32m[OK]\033[0m %s\n' "$*"; } +_yellow() { printf '\033[1;33m[WARN]\033[0m %s\n' "$*"; } +_red() { printf '\033[0;31m[ERROR]\033[0m %s\n' "$*" >&2; } + +# ── Dependency checks ───────────────────────────────────────────────────────── +check_deps() { + local missing=0 + + if ! command -v yq &>/dev/null; then + _red "yq is required but not installed." + echo "" + echo " Install with:" + echo " sudo wget -qO /usr/local/bin/yq \\" + echo " https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64" + echo " sudo chmod +x /usr/local/bin/yq" + echo "" + missing=1 + else + local yq_ver + yq_ver=$(yq --version 2>&1 | grep -oP '\d+\.\d+' | head -1) + local major="${yq_ver%%.*}" + if [[ "$major" -lt 4 ]]; then + _red "yq v4+ is required (found v${yq_ver}). The mikefarah/yq version is needed." + missing=1 + fi + fi + + if ! docker compose version &>/dev/null; then + _red "Docker Compose plugin (v2) is required." + echo " Install: sudo apt-get install -y docker-compose-plugin" + missing=1 + fi + + [[ "$missing" -eq 0 ]] || exit 1 +} + +# ── Locate install directory ────────────────────────────────────────────────── +find_install_dir() { + local dir="${1:-}" + + if [[ -n "$dir" ]]; then + echo "$dir" + return + fi + + # Common locations + local candidates=( + "$HOME/docker/filebrowser-quantum" + "$HOME/docker/filebrowser" + "/opt/filebrowser-quantum" + "/opt/filebrowser" + ) + + for c in "${candidates[@]}"; do + if [[ -f "$c/docker-compose.yml" && -f "$c/data/config.yaml" ]]; then + echo "$c" + return + fi + done + + # Not found — ask + echo "" + read -r -p " Path to FileBrowser Quantum directory (contains docker-compose.yml): " dir + dir="${dir%/}" + if [[ ! -f "$dir/docker-compose.yml" ]]; then + _red "docker-compose.yml not found in: $dir" + exit 1 + fi + if [[ ! -f "$dir/data/config.yaml" ]]; then + _red "data/config.yaml not found in: $dir" + exit 1 + fi + echo "$dir" +} + +# ── Backup a file ───────────────────────────────────────────────────────────── +backup() { + local file="$1" + local bk="${file}.backup.$(date +%Y%m%d-%H%M%S)" + cp "$file" "$bk" + _blue "Backed up $(basename "$file") → $(basename "$bk")" +} + +# ── Check if a volume mount already exists in compose ──────────────────────── +volume_exists() { + local compose="$1" container_path="$2" + yq e '.services.filebrowser.volumes[]' "$compose" 2>/dev/null \ + | grep -qF ":${container_path}" || return 1 +} + +# ── Check if a source already exists in config ─────────────────────────────── +source_exists() { + local config="$1" container_path="$2" + yq e '.server.sources[].path' "$config" 2>/dev/null \ + | grep -qxF "$container_path" || return 1 +} + +# ── Add a single source ─────────────────────────────────────────────────────── +add_source() { + local compose="$1" config="$2" + + echo "" + echo "────────────────────────────────────────────────" + + # Host path + local host_path="" + while true; do + read -r -p " Host directory path (e.g. /mnt/data/music): " host_path + host_path="${host_path%/}" + if [[ -z "$host_path" ]]; then + _yellow "No path entered — skipping." + return 0 + fi + if [[ ! -d "$host_path" ]]; then + _yellow "'$host_path' does not exist on this host." + local yn="" + read -r -p " Add it anyway? [y/N]: " yn + [[ "${yn,,}" == "y" ]] && break + else + break + fi + done + + # Source name (used as both the container mount point and the Quantum source name) + local default_name + default_name=$(basename "$host_path" | tr '[:upper:]' '[:lower:]' | tr ' ' '-') + local source_name="" + read -r -p " Source name [${default_name}]: " source_name + source_name="${source_name:-$default_name}" + # Sanitise: lowercase, alphanumeric + hyphen only + source_name=$(echo "$source_name" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9-' '-' | sed 's/-*$//') + + local container_path="/${source_name}" + + # Collision checks + if volume_exists "$compose" "$container_path"; then + _yellow "Volume mount ':${container_path}' already exists in docker-compose.yml — skipping." + return 0 + fi + if source_exists "$config" "$container_path"; then + _yellow "Source path '${container_path}' already exists in config.yaml — skipping." + return 0 + fi + + # Access defaults + local default_enabled="" + read -r -p " Enable for all users by default? [y/N]: " default_enabled + local default_enabled_bool="false" + [[ "${default_enabled,,}" == "y" ]] && default_enabled_bool="true" + + local read_only="" + read -r -p " Read-only? [y/N]: " read_only + local read_only_bool="false" + [[ "${read_only,,}" == "y" ]] && read_only_bool="true" + + # ── Write changes ────────────────────────────────────────────────────────── + + # 1. docker-compose.yml — add volume mount + backup "$compose" + yq e -i ".services.filebrowser.volumes += [\"${host_path}:${container_path}\"]" "$compose" + _green "Added volume: ${host_path}:${container_path}" + + # 2. config.yaml — add source entry + backup "$config" + yq e -i ".server.sources += [{ + \"path\": \"${container_path}\", + \"name\": \"${source_name}\", + \"config\": { + \"defaultEnabled\": ${default_enabled_bool}, + \"readOnly\": ${read_only_bool} + } + }]" "$config" + _green "Added source: ${source_name} → ${container_path}" +} + +# ── Remove a source ─────────────────────────────────────────────────────────── +remove_source() { + local compose="$1" config="$2" + + echo "" + echo "Current sources:" + echo "" + + # List sources from config + local paths=() + while IFS= read -r p; do + paths+=("$p") + done < <(yq e '.server.sources[].path' "$config" 2>/dev/null) + + if [[ ${#paths[@]} -eq 0 ]]; then + _yellow "No sources found in config.yaml." + return 0 + fi + + local i=1 + for p in "${paths[@]}"; do + printf " %2d) %s\n" "$i" "$p" + (( i++ )) + done + echo "" + + local choice="" + read -r -p " Remove source number [cancel]: " choice + if [[ -z "$choice" || ! "$choice" =~ ^[0-9]+$ ]]; then + _blue "Cancelled." + return 0 + fi + + local idx=$(( choice - 1 )) + if [[ "$idx" -lt 0 || "$idx" -ge ${#paths[@]} ]]; then + _yellow "Invalid selection." + return 0 + fi + + local target_path="${paths[$idx]}" + + # Confirm + echo "" + _yellow "Will remove source '$target_path' from config.yaml and its volume mount from docker-compose.yml." + local confirm="" + read -r -p " Confirm? [y/N]: " confirm + [[ "${confirm,,}" == "y" ]] || { _blue "Cancelled."; return 0; } + + # Remove from config.yaml + backup "$config" + yq e -i "del(.server.sources[] | select(.path == \"${target_path}\"))" "$config" + _green "Removed source from config.yaml" + + # Remove from docker-compose.yml (volume entry ending with :target_path) + if volume_exists "$compose" "$target_path"; then + backup "$compose" + yq e -i "del(.services.filebrowser.volumes[] | select(. == \"*:${target_path}\"))" "$compose" + # yq glob select doesn't do substring — use a precise match + yq e -i "del(.services.filebrowser.volumes[] | select(test(\":${target_path}$\")))" "$compose" + _green "Removed volume mount from docker-compose.yml" + else + _yellow "No matching volume mount found in docker-compose.yml (may have been added manually)." + fi +} + +# ── Show current sources ────────────────────────────────────────────────────── +show_sources() { + local compose="$1" config="$2" + + echo "" + echo " Sources in config.yaml:" + echo "" + yq e '.server.sources[] | " " + .name + " → " + .path + + " (defaultEnabled: " + (.config.defaultEnabled | tostring) + ")"' \ + "$config" 2>/dev/null || _yellow " None found." + + echo "" + echo " Volume mounts in docker-compose.yml:" + echo "" + yq e '.services.filebrowser.volumes[]' "$compose" 2>/dev/null \ + | grep -v '/home/filebrowser/data' \ + | sed 's/^/ /' \ + || _yellow " None found." + echo "" +} + +# ── Restart container ───────────────────────────────────────────────────────── +restart_container() { + local dir="$1" + echo "" + local yn="" + read -r -p " Restart FileBrowser Quantum now to apply changes? [Y/n]: " yn + if [[ "${yn,,}" != "n" ]]; then + _blue "Restarting..." + docker compose -f "$dir/docker-compose.yml" down + docker compose -f "$dir/docker-compose.yml" up -d + _green "FileBrowser Quantum restarted." + echo "" + _blue "Assign the new source(s) to users: Settings → Users → edit user → Add source" + else + _yellow "Remember to restart manually: docker compose down && docker compose up -d" + fi +} + +# ── Main ────────────────────────────────────────────────────────────────────── +main() { + local explicit_dir="" + while [[ $# -gt 0 ]]; do + case "$1" in + --dir|-d) explicit_dir="$2"; shift 2 ;; + *) _red "Unknown argument: $1"; exit 1 ;; + esac + done + + check_deps + + echo "" + echo "┌──────────────────────────────────────────────────────┐" + echo "│ FileBrowser Quantum — Source Manager │" + echo "│ Edits docker-compose.yml + config.yaml in sync │" + echo "└──────────────────────────────────────────────────────┘" + + local install_dir + install_dir=$(find_install_dir "$explicit_dir") + local compose="$install_dir/docker-compose.yml" + local config="$install_dir/data/config.yaml" + + _blue "Install dir: $install_dir" + + local changed=0 + + while true; do + echo "" + echo " What would you like to do?" + echo " 1) Add a source" + echo " 2) Remove a source" + echo " 3) Show current sources" + echo " 4) Quit" + echo "" + read -r -p " Choice [1]: " action + action="${action:-1}" + + case "$action" in + 1) + add_source "$compose" "$config" + changed=1 + local another="" + read -r -p " Add another source? [y/N]: " another + [[ "${another,,}" == "y" ]] || break + ;; + 2) + remove_source "$compose" "$config" + changed=1 + ;; + 3) + show_sources "$compose" "$config" + ;; + 4|q|Q) + break + ;; + *) + _yellow "Invalid choice." + ;; + esac + done + + if [[ "$changed" -eq 1 ]]; then + restart_container "$install_dir" + fi + + echo "" + _green "Done." +} + +main "$@" From 99e86711101acb8b55e7e8b900b2de49ce453626 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 8 Jun 2026 05:20:41 +0000 Subject: [PATCH 2/2] =?UTF-8?q?tools:=20simplify=20fbq-add-source.sh=20?= =?UTF-8?q?=E2=80=94=20config.yaml=20only,=20no=20compose=20edits?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assumes the parent data directory is already broadly mounted in docker-compose.yml so subdirectories are already inside the container. Script now only edits config.yaml and restarts the container — no docker-compose surgery needed per new source. https://claude.ai/code/session_014CCYqVwW6d6f5dw1qRokYt --- tools/fbq-add-source.sh | 397 ++++++++++++++++++---------------------- 1 file changed, 175 insertions(+), 222 deletions(-) diff --git a/tools/fbq-add-source.sh b/tools/fbq-add-source.sh index 789981e..4d4ba2d 100644 --- a/tools/fbq-add-source.sh +++ b/tools/fbq-add-source.sh @@ -1,10 +1,9 @@ #!/usr/bin/env bash -# fbq-add-source.sh — Add file sources to a FileBrowser Quantum installation. +# fbq-add-source.sh — Manage file sources for FileBrowser Quantum. # -# Edits docker-compose.yml and config.yaml together so they stay in sync: -# - Adds a volume mount to the filebrowser service -# - Adds a matching source entry under server.sources -# - Optionally restarts the container to apply changes +# Assumes the parent data directory is already mounted in docker-compose.yml. +# Subdirectories of that mount are already accessible inside the container, +# so only config.yaml needs editing — no compose changes required. # # Usage: # bash fbq-add-source.sh [--dir /path/to/compose] @@ -19,17 +18,17 @@ set -euo pipefail # ── Colour helpers ──────────────────────────────────────────────────────────── -_blue() { printf '\033[0;34m[INFO]\033[0m %s\n' "$*"; } -_green() { printf '\033[0;32m[OK]\033[0m %s\n' "$*"; } -_yellow() { printf '\033[1;33m[WARN]\033[0m %s\n' "$*"; } -_red() { printf '\033[0;31m[ERROR]\033[0m %s\n' "$*" >&2; } +info() { printf '\033[0;34m[INFO]\033[0m %s\n' "$*"; } +ok() { printf '\033[0;32m[OK]\033[0m %s\n' "$*"; } +warn() { printf '\033[1;33m[WARN]\033[0m %s\n' "$*"; } +err() { printf '\033[0;31m[ERROR]\033[0m %s\n' "$*" >&2; } # ── Dependency checks ───────────────────────────────────────────────────────── check_deps() { local missing=0 if ! command -v yq &>/dev/null; then - _red "yq is required but not installed." + err "yq is required but not installed." echo "" echo " Install with:" echo " sudo wget -qO /usr/local/bin/yq \\" @@ -38,17 +37,16 @@ check_deps() { echo "" missing=1 else - local yq_ver - yq_ver=$(yq --version 2>&1 | grep -oP '\d+\.\d+' | head -1) - local major="${yq_ver%%.*}" + local major + major=$(yq --version 2>&1 | grep -oP '(?<=v)\d+' | head -1 || echo 0) if [[ "$major" -lt 4 ]]; then - _red "yq v4+ is required (found v${yq_ver}). The mikefarah/yq version is needed." + err "yq v4+ required (found v${major}). Get it from github.com/mikefarah/yq" missing=1 fi fi if ! docker compose version &>/dev/null; then - _red "Docker Compose plugin (v2) is required." + err "Docker Compose plugin (v2) is required." echo " Install: sudo apt-get install -y docker-compose-plugin" missing=1 fi @@ -61,312 +59,267 @@ find_install_dir() { local dir="${1:-}" if [[ -n "$dir" ]]; then - echo "$dir" - return + [[ -f "$dir/docker-compose.yml" ]] || { err "docker-compose.yml not found in: $dir"; exit 1; } + [[ -f "$dir/data/config.yaml" ]] || { err "data/config.yaml not found in: $dir"; exit 1; } + echo "$dir"; return fi - # Common locations local candidates=( "$HOME/docker/filebrowser-quantum" "$HOME/docker/filebrowser" "/opt/filebrowser-quantum" "/opt/filebrowser" ) - for c in "${candidates[@]}"; do if [[ -f "$c/docker-compose.yml" && -f "$c/data/config.yaml" ]]; then - echo "$c" - return + echo "$c"; return fi done - # Not found — ask echo "" - read -r -p " Path to FileBrowser Quantum directory (contains docker-compose.yml): " dir + read -r -p " Path to FileBrowser Quantum directory: " dir dir="${dir%/}" - if [[ ! -f "$dir/docker-compose.yml" ]]; then - _red "docker-compose.yml not found in: $dir" - exit 1 - fi - if [[ ! -f "$dir/data/config.yaml" ]]; then - _red "data/config.yaml not found in: $dir" - exit 1 - fi + [[ -f "$dir/docker-compose.yml" ]] || { err "docker-compose.yml not found in: $dir"; exit 1; } + [[ -f "$dir/data/config.yaml" ]] || { err "data/config.yaml not found in: $dir"; exit 1; } echo "$dir" } +# ── Get the container-side path for the main data mount ────────────────────── +# Reads docker-compose.yml and returns the container path of the first +# non-data-dir volume mount (i.e. the user files mount). +get_data_mount() { + local compose="$1" + yq e '.services.filebrowser.volumes[]' "$compose" 2>/dev/null \ + | grep -v '/home/filebrowser/data' \ + | head -1 \ + | cut -d: -f2 \ + | sed 's|/$||' +} + # ── Backup a file ───────────────────────────────────────────────────────────── backup() { local file="$1" local bk="${file}.backup.$(date +%Y%m%d-%H%M%S)" cp "$file" "$bk" - _blue "Backed up $(basename "$file") → $(basename "$bk")" + info "Backed up $(basename "$file") → $(basename "$bk")" } -# ── Check if a volume mount already exists in compose ──────────────────────── -volume_exists() { - local compose="$1" container_path="$2" - yq e '.services.filebrowser.volumes[]' "$compose" 2>/dev/null \ - | grep -qF ":${container_path}" || return 1 -} - -# ── Check if a source already exists in config ─────────────────────────────── +# ── Check if a source path already exists in config ────────────────────────── source_exists() { - local config="$1" container_path="$2" - yq e '.server.sources[].path' "$config" 2>/dev/null \ - | grep -qxF "$container_path" || return 1 + local config="$1" path="$2" + yq e '.server.sources[].path' "$config" 2>/dev/null | grep -qxF "$path" } -# ── Add a single source ─────────────────────────────────────────────────────── -add_source() { - local compose="$1" config="$2" +# ── List existing sources ───────────────────────────────────────────────────── +list_sources() { + local config="$1" + yq e '.server.sources[] | .path' "$config" 2>/dev/null +} + +# ── Restart the container ───────────────────────────────────────────────────── +restart_container() { + local dir="$1" prompt="${2:-true}" + + if [[ "$prompt" == "true" ]]; then + echo "" + local yn="" + read -r -p " Restart FileBrowser Quantum now to apply changes? [Y/n]: " yn + [[ "${yn,,}" == "n" ]] && { + warn "Remember to restart: cd $dir && docker compose restart" + return 0 + } + fi + + info "Restarting FileBrowser Quantum..." + docker compose -f "$dir/docker-compose.yml" restart \ + && ok "FileBrowser Quantum restarted." \ + || warn "Restart failed — try: cd $dir && docker compose restart" +} + +# ── ADD ─────────────────────────────────────────────────────────────────────── +cmd_add() { + local dir="$1" config="$2" data_mount="$3" echo "" - echo "────────────────────────────────────────────────" + echo " The data directory is mounted at: ${data_mount:-unknown}" + echo " Enter subdirectory names (relative to that mount) to add as sources." + echo " Example: if your music lives at ${data_mount}/music, enter 'music'" + echo "" + + local added=0 - # Host path - local host_path="" while true; do - read -r -p " Host directory path (e.g. /mnt/data/music): " host_path - host_path="${host_path%/}" - if [[ -z "$host_path" ]]; then - _yellow "No path entered — skipping." - return 0 - fi - if [[ ! -d "$host_path" ]]; then - _yellow "'$host_path' does not exist on this host." - local yn="" - read -r -p " Add it anyway? [y/N]: " yn - [[ "${yn,,}" == "y" ]] && break - else - break + local subdir="" + read -r -p " Subdirectory to add (or Enter to finish): " subdir + [[ -z "$subdir" ]] && break + subdir="${subdir#/}" # strip any leading slash + + local container_path="${data_mount}/${subdir}" + + # Default name = subdir basename, lowercase, hyphenated + local default_name + default_name=$(basename "$subdir" | tr '[:upper:]' '[:lower:]' | tr ' _' '-') + local source_name="" + read -r -p " Display name for this source [${default_name}]: " source_name + source_name="${source_name:-$default_name}" + source_name=$(echo "$source_name" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9-' '-' | sed 's/-*$//') + + if source_exists "$config" "$container_path"; then + warn "Source '$container_path' already in config.yaml — skipping." + continue fi + + local default_enabled_bool="false" + local yn="" + read -r -p " Enable for all users by default? [y/N]: " yn + [[ "${yn,,}" == "y" ]] && default_enabled_bool="true" + + local read_only_bool="false" + read -r -p " Read-only? [y/N]: " read_only_bool_raw + [[ "${read_only_bool_raw,,}" == "y" ]] && read_only_bool="true" + + backup "$config" + yq e -i ".server.sources += [{ + \"path\": \"${container_path}\", + \"name\": \"${source_name}\", + \"config\": { + \"defaultEnabled\": ${default_enabled_bool}, + \"readOnly\": ${read_only_bool} + } + }]" "$config" + + ok "Added source '${source_name}' → ${container_path}" + (( added++ )) done - # Source name (used as both the container mount point and the Quantum source name) - local default_name - default_name=$(basename "$host_path" | tr '[:upper:]' '[:lower:]' | tr ' ' '-') - local source_name="" - read -r -p " Source name [${default_name}]: " source_name - source_name="${source_name:-$default_name}" - # Sanitise: lowercase, alphanumeric + hyphen only - source_name=$(echo "$source_name" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9-' '-' | sed 's/-*$//') - - local container_path="/${source_name}" - - # Collision checks - if volume_exists "$compose" "$container_path"; then - _yellow "Volume mount ':${container_path}' already exists in docker-compose.yml — skipping." - return 0 + if [[ "$added" -gt 0 ]]; then + restart_container "$dir" + echo "" + ok "$added source(s) added." + info "Assign them to users: Settings → Users → edit user → Add source" + else + info "Nothing added." fi - if source_exists "$config" "$container_path"; then - _yellow "Source path '${container_path}' already exists in config.yaml — skipping." - return 0 - fi - - # Access defaults - local default_enabled="" - read -r -p " Enable for all users by default? [y/N]: " default_enabled - local default_enabled_bool="false" - [[ "${default_enabled,,}" == "y" ]] && default_enabled_bool="true" - - local read_only="" - read -r -p " Read-only? [y/N]: " read_only - local read_only_bool="false" - [[ "${read_only,,}" == "y" ]] && read_only_bool="true" - - # ── Write changes ────────────────────────────────────────────────────────── - - # 1. docker-compose.yml — add volume mount - backup "$compose" - yq e -i ".services.filebrowser.volumes += [\"${host_path}:${container_path}\"]" "$compose" - _green "Added volume: ${host_path}:${container_path}" - - # 2. config.yaml — add source entry - backup "$config" - yq e -i ".server.sources += [{ - \"path\": \"${container_path}\", - \"name\": \"${source_name}\", - \"config\": { - \"defaultEnabled\": ${default_enabled_bool}, - \"readOnly\": ${read_only_bool} - } - }]" "$config" - _green "Added source: ${source_name} → ${container_path}" } -# ── Remove a source ─────────────────────────────────────────────────────────── -remove_source() { - local compose="$1" config="$2" +# ── REMOVE ──────────────────────────────────────────────────────────────────── +cmd_remove() { + local dir="$1" config="$2" - echo "" - echo "Current sources:" - echo "" - - # List sources from config local paths=() - while IFS= read -r p; do - paths+=("$p") - done < <(yq e '.server.sources[].path' "$config" 2>/dev/null) + while IFS= read -r p; do paths+=("$p"); done < <(list_sources "$config") if [[ ${#paths[@]} -eq 0 ]]; then - _yellow "No sources found in config.yaml." + warn "No sources found in config.yaml." return 0 fi + echo "" + echo " Current sources:" + echo "" local i=1 for p in "${paths[@]}"; do - printf " %2d) %s\n" "$i" "$p" + local name + name=$(yq e ".server.sources[] | select(.path == \"$p\") | .name" "$config" 2>/dev/null || echo "") + printf " %2d) %-20s %s\n" "$i" "${name:-?}" "$p" (( i++ )) done echo "" local choice="" read -r -p " Remove source number [cancel]: " choice - if [[ -z "$choice" || ! "$choice" =~ ^[0-9]+$ ]]; then - _blue "Cancelled." - return 0 - fi + [[ -z "$choice" || ! "$choice" =~ ^[0-9]+$ ]] && { info "Cancelled."; return 0; } local idx=$(( choice - 1 )) if [[ "$idx" -lt 0 || "$idx" -ge ${#paths[@]} ]]; then - _yellow "Invalid selection." - return 0 + warn "Invalid selection."; return 0 fi - local target_path="${paths[$idx]}" - - # Confirm - echo "" - _yellow "Will remove source '$target_path' from config.yaml and its volume mount from docker-compose.yml." + local target="${paths[$idx]}" + warn "Will remove source '$target' from config.yaml." local confirm="" read -r -p " Confirm? [y/N]: " confirm - [[ "${confirm,,}" == "y" ]] || { _blue "Cancelled."; return 0; } + [[ "${confirm,,}" == "y" ]] || { info "Cancelled."; return 0; } - # Remove from config.yaml backup "$config" - yq e -i "del(.server.sources[] | select(.path == \"${target_path}\"))" "$config" - _green "Removed source from config.yaml" - - # Remove from docker-compose.yml (volume entry ending with :target_path) - if volume_exists "$compose" "$target_path"; then - backup "$compose" - yq e -i "del(.services.filebrowser.volumes[] | select(. == \"*:${target_path}\"))" "$compose" - # yq glob select doesn't do substring — use a precise match - yq e -i "del(.services.filebrowser.volumes[] | select(test(\":${target_path}$\")))" "$compose" - _green "Removed volume mount from docker-compose.yml" - else - _yellow "No matching volume mount found in docker-compose.yml (may have been added manually)." - fi + yq e -i "del(.server.sources[] | select(.path == \"${target}\"))" "$config" + ok "Removed source '$target' from config.yaml." + restart_container "$dir" } -# ── Show current sources ────────────────────────────────────────────────────── -show_sources() { - local compose="$1" config="$2" +# ── SHOW ────────────────────────────────────────────────────────────────────── +cmd_show() { + local config="$1" + echo "" + echo " Sources configured in config.yaml:" + echo "" - echo "" - echo " Sources in config.yaml:" - echo "" - yq e '.server.sources[] | " " + .name + " → " + .path + - " (defaultEnabled: " + (.config.defaultEnabled | tostring) + ")"' \ - "$config" 2>/dev/null || _yellow " None found." + local count=0 + while IFS= read -r p; do + local name enabled readonly + name=$(yq e ".server.sources[] | select(.path == \"$p\") | .name" "$config" 2>/dev/null || echo "?") + enabled=$(yq e ".server.sources[] | select(.path == \"$p\") | .config.defaultEnabled" "$config" 2>/dev/null || echo "false") + readonly=$(yq e ".server.sources[] | select(.path == \"$p\") | .config.readOnly" "$config" 2>/dev/null || echo "false") + printf " %-20s %-35s defaultEnabled=%-5s readOnly=%s\n" \ + "${name}" "${p}" "${enabled}" "${readonly}" + (( count++ )) + done < <(list_sources "$config") - echo "" - echo " Volume mounts in docker-compose.yml:" - echo "" - yq e '.services.filebrowser.volumes[]' "$compose" 2>/dev/null \ - | grep -v '/home/filebrowser/data' \ - | sed 's/^/ /' \ - || _yellow " None found." + [[ "$count" -eq 0 ]] && warn "No sources found." echo "" } -# ── Restart container ───────────────────────────────────────────────────────── -restart_container() { - local dir="$1" - echo "" - local yn="" - read -r -p " Restart FileBrowser Quantum now to apply changes? [Y/n]: " yn - if [[ "${yn,,}" != "n" ]]; then - _blue "Restarting..." - docker compose -f "$dir/docker-compose.yml" down - docker compose -f "$dir/docker-compose.yml" up -d - _green "FileBrowser Quantum restarted." - echo "" - _blue "Assign the new source(s) to users: Settings → Users → edit user → Add source" - else - _yellow "Remember to restart manually: docker compose down && docker compose up -d" - fi -} - -# ── Main ────────────────────────────────────────────────────────────────────── +# ── MAIN ────────────────────────────────────────────────────────────────────── main() { local explicit_dir="" while [[ $# -gt 0 ]]; do case "$1" in --dir|-d) explicit_dir="$2"; shift 2 ;; - *) _red "Unknown argument: $1"; exit 1 ;; + *) err "Unknown argument: $1"; exit 1 ;; esac done check_deps echo "" - echo "┌──────────────────────────────────────────────────────┐" - echo "│ FileBrowser Quantum — Source Manager │" - echo "│ Edits docker-compose.yml + config.yaml in sync │" - echo "└──────────────────────────────────────────────────────┘" + echo "┌──────────────────────────────────────────────────────────┐" + echo "│ FileBrowser Quantum — Source Manager │" + echo "│ Manages config.yaml and restarts the container │" + echo "└──────────────────────────────────────────────────────────┘" local install_dir install_dir=$(find_install_dir "$explicit_dir") - local compose="$install_dir/docker-compose.yml" local config="$install_dir/data/config.yaml" + local compose="$install_dir/docker-compose.yml" + local data_mount + data_mount=$(get_data_mount "$compose") - _blue "Install dir: $install_dir" - - local changed=0 + info "Install dir : $install_dir" + info "Data mount : ${data_mount:-not detected — enter full container paths}" + echo "" while true; do - echo "" echo " What would you like to do?" - echo " 1) Add a source" + echo " 1) Add source(s)" echo " 2) Remove a source" echo " 3) Show current sources" echo " 4) Quit" echo "" read -r -p " Choice [1]: " action action="${action:-1}" + echo "" case "$action" in - 1) - add_source "$compose" "$config" - changed=1 - local another="" - read -r -p " Add another source? [y/N]: " another - [[ "${another,,}" == "y" ]] || break - ;; - 2) - remove_source "$compose" "$config" - changed=1 - ;; - 3) - show_sources "$compose" "$config" - ;; - 4|q|Q) - break - ;; - *) - _yellow "Invalid choice." - ;; + 1) cmd_add "$install_dir" "$config" "$data_mount" ;; + 2) cmd_remove "$install_dir" "$config" ;; + 3) cmd_show "$config" ;; + 4|q|Q) break ;; + *) warn "Invalid choice." ;; esac + echo "" done - if [[ "$changed" -eq 1 ]]; then - restart_container "$install_dir" - fi - - echo "" - _green "Done." + ok "Done." } main "$@"