The set_env/env_set helper escaped its value with
`esc=${val//\/\\}; esc=${esc//|/\|}; esc=${esc//&/\&}` and then
interpolated it into `sed -i -e "s|^KEY=.*|KEY=${esc}|"`. The escaping does
not do what it looks like. Tested on bash 5.2:
set_env K 'a&b' -> K=aK=seedb (sed expanded & to the whole match)
set_env K 'a|b' -> sed: unknown option to `s' (rc!=0, aborts under set -e)
So any value containing & is silently corrupted and any value containing the
s||| delimiter kills the run. That is reachable: headscale writes
OIDC_CLIENT_SECRET through this, pocket-id writes REDIRECT_URL, copyparty
writes DATA_DIR. A generated secret or a URL query string hits both cases.
The copies in headscale and pocket-id were additionally mangled when they
were introduced -- `${val//\/\}` (pattern `\/`, a literal SLASH) and a raw
newline inside `printf '%s=%s\n'`. The mangled form is a no-op rather than a
corrupter, so the practical failure mode was the same as the original.
Replace all of them with an awk rewrite that passes the key and value through
the ENVIRONMENT, so the value is never parsed as part of a script and needs no
escaping at all. ENVIRON and index() are POSIX, so busybox awk handles them.
Output goes to a temp file and is copied back with `cat >`, which preserves the
original mode and owner -- a .env holding secrets stays 0600. If awk fails,
set -e aborts before .env is touched, which `sed -i` could not promise.
Verified against plain, a&b, a|b, a\b, p@ss&w|rd\x, R&D, a URL with a query
string, s/foo/bar/, a trailing space and the empty string; plus the
append-when-key-absent path, the file-does-not-exist path, non-target lines
left intact, no line-count drift, and mode preservation.
copyparty/update.sh and the rebuilt copyparty payload are included because
update.sh is embedded; regenerated with build.sh.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
452 lines
18 KiB
Bash
452 lines
18 KiB
Bash
#!/usr/bin/env bash
|
|
#
|
|
# update.sh -- update the copyparty container, optionally driven by copyparty's
|
|
# security-notices API. Companion to deploy.sh; installed alongside the stack.
|
|
#
|
|
# copyparty does NOT self-update, but it DOES publish a machine-readable feed of
|
|
# security advisories (the same one its built-in `--vc-url` check uses). This
|
|
# script reads that feed to decide when to act, pins the new version in .env,
|
|
# and recreates the container -- so the running version is always explicit and
|
|
# a bad release can be rolled back by editing .env.
|
|
#
|
|
# Subcommands:
|
|
# check (default) report current vs latest + any matching advisory; no changes
|
|
# run apply per UPDATE_POLICY, then notify (this is what the schedule runs)
|
|
# update force an update now (to latest, or TARGET_VERSION=x.y.z)
|
|
# install schedule the daily `run`
|
|
# uninstall remove the schedule
|
|
#
|
|
# Policy (UPDATE_POLICY):
|
|
# latest update to the newest release whenever one exists (default)
|
|
# security update ONLY when the running version has a known advisory, to the
|
|
# patched release named in the feed
|
|
# off never change the running version (check/notify only)
|
|
#
|
|
# Env (also read from /etc/copyparty-update.conf; environment wins):
|
|
# STACK_DIR=/srv/copyparty UPDATE_POLICY=latest VC_FEED=advisories
|
|
# NOTIFY=1 SSH_NOTIFY_CONF=/etc/ssh-notify.conf DRY_RUN=0
|
|
# TARGET_VERSION= GH_REPO=9001/copyparty
|
|
|
|
set -euo pipefail
|
|
|
|
SELF="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")"
|
|
|
|
# Load defaults from the conf for any var not already set in the environment
|
|
# (precedence: environment > conf > built-in).
|
|
: "${COPYPARTY_UPDATE_CONF:=/etc/copyparty-update.conf}"
|
|
if [[ -r "$COPYPARTY_UPDATE_CONF" ]]; then
|
|
while IFS= read -r _line; do
|
|
[[ "$_line" =~ ^[[:space:]]*# || -z "${_line//[[:space:]]/}" ]] && continue
|
|
_k="${_line%%=*}"; _v="${_line#*=}"; _k="${_k//[[:space:]]/}"
|
|
[[ "$_k" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || continue
|
|
[[ -n "${!_k:-}" ]] && continue
|
|
_v="${_v%\"}"; _v="${_v#\"}"
|
|
printf -v "$_k" '%s' "$_v"
|
|
done < "$COPYPARTY_UPDATE_CONF"
|
|
fi
|
|
|
|
: "${STACK_DIR:=/srv/copyparty}"
|
|
: "${UPDATE_POLICY:=latest}"
|
|
: "${VC_FEED:=advisories}"
|
|
: "${NOTIFY:=1}"
|
|
: "${SSH_NOTIFY_CONF:=/etc/ssh-notify.conf}"
|
|
: "${DRY_RUN:=0}"
|
|
: "${TARGET_VERSION:=}"
|
|
: "${GH_REPO:=9001/copyparty}"
|
|
: "${LOG:=/var/log/copyparty-update.log}"
|
|
|
|
log() { printf '\033[1;32m[+]\033[0m %s\n' "$*"; }
|
|
warn() { printf '\033[1;33m[!]\033[0m %s\n' "$*" >&2; }
|
|
die() { printf '\033[1;31m[x]\033[0m %s\n' "$*" >&2; exit 1; }
|
|
run() { if [[ "$DRY_RUN" == "1" ]]; then echo "DRY: $*"; else eval "$@"; fi; }
|
|
|
|
# docker compose, scoped to the stack dir (so ./Caddyfile, ./cfg resolve and
|
|
# .env is auto-loaded).
|
|
dc() { ( cd "$STACK_DIR" && docker compose "$@" ); }
|
|
|
|
osfam() {
|
|
local id="" like=""
|
|
if [[ -r /etc/os-release ]]; then
|
|
id="$(. /etc/os-release 2>/dev/null && echo "${ID:-}")"
|
|
like="$(. /etc/os-release 2>/dev/null && echo "${ID_LIKE:-}")"
|
|
fi
|
|
case " $id $like " in
|
|
*" alpine "*) echo alpine ;;
|
|
*" debian "*|*" ubuntu "*) echo debian ;;
|
|
*" rhel "*|*" fedora "*|*" centos "*) echo rhel ;;
|
|
*) echo "${id:-unknown}" ;;
|
|
esac
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# .env helpers
|
|
# ---------------------------------------------------------------------------
|
|
ENV_FILE="$STACK_DIR/.env"
|
|
env_get() { [[ -f "$ENV_FILE" ]] && grep -E "^$1=" "$ENV_FILE" | head -n1 | cut -d= -f2- || true; }
|
|
env_set() { # <KEY> <value>: update KEY in .env, or append if absent
|
|
# The value goes through the ENVIRONMENT, never interpolated into a sed
|
|
# script. Interpolating it corrupts any value containing & (sed expands it to
|
|
# the whole match) and aborts the run on one containing the s||| delimiter --
|
|
# which is reachable for an OIDC secret, a password or a URL query string.
|
|
local key="$1" val="$2" tmp
|
|
if [[ ! -f "$ENV_FILE" ]]; then
|
|
printf '%s=%s
|
|
' "$key" "$val" >> "$ENV_FILE"
|
|
return 0
|
|
fi
|
|
tmp="$(mktemp)"
|
|
_SE_KEY="$key" _SE_VAL="$val" awk '
|
|
BEGIN { k = ENVIRON["_SE_KEY"]; v = ENVIRON["_SE_VAL"]; seen = 0 }
|
|
!seen && index($0, k "=") == 1 { print k "=" v; seen = 1; next }
|
|
{ print }
|
|
END { if (!seen) print k "=" v }
|
|
' "$ENV_FILE" > "$tmp"
|
|
cat "$tmp" > "$ENV_FILE" # rewrite in place: keeps the original mode/owner
|
|
rm -f "$tmp"
|
|
}
|
|
|
|
# strip a leading v and anything after the X.Y.Z core
|
|
normver() { printf '%s' "$1" | grep -oE '[0-9]+\.[0-9]+(\.[0-9]+)?' | head -n1; }
|
|
# $1 <= $2 ?
|
|
ver_le() { [[ "$1" == "$2" ]] || [[ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | head -n1)" == "$1" ]]; }
|
|
ver_gt() { ! ver_le "$1" "$2"; }
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Version discovery
|
|
# ---------------------------------------------------------------------------
|
|
# Currently-running version: prefer the live container, fall back to the pinned
|
|
# tag in .env.
|
|
current_version() {
|
|
local v=""
|
|
v="$(dc exec -T copyparty python3 -m copyparty --versionb 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n1 || true)"
|
|
if [[ -z "$v" ]]; then v="$(normver "$(env_get COPYPARTY_TAG)")"; fi
|
|
printf '%s' "$v"
|
|
}
|
|
|
|
# Newest released version from the GitHub releases API (tag_name, e.g. v1.20.11).
|
|
latest_version() {
|
|
command -v curl >/dev/null 2>&1 || { warn "curl missing; cannot resolve latest release."; return 0; }
|
|
curl -fsSL "https://api.github.com/repos/${GH_REPO}/releases/latest" 2>/dev/null \
|
|
| grep -oE '"tag_name"[[:space:]]*:[[:space:]]*"[^"]+"' | head -n1 \
|
|
| sed -E 's/.*"tag_name"[[:space:]]*:[[:space:]]*"v?([^"]+)".*/\1/'
|
|
}
|
|
|
|
# Advisory scan: ask copyparty's own python (inside the container -- no host jq/
|
|
# python dependency) whether $1 is covered by any advisory in $VC_FEED. Prints:
|
|
# OK
|
|
# ERR <msg>
|
|
# VULN <max-patched-version>\n<comma-separated advisory ids>
|
|
advisory_scan() {
|
|
local ver="$1" feed="https://api.copyparty.eu/${VC_FEED}"
|
|
dc exec -T -e CPV="$ver" -e FEED="$feed" copyparty python3 - <<'PY' 2>/dev/null || echo "ERR exec"
|
|
import json, os, re, sys, urllib.request
|
|
cpv = os.environ.get("CPV", "")
|
|
feed = os.environ.get("FEED", "")
|
|
def vt(s):
|
|
out = []
|
|
for p in re.split(r'[.\-]', s.strip().lstrip("vV")):
|
|
m = re.match(r'\d+', p)
|
|
out.append(int(m.group(0)) if m else 0)
|
|
return tuple(out)
|
|
def cmp(a, b):
|
|
ta, tb = vt(a), vt(b)
|
|
n = max(len(ta), len(tb))
|
|
ta += (0,) * (n - len(ta)); tb += (0,) * (n - len(tb))
|
|
return (ta > tb) - (ta < tb)
|
|
def satisfies(ver, rng):
|
|
for part in rng.split(","):
|
|
part = part.strip()
|
|
if not part:
|
|
continue
|
|
m = re.match(r'(<=|>=|==|=|<|>)?\s*v?([0-9][0-9A-Za-z.\-]*)', part)
|
|
if not m:
|
|
return False
|
|
op = m.group(1) or "="
|
|
c = cmp(ver, m.group(2))
|
|
ok = ((op in ("=", "==") and c == 0) or (op == "<" and c < 0) or
|
|
(op == "<=" and c <= 0) or (op == ">" and c > 0) or (op == ">=" and c >= 0))
|
|
if not ok:
|
|
return False
|
|
return True
|
|
if not cpv:
|
|
print("ERR no-version"); sys.exit(0)
|
|
try:
|
|
raw = urllib.request.urlopen(feed, timeout=10).read()
|
|
data = json.loads(raw)
|
|
except Exception as e:
|
|
print("ERR " + str(e)[:80]); sys.exit(0)
|
|
if isinstance(data, dict):
|
|
data = data.get("advisories") or data.get("data") or [data]
|
|
hits, patched = [], []
|
|
for adv in data:
|
|
for v in (adv.get("vulnerabilities") or []):
|
|
name = ((v.get("package") or {}).get("name") or "").lower()
|
|
if name and name != "copyparty":
|
|
continue
|
|
rng = v.get("vulnerable_version_range") or ""
|
|
if rng and satisfies(cpv, rng):
|
|
hits.append(adv.get("ghsa_id") or adv.get("cve_id") or "advisory")
|
|
pv = v.get("patched_versions") or v.get("first_patched_version") or ""
|
|
if isinstance(pv, dict):
|
|
pv = pv.get("identifier", "")
|
|
pv = re.sub(r'^[^0-9]*', '', str(pv))
|
|
if pv:
|
|
patched.append(pv)
|
|
if not hits:
|
|
print("OK")
|
|
else:
|
|
best = ""
|
|
for p in patched:
|
|
if not best or cmp(p, best) > 0:
|
|
best = p
|
|
print("VULN " + best)
|
|
print(",".join(sorted(set(hits))))
|
|
PY
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Notify (reuse the login-notifier's ntfy config, like scripts/auto-update.sh)
|
|
# ---------------------------------------------------------------------------
|
|
send_notice() { # <title> <priority> <body>
|
|
[[ "$NOTIFY" == "1" ]] || return 0
|
|
[[ -r "$SSH_NOTIFY_CONF" ]] || return 0
|
|
# shellcheck disable=SC1090
|
|
. "$SSH_NOTIFY_CONF"
|
|
[[ -n "${NTFY_URL:-}" ]] || return 0
|
|
command -v curl >/dev/null 2>&1 || return 0
|
|
local title="$1" prio="$2" body="$3" host
|
|
host="$(hostname -f 2>/dev/null || hostname 2>/dev/null || echo unknown)"
|
|
set -- -fsS -m 5 -H "X-Title: ${title}" -H "X-Priority: ${prio}"
|
|
[[ -n "${NTFY_TOKEN:-}" ]] && set -- "$@" -H "Authorization: Bearer ${NTFY_TOKEN}"
|
|
[[ -n "${NTFY_EMAIL:-}" ]] && set -- "$@" -H "X-Email: ${NTFY_EMAIL}"
|
|
local t="copyparty"; [[ -n "${NTFY_REGION:-}" ]] && t="${t},${NTFY_REGION}"
|
|
set -- "$@" -H "X-Tags: ${t}"
|
|
if [[ "$DRY_RUN" == "1" ]]; then
|
|
echo "DRY: curl ntfy ($prio): $body"
|
|
else
|
|
curl "$@" -d "${body} [${host}]" "$NTFY_URL" >/dev/null 2>&1 || true
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Health wait (same shape as deploy.sh)
|
|
# ---------------------------------------------------------------------------
|
|
wait_health() {
|
|
local deadline; deadline=$(( $(date +%s) + 120 ))
|
|
while (( $(date +%s) < deadline )); do
|
|
local status unhealthy
|
|
status="$(dc ps --format '{{.Service}} {{.Health}}' 2>/dev/null || true)"
|
|
unhealthy="$(echo "$status" | awk '$2 != "healthy" && $2 != "" {print $1}')"
|
|
[[ -z "$unhealthy" && -n "$status" ]] && return 0
|
|
sleep 5
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Apply an update: pin the tag, pull, recreate, verify; roll back on failure.
|
|
# ---------------------------------------------------------------------------
|
|
apply_update() { # <from> <to> [advisory-ids]
|
|
local from="$1" to="$2" advs="${3:-}"
|
|
log "Updating copyparty ${from:-?} -> ${to}..."
|
|
if [[ "$DRY_RUN" == "1" ]]; then
|
|
echo "DRY: set COPYPARTY_TAG=${to}; docker compose pull copyparty; docker compose up -d"
|
|
return 0
|
|
fi
|
|
cp -a "$ENV_FILE" "${ENV_FILE}.bak.$(date -u +%Y%m%d%H%M%S)" 2>/dev/null || true
|
|
env_set COPYPARTY_TAG "$to"
|
|
if ! dc pull copyparty; then
|
|
warn "pull failed; restoring COPYPARTY_TAG=${from}."
|
|
env_set COPYPARTY_TAG "$from"
|
|
send_notice "copyparty update FAILED" "high" "pull of ${to} failed; staying on ${from}"
|
|
return 1
|
|
fi
|
|
dc up -d --remove-orphans
|
|
if wait_health; then
|
|
log "copyparty ${to} is healthy."
|
|
local b="updated ${from:-?} -> ${to}"; [[ -n "$advs" ]] && b="${b} (advisory: ${advs})"
|
|
send_notice "copyparty updated" "default" "$b"
|
|
return 0
|
|
fi
|
|
warn "copyparty ${to} did not become healthy; rolling back to ${from}."
|
|
env_set COPYPARTY_TAG "$from"
|
|
dc up -d --remove-orphans || true
|
|
send_notice "copyparty update FAILED" "high" "${to} unhealthy; rolled back to ${from}"
|
|
return 1
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Decide the target version for the current policy. Sets globals TARGET,
|
|
# TARGET_IDS and SCAN_NOTE (NOT via stdout -- a $() subshell would lose them).
|
|
# ---------------------------------------------------------------------------
|
|
TARGET=""; TARGET_IDS=""; SCAN_NOTE=""
|
|
resolve_target() { # <current-version>
|
|
local cur="$1"
|
|
TARGET=""; TARGET_IDS=""; SCAN_NOTE=""
|
|
if [[ -n "$TARGET_VERSION" ]]; then
|
|
TARGET="$(normver "$TARGET_VERSION")"; SCAN_NOTE="target override ${TARGET}"; return 0
|
|
fi
|
|
case "$UPDATE_POLICY" in
|
|
off)
|
|
SCAN_NOTE="policy=off (no changes)"; return 0 ;;
|
|
security)
|
|
local scan first rest
|
|
scan="$(advisory_scan "$cur")"
|
|
first="$(printf '%s' "$scan" | head -n1)"
|
|
rest="$(printf '%s' "$scan" | sed -n '2p')"
|
|
case "$first" in
|
|
VULN*)
|
|
TARGET="$(printf '%s' "$first" | awk '{print $2}')"
|
|
TARGET_IDS="$rest"
|
|
SCAN_NOTE="VULNERABLE (${rest:-?}); patched in ${TARGET:-?}"
|
|
# if the feed names no patched version, fall forward to latest
|
|
[[ -z "$TARGET" ]] && TARGET="$(normver "$(latest_version)")" ;;
|
|
OK) SCAN_NOTE="no advisory matches ${cur}" ;;
|
|
*) SCAN_NOTE="advisory check unavailable (${first})" ;;
|
|
esac ;;
|
|
latest|*)
|
|
TARGET="$(normver "$(latest_version)")"
|
|
SCAN_NOTE="policy=latest" ;;
|
|
esac
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Subcommands
|
|
# ---------------------------------------------------------------------------
|
|
preflight() {
|
|
[[ $EUID -eq 0 ]] || die "Run as root."
|
|
[[ -f "$STACK_DIR/docker-compose.yml" ]] || die "No stack at $STACK_DIR (set STACK_DIR)."
|
|
command -v docker >/dev/null 2>&1 || die "docker not found."
|
|
}
|
|
|
|
do_check() {
|
|
preflight
|
|
local cur latest scan
|
|
cur="$(current_version)"
|
|
latest="$(normver "$(latest_version)")"
|
|
log "Running version: ${cur:-unknown} | latest release: ${latest:-unknown} | policy: ${UPDATE_POLICY}"
|
|
scan="$(advisory_scan "$cur")"
|
|
local first ids; first="$(printf '%s' "$scan" | head -n1)"; ids="$(printf '%s' "$scan" | sed -n '2p')"
|
|
local prio="min" note=""
|
|
case "$first" in
|
|
VULN*) note="VULNERABLE: ${ids} (patched ${first#VULN })"; prio="high"; warn "$note" ;;
|
|
OK) note="no known advisory affects ${cur:-?}"; log "$note" ;;
|
|
*) note="advisory check unavailable (${first})"; warn "$note" ;;
|
|
esac
|
|
if [[ -n "$cur" && -n "$latest" ]] && ver_gt "$latest" "$cur"; then
|
|
log "A newer release is available: ${cur} -> ${latest}"
|
|
[[ "$prio" == "min" ]] && prio="default"
|
|
note="${note}; newer release ${latest} available"
|
|
fi
|
|
send_notice "copyparty check" "$prio" "${note}"
|
|
}
|
|
|
|
do_run() {
|
|
preflight
|
|
[[ "$DRY_RUN" == "1" ]] || { install -d -m 0755 "$(dirname "$LOG")" 2>/dev/null || true; echo "=== copyparty-update $(date -u +%FT%TZ) ===" >> "$LOG"; }
|
|
local cur
|
|
cur="$(current_version)"
|
|
resolve_target "$cur"
|
|
log "current=${cur:-?} | ${SCAN_NOTE}"
|
|
if [[ -z "$TARGET" ]]; then
|
|
log "No update to apply."
|
|
return 0
|
|
fi
|
|
if [[ -n "$cur" ]] && ! ver_gt "$TARGET" "$cur"; then
|
|
log "Target ${TARGET} is not newer than ${cur}; nothing to do."
|
|
return 0
|
|
fi
|
|
apply_update "$cur" "$TARGET" "$TARGET_IDS"
|
|
}
|
|
|
|
do_update() {
|
|
# force: default to latest unless TARGET_VERSION/policy says otherwise
|
|
preflight
|
|
local cur
|
|
cur="$(current_version)"
|
|
if [[ -z "$TARGET_VERSION" && "$UPDATE_POLICY" == "off" ]]; then UPDATE_POLICY=latest; fi
|
|
resolve_target "$cur"
|
|
[[ -n "$TARGET" ]] || die "Could not determine a target version (${SCAN_NOTE})."
|
|
if [[ -n "$cur" ]] && ! ver_gt "$TARGET" "$cur" && [[ -z "$TARGET_VERSION" ]]; then
|
|
log "Already on ${cur} (latest ${TARGET}); nothing to do."
|
|
return 0
|
|
fi
|
|
apply_update "$cur" "$TARGET" "$TARGET_IDS"
|
|
}
|
|
|
|
# Write /etc/copyparty-update.conf so the scheduled run inherits these.
|
|
write_conf() {
|
|
cat > "$COPYPARTY_UPDATE_CONF" <<CONF
|
|
# Defaults for the scheduled copyparty updater (deployments/copyparty/update.sh).
|
|
# Environment variables still override these at runtime.
|
|
STACK_DIR="${STACK_DIR}"
|
|
UPDATE_POLICY="${UPDATE_POLICY}"
|
|
VC_FEED="${VC_FEED}"
|
|
NOTIFY="${NOTIFY}"
|
|
CONF
|
|
chmod 644 "$COPYPARTY_UPDATE_CONF"
|
|
log "Wrote $COPYPARTY_UPDATE_CONF"
|
|
}
|
|
|
|
do_install() {
|
|
[[ $EUID -eq 0 ]] || die "Run as root."
|
|
write_conf
|
|
case "$(osfam)" in
|
|
alpine)
|
|
install -d -m 0755 /etc/periodic/daily
|
|
cat > /etc/periodic/daily/copyparty-update <<EOF
|
|
#!/bin/sh
|
|
exec bash "$SELF" run
|
|
EOF
|
|
chmod +x /etc/periodic/daily/copyparty-update
|
|
command -v rc-update >/dev/null 2>&1 && { rc-update add crond default >/dev/null 2>&1 || true; rc-service crond start >/dev/null 2>&1 || true; }
|
|
log "Scheduled daily via /etc/periodic/daily/copyparty-update (policy=${UPDATE_POLICY})." ;;
|
|
*)
|
|
cat > /etc/systemd/system/copyparty-update.service <<EOF
|
|
[Unit]
|
|
Description=copyparty container updater
|
|
After=docker.service
|
|
|
|
[Service]
|
|
Type=oneshot
|
|
ExecStart=/usr/bin/env bash $SELF run
|
|
EOF
|
|
cat > /etc/systemd/system/copyparty-update.timer <<EOF
|
|
[Unit]
|
|
Description=Daily copyparty update check
|
|
|
|
[Timer]
|
|
OnCalendar=daily
|
|
Persistent=true
|
|
RandomizedDelaySec=1h
|
|
|
|
[Install]
|
|
WantedBy=timers.target
|
|
EOF
|
|
systemctl daemon-reload
|
|
systemctl enable --now copyparty-update.timer >/dev/null 2>&1 || true
|
|
log "Scheduled daily via systemd timer copyparty-update.timer (policy=${UPDATE_POLICY})." ;;
|
|
esac
|
|
}
|
|
|
|
do_uninstall() {
|
|
[[ $EUID -eq 0 ]] || die "Run as root."
|
|
rm -f /etc/periodic/daily/copyparty-update
|
|
if command -v systemctl >/dev/null 2>&1; then
|
|
systemctl disable --now copyparty-update.timer >/dev/null 2>&1 || true
|
|
rm -f /etc/systemd/system/copyparty-update.timer /etc/systemd/system/copyparty-update.service
|
|
systemctl daemon-reload >/dev/null 2>&1 || true
|
|
fi
|
|
log "Removed the scheduled copyparty updater."
|
|
}
|
|
|
|
case "${1:-check}" in
|
|
check) do_check ;;
|
|
run) do_run ;;
|
|
update) do_update ;;
|
|
install) do_install ;;
|
|
uninstall) do_uninstall ;;
|
|
*) die "Usage: update.sh [check|run|update|install|uninstall]" ;;
|
|
esac
|