Previously only edited config.yaml. Now: - Shows existing volume mounts with host→container paths - Detects when a host path isn't mounted yet and adds it to compose - Remove flow optionally removes the volume mount from compose too - Show displays both compose mounts and config sources side by side - Uses down/up (not just restart) when compose changes https://claude.ai/code/session_014CCYqVwW6d6f5dw1qRokYt
431 lines
16 KiB
Bash
431 lines
16 KiB
Bash
#!/usr/bin/env bash
|
|
# fbq-add-source.sh — Manage file sources for FileBrowser Quantum.
|
|
#
|
|
# Handles both docker-compose.yml volume mounts and config.yaml source
|
|
# entries together, keeping them in sync.
|
|
#
|
|
# 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 all non-data container mount points from compose ──────────────────────
|
|
get_compose_mounts() {
|
|
local compose="$1"
|
|
yq e '.services.filebrowser.volumes[]' "$compose" 2>/dev/null \
|
|
| grep -v '/home/filebrowser/data' \
|
|
| grep -v '^\./' \
|
|
| sed 's|/$||'
|
|
}
|
|
|
|
# ── 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}"
|
|
}
|
|
|
|
# ── 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"
|
|
sed 's/[[:space:]]*#.*$//' "$config" \
|
|
| yq e '.server.sources[].path' - 2>/dev/null \
|
|
| grep -qxF "$path"
|
|
}
|
|
|
|
# ── List existing sources ─────────────────────────────────────────────────────
|
|
list_sources() {
|
|
local config="$1"
|
|
sed 's/[[:space:]]*#.*$//' "$config" | yq e '.server.sources[] | .path' - 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 down && docker compose up -d"
|
|
return 0
|
|
}
|
|
fi
|
|
|
|
info "Restarting FileBrowser Quantum..."
|
|
docker compose -f "$dir/docker-compose.yml" down \
|
|
&& docker compose -f "$dir/docker-compose.yml" up -d \
|
|
&& ok "FileBrowser Quantum restarted." \
|
|
|| warn "Restart failed — try: cd $dir && docker compose down && docker compose up -d"
|
|
}
|
|
|
|
# ── ADD ───────────────────────────────────────────────────────────────────────
|
|
cmd_add() {
|
|
local dir="$1" config="$2" compose="$3"
|
|
|
|
# Build list of existing mounts from compose (host:container pairs)
|
|
local mount_pairs=()
|
|
local container_paths=()
|
|
while IFS= read -r line; do
|
|
mount_pairs+=("$line")
|
|
container_paths+=("$(echo "$line" | cut -d: -f2 | sed 's|/$||')")
|
|
done < <(get_compose_mounts "$compose")
|
|
|
|
echo ""
|
|
if [[ ${#container_paths[@]} -gt 0 ]]; then
|
|
echo " Current volume mounts:"
|
|
echo ""
|
|
local i=1
|
|
for pair in "${mount_pairs[@]}"; do
|
|
local h; h=$(echo "$pair" | cut -d: -f1)
|
|
local c; c=$(echo "$pair" | cut -d: -f2 | sed 's|/$||')
|
|
printf " %d) %-30s → %s\n" "$i" "$h" "$c"
|
|
i=$(( i + 1 ))
|
|
done
|
|
else
|
|
echo " No existing volume mounts detected."
|
|
fi
|
|
|
|
echo ""
|
|
echo " Options:"
|
|
echo " • Enter a number to add a source under an existing mount"
|
|
echo " • Enter a host path (e.g. /mnt/data2) to add a NEW mount + source"
|
|
echo " • Enter a full container path (e.g. /data2/music) for an existing mount"
|
|
echo " • Enter to finish"
|
|
echo ""
|
|
|
|
local added=0
|
|
local compose_changed=0
|
|
|
|
while true; do
|
|
local raw=""
|
|
read -r -p " Number, host path, container path, or Enter to finish: " raw
|
|
[[ -z "$raw" ]] && break
|
|
|
|
local host_path="" container_path=""
|
|
|
|
if [[ "$raw" =~ ^[0-9]+$ ]]; then
|
|
# Existing mount by number
|
|
local midx=$(( raw - 1 ))
|
|
if [[ "$midx" -lt 0 || "$midx" -ge ${#container_paths[@]} ]]; then
|
|
warn "Invalid number."; continue
|
|
fi
|
|
local base="${container_paths[$midx]}"
|
|
local subdir=""
|
|
read -r -p " Subdirectory under ${base} (or Enter for the mount root): " subdir
|
|
subdir="${subdir#/}"
|
|
container_path="${base}${subdir:+/$subdir}"
|
|
|
|
elif [[ "$raw" == /* ]]; then
|
|
# Could be a host path or a container path
|
|
if [[ -d "$raw" ]] && ! volume_exists "$compose" "$raw"; then
|
|
# Looks like a host path not yet mounted
|
|
host_path="$raw"
|
|
local default_cname
|
|
default_cname="/$(basename "$raw" | tr '[:upper:]' '[:lower:]' | tr ' _' '-')"
|
|
local cname=""
|
|
read -r -p " Container mount point [${default_cname}]: " cname
|
|
cname="${cname:-$default_cname}"
|
|
cname="/${cname#/}"
|
|
container_path="$cname"
|
|
else
|
|
# Treat as full container path
|
|
container_path="${raw%/}"
|
|
fi
|
|
else
|
|
warn "Enter a number, a host path, or a container path starting with /."; continue
|
|
fi
|
|
|
|
# If this is a new host mount, add it to docker-compose.yml
|
|
if [[ -n "$host_path" ]]; then
|
|
if volume_exists "$compose" "$container_path"; then
|
|
warn "Volume mount ':${container_path}' already in docker-compose.yml."
|
|
else
|
|
backup "$compose"
|
|
yq e -i ".services.filebrowser.volumes += [\"${host_path}:${container_path}\"]" "$compose"
|
|
ok "Added volume mount: ${host_path}:${container_path}"
|
|
compose_changed=1
|
|
fi
|
|
fi
|
|
|
|
# Default source name from container path basename
|
|
local default_name
|
|
default_name=$(basename "$container_path" | 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"
|
|
|
|
backup "$config"
|
|
local tmp; tmp=$(mktemp)
|
|
sed 's/[[:space:]]*#.*$//' "$config" > "$tmp" && mv "$tmp" "$config"
|
|
yq e -i ".server.sources += [{
|
|
\"path\": \"${container_path}\",
|
|
\"name\": \"${source_name}\",
|
|
\"config\": {
|
|
\"defaultEnabled\": ${default_enabled_bool}
|
|
}
|
|
}]" "$config"
|
|
|
|
ok "Added source '${source_name}' → ${container_path}"
|
|
added=$(( added + 1 ))
|
|
done
|
|
|
|
if [[ "$added" -gt 0 || "$compose_changed" -gt 0 ]]; then
|
|
if [[ "$compose_changed" -gt 0 ]]; then
|
|
info "docker-compose.yml was changed — a full down/up is required (not just restart)."
|
|
fi
|
|
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" compose="$3"
|
|
|
|
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=$(sed 's/[[:space:]]*#.*$//' "$config" \
|
|
| yq e ".server.sources[] | select(.path == \"$p\") | .name // \"(unnamed)\"" - 2>/dev/null \
|
|
|| echo "(unnamed)")
|
|
printf " %2d) %-20s %s\n" "$i" "${name}" "$p"
|
|
i=$(( i + 1 ))
|
|
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."
|
|
|
|
# Check if there's also a compose volume mount for this exact path
|
|
local remove_mount=false
|
|
if volume_exists "$compose" "$target"; then
|
|
local yn=""
|
|
read -r -p " Also remove the volume mount for '$target' from docker-compose.yml? [y/N]: " yn
|
|
[[ "${yn,,}" == "y" ]] && remove_mount=true
|
|
fi
|
|
|
|
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."
|
|
|
|
if [[ "$remove_mount" == true ]]; then
|
|
backup "$compose"
|
|
yq e -i "del(.services.filebrowser.volumes[] | select(test(\":${target}$\")))" "$compose"
|
|
ok "Removed volume mount from docker-compose.yml."
|
|
info "A full down/up is required to apply compose changes."
|
|
fi
|
|
|
|
restart_container "$dir"
|
|
}
|
|
|
|
# ── SHOW ──────────────────────────────────────────────────────────────────────
|
|
cmd_show() {
|
|
local config="$1" compose="$2"
|
|
|
|
echo ""
|
|
echo " Volume mounts (docker-compose.yml):"
|
|
echo ""
|
|
local i=1
|
|
while IFS= read -r pair; do
|
|
local h; h=$(echo "$pair" | cut -d: -f1)
|
|
local c; c=$(echo "$pair" | cut -d: -f2 | sed 's|/$||')
|
|
printf " %2d) %-35s → %s\n" "$i" "$h" "$c"
|
|
i=$(( i + 1 ))
|
|
done < <(get_compose_mounts "$compose")
|
|
[[ "$i" -eq 1 ]] && echo " (none)"
|
|
|
|
echo ""
|
|
echo " Sources (config.yaml):"
|
|
echo ""
|
|
|
|
local count=0
|
|
while IFS= read -r p; do
|
|
local name enabled
|
|
name=$(sed 's/[[:space:]]*#.*$//' "$config" \
|
|
| yq e ".server.sources[] | select(.path == \"$p\") | .name // \"(unnamed)\"" - 2>/dev/null \
|
|
|| echo "?")
|
|
enabled=$(sed 's/[[:space:]]*#.*$//' "$config" \
|
|
| yq e ".server.sources[] | select(.path == \"$p\") | .config.defaultEnabled // false" - 2>/dev/null \
|
|
|| echo "false")
|
|
printf " %-20s %-35s defaultEnabled=%s\n" "${name}" "${p}" "${enabled}"
|
|
count=$(( count + 1 ))
|
|
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 docker-compose.yml and config.yaml in sync │"
|
|
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"
|
|
|
|
info "Install dir : $install_dir"
|
|
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 mounts and sources"
|
|
echo " 0) Quit"
|
|
echo ""
|
|
read -r -p " Choice [1]: " action
|
|
action="${action:-1}"
|
|
echo ""
|
|
|
|
case "$action" in
|
|
1) cmd_add "$install_dir" "$config" "$compose" ;;
|
|
2) cmd_remove "$install_dir" "$config" "$compose" ;;
|
|
3) cmd_show "$config" "$compose" ;;
|
|
0|q|Q) break ;;
|
|
*) warn "Invalid choice." ;;
|
|
esac
|
|
echo ""
|
|
done
|
|
|
|
ok "Done."
|
|
}
|
|
|
|
main "$@"
|