#!/usr/bin/env bash
# Root-owned publish step for storebound desktop releases (install as /usr/local/sbin/storebound-desktop-publish,
# root:root 755; see docs/desktop-release.md). tools/desktop-release.ts publish uploads one version folder to
# /var/lib/storebound-desktop/incoming/<version>/ and then runs:  storebound-desktop-publish <version> [stable|beta|rehearsal]
# (rehearsal: tools/release.ts --rehearsal, a throwaway prerelease at /desktop/rehearsal/latest.json that no app reads)
#
# It checks the upload (regular files with plain names only, SHA256SUMS, latest.json of that version whose every URL
# points at an uploaded file of /desktop/dl/<version>/), copies the files into /var/www/storebound-desktop/dl/<version>/
# (a new version: a temp folder renamed into place; an existing version: only new files, a file already published is
# never replaced by different bytes), writes the version's own dl/<version>/latest.json (what `storebound desktop` of
# the same CLI version installs), and only then swaps latest.json of the channel atomically (temp file + rename).
# Apache serves that folder under https://storebound.dev/desktop/ (tools/vds/apache-storebound-desktop.conf), apart
# from the site's own rsync, which deletes what is not in the repository. Nothing from the upload is executed.
set -euo pipefail
umask 022

VERSION="${1:-}"
CHANNEL="${2:-stable}"
INCOMING=/var/lib/storebound-desktop/incoming
ROOT=/var/www/storebound-desktop
SITE=https://storebound.dev/desktop

die() { echo "storebound-desktop-publish: $*" >&2; exit 1; }
[[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$ ]] || die "usage: storebound-desktop-publish <version> [stable|beta|rehearsal]"
[[ "$CHANNEL" == stable || "$CHANNEL" == beta || "$CHANNEL" == rehearsal ]] || die "channel: stable, beta or rehearsal"
# a stable or beta version is never a rehearsal, and a rehearsal never reaches stable or beta
if [[ "$VERSION" == *-rehearsal.* ]]; then [ "$CHANNEL" = rehearsal ] || die "a rehearsal version goes to the rehearsal channel only"; else [ "$CHANNEL" != rehearsal ] || die "the rehearsal channel takes rehearsal versions only"; fi
IN="$INCOMING/$VERSION"
DL="$ROOT/dl/$VERSION"
[ -d "$IN" ] && [ ! -L "$IN" ] || die "nothing uploaded at $IN"

# only regular files with plain names, nothing hidden, no folders, no links
if find "$IN" -mindepth 1 \( ! -type f -o -name '.*' \) -print | grep -q .; then die "the upload holds something other than plain files"; fi
if find "$IN" -mindepth 1 -type f -printf '%f\n' | grep -Ev '^[A-Za-z0-9][A-Za-z0-9._-]*$' | grep -q .; then die "a file name has unexpected characters"; fi
[ -f "$IN/latest.json" ] && [ -f "$IN/SHA256SUMS" ] || die "latest.json and SHA256SUMS are required"
(cd "$IN" && sha256sum --quiet --strict -c SHA256SUMS) || die "SHA256SUMS does not match the files"

python3 - "$IN" "$VERSION" "$SITE" <<'PY' || die "latest.json does not describe this upload"
import json, os, re, sys
src, version, site = sys.argv[1], sys.argv[2], sys.argv[3]
m = json.load(open(os.path.join(src, "latest.json")))
assert m.get("version") == version, "version"
files = set(os.listdir(src))
listed = {l.split()[1] for l in open(os.path.join(src, "SHA256SUMS")) if l.strip()}
assert files - {"SHA256SUMS"} <= listed, "files missing from SHA256SUMS"
prefix = f"{site}/dl/{version}/"
entries = list((m.get("platforms") or {}).values()) + list((m.get("downloads") or {}).values())
assert entries, "no platforms"
for e in entries:
    url = e.get("url", "")
    assert url.startswith(prefix), url
    name = url[len(prefix):]
    assert re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]*", name) and name in files, url
    assert e.get("signature"), url
PY

install -d -m 755 "$ROOT" "$ROOT/dl" "$ROOT/beta" "$ROOT/rehearsal"
if [ ! -e "$DL" ]; then
  TMP="$(mktemp -d "$ROOT/dl/.tmp-$VERSION.XXXXXX")"
  for f in "$IN"/*; do n="$(basename "$f")"; [ "$n" = latest.json ] || install -m 644 "$f" "$TMP/$n"; done
  chmod 755 "$TMP"
  mv -T "$TMP" "$DL"
else
  for f in "$IN"/*; do
    n="$(basename "$f")"
    [ "$n" = latest.json ] && continue
    if [ -e "$DL/$n" ]; then
      [ "$n" = SHA256SUMS ] && continue
      cmp -s "$f" "$DL/$n" || die "$n is already published with other bytes: a published file is never replaced (bump the version)"
    else
      install -m 644 "$f" "$DL/.$n.tmp" && mv -f "$DL/.$n.tmp" "$DL/$n"
    fi
  done
  install -m 644 "$IN/SHA256SUMS" "$DL/.SHA256SUMS.tmp" && mv -f "$DL/.SHA256SUMS.tmp" "$DL/SHA256SUMS"
fi

# the version's own manifest: dl/<version>/latest.json. `storebound desktop` of CLI <version> installs exactly that
# version from it (the CLI and the app share one version number); it may be updated when installers are added
install -m 644 "$IN/latest.json" "$DL/.latest.json.tmp" && mv -f "$DL/.latest.json.tmp" "$DL/latest.json"

# latest.json last: installed apps see the new version only once every file it names is in place
if [ "$CHANNEL" = stable ]; then LJ="$ROOT/latest.json"; URL="$SITE/latest.json"; else LJ="$ROOT/$CHANNEL/latest.json"; URL="$SITE/$CHANNEL/latest.json"; fi
install -m 644 "$IN/latest.json" "$LJ.tmp"
mv -f "$LJ.tmp" "$LJ"
rm -rf "$IN"
curl -fsS -o /dev/null --resolve storebound.dev:443:127.0.0.1 "$URL" && echo "storebound desktop $VERSION published ($CHANNEL): $URL"
