Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/cloud_backup.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ The Cloud Backup page manages cloud backup settings, schedules, and status.
- **Save**: Button to save schedule settings by disabling during the request.
- **Error/Success Feedback**: Inline messages for save actions.

## File Filters
- **Exclude Patterns**: Skip files or folders that match these rclone patterns.
- **Include Patterns**: Back up only files or folders that match these rclone patterns.
- Enter one plain rclone pattern per line. Blank lines and lines that start with `#` or `;` are ignored.
- Do not enter rclone filter rule prefixes such as `+`, `-`, or `!`. SimpleSaferServer creates the rclone `--filter-from` file for the backup run.
- Exclude patterns are written first, then include patterns. If at least one include pattern exists, SimpleSaferServer adds a final `- **` rule so the include list works like an allow-list.
- These pattern files are stored under the app config directory and are passed to rclone through `--filter-from`, so the full pattern list is not placed in the rclone process arguments.

## Cloud Backup Settings
- **Backup Mode**: Choose between:
- MEGA (Simple)
Expand Down
7 changes: 6 additions & 1 deletion docs/dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,19 @@ Four cards display real-time status:
- **System Resources**: Displays CPU and RAM usage, and live network traffic (up/down rates).

## Task Schedule
- **Table**: Lists all scheduled tasks with columns for Task, Status, Last Run, and Next Run.
- **Table**: Lists all scheduled tasks with columns for Task, Status, Last Run, Next Run, and
Automatic Runs.
- **Next Run**: Shows the active next run time or a short schedule state label. Temporary disables
show `Disabled until 18:00`, `Disabled until Tomorrow 18:00`, or a later date such as
`Disabled until May 16 18:00`. Permanent disables show `Disabled`. Timers disabled outside
SimpleSaferServer show `Disabled externally`; unexpected timer states show `Schedule issue`.
Disabled schedule labels are danger-colored in this field only, so automatic-run suspension stands
out without making the entire task row look failed. Schedule issues remain warning-colored because
they mean the timer state needs investigation.
- **Automatic Runs**: The `Check Mount`, `Drive Health Check`, and `Cloud Backup` rows have a direct
on/off switch. Turning the switch off permanently disables that task's systemd `.timer`, so the
task no longer starts on its schedule. Turning the switch on enables the timer again. Manual Start
from the task page still starts the `.service` immediately.
- **Task Schedule Control**: Right-click a task row to Start, Stop, Disable Schedule, or Enable
Schedule when that action applies. The menu stays open across passive schedule refreshes so the
operator does not lose the selected row actions while reading the menu.
Expand Down
4 changes: 4 additions & 0 deletions docs/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ Advanced mode:
gives the mount check time to finish before health probes the drive, even with systemd's small
randomized delay.
- The installer may generate those unit files earlier, but it keeps the timers inactive while `system.setup_complete` is false so persistent timers cannot run with placeholder setup values.
- Setup creates a small self-backup archive on the mounted backup drive after completion.
- A daily self-backup timer then runs one minute before cloud backup, so cloud backup can copy the fresh setup archive too.
- Self-backup archives include SimpleSaferServer-owned setup config, rclone config, msmtp config, and owned Samba include files. They do not include `/etc/fstab` or a full system backup.
- Manual backup and restore commands are documented in [Setup Self-Backup](setup_self_backup.md).

## Later Changes

Expand Down
79 changes: 79 additions & 0 deletions docs/setup_self_backup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Setup Self-Backup

SimpleSaferServer can back up the files that its setup flow owns. This helps you recover the web app setup after a reinstall without copying the whole server.

This is not a full system backup.

## What It Includes

The archive includes these files when they exist:

- SimpleSaferServer config from `/etc/SimpleSaferServer`
- the user database, secret key, encrypted secrets, alerts, and disabled timer records
- rclone config from root's rclone config folder
- msmtp config from `/etc/msmtprc`
- SimpleSaferServer-owned Samba include files

