From 84e95942840e993ae1464b07d14789a5d59284f4 Mon Sep 17 00:00:00 2001 From: nnduc Date: Sat, 24 Oct 2026 14:34:00 +0700 Subject: [PATCH] install script --- .gitignore | 1 + build-release.sh | 52 +++++++++ install.sh | 286 +++++++++++++++++++++++++++++++++++++++++++++++ src/db.rs | 25 ++++- 4 files changed, 358 insertions(+), 6 deletions(-) create mode 100755 build-release.sh create mode 100755 install.sh diff --git a/.gitignore b/.gitignore index ea8c4bf..4f96631 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +/dist diff --git a/build-release.sh b/build-release.sh new file mode 100755 index 0000000..8bb7cbc --- /dev/null +++ b/build-release.sh @@ -0,0 +1,52 @@ +#!/bin/sh +# Build a release tarball for Debian servers, in a Debian container so the +# binary links against Debian's glibc (a binary built on a newer distribution +# does not start on an older glibc). +# +# ./build-release.sh -> dist/movim--.tar.gz +# +# Copy it to the server, then: tar xzf movim-*.tar.gz && cd movim-* && ./install.sh +# +# Overridable: DEBIAN (default bookworm: Debian 12, also runs on 13 trixie), +# RUST_IMAGE (default rust:1-$DEBIAN), DOCKER (default docker; podman works). +set -eu + +DEBIAN=${DEBIAN:-bookworm} +RUST_IMAGE=${RUST_IMAGE:-rust:1-$DEBIAN} +DOCKER=${DOCKER:-docker} + +die() { echo "error: $*" >&2; exit 1; } + +cd "$(dirname "$0")" +[ -f Cargo.toml ] || die "run this from the movim-rs repository" +command -v "$DOCKER" >/dev/null || die "$DOCKER not found" +"$DOCKER" info >/dev/null 2>&1 || die "cannot reach $DOCKER; start it (sudo systemctl start docker) and make sure you may use it (docker group)" + +version=$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -n1) +name="movim-$version-$DEBIAN" +# Separate target and cargo cache per image: incremental rebuilds stay fast and +# never mix with host builds in target/release. +cache="target/docker-$DEBIAN" +mkdir -p "$cache/cargo" + +echo "==> Building $name in $RUST_IMAGE" +"$DOCKER" run --rm \ + --user "$(id -u):$(id -g)" \ + -v "$PWD:/src" -w /src \ + -e CARGO_HOME="/src/$cache/cargo" \ + -e CARGO_TARGET_DIR="/src/$cache" \ + "$RUST_IMAGE" \ + sh -c 'rustc --version && cargo build --release --locked' + +echo "==> Packing dist/$name.tar.gz" +stage=$(mktemp -d) +trap 'rm -rf "$stage"' EXIT +mkdir "$stage/$name" +cp "$cache/release/movim" install.sh "$stage/$name/" +cp -R frontend templates "$stage/$name/" +mkdir -p dist +tar -C "$stage" -czf "dist/$name.tar.gz" "$name" + +echo +echo "dist/$name.tar.gz ($(du -h "dist/$name.tar.gz" | cut -f1))" +echo "On the server: tar xzf $name.tar.gz && cd $name && ./install.sh" diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..a0d1d04 --- /dev/null +++ b/install.sh @@ -0,0 +1,286 @@ +#!/bin/sh +# Install Movim as a systemd service (not enabled or started). +# +# From a release tarball (see build-release.sh), installs the prebuilt binary: +# tar xzf movim-*.tar.gz && cd movim-* && ./install.sh +# From the repository, compiles first (as a normal user, not root): +# ./install.sh +# Privileged steps go through sudo when not run as root. +# +# Overridable: MOVIM_USER (default movim, created if missing), PREFIX +# (default /opt/movim), DATA_DIR (default /var/lib/movim), CONF_DIR +# (default /etc/movim), XMPP_DOMAIN and WEB_HOST (asked when run in a +# terminal and the configuration does not exist yet). +set -eu + +MOVIM_USER=${MOVIM_USER:-movim} +PREFIX=${PREFIX:-/opt/movim} +DATA_DIR=${DATA_DIR:-/var/lib/movim} +CONF_DIR=${CONF_DIR:-/etc/movim} +XMPP_DOMAIN=${XMPP_DOMAIN:-} +WEB_HOST=${WEB_HOST:-} +UNIT=/etc/systemd/system/movim.service +MIN_RUSTC=1.94 + +die() { echo "error: $*" >&2; exit 1; } + +valid_host() { + printf '%s' "$1" | grep -Eq '^([A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?\.)+[A-Za-z]{2,}$' +} + +# ask VAR "question" default: prompt on the terminal until a valid hostname. +ask() { + eval "current=\${$1}" + [ -z "$current" ] || { valid_host "$current" || die "$1=$current is not a valid domain name"; return 0; } + if [ ! -t 0 ]; then + echo " no terminal, $1 defaults to $3 (set $1=... to choose)" + eval "$1=\$3" + return 0 + fi + while :; do + printf '%s [%s]: ' "$2" "$3" + read -r answer || die "aborted" + answer=${answer:-$3} + if valid_host "$answer"; then + eval "$1=\$answer" + return 0 + fi + echo " not a valid domain name: $answer" + done +} + +cd "$(dirname "$0")" +[ -d frontend ] && [ -d templates ] || die "run this from the movim-rs repository or an extracted release" +command -v systemctl >/dev/null || die "systemd is required" +if [ "$(id -u)" -eq 0 ]; then + sudo() { "$@"; } +else + command -v sudo >/dev/null || die "sudo is required (or run as root)" +fi + +if [ -f Cargo.toml ]; then + [ "$(id -u)" -ne 0 ] || die "do not compile as root; run as a normal user, privileged steps use sudo" + BIN=target/release/movim +elif [ -x movim ]; then + BIN=./movim + echo "==> Prebuilt binary: $(file -b movim 2>/dev/null || echo movim)" +else + die "no Cargo.toml and no prebuilt ./movim" +fi + +# --- Questions (before the build, so nobody has to wait for it) ----------- +if [ ! -e "$CONF_DIR/movim.env" ]; then + ask XMPP_DOMAIN "XMPP domain users log in with (MOVIM_DOMAIN)" example.org +fi +if [ -d /etc/nginx/sites-available ] && [ ! -e /etc/nginx/sites-available/movim ]; then + default_web=movim.${XMPP_DOMAIN:-example.org} + ask WEB_HOST "Web hostname serving Movim (nginx server_name)" "$default_web" +fi + +# --- Build ----------------------------------------------------------------- +build() { +rustup_help="Distribution packages (e.g. Debian's rustc) are too old. Install a current +toolchain for your user with rustup, it does not conflict with apt's rustc: + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +then run this script again." + +# Prefer a rustup toolchain over the distribution's cargo, even if +# ~/.cargo/bin is not in PATH (non-login shell, Debian's cargo first). +[ ! -x "$HOME/.cargo/bin/cargo" ] || PATH="$HOME/.cargo/bin:$PATH" +command -v cargo >/dev/null || die "cargo not found, need Rust >= $MIN_RUSTC. +$rustup_help" +rustc_version=$(rustc --version | cut -d' ' -f2) +lowest=$(printf '%s\n%s\n' "$MIN_RUSTC" "$rustc_version" | sort -V | head -n1) +if [ "$lowest" != "$MIN_RUSTC" ]; then + if command -v rustup >/dev/null; then + die "rustc $rustc_version is too old, need >= $MIN_RUSTC: rustup update stable && rustup default stable" + fi + die "rustc $rustc_version ($(command -v rustc)) is too old, need >= $MIN_RUSTC. +$rustup_help" +fi +# Linker and C compiler (bundled SQLite, crypto libraries). +command -v cc >/dev/null || die "no C compiler; on Debian: sudo apt install build-essential cmake" + +echo "==> Building (rustc $rustc_version)" +cargo build --release --locked +} +[ ! -f Cargo.toml ] || build + +# --- Install --------------------------------------------------------------- +echo "==> Installing to $PREFIX (sudo)" + +if ! id "$MOVIM_USER" >/dev/null 2>&1; then + nologin=$(command -v nologin || echo /usr/sbin/nologin) + echo " creating system user $MOVIM_USER" + sudo useradd --system --user-group --home-dir "$DATA_DIR" --no-create-home \ + --shell "$nologin" "$MOVIM_USER" +fi +group=$(id -gn "$MOVIM_USER") + +# Code and assets are owned by root and read-only for the service. +sudo install -d -m 0755 -o root -g root "$PREFIX" +sudo install -m 0755 -o root -g root "$BIN" "$PREFIX/movim.new" +for d in frontend templates; do + sudo rm -rf "$PREFIX/$d.new" + sudo cp -R "$d" "$PREFIX/$d.new" + sudo chown -R root:root "$PREFIX/$d.new" + sudo chmod -R u=rwX,go=rX "$PREFIX/$d.new" +done +# Swap in the new files only once everything is copied. +sudo mv -f "$PREFIX/movim.new" "$PREFIX/movim" +for d in frontend templates; do + sudo rm -rf "$PREFIX/$d.old" + [ ! -e "$PREFIX/$d" ] || sudo mv "$PREFIX/$d" "$PREFIX/$d.old" + sudo mv "$PREFIX/$d.new" "$PREFIX/$d" + sudo rm -rf "$PREFIX/$d.old" +done + +# Database, VAPID key: only the service user can read them. +sudo install -d -m 0750 -o "$MOVIM_USER" -g "$group" "$DATA_DIR" + +# Configuration: never overwrite an existing one. +sudo install -d -m 0755 -o root -g root "$CONF_DIR" +if [ ! -e "$CONF_DIR/movim.env" ]; then + echo " writing $CONF_DIR/movim.env" + sudo tee "$CONF_DIR/movim.env" >/dev/null </dev/null </dev/null < Result { - let pool = SqlitePoolOptions::new().max_connections(8).connect(url).await?; - sqlx::query("PRAGMA journal_mode=WAL").execute(&pool).await?; + // WAL: readers never block the writer. synchronous=NORMAL is safe in WAL + // (no corruption, a power loss can only drop the last commits) and avoids an + // fsync per message. Writers queue on the busy timeout instead of failing. + let options = SqliteConnectOptions::from_str(url)? + .journal_mode(SqliteJournalMode::Wal) + .synchronous(SqliteSynchronous::Normal) + .busy_timeout(Duration::from_secs(10)); + let pool = SqlitePoolOptions::new().max_connections(8).connect_with(options).await?; for statement in SCHEMA.split(';') { let statement = statement.trim(); if !statement.is_empty() { @@ -840,15 +849,17 @@ pub async fn reactions_for(db: &Db, mids: &[i64]) -> Result> { } pub async fn reactions_replace(db: &Db, mid: i64, jidfrom: &str, emojis: &[String]) -> Result<()> { - sqlx::query("DELETE FROM reactions WHERE message_mid = ? AND jidfrom = ?").bind(mid).bind(jidfrom).execute(db).await?; + let mut tx = db.begin().await?; + sqlx::query("DELETE FROM reactions WHERE message_mid = ? AND jidfrom = ?").bind(mid).bind(jidfrom).execute(&mut *tx).await?; for emoji in emojis { sqlx::query("INSERT OR IGNORE INTO reactions (message_mid, jidfrom, emoji) VALUES (?, ?, ?)") .bind(mid) .bind(jidfrom) .bind(emoji) - .execute(db) + .execute(&mut *tx) .await?; } + tx.commit().await?; Ok(()) } @@ -1014,6 +1025,7 @@ pub async fn messages_search(db: &Db, user: &str, jid: &str, muc: bool, keywords /// Forget everything stored for an account pub async fn user_delete_data(db: &Db, user: &str) -> Result<()> { + let mut tx = db.begin().await?; for sql in [ "DELETE FROM reactions WHERE message_mid IN (SELECT mid FROM messages WHERE user_id = ?)", "DELETE FROM message_files WHERE message_mid IN (SELECT mid FROM messages WHERE user_id = ?)", @@ -1025,8 +1037,9 @@ pub async fn user_delete_data(db: &Db, user: &str) -> Result<()> { "DELETE FROM push_subscriptions WHERE user_id = ?", "DELETE FROM users WHERE id = ?", ] { - sqlx::query(sql).bind(user).execute(db).await?; + sqlx::query(sql).bind(user).execute(&mut *tx).await?; } + tx.commit().await?; Ok(()) }