#!/usr/bin/env sh
# vaultsync-notify one-line installer.
#
# curl -fsSL https://vaultsync.eu/notify.sh | sh
#
# Skeptical of curl|sh? Quite right — append `-s -- --dry-run` to see every
# action without changing anything, or read this file first:
# https://github.com/psimaker/vaultsync/blob/main/notify/scripts/install.sh
#
# What it does, in order:
# 1. Finds Syncthing's config.xml (same probe order as the helper binary).
# 2. Reads the file owner, so the helper runs with exactly that uid:gid —
# config.xml is mode 0600, and a mismatched uid is the #1 setup failure.
# 3. Starts the helper:
# - Linux with Docker → ghcr.io/psimaker/vaultsync-notify container
# - Linux without → prebuilt binary + systemd service
# - macOS → prebuilt binary + launchd agent
# Docker on macOS is skipped on purpose: without host networking the
# container cannot reach a natively-running Syncthing on 127.0.0.1.
# 4. Runs the helper's --doctor preflight where possible, then verifies the
# service actually came up.
#
# The helper sends only your Syncthing Device ID to the relay — never file
# names, folder names, or content. There is nothing user-specific in this
# script: identity comes from your own Syncthing instance at runtime.
#
# Environment overrides (all optional):
# SYNCTHING_CONFIG path to config.xml when auto-detection misses it
# (Synology/QNAP/Unraid host layouts are probed
# automatically since #86)
# RELAY_URL relay endpoint (default: production relay)
# VAULTSYNC_NOTIFY_MODE auto|docker|binary (default: auto)
# VAULTSYNC_NOTIFY_IMAGE container image override (development)
set -eu
RELAY_URL="${RELAY_URL:-https://relay.vaultsync.eu}"
MODE="${VAULTSYNC_NOTIFY_MODE:-auto}"
IMAGE="${VAULTSYNC_NOTIFY_IMAGE:-ghcr.io/psimaker/vaultsync-notify:2.0.2}"
REPO="psimaker/vaultsync"
CONTAINER_NAME="vaultsync-notify"
DRY_RUN=0
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=1 ;;
*)
printf 'ERROR: unknown argument: %s (only --dry-run is supported)\n' "$arg" >&2
exit 1
;;
esac
done
info() {
printf '%s\n' "$*"
}
warn() {
printf 'WARN: %s\n' "$*" >&2
}
fail() {
printf 'ERROR: %s\n' "$*" >&2
exit 1
}
# Execute a simple command, or print it instead under --dry-run.
run() {
if [ "$DRY_RUN" = 1 ]; then
info "[dry-run] would run: $*"
return 0
fi
"$@"
}
SUDO=""
need_root() {
# Returns a prefix that makes the following command run as root. sudo prompts
# on /dev/tty, so this works under `curl | sh` too.
if [ "$(id -u)" = 0 ]; then
SUDO=""
return 0
fi
if command -v sudo >/dev/null 2>&1; then
SUDO="sudo"
return 0
fi
return 1
}
# --- 1. Locate config.xml ----------------------------------------------------
# Probe order mirrors the helper binary (notify/syncthing_config.go):
# current XDG state dir, legacy config dir, macOS, container/system service
# layouts, then NAS host layouts (#86 — the installer runs on the NAS *host*,
# where the package/appdata paths hold config.xml). ${HOME:-} keeps set -u
# happy in HOME-less contexts (containers, cron) — the unusable candidates
# simply never match. The unquoted entries are globs (volume/package names
# vary per NAS); a non-matching glob stays literal and never exists. One
# candidate per line, so paths with spaces survive the while-read consumers.
config_candidates() {
for candidate in \
"${XDG_STATE_HOME:-${HOME:-}/.local/state}/syncthing/config.xml" \
"${XDG_CONFIG_HOME:-${HOME:-}/.config}/syncthing/config.xml" \
"${HOME:-}/Library/Application Support/Syncthing/config.xml" \
"/var/syncthing/config/config.xml" \
"/config/config.xml" \
"/var/syncthing/config.xml" \
"/var/lib/syncthing/config.xml" \
"/etc/syncthing/config.xml" \
"/var/packages/syncthing/var/config.xml" \
"/var/packages/syncthing/target/var/config.xml" \
"/mnt/user/appdata/syncthing/config.xml" \
/volume*/@appdata/syncthing/config.xml \
/volume*/@appstore/syncthing/var/config.xml \
/share/*/.qpkg/*yncthing*/var/config.xml \
/share/*/.qpkg/*yncthing*/.config/syncthing/config.xml; do
printf '%s\n' "$candidate"
done
}
# NOTE: runs in a command substitution, so fail() here would exit only the
# subshell and the caller would print its generic error on top — explicit
# SYNCTHING_CONFIG is therefore validated in the main flow below.
find_syncthing_config() {
if [ -n "${SYNCTHING_CONFIG:-}" ]; then
printf '%s\n' "$SYNCTHING_CONFIG"
return 0
fi
found=$(config_candidates | while IFS= read -r candidate; do
if [ -e "$candidate" ]; then
printf '%s\n' "$candidate"
break
fi
done)
[ -n "$found" ] || return 1
printf '%s\n' "$found"
}
# A config can exist but sit behind a 0700 system dir our uid cannot see
# (e.g. /var/lib/syncthing owned by a dedicated syncthing user). Re-probe as
# root — sudo -n only: never prompt for a password just for a diagnostic.
find_config_as_root() {
[ "$(id -u)" != 0 ] || return 1
command -v sudo >/dev/null 2>&1 || return 1
found=$(config_candidates | while IFS= read -r candidate; do
if sudo -n test -e "$candidate" 2>/dev/null; then
printf '%s\n' "$candidate"
break
fi
done)
[ -n "$found" ] || return 1
printf '%s\n' "$found"
}
# --- 2. File owner -----------------------------------------------------------
owner_of() {
# GNU stat (Linux) vs BSD stat (macOS). Reading the owner needs no read
# permission on the file itself, so this works even when the config is 0600
# under another user.
path="$1"
if stat -c '%u:%g' "$path" 2>/dev/null; then
return 0
fi
stat -f '%u:%g' "$path" 2>/dev/null
}
# --- Cross-flavor guard (#87) --------------------------------------------------
# A helper installed under the OTHER flavor keeps running in parallel with the
# one being (re)installed — duplicate helpers and "fixed but still happening"
# reports. Never removed automatically: explain and stop, the operator acts.
guard_against_systemd_flavor() {
command -v systemctl >/dev/null 2>&1 || return 0
[ -d /run/systemd/system ] || return 0
if systemctl is-enabled vaultsync-notify >/dev/null 2>&1 \
|| systemctl is-active --quiet vaultsync-notify 2>/dev/null; then
fail "A systemd install of the helper already exists and would keep running in
parallel with the Docker container. Remove it first, then re-run this installer:
sudo systemctl disable --now vaultsync-notify
sudo rm /etc/systemd/system/vaultsync-notify.service
sudo systemctl daemon-reload"
fi
}
guard_against_docker_flavor() {
if resolve_docker; then
if $DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -qx "$CONTAINER_NAME"; then
fail "A Docker install of the helper already exists and would keep running in
parallel with the systemd service. Remove it first, then re-run this installer:
$DOCKER rm -f $CONTAINER_NAME"
fi
elif command -v docker >/dev/null 2>&1; then
warn "Docker is installed but its daemon is unreachable, so an existing $CONTAINER_NAME container cannot be ruled out. If the helper ever ran via Docker on this machine, remove it once Docker is back: docker rm -f $CONTAINER_NAME"
fi
}
# --- 3a. Docker path ---------------------------------------------------------
resolve_docker() {
command -v docker >/dev/null 2>&1 || return 1
if docker info >/dev/null 2>&1; then
DOCKER="docker"
return 0
fi
if command -v sudo >/dev/null 2>&1 && sudo docker info >/dev/null 2>&1; then
DOCKER="sudo docker"
return 0
fi
return 1
}
install_docker() {
config_dir=$(dirname -- "$CONFIG_PATH")
config_name=$(basename -- "$CONFIG_PATH")
guard_against_systemd_flavor
# A re-run must re-resolve the reviewed version tag, then every operation in
# this invocation uses the resulting immutable local content ID. A failed
# pull never falls back to an unverified or stale local tag.
runtime_image=$IMAGE
if [ "$DRY_RUN" = 1 ]; then
info "[dry-run] would run: $DOCKER pull $IMAGE"
else
old_image_id=$($DOCKER image inspect -f '{{.Id}}' "$IMAGE" 2>/dev/null) || old_image_id=""
$DOCKER pull "$IMAGE" || fail "Could not pull the reviewed helper image $IMAGE. No local fallback was used."
new_image_id=$($DOCKER image inspect -f '{{.Id}}' "$IMAGE" 2>/dev/null) ||
fail "Docker did not resolve $IMAGE after the successful pull."
case $new_image_id in
sha256:*) ;;
*) fail "Docker returned a non-immutable image identity for $IMAGE." ;;
esac
runtime_image=$new_image_id
if [ -n "$old_image_id" ] && [ "$old_image_id" != "$new_image_id" ]; then
info "Helper image updated ($(printf '%.19s' "$old_image_id")… -> $(printf '%.19s' "$new_image_id")…)."
fi
fi
if $DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -qx "$CONTAINER_NAME"; then
info "Replacing existing $CONTAINER_NAME container (it keeps no state)."
run $DOCKER rm -f "$CONTAINER_NAME"
fi
info "Running preflight checks (doctor)..."
if [ "$DRY_RUN" = 1 ]; then
info "[dry-run] would run: $DOCKER run --rm --network host -u $OWNER -v $config_dir:/config:ro -e SYNCTHING_CONFIG=/config/$config_name -e RELAY_URL=$RELAY_URL $IMAGE --doctor"
else
$DOCKER run --rm --network host \
-u "$OWNER" \
-v "$config_dir":/config:ro \
-e SYNCTHING_CONFIG="/config/$config_name" \
-e RELAY_URL="$RELAY_URL" \
"$runtime_image" --doctor \
|| fail "Preflight failed — see the messages above for the fix, then re-run this installer."
fi
info "Starting the helper container..."
if [ "$DRY_RUN" = 1 ]; then
info "[dry-run] would run: $DOCKER run -d --name $CONTAINER_NAME --restart unless-stopped --network host -u $OWNER -v $config_dir:/config:ro -e SYNCTHING_CONFIG=/config/$config_name -e RELAY_URL=$RELAY_URL $IMAGE"
return 0
fi
$DOCKER run -d --name "$CONTAINER_NAME" --restart unless-stopped \
--network host \
-u "$OWNER" \
-v "$config_dir":/config:ro \
-e SYNCTHING_CONFIG="/config/$config_name" \
-e RELAY_URL="$RELAY_URL" \
"$runtime_image" >/dev/null
# Doctor passing makes a crash here unlikely, but verify the container
# actually stayed up rather than reporting success on a restart loop.
sleep 3
if [ "$($DOCKER inspect -f '{{.State.Running}}' "$CONTAINER_NAME" 2>/dev/null)" != "true" ]; then
$DOCKER logs --tail 20 "$CONTAINER_NAME" >&2 || true
fail "The helper container did not stay up — the log above explains why. Fix it and re-run this installer."
fi
}
# --- 3b. Binary path ---------------------------------------------------------
detect_asset() {
os=$(uname -s)
arch=$(uname -m)
case "$os" in
Linux) goos="linux" ;;
Darwin) goos="darwin" ;;
*) fail "Unsupported OS for the binary install: $os. Use Docker, or build from source (notify/README.md)." ;;
esac
case "$arch" in
x86_64 | amd64) goarch="amd64" ;;
aarch64 | arm64) goarch="arm64" ;;
*) fail "Unsupported CPU architecture: $arch (prebuilt binaries cover amd64 and arm64). Build from source: notify/README.md." ;;
esac
printf 'vaultsync-notify_%s_%s\n' "$goos" "$goarch"
}
latest_notify_tag() {
# The repo also publishes app releases (v*); pick the newest notify-v* tag.
curl -fsSL "https://api.github.com/repos/$REPO/releases?per_page=30" \
| grep -o '"tag_name": *"notify-v[^"]*"' \
| head -1 \
| sed 's/.*"\(notify-v[^"]*\)"/\1/'
}
download_binary() {
asset="$1"
tag="$2"
dest="$3"
base="https://github.com/$REPO/releases/download/$tag"
# Make the upgrade visible (#87). Best effort: binaries older than the
# --version flag print nothing and the line is simply skipped.
if [ -x "$dest" ]; then
old_version=$("$dest" --version 2>/dev/null) || old_version=""
if [ -n "$old_version" ]; then
info "Currently installed: $old_version — installing ${tag#notify-v}."
fi
fi
if [ "$DRY_RUN" = 1 ]; then
info "[dry-run] would download: $base/$asset -> $dest (and verify against $base/SHA256SUMS)"
return 0
fi
tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT
curl -fsSL -o "$tmpdir/$asset" "$base/$asset" \
|| fail "Download failed: $base/$asset"
# Release binaries are never installed without the reviewed checksum asset
# and a local SHA-256 implementation.
curl -fsSL -o "$tmpdir/SHA256SUMS" "$base/SHA256SUMS" \
|| fail "Could not fetch SHA256SUMS for $tag — no binary was installed."
checksum=$(awk -v asset="$asset" '
$2 == asset { matches++; value = $1 }
END {
if (matches != 1) exit 1
print value
}
' "$tmpdir/SHA256SUMS") ||
fail "SHA256SUMS must contain exactly one checksum for $asset — no binary was installed."
if [ "${#checksum}" -ne 64 ]; then
fail "SHA256SUMS contains a non-canonical checksum for $asset — no binary was installed."
fi
case $checksum in
*[!0-9a-f]*) fail "SHA256SUMS contains a non-canonical checksum for $asset — no binary was installed." ;;
esac
if command -v sha256sum >/dev/null 2>&1; then
(cd "$tmpdir" && printf '%s %s\n' "$checksum" "$asset" | sha256sum -c - >/dev/null) \
|| fail "Checksum mismatch for $asset — aborting."
elif command -v shasum >/dev/null 2>&1; then
(cd "$tmpdir" && printf '%s %s\n' "$checksum" "$asset" | shasum -a 256 -c - >/dev/null) \
|| fail "Checksum mismatch for $asset — aborting."
else
fail "sha256sum or shasum is required to verify $asset — no binary was installed."
fi
chmod 755 "$tmpdir/$asset"
if [ -w "$(dirname -- "$dest")" ]; then
mv "$tmpdir/$asset" "$dest"
else
need_root || fail "Cannot write $dest and sudo is unavailable. Re-run as root."
$SUDO mv "$tmpdir/$asset" "$dest"
fi
}
run_doctor_binary() {
bin="$1"
# Preflight as the current user. If the config is unreadable for us (it
# belongs to a dedicated syncthing user), skip — the service runs as the
# owner, and the post-start check below still catches real failures.
if [ ! -r "$CONFIG_PATH" ]; then
info "Skipping doctor preflight ($CONFIG_PATH is not readable by $(id -un); the service will run as uid:gid $OWNER)."
return 0
fi
info "Running preflight checks (doctor)..."
run env SYNCTHING_CONFIG="$CONFIG_PATH" RELAY_URL="$RELAY_URL" "$bin" --doctor \
|| fail "Preflight failed — see the messages above for the fix, then re-run this installer."
}
install_systemd() {
bin="/usr/local/bin/vaultsync-notify"
unit="/etc/systemd/system/vaultsync-notify.service"
guard_against_docker_flavor
need_root || fail "Installing the systemd service needs root. Re-run with sudo, or use Docker."
download_binary "$ASSET" "$TAG" "$bin"
run_doctor_binary "$bin"
owner_uid=${OWNER%%:*}
owner_gid=${OWNER#*:}
unit_content="[Unit]
Description=VaultSync notify helper (Cloud Relay wake-ups)
Documentation=https://github.com/$REPO/blob/main/notify/README.md
After=network-online.target syncthing.service
Wants=network-online.target
[Service]
ExecStart=$bin
Environment=RELAY_URL=$RELAY_URL
Environment=SYNCTHING_CONFIG=$CONFIG_PATH
User=$owner_uid
Group=$owner_gid
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target"
if [ "$DRY_RUN" = 1 ]; then
info "[dry-run] would write $unit:"
printf '%s\n' "$unit_content" | sed 's/^/[dry-run] /'
info "[dry-run] would run: $SUDO systemctl daemon-reload && $SUDO systemctl enable vaultsync-notify && $SUDO systemctl restart vaultsync-notify"
return 0
fi
printf '%s\n' "$unit_content" | $SUDO tee "$unit" >/dev/null
$SUDO systemctl daemon-reload
$SUDO systemctl enable vaultsync-notify
# restart, not `enable --now`: --now leaves an already-running old process
# in place after the binary swap — the silent-no-upgrade case of #87.
# restart also starts a currently-stopped unit.
$SUDO systemctl restart vaultsync-notify
sleep 2
if ! $SUDO systemctl is-active --quiet vaultsync-notify; then
$SUDO journalctl -u vaultsync-notify -n 20 --no-pager 2>/dev/null || true
fail "The service did not stay up — the log above explains why. Fix it and re-run this installer."
fi
}
# Root path (#89): a LaunchAgent lives in the gui/$uid domain — it cannot even
# bootstrap in a headless SSH session ("Bootstrap failed: 5"), and it stops at
# logout/reboot without auto-login. The headless Mac mini is exactly the server
# scenario the one-liner targets, so with root we install a LaunchDaemon that
# runs as the config.xml owner (UserName key) and survives logout and reboot.
install_launchdaemon() {
bin="/usr/local/bin/vaultsync-notify"
plist="/Library/LaunchDaemons/eu.vaultsync.notify.plist"
label="eu.vaultsync.notify"
log="/Library/Logs/vaultsync-notify.log"
owner_uid=${OWNER%%:*}
run_user=$(id -un "$owner_uid" 2>/dev/null) || run_user=""
[ -n "$run_user" ] || fail "Cannot resolve a username for uid $owner_uid (the owner of $CONFIG_PATH)."
run mkdir -p /usr/local/bin
download_binary "$ASSET" "$TAG" "$bin"
run_doctor_binary "$bin"
plist_content="
Label
$label
ProgramArguments
$bin
EnvironmentVariables
RELAY_URL
$RELAY_URL
SYNCTHING_CONFIG
$CONFIG_PATH
UserName
$run_user
RunAtLoad
KeepAlive
StandardErrorPath
$log
"
if [ "$DRY_RUN" = 1 ]; then
info "[dry-run] would write $plist (LaunchDaemon, runs as $run_user), replace any existing per-user LaunchAgent, and (re)load it via launchctl bootstrap system"
return 0
fi
# Cross-flavor guard (#89/#87): the agent path itself recommends this sudo
# upgrade — without removing the old per-user agent, agent AND daemon would
# run in parallel after the next login. Replacing our OWN helper artifact
# mirrors the Docker path's container replacement; user data is never touched.
if [ -n "${SUDO_USER:-}" ]; then
agent_home=$(sudo -u "$SUDO_USER" sh -c 'echo "$HOME"') || agent_home=""
agent_plist="$agent_home/Library/LaunchAgents/$label.plist"
if [ -n "$agent_home" ] && [ -e "$agent_plist" ]; then
info "Replacing the existing per-user LaunchAgent with the LaunchDaemon."
launchctl bootout "gui/$(id -u "$SUDO_USER")/$label" 2>/dev/null || true
rm -f "$agent_plist"
fi
fi
printf '%s\n' "$plist_content" >"$plist"
launchctl bootout "system/$label" 2>/dev/null || true
launchctl bootstrap system "$plist" \
|| fail "launchctl could not load the LaunchDaemon — check $log and 'launchctl print system/$label', fix it, and re-run this installer."
sleep 2
if ! launchctl print "system/$label" >/dev/null 2>&1; then
fail "The LaunchDaemon did not start — check $log, fix it, and re-run this installer."
fi
info "Installed as a LaunchDaemon (runs as $run_user; survives logout and reboot)."
}
install_launchd() {
# With root, install the reboot-safe LaunchDaemon instead of a per-user
# agent (#89) — also the only path that works over headless SSH.
if [ "$(id -u)" = 0 ]; then
install_launchdaemon
return 0
fi
# Cross-flavor guard (#89/#87): a LaunchDaemon install already covers this
# Mac for every user — adding a per-user agent next to it would run two
# helpers in parallel. Explain and stop; never removed automatically.
if [ -e "/Library/LaunchDaemons/eu.vaultsync.notify.plist" ]; then
fail "A LaunchDaemon install of the helper already exists and covers this Mac.
To upgrade it, re-run this installer with sudo:
curl -fsSL https://vaultsync.eu/notify.sh | sudo sh
To remove it first:
sudo launchctl bootout system/eu.vaultsync.notify
sudo rm /Library/LaunchDaemons/eu.vaultsync.notify.plist"
fi
bin_dir="$HOME/.local/bin"
bin="$bin_dir/vaultsync-notify"
agent_dir="$HOME/Library/LaunchAgents"
plist="$agent_dir/eu.vaultsync.notify.plist"
label="eu.vaultsync.notify"
run mkdir -p "$bin_dir" "$agent_dir"
download_binary "$ASSET" "$TAG" "$bin"
run_doctor_binary "$bin"
plist_content="
Label
$label
ProgramArguments
$bin
EnvironmentVariables
RELAY_URL
$RELAY_URL
SYNCTHING_CONFIG
$CONFIG_PATH
RunAtLoad
KeepAlive
StandardErrorPath
/tmp/vaultsync-notify.log
"
if [ "$DRY_RUN" = 1 ]; then
info "[dry-run] would write $plist and (re)load it via launchctl"
return 0
fi
printf '%s\n' "$plist_content" >"$plist"
uid=$(id -u)
launchctl bootout "gui/$uid/$label" 2>/dev/null || true
# Guarded (#89): under set -eu a failed bootstrap used to kill the script
# with launchctl's raw "Bootstrap failed: 5" — typical for an SSH session
# with no GUI login, where the gui/$uid domain does not exist.
launchctl bootstrap "gui/$uid" "$plist" \
|| fail "Could not start the launchd agent — usually there is no GUI session for $(id -un) (headless Mac / SSH). Two ways out:
- log in once on this Mac's screen, then re-run this installer, or
- install as a LaunchDaemon instead (survives logout and reboot — right for a server):
curl -fsSL https://vaultsync.eu/notify.sh | sudo sh"
sleep 2
if ! launchctl print "gui/$uid/$label" >/dev/null 2>&1; then
fail "The launchd agent did not start — check /tmp/vaultsync-notify.log, fix it, and re-run this installer."
fi
info "Note: as a LaunchAgent, the helper runs only while $(id -un) is logged in."
info "For an always-on server Mac, install a LaunchDaemon instead:"
info " curl -fsSL https://vaultsync.eu/notify.sh | sudo sh"
}
install_binary() {
command -v curl >/dev/null 2>&1 || fail "curl is required for the binary install."
ASSET=$(detect_asset)
TAG=$(latest_notify_tag) || TAG=""
[ -n "$TAG" ] || fail "Could not find a notify release on GitHub ($REPO). Check your network, or build from source: notify/README.md."
info "Installing $ASSET from release $TAG."
case "$(uname -s)" in
Darwin)
install_launchd
;;
Linux)
if command -v systemctl >/dev/null 2>&1 && [ -d /run/systemd/system ]; then
install_systemd
else
bin="./vaultsync-notify"
download_binary "$ASSET" "$TAG" "$bin"
run_doctor_binary "$bin"
warn "No systemd found — downloaded $bin but could not install a service."
info "Start it manually and keep it running:"
info " SYNCTHING_CONFIG=\"$CONFIG_PATH\" RELAY_URL=\"$RELAY_URL\" $bin"
exit 0
fi
;;
esac
}
# --- Main --------------------------------------------------------------------
info "VaultSync Cloud Relay — server helper installer"
[ "$DRY_RUN" = 1 ] && info "(dry run — nothing will be changed)"
if [ -n "${SYNCTHING_CONFIG:-}" ] && [ ! -e "${SYNCTHING_CONFIG}" ]; then
fail "SYNCTHING_CONFIG is set to $SYNCTHING_CONFIG but no file exists there."
fi
if CONFIG_PATH=$(find_syncthing_config); then
info "Found Syncthing config: $CONFIG_PATH"
else
# Distinguish "not there" from "there, but invisible to this uid" — the
# 0700-system-dir case would otherwise end in the same generic message.
if ROOT_CONFIG=$(find_config_as_root); then
fail "Syncthing's config exists at $ROOT_CONFIG but is not visible to $(id -un) —
it likely belongs to a dedicated syncthing user. Re-run this installer with sudo:
curl -fsSL https://vaultsync.eu/notify.sh | sudo sh"
fi
fail "Could not find Syncthing's config.xml. Is Syncthing installed on THIS machine?
- If it lives elsewhere, re-run with its path (the variable must prefix sh, not curl):
curl -fsSL https://vaultsync.eu/notify.sh | SYNCTHING_CONFIG=/path/to/config.xml sh
- If you ran this installer with sudo but Syncthing runs under YOUR user, sudo's
HOME hides its config — re-run without sudo, or pass SYNCTHING_CONFIG as above.
- Search for it: sudo find / -name config.xml -path '*syncthing*' 2>/dev/null
- Synology/QNAP/Unraid and custom setups: https://github.com/$REPO/blob/main/notify/README.md"
fi
OWNER=$(owner_of "$CONFIG_PATH") || fail "Could not read the owner of $CONFIG_PATH."
info "Helper will run as uid:gid $OWNER (the owner of config.xml)."
case "$MODE" in
docker)
resolve_docker || fail "VAULTSYNC_NOTIFY_MODE=docker, but Docker is not usable here."
install_docker
;;
binary)
install_binary
;;
auto)
# Docker on macOS cannot use host networking to reach a native Syncthing
# on 127.0.0.1, so macOS always takes the launchd binary path.
if [ "$(uname -s)" = "Linux" ] && resolve_docker; then
install_docker
else
install_binary
fi
;;
*)
fail "Invalid VAULTSYNC_NOTIFY_MODE: $MODE (use auto, docker, or binary)."
;;
esac
info ""
if [ "$DRY_RUN" = 1 ]; then
info "Dry run complete — nothing was changed. Re-run without --dry-run to install."
else
info "Done. The helper has sent a first wake-up — within a minute, VaultSync on"
info "your iPhone shows \"Cloud Relay active\" (Relay tab). Nothing but your"
info "Syncthing Device ID ever leaves this machine."
fi