1
0
forked from finn/tinyboard
Files
tinyboard/syncthing.sh

435 lines
16 KiB
Bash
Raw Normal View History

#!/usr/bin/env bash
set -euo pipefail
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
info() { echo -e "${GREEN}[+]${NC} $*"; }
warn() { echo -e "${YELLOW}[!]${NC} $*"; }
die() { echo -e "${RED}[ERROR]${NC} $*" >&2; exit 1; }
header() { echo -e "\n${CYAN}══════════════════════════════════════════${NC}"; echo -e "${CYAN} $*${NC}"; echo -e "${CYAN}══════════════════════════════════════════${NC}"; }
ST_URL="http://127.0.0.1:8384"
APIKEY=""
get_apikey() {
local container
container=$(docker ps --format '{{.Names}}' | grep -i syncthing | head -1 || true)
[ -n "$container" ] || die "No running Syncthing container found."
APIKEY=$(docker exec "$container" cat /var/syncthing/config/config.xml 2>/dev/null | grep -oP '(?<=<apikey>)[^<]+' || true)
[ -n "$APIKEY" ] || die "Could not read Syncthing API key from container '$container'."
}
st_get() {
curl -sf -H "X-API-Key: $APIKEY" "${ST_URL}${1}"
}
st_post() {
curl -sf -X POST -H "X-API-Key: $APIKEY" -H "Content-Type: application/json" -d "$2" "${ST_URL}${1}"
}
st_put() {
curl -sf -X PUT -H "X-API-Key: $APIKEY" -H "Content-Type: application/json" -d "$2" "${ST_URL}${1}"
}
st_delete() {
curl -sf -X DELETE -H "X-API-Key: $APIKEY" "${ST_URL}${1}"
}
check_deps() {
local missing=()
for cmd in "$@"; do
if ! command -v "$cmd" >/dev/null 2>&1; then
missing+=("$cmd")
fi
done
if [ ${#missing[@]} -gt 0 ]; then
die "Missing required dependencies: ${missing[*]}"
fi
}
show_own_device_id() {
header "This Device's ID"
local id
id=$(st_get /rest/system/status | python3 -c 'import sys,json; print(json.load(sys.stdin)["myID"])')
echo -e " ${GREEN}${id}${NC}"
echo ""
}
show_pending_devices() {
header "Pending Devices"
local pending
pending=$(st_get /rest/cluster/pending/devices)
local count
count=$(echo "$pending" | python3 -c 'import sys,json; d=json.load(sys.stdin); print(len(d))')
if [ "$count" -eq 0 ]; then
warn "No pending devices."
return
fi
echo "$pending" | python3 -c '
import sys, json
devices = json.load(sys.stdin)
for device_id, info in devices.items():
name = info.get("name", "(unknown)")
print(f" Name: {name}")
print(f" ID: {device_id}")
print()
'
read -rp "Add a pending device? [y/N]: " ADD_PENDING
if [[ "${ADD_PENDING,,}" == "y" ]]; then
add_device_by_pending "$pending"
fi
}
add_device_by_pending() {
local pending="$1"
local ids
ids=$(echo "$pending" | python3 -c 'import sys,json; [print(k) for k in json.load(sys.stdin)]')
local id_list=()
while IFS= read -r line; do
id_list+=("$line")
done <<< "$ids"
if [ ${#id_list[@]} -eq 1 ]; then
DEVICE_ID="${id_list[0]}"
local pending_name
pending_name=$(echo "$pending" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['${DEVICE_ID}'].get('name',''))")
else
echo "Pending devices:"
local i=1
for id in "${id_list[@]}"; do
local name
name=$(echo "$pending" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['${id}'].get('name','(unknown)'))")
echo " $i) $name$id"
i=$((i+1))
done
read -rp "Choose [1-${#id_list[@]}]: " CHOICE
[[ "$CHOICE" =~ ^[0-9]+$ ]] && [ "$CHOICE" -ge 1 ] && [ "$CHOICE" -le "${#id_list[@]}" ] || die "Invalid choice."
DEVICE_ID="${id_list[$((CHOICE-1))]}"
pending_name=$(echo "$pending" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['${DEVICE_ID}'].get('name',''))")
fi
read -rp "Device name [${pending_name:-new-device}]: " DEVICE_NAME
DEVICE_NAME="${DEVICE_NAME:-${pending_name:-new-device}}"
_do_add_device "$DEVICE_ID" "$DEVICE_NAME"
}
list_devices() {
header "Devices"
st_get /rest/config/devices | python3 -c '
import sys, json
devices = json.load(sys.stdin)
if not devices:
print(" No devices configured.")
else:
for d in devices:
print(f" Name: {d[\"name\"]}")
print(f" ID: {d[\"deviceID\"]}")
print()
'
}
add_device() {
header "Add Device"
read -rp "Device ID: " DEVICE_ID
[ -n "$DEVICE_ID" ] || die "Device ID cannot be empty."
read -rp "Device name: " DEVICE_NAME
[ -n "$DEVICE_NAME" ] || die "Device name cannot be empty."
_do_add_device "$DEVICE_ID" "$DEVICE_NAME"
}
_do_add_device() {
local device_id="$1"
local device_name="$2"
local existing
existing=$(st_get /rest/config/devices)
local already
already=$(echo "$existing" | python3 -c "import sys,json; devs=json.load(sys.stdin); print(any(d['deviceID']=='${device_id}' for d in devs))")
if [ "$already" = "True" ]; then
warn "Device '${device_name}' is already configured."
return
fi
local payload
payload=$(python3 -c "import json; print(json.dumps({'deviceID': '${device_id}', 'name': '${device_name}', 'addresses': ['dynamic'], 'autoAcceptFolders': False}))")
st_post /rest/config/devices "$payload" >/dev/null
info "Device '${device_name}' added."
}
remove_device() {
header "Remove Device"
local devices
devices=$(st_get /rest/config/devices)
local count
count=$(echo "$devices" | python3 -c 'import sys,json; print(len(json.load(sys.stdin)))')
[ "$count" -gt 0 ] || { warn "No devices configured."; return; }
echo "$devices" | python3 -c '
import sys, json
for i, d in enumerate(json.load(sys.stdin), 1):
print(f" {i}) {d[\"name\"]} — {d[\"deviceID\"]}")
'
echo ""
read -rp "Choose device to remove [1-${count}]: " CHOICE
[[ "$CHOICE" =~ ^[0-9]+$ ]] && [ "$CHOICE" -ge 1 ] && [ "$CHOICE" -le "$count" ] || die "Invalid choice."
local device_id device_name
device_id=$(echo "$devices" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d[$((CHOICE-1))]['deviceID'])")
device_name=$(echo "$devices" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d[$((CHOICE-1))]['name'])")
local folders
folders=$(st_get /rest/config/folders)
local shared_folders
shared_folders=$(echo "$folders" | python3 -c "
import sys, json
folders = json.load(sys.stdin)
shared = [f['label'] or f['id'] for f in folders if any(dev['deviceID']=='${device_id}' for dev in f.get('devices',[]))]
print('\n'.join(shared))
")
if [ -n "$shared_folders" ]; then
warn "Device '${device_name}' is still sharing these folders:"
echo "$shared_folders" | sed 's/^/ /'
warn "Removing the device will unshare these folders from it."
fi
read -rp "Remove '${device_name}'? [y/N]: " CONFIRM
[[ "${CONFIRM,,}" == "y" ]] || { info "Aborted."; return; }
st_delete "/rest/config/devices/${device_id}" >/dev/null
info "Device '${device_name}' removed."
}
list_folders() {
header "Folders"
st_get /rest/config/folders | python3 -c '
import sys, json
folders = json.load(sys.stdin)
if not folders:
print(" No folders configured.")
else:
for f in folders:
label = f["label"] or f["id"]
shared = [d["deviceID"][:8]+"..." for d in f.get("devices", [])]
print(f" Label: {label}")
print(f" ID: {f[\"id\"]}")
print(f" Path: {f[\"path\"]}")
print(f" Shared: {len(shared)} device(s)")
print()
'
}
add_folder() {
header "Add Folder"
read -rp "Folder path on this device (e.g. /var/syncthing/docs): " FOLDER_PATH
[ -n "$FOLDER_PATH" ] || die "Path cannot be empty."
read -rp "Folder label (human-readable name): " FOLDER_LABEL
[ -n "$FOLDER_LABEL" ] || die "Label cannot be empty."
read -rp "Folder ID (leave blank to use label): " FOLDER_ID
FOLDER_ID="${FOLDER_ID:-$FOLDER_LABEL}"
local existing
existing=$(st_get /rest/config/folders)
local already
already=$(echo "$existing" | python3 -c "import sys,json; folders=json.load(sys.stdin); print(any(f['id']=='${FOLDER_ID}' for f in folders))")
[ "$already" = "False" ] || die "Folder ID '${FOLDER_ID}' already exists."
local payload
payload=$(python3 -c "import json; print(json.dumps({'id': '${FOLDER_ID}', 'label': '${FOLDER_LABEL}', 'path': '${FOLDER_PATH}', 'type': 'sendreceive', 'devices': []}))")
st_post /rest/config/folders "$payload" >/dev/null
info "Folder '${FOLDER_LABEL}' added."
}
remove_folder() {
header "Remove Folder"
local folders
folders=$(st_get /rest/config/folders)
local count
count=$(echo "$folders" | python3 -c 'import sys,json; print(len(json.load(sys.stdin)))')
[ "$count" -gt 0 ] || { warn "No folders configured."; return; }
echo "$folders" | python3 -c '
import sys, json
for i, f in enumerate(json.load(sys.stdin), 1):
label = f["label"] or f["id"]
print(f" {i}) {label} ({f[\"path\"]})")
'
echo ""
read -rp "Choose folder to remove [1-${count}]: " CHOICE
[[ "$CHOICE" =~ ^[0-9]+$ ]] && [ "$CHOICE" -ge 1 ] && [ "$CHOICE" -le "$count" ] || die "Invalid choice."
local folder_id folder_label
folder_id=$(echo "$folders" | python3 -c "import sys,json; f=json.load(sys.stdin)[$((CHOICE-1))]; print(f['id'])")
folder_label=$(echo "$folders" | python3 -c "import sys,json; f=json.load(sys.stdin)[$((CHOICE-1))]; print(f['label'] or f['id'])")
read -rp "Remove folder '${folder_label}'? [y/N]: " CONFIRM
[[ "${CONFIRM,,}" == "y" ]] || { info "Aborted."; return; }
st_delete "/rest/config/folders/${folder_id}" >/dev/null
info "Folder '${folder_label}' removed."
}
share_folder() {
header "Share Folder with Device"
local folders devices
folders=$(st_get /rest/config/folders)
devices=$(st_get /rest/config/devices)
local f_count d_count
f_count=$(echo "$folders" | python3 -c 'import sys,json; print(len(json.load(sys.stdin)))')
d_count=$(echo "$devices" | python3 -c 'import sys,json; print(len(json.load(sys.stdin)))')
[ "$f_count" -gt 0 ] || { warn "No folders configured."; return; }
[ "$d_count" -gt 0 ] || { warn "No devices configured."; return; }
echo "Folders:"
echo "$folders" | python3 -c '
import sys, json
for i, f in enumerate(json.load(sys.stdin), 1):
label = f["label"] or f["id"]
print(f" {i}) {label}")
'
echo ""
read -rp "Choose folder [1-${f_count}]: " F_CHOICE
[[ "$F_CHOICE" =~ ^[0-9]+$ ]] && [ "$F_CHOICE" -ge 1 ] && [ "$F_CHOICE" -le "$f_count" ] || die "Invalid choice."
echo ""
echo "Devices:"
echo "$devices" | python3 -c '
import sys, json
for i, d in enumerate(json.load(sys.stdin), 1):
print(f" {i}) {d[\"name\"]}")
'
echo ""
read -rp "Choose device [1-${d_count}]: " D_CHOICE
[[ "$D_CHOICE" =~ ^[0-9]+$ ]] && [ "$D_CHOICE" -ge 1 ] && [ "$D_CHOICE" -le "$d_count" ] || die "Invalid choice."
local folder_id device_id device_name folder_label
folder_id=$(echo "$folders" | python3 -c "import sys,json; f=json.load(sys.stdin)[$((F_CHOICE-1))]; print(f['id'])")
folder_label=$(echo "$folders" | python3 -c "import sys,json; f=json.load(sys.stdin)[$((F_CHOICE-1))]; print(f['label'] or f['id'])")
device_id=$(echo "$devices" | python3 -c "import sys,json; d=json.load(sys.stdin)[$((D_CHOICE-1))]; print(d['deviceID'])")
device_name=$(echo "$devices" | python3 -c "import sys,json; d=json.load(sys.stdin)[$((D_CHOICE-1))]; print(d['name'])")
local folder_config already
folder_config=$(st_get "/rest/config/folders/${folder_id}")
already=$(echo "$folder_config" | python3 -c "import sys,json; f=json.load(sys.stdin); print(any(d['deviceID']=='${device_id}' for d in f.get('devices',[])))")
if [ "$already" = "True" ]; then
warn "Folder '${folder_label}' is already shared with '${device_name}'."
return
fi
local updated
updated=$(echo "$folder_config" | python3 -c "
import sys, json
f = json.load(sys.stdin)
f['devices'].append({'deviceID': '${device_id}', 'introducedBy': ''})
print(json.dumps(f))
")
st_put "/rest/config/folders/${folder_id}" "$updated" >/dev/null
info "Folder '${folder_label}' shared with '${device_name}'."
}
unshare_folder() {
header "Unshare Folder from Device"
local folders devices
folders=$(st_get /rest/config/folders)
devices=$(st_get /rest/config/devices)
local f_count
f_count=$(echo "$folders" | python3 -c 'import sys,json; print(len(json.load(sys.stdin)))')
[ "$f_count" -gt 0 ] || { warn "No folders configured."; return; }
echo "Folders:"
echo "$folders" | python3 -c '
import sys, json
for i, f in enumerate(json.load(sys.stdin), 1):
label = f["label"] or f["id"]
shared = len(f.get("devices", []))
print(f" {i}) {label} ({shared} device(s))")
'
echo ""
read -rp "Choose folder [1-${f_count}]: " F_CHOICE
[[ "$F_CHOICE" =~ ^[0-9]+$ ]] && [ "$F_CHOICE" -ge 1 ] && [ "$F_CHOICE" -le "$f_count" ] || die "Invalid choice."
local folder_id folder_label folder_config
folder_id=$(echo "$folders" | python3 -c "import sys,json; f=json.load(sys.stdin)[$((F_CHOICE-1))]; print(f['id'])")
folder_label=$(echo "$folders" | python3 -c "import sys,json; f=json.load(sys.stdin)[$((F_CHOICE-1))]; print(f['label'] or f['id'])")
folder_config=$(st_get "/rest/config/folders/${folder_id}")
local shared_count
shared_count=$(echo "$folder_config" | python3 -c 'import sys,json; print(len(json.load(sys.stdin).get("devices",[])))')
[ "$shared_count" -gt 0 ] || { warn "Folder '${folder_label}' is not shared with any devices."; return; }
local shared_ids
shared_ids=$(echo "$folder_config" | python3 -c "import sys,json; [print(d['deviceID']) for d in json.load(sys.stdin).get('devices',[])]")
echo ""
echo "Shared with:"
local i=1
while IFS= read -r did; do
local dname
dname=$(echo "$devices" | python3 -c "import sys,json; devs=json.load(sys.stdin); match=[d['name'] for d in devs if d['deviceID']=='${did}']; print(match[0] if match else '${did}')")
echo " $i) $dname$did"
i=$((i+1))
done <<< "$shared_ids"
local shared_count_actual=$((i-1))
echo ""
read -rp "Choose device to unshare [1-${shared_count_actual}]: " D_CHOICE
[[ "$D_CHOICE" =~ ^[0-9]+$ ]] && [ "$D_CHOICE" -ge 1 ] && [ "$D_CHOICE" -le "$shared_count_actual" ] || die "Invalid choice."
local target_id
target_id=$(echo "$shared_ids" | sed -n "${D_CHOICE}p")
local target_name
target_name=$(echo "$devices" | python3 -c "import sys,json; devs=json.load(sys.stdin); match=[d['name'] for d in devs if d['deviceID']=='${target_id}']; print(match[0] if match else '${target_id}')")
read -rp "Unshare '${folder_label}' from '${target_name}'? [y/N]: " CONFIRM
[[ "${CONFIRM,,}" == "y" ]] || { info "Aborted."; return; }
local updated
updated=$(echo "$folder_config" | python3 -c "
import sys, json
f = json.load(sys.stdin)
f['devices'] = [d for d in f['devices'] if d['deviceID'] != '${target_id}']
print(json.dumps(f))
")
st_put "/rest/config/folders/${folder_id}" "$updated" >/dev/null
info "Folder '${folder_label}' unshared from '${target_name}'."
}
check_deps curl docker python3
get_apikey
while true; do
header "Syncthing Manager"
echo " 1) Show This Device's ID"
echo " 2) Pending Devices"
echo " 3) List Devices"
echo " 4) Add Device"
echo " 5) Remove Device"
echo " 6) List Folders"
echo " 7) Add Folder"
echo " 8) Remove Folder"
echo " 9) Share Folder with Device"
echo " 10) Unshare Folder from Device"
echo " q) Quit"
echo ""
read -rp "Choose: " OPT
echo ""
case "$OPT" in
1) show_own_device_id ;;
2) show_pending_devices ;;
3) list_devices ;;
4) add_device ;;
5) remove_device ;;
6) list_folders ;;
7) add_folder ;;
8) remove_folder ;;
9) share_folder ;;
10) unshare_folder ;;
q|Q) echo "Bye."; exit 0 ;;
*) warn "Invalid choice." ;;
esac
done