forked from finn/tinyboard
syncthing.sh: new script for managing Syncthing devices and folders via REST API with interactive menu
This commit is contained in:
434
syncthing.sh
Normal file
434
syncthing.sh
Normal file
@@ -0,0 +1,434 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user