2026-04-18 18:36:01 -07:00
#!/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 = ""
2026-04-18 21:13:34 -07:00
ST_CONTAINER = ""
2026-04-18 18:36:01 -07:00
get_apikey() {
2026-04-18 21:13:34 -07:00
ST_CONTAINER = $( docker ps --format '{{.Names}}' | grep -i syncthing | head -1 || true )
[ -n " $ST_CONTAINER " ] || die "No running Syncthing container found."
2026-04-19 15:50:41 -07:00
APIKEY = $( docker exec " $ST_CONTAINER " cat /var/syncthing/config/config.xml 2>/dev/null | grep -o '<apikey>[^<]*</apikey>' | sed 's/<[^>]*>//g' || true )
2026-04-18 21:13:34 -07:00
[ -n " $APIKEY " ] || die "Could not read Syncthing API key from container ' $ST_CONTAINER '."
2026-04-18 18:36:01 -07:00
}
st_get() {
2026-04-18 21:15:46 -07:00
curl -sf -H "X-API-Key: $APIKEY " " ${ ST_URL }${ 1 } " || die "API call failed: GET ${ 1 } "
2026-04-18 18:36:01 -07:00
}
st_post() {
2026-04-18 21:15:46 -07:00
curl -sf -X POST -H "X-API-Key: $APIKEY " -H "Content-Type: application/json" -d " $2 " " ${ ST_URL }${ 1 } " || die "API call failed: POST ${ 1 } "
2026-04-18 18:36:01 -07:00
}
st_put() {
2026-04-18 21:15:46 -07:00
curl -sf -X PUT -H "X-API-Key: $APIKEY " -H "Content-Type: application/json" -d " $2 " " ${ ST_URL }${ 1 } " || die "API call failed: PUT ${ 1 } "
2026-04-18 18:36:01 -07:00
}
st_delete() {
2026-04-18 21:15:46 -07:00
curl -sf -X DELETE -H "X-API-Key: $APIKEY " " ${ ST_URL }${ 1 } " || die "API call failed: DELETE ${ 1 } "
2026-04-18 18:36:01 -07:00
}
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
}
2026-04-20 13:19:37 -07:00
show_ignored_folders() {
local config
config = $( st_get /rest/config)
local ignored
ignored = $( echo " $config " | python3 -c '
import sys,json
d=json.load(sys.stdin)
folders = d.get("ignoredFolders", [])
for f in folders:
print(f.get("id","?") + " — " + f.get("label","(no label)"))
' 2>/dev/null || true )
if [ -z " $ignored " ] ; then
info "No ignored folders."
return
fi
echo ""
warn "Ignored folders (click Ignore in WUI causes this):"
echo " $ignored " | sed 's/^/ /'
echo ""
read -rp "Un-ignore a folder? [y/N]: " UNIGNORE
UNIGNORE = " ${ UNIGNORE :- n } "
if [[ " ${ UNIGNORE ,, } " != "y" ]] ; then return ; fi
local folder_ids
folder_ids = $( echo " $config " | python3 -c '
import sys,json
d=json.load(sys.stdin)
for f in d.get("ignoredFolders",[]):
print(f.get("id",""))
' 2>/dev/null || true )
echo "Ignored folder IDs:"
local i = 1
while IFS = read -r fid; do
echo " $i ) $fid "
i = $(( i+1))
done <<< " $folder_ids "
read -rp "Choose folder number to un-ignore: " CHOICE
local TARGET_ID
TARGET_ID = $( echo " $folder_ids " | sed -n " ${ CHOICE } p" )
[ -n " $TARGET_ID " ] || { warn "Invalid choice." ; return ; }
local new_config
new_config = $( echo " $config " | python3 -c "
import sys,json
d=json.load(sys.stdin)
d['ignoredFolders'] = [f for f in d.get('ignoredFolders',[]) if f.get('id') != sys.argv[1]]
print(json.dumps(d))
" " $TARGET_ID " )
st_put /rest/config " $new_config "
info "Folder $TARGET_ID removed from ignored list. It should now appear as pending."
}
2026-04-20 13:08:29 -07:00
show_pending_folders() {
header "Pending Folders"
local pending
pending = $( st_get /rest/cluster/pending/folders)
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 folders."
2026-04-20 13:19:37 -07:00
echo ""
read -rp "Check ignored folders? [y/N]: " CHECK_IGNORED
if [[ " ${ CHECK_IGNORED ,, } " == "y" ]] ; then
show_ignored_folders
fi
2026-04-20 13:08:29 -07:00
return
fi
echo " $pending " | python3 -c '
import sys,json
d=json.load(sys.stdin)
for fid,info in d.items():
2026-04-20 19:04:12 -07:00
label = info.get("label", "") or info.get("offeredBy", {})
label = list(info.get("offeredBy",{}).values())[0].get("receiveEncrypted","") if info.get("offeredBy") else ""
2026-04-20 13:08:29 -07:00
offered_by = list(info.get("offeredBy",{}).keys())
2026-04-20 19:04:12 -07:00
folder_label = ""
for dev_info in info.get("offeredBy",{}).values():
if dev_info.get("folderLabel"):
folder_label = dev_info["folderLabel"]
break
if folder_label:
print(f" Folder Name: {folder_label}")
2026-04-20 13:08:29 -07:00
print(f" Folder ID: {fid}")
print(f" Offered by: {offered_by}")
print("")
'
2026-04-20 13:19:37 -07:00
read -rp "Accept a pending folder? [y/N/R (retry/check ignored)]: " ACCEPT
2026-04-20 13:08:29 -07:00
ACCEPT = " ${ ACCEPT :- n } "
2026-04-20 13:19:37 -07:00
if [[ " ${ ACCEPT ,, } " == "r" ]] ; then
show_ignored_folders
return
fi
2026-04-20 13:08:29 -07:00
if [[ " ${ ACCEPT ,, } " != "y" ]] ; then return ; fi
2026-04-20 19:04:12 -07:00
local folder_ids folder_labels
2026-04-20 13:08:29 -07:00
folder_ids = $( echo " $pending " | python3 -c 'import sys,json; [print(k) for k in json.load(sys.stdin)]' )
2026-04-20 19:04:12 -07:00
folder_labels = $( echo " $pending " | python3 -c '
import sys,json
d=json.load(sys.stdin)
for fid,info in d.items():
label=""
for dev_info in info.get("offeredBy",{}).values():
if dev_info.get("folderLabel"):
label=dev_info["folderLabel"]
break
print(label if label else fid)
' )
2026-04-20 13:08:29 -07:00
local FOLDER_ID
if [ " $( echo " $folder_ids " | wc -l) " -eq 1 ] ; then
FOLDER_ID = " $folder_ids "
else
echo "Available pending folders:"
local i = 1
2026-04-20 19:04:12 -07:00
while IFS = read -r fid && IFS = read -r flabel <& 3; do
echo " $i ) $flabel ( $fid )"
2026-04-20 13:08:29 -07:00
i = $(( i+1))
2026-04-20 19:04:12 -07:00
done <<< " $folder_ids " 3<<< " $folder_labels "
2026-04-20 13:08:29 -07:00
read -rp "Choose folder number: " FOLDER_NUM
FOLDER_ID = $( echo " $folder_ids " | sed -n " ${ FOLDER_NUM } p" )
fi
[ -n " $FOLDER_ID " ] || { warn "No folder selected." ; return ; }
echo ""
info "Available directories in /var/syncthing/data/:"
ls /var/syncthing/data/ 2>/dev/null | sed 's/^/ /' || true
echo ""
read -rp "Folder path on this device: " FOLDER_PATH
[ -n " $FOLDER_PATH " ] || { warn "Path cannot be empty." ; return ; }
read -rp "Folder label [ $FOLDER_ID ]: " FOLDER_LABEL
FOLDER_LABEL = " ${ FOLDER_LABEL :- $FOLDER_ID } "
st_post /rest/config/folders " $( python3 -c "
import json
print(json.dumps({'id': ' $FOLDER_ID ', 'label': ' $FOLDER_LABEL ', 'path': ' $FOLDER_PATH '}))
" ) "
info "Folder ' $FOLDER_LABEL ' added at $FOLDER_PATH ."
}
2026-04-18 18:36:01 -07:00
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
2026-04-18 21:18:04 -07:00
pending_name = $( echo " $pending " | python3 -c "import sys,json; d=json.load(sys.stdin); print(d[sys.argv[1]].get('name',''))" " $DEVICE_ID " )
2026-04-18 18:36:01 -07:00
else
echo "Pending devices:"
local i = 1
for id in " ${ id_list [@] } " ; do
local name
2026-04-18 21:18:04 -07:00
name = $( echo " $pending " | python3 -c "import sys,json; d=json.load(sys.stdin); print(d[sys.argv[1]].get('name','(unknown)'))" " $id " )
2026-04-18 18:36:01 -07:00
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)) ] } "
2026-04-18 21:18:04 -07:00
pending_name = $( echo " $pending " | python3 -c "import sys,json; d=json.load(sys.stdin); print(d[sys.argv[1]].get('name',''))" " $DEVICE_ID " )
2026-04-18 18:36:01 -07:00
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:
2026-04-18 18:42:00 -07:00
print(" Name: " + d["name"])
print(" ID: " + d["deviceID"])
2026-04-18 18:36:01 -07:00
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
2026-04-18 21:15:46 -07:00
already = $( echo " $existing " | python3 -c "import sys,json; devs=json.load(sys.stdin); print(any(d['deviceID']==sys.argv[1] for d in devs))" " $device_id " )
2026-04-18 18:36:01 -07:00
if [ " $already " = "True" ] ; then
warn "Device ' ${ device_name } ' is already configured."
return
fi
local payload
2026-04-18 21:15:46 -07:00
payload = $( python3 -c "import sys,json; print(json.dumps({'deviceID':sys.argv[1],'name':sys.argv[2],'addresses':['dynamic'],'autoAcceptFolders':False}))" " $device_id " " $device_name " )
2026-04-18 18:36:01 -07:00
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):
2026-04-18 18:42:00 -07:00
print(" " + str(i) + ") " + d["name"] + " — " + d["deviceID"])
2026-04-18 18:36:01 -07:00
'
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)
2026-04-18 21:18:04 -07:00
shared = [f['label'] or f['id'] for f in folders if any(dev['deviceID']==sys.argv[1] for dev in f.get('devices',[]))]
2026-04-18 18:36:01 -07:00
print('\n'.join(shared))
2026-04-18 21:18:04 -07:00
" " $device_id " )
2026-04-18 18:36:01 -07:00
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"]
2026-04-18 18:42:00 -07:00
shared = len(f.get("devices", []))
print(" Label: " + label)
print(" ID: " + f["id"])
print(" Path: " + f["path"])
print(" Shared: " + str(shared) + " device(s)")
2026-04-18 18:36:01 -07:00
print()
'
}
add_folder() {
header "Add Folder"
2026-04-18 21:13:34 -07:00
if [ -n " $ST_CONTAINER " ] ; then
2026-04-18 21:12:47 -07:00
echo "Available folders in /var/syncthing/data/:"
2026-04-18 21:13:34 -07:00
docker exec " $ST_CONTAINER " ls /var/syncthing/data/ 2>/dev/null | sed 's/^/ /' || warn "Could not list data directory."
2026-04-18 21:12:47 -07:00
echo ""
fi
2026-04-18 21:08:22 -07:00
read -rp "Folder path on this device (e.g. /var/syncthing/data/books): " FOLDER_PATH
2026-04-18 18:36:01 -07:00
[ -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
2026-04-18 21:15:46 -07:00
already = $( echo " $existing " | python3 -c "import sys,json; folders=json.load(sys.stdin); print(any(f['id']==sys.argv[1] for f in folders))" " $FOLDER_ID " )
2026-04-18 18:36:01 -07:00
[ " $already " = "False" ] || die "Folder ID ' ${ FOLDER_ID } ' already exists."
local payload
2026-04-18 21:15:46 -07:00
payload = $( python3 -c "import sys,json; print(json.dumps({'id':sys.argv[1],'label':sys.argv[2],'path':sys.argv[3],'type':'sendreceive','devices':[]}))" " $FOLDER_ID " " $FOLDER_LABEL " " $FOLDER_PATH " )
2026-04-18 18:36:01 -07:00
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"]
2026-04-18 18:44:59 -07:00
print(" " + str(i) + ") " + label + " (" + f["path"] + ")")
2026-04-18 18:36:01 -07:00
'
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'])" )
2026-04-18 21:21:24 -07:00
local folder_config shared_devices
folder_config = $( st_get "/rest/config/folders/ ${ folder_id } " )
shared_devices = $( echo " $folder_config " | python3 -c "import sys,json; [print(d['deviceID']) for d in json.load(sys.stdin).get('devices',[])]" )
if [ -n " $shared_devices " ] ; then
local devices
devices = $( st_get /rest/config/devices)
warn "Folder ' ${ folder_label } ' is currently shared with:"
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']==sys.argv[1]]; print(match[0] if match else sys.argv[1])" " $did " )
echo " - $dname "
done <<< " $shared_devices "
warn "It will be unshared from all devices before removal."
read -rp "Proceed? [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'] = []
print(json.dumps(f))
" )
st_put "/rest/config/folders/ ${ folder_id } " " $updated " >/dev/null
info "Folder ' ${ folder_label } ' unshared from all devices."
else
read -rp "Remove folder ' ${ folder_label } '? [y/N]: " CONFIRM
[[ " ${ CONFIRM ,, } " == "y" ]] || { info "Aborted." ; return ; }
fi
2026-04-18 18:36:01 -07:00
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"]
2026-04-18 18:42:00 -07:00
print(" " + str(i) + ") " + label)
2026-04-18 18:36:01 -07:00
'
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):
2026-04-18 18:42:00 -07:00
print(" " + str(i) + ") " + d["name"])
2026-04-18 18:36:01 -07:00
'
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 } " )
2026-04-18 21:15:46 -07:00
already = $( echo " $folder_config " | python3 -c "import sys,json; f=json.load(sys.stdin); print(any(d['deviceID']==sys.argv[1] for d in f.get('devices',[])))" " $device_id " )
2026-04-18 18:36:01 -07:00
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)
2026-04-18 21:15:46 -07:00
f['devices'].append({'deviceID': sys.argv[1], 'introducedBy': ''})
2026-04-18 18:36:01 -07:00
print(json.dumps(f))
2026-04-18 21:15:46 -07:00
" " $device_id " )
2026-04-18 18:36:01 -07:00
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", []))
2026-04-18 18:42:00 -07:00
print(" " + str(i) + ") " + label + " (" + str(shared) + " device(s))")
2026-04-18 18:36:01 -07:00
'
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
2026-04-18 21:18:04 -07:00
dname = $( echo " $devices " | python3 -c "import sys,json; devs=json.load(sys.stdin); match=[d['name'] for d in devs if d['deviceID']==sys.argv[1]]; print(match[0] if match else sys.argv[1])" " $did " )
2026-04-18 18:36:01 -07:00
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
2026-04-18 21:18:04 -07:00
target_name = $( echo " $devices " | python3 -c "import sys,json; devs=json.load(sys.stdin); match=[d['name'] for d in devs if d['deviceID']==sys.argv[1]]; print(match[0] if match else sys.argv[1])" " $target_id " )
2026-04-18 18:36:01 -07:00
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)
2026-04-18 21:15:46 -07:00
f['devices'] = [d for d in f['devices'] if d['deviceID'] != sys.argv[1]]
2026-04-18 18:36:01 -07:00
print(json.dumps(f))
2026-04-18 21:15:46 -07:00
" " $target_id " )
2026-04-18 18:36:01 -07:00
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"
2026-04-18 18:57:36 -07:00
echo " 0) Show This Device's ID"
2026-04-20 13:08:29 -07:00
echo " 1) Pending Devices & Folders"
2026-04-18 18:57:36 -07:00
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"
2026-04-18 18:36:01 -07:00
echo " q) Quit"
echo ""
read -rp "Choose: " OPT
echo ""
case " $OPT " in
2026-04-18 18:57:36 -07:00
0) show_own_device_id ;;
2026-04-20 13:08:29 -07:00
1) show_pending_devices; show_pending_folders ;;
2026-04-18 18:57:36 -07:00
2) list_devices ;;
3) add_device ;;
4) remove_device ;;
5) list_folders ;;
6) add_folder ;;
7) remove_folder ;;
8) share_folder ;;
9) unshare_folder ;;
2026-04-18 18:36:01 -07:00
q| Q) echo "Bye." ; exit 0 ;;
*) warn "Invalid choice." ;;
esac
done