From f7ba96321eef03d3c9c1afb1ea58b658c2118c84 Mon Sep 17 00:00:00 2001 From: BPR02 Date: Wed, 18 Mar 2026 19:38:52 -0700 Subject: [PATCH 01/12] fix(control): give dns time to boot up after tailscale up --- peerstash-control/peerstash/core/tailscale.py | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/peerstash-control/peerstash/core/tailscale.py b/peerstash-control/peerstash/core/tailscale.py index 73ac313..c63f319 100644 --- a/peerstash-control/peerstash/core/tailscale.py +++ b/peerstash-control/peerstash/core/tailscale.py @@ -16,11 +16,14 @@ import json import subprocess +import time from typing import Optional import commentjson import requests +from peerstash.core.utils import logger + TAILSCALE_API = "https://api.tailscale.com/api/v2" @@ -162,8 +165,21 @@ def generate_device_invite(api_token: str) -> Optional[str]: device_id = _get_local_device_id() url = f"{TAILSCALE_API}/device/{device_id}/device-invites" payload = [{"multiUse": True}] - response = requests.post(url, auth=(api_token, ""), json=payload) - response.raise_for_status() + # Retry loop to wait for MagicDNS to stabilize + max_retries = 5 + for attempt in range(max_retries): + try: + response = requests.post(url, auth=(api_token, ""), json=payload) + response.raise_for_status() - full_url: Optional[str] = response.json()[0].get("inviteUrl") - return full_url.split("/")[-1] if full_url else None + full_url: Optional[str] = response.json()[0].get("inviteUrl") + return full_url.split("/")[-1] if full_url else None + + except requests.exceptions.ConnectionError as e: + if attempt < max_retries - 1: + logger.warning( + f"Tailscale URL failed to resolve ({attempt}). Retrying in 2 seconds..." + ) + time.sleep(2) # Wait 2 seconds and try again + continue + raise RuntimeError(f"DNS failed to stabilize after Tailscale up: {e}") From aa098474e2d00079adcaf80eb4da3ff7a6000d6f Mon Sep 17 00:00:00 2001 From: BPR02 Date: Thu, 19 Mar 2026 10:01:24 -0700 Subject: [PATCH 02/12] Revert "fix(control): give dns time to boot up after tailscale up" This reverts commit f7ba96321eef03d3c9c1afb1ea58b658c2118c84. --- peerstash-control/peerstash/core/tailscale.py | 24 ++++--------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/peerstash-control/peerstash/core/tailscale.py b/peerstash-control/peerstash/core/tailscale.py index c63f319..73ac313 100644 --- a/peerstash-control/peerstash/core/tailscale.py +++ b/peerstash-control/peerstash/core/tailscale.py @@ -16,14 +16,11 @@ import json import subprocess -import time from typing import Optional import commentjson import requests -from peerstash.core.utils import logger - TAILSCALE_API = "https://api.tailscale.com/api/v2" @@ -165,21 +162,8 @@ def generate_device_invite(api_token: str) -> Optional[str]: device_id = _get_local_device_id() url = f"{TAILSCALE_API}/device/{device_id}/device-invites" payload = [{"multiUse": True}] - # Retry loop to wait for MagicDNS to stabilize - max_retries = 5 - for attempt in range(max_retries): - try: - response = requests.post(url, auth=(api_token, ""), json=payload) - response.raise_for_status() + response = requests.post(url, auth=(api_token, ""), json=payload) + response.raise_for_status() - full_url: Optional[str] = response.json()[0].get("inviteUrl") - return full_url.split("/")[-1] if full_url else None - - except requests.exceptions.ConnectionError as e: - if attempt < max_retries - 1: - logger.warning( - f"Tailscale URL failed to resolve ({attempt}). Retrying in 2 seconds..." - ) - time.sleep(2) # Wait 2 seconds and try again - continue - raise RuntimeError(f"DNS failed to stabilize after Tailscale up: {e}") + full_url: Optional[str] = response.json()[0].get("inviteUrl") + return full_url.split("/")[-1] if full_url else None From 3c9e10ec46a82465ca1d6a667580d3b3fbf5fec5 Mon Sep 17 00:00:00 2001 From: BPR02 Date: Thu, 19 Mar 2026 10:12:36 -0700 Subject: [PATCH 03/12] allow non-sudo restic password file gen --- peerstash-control/scripts/setup.sh | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/peerstash-control/scripts/setup.sh b/peerstash-control/scripts/setup.sh index e4651ec..8d9c836 100644 --- a/peerstash-control/scripts/setup.sh +++ b/peerstash-control/scripts/setup.sh @@ -17,13 +17,14 @@ # along with this program. If not, see . -SSH_FOLDER="/var/lib/peerstash" +PEERSTASH_CONFIG="/var/lib/peerstash" +chown "$USERNAME":"$USERNAME" "$PEERSTASH_CONFIG" # set up logging LOG_DIR="/var/log/peerstash" mkdir -p "$LOG_DIR" -BIND_LOG_DIR="/var/lib/peerstash/logs" +BIND_LOG_DIR="$PEERSTASH_CONFIG/logs" mkdir -p "$BIND_LOG_DIR" if [ ! -L "$LOG_DIR" ]; then @@ -46,13 +47,13 @@ exec > >(while IFS= read -r line; do echo "[$(date '+%Y-%m-%d %H:%M:%S')] $line" # Generate SSH host keys mkdir -p /var/run/sshd -if [ ! -f "$SSH_FOLDER"/ssh_host_rsa_key ]; then +if [ ! -f "$PEERSTASH_CONFIG"/ssh_host_rsa_key ]; then echo "Generating SSH host keys..." ssh-keygen -A - cp /etc/ssh/ssh_host_* "$SSH_FOLDER"/ + cp /etc/ssh/ssh_host_* "$PEERSTASH_CONFIG"/ else echo "Using existing SSH host keys..." - cp "$SSH_FOLDER"/ssh_host_* /etc/ssh/ + cp "$PEERSTASH_CONFIG"/ssh_host_* /etc/ssh/ fi # create admin user @@ -63,15 +64,15 @@ adduser "$USERNAME" sudo # Generate SSH user keys mkdir -p /home/"$USERNAME"/.ssh -if [ ! -f "$SSH_FOLDER"/id_ed25519 ]; then +if [ ! -f "$PEERSTASH_CONFIG"/id_ed25519 ]; then echo "Generating SSH user keys..." >&2 if [ ! -f /home/"$USERNAME"/.ssh/id_ed25519 ]; then ssh-keygen -t ed25519 -N "" -f /home/"$USERNAME"/.ssh/id_ed25519 -C "$USERNAME" fi - cp /home/"$USERNAME"/.ssh/id_* $SSH_FOLDER/ + cp /home/"$USERNAME"/.ssh/id_* $PEERSTASH_CONFIG/ else echo "Using existing SSH user keys..." >&2 - cp $SSH_FOLDER/id_* /home/"$USERNAME"/.ssh/ + cp $PEERSTASH_CONFIG/id_* /home/"$USERNAME"/.ssh/ fi { echo "" From 61b1eed7b141a3fbb49bebde95ed8296384b9f74 Mon Sep 17 00:00:00 2001 From: BPR02 Date: Thu, 19 Mar 2026 10:25:59 -0700 Subject: [PATCH 04/12] fix config folder perms attempt 2 --- peerstash-control/scripts/setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/peerstash-control/scripts/setup.sh b/peerstash-control/scripts/setup.sh index 8d9c836..54cf048 100644 --- a/peerstash-control/scripts/setup.sh +++ b/peerstash-control/scripts/setup.sh @@ -18,7 +18,7 @@ PEERSTASH_CONFIG="/var/lib/peerstash" -chown "$USERNAME":"$USERNAME" "$PEERSTASH_CONFIG" +chown "$USERNAME":"$USERNAME" $PEERSTASH_CONFIG # set up logging LOG_DIR="/var/log/peerstash" From 87750d217006472e31d5d66e74c0d73c6040f701 Mon Sep 17 00:00:00 2001 From: BPR02 Date: Thu, 19 Mar 2026 10:32:36 -0700 Subject: [PATCH 05/12] attempt 3 --- peerstash-control/scripts/setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/peerstash-control/scripts/setup.sh b/peerstash-control/scripts/setup.sh index 54cf048..c2c29dc 100644 --- a/peerstash-control/scripts/setup.sh +++ b/peerstash-control/scripts/setup.sh @@ -18,7 +18,6 @@ PEERSTASH_CONFIG="/var/lib/peerstash" -chown "$USERNAME":"$USERNAME" $PEERSTASH_CONFIG # set up logging LOG_DIR="/var/log/peerstash" @@ -81,6 +80,7 @@ fi } > /home/"$USERNAME"/.ssh/config echo "" > /home/"$USERNAME"/.ssh/known_hosts chown -R "$USERNAME":"$USERNAME" /home/"$USERNAME"/.ssh +chown "$USERNAME":"$USERNAME" "$PEERSTASH_CONFIG" # prevent indexing FUSE mounts From c0dfdc38574b67d5b8b14501c726c6f7bd70059e Mon Sep 17 00:00:00 2001 From: BPR02 Date: Thu, 19 Mar 2026 13:58:45 -0700 Subject: [PATCH 06/12] force perms of `/tmp/peerstash_mnt` - it's set up in Dockerfile, but maybe something breaks it --- peerstash-control/scripts/setup.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/peerstash-control/scripts/setup.sh b/peerstash-control/scripts/setup.sh index c2c29dc..546bdd1 100644 --- a/peerstash-control/scripts/setup.sh +++ b/peerstash-control/scripts/setup.sh @@ -80,7 +80,10 @@ fi } > /home/"$USERNAME"/.ssh/config echo "" > /home/"$USERNAME"/.ssh/known_hosts chown -R "$USERNAME":"$USERNAME" /home/"$USERNAME"/.ssh + +# set up filesystem perms chown "$USERNAME":"$USERNAME" "$PEERSTASH_CONFIG" +chmod 777 /tmp/peerstash_mnt # prevent indexing FUSE mounts From 9126b92bc8f20ec3087d72a0ed7580b4e4ef3648 Mon Sep 17 00:00:00 2001 From: BPR02 Date: Thu, 19 Mar 2026 14:19:59 -0700 Subject: [PATCH 07/12] add error message for unmounting without perms --- peerstash-control/peerstash/cli/cmd_unmount.py | 3 +++ peerstash-control/peerstash/core/backup.py | 8 ++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/peerstash-control/peerstash/cli/cmd_unmount.py b/peerstash-control/peerstash/cli/cmd_unmount.py index 9e8a66a..ffaf8f7 100644 --- a/peerstash-control/peerstash/cli/cmd_unmount.py +++ b/peerstash-control/peerstash/cli/cmd_unmount.py @@ -39,6 +39,9 @@ def unmount( except ValueError as e: typer.secho(f"Error: {e}", fg=typer.colors.RED, err=True) raise typer.Exit(1) + except RuntimeError as e: + typer.secho(f"Error: {e}", fg=typer.colors.RED, err=True) + raise typer.Exit(1) except Exception as e: typer.secho(f"System Error: {e}", fg=typer.colors.RED, err=True) raise typer.Exit(1) diff --git a/peerstash-control/peerstash/core/backup.py b/peerstash-control/peerstash/core/backup.py index 884dea8..e433096 100644 --- a/peerstash-control/peerstash/core/backup.py +++ b/peerstash-control/peerstash/core/backup.py @@ -576,7 +576,11 @@ def unmount_task(name: str) -> None: subprocess.run(["fusermount", "-uz", mount_point], capture_output=True) # delete the file if exists - if os.path.exists(mount_point): - shutil.rmtree(mount_point) + try: + if os.path.exists(mount_point): + shutil.rmtree(mount_point) + except PermissionError: + log(f"[{name}] Failed to remove repo folders. No permissions for {mount_point}", "warning") + raise RuntimeError(f"No permissions. Use sudo if mounted as root.") logger.info(f"[{name}] Unmounted repo.") From 74650f14b46ab8fab30ce3392d9a2ec71e1d34a5 Mon Sep 17 00:00:00 2001 From: BPR02 Date: Thu, 19 Mar 2026 14:29:47 -0700 Subject: [PATCH 08/12] catch proper error --- peerstash-control/peerstash/core/backup.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/peerstash-control/peerstash/core/backup.py b/peerstash-control/peerstash/core/backup.py index e433096..60e567b 100644 --- a/peerstash-control/peerstash/core/backup.py +++ b/peerstash-control/peerstash/core/backup.py @@ -14,6 +14,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import errno import os import random import shutil @@ -579,8 +580,11 @@ def unmount_task(name: str) -> None: try: if os.path.exists(mount_point): shutil.rmtree(mount_point) - except PermissionError: - log(f"[{name}] Failed to remove repo folders. No permissions for {mount_point}", "warning") - raise RuntimeError(f"No permissions. Use sudo if mounted as root.") + except OSError as e: + if e.errno == errno.EROFS: + log(f"[{name}] Failed to remove repo folders. No permissions for {mount_point}", "warning") + raise RuntimeError(f"No permissions. Use sudo if mounted as root.") + else: + raise OSError(e) logger.info(f"[{name}] Unmounted repo.") From ad7f8eca0c57fde0237146ad1e21dcc6a6ed529c Mon Sep 17 00:00:00 2001 From: BPR02 Date: Thu, 19 Mar 2026 14:32:58 -0700 Subject: [PATCH 09/12] log any uncaught errors with unmount --- peerstash-control/peerstash/core/backup.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/peerstash-control/peerstash/core/backup.py b/peerstash-control/peerstash/core/backup.py index 60e567b..18b6319 100644 --- a/peerstash-control/peerstash/core/backup.py +++ b/peerstash-control/peerstash/core/backup.py @@ -585,6 +585,10 @@ def unmount_task(name: str) -> None: log(f"[{name}] Failed to remove repo folders. No permissions for {mount_point}", "warning") raise RuntimeError(f"No permissions. Use sudo if mounted as root.") else: + logger.error(f"[{name}] Failed to unmount repo: {e}") raise OSError(e) + except Exception as e: + logger.error(f"[{name}] Failed to unmount repo: {e}") + raise Exception(e) logger.info(f"[{name}] Unmounted repo.") From 9975ba911316434a0f730462d2f407be4fb56007 Mon Sep 17 00:00:00 2001 From: BPR02 Date: Thu, 19 Mar 2026 14:44:26 -0700 Subject: [PATCH 10/12] unmount all tasks upon start up --- peerstash-control/peerstash/setup.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/peerstash-control/peerstash/setup.py b/peerstash-control/peerstash/setup.py index 642ae41..99f4d77 100644 --- a/peerstash-control/peerstash/setup.py +++ b/peerstash-control/peerstash/setup.py @@ -23,6 +23,7 @@ import requests +from peerstash.core.backup import unmount_task from peerstash.core.utils import update_crontab from peerstash.cli import __version__ @@ -60,6 +61,8 @@ def init_db_and_restore(): f"{prune_schedule} {PEERSTASH_BIN} prune {name} 10" ) update_crontab(name, [backup_job, prune_job]) + # force unmount stale repos + unmount_task(name) else: print("No database found. Creating a new empty database...") From 49c9961b1f2e9d81a7144254fdaf37a095d97b51 Mon Sep 17 00:00:00 2001 From: BPR02 Date: Thu, 19 Mar 2026 17:43:16 -0700 Subject: [PATCH 11/12] allow anyone to write to `/mnt/peerstash_restore` --- peerstash-control/scripts/setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/peerstash-control/scripts/setup.sh b/peerstash-control/scripts/setup.sh index 546bdd1..b2b8d9a 100644 --- a/peerstash-control/scripts/setup.sh +++ b/peerstash-control/scripts/setup.sh @@ -84,7 +84,7 @@ chown -R "$USERNAME":"$USERNAME" /home/"$USERNAME"/.ssh # set up filesystem perms chown "$USERNAME":"$USERNAME" "$PEERSTASH_CONFIG" chmod 777 /tmp/peerstash_mnt - +chmod 777 /mnt/peerstash_restore # prevent indexing FUSE mounts touch /tmp/peerstash_mnt/.nomedia From 8eff7bff6281d4891399027d34ecc9b1b4a8e9df Mon Sep 17 00:00:00 2001 From: BPR02 Date: Thu, 19 Mar 2026 17:44:17 -0700 Subject: [PATCH 12/12] remove temp restore folder after use --- peerstash-control/peerstash/core/backup.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/peerstash-control/peerstash/core/backup.py b/peerstash-control/peerstash/core/backup.py index 18b6319..78c77d0 100644 --- a/peerstash-control/peerstash/core/backup.py +++ b/peerstash-control/peerstash/core/backup.py @@ -497,6 +497,9 @@ def restore_snapshot( raise Exception( f"Failed to restore snapshot '{snapshot}' for task '{name}' ({e})" ) + finally: + if os.path.exists(temp_folder): + shutil.rmtree(temp_folder) logger.info(f"[{name}] Restored snapshot {snapshot} in {folder}") return folder