#!/usr/bin/sh
# Adopt the database of a previous Conduit lineage homeserver into
# /var/lib/tuwunel so an existing server continues transparently and migrates
# on the next start. The old directory is moved into place, never copied or
# deleted, and the location it came from is left behind as a symlink.
#
# The package runs this once on install. It is safe to run again by hand,
# which is how an operator finishes a migration that was deferred because the
# old server was still running.
set -e

TUWUNEL_DATABASE_PATH=/var/lib/tuwunel
TUWUNEL_CONFIG_FILE=/etc/tuwunel/tuwunel.toml
SELF=/usr/libexec/tuwunel/adopt-legacy-database

# Never reshape another homeserver's data while it could still be writing.
legacy_active=''
if [ -d /run/systemd/system ] && command -v systemctl > /dev/null 2>&1; then
  for unit in conduwuit.service matrix-conduwuit.service \
    conduit.service matrix-conduit.service; do
    if systemctl is-active --quiet "$unit" 2>/dev/null; then
      legacy_active="$unit"
    fi
  done
fi

if [ -n "$legacy_active" ]; then
  echo "NOTE: $legacy_active is still running, its database was left in place." 1>&2
  echo "NOTE: stop it, then adopt its database with: $SELF" 1>&2
  exit 0
fi

mkdir -p "$TUWUNEL_DATABASE_PATH"

# A directory on another filesystem is never moved: mv would silently become a
# copy of the whole database, and the target may not even have room for it.
# Comparing devices covers a mount at the directory itself and a mount further
# up the path alike.
state_device=$(stat -c %d /var/lib)

# The /var/lib/private paths hold the real directory when the old server ran
# under systemd's DynamicUser=, reaching the public path through a symlink.
adopted=''
for legacy in /var/lib/conduwuit /var/lib/private/conduwuit \
  /var/lib/matrix-conduit /var/lib/private/matrix-conduit; do
  { [ -d "$legacy" ] && ! [ -L "$legacy" ]; } || continue

  if [ "$(stat -c %d "$legacy")" != "$state_device" ]; then
    echo "NOTE: $legacy is on another filesystem, mount it at $TUWUNEL_DATABASE_PATH instead" 1>&2
    continue
  fi

  # An empty leftover directory is cleared rather than left behind.
  if ! [ "$(ls -A "$legacy")" ]; then
    rmdir "$legacy"
    continue
  fi

  # Clear an empty target so the old directory can take its place, unless a
  # filesystem is mounted there, where rmdir would fail and abort the install.
  if [ -d "$TUWUNEL_DATABASE_PATH" ] && ! [ -L "$TUWUNEL_DATABASE_PATH" ] \
    && [ "$(stat -c %d "$TUWUNEL_DATABASE_PATH")" = "$state_device" ] \
    && ! [ "$(ls -A "$TUWUNEL_DATABASE_PATH")" ]; then
    rmdir "$TUWUNEL_DATABASE_PATH"
  fi

  if [ -e "$TUWUNEL_DATABASE_PATH" ]; then
    echo "NOTE: leaving $legacy in place, $TUWUNEL_DATABASE_PATH already exists" 1>&2
  else
    echo "Adopting the existing database at $legacy" 1>&2
    mv "$legacy" "$TUWUNEL_DATABASE_PATH"
    # the public name is the one a configuration carries, even where the
    # database itself lived under /var/lib/private
    adopted="$adopted /var/lib/${legacy##*/}"
    # clean up the stray symlink a previous package version may have left
    # inside the old database directory
    if [ -L "$TUWUNEL_DATABASE_PATH/tuwunel" ] \
      && [ "$(readlink "$TUWUNEL_DATABASE_PATH/tuwunel")" = "$TUWUNEL_DATABASE_PATH" ]; then
      rm "$TUWUNEL_DATABASE_PATH/tuwunel"
    fi
  fi
done

# A configuration that sets database_path may name a previous location, so an
# installation carrying one keeps whatever symlinks it has. The server reads
# the packaged file and any the unit's environment names, and a file that
# cannot be read counts as naming a path, since what it holds cannot be known.
configured_path=''
config_files="$TUWUNEL_CONFIG_FILE"
if command -v systemctl > /dev/null 2>&1; then
  unit_env=$(systemctl show -p Environment --value tuwunel.service 2>/dev/null || true)
  case "$unit_env" in
    *_DATABASE_PATH=*) configured_path='yes' ;;
  esac
  for assignment in $unit_env; do
    case "$assignment" in
      CONDUIT_CONFIG=* | CONDUWUIT_CONFIG=* | TUWUNEL_CONFIG=*)
        config_files="$config_files ${assignment#*=}" ;;
    esac
  done
fi
for config_file in $config_files; do
  if ! [ -r "$config_file" ] \
    || grep -qE '^[[:space:]]*database_path[[:space:]]*=' "$config_file"; then
    configured_path='yes'
  fi
done

# Symlink a location a database was adopted from, so a configuration ported
# from the old server keeps reaching it, and clear a symlink left dangling by
# that adoption or by an uninstalled server. Nowhere else is linked: a previous
# server's name pointing at the database is one `rm -r` away from deleting it,
# because a trailing slash makes that command follow the symlink. A symlink an
# earlier version of this package left where nothing was ever adopted is
# removed for the same reason, once the configuration is known not to name it.
for legacy in /var/lib/matrix-conduit /var/lib/conduwuit; do
  if [ -L "$legacy" ] && ! [ -e "$legacy" ]; then
    rm "$legacy"
  fi
  case " $adopted " in
    *" $legacy "*)
      if ! [ -e "$legacy" ] && ! [ -L "$legacy" ]; then
        ln -s "$TUWUNEL_DATABASE_PATH" "$legacy"
      fi
      continue
      ;;
  esac
  [ -L "$legacy" ] || continue
  if [ "$(readlink -f "$legacy")" != "$(readlink -f "$TUWUNEL_DATABASE_PATH")" ]; then
    echo "NOTE: $legacy points to $(readlink -f "$legacy") and was not adopted" 1>&2
  elif [ -z "$adopted" ] && [ -z "$configured_path" ]; then
    echo "Removing the unused compatibility symlink $legacy" 1>&2
    rm "$legacy"
  fi
done

# Only an adopted database or a directory not yet the server's is walked: an
# upgrade leaves the server running, creating and deleting files under a walk
# that would fail on the first one to vanish.
if [ -n "$adopted" ] \
  || [ "$(stat -c %U:%G "$TUWUNEL_DATABASE_PATH")" != tuwunel:tuwunel ]; then
  chown -R tuwunel:tuwunel "$TUWUNEL_DATABASE_PATH"
fi
chmod 0740 "$TUWUNEL_DATABASE_PATH"

# An adopted directory carries the labels of wherever it came from.
if command -v restorecon > /dev/null 2>&1; then
  restorecon -R "$TUWUNEL_DATABASE_PATH" > /dev/null 2>&1 || :
fi
