Ctrl K
ComunidadGuías…

simple_nas/run.sh

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/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