Add Manual Backup improvements: lsblk drive filtering, UI instructions, CSS border fixes
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/a43d270d-eb78-4ad3-b721-fe958883c305 Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
34db1439fa
commit
cc72968583
@@ -1,11 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
# ── Sovran Hub Backup Script ─────────────────────────────────────
|
||||
# ── Sovran Hub External Backup Script ────────────────────────────
|
||||
# Backs up Sovran_SystemsOS data to an external USB hard drive.
|
||||
# Designed for the Hub web UI (no GUI dependencies).
|
||||
#
|
||||
# Your Sovran Pro already backs up your data automatically to its
|
||||
# internal second drive (BTCEcoandBackup at /run/media/Second_Drive).
|
||||
# This script creates an additional copy on an external USB drive —
|
||||
# storing your data in a third location for maximum protection.
|
||||
#
|
||||
# Usage:
|
||||
# BACKUP_TARGET=/run/media/<user>/<drive> bash sovran-hub-backup.sh
|
||||
# (or run with no env var to auto-detect the first external drive)
|
||||
# (or run with no env var to auto-detect the first external USB drive)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
@@ -14,6 +19,10 @@ BACKUP_STATUS="/var/log/sovran-hub-backup.status"
|
||||
MEDIA_ROOT="/run/media"
|
||||
MIN_FREE_GB=10
|
||||
|
||||
# ── Internal drive labels/paths to NEVER use as backup targets ───
|
||||
INTERNAL_LABELS=("BTCEcoandBackup" "sovran_systemsos")
|
||||
INTERNAL_MOUNTS=("/run/media/Second_Drive" "/boot/efi" "/")
|
||||
|
||||
# ── Logging helpers ──────────────────────────────────────────────
|
||||
|
||||
log() {
|
||||
@@ -31,33 +40,113 @@ fail() {
|
||||
exit 1
|
||||
}
|
||||
|
||||
# ── Check whether a mount point is an internal drive ────────────
|
||||
|
||||
is_internal() {
|
||||
local mnt="$1"
|
||||
# Reject known internal mount points and their subdirectories
|
||||
for internal in "${INTERNAL_MOUNTS[@]}"; do
|
||||
if [[ "$mnt" == "$internal" || "$mnt" == "${internal}/"* ]]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# ── Use lsblk to find the first genuine external USB drive ───────
|
||||
|
||||
find_external_drive() {
|
||||
local target=""
|
||||
# lsblk JSON output: NAME,LABEL,MOUNTPOINT,HOTPLUG,RM,TYPE
|
||||
if command -v lsblk &>/dev/null; then
|
||||
while IFS=$'\t' read -r dev_type hotplug removable label mountpoint; do
|
||||
# Must be a partition or disk, and be removable/hotplug
|
||||
[[ "$dev_type" == "part" || "$dev_type" == "disk" ]] || continue
|
||||
[[ "$hotplug" == "1" || "$removable" == "1" ]] || continue
|
||||
[[ -n "$mountpoint" ]] || continue
|
||||
|
||||
# Filter out internal labels
|
||||
local skip=0
|
||||
for lbl in "${INTERNAL_LABELS[@]}"; do
|
||||
[[ "$label" == "$lbl" ]] && skip=1 && break
|
||||
done
|
||||
[[ "$skip" -eq 1 ]] && continue
|
||||
|
||||
# Filter out internal mount points
|
||||
is_internal "$mountpoint" && continue
|
||||
|
||||
if mountpoint -q "$mountpoint" 2>/dev/null; then
|
||||
target="$mountpoint"
|
||||
break
|
||||
fi
|
||||
done < <(lsblk -J -o NAME,LABEL,MOUNTPOINT,HOTPLUG,RM,TYPE 2>/dev/null | \
|
||||
python3 -c "
|
||||
import sys, json
|
||||
data = json.load(sys.stdin)
|
||||
def flatten(devs):
|
||||
for d in devs:
|
||||
yield d
|
||||
for c in d.get('children', []):
|
||||
yield from flatten([c])
|
||||
for d in flatten(data.get('blockdevices', [])):
|
||||
print('\t'.join([
|
||||
d.get('type') or '',
|
||||
str(d.get('hotplug') or '0'),
|
||||
str(d.get('rm') or '0'),
|
||||
d.get('label') or '',
|
||||
d.get('mountpoint') or '',
|
||||
]))
|
||||
" 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
# Fallback: walk /run/media/ if lsblk produced nothing
|
||||
if [[ -z "$target" && -d "$MEDIA_ROOT" ]]; then
|
||||
while IFS= read -r -d '' mnt; do
|
||||
is_internal "$mnt" && continue
|
||||
# Check label via lsblk on the device backing this mount
|
||||
local dev
|
||||
dev=$(findmnt -n -o SOURCE "$mnt" 2>/dev/null || true)
|
||||
if [[ -n "$dev" ]]; then
|
||||
local lbl
|
||||
lbl=$(lsblk -n -o LABEL "$dev" 2>/dev/null || true)
|
||||
local skip=0
|
||||
for internal_lbl in "${INTERNAL_LABELS[@]}"; do
|
||||
[[ "$lbl" == "$internal_lbl" ]] && skip=1 && break
|
||||
done
|
||||
[[ "$skip" -eq 1 ]] && continue
|
||||
fi
|
||||
if mountpoint -q "$mnt" 2>/dev/null; then
|
||||
target="$mnt"
|
||||
break
|
||||
fi
|
||||
done < <(find "$MEDIA_ROOT" -mindepth 2 -maxdepth 2 -type d -print0 2>/dev/null)
|
||||
fi
|
||||
|
||||
echo "$target"
|
||||
}
|
||||
|
||||
# ── Initialise log file ──────────────────────────────────────────
|
||||
|
||||
: > "$BACKUP_LOG"
|
||||
set_status "RUNNING"
|
||||
|
||||
log "=== Sovran_SystemsOS Hub Backup ==="
|
||||
log "=== Sovran_SystemsOS External Hub Backup ==="
|
||||
log "Starting backup process…"
|
||||
|
||||
# ── Detect target drive ──────────────────────────────────────────
|
||||
|
||||
if [[ -n "${BACKUP_TARGET:-}" ]]; then
|
||||
TARGET="$BACKUP_TARGET"
|
||||
# Safety: never allow internal drives even if explicitly passed
|
||||
if is_internal "$TARGET"; then
|
||||
fail "Target '$TARGET' is an internal system drive and cannot be used for external backup."
|
||||
fi
|
||||
log "Using specified backup target: $TARGET"
|
||||
else
|
||||
log "Auto-detecting external drives under $MEDIA_ROOT …"
|
||||
TARGET=""
|
||||
if [[ -d "$MEDIA_ROOT" ]]; then
|
||||
# Walk /run/media/<user>/<drive>
|
||||
while IFS= read -r -d '' mnt; do
|
||||
if mountpoint -q "$mnt" 2>/dev/null; then
|
||||
TARGET="$mnt"
|
||||
break
|
||||
fi
|
||||
done < <(find "$MEDIA_ROOT" -mindepth 2 -maxdepth 2 -type d -print0 2>/dev/null)
|
||||
fi
|
||||
log "Auto-detecting external USB drives…"
|
||||
TARGET="$(find_external_drive)"
|
||||
if [[ -z "$TARGET" ]]; then
|
||||
fail "No external drive detected under $MEDIA_ROOT. " \
|
||||
fail "No external USB drive detected. " \
|
||||
"Please plug in an exFAT-formatted USB drive (≥500 GB) and try again."
|
||||
fi
|
||||
log "Detected external drive: $TARGET"
|
||||
@@ -169,5 +258,6 @@ log "Manifest written to $BACKUP_DIR/BACKUP_MANIFEST.txt"
|
||||
# ── Done ─────────────────────────────────────────────────────────
|
||||
|
||||
log ""
|
||||
log "All Finished! Please eject the drive before removing it from your Sovran Pro."
|
||||
log "All Finished! Your data is now backed up to a third location."
|
||||
log "Please eject the drive safely before removing it from your Sovran Pro."
|
||||
set_status "SUCCESS"
|
||||
|
||||
@@ -774,10 +774,94 @@ def _read_backup_log(offset: int = 0) -> tuple[str, int]:
|
||||
return "", 0
|
||||
|
||||
|
||||
_INTERNAL_LABELS = {"BTCEcoandBackup", "sovran_systemsos"}
|
||||
_INTERNAL_MOUNTS = {"/", "/boot/efi"}
|
||||
_INTERNAL_MOUNT_PREFIX = "/run/media/Second_Drive"
|
||||
|
||||
|
||||
def _is_internal_mount(mnt: str) -> bool:
|
||||
"""Return True if *mnt* is a known internal system path."""
|
||||
if mnt in _INTERNAL_MOUNTS:
|
||||
return True
|
||||
if mnt == _INTERNAL_MOUNT_PREFIX or mnt.startswith(_INTERNAL_MOUNT_PREFIX + "/"):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _detect_external_drives() -> list[dict]:
|
||||
"""Scan /run/media/ for mounted external drives.
|
||||
Returns a list of dicts with name, path, free_gb, total_gb."""
|
||||
drives = []
|
||||
"""Scan for mounted external USB drives.
|
||||
|
||||
Uses ``lsblk`` to identify genuinely removable/hotplug devices and
|
||||
filters out internal system drives (BTCEcoandBackup, sovran_systemsos,
|
||||
/boot/efi, /run/media/Second_Drive). Falls back to scanning
|
||||
/run/media/ directly if lsblk is unavailable, applying the same
|
||||
label/path filters.
|
||||
|
||||
Returns a list of dicts with name, path, free_gb, total_gb.
|
||||
"""
|
||||
import json as _json
|
||||
import subprocess as _subprocess
|
||||
|
||||
drives: list[dict] = []
|
||||
seen_paths: set[str] = set()
|
||||
|
||||
# ── Primary path: lsblk JSON ────────────────────────────────
|
||||
try:
|
||||
result = _subprocess.run(
|
||||
["lsblk", "-J", "-o", "NAME,LABEL,MOUNTPOINT,HOTPLUG,RM,TYPE"],
|
||||
capture_output=True, text=True, timeout=10
|
||||
)
|
||||
if result.returncode == 0:
|
||||
data = _json.loads(result.stdout)
|
||||
|
||||
def _flatten(devs: list) -> list:
|
||||
out = []
|
||||
for d in devs:
|
||||
out.append(d)
|
||||
out.extend(_flatten(d.get("children") or []))
|
||||
return out
|
||||
|
||||
for dev in _flatten(data.get("blockdevices", [])):
|
||||
dev_type = dev.get("type", "")
|
||||
hotplug = str(dev.get("hotplug", "0"))
|
||||
rm = str(dev.get("rm", "0"))
|
||||
label = dev.get("label") or ""
|
||||
mountpoint = dev.get("mountpoint") or ""
|
||||
|
||||
if dev_type not in ("part", "disk"):
|
||||
continue
|
||||
if hotplug != "1" and rm != "1":
|
||||
continue
|
||||
if not mountpoint:
|
||||
continue
|
||||
if label in _INTERNAL_LABELS:
|
||||
continue
|
||||
if _is_internal_mount(mountpoint):
|
||||
continue
|
||||
if mountpoint in seen_paths:
|
||||
continue
|
||||
|
||||
try:
|
||||
st = os.statvfs(mountpoint)
|
||||
total_gb = round((st.f_blocks * st.f_frsize) / (1024 ** 3), 1)
|
||||
free_gb = round((st.f_bavail * st.f_frsize) / (1024 ** 3), 1)
|
||||
name = label if label else os.path.basename(mountpoint)
|
||||
drives.append({
|
||||
"name": name,
|
||||
"path": mountpoint,
|
||||
"free_gb": free_gb,
|
||||
"total_gb": total_gb,
|
||||
})
|
||||
seen_paths.add(mountpoint)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
if drives:
|
||||
return drives
|
||||
except Exception: # lsblk not available or JSON parse error
|
||||
pass
|
||||
|
||||
# ── Fallback: scan /run/media/ ───────────────────────────────
|
||||
media_root = "/run/media"
|
||||
if not os.path.isdir(media_root):
|
||||
return drives
|
||||
@@ -786,20 +870,27 @@ def _detect_external_drives() -> list[dict]:
|
||||
user_path = os.path.join(media_root, user)
|
||||
if not os.path.isdir(user_path):
|
||||
continue
|
||||
for drive in os.listdir(user_path):
|
||||
drive_path = os.path.join(user_path, drive)
|
||||
for drive_name in os.listdir(user_path):
|
||||
drive_path = os.path.join(user_path, drive_name)
|
||||
if not os.path.isdir(drive_path):
|
||||
continue
|
||||
if drive_name in _INTERNAL_LABELS:
|
||||
continue
|
||||
if _is_internal_mount(drive_path):
|
||||
continue
|
||||
if drive_path in seen_paths:
|
||||
continue
|
||||
try:
|
||||
st = os.statvfs(drive_path)
|
||||
total_gb = round((st.f_blocks * st.f_frsize) / (1024 ** 3), 1)
|
||||
free_gb = round((st.f_bavail * st.f_frsize) / (1024 ** 3), 1)
|
||||
drives.append({
|
||||
"name": drive,
|
||||
"name": drive_name,
|
||||
"path": drive_path,
|
||||
"free_gb": free_gb,
|
||||
"total_gb": total_gb,
|
||||
})
|
||||
seen_paths.add(drive_path)
|
||||
except OSError:
|
||||
pass
|
||||
except OSError:
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
background-color: var(--card-color);
|
||||
border: 2px dashed var(--accent-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
padding: 12px 14px;
|
||||
color: var(--text-primary);
|
||||
@@ -44,11 +44,15 @@
|
||||
}
|
||||
|
||||
.sidebar-support-btn:hover {
|
||||
border-color: var(--accent-color);
|
||||
border-style: solid;
|
||||
border-color: #a8c8ff;
|
||||
background-color: #35354a;
|
||||
}
|
||||
|
||||
.sidebar-support-btn + .sidebar-support-btn {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.sidebar-support-icon {
|
||||
font-size: 1.5rem;
|
||||
flex-shrink: 0;
|
||||
|
||||
@@ -351,13 +351,13 @@
|
||||
/* ── Tech Support tile ───────────────────────────────────────────── */
|
||||
|
||||
.support-tile {
|
||||
border-color: var(--accent-color);
|
||||
border-width: 2px;
|
||||
border-style: dashed;
|
||||
border-color: var(--border-color);
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.support-tile:hover {
|
||||
border-color: #a8c8ff;
|
||||
border-color: var(--accent-color);
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
|
||||
@@ -301,7 +301,8 @@ function renderBackupReady(drives) {
|
||||
if (drives.length > 0) {
|
||||
driveSelector = [
|
||||
'<label class="support-info-label" style="display:block;margin-bottom:6px;">Select drive:</label>',
|
||||
'<select id="backup-drive-select" class="support-unlock-select" style="width:100%;margin-bottom:14px;">',
|
||||
'<div style="display:flex;gap:8px;align-items:center;margin-bottom:14px;">',
|
||||
'<select id="backup-drive-select" class="support-unlock-select" style="flex:1;">',
|
||||
].join("");
|
||||
for (var i = 0; i < drives.length; i++) {
|
||||
var d = drives[i];
|
||||
@@ -310,6 +311,8 @@ function renderBackupReady(drives) {
|
||||
'</option>';
|
||||
}
|
||||
driveSelector += '</select>';
|
||||
driveSelector += '<button class="btn support-btn-auditlog" id="btn-backup-refresh" style="white-space:nowrap;">↻ Refresh</button>';
|
||||
driveSelector += '</div>';
|
||||
driveSelector += '<button class="btn support-btn-enable" id="btn-start-backup">Start Backup</button>';
|
||||
} else {
|
||||
driveSelector = [
|
||||
@@ -331,7 +334,15 @@ function renderBackupReady(drives) {
|
||||
'<div class="support-section">',
|
||||
'<div class="support-icon-big">\ud83d\udcbe</div>',
|
||||
'<h3 class="support-heading">Manual Backup</h3>',
|
||||
'<p class="support-desc">Back up your Sovran_SystemsOS data to an external USB hard drive.</p>',
|
||||
|
||||
'<div class="support-wallet-box support-wallet-protected" style="margin-bottom:16px;">',
|
||||
'<p class="support-wallet-desc">',
|
||||
'Your Sovran Pro already backs up your data automatically to its internal second drive. ',
|
||||
'This manual backup lets you create an additional copy on an external USB drive \u2014 ',
|
||||
'storing your data in a third location, outside the computer, for maximum protection ',
|
||||
'against hardware failure or physical damage.',
|
||||
'</p>',
|
||||
'</div>',
|
||||
|
||||
'<div class="support-steps">',
|
||||
'<div class="support-steps-title">Requirements</div>',
|
||||
@@ -353,7 +364,7 @@ function renderBackupReady(drives) {
|
||||
'</ol>',
|
||||
'</div>',
|
||||
|
||||
'<div class="support-wallet-box support-wallet-protected">',
|
||||
'<div class="support-wallet-box support-wallet-warning">',
|
||||
'<div class="support-wallet-header">',
|
||||
'<span class="support-wallet-icon">\u23f1\ufe0f</span>',
|
||||
'<span class="support-wallet-title">Time Estimate</span>',
|
||||
@@ -367,9 +378,13 @@ function renderBackupReady(drives) {
|
||||
|
||||
if (drives.length > 0) {
|
||||
document.getElementById("btn-start-backup").addEventListener("click", startBackup);
|
||||
document.getElementById("btn-backup-refresh").addEventListener("click", function() {
|
||||
$supportBody.innerHTML = '<p class="creds-loading">Scanning for external drives\u2026</p>';
|
||||
detectDrivesAndRender();
|
||||
});
|
||||
} else {
|
||||
document.getElementById("btn-backup-refresh").addEventListener("click", function() {
|
||||
$supportBody.innerHTML = '<p class="creds-loading">Detecting external drives\u2026</p>';
|
||||
$supportBody.innerHTML = '<p class="creds-loading">Scanning for external drives\u2026</p>';
|
||||
detectDrivesAndRender();
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user