forked from finn/tinyboard
435 lines
16 KiB
Bash
Executable File
435 lines
16 KiB
Bash
Executable File
#!/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(" Name: " + d["name"])
|
|
print(" 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(" " + str(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 = len(f.get("devices", []))
|
|
print(" Label: " + label)
|
|
print(" ID: " + f["id"])
|
|
print(" Path: " + f["path"])
|
|
print(" Shared: " + str(shared) + " device(s)")
|
|
print()
|
|
'
|
|
}
|
|
|
|
add_folder() {
|
|
header "Add Folder"
|
|
read -rp "Folder path on this device (e.g. /var/syncthing/data/books): " 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(" " + str(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(" " + str(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(" " + str(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(" " + str(i) + ") " + label + " (" + str(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 " 0) Show This Device's ID"
|
|
echo " 1) Pending Devices"
|
|
echo " 2) List Devices"
|
|
echo " 3) Add Device"
|
|
echo " 4) Remove Device"
|
|
echo " 5) List Folders"
|
|
echo " 6) Add Folder"
|
|
echo " 7) Remove Folder"
|
|
echo " 8) Share Folder with Device"
|
|
echo " 9) Unshare Folder from Device"
|
|
echo " q) Quit"
|
|
echo ""
|
|
read -rp "Choose: " OPT
|
|
echo ""
|
|
case "$OPT" in
|
|
0) show_own_device_id ;;
|
|
1) show_pending_devices ;;
|
|
2) list_devices ;;
|
|
3) add_device ;;
|
|
4) remove_device ;;
|
|
5) list_folders ;;
|
|
6) add_folder ;;
|
|
7) remove_folder ;;
|
|
8) share_folder ;;
|
|
9) unshare_folder ;;
|
|
q|Q) echo "Bye."; exit 0 ;;
|
|
*) warn "Invalid choice." ;;
|
|
esac
|
|
done
|