It does not include `/etc/fstab`. That is intentional. Restoring an old mount table can make a server fail to boot or mount the wrong disk.

It also does not include the full backup drive data, operating system files, package state, or unmanaged Samba config.

## Automatic Backup

After setup is complete, SimpleSaferServer creates a self-backup on the configured backup drive.

It also installs a daily `setup_self_backup.timer`. This timer runs one minute before the normal cloud backup time, so the fresh self-backup archive can be copied to your cloud target by the normal cloud backup.

Archives are stored here on the mounted backup drive:

```text
SimpleSaferServer-self-backups/
```

Only the newest 30 archives are kept by the scheduled command.

## Manual Backup

Run this as root:

```bash
sudo /opt/SimpleSaferServer/.venv/bin/python /opt/SimpleSaferServer/scripts/setup_self_backup.py create
```

To list existing archives:

```bash
sudo /opt/SimpleSaferServer/.venv/bin/python /opt/SimpleSaferServer/scripts/setup_self_backup.py list
```

To write to a specific mounted drive path:

```bash
sudo /opt/SimpleSaferServer/.venv/bin/python /opt/SimpleSaferServer/scripts/setup_self_backup.py create --destination /media/backup
```

## Restore During Setup

Use this after reinstalling SimpleSaferServer, before finishing the setup wizard.

1. Mount the backup drive.
2. Find the archive under `SimpleSaferServer-self-backups/`.
3. Restore it:

```bash
sudo /opt/SimpleSaferServer/.venv/bin/python /opt/SimpleSaferServer/scripts/setup_self_backup.py restore /media/backup/SimpleSaferServer-self-backups/setup-self-backup-YYYYMMDDTHHMMSSZ.tar.gz
```

By default, restore sets `system.setup_complete` to `false`. This lets the setup wizard reinstall services, timers, Samba share setup, and the managed backup-drive setup for the current machine.

After the restore, open the setup wizard and finish setup. Check the backup drive step carefully. The self-backup does not restore `/etc/fstab`, so the current backup drive still needs to be mounted and registered by setup.

## Restore On A Running Install

The restore command is mainly meant for reinstall recovery. If you run it on an already working install, restart SimpleSaferServer afterwards so the web app reloads the restored files.

Only use `--preserve-setup-complete` when you understand that setup will not be forced to rerun service and timer installation:

```bash
sudo /opt/SimpleSaferServer/.venv/bin/python /opt/SimpleSaferServer/scripts/setup_self_backup.py restore /path/to/archive.tar.gz --preserve-setup-complete
```
3 changes: 3 additions & 0 deletions docs/task_detail.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ The Task Detail page shows information and logs for a specific scheduled task.
## Controls
- **Start**: Button to start the task (confirmation required).
- **Stop**: Button to stop the task (confirmation required).
- **Automatic Runs**: On `Check Mount`, `Drive Health Check`, and `Cloud Backup`, this switch turns
scheduled runs on or off. Off permanently disables that task's systemd `.timer`; On enables it
again. Manual Start still starts the task immediately.
- **Disable Schedule**: Opens a modal for disabling automatic runs for 1 hour, 6 hours, 24 hours,
7 days, or permanently. This disables the systemd `.timer` only; manual Start still starts the
`.service`.
Expand Down
1 change: 1 addition & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ <h2 id="documentation" class="mb-3"><i class="fa-solid fa-book"></i> Documentati
<li><i class="fa-solid fa-flask"></i> <a href="https://github.com/chrismin13/SimpleSaferServer/blob/main/docs/fake_mode.md" target="_blank">Fake Mode</a></li>
<li><i class="fa-solid fa-train-subway"></i> <a href="https://github.com/chrismin13/SimpleSaferServer/blob/main/docs/railway.md" target="_blank">Railway Deployment Notes</a></li>
<li><i class="fa-solid fa-gear"></i> <a href="https://github.com/chrismin13/SimpleSaferServer/blob/main/docs/setup.md" target="_blank">Setup Guide</a></li>
<li><i class="fa-solid fa-box-archive"></i> <a href="https://github.com/chrismin13/SimpleSaferServer/blob/main/docs/setup_self_backup.md" target="_blank">Setup Self-Backup</a></li>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Add rel attributes to the external docs link opened in a new tab.

Line 162 uses target="_blank" without rel="noopener noreferrer", which weakens tab isolation.

Suggested fix
-            <li><i class="fa-solid fa-box-archive"></i> <a href="https://github.com/chrismin13/SimpleSaferServer/blob/main/docs/setup_self_backup.md" target="_blank">Setup Self-Backup</a></li>
+            <li><i class="fa-solid fa-box-archive"></i> <a href="https://github.com/chrismin13/SimpleSaferServer/blob/main/docs/setup_self_backup.md" target="_blank" rel="noopener noreferrer">Setup Self-Backup</a></li>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<li><i class="fa-solid fa-box-archive"></i> <a href="https://github.com/chrismin13/SimpleSaferServer/blob/main/docs/setup_self_backup.md" target="_blank">Setup Self-Backup</a></li>
<li><i class="fa-solid fa-box-archive"></i> <a href="https://github.com/chrismin13/SimpleSaferServer/blob/main/docs/setup_self_backup.md" target="_blank" rel="noopener noreferrer">Setup Self-Backup</a></li>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@index.html` at line 162, The external link to the SimpleSaferServer
repository with target="_blank" is missing the rel attribute for security
purposes. Locate the anchor tag that links to the GitHub setup_self_backup.md
documentation and add the rel="noopener noreferrer" attribute to the opening
anchor tag alongside the existing target="_blank" attribute. This prevents the
new tab from gaining access to the window object of the original page,
strengthening tab isolation.

<li><i class="fa-solid fa-right-to-bracket"></i> <a href="https://github.com/chrismin13/SimpleSaferServer/blob/main/docs/login.md" target="_blank">Login &amp; User Management</a></li>
<li><i class="fa-solid fa-gauge-high"></i> <a href="https://github.com/chrismin13/SimpleSaferServer/blob/main/docs/dashboard.md" target="_blank">Dashboard</a></li>
<li><i class="fa-solid fa-hard-drive"></i> <a href="https://github.com/chrismin13/SimpleSaferServer/blob/main/docs/drive_health.md" target="_blank">Drive Health</a></li>
Expand Down
60 changes: 60 additions & 0 deletions scripts/backup_cloud.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
#!/bin/bash

CONFIG_FILE="/etc/SimpleSaferServer/config.conf"
CONFIG_DIR="/etc/SimpleSaferServer"
RCLONE_INCLUDE_PATTERNS_FILE="$CONFIG_DIR/rclone_include_patterns.txt"
RCLONE_EXCLUDE_PATTERNS_FILE="$CONFIG_DIR/rclone_exclude_patterns.txt"
PYTHON_BIN="/opt/SimpleSaferServer/.venv/bin/python"
FILTER_FILE=""
FILTER_RULE_COUNT=0
INCLUDE_RULE_COUNT=0

if [ ! -x "$PYTHON_BIN" ]; then
echo "Missing SimpleSaferServer Python environment at $PYTHON_BIN" >&2
Expand All @@ -18,6 +24,58 @@ get_config_value() {
' "$CONFIG_FILE" | tr -d '"'
}

cleanup_filter_file() {
if [ -n "$FILTER_FILE" ] && [ -f "$FILTER_FILE" ]; then
rm -f "$FILTER_FILE"
fi
}

append_filter_patterns() {
local rule_prefix=$1
local pattern_file=$2
local pattern
local trimmed

[ -f "$pattern_file" ] || return 0

while IFS= read -r pattern || [ -n "$pattern" ]; do
trimmed=$pattern
trimmed="${trimmed#"${trimmed%%[![:space:]]*}"}"
trimmed="${trimmed%"${trimmed##*[![:space:]]}"}"
if [ -z "$trimmed" ] || [[ "$trimmed" == \#* ]] || [[ "$trimmed" == \;* ]]; then
continue
fi
printf "%s %s\n" "$rule_prefix" "$trimmed" >>"$FILTER_FILE"
FILTER_RULE_COUNT=$((FILTER_RULE_COUNT + 1))
if [ "$rule_prefix" = "+" ]; then
INCLUDE_RULE_COUNT=$((INCLUDE_RULE_COUNT + 1))
fi
done <"$pattern_file"
}

add_rclone_filter_args() {
FILTER_FILE=$(mktemp)
append_filter_patterns "-" "$RCLONE_EXCLUDE_PATTERNS_FILE"
append_filter_patterns "+" "$RCLONE_INCLUDE_PATTERNS_FILE"

if [ "$INCLUDE_RULE_COUNT" -gt 0 ]; then
# rclone --filter-from reads rules in order. With include rules present,
# this final rule makes the include list act like an allow-list.
printf "%s\n" "- **" >>"$FILTER_FILE"
FILTER_RULE_COUNT=$((FILTER_RULE_COUNT + 1))
fi

if [ "$FILTER_RULE_COUNT" -gt 0 ]; then
extra_args+=(--filter-from "$FILTER_FILE")
echo "Using configured rclone file filters."
else
cleanup_filter_file
FILTER_FILE=""
fi
}

trap cleanup_filter_file EXIT

MOUNT_POINT=$(get_config_value backup mount_point)
FROM_ADDRESS=$(get_config_value backup from_address)
EMAIL_ADDRESS=$(get_config_value backup email_address)
Expand Down Expand Up @@ -61,6 +119,8 @@ else
extra_args=()
fi

add_rclone_filter_args

echo "Starting cloud backup to $RCLONE_DIR..."
echo "Source: $MOUNT_POINT"
echo "Destination: $RCLONE_DIR"
Expand Down
118 changes: 118 additions & 0 deletions scripts/setup_self_backup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#!/usr/bin/env python3

import argparse
import json
import logging
import sys
from pathlib import Path


def _add_app_to_path() -> None:
script_path = Path(__file__).resolve()
candidates = [script_path.parents[1], Path("/opt/SimpleSaferServer")]
for candidate in candidates:
if (candidate / "simple_safer_server").is_dir():
sys.path.insert(0, str(candidate))
return
Comment on lines +10 to +16

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document the path-injection fallback and implicit default command.

Please add brief inline comments explaining (1) why sys.path is patched at runtime and (2) why no subcommand defaults to create, since both are easy to misinterpret later.

✍️ Suggested comment additions
 def _add_app_to_path() -> None:
+    # This script may run from installed locations where project root is not
+    # already on PYTHONPATH (for example under /opt).
     script_path = Path(__file__).resolve()
     candidates = [script_path.parents[1], Path("/opt/SimpleSaferServer")]
@@
 def main() -> int:
@@
-    command = args.command or "create"
+    # Timer/non-interactive invocations omit a subcommand, so default to create.
+    command = args.command or "create"

As per coding guidelines, **/*.{js,ts,tsx,jsx,py,java,go,rs,rb,php,cpp,c,h,cs}: Always include comments about things that might be forgotten in a few months or that might not immediately seem obvious on a first read.

Also applies to: 75-76

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/setup_self_backup.py` around lines 10 - 16, Add inline comments to
the _add_app_to_path function to explain why sys.path is being patched at
runtime (document the runtime path injection strategy). Additionally, add a
brief comment near the implicit default subcommand logic (around the area
mentioned at lines 75-76) to clarify why the 'create' subcommand is used as the
default when no subcommand is specified. Both comments should be concise and
explain the reasoning behind these design decisions to prevent future
misinterpretation.

Source: Coding guidelines



try:
from simple_safer_server.services.config_manager import ConfigManager
from simple_safer_server.services.runtime import get_runtime
from simple_safer_server.services.setup_self_backup import (
DEFAULT_RETENTION_COUNT,
SetupSelfBackupError,
SetupSelfBackupService,
)
except ImportError:
_add_app_to_path()
from simple_safer_server.services.config_manager import ConfigManager
from simple_safer_server.services.runtime import get_runtime
from simple_safer_server.services.setup_self_backup import (
DEFAULT_RETENTION_COUNT,
SetupSelfBackupError,
SetupSelfBackupService,
)


def _service() -> SetupSelfBackupService:
runtime = get_runtime()
return SetupSelfBackupService(runtime, ConfigManager(runtime=runtime))


def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Create, list, or restore SimpleSaferServer setup self-backups."
)
subparsers = parser.add_subparsers(dest="command")

create = subparsers.add_parser("create", help="Create a self-backup archive.")
create.add_argument("--destination", help="Mounted backup drive path to write under.")
create.add_argument(
"--keep",
type=int,
default=DEFAULT_RETENTION_COUNT,
help=f"Number of newest archives to keep. Default: {DEFAULT_RETENTION_COUNT}.",
)

list_parser = subparsers.add_parser("list", help="List self-backup archives.")
list_parser.add_argument("--destination", help="Mounted backup drive path to inspect.")
list_parser.add_argument("--json", action="store_true", help="Print machine-readable JSON.")

restore = subparsers.add_parser("restore", help="Restore a self-backup archive.")
restore.add_argument("archive", help="Archive path to restore.")
restore.add_argument(
"--preserve-setup-complete",
action="store_true",
help="Keep system.setup_complete exactly as stored in the archive.",
)
return parser.parse_args()


def main() -> int:
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
args = parse_args()
command = args.command or "create"
service = _service()

if command == "create":
result = service.create_backup(
destination=args.destination,
retention_count=args.keep,
)
print(f"Created setup self-backup: {result['archive']}")
print(f"Files included: {result['file_count']}")
return 0

if command == "list":
backups = service.list_backups(destination=args.destination)
if args.json:
print(json.dumps(backups, indent=2))
return 0
if not backups:
print("No setup self-backups found.")
return 0
for backup in backups:
print(f"{backup['name']} {backup['modified_at']} {backup['size']} bytes")
return 0

if command == "restore":
result = service.restore_backup(
args.archive,
force_setup_incomplete=not args.preserve_setup_complete,
)
print(f"Restored setup self-backup: {result['archive']}")
print(f"Files restored: {len(result['restored'])}")
if not args.preserve_setup_complete:
print("system.setup_complete was set to false so setup can reinstall services.")
return 0

raise SetupSelfBackupError(f"Unknown command: {command}")


if __name__ == "__main__":
try:
raise SystemExit(main())
except SetupSelfBackupError as exc:
print(f"ERROR: {exc}", file=sys.stderr)
raise SystemExit(1) from exc
5 changes: 5 additions & 0 deletions simple_safer_server/adapters/rclone.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ def sync(
*,
config_path: str | None = None,
bandwidth_limit: str = "",
filter_from: str | None = None,
):
command = self.build_sync_command(
source,
destination,
config_path=config_path,
bandwidth_limit=bandwidth_limit,
filter_from=filter_from,
)
return self._command_runner.popen(
command,
Expand All @@ -36,10 +38,13 @@ def build_sync_command(
*,
config_path: str | None = None,
bandwidth_limit: str = "",
filter_from: str | None = None,
) -> list[str]:
command = ["rclone", "sync", source, destination, "--create-empty-src-dirs", "-v"]
if config_path:
command.extend(["--config", config_path])
if bandwidth_limit:
command.extend(["--bwlimit", bandwidth_limit])
if filter_from:
command.extend(["--filter-from", filter_from])
return command
Loading
Loading