#!/bin/sh
# Runs after files are unpacked, on both first install and upgrade.
#
# The guiding rule: an upgrade must never touch the operator's config or
# database. A fresh ini would silently reset thresholds and drop the hub
# enrolment key, which on the hub side looks like a brand new host and
# consumes another seat.

# Overridable ONLY so the acceptance harness can run this exact file rather
# than a doctored copy; DSM never sets them and the defaults are what ships.
VAR_DIR="${SVRGUARD_VAR_DIR:-/var/packages/SvrGuard/var}"
PKG_DIR="${SVRGUARD_PKG_DIR:-/var/packages/SvrGuard/target}"
VOLUMES="${SVRGUARD_VOLUMES:-/volume1 /volume2 /volume3}"
CFG="${VAR_DIR}/svrguard.ini"

mkdir -p "$VAR_DIR"
chmod 700 "$VAR_DIR"

# Restore what a previous uninstall copied out.
#
# DSM deletes the whole package directory on removal AND on upgrade (both
# measured on DSM 6), so preuninst saves var/ to a volume. Without this,
# every reinstall — and every version bump — starts from an empty database
# and, worse, a new hub enrolment, which the hub reads as a brand new host
# and charges another seat for. This restore is what makes upgrades
# non-destructive; it is not a rare-path safety net.
#
# Only when there is nothing here already: an existing install's data must
# never be overwritten by an older backup.
#
# ⛔ -wal/-shm are NOT restored. preuninst writes one consistent .db
# (VACUUM INTO), so there is no log to replay; restoring a stale one next
# to a fresh database is what corrupted ds414 on 2026-09-10 -- SQLite
# replays a log belonging to a different image and every page it touches
# comes out doubly referenced. The single exception is a RAW_COPY backup:
# that one is three files taken together and only means anything as a set.
#
# A blacklist, not a whitelist: what must not travel is those two
# suffixes, and everything else in var/ (the rollback packages above all)
# has to survive an upgrade. A whitelist would silently drop each new kind
# of file somebody adds later.
if [ ! -f "$CFG" ]; then
    for VOL in $VOLUMES; do
        BACKUP="${VOL}/SvrGuard-backup"
        if [ -f "${BACKUP}/svrguard.ini" ]; then
            echo "SvrGuard: restoring configuration and database from $BACKUP"
            RESTORED=1
            for f in "$BACKUP"/*; do
                [ -e "$f" ] || continue
                case "${f##*/}" in
                    svrguard.db-wal|svrguard.db-shm)
                        [ -f "${BACKUP}/RAW_COPY" ] || continue ;;
                    RAW_COPY)
                        # A property of the backup, not of var/. Copying it
                        # in would make the next backup inherit a marker
                        # that is no longer true.
                        continue ;;
                esac
                cp -a "$f" "$VAR_DIR/" 2>/dev/null || RESTORED=""
            done
            [ -n "$RESTORED" ] && \
                echo "SvrGuard: restored; $BACKUP can be deleted once you have verified the console"
            break
        fi
    done
fi

# Check what was just restored, before anything starts using it.
#
# The fallback path in preuninst copies three files one at a time, which is
# only consistent if nothing was writing -- and "nothing was writing" is
# exactly what cannot be assumed on the path that needed a fallback. This
# is where that gets caught, while the damaged file can still be set aside
# with its name intact.
if [ -f "$CFG" ] && [ -f "${VAR_DIR}/svrguard.db" ] && [ -x "${PKG_DIR}/bin/svrguard" ]; then
    if ! "${PKG_DIR}/bin/svrguard" db-check -config "$CFG" >/dev/null 2>&1; then
        KEEP="${VAR_DIR}/svrguard.db.corrupt-$(date +%Y%m%d-%H%M%S)"
        mv "${VAR_DIR}/svrguard.db" "$KEEP" 2>/dev/null
        rm -f "${VAR_DIR}/svrguard.db-wal" "${VAR_DIR}/svrguard.db-shm"
        echo "SvrGuard: WARNING the restored database is damaged; kept as $KEEP"
        echo "SvrGuard: the service will rebuild what it can on start-up and say so in the console"
    fi
fi

if [ ! -f "$CFG" ]; then
    # Defaults chosen for this platform, not copied from the generic sample:
    #   - backend=none: DSM kernels ship neither nftables nor the ip_set
    #     modules, so no kernel blocking layer is available. Detection,
    #     notification and hub reporting all still work. Saying so here is
    #     better than letting the operator discover it from a failed sync.
    #   - auth_log_paths: this is where DSM logs SSH and FTP failures.
    #   - console on 8092 so DSM's "open" button reaches it.
    cat > "$CFG" <<'EOF'
[database]
path = /var/packages/SvrGuard/var/svrguard.db

[collector]
; DSM keeps web logs here; harmless when the web station is not installed
log_dir = /var/log/httpd
; DSM names them apache24-access_log, NOT access.log — an underscore, and
; no dot before "log". The project defaults (*access.log) match nothing
; here, so these are spelled out or the web logs are silently never read.
log_globs = *access_log,*access_log.1
error_log_globs = *error_log,*error_log.1
; nginx (DSM's own reverse proxy) — enable if you serve sites through it
; nginx_log_dir = /var/log/nginx
; SSH and FTP authentication failures
auth_log_paths = /var/log/auth.log

[scheduler]
interval_minutes = 5

[web]
listen_addr = 0.0.0.0:8092

[console]
lang = zh-Hant

[blocklist]
; auto probes the kernel in order — nftables, ipset, iptables — and uses
; the first that works. On DSM the first two are absent (no nf_tables, no
; ip_set modules) and it lands on iptables, which is verified working.
; This was pinned to "none" while the iptables backend did not exist yet.
;
; Set to "none" only if you want detection and notifications WITHOUT the
; firewall being touched — a monitoring-only deployment, or a host whose
; firewall another system owns.
backend = auto

[maintenance]
retention_days = 3

; ⛔ This section is what makes the package updatable at all. On DSM the
; console's update button and the daily schedule both read [update] url --
; unlike a Linux host, where the licence check reaches the supply channel and
; only the download needs this. Leave it out and the button is drawn, pressed,
; and refused, which is what every DSM install did before 2026-09-09.
;
; Clear the value to switch update checks off.
[update]
url = https://svrguard.ofuyuan.com/update/agent

; Central management (optional). The usual way to join a hub is from this
; package's own page: open it in Package Center (port 8092) and sign in with
; your SvrGuard Hub account -- nothing has to be edited here.
;
; The keys below are for the headless case only: a NAS whose console you
; cannot open in a browser. enroll_token is the per-host code minted by
; pre-registering this host on the Users page of the admin console, which
; only the vendor's operators can open -- ask your supplier for it. It is
; single-use and bound to this one host.
; [fleet]
; host_url = https://svrguard.ofuyuan.com
; agent_uid = my-nas
; enroll_token = <per-host code, ask your supplier>
; heartbeat_minutes = 1
EOF
    chmod 600 "$CFG"
else
    # Upgrade path. The console default moved from 8080 to 8092 (8080 is the
    # most contested port on any machine), and INFO's adminport moved with it
    # — that is what DSM's "Open" button and the URL on the package page use.
    # An existing install keeps its ini, so without this the button would
    # point at a port nobody is listening on.
    #
    # Only the value this script itself wrote is migrated. If the operator
    # chose their own address, it is left exactly as they set it: a package
    # upgrade rewriting a deliberate configuration change is worse than a
    # button pointing at the wrong port.
    if grep -q '^[[:space:]]*listen_addr[[:space:]]*=[[:space:]]*0\.0\.0\.0:8080[[:space:]]*$' "$CFG"; then
        sed -i 's/^\([[:space:]]*listen_addr[[:space:]]*=[[:space:]]*\)0\.0\.0\.0:8080[[:space:]]*$/\10.0.0.0:8092/' "$CFG"
        echo "SvrGuard: console port migrated 8080 -> 8092 to match this package's Open button" >&2
    fi

    # Every ini this script wrote before 2026-09-09 has no [update] section at
    # all, so those hosts could never update themselves: the button was drawn
    # and then refused with "no update source configured".
    #
    # ⭐ The section MISSING and the url EMPTY are different facts, and only
    # the first one is safe to fix here. A missing section means nobody ever
    # made a choice -- this script simply never wrote one. An empty url is a
    # documented way to switch updates off, so it is left exactly as it is.
    # That is why this tests for the section header, not for the value.
    if ! grep -q '^[[:space:]]*\[update\][[:space:]]*$' "$CFG"; then
        # Appended, not inserted: ini sections do not care about order, and
        # a section written at the end cannot land inside another one.
        cat >> "$CFG" <<'EOF'

; Added by the package upgrade: installs made before 2026-09-09 had no update
; source, so the console's update button always refused. Clear the value to
; switch update checks off.
[update]
url = https://svrguard.ofuyuan.com/update/agent
EOF
        echo "SvrGuard: added the missing [update] source; this host can now update itself" >&2
    fi

    # Web log location and file names. Installs whose ini was first written
    # before r5088 (2026-07-27 15:22) have no log_dir at all; those written
    # between r5088 and r5127 (2026-07-28 07:02) have the directory but not
    # the globs. Both fall back to the program defaults -- /var/log/apache2
    # and *access.log -- and on DSM neither matches: the directory does not
    # exist, and DSM spells them apache24-access_log (an underscore, and no
    # dot before "log"). The collector then logs "no apache log files found"
    # once a pass and reads nothing from the web server, for good.
    #
    # ⭐ Appended as a whole [collector] section, exactly like [update]
    # above, and for the same reason: the parser merges repeated sections
    # and does not care about order, so a section written at the end cannot
    # land inside another one. Inserting a bare key in the middle of the
    # file would put it in whatever section happens to be last -- which
    # fails silently, since an ini key under the wrong section simply is
    # not read.
    #
    # ⭐ Each key is judged on its own (that sixteen-hour window above is
    # why), and the test scans the WHOLE file: a later duplicate would
    # override the operator's value, and overriding is the one thing this
    # branch must never do.
    #
    # ⚠️ This repairs the ini at the next install. It cannot help a host
    # that is sitting there reading nothing right now -- somebody has to
    # install the package once, exactly as with [update].
    #
    # ⚠️ These names appear twice more: in the fresh-install ini above, and
    # in globsFor() in src/go/cmd/svrguard/init.go, which is the same
    # mapping for non-DSM hosts. Shell cannot call Go, so the copies are
    # deliberate -- change one, change the others.
    COLLECTOR_ADD=""
    COLLECTOR_EMPTY=""
    for kv in "log_dir=/var/log/httpd" \
              "log_globs=*access_log,*access_log.1" \
              "error_log_globs=*error_log,*error_log.1"; do
        k="${kv%%=*}"
        v="${kv#*=}"
        if grep -q "^[[:space:]]*${k}[[:space:]]*=" "$CFG"; then
            # Present. An empty value is left exactly as it is: we cannot
            # tell a deliberate blanking from an accident, and rewriting a
            # value the operator set is what the port migration above
            # already refuses to do. Say so, because an empty glob means
            # nothing is scanned and the reason belongs in front of a
            # person, not only in the service log.
            if grep -q "^[[:space:]]*${k}[[:space:]]*=[[:space:]]*$" "$CFG"; then
                COLLECTOR_EMPTY="${COLLECTOR_EMPTY} ${k}"
            fi
            continue
        fi
        COLLECTOR_ADD="${COLLECTOR_ADD}
${k} = ${v}"
    done
    if [ -n "$COLLECTOR_ADD" ]; then
        {
            echo ""
            echo "; Added by the package upgrade: this ini predates the DSM web-log"
            echo "; defaults, so the web server's logs were never read. DSM names them"
            echo "; apache24-access_log -- an underscore, and no dot before \"log\"."
            echo "[collector]${COLLECTOR_ADD}"
        } >> "$CFG"
        echo "SvrGuard: added missing [collector] web-log settings:${COLLECTOR_ADD}" >&2
    fi
    if [ -n "$COLLECTOR_EMPTY" ]; then
        echo "SvrGuard: NOTE these [collector] keys are set but empty, so nothing matching them is read:${COLLECTOR_EMPTY}" >&2
        echo "SvrGuard: left as they are -- set them in $CFG if that was not deliberate" >&2
    fi
fi

chmod 755 "${PKG_DIR}/bin/svrguard" 2>/dev/null

# ---- capabilities ----
#
# Declared in config/synology/privilege, not granted here. DSM 7 applies the
# `capabilities` field of a package tool at install time, and that is the
# mechanism the platform supports: AntiVirus asks for cap_dac_read_search the
# same way for bin/synoavscan.
#
# ⛔ setcap cannot work from this script, on any DSM 7 host. /bin/setcap and
# /usr/bin/setcap are both `-rwx------ root root` (measured on ds218j and on
# MR12) and control scripts do not run with enough privilege to execute them,
# so every candidate path fails and the block that used to be here could only
# ever end in a warning. ⭐ Once conf/privilege grants the capability that
# warning is not merely useless, it is wrong -- and a warning that is always
# on is how the next real one gets ignored.
#
# DSM 6 needs none of it: there the package runs as root.
#
# ⛔ And conf/privilege cannot grant a CAPABILITY either: DSM answers 319
# "invalid package privilege content" to any capabilities key from this
# package, whatever the permission bits (measured on two DSM 7 machines on
# 2026-09-08 as probe P2, and again on a shipped package on 2026-09-12).
# AntiVirus declares one and installs -- it is signed by Synology, we are not.
#
# ⭐ What DOES work is group membership: conf/privilege declares
# "join-groupname": "log", /var/log/auth.log is `-rw-r----- root log`, and the
# service really starts in that group (probe P4, both machines).
#
# ⚠️ Neither this script nor the package can confirm it from here. The
# judgement is on the installed host: `id` for the group, and the console's
# own warning about unreadable files going away.

touch "${VAR_DIR}/svrguard.log"
chmod 600 "${VAR_DIR}/svrguard.log"

# Clear the emergency marker: a package install is exactly the repair that
# marker asks a person to perform, so leaving it behind would keep a banner
# up about a state that no longer exists.
#
# ⚠️ Here rather than in the helper that wrote it. The helper cannot know
# whether anybody acted on it -- it is gone by then -- and a warning that
# clears itself on a timer would be telling the reader the host recovered
# when nothing about the host changed.
rm -f "${VAR_DIR}/update/EMERGENCY"

# And the log of the last self-upgrade attempt, for exactly the same reason.
#
# ⛔ The console's "automatic updates are paused" banner is not a stored flag:
# InspectSelfInstall re-reads that log every render and reports the failure it
# finds there. On DSM 7 the only thing that can replace it is a successful
# agent-initiated install -- which is the one thing that cannot happen,
# because synopkg refuses a non-root caller (263). So a person installs the
# package by hand, fixes the problem, and the banner still tells them the last
# install failed. Measured on ds218j 2026-09-12.
#
# ⭐ Moved aside, not deleted: the failure is evidence, and the same rule the
# database follows when postinst finds it damaged. What changes is that the
# NEXT inspection starts from what just happened rather than from a failure
# this install has already answered.
#
# ⚠️ Both copies, because upgradeLogPath() takes whichever is newer.
for stale in "${VAR_DIR}/update/last_upgrade.log" /tmp/svrguard-upgrade.log; do
    [ -f "$stale" ] || continue
    mv "$stale" "$stale.before-$(date +%Y%m%d-%H%M%S)" 2>/dev/null \
        && echo "SvrGuard: set aside $stale (a successful install answers it)" >&2
done

# No account is created here on purpose. The console detects an empty
# users table and serves a first-run setup form instead of a login page,
# so the administrator is created in the browser after clicking "Open" in
# Package Center. Generating a password here would mean telling a NAS owner
# to read it out of a file with File Station or ssh, which for most people
# is the same as "it does not work".
#
# Setup is only accepted for a short window after the service starts, so an
# unconfigured console cannot be claimed by a passer-by; missing the window
# just means restarting the package.

exit 0
