diff --git a/tools/fbq-add-source.sh b/tools/fbq-add-source.sh new file mode 100644 index 0000000..4d4ba2d --- /dev/null +++ b/tools/fbq-add-source.sh @@ -0,0 +1,325 @@ +#!/usr/bin/env bash +# fbq-add-source.sh — Manage file sources for FileBrowser Quantum. +# +# 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] +# +# 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 ──────────────────────────────────────────────────────────── +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 + err "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 major + major=$(yq --version 2>&1 | grep -oP '(?<=v)\d+' | head -1 || echo 0) + if [[ "$major" -lt 4 ]]; then + 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 + err "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 + [[ -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 + + 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 + + echo "" + read -r -p " Path to FileBrowser Quantum directory: " dir + dir="${dir%/}" + [[ -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" + info "Backed up $(basename "$file") → $(basename "$bk")" +} + +# ── Check if a source path already exists in config ────────────────────────── +source_exists() { + local config="$1" path="$2" + yq e '.server.sources[].path' "$config" 2>/dev/null | grep -qxF "$path" +} + +# ── 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 " 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 + + while true; do + 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 + + 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 +} + +# ── REMOVE ──────────────────────────────────────────────────────────────────── +cmd_remove() { + local dir="$1" config="$2" + + local paths=() + while IFS= read -r p; do paths+=("$p"); done < <(list_sources "$config") + + if [[ ${#paths[@]} -eq 0 ]]; then + warn "No sources found in config.yaml." + return 0 + fi + + echo "" + echo " Current sources:" + echo "" + local i=1 + for p in "${paths[@]}"; do + 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 + [[ -z "$choice" || ! "$choice" =~ ^[0-9]+$ ]] && { info "Cancelled."; return 0; } + + local idx=$(( choice - 1 )) + if [[ "$idx" -lt 0 || "$idx" -ge ${#paths[@]} ]]; then + warn "Invalid selection."; return 0 + fi + + local target="${paths[$idx]}" + warn "Will remove source '$target' from config.yaml." + local confirm="" + read -r -p " Confirm? [y/N]: " confirm + [[ "${confirm,,}" == "y" ]] || { info "Cancelled."; return 0; } + + backup "$config" + yq e -i "del(.server.sources[] | select(.path == \"${target}\"))" "$config" + ok "Removed source '$target' from config.yaml." + restart_container "$dir" +} + +# ── SHOW ────────────────────────────────────────────────────────────────────── +cmd_show() { + local config="$1" + echo "" + echo " Sources configured in config.yaml:" + echo "" + + 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") + + [[ "$count" -eq 0 ]] && warn "No sources found." + echo "" +} + +# ── MAIN ────────────────────────────────────────────────────────────────────── +main() { + local explicit_dir="" + while [[ $# -gt 0 ]]; do + case "$1" in + --dir|-d) explicit_dir="$2"; shift 2 ;; + *) err "Unknown argument: $1"; exit 1 ;; + esac + done + + check_deps + + 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 config="$install_dir/data/config.yaml" + local compose="$install_dir/docker-compose.yml" + local data_mount + data_mount=$(get_data_mount "$compose") + + info "Install dir : $install_dir" + info "Data mount : ${data_mount:-not detected — enter full container paths}" + echo "" + + while true; do + echo " What would you like to do?" + 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) 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 + + ok "Done." +} + +main "$@"