simple_nas/run.sh
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/with-contenv bashio
set -e
bashio::log.info "Starting Simple NAS..."
export WORKGROUP
WORKGROUP=$(bashio::config 'workgroup')
bashio::log.info "Workgroup: ${WORKGROUP}"
export NAS_NAME
NAS_NAME=$(bashio::config 'nas_name')
bashio::log.info "NAS Name: ${NAS_NAME}"
export WEB_PORT
WEB_PORT=$(bashio::config 'web_port')
bashio::log.info "Web GUI Port: ${WEB_PORT}"
# Home Assistant assigns the Ingress port itself (ingress_port: 0 in config.yaml). A fixed 8100 clashed
# with other host-network add-ons such as Dnsmasq-DHCP (issue #10); the GUI listens on both ports.
# bashio renamed addon.* to app.*; the old name only warns for now.
export INGRESS_PORT
if declare -F bashio::app.ingress_port > /dev/null; then
INGRESS_PORT=$(bashio::app.ingress_port 2>/dev/null || true)
else
INGRESS_PORT=$(bashio::addon.ingress_port 2>/dev/null || true)
fi
case "${INGRESS_PORT}" in
''|*[!0-9]*)
INGRESS_PORT=""
bashio::log.warning "Ingress port unknown - the Open button in Home Assistant will not work, use http://HA-IP:${WEB_PORT}" ;;
*) bashio::log.info "Ingress Port: ${INGRESS_PORT}" ;;
esac
export ADMIN_PASSWORD_ENABLED
ADMIN_PASSWORD_ENABLED=$(bashio::config 'admin_password_enabled')
export ADMIN_USERNAME
ADMIN_USERNAME=$(bashio::config 'admin_username')
export ADMIN_PASSWORD
ADMIN_PASSWORD=$(bashio::config 'admin_password')
export SMB_PORT
SMB_PORT=$(bashio::config 'smb_port')
bashio::log.info "SMB Port: ${SMB_PORT}"
export WEB_GUI_ENABLED
WEB_GUI_ENABLED=$(bashio::config 'web_gui_enabled')
bashio::log.info "Web GUI enabled: ${WEB_GUI_ENABLED}"
export HDD_IDLE_SECONDS
HDD_IDLE_SECONDS=$(bashio::config 'hdd_idle_seconds')
# Auto-restore settings from /config/.simplenas/auto (reinstall-safe backup)
python3 - << 'PYEOF'
import os, shutil, json
AUTO_DIR = "/config/.simplenas/auto"
DATA_DIR = "/data"
meta = os.path.join(AUTO_DIR, "meta.json")
shares = os.path.join(DATA_DIR, "shares.json")
# Restore only when auto-backup exists AND /data is empty/missing
if os.path.exists(meta) and (not os.path.exists(shares) or os.path.getsize(shares) <= 5):
print("[RESTORE] Fresh install detected – restoring settings from /config/.simplenas/auto ...")
os.makedirs(DATA_DIR, exist_ok=True)
for fname in ("shares.json", "users.json", "groups.json", "mounts.json", "backups.json", "admin_auth.json",
"file_access.json", "share_links.json", "share_accounts.json", "share_auth.json",
"crowdsec_setup.json"):
src = os.path.join(AUTO_DIR, fname)
if os.path.exists(src):
shutil.copy2(src, os.path.join(DATA_DIR, fname))
print(f"[RESTORE] {fname} restored")
samba_src = os.path.join(AUTO_DIR, "samba")
if os.path.isdir(samba_src):
samba_dst = os.path.join(DATA_DIR, "samba")
os.makedirs(samba_dst, exist_ok=True)
for f in os.listdir(samba_src):
shutil.copy2(os.path.join(samba_src, f), os.path.join(samba_dst, f))
print("[RESTORE] Done.")
PYEOF
# Initialize persistent data files (only if still missing after restore)
for f in shares users mounts groups backups share_links share_accounts; do
if [ ! -f "/data/${f}.json" ]; then
echo '[]' > "/data/${f}.json"
fi
done
chmod 600 /data/share_accounts.json 2>/dev/null || true
# Generate smb.conf
python3 /app/generate_smb_conf.py "$WORKGROUP" "$NAS_NAME" "$SMB_PORT"
# Restore system users and groups from persistent JSON files
# (Container is ephemeral - Linux users are lost on restart)
bashio::log.info "Restoring users and groups from persistent storage..."
python3 - << 'PYEOF'
import json, subprocess, os, shutil
# Restore groups
try:
groups = json.load(open("/data/groups.json"))
for g in groups:
name = g.get("name", "")
if name:
subprocess.run(["addgroup", name], capture_output=True)
for m in g.get("members", []):
# Create user first if needed (for group membership)
subprocess.run(["adduser", "-D", "-H", "-s", "/sbin/nologin", m], capture_output=True)
subprocess.run(["addgroup", m, name], capture_output=True)
print(f" Group restored: {name} ({len(g.get('members',[]))} members)")
except Exception as e:
print(f" Groups: {e}")
# Restore system users
try:
users = json.load(open("/data/users.json"))
for u in users:
username = u.get("username", "")
if username:
subprocess.run(["adduser", "-D", "-H", "-s", "/sbin/nologin", username], capture_output=True)
print(f" User restored: {username}")
except Exception as e:
print(f" Users: {e}")
# Restore Samba password database from persistent storage
samba_dir = "/var/lib/samba/private"
os.makedirs(samba_dir, exist_ok=True)
restored = 0
for fname in ["passdb.tdb", "secrets.tdb", "smbpasswd"]:
src = f"/data/samba/{fname}"
dst = f"{samba_dir}/{fname}"
if os.path.exists(src):
shutil.copy2(src, dst)
restored += 1
# Also check /etc/samba/smbpasswd
if os.path.exists("/data/samba/smbpasswd"):
shutil.copy2("/data/samba/smbpasswd", "/etc/samba/smbpasswd")
if restored > 0:
print(f" Samba password DB restored ({restored} files)")
else:
print(" No Samba password DB backup found (passwords need to be set via GUI)")
print(" Done.")
PYEOF
# Ensure Samba runtime directories
mkdir -p /var/log/samba /var/run/samba
ln -sf /config /homeassistant
# Start privileged mount helper daemon (communicates via FIFO)
bashio::log.info "Starting mount helper..."
/app/mount_helper.sh &
# Wait for FIFO to be ready
sleep 1
# Migrate mounts.json: replace raw /dev/sdX paths with stable /dev/disk/by-id/
python3 - << 'PYEOF'
import json, os, re
MOUNTS_FILE = "/data/mounts.json"
BY_ID_DIR = "/dev/disk/by-id"
def get_by_id_path(dev_path):
"""Resolve a /dev/sdX (or similar) path to a stable /dev/disk/by-id/ symlink."""
if not os.path.isdir(BY_ID_DIR):
return None
try:
real_dev = os.path.realpath(dev_path)
except Exception:
return None
candidates = []
try:
for link in os.listdir(BY_ID_DIR):
link_path = os.path.join(BY_ID_DIR, link)
try:
if os.path.realpath(link_path) == real_dev:
candidates.append(link)
except Exception:
pass
except Exception:
return None
if not candidates:
return None
for prefix in ("usb-", "ata-", "nvme-", "mmc-", "scsi-"):
for c in sorted(candidates):
if c.startswith(prefix):
return os.path.join(BY_ID_DIR, c)
return os.path.join(BY_ID_DIR, sorted(candidates)[0])
try:
mounts = json.load(open(MOUNTS_FILE))
except Exception:
mounts = []
changed = 0
# Clean up leftover bind-mount entries/fields from versions 3.0.38–3.0.47.
# The bind-mount feature was removed in 3.0.48 because it was ineffective
# (HA OS slave namespaces) AND dangerous (rm -rf /share/<name> would recurse
# through the bind and wipe the underlying drive). Make sure no leftover
# entry can ever re-create such a bind mount on startup.
BIND_FIELDS = ("share_bind", "share_bind_name", "bind_path", "bind", "bind_target")
cleaned_mounts = []
for m in mounts:
if m.get("type") == "bind" or m.get("kind") == "bind":
print(f"[MIGRATE] removed obsolete bind-mount entry for {m.get('mountpoint') or m.get('device')}")
changed += 1
continue
removed = [k for k in BIND_FIELDS if k in m]
for k in removed:
m.pop(k, None)
if removed:
print(f"[MIGRATE] removed obsolete bind-mount fields {removed} from {m.get('device')}")
changed += 1
cleaned_mounts.append(m)
mounts = cleaned_mounts
for m in mounts:
dev = m.get("device", "")
# Only migrate raw /dev/sd*, /dev/hd*, /dev/vd* paths — leave by-id and others alone
if re.match(r"^/dev/[shv]d[a-z]\d*$", dev):
by_id = get_by_id_path(dev)
if by_id:
print(f"[MIGRATE] {dev} → {by_id}")
m["dev_path"] = dev # keep original for reference
m["device"] = by_id
changed += 1
else:
print(f"[MIGRATE] {dev}: no by-id path found (device not connected?)")
if changed:
with open(MOUNTS_FILE, "w") as f:
json.dump(mounts, f, indent=2)
print(f"[MIGRATE] {changed} change(s) written to mounts.json.")
else:
print("[MIGRATE] No migration needed.")
PYEOF
# Restore saved mounts via mount helper
python3 /app/restore_mounts.py
# Regenerate smb.conf AFTER mounts are restored so that shares under /media/
# are correctly marked available = yes instead of available = no
bashio::log.info "Regenerating smb.conf after mount restore..."
python3 /app/generate_smb_conf.py "$WORKGROUP" "$NAS_NAME" "$SMB_PORT"
# ── HDD Spindown (hd-idle) ─────────────────────────────────────
# Spin down user-mounted drives after inactivity. Started AFTER restore_mounts
# so the devices exist. Global default '-i 0' means no disk is touched unless
# it is explicitly named below — this guarantees the HA system disk keeps
# spinning (it is never in mounts.json).
if [ "${HDD_IDLE_SECONDS:-0}" -gt 0 ] 2>/dev/null; then
if command -v hd-idle > /dev/null 2>&1; then
HD_IDLE_ARGS=$(python3 /app/hd_idle_args.py "${HDD_IDLE_SECONDS}")
if [ -n "${HD_IDLE_ARGS}" ]; then
hd-idle -i 0 ${HD_IDLE_ARGS} 2>&1 | python3 -u /app/hd_idle_log.py &
bashio::log.info "hd-idle started: spin down mounted drives after ${HDD_IDLE_SECONDS}s idle (${HD_IDLE_ARGS})"
else
bashio::log.info "hd-idle: no user-mounted drives found — not starting"
fi
else
bashio::log.warning "hd-idle not installed — spindown unavailable"
fi
else
bashio::log.info "HDD spindown disabled (hdd_idle_seconds: 0)"
fi
# Start Samba daemons
bashio::log.info "Starting Samba daemons (smbd + nmbd)..."
smbd --foreground --no-process-group &
nmbd --foreground --no-process-group &
sleep 2
# ── Network Discovery ──────────────────────────────────────────
bashio::log.info "Starting network discovery..."
# 1) DBus (required by avahi)
mkdir -p /var/run/dbus
if [ ! -S /var/run/dbus/system_bus_socket ]; then
dbus-daemon --system --nofork --nopidfile &
sleep 1
bashio::log.info "DBus started"
fi
# 2) Avahi – mDNS/DNS-SD (Linux Nautilus/Dolphin, macOS Finder)
if command -v avahi-daemon > /dev/null 2>&1; then
# Fix avahi config for container environment
sed -i 's/^#*enable-dbus=.*/enable-dbus=yes/' /etc/avahi/avahi-daemon.conf 2>/dev/null || true
sed -i 's/^#*use-ipv6=.*/use-ipv6=no/' /etc/avahi/avahi-daemon.conf 2>/dev/null || true
sed -i 's/^rlimit-nproc=.*/#rlimit-nproc=3/' /etc/avahi/avahi-daemon.conf 2>/dev/null || true
AVAHI_ERR=$(avahi-daemon --daemonize --no-chroot 2>&1)
if [ $? -eq 0 ]; then
bashio::log.info "Avahi daemon started"
sleep 1
# Explicitly publish SMB service with friendly name (container hostname is ugly)
avahi-publish -s "${NAS_NAME}" _smb._tcp "${SMB_PORT}" &
bashio::log.info "Avahi mDNS: published _smb._tcp on port ${SMB_PORT} as '${NAS_NAME}'"
else
bashio::log.warning "Avahi daemon error: ${AVAHI_ERR}"
# Try avahi-publish directly (works if HA OS already runs avahi)
avahi-publish -s "${NAS_NAME}" _smb._tcp "${SMB_PORT}" &
bashio::log.info "Avahi: service published via avahi-publish (using system Avahi)"
fi
else
bashio::log.warning "avahi-daemon not installed"
fi
# 3) WSDD – WS-Discovery (Windows 10/11 network browser)
# Bound to the LAN interface (default route) only. Home Assistant OS always has hassio, docker0 and
# veth interfaces besides the LAN one; bound to all of them, the multicast announcements never reached
# the LAN and the NAS did not show up in the Windows "Network" folder (issue #13, fix by @mirican).
if command -v wsdd > /dev/null 2>&1; then
WSDD_IFACE="$(ip route show default 2>/dev/null | awk '{for (i = 1; i < NF; i++) if ($i == "dev") {print $(i + 1); exit}}')"
if [ -n "${WSDD_IFACE}" ]; then
python3 /usr/local/bin/wsdd --workgroup "${WORKGROUP}" --hostname "${NAS_NAME}" --interface "${WSDD_IFACE}" &
bashio::log.info "WSDD started (WS-Discovery for Windows 10/11, interface=${WSDD_IFACE}, hostname=${NAS_NAME})"
else
python3 /usr/local/bin/wsdd --workgroup "${WORKGROUP}" --hostname "${NAS_NAME}" &
bashio::log.warning "WSDD started on all interfaces (no default route found)"
fi
else
bashio::log.warning "wsdd not installed"
fi
bashio::log.info "Network discovery active"
# 4) LLMNR responder – answers Windows 11 name queries for \\NAS_NAME
# without any client-side configuration (UDP multicast on port 5355)
python3 /app/llmnr_responder.py "${NAS_NAME}" &
bashio::log.info "LLMNR responder started (\\\\${NAS_NAME} name resolution for Windows)"
# ── Delayed smb.conf reload for slow-mounting USB drives ───────
# Some USB drives take 60-120s to mount. The smb.conf generated above
# may have marked those shares as inactive. This background job waits,
# then regenerates smb.conf and reloads Samba so late-mounting shares
# become available without a manual restart.
(
sleep 90
NEW_CONF=$(python3 /app/generate_smb_conf.py "$WORKGROUP" "$NAS_NAME" "$SMB_PORT" 2>&1)
if echo "$NEW_CONF" | grep -q "inactive"; then
# Still inactive — try again after another 90s
sleep 90
python3 /app/generate_smb_conf.py "$WORKGROUP" "$NAS_NAME" "$SMB_PORT"
fi
smbcontrol smbd reload-config 2>/dev/null || pkill -HUP smbd 2>/dev/null
echo "[delayed-reload] smb.conf regenerated and Samba reloaded"
) &
# ── Web GUI ────────────────────────────────────────────────────
# The public sharing site must never run without a working admin password.
# app.py checks this again on its own.
if bashio::config.true 'sharing_enabled'; then
if ! bashio::config.true 'admin_password_enabled' || bashio::config.is_empty 'admin_password'; then
bashio::log.fatal "sharing_enabled verlangt admin_password_enabled und ein gesetztes admin_password."
bashio::log.fatal "Die oeffentliche Freigabe-Seite wird NICHT gestartet."
elif bashio::config.is_empty 'share_public_url'; then
bashio::log.warning "share_public_url ist leer - Freigabe-Links zeigen auf die LAN-Adresse."
fi
fi
# Request bodies are buffered to a temp file before the app sees them. On
# Home Assistant OS /tmp is RAM, so point TMPDIR at the data partition.
mkdir -p /data/tmp
export TMPDIR=/data/tmp
rm -f /data/tmp/tmp* 2>/dev/null || true
if [ "${WEB_GUI_ENABLED}" = "true" ]; then
bashio::log.info "Starting Web GUI on port ${WEB_PORT}..."
exec python3 /app/app.py
else
bashio::log.info "Web GUI disabled (web_gui_enabled: false) – Samba-only mode."
exec tail -f /dev/null
fi