fix(install): handle dirty one-click upgrades (#401)

* fix(install): handle dirty one-click upgrades

* fix(install): address dirty state review feedback
This commit is contained in:
Chrys
2026-04-26 17:20:18 +08:00
committed by GitHub
parent 132aa71d01
commit 8b9ed92a11
4 changed files with 527 additions and 52 deletions
+25 -5
View File
@@ -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 |
+13 -9
View File
@@ -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.
+41
View File
@@ -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 {
+448 -38
View File
@@ -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}"