@@ -0,0 +1,292 @@
|
||||
#!/bin/bash
|
||||
|
||||
# configure reads TITLE and DESCRIPTION
|
||||
# shellcheck disable=SC2034
|
||||
TITLE="AI servers"
|
||||
# shellcheck disable=SC2034
|
||||
DESCRIPTION="Use LLM servers on your network, e.g. Ollama"
|
||||
|
||||
# The servers you've added, one host:port per line. It exists once you've run
|
||||
# this, even if you added none, so this isn't "to do" on machines without one.
|
||||
AI_SERVERS_CONF="$HOME/.config/coredesktop/ai-servers"
|
||||
# The sandbox config every sandboxed agent reads (see sandbox/agent)
|
||||
SANDBOX_CONF="$HOME/.config/coredesktop/sandbox/base.json"
|
||||
CLAUDE_SETTINGS="$HOME/.claude/settings.json"
|
||||
OPENCODE_CONF="$HOME/.config/opencode/opencode.json"
|
||||
source "$COREDESKTOP/configure.d/lib/mdns.sh"
|
||||
|
||||
# Ports LLM servers listen on by default, and who uses each. All of them answer
|
||||
# OpenAI's API at /v1, which is how every agent here talks to them.
|
||||
LLM_PORTS=(11434 1234 8080 8000)
|
||||
declare -A LLM_PORT_NAMES=([11434]=Ollama [1234]="LM Studio" [8080]=llama.cpp [8000]=vLLM)
|
||||
|
||||
is_installed() {
|
||||
command -v avahi-browse
|
||||
}
|
||||
|
||||
is_configured() {
|
||||
[ -f "$AI_SERVERS_CONF" ]
|
||||
}
|
||||
|
||||
# The models a server at host:port offers, one per line; fails if it doesn't
|
||||
# answer like an LLM server
|
||||
server_models() {
|
||||
curl -sf -m 3 "http://$1/v1/models" | jq -er '.data[].id' 2> /dev/null
|
||||
}
|
||||
|
||||
# What the server is, e.g. Ollama, for its name in the agents' settings
|
||||
server_kind() {
|
||||
local port="${1##*:}"
|
||||
if curl -sf -m 3 "http://$1/api/version" > /dev/null; then
|
||||
echo Ollama
|
||||
else
|
||||
echo "${LLM_PORT_NAMES[$port]:-LLM server}"
|
||||
fi
|
||||
}
|
||||
|
||||
# The LLM servers on the network: "host:port|models" per line. mDNS finds the
|
||||
# machines, but LLM servers don't announce themselves on it, so each machine
|
||||
# is asked on the usual ports.
|
||||
find_servers() {
|
||||
local dir host port
|
||||
dir="$(mktemp -d)" || return 1
|
||||
for host in $(mdns_hosts); do
|
||||
for port in "${LLM_PORTS[@]}"; do
|
||||
(server_models "$host:$port" | wc -l > "$dir/$host:$port") &
|
||||
done
|
||||
done
|
||||
wait
|
||||
for host in "$dir"/*; do
|
||||
[ -f "$host" ] && [ "$(cat "$host")" -gt 0 ] && echo "$(basename "$host")|$(cat "$host")"
|
||||
done
|
||||
rm -rf "$dir"
|
||||
}
|
||||
|
||||
# Edit a JSON file in place with jq, creating it from $2 if it's missing.
|
||||
# Leaves the file alone if it isn't valid JSON or jq fails.
|
||||
edit_json() {
|
||||
local file="$1" empty="$2" tmp
|
||||
shift 2
|
||||
mkdir -p "$(dirname "$file")" || return 1
|
||||
[ -f "$file" ] || echo "$empty" > "$file"
|
||||
tmp="$(mktemp "$file.XXXXXX")" || return 1
|
||||
if jq "$@" "$file" > "$tmp"; then
|
||||
mv -f "$tmp" "$file"
|
||||
else
|
||||
echo "Couldn't change $file; is it valid JSON?"
|
||||
rm -f "$tmp"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Let every sandboxed agent (opencode, Hermes, Cline) reach host:port, and
|
||||
# only that port
|
||||
sandbox_allow() {
|
||||
edit_json "$SANDBOX_CONF" '{}' --arg server "$1" \
|
||||
'.network.allowedDomains = ((.network.allowedDomains // []) - [$server] + [$server])'
|
||||
}
|
||||
|
||||
sandbox_disallow() {
|
||||
[ -f "$SANDBOX_CONF" ] || return 0
|
||||
edit_json "$SANDBOX_CONF" '{}' --arg server "$1" \
|
||||
'.network.allowedDomains -= [$server]'
|
||||
}
|
||||
|
||||
# Claude Code's sandbox only covers the commands it runs, e.g. curl, not its
|
||||
# own connection to Anthropic. Its list replaces the defaults sandbox/setup.sh
|
||||
# gives it, so if it has none yet, it starts from those.
|
||||
claude_allow() {
|
||||
local defaults
|
||||
[ -f "$CLAUDE_SETTINGS" ] || return 0
|
||||
defaults="$(jq '.network.allowedDomains' "$COREDESKTOP/sandbox/config/base.json")" || return 1
|
||||
edit_json "$CLAUDE_SETTINGS" '{}' --arg server "$1" --argjson defaults "$defaults" \
|
||||
'.sandbox.network.allowedDomains = ((.sandbox.network.allowedDomains // $defaults) - [$server] + [$server])'
|
||||
}
|
||||
|
||||
claude_disallow() {
|
||||
[ -f "$CLAUDE_SETTINGS" ] || return 0
|
||||
edit_json "$CLAUDE_SETTINGS" '{}' --arg server "$1" \
|
||||
'if .sandbox.network.allowedDomains then .sandbox.network.allowedDomains -= [$server] else . end'
|
||||
}
|
||||
|
||||
# opencode's name for the server: llm.local:11434 -> llm-11434
|
||||
opencode_provider() {
|
||||
local id="${1%%:*}"
|
||||
id="${id%.local}-${1##*:}"
|
||||
echo "${id//[^a-zA-Z0-9-]/-}" | tr '[:upper:]' '[:lower:]'
|
||||
}
|
||||
|
||||
# Add the server to opencode as a provider with its models, and make $3 the
|
||||
# model opencode starts with if you say so
|
||||
opencode_add() {
|
||||
local server="$1" kind="$2" model="$3" id models
|
||||
id="$(opencode_provider "$server")"
|
||||
models="$(server_models "$server" | jq -R '{(.): {name: .}}' | jq -s 'add')" || return 1
|
||||
edit_json "$OPENCODE_CONF" '{"$schema": "https://opencode.ai/config.json"}' \
|
||||
--arg id "$id" --arg name "$kind on ${server%%:*}" --arg url "http://$server/v1" \
|
||||
--argjson models "$models" \
|
||||
'.provider[$id] = {npm: "@ai-sdk/openai-compatible", name: $name, options: {baseURL: $url}, models: $models}' \
|
||||
|| return 1
|
||||
echo "opencode: added $kind on ${server%%:*} (choose its models with /models)."
|
||||
if confirm_yes "Start opencode with $model?"; then
|
||||
edit_json "$OPENCODE_CONF" '{}' --arg model "$id/$model" '.model = $model' || return 1
|
||||
fi
|
||||
}
|
||||
|
||||
opencode_remove() {
|
||||
local id
|
||||
[ -f "$OPENCODE_CONF" ] || return 0
|
||||
id="$(opencode_provider "$1")"
|
||||
edit_json "$OPENCODE_CONF" '{}' --arg id "$id" \
|
||||
'del(.provider[$id]) | if (.model // "") | startswith($id + "/") then del(.model) else . end'
|
||||
}
|
||||
|
||||
# Hermes uses one model at a time, so ask before replacing the one it has
|
||||
hermes_use() {
|
||||
local server="$1" model="$2"
|
||||
confirm_yes "Use $model on ${server%%:*} for Hermes?" || return 0
|
||||
unsandboxed hermes config set model.provider custom > /dev/null \
|
||||
&& unsandboxed hermes config set model.base_url "http://$server/v1" > /dev/null \
|
||||
&& unsandboxed hermes config set model.default "$model" > /dev/null || return 1
|
||||
echo "Hermes: now uses $model."
|
||||
}
|
||||
|
||||
# Hermes' gateway reads the sandbox config when it starts, so it can't reach a
|
||||
# new server until it restarts. (A sandboxed opencode starts its own server
|
||||
# each time, so it reads the config again then.)
|
||||
restart_gateway() {
|
||||
if systemctl --user is-active -q hermes-gateway 2> /dev/null \
|
||||
&& confirm_yes "Restart Hermes' gateway so it can reach the server?"; then
|
||||
systemctl --user restart hermes-gateway || return 1
|
||||
fi
|
||||
}
|
||||
|
||||
saved_servers() {
|
||||
grep -v '^$' "$AI_SERVERS_CONF" 2> /dev/null
|
||||
}
|
||||
|
||||
# Choose a model from the server and set the agents up to use it
|
||||
add_server() {
|
||||
local server="$1" kind models model choice i
|
||||
mapfile -t models < <(server_models "$server")
|
||||
if [ ${#models[@]} -eq 0 ]; then
|
||||
echo "$server doesn't answer like an LLM server (http://$server/v1/models)."
|
||||
return 1
|
||||
fi
|
||||
kind="$(server_kind "$server")"
|
||||
|
||||
echo
|
||||
echo "$kind on $server has these models:"
|
||||
for i in "${!models[@]}"; do
|
||||
printf ' %2d) %s\n' $((i + 1)) "${models[$i]}"
|
||||
done
|
||||
read -rp "Which should the agents use? [1] " choice
|
||||
choice="${choice:-1}"
|
||||
if ! [[ "$choice" =~ ^[0-9]+$ ]] || (( choice < 1 || choice > ${#models[@]} )); then
|
||||
echo "Choose a number from the list."
|
||||
return 1
|
||||
fi
|
||||
model="${models[$((choice - 1))]}"
|
||||
|
||||
sandbox_allow "$server" || return 1
|
||||
claude_allow "$server" || return 1
|
||||
grep -qsxF "$server" "$AI_SERVERS_CONF" || echo "$server" >> "$AI_SERVERS_CONF"
|
||||
echo "The sandbox lets the agents reach $server."
|
||||
|
||||
if [ -x /usr/bin/opencode ]; then
|
||||
opencode_add "$server" "$kind" "$model" || return 1
|
||||
fi
|
||||
if [ -x "$HOME/.local/bin/hermes" ]; then
|
||||
hermes_use "$server" "$model" || return 1
|
||||
fi
|
||||
restart_gateway
|
||||
}
|
||||
|
||||
remove_server() {
|
||||
local server="$1" tmp
|
||||
sandbox_disallow "$server" || return 1
|
||||
claude_disallow "$server" || return 1
|
||||
opencode_remove "$server" || return 1
|
||||
tmp="$(mktemp)" || return 1
|
||||
grep -vxF "$server" "$AI_SERVERS_CONF" > "$tmp"
|
||||
mv -f "$tmp" "$AI_SERVERS_CONF"
|
||||
echo "The agents can no longer reach $server."
|
||||
if grep -qsF "$server" "$HOME/.hermes/config.yaml"; then
|
||||
echo "Hermes still uses it; choose another model provider with: configure hermes"
|
||||
fi
|
||||
}
|
||||
|
||||
# Find servers on the network and add the one you choose
|
||||
find_and_add() {
|
||||
local servers server models choice i
|
||||
echo "Looking for LLM servers on your network..."
|
||||
mapfile -t servers < <(find_servers)
|
||||
if [ ${#servers[@]} -eq 0 ]; then
|
||||
echo "No LLM servers found. A server has to accept connections from other machines:"
|
||||
echo "in Ollama's settings, turn on \"Expose Ollama to the network\" (or set OLLAMA_HOST=0.0.0.0);"
|
||||
echo "in LM Studio, turn on \"Serve on Local Network\". Or enter its address instead."
|
||||
return 0
|
||||
fi
|
||||
for i in "${!servers[@]}"; do
|
||||
IFS='|' read -r server models <<< "${servers[$i]}"
|
||||
printf ' %2d) %-28s %s, %s models\n' $((i + 1)) "$server" "$(server_kind "$server")" "$models"
|
||||
done
|
||||
read -rp "Which server? " choice
|
||||
if ! [[ "$choice" =~ ^[0-9]+$ ]] || (( choice < 1 || choice > ${#servers[@]} )); then
|
||||
echo "Choose a number from the list."
|
||||
return 1
|
||||
fi
|
||||
add_server "${servers[$((choice - 1))]%%|*}"
|
||||
}
|
||||
|
||||
# host, host:port or a URL such as http://host:port/v1 -> host:port
|
||||
enter_and_add() {
|
||||
local address
|
||||
read -rp "The server's address, e.g. llm.local or llm.local:11434: " address
|
||||
address="${address#*://}"
|
||||
address="${address%%/*}"
|
||||
[ -n "$address" ] || return 0
|
||||
[[ "$address" == *:* ]] || address="$address:11434"
|
||||
add_server "$address"
|
||||
}
|
||||
|
||||
choose_and_remove() {
|
||||
local servers choice i
|
||||
mapfile -t servers < <(saved_servers)
|
||||
for i in "${!servers[@]}"; do
|
||||
printf ' %2d) %s\n' $((i + 1)) "${servers[$i]}"
|
||||
done
|
||||
read -rp "Which server? " choice
|
||||
if ! [[ "$choice" =~ ^[0-9]+$ ]] || (( choice < 1 || choice > ${#servers[@]} )); then
|
||||
echo "Choose a number from the list."
|
||||
return 1
|
||||
fi
|
||||
remove_server "${servers[$((choice - 1))]}"
|
||||
}
|
||||
|
||||
run_configure() {
|
||||
local choice
|
||||
mkdir -p "$(dirname "$AI_SERVERS_CONF")" && touch "$AI_SERVERS_CONF" || return 1
|
||||
echo "Agents run in a sandbox that only lets them reach the servers you allow. This finds"
|
||||
echo "LLM servers on your network, such as Ollama on a Mac, allows them and sets up the agents."
|
||||
|
||||
while true; do
|
||||
echo
|
||||
if [ -s "$AI_SERVERS_CONF" ]; then
|
||||
echo "The agents can reach: $(saved_servers | paste -sd' ')"
|
||||
echo
|
||||
fi
|
||||
echo " 1) Find servers on your network"
|
||||
echo " 2) Enter a server's address"
|
||||
[ -s "$AI_SERVERS_CONF" ] && echo " 3) Remove a server"
|
||||
echo " q) Done"
|
||||
echo
|
||||
read -rp "Choose: " choice || return 0
|
||||
case "$choice" in
|
||||
1) find_and_add ;;
|
||||
2) enter_and_add ;;
|
||||
3) [ -s "$AI_SERVERS_CONF" ] && choose_and_remove ;;
|
||||
*) return 0 ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
|
||||
# configure reads TITLE and DESCRIPTION
|
||||
# shellcheck disable=SC2034
|
||||
TITLE="Printers"
|
||||
# shellcheck disable=SC2034
|
||||
DESCRIPTION="Add your network's printers to every print dialog"
|
||||
|
||||
# Exists once you've run this, even if you added none, so this isn't "to do"
|
||||
# on machines without a printer
|
||||
PRINTERS_CONF="$HOME/.config/coredesktop/printers"
|
||||
|
||||
is_installed() {
|
||||
command -v lpadmin
|
||||
}
|
||||
|
||||
# Set up here, or a network printer was already added some other way
|
||||
is_configured() {
|
||||
[ -f "$PRINTERS_CONF" ] || LC_ALL=C lpstat -v 2> /dev/null | grep -qE ': (ipps?|dnssd)://'
|
||||
}
|
||||
|
||||
run_configure() {
|
||||
bash "$COREDESKTOP/system/setup-printers.sh" || return 1
|
||||
mkdir -p "$(dirname "$PRINTERS_CONF")" && touch "$PRINTERS_CONF"
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
#!/bin/bash
|
||||
|
||||
# configure reads TITLE and DESCRIPTION
|
||||
# shellcheck disable=SC2034
|
||||
TITLE="Network storage"
|
||||
# shellcheck disable=SC2034
|
||||
DESCRIPTION="Mount shared folders from your network in /mnt"
|
||||
|
||||
# What's mounted, one "kind|source|mount point" per line, e.g.
|
||||
# smb|//pi-star.local/files|/mnt/pi-star-files. It exists once you've run
|
||||
# this, even if you mounted nothing, so this isn't "to do" without a server.
|
||||
STORAGE_CONF="$HOME/.config/coredesktop/storage"
|
||||
# Passwords for SMB shares, readable only by root, which mounts them
|
||||
SMB_CREDENTIALS="/etc/coredesktop/smb"
|
||||
BOOKMARKS="$HOME/.config/gtk-3.0/bookmarks"
|
||||
source "$COREDESKTOP/configure.d/lib/mdns.sh"
|
||||
|
||||
is_installed() {
|
||||
command -v avahi-browse
|
||||
}
|
||||
|
||||
is_configured() {
|
||||
[ -f "$STORAGE_CONF" ]
|
||||
}
|
||||
|
||||
# A name usable in a path: "Pi-Star Files" -> pi-star-files
|
||||
slug() {
|
||||
local s
|
||||
s="$(tr '[:upper:]' '[:lower:]' <<< "$1" | sed -E 's/[^a-z0-9._-]+/-/g; s/^-+//; s/-+$//')"
|
||||
echo "${s:-share}"
|
||||
}
|
||||
|
||||
# /mnt/<host>[-<name>], with -2, -3... if that's taken
|
||||
mount_point() {
|
||||
local base n=2 point
|
||||
base="/mnt/$(slug "${1%.local}")"
|
||||
[ -n "$2" ] && base="$base-$(slug "$2")"
|
||||
point="$base"
|
||||
while [ -e "$point" ] || grep -qsF "|$point" "$STORAGE_CONF"; do
|
||||
point="$base-$n"
|
||||
n=$((n + 1))
|
||||
done
|
||||
echo "$point"
|
||||
}
|
||||
|
||||
# --- SMB: Windows, Samba (e.g. a NAS) and Mac file sharing --------------------
|
||||
|
||||
# The disk shares on an SMB server, one per line, leaving out admin shares
|
||||
# like C$. smbclient reads the password from PASSWD, not the command line.
|
||||
smb_shares() {
|
||||
local host="$1" user="$2" password="$3" auth=(-N)
|
||||
[ -n "$user" ] && auth=(-U "$user")
|
||||
PASSWD="$password" timeout 20 smbclient -L "//$host" "${auth[@]}" -g 2> /dev/null \
|
||||
| awk -F'|' '$1 == "Disk" && $2 !~ /\$$/ { print $2 }'
|
||||
}
|
||||
|
||||
# The /etc/fstab line: root mounts the share the first time something opens
|
||||
# the folder (x-systemd.automount), as you, and unmounts it after 10 idle
|
||||
# minutes. nofail and the timeout keep a missing server from holding up boot
|
||||
# or hanging whatever opened the folder. Spaces are \040 in fstab.
|
||||
smb_fstab_line() {
|
||||
local source="${1// /\\040}" point="$2" credentials="$3" auth
|
||||
if [ -n "$credentials" ]; then auth="credentials=$credentials"; else auth="guest"; fi
|
||||
echo "$source $point cifs $auth,uid=$(id -u),gid=$(id -g),iocharset=utf8,nofail,_netdev,x-systemd.automount,x-systemd.idle-timeout=600,x-systemd.mount-timeout=20 0 0"
|
||||
}
|
||||
|
||||
add_smb() {
|
||||
local host="$1" share="$2" user password shares point source credentials="" choice i
|
||||
echo "Sign in to $host. Leave the user name empty for a share anyone can open."
|
||||
read -rp "User name: " user
|
||||
password=""
|
||||
if [ -n "$user" ]; then
|
||||
read -rsp "Password: " password
|
||||
echo
|
||||
fi
|
||||
|
||||
if [ -z "$share" ]; then
|
||||
mapfile -t shares < <(smb_shares "$host" "$user" "$password")
|
||||
if [ ${#shares[@]} -eq 0 ]; then
|
||||
echo "$host didn't list any shares for ${user:-guests} (wrong password, or it doesn't list them)."
|
||||
read -rp "The share's name, if you know it (Enter to stop): " share
|
||||
[ -n "$share" ] || return 1
|
||||
else
|
||||
for i in "${!shares[@]}"; do
|
||||
printf ' %2d) %s\n' $((i + 1)) "${shares[$i]}"
|
||||
done
|
||||
read -rp "Which share? " choice
|
||||
if ! [[ "$choice" =~ ^[0-9]+$ ]] || (( choice < 1 || choice > ${#shares[@]} )); then
|
||||
echo "Choose a number from the list."
|
||||
return 1
|
||||
fi
|
||||
share="${shares[$((choice - 1))]}"
|
||||
fi
|
||||
fi
|
||||
|
||||
source="//$host/$share"
|
||||
point="$(mount_point "$host" "$share")"
|
||||
if [ -n "$user" ]; then
|
||||
credentials="$SMB_CREDENTIALS/$(basename "$point")"
|
||||
printf 'username=%s\npassword=%s\n' "$user" "$password" \
|
||||
| sudo install -D -m 600 -o root -g root /dev/stdin "$credentials" || return 1
|
||||
fi
|
||||
sudo install -d "$point" || return 1
|
||||
smb_fstab_line "$source" "$point" "$credentials" | sudo tee -a /etc/fstab > /dev/null || return 1
|
||||
sudo systemctl daemon-reload || return 1
|
||||
sudo systemctl restart "$(systemd-escape -p --suffix=automount "$point")" || return 1
|
||||
finish_add smb "$source" "$point" "$share on ${host%.local}"
|
||||
}
|
||||
|
||||
remove_smb() {
|
||||
local point="$1" tmp
|
||||
sudo systemctl stop "$(systemd-escape -p --suffix=automount "$point")" \
|
||||
"$(systemd-escape -p --suffix=mount "$point")" 2> /dev/null
|
||||
tmp="$(mktemp)" || return 1
|
||||
awk -v point="$point" '!($2 == point && $3 == "cifs")' /etc/fstab > "$tmp"
|
||||
sudo cp /etc/fstab /etc/fstab.bak && sudo install -m 644 "$tmp" /etc/fstab || { rm -f "$tmp"; return 1; }
|
||||
rm -f "$tmp"
|
||||
sudo systemctl daemon-reload
|
||||
sudo rm -f "$SMB_CREDENTIALS/$(basename "$point")"
|
||||
sudo rmdir "$point" 2> /dev/null
|
||||
return 0
|
||||
}
|
||||
|
||||
# --- SFTP: any machine you can ssh to --------------------------------------------
|
||||
|
||||
sftp_unit_name() {
|
||||
echo "coredesktop-sftp-$(basename "$1").service"
|
||||
}
|
||||
|
||||
# systemd would expand $ and % in the unit's ExecStart
|
||||
sftp_unit() {
|
||||
local source="$1" point="$2"
|
||||
cat << EOF
|
||||
# Written by CoreDesktop (configure storage)
|
||||
[Unit]
|
||||
Description=Mount $source at $point
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
ExecStart=%h/.local/share/coredesktop/system/sftp-mount "$source" "$point"
|
||||
ExecStop=/usr/bin/fusermount3 -u "$point"
|
||||
Restart=on-failure
|
||||
RestartSec=30
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
EOF
|
||||
}
|
||||
|
||||
add_sftp() {
|
||||
local host="$1" user="${2:-$USER}" path="$3" source point name unit
|
||||
if ! command -v sshfs > /dev/null; then
|
||||
echo "Installing sshfs..."
|
||||
sudo pacman -S --needed --noconfirm sshfs || return 1
|
||||
fi
|
||||
read -rp "User name on $host [$user]: " name
|
||||
user="${name:-$user}"
|
||||
if [ -z "$path" ]; then
|
||||
read -rp "Folder on $host (Enter for your home folder): " path
|
||||
fi
|
||||
if [[ "$user$host$path" == *[\$%\"\\]* ]]; then
|
||||
echo "The user name and folder can't contain \$, %, \" or \\."
|
||||
return 1
|
||||
fi
|
||||
source="$user@$host:$path"
|
||||
|
||||
# Accepts the server's key the first time; the mount can't ask
|
||||
echo "Checking that $host accepts one of your SSH keys..."
|
||||
if [ -S "$HOME/.bitwarden-ssh-agent.sock" ]; then
|
||||
export SSH_AUTH_SOCK="$HOME/.bitwarden-ssh-agent.sock"
|
||||
fi
|
||||
ssh -o ConnectTimeout=10 -o PasswordAuthentication=no -o KbdInteractiveAuthentication=no \
|
||||
"$user@$host" true || {
|
||||
echo "$host didn't accept any of your SSH keys, and the mount can't ask for a password."
|
||||
echo "Add your key to $host first (e.g. ssh-copy-id $user@$host), then try again."
|
||||
return 1
|
||||
}
|
||||
|
||||
point="$(mount_point "$host" "$(basename "${path:-}")")"
|
||||
unit="$(sftp_unit_name "$point")"
|
||||
sudo install -d -o "$USER" -g "$(id -gn)" "$point" || return 1
|
||||
mkdir -p "$HOME/.config/systemd/user"
|
||||
sftp_unit "$source" "$point" > "$HOME/.config/systemd/user/$unit" || return 1
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable --now "$unit" || return 1
|
||||
finish_add sftp "$source" "$point" "${host%.local}${path:+ $(basename "$path")}"
|
||||
}
|
||||
|
||||
remove_sftp() {
|
||||
local point="$1" unit
|
||||
unit="$(sftp_unit_name "$point")"
|
||||
systemctl --user disable --now "$unit" 2> /dev/null
|
||||
rm -f "$HOME/.config/systemd/user/$unit"
|
||||
systemctl --user daemon-reload
|
||||
sudo rmdir "$point" 2> /dev/null
|
||||
return 0
|
||||
}
|
||||
|
||||
# --- Both ------------------------------------------------------------------------
|
||||
|
||||
# Record it, add it to the Files sidebar and check it opens
|
||||
finish_add() {
|
||||
local kind="$1" source="$2" point="$3" label="$4"
|
||||
echo "$kind|$source|$point" >> "$STORAGE_CONF"
|
||||
mkdir -p "$(dirname "$BOOKMARKS")"
|
||||
grep -qs "^file://$point " "$BOOKMARKS" || echo "file://$point $label" >> "$BOOKMARKS"
|
||||
if timeout 30 ls "$point" > /dev/null 2>&1 && mountpoint -q "$point"; then
|
||||
echo "$source is at $point (\"$label\" in the Files sidebar)."
|
||||
else
|
||||
echo "$source was added at $point, but didn't open yet. If it doesn't, see:"
|
||||
if [ "$kind" = smb ]; then
|
||||
echo " journalctl -b -u '$(systemd-escape -p --suffix=mount "$point")'"
|
||||
else
|
||||
echo " journalctl --user -u $(sftp_unit_name "$point")"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
remove_storage() {
|
||||
local kind="$1" point="$3" tmp
|
||||
"remove_$kind" "$point" || return 1
|
||||
tmp="$(mktemp)" || return 1
|
||||
grep -vF "|$point" "$STORAGE_CONF" > "$tmp"
|
||||
mv -f "$tmp" "$STORAGE_CONF"
|
||||
if [ -f "$BOOKMARKS" ]; then
|
||||
grep -v "^file://$point " "$BOOKMARKS" > "$tmp"
|
||||
mv -f "$tmp" "$BOOKMARKS"
|
||||
fi
|
||||
echo "$point is no longer mounted."
|
||||
}
|
||||
|
||||
# The file servers on the network: "kind|host|name" per line
|
||||
find_storage() {
|
||||
local own name type host
|
||||
own="$(mdns_own_host)"
|
||||
mdns_services | while IFS='|' read -r name type host _; do
|
||||
[[ "${host,,}" == "${own,,}" ]] && continue
|
||||
case "$type" in
|
||||
_smb._tcp) echo "smb|$host|$name" ;;
|
||||
_sftp-ssh._tcp) echo "sftp|$host|$name" ;;
|
||||
esac
|
||||
done | sort -u
|
||||
}
|
||||
|
||||
find_and_add() {
|
||||
local found kind host name choice i
|
||||
echo "Looking for shared folders on your network..."
|
||||
mapfile -t found < <(find_storage)
|
||||
if [ ${#found[@]} -eq 0 ]; then
|
||||
echo "Nothing found. A Mac shares folders over SMB once File Sharing is on (System Settings >"
|
||||
echo "General > Sharing). Or enter the address instead."
|
||||
return 0
|
||||
fi
|
||||
for i in "${!found[@]}"; do
|
||||
IFS='|' read -r kind host name <<< "${found[$i]}"
|
||||
printf ' %2d) %-26s %s\n' $((i + 1)) "$host" "$( [ "$kind" = smb ] && echo "SMB shared folders" || echo "SFTP (over SSH)")"
|
||||
done
|
||||
read -rp "Which? " choice
|
||||
if ! [[ "$choice" =~ ^[0-9]+$ ]] || (( choice < 1 || choice > ${#found[@]} )); then
|
||||
echo "Choose a number from the list."
|
||||
return 1
|
||||
fi
|
||||
IFS='|' read -r kind host _ <<< "${found[$((choice - 1))]}"
|
||||
"add_$kind" "$host"
|
||||
}
|
||||
|
||||
# smb://host/share or sftp://[user@]host[/path]
|
||||
enter_and_add() {
|
||||
local address rest host user="" path
|
||||
read -rp "Address, e.g. smb://nas.local/files or sftp://me@server.local/srv/data: " address
|
||||
rest="${address#*://}"
|
||||
case "$address" in
|
||||
smb://*/?*) add_smb "${rest%%/*}" "${rest#*/}" ;;
|
||||
sftp://*)
|
||||
[[ "$rest" == *@* ]] && { user="${rest%%@*}"; rest="${rest#*@}"; }
|
||||
host="${rest%%/*}"
|
||||
path=""
|
||||
[[ "$rest" == */* ]] && path="/${rest#*/}"
|
||||
add_sftp "$host" "$user" "$path" ;;
|
||||
"") return 0 ;;
|
||||
*) echo "Start it with smb:// (and include the share) or sftp://."; return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
choose_and_remove() {
|
||||
local entries choice i
|
||||
mapfile -t entries < <(grep -v '^$' "$STORAGE_CONF")
|
||||
for i in "${!entries[@]}"; do
|
||||
IFS='|' read -r _ source point <<< "${entries[$i]}"
|
||||
printf ' %2d) %-30s %s\n' $((i + 1)) "$point" "$source"
|
||||
done
|
||||
read -rp "Which? " choice
|
||||
if ! [[ "$choice" =~ ^[0-9]+$ ]] || (( choice < 1 || choice > ${#entries[@]} )); then
|
||||
echo "Choose a number from the list."
|
||||
return 1
|
||||
fi
|
||||
IFS='|' read -r -a entry <<< "${entries[$((choice - 1))]}"
|
||||
remove_storage "${entry[@]}"
|
||||
}
|
||||
|
||||
run_configure() {
|
||||
local choice source point
|
||||
mkdir -p "$(dirname "$STORAGE_CONF")" && touch "$STORAGE_CONF" || return 1
|
||||
echo "This mounts shared folders from other machines on your network in /mnt: SMB shares"
|
||||
echo "(Windows, a NAS, a Mac with File Sharing on) and, over SSH, any machine you can ssh to."
|
||||
|
||||
while true; do
|
||||
echo
|
||||
if [ -s "$STORAGE_CONF" ]; then
|
||||
while IFS='|' read -r _ source point; do
|
||||
echo " $point <- $source"
|
||||
done < "$STORAGE_CONF"
|
||||
echo
|
||||
fi
|
||||
echo " 1) Find shared folders on your network"
|
||||
echo " 2) Enter an address"
|
||||
[ -s "$STORAGE_CONF" ] && echo " 3) Remove one"
|
||||
echo " q) Done"
|
||||
echo
|
||||
read -rp "Choose: " choice || return 0
|
||||
case "$choice" in
|
||||
1) find_and_add ;;
|
||||
2) enter_and_add ;;
|
||||
3) [ -s "$STORAGE_CONF" ] && choose_and_remove ;;
|
||||
*) return 0 ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Purpose : Find what's on the local network over mDNS (Avahi), for the
|
||||
# configure.d items that set up servers, printers and storage on it.
|
||||
# Source this file.
|
||||
|
||||
# Every service Avahi can see, or only those of type $1 (e.g. _ipp._tcp), one
|
||||
# per line after waiting a few seconds for answers:
|
||||
# "name|type|host|address|port|txt". IPv4 only, so a machine isn't listed once
|
||||
# per address family. The name has Avahi's escapes decoded.
|
||||
mdns_services() {
|
||||
local name type host address port txt
|
||||
timeout 15 avahi-browse "${1:---all}" --resolve --terminate --parsable --no-db-lookup 2> /dev/null \
|
||||
| awk -F';' '$1 == "=" && $3 == "IPv4"' \
|
||||
| while IFS=';' read -r _ _ _ name type _ host address port txt; do
|
||||
printf '%s|%s|%s|%s|%s|%s\n' "$(mdns_unescape "$name")" "$type" "$host" "$address" "$port" "$txt"
|
||||
done | sort -u
|
||||
}
|
||||
|
||||
# Avahi writes a space as \032 and a dot in a name as \. (decimal escapes)
|
||||
mdns_unescape() {
|
||||
local s="$1" out="" ch code escape='^([^\\]*)\\([0-9][0-9][0-9]|.)(.*)$'
|
||||
while [[ "$s" =~ $escape ]]; do
|
||||
out+="${BASH_REMATCH[1]}"
|
||||
code="${BASH_REMATCH[2]}"
|
||||
s="${BASH_REMATCH[3]}"
|
||||
if [[ "$code" == [0-9][0-9][0-9] ]]; then
|
||||
# shellcheck disable=SC2059
|
||||
printf -v ch "\\$(printf %03o "$((10#$code))")"
|
||||
out+="$ch"
|
||||
else
|
||||
out+="$code"
|
||||
fi
|
||||
done
|
||||
echo "$out$s"
|
||||
}
|
||||
|
||||
# This machine's own .local name, which mdns_hosts leaves out
|
||||
mdns_own_host() {
|
||||
echo "$(hostnamectl hostname 2> /dev/null || cat /etc/hostname).local"
|
||||
}
|
||||
|
||||
# The other machines on the network: one .local name per line
|
||||
mdns_hosts() {
|
||||
local own
|
||||
own="$(mdns_own_host)"
|
||||
mdns_services | cut -d'|' -f3 | grep -vixF "$own" | sort -u
|
||||
}
|
||||
Reference in New Issue
Block a user