From 8b9ed92a11e25cfd605fd54521a155469d4903fd Mon Sep 17 00:00:00 2001 From: Chrys <53332481+ChrAlpha@users.noreply.github.com> Date: Sun, 26 Apr 2026 17:20:18 +0800 Subject: [PATCH] fix(install): handle dirty one-click upgrades (#401) * fix(install): handle dirty one-click upgrades * fix(install): address dirty state review feedback --- docs/docs/installation/docker.md | 30 +- internal/db/utils.go | 22 +- internal/db/utils_test.go | 41 +++ scripts/install.sh | 486 ++++++++++++++++++++++++++++--- 4 files changed, 527 insertions(+), 52 deletions(-) diff --git a/docs/docs/installation/docker.md b/docs/docs/installation/docker.md index 14bfad1d..34876ada 100644 --- a/docs/docs/installation/docker.md +++ b/docs/docs/installation/docker.md @@ -58,11 +58,15 @@ curl -fsSL https://memoh.sh | sudo sh The script will: 1. Check for Docker and Docker Compose -2. Prompt for configuration (workspace, data directory, admin credentials, JWT secret, Postgres password, sparse service toggle, browser core selection) -3. Fetch the latest release tag from GitHub and clone the repository -4. Generate `config.toml` from the Docker template with your settings -5. Pin Docker image versions to the release -6. Build the browser image with selected cores and start all services +2. Detect whether this is a first-time install, an upgrade, or a reinstall +3. Prompt for configuration (workspace, data directory, admin credentials, JWT secret, Postgres password, sparse service toggle, browser core selection) +4. Reuse the existing `config.toml` automatically during upgrades so database credentials stay aligned with the persisted PostgreSQL volume +5. Offer a clean reinstall mode that removes Memoh Docker containers, volumes, and network before starting again +6. Fetch the latest release tag from GitHub and clone the repository +7. Generate `config.toml` from the Docker template with your settings when needed +8. Pin Docker image versions to the release +9. Select and pull the prebuilt browser image for the chosen cores and start all services +10. Print recent `postgres` and `migrate` logs automatically if startup fails **Silent install** (use all defaults, no prompts): @@ -78,6 +82,20 @@ Defaults when running silently: - JWT secret: auto-generated - Postgres password: `memoh123` +If the script detects an existing Memoh installation in silent mode, it defaults to **upgrade** and reuses the previous `config.toml`. If Docker state exists but no reusable `config.toml` can be found, the script exits and asks you to choose an explicit reinstall. + +**Force a clean reinstall** (removes Memoh Docker data before starting again): + +```bash +curl -fsSL https://memoh.sh | sudo MEMOH_INSTALL_MODE=reinstall sh +``` + +You can also pass the install mode as an argument: + +```bash +curl -fsSL https://memoh.sh | sudo sh -s -- --install-mode reinstall +``` + **Install a specific version:** ```bash @@ -187,6 +205,7 @@ The `config.toml` file controls all server behavior. Here is a summary of the av ```bash docker compose up -d # Start docker compose down # Stop +docker compose down -v # Stop and remove Memoh Docker data docker compose logs -f # View logs docker compose ps # Status docker compose pull && docker compose up -d # Update to latest images @@ -199,6 +218,7 @@ docker compose pull && docker compose up -d # Update to latest images | `POSTGRES_PASSWORD`| `memoh123` | PostgreSQL password (must match `postgres.password` in `config.toml`) | | `MEMOH_CONFIG` | `./config.toml` | Path to the configuration file | | `MEMOH_VERSION` | *(latest release)* | Git tag to install (e.g. `v0.6.0`). Also pins Docker image versions. | +| `MEMOH_INSTALL_MODE` | `auto` | Install mode: `auto`, `fresh`, `upgrade`, or `reinstall` | | `USE_CN_MIRROR` | `false` | Set to `true` to use China mainland image mirrors | | `BROWSER_CORES` | `chromium,firefox` | Browser engines to include in the browser image | | `BROWSER_TAG` | `latest` | Docker tag for the browser image | diff --git a/internal/db/utils.go b/internal/db/utils.go index 7fc5f888..d67e3936 100644 --- a/internal/db/utils.go +++ b/internal/db/utils.go @@ -3,6 +3,9 @@ package db import ( "errors" "fmt" + "net" + "net/url" + "strconv" "strings" "time" @@ -15,15 +18,16 @@ import ( // DSN builds a PostgreSQL connection string from config. func DSN(cfg config.PostgresConfig) string { - return fmt.Sprintf( - "postgres://%s:%s@%s:%d/%s?sslmode=%s", - cfg.User, - cfg.Password, - cfg.Host, - cfg.Port, - cfg.Database, - cfg.SSLMode, - ) + dsn := &url.URL{ + Scheme: "postgres", + User: url.UserPassword(cfg.User, cfg.Password), + Host: net.JoinHostPort(cfg.Host, strconv.Itoa(cfg.Port)), + Path: cfg.Database, + } + query := dsn.Query() + query.Set("sslmode", cfg.SSLMode) + dsn.RawQuery = query.Encode() + return dsn.String() } // ParseUUID converts a string UUID to pgtype.UUID. diff --git a/internal/db/utils_test.go b/internal/db/utils_test.go index 15df9585..865388d4 100644 --- a/internal/db/utils_test.go +++ b/internal/db/utils_test.go @@ -3,6 +3,8 @@ package db import ( "errors" "fmt" + "net/url" + "strconv" "testing" "time" @@ -29,6 +31,45 @@ func TestDSN(t *testing.T) { } } +func TestDSNEncodesSpecialCharacters(t *testing.T) { + specialValue := "pa" + "@ss word/#'\"\\" + cfg := config.PostgresConfig{ + Host: "localhost", + Port: 5432, + User: "memoh@example", + Password: specialValue, + Database: "memoh", + SSLMode: "disable", + } + + parsed, err := url.Parse(DSN(cfg)) + if err != nil { + t.Fatalf("DSN() produced invalid URL: %v", err) + } + if parsed.User.Username() != cfg.User { + t.Errorf("DSN() username = %q, want %q", parsed.User.Username(), cfg.User) + } + password, ok := parsed.User.Password() + if !ok { + t.Fatal("DSN() did not include a password") + } + if password != cfg.Password { + t.Errorf("DSN() password = %q, want %q", password, cfg.Password) + } + if parsed.Hostname() != cfg.Host { + t.Errorf("DSN() hostname = %q, want %q", parsed.Hostname(), cfg.Host) + } + if parsed.Port() != strconv.Itoa(cfg.Port) { + t.Errorf("DSN() port = %q, want %d", parsed.Port(), cfg.Port) + } + if parsed.Path != "/"+cfg.Database { + t.Errorf("DSN() path = %q, want /%s", parsed.Path, cfg.Database) + } + if parsed.Query().Get("sslmode") != cfg.SSLMode { + t.Errorf("DSN() sslmode = %q, want %q", parsed.Query().Get("sslmode"), cfg.SSLMode) + } +} + func TestParseUUID(t *testing.T) { validUUID := uuid.MustParse("550e8400-e29b-41d4-a716-446655440000") tests := []struct { diff --git a/scripts/install.sh b/scripts/install.sh index ab8ed515..c4a78459 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -10,8 +10,46 @@ NC='\033[0m' GITHUB_REPO="memohai/Memoh" REPO="https://github.com/${GITHUB_REPO}.git" DIR="Memoh" +COMPOSE_PROJECT_NAME="memoh" SILENT=false +# Track whether the user explicitly set environment-backed options so upgrades +# can reuse prior install values by default. +if [ "${MEMOH_INSTALL_MODE+x}" = x ]; then + INSTALL_MODE="$MEMOH_INSTALL_MODE" +else + INSTALL_MODE="auto" +fi +if [ "${USE_CN_MIRROR+x}" = x ]; then + USE_CN_MIRROR_SET=true +else + USE_CN_MIRROR_SET=false +fi +if [ "${USE_SPARSE+x}" = x ]; then + USE_SPARSE_SET=true +else + USE_SPARSE_SET=false +fi +if [ "${BROWSER_CORE+x}" = x ]; then + BROWSER_CORE_SET=true +else + BROWSER_CORE_SET=false +fi + +NETWORK_NAME="${COMPOSE_PROJECT_NAME}_memoh-network" +PROJECT_CONTAINERS="memoh-postgres memoh-migrate memoh-server memoh-web memoh-sparse memoh-qdrant memoh-browser" +PROJECT_VOLUMES="${COMPOSE_PROJECT_NAME}_postgres_data ${COMPOSE_PROJECT_NAME}_containerd_data ${COMPOSE_PROJECT_NAME}_memoh_data ${COMPOSE_PROJECT_NAME}_server_cni_state ${COMPOSE_PROJECT_NAME}_qdrant_data ${COMPOSE_PROJECT_NAME}_openviking_data" + +EXISTING_CONFIG_SOURCE="" +EXISTING_ENV_SOURCE="" +EXISTING_INSTALL_STATE=false +EXISTING_DOCKER_STATE=false +EXISTING_DOCKER_VOLUMES="" +EXISTING_DOCKER_CONTAINERS=false +EXISTING_DOCKER_NETWORK=false +EXISTING_WORKSPACE_FILES=false +EXISTING_REPO_DIR=false + # Parse flags while [ $# -gt 0 ]; do case "$1" in @@ -23,6 +61,13 @@ while [ $# -gt 0 ]; do --version=*) MEMOH_VERSION="${1#--version=}" ;; + --install-mode) + shift + INSTALL_MODE="$1" + ;; + --install-mode=*) + INSTALL_MODE="${1#--install-mode=}" + ;; esac shift done @@ -34,6 +79,321 @@ fi echo "${PURPLE}Memoh One-Click Install${NC}" +read_env_file_value() { + file="$1" + key="$2" + if [ ! -f "$file" ]; then + return 1 + fi + value=$(grep "^${key}=" "$file" 2>/dev/null | tail -n 1 | cut -d '=' -f 2-) + if [ -z "$value" ]; then + return 1 + fi + case "$value" in + \'*\') + value=${value#\'} + value=${value%\'} + value=$(printf '%s' "$value" | sed "s/\\\\'/'/g") + ;; + esac + printf '%s' "$value" +} + +read_toml_value() { + file="$1" + section="$2" + key="$3" + if [ ! -f "$file" ]; then + return 1 + fi + value=$(awk -v target_section="[$section]" -v target_key="$key" ' + /^\[[^]]+\]/ { + in_section = ($0 == target_section) + next + } + in_section && $0 ~ "^[[:space:]]*" target_key "[[:space:]]*=" { + value = substr($0, index($0, "=") + 1) + sub(/^[[:space:]]*/, "", value) + sub(/[[:space:]]*$/, "", value) + if (value ~ /^".*"$/) { + sub(/^"/, "", value) + sub(/"$/, "", value) + } + print value + exit + } + ' "$file") + if [ -z "$value" ]; then + return 1 + fi + printf '%s' "$value" | sed 's/\\"/"/g; s/\\\\/\\/g' +} + +browser_core_from_cores() { + case "$1" in + firefox) printf '%s' "firefox" ;; + all|chromium,firefox|firefox,chromium) printf '%s' "all" ;; + *) printf '%s' "chromium" ;; + esac +} + +escape_toml_string() { + printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g' +} + +set_toml_string_value() { + file="$1" + section="$2" + key="$3" + value=$(escape_toml_string "$4") + tmp="${file}.tmp.$$" + if TOML_VALUE="$value" awk -v target_section="[$section]" -v target_key="$key" ' + BEGIN { + target_value = ENVIRON["TOML_VALUE"] + } + /^\[[^]]+\]/ { + in_section = ($0 == target_section) + } + in_section && $0 ~ "^[[:space:]]*" target_key "[[:space:]]*=" { + indent = $0 + sub(/[^[:space:]].*/, "", indent) + print indent target_key " = \"" target_value "\"" + next + } + { print } + ' "$file" > "$tmp"; then + mv "$tmp" "$file" + else + rm -f "$tmp" + return 1 + fi +} + +write_env_value() { + key="$1" + value=$(printf '%s' "$2" | sed "s/'/\\\\'/g") + printf "%s='%s'\n" "$key" "$value" >> .env +} + +fetch_latest_version() { + if command -v curl >/dev/null 2>&1; then + curl -fsSL "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" 2>/dev/null + elif command -v wget >/dev/null 2>&1; then + wget -qO- "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" 2>/dev/null + else + echo "${RED}Error: curl or wget is required${NC}" >&2 + exit 1 + fi +} + +detect_existing_installation() { + EXISTING_CONFIG_SOURCE="" + EXISTING_ENV_SOURCE="" + EXISTING_INSTALL_STATE=false + EXISTING_DOCKER_STATE=false + EXISTING_DOCKER_VOLUMES="" + EXISTING_DOCKER_CONTAINERS=false + EXISTING_DOCKER_NETWORK=false + EXISTING_WORKSPACE_FILES=false + EXISTING_REPO_DIR=false + + if [ -d "$WORKSPACE/$DIR" ]; then + EXISTING_REPO_DIR=true + EXISTING_INSTALL_STATE=true + fi + + if [ -f "$WORKSPACE/config.toml" ]; then + EXISTING_CONFIG_SOURCE="$WORKSPACE/config.toml" + EXISTING_WORKSPACE_FILES=true + EXISTING_INSTALL_STATE=true + if [ -f "$WORKSPACE/.env" ]; then + EXISTING_ENV_SOURCE="$WORKSPACE/.env" + fi + elif [ -f "$WORKSPACE/$DIR/config.toml" ]; then + EXISTING_CONFIG_SOURCE="$WORKSPACE/$DIR/config.toml" + EXISTING_INSTALL_STATE=true + if [ -f "$WORKSPACE/$DIR/.env" ]; then + EXISTING_ENV_SOURCE="$WORKSPACE/$DIR/.env" + fi + fi + + if [ -f "$WORKSPACE/docker-compose.yml" ] || [ -f "$WORKSPACE/.env" ]; then + EXISTING_WORKSPACE_FILES=true + EXISTING_INSTALL_STATE=true + if [ -z "$EXISTING_ENV_SOURCE" ] && [ -f "$WORKSPACE/.env" ]; then + EXISTING_ENV_SOURCE="$WORKSPACE/.env" + fi + fi + + for volume in $PROJECT_VOLUMES; do + if $DOCKER volume inspect "$volume" >/dev/null 2>&1; then + EXISTING_DOCKER_STATE=true + EXISTING_INSTALL_STATE=true + EXISTING_DOCKER_VOLUMES="${EXISTING_DOCKER_VOLUMES} ${volume}" + fi + done + + for container in $PROJECT_CONTAINERS; do + if $DOCKER container inspect "$container" >/dev/null 2>&1; then + EXISTING_DOCKER_STATE=true + EXISTING_DOCKER_CONTAINERS=true + EXISTING_INSTALL_STATE=true + break + fi + done + + if $DOCKER network inspect "$NETWORK_NAME" >/dev/null 2>&1; then + EXISTING_DOCKER_STATE=true + EXISTING_DOCKER_NETWORK=true + EXISTING_INSTALL_STATE=true + fi +} + +load_existing_settings() { + if [ -n "$EXISTING_CONFIG_SOURCE" ]; then + value=$(read_toml_value "$EXISTING_CONFIG_SOURCE" "admin" "username" || true) + [ -n "$value" ] && ADMIN_USER="$value" + + value=$(read_toml_value "$EXISTING_CONFIG_SOURCE" "admin" "password" || true) + [ -n "$value" ] && ADMIN_PASS="$value" + + value=$(read_toml_value "$EXISTING_CONFIG_SOURCE" "auth" "jwt_secret" || true) + [ -n "$value" ] && JWT_SECRET="$value" + + value=$(read_toml_value "$EXISTING_CONFIG_SOURCE" "postgres" "password" || true) + [ -n "$value" ] && PG_PASS="$value" + + if [ "$USE_CN_MIRROR_SET" = false ]; then + value=$(read_toml_value "$EXISTING_CONFIG_SOURCE" "workspace" "registry" || true) + if [ "$value" = "memoh.cn" ]; then + USE_CN_MIRROR=true + fi + fi + fi + + if [ -n "$EXISTING_ENV_SOURCE" ]; then + if [ "$USE_SPARSE_SET" = false ]; then + value=$(read_env_file_value "$EXISTING_ENV_SOURCE" "USE_SPARSE" || true) + [ -n "$value" ] && USE_SPARSE="$value" + fi + + if [ "$BROWSER_CORE_SET" = false ]; then + value=$(read_env_file_value "$EXISTING_ENV_SOURCE" "BROWSER_CORES" || true) + [ -n "$value" ] && BROWSER_CORE=$(browser_core_from_cores "$value") + fi + + value=$(read_env_file_value "$EXISTING_ENV_SOURCE" "POSTGRES_PASSWORD" || true) + [ -n "$value" ] && PG_PASS="$value" + + value=$(read_env_file_value "$EXISTING_ENV_SOURCE" "MEMOH_DATA_DIR" || true) + [ -n "$value" ] && MEMOH_DATA_DIR="$value" + fi +} + +prompt_install_mode() { + if [ "$SILENT" = true ]; then + if [ "$INSTALL_MODE" = "auto" ]; then + if [ -n "$EXISTING_CONFIG_SOURCE" ]; then + INSTALL_MODE="upgrade" + echo "${YELLOW}ℹ Existing Memoh installation detected. Reusing existing configuration in silent mode.${NC}" + elif [ "$EXISTING_DOCKER_STATE" = true ]; then + echo "${RED}Error: Existing Memoh Docker state was detected but no reusable config.toml was found.${NC}" + echo "Run again with MEMOH_INSTALL_MODE=reinstall to wipe Docker data, or restore the previous config.toml." + exit 1 + else + INSTALL_MODE="fresh" + if [ "$EXISTING_INSTALL_STATE" = true ]; then + echo "${YELLOW}ℹ Existing Memoh files were detected, but no Docker state or reusable config.toml was found. Proceeding with a fresh install in silent mode.${NC}" + fi + fi + fi + return + fi + + if [ "$INSTALL_MODE" != "auto" ]; then + return + fi + + if [ "$EXISTING_INSTALL_STATE" = false ]; then + INSTALL_MODE="fresh" + return + fi + + echo "${YELLOW}Detected existing Memoh installation state:${NC}" > /dev/tty + if [ -n "$EXISTING_CONFIG_SOURCE" ]; then + echo " - Config: ${EXISTING_CONFIG_SOURCE}" > /dev/tty + fi + if [ -n "$EXISTING_ENV_SOURCE" ]; then + echo " - Env: ${EXISTING_ENV_SOURCE}" > /dev/tty + fi + if [ "$EXISTING_REPO_DIR" = true ]; then + echo " - Repository checkout: ${WORKSPACE}/${DIR}" > /dev/tty + fi + if [ -n "$EXISTING_DOCKER_VOLUMES" ]; then + echo " - Docker volumes:${EXISTING_DOCKER_VOLUMES}" > /dev/tty + fi + if [ "$EXISTING_DOCKER_CONTAINERS" = true ]; then + echo " - Existing Memoh containers" > /dev/tty + fi + if [ "$EXISTING_DOCKER_NETWORK" = true ]; then + echo " - Docker network: ${NETWORK_NAME}" > /dev/tty + fi + echo "" > /dev/tty + + if [ -n "$EXISTING_CONFIG_SOURCE" ]; then + echo "Choose install mode:" > /dev/tty + echo " 1) Upgrade existing installation (recommended, reuses config and DB password)" > /dev/tty + echo " 2) Reinstall from scratch (removes Memoh Docker data)" > /dev/tty + echo " 3) Abort" > /dev/tty + printf " Install mode [1]: " > /dev/tty + read -r input < /dev/tty || true + case "$input" in + 2) INSTALL_MODE="reinstall" ;; + 3) INSTALL_MODE="abort" ;; + *) INSTALL_MODE="upgrade" ;; + esac + elif [ "$EXISTING_DOCKER_STATE" = true ]; then + echo "No reusable config.toml was found for a safe upgrade." > /dev/tty + echo "Choose install mode:" > /dev/tty + echo " 1) Reinstall from scratch (removes Memoh Docker data)" > /dev/tty + echo " 2) Abort" > /dev/tty + printf " Install mode [2]: " > /dev/tty + read -r input < /dev/tty || true + case "$input" in + 1) INSTALL_MODE="reinstall" ;; + *) INSTALL_MODE="abort" ;; + esac + else + echo "No reusable config.toml or Docker state was found." > /dev/tty + echo "Choose install mode:" > /dev/tty + echo " 1) Continue fresh install (recommended)" > /dev/tty + echo " 2) Abort" > /dev/tty + printf " Install mode [1]: " > /dev/tty + read -r input < /dev/tty || true + case "$input" in + 2) INSTALL_MODE="abort" ;; + *) INSTALL_MODE="fresh" ;; + esac + fi +} + +cleanup_existing_installation() { + echo "${YELLOW}Removing existing Memoh Docker containers, volumes, and network...${NC}" + for container in $PROJECT_CONTAINERS; do + $DOCKER rm -f "$container" >/dev/null 2>&1 || true + done + for volume in $PROJECT_VOLUMES; do + $DOCKER volume rm -f "$volume" >/dev/null 2>&1 || true + done + $DOCKER network rm "$NETWORK_NAME" >/dev/null 2>&1 || true +} + +show_failure_logs() { + echo "" + echo "${RED}Startup failed. Recent PostgreSQL and migration logs:${NC}" + $DOCKER compose $COMPOSE_FILES $COMPOSE_PROFILES logs --no-color --tail=200 postgres migrate || true +} + # Check Docker and determine if sudo is needed DOCKER="docker" if ! command -v docker >/dev/null 2>&1; then @@ -61,16 +421,6 @@ echo "${GREEN}✓ Docker and Docker Compose detected${NC}" if [ -n "$MEMOH_VERSION" ]; then echo "${GREEN}✓ Using specified version: ${MEMOH_VERSION}${NC}" else - fetch_latest_version() { - if command -v curl >/dev/null 2>&1; then - curl -fsSL "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" 2>/dev/null - elif command -v wget >/dev/null 2>&1; then - wget -qO- "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" 2>/dev/null - else - echo "${RED}Error: curl or wget is required${NC}" >&2 - exit 1 - fi - } MEMOH_VERSION=$(fetch_latest_version | grep '"tag_name"' | sed 's/.*"tag_name": *"\([^"]*\)".*/\1/') if [ -n "$MEMOH_VERSION" ]; then echo "${GREEN}✓ Latest release: ${MEMOH_VERSION}${NC}" @@ -122,17 +472,47 @@ if [ "$SILENT" = false ]; then *) WORKSPACE="$input" ;; esac fi +fi - printf " Data directory (bind mount for server container data) [%s]: " "$WORKSPACE/data" > /dev/tty +mkdir -p "$WORKSPACE" +WORKSPACE=$(cd "$WORKSPACE" && pwd) + +detect_existing_installation +load_existing_settings +prompt_install_mode + +case "$INSTALL_MODE" in + auto) INSTALL_MODE="fresh" ;; + fresh|upgrade|reinstall) ;; + abort) + echo "Installation aborted." + exit 0 + ;; + *) + echo "${RED}Error: Unknown install mode '${INSTALL_MODE}'. Use fresh, upgrade, reinstall, or auto.${NC}" + exit 1 + ;; +esac + +if [ "$INSTALL_MODE" = "upgrade" ] && [ -z "$EXISTING_CONFIG_SOURCE" ]; then + echo "${RED}Error: Upgrade mode requires an existing config.toml to reuse.${NC}" + exit 1 +fi + +if [ "$INSTALL_MODE" = "fresh" ] && [ "$EXISTING_DOCKER_STATE" = true ]; then + echo "${RED}Error: Existing Memoh Docker state was detected. Use upgrade or reinstall instead of fresh.${NC}" + exit 1 +fi + +if [ "$SILENT" = false ] && [ "$INSTALL_MODE" != "upgrade" ]; then + printf " Data directory (reserved for future bind-mount support) [%s]: " "$MEMOH_DATA_DIR" > /dev/tty read -r input < /dev/tty || true if [ -n "$input" ]; then case "$input" in - ~) MEMOH_DATA_DIR="${HOME:-/tmp}" ;; - ~/*) MEMOH_DATA_DIR="${HOME:-/tmp}${input#\~}" ;; + "~") MEMOH_DATA_DIR="${HOME:-/tmp}" ;; + "~"/*) MEMOH_DATA_DIR="${HOME:-/tmp}${input#\~}" ;; *) MEMOH_DATA_DIR="$input" ;; esac - else - MEMOH_DATA_DIR="$WORKSPACE/data" fi printf " Admin username [%s]: " "$ADMIN_USER" > /dev/tty @@ -143,7 +523,7 @@ if [ "$SILENT" = false ]; then read -r input < /dev/tty || true [ -n "$input" ] && ADMIN_PASS="$input" - printf " JWT secret [auto-generated]: " > /dev/tty + printf " JWT secret [current/default value retained]: " > /dev/tty read -r input < /dev/tty || true [ -n "$input" ] && JWT_SECRET="$input" @@ -151,10 +531,11 @@ if [ "$SILENT" = false ]; then read -r input < /dev/tty || true [ -n "$input" ] && PG_PASS="$input" - printf " Enable sparse memory service? [y/N]: " > /dev/tty + printf " Enable sparse memory service? [%s]: " "$( [ "$USE_SPARSE" = true ] && printf 'Y/n' || printf 'y/N' )" > /dev/tty read -r input < /dev/tty || true case "$input" in y|Y|yes|YES) USE_SPARSE=true ;; + n|N|no|NO) USE_SPARSE=false ;; esac echo "" > /dev/tty @@ -162,19 +543,32 @@ if [ "$SILENT" = false ]; then echo " 1) Chromium only (default, smaller image)" > /dev/tty echo " 2) Firefox only" > /dev/tty echo " 3) Both Chromium and Firefox" > /dev/tty - printf " Browser core [1]: " > /dev/tty + case "$BROWSER_CORE" in + firefox) browser_default="2" ;; + all) browser_default="3" ;; + *) browser_default="1" ;; + esac + printf " Browser core [%s]: " "$browser_default" > /dev/tty read -r input < /dev/tty || true case "$input" in 2) BROWSER_CORE="firefox" ;; 3) BROWSER_CORE="all" ;; + "") + case "$browser_default" in + 2) BROWSER_CORE="firefox" ;; + 3) BROWSER_CORE="all" ;; + *) BROWSER_CORE="chromium" ;; + esac + ;; *) BROWSER_CORE="chromium" ;; esac echo "" > /dev/tty +elif [ "$INSTALL_MODE" = "upgrade" ]; then + echo "${GREEN}✓ Upgrade mode: reusing existing configuration and database credentials${NC}" fi # Enter workspace (all operations run here) -mkdir -p "$WORKSPACE" cd "$WORKSPACE" # Clone or update @@ -211,23 +605,28 @@ if [ "$MEMOH_DOCKER_VERSION" != "latest" ]; then echo "${GREEN}✓ Docker images pinned to ${MEMOH_DOCKER_VERSION}${NC}" fi -# Generate config.toml from template -cp conf/app.docker.toml config.toml -sed -i.bak "s|username = \"admin\"|username = \"${ADMIN_USER}\"|" config.toml -sed -i.bak "s|password = \"admin123\"|password = \"${ADMIN_PASS}\"|" config.toml -sed -i.bak "s|jwt_secret = \".*\"|jwt_secret = \"${JWT_SECRET}\"|" config.toml -sed -i.bak "s|password = \"memoh123\"|password = \"${PG_PASS}\"|" config.toml -export POSTGRES_PASSWORD="${PG_PASS}" -if [ "$USE_CN_MIRROR" = true ]; then - sed -i.bak 's|# registry = "memoh.cn"|registry = "memoh.cn"|' config.toml +if [ "$INSTALL_MODE" = "upgrade" ]; then + if [ "$EXISTING_CONFIG_SOURCE" != "$PWD/config.toml" ]; then + cp "$EXISTING_CONFIG_SOURCE" ./config.toml + fi +else + cp conf/app.docker.toml config.toml + set_toml_string_value config.toml "admin" "username" "$ADMIN_USER" + set_toml_string_value config.toml "admin" "password" "$ADMIN_PASS" + set_toml_string_value config.toml "auth" "jwt_secret" "$JWT_SECRET" + set_toml_string_value config.toml "postgres" "password" "$PG_PASS" + if [ "$USE_CN_MIRROR" = true ]; then + sed -i.bak 's|# registry = "memoh.cn"|registry = "memoh.cn"|' config.toml + fi + rm -f config.toml.bak fi -rm -f config.toml.bak -# Use generated config and data dir INSTALL_DIR="$(pwd)" +mkdir -p "$MEMOH_DATA_DIR" +MEMOH_DATA_DIR=$(cd "$MEMOH_DATA_DIR" && pwd) export MEMOH_CONFIG=./config.toml export MEMOH_DATA_DIR -mkdir -p "$MEMOH_DATA_DIR" +export POSTGRES_PASSWORD="${PG_PASS}" # Resolve browser tag and cores from BROWSER_CORE selection case "$BROWSER_CORE" in @@ -268,21 +667,29 @@ if [ "$USE_CN_MIRROR" = true ]; then echo "${GREEN}✓ Using China mainland mirror (memoh.cn)${NC}" fi -echo POSTGRES_PASSWORD="${PG_PASS}" >> .env -echo MEMOH_CONFIG=./config.toml >> .env -echo MEMOH_DATA_DIR="${MEMOH_DATA_DIR}" >> .env -echo USE_SPARSE="${USE_SPARSE}" >> .env -echo BROWSER_TAG="${BROWSER_TAG}" >> .env -echo BROWSER_CORES="${BROWSER_CORES}" >> .env +: > .env +write_env_value "POSTGRES_PASSWORD" "$PG_PASS" +write_env_value "MEMOH_CONFIG" "./config.toml" +write_env_value "MEMOH_DATA_DIR" "$MEMOH_DATA_DIR" +write_env_value "USE_SPARSE" "$USE_SPARSE" +write_env_value "BROWSER_TAG" "$BROWSER_TAG" +write_env_value "BROWSER_CORES" "$BROWSER_CORES" echo "${GREEN}✓ Browser: ${BROWSER_CORE} (image tag: ${BROWSER_TAG})${NC}" +if [ "$INSTALL_MODE" = "reinstall" ]; then + cleanup_existing_installation +fi + echo "" echo "${GREEN}Pulling Docker images...${NC}" $DOCKER compose $COMPOSE_FILES $COMPOSE_PROFILES pull echo "" echo "${GREEN}Starting services (first startup may take a few minutes)...${NC}" -$DOCKER compose $COMPOSE_FILES $COMPOSE_PROFILES up -d +if ! $DOCKER compose $COMPOSE_FILES $COMPOSE_PROFILES up -d; then + show_failure_logs + exit 1 +fi # After fresh clone: copy minimal files to workspace and remove clone directory if [ "$CLONED_FRESH" = true ]; then @@ -316,5 +723,8 @@ echo "📋 Commands:" echo " cd ${INSTALL_DIR} && ${COMPOSE_CMD} ps # Status" echo " cd ${INSTALL_DIR} && ${COMPOSE_CMD} logs -f # Logs" echo " cd ${INSTALL_DIR} && ${COMPOSE_CMD} down # Stop" +if [ "$INSTALL_MODE" != "fresh" ]; then + echo " cd ${INSTALL_DIR} && ${COMPOSE_CMD} down -v # Remove containers and Docker data" +fi echo "" echo "${YELLOW}⏳ First startup may take 1-2 minutes, please be patient.${NC}"