Merge pull request #66 from outis1one/claude/zealous-feynman-odDrh
Claude/zealous feynman od drh
This commit is contained in:
@@ -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 "$@"
|
||||
Reference in New Issue
Block a user