deerflow-code/offline-backend-20260512/switch_database_to_mysql.sh
2026-09-07 18:24:55 +08:00

167 lines
6.5 KiB
Bash

#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
IMAGE_TAG="${IMAGE_TAG:-deerflow-backend-offline:20260505}"
CONTAINER_NAME="${CONTAINER_NAME:-deerflow-backend}"
IMAGE_TAR="${IMAGE_TAR:-${SCRIPT_DIR}/deerflow-backend-offline-image-20260505.tar}"
MYSQL_DRIVER_IMPORT="${MYSQL_DRIVER_IMPORT:-asyncmy}"
MYSQL_PIP_PACKAGES="${MYSQL_PIP_PACKAGES:-asyncmy==0.2.10}"
CREATE_DATABASE="${CREATE_DATABASE:-1}"
DROP_EMPTY_TARGET_SCHEMA="${DROP_EMPTY_TARGET_SCHEMA:-1}"
TRUNCATE_TARGET="${TRUNCATE_TARGET:-0}"
SQLITE_DB_HOST="${SQLITE_DB_HOST:-${SCRIPT_DIR}/data/data/deerflow.db}"
SQLITE_DB_CONTAINER="${SQLITE_DB_CONTAINER:-/app/backend/.deer-flow/data/deerflow.db}"
VENDOR_DIR_CONTAINER="${VENDOR_DIR_CONTAINER:-/app/backend/.deer-flow/python-vendor}"
die() {
echo "ERROR: $*" >&2
exit 1
}
require_cmd() {
command -v "$1" >/dev/null 2>&1 || die "command not found: $1"
}
require_file() {
[ -f "$1" ] || die "required file not found: $1"
}
read_env_value() {
local key="$1"
[ -f "${SCRIPT_DIR}/.env" ] || return 0
grep -E "^[[:space:]]*${key}=" "${SCRIPT_DIR}/.env" 2>/dev/null | tail -n 1 | sed 's/^[^=]*=//'
}
upsert_env_key() {
local key="$1"
local value="$2"
local tmp
tmp="$(mktemp "${SCRIPT_DIR}/.env.mysql-switch.XXXXXX")"
grep -Ev "^[[:space:]]*${key}=" "${SCRIPT_DIR}/.env" >"${tmp}" || true
printf '%s=%s\n' "${key}" "${value}" >>"${tmp}"
mv "${tmp}" "${SCRIPT_DIR}/.env"
}
require_cmd docker
require_file "${SCRIPT_DIR}/.env"
require_file "${SCRIPT_DIR}/config.yaml"
require_file "${SCRIPT_DIR}/backend/pyproject.toml"
require_file "${SCRIPT_DIR}/backend/app/gateway/app.py"
require_file "${SCRIPT_DIR}/backend/scripts/migrate_business_sqlite_to_mysql.py"
require_file "${SCRIPT_DIR}/backend/scripts/update_database_config.py"
require_file "${SCRIPT_DIR}/extensions_config.json"
require_file "${SCRIPT_DIR}/skill_evolution.json"
[ -d "${SCRIPT_DIR}/skills" ] || die "required directory not found: ${SCRIPT_DIR}/skills"
[ -d "${SCRIPT_DIR}/wheelhouse" ] || die "required directory not found: ${SCRIPT_DIR}/wheelhouse"
MYSQL_DATABASE_URL="${MYSQL_DATABASE_URL:-$(read_env_value MYSQL_DATABASE_URL)}"
[ -n "${MYSQL_DATABASE_URL}" ] || die "MYSQL_DATABASE_URL is required. Example: MYSQL_DATABASE_URL='mysql+asyncmy://user:pass@host:3306/deerflow?charset=utf8mb4' bash switch_database_to_mysql.sh"
if docker ps --format '{{.Names}}' | awk -v name="${CONTAINER_NAME}" '$0 == name { found=1 } END { exit(found ? 0 : 1) }'; then
die "container ${CONTAINER_NAME} is running. Stop the backend first: bash stop_backend.sh"
fi
require_file "${SQLITE_DB_HOST}"
mkdir -p "${SCRIPT_DIR}/data/backups"
timestamp="$(date +%Y%m%d_%H%M%S)"
backup_dir="${SCRIPT_DIR}/data/backups/mysql-switch-${timestamp}"
mkdir -p "${backup_dir}"
cp "${SCRIPT_DIR}/config.yaml" "${backup_dir}/config.yaml.bak"
cp "${SCRIPT_DIR}/.env" "${backup_dir}/env.bak"
cp "${SQLITE_DB_HOST}" "${backup_dir}/deerflow.db.bak"
upsert_env_key "MYSQL_DATABASE_URL" "${MYSQL_DATABASE_URL}"
if ! docker image inspect "${IMAGE_TAG}" >/dev/null 2>&1; then
require_file "${IMAGE_TAR}"
echo "Loading Docker image from ${IMAGE_TAR}"
docker load -i "${IMAGE_TAR}"
fi
if docker ps -a --format '{{.Names}}' | awk -v name="${CONTAINER_NAME}-mysql-switch" '$0 == name { found=1 } END { exit(found ? 0 : 1) }'; then
docker rm -f "${CONTAINER_NAME}-mysql-switch" >/dev/null
fi
echo "Backups written to: ${backup_dir}"
echo "Migrating business tables to MySQL..."
docker run --rm \
--name "${CONTAINER_NAME}-mysql-switch" \
--env-file "${SCRIPT_DIR}/.env" \
-e MYSQL_DATABASE_URL="${MYSQL_DATABASE_URL}" \
-e MYSQL_DRIVER_IMPORT="${MYSQL_DRIVER_IMPORT}" \
-e MYSQL_PIP_PACKAGES="${MYSQL_PIP_PACKAGES}" \
-e CREATE_DATABASE="${CREATE_DATABASE}" \
-e DROP_EMPTY_TARGET_SCHEMA="${DROP_EMPTY_TARGET_SCHEMA}" \
-e TRUNCATE_TARGET="${TRUNCATE_TARGET}" \
-e VENDOR_DIR_CONTAINER="${VENDOR_DIR_CONTAINER}" \
-e DEER_FLOW_PROJECT_ROOT=/app \
-e DEER_FLOW_HOME=/app/backend/.deer-flow \
-e DEER_FLOW_CONFIG_PATH=/app/config.yaml \
-e DEER_FLOW_EXTENSIONS_CONFIG_PATH=/app/extensions_config.json \
-e DEER_FLOW_SKILL_EVOLUTION_CONFIG_PATH=/app/skill_evolution.json \
-e DEER_FLOW_SKILLS_PATH=/app/skills \
-e UV_LINK_MODE=copy \
-v "${SCRIPT_DIR}/backend/app:/app/backend/app" \
-v "${SCRIPT_DIR}/backend/packages:/app/backend/packages" \
-v "${SCRIPT_DIR}/backend/scripts:/app/backend/scripts" \
-v "${SCRIPT_DIR}/backend/pyproject.toml:/app/backend/pyproject.toml:ro" \
-v "${SCRIPT_DIR}/backend/uv.lock:/app/backend/uv.lock:ro" \
-v "${SCRIPT_DIR}/backend/langgraph.json:/app/backend/langgraph.json:ro" \
-v "${SCRIPT_DIR}/config.yaml:/app/config.yaml" \
-v "${SCRIPT_DIR}/extensions_config.json:/app/extensions_config.json:ro" \
-v "${SCRIPT_DIR}/skill_evolution.json:/app/skill_evolution.json:ro" \
-v "${SCRIPT_DIR}/skills:/app/skills:ro" \
-v "${SCRIPT_DIR}/wheelhouse:/app/wheelhouse:ro" \
-v "${SCRIPT_DIR}/data:/app/backend/.deer-flow" \
"${IMAGE_TAG}" \
sh -c '
set -eu
cd /app/backend
mkdir -p "${VENDOR_DIR_CONTAINER:-/app/backend/.deer-flow/python-vendor}"
export PYTHONPATH=".:${VENDOR_DIR_CONTAINER:-/app/backend/.deer-flow/python-vendor}"
if ! uv run --no-sync python -c "import ${MYSQL_DRIVER_IMPORT:-asyncmy}" >/dev/null 2>&1; then
echo "Installing ${MYSQL_PIP_PACKAGES:-asyncmy==0.2.10} into ${VENDOR_DIR_CONTAINER:-/app/backend/.deer-flow/python-vendor}"
uv pip install \
--target "${VENDOR_DIR_CONTAINER:-/app/backend/.deer-flow/python-vendor}" \
--no-index \
--find-links /app/wheelhouse \
${MYSQL_PIP_PACKAGES:-asyncmy==0.2.10}
fi
MIGRATION_FLAGS=""
if [ "${CREATE_DATABASE:-1}" = "1" ]; then
MIGRATION_FLAGS="${MIGRATION_FLAGS} --create-database"
fi
if [ "${DROP_EMPTY_TARGET_SCHEMA:-1}" = "1" ]; then
MIGRATION_FLAGS="${MIGRATION_FLAGS} --drop-empty-target-schema"
fi
if [ "${TRUNCATE_TARGET:-0}" = "1" ]; then
MIGRATION_FLAGS="${MIGRATION_FLAGS} --truncate-target"
fi
uv run --no-sync python scripts/migrate_business_sqlite_to_mysql.py \
--sqlite "'"${SQLITE_DB_CONTAINER}"'" \
--mysql-url "${MYSQL_DATABASE_URL}" \
--vendor-dir "${VENDOR_DIR_CONTAINER:-/app/backend/.deer-flow/python-vendor}" \
${MIGRATION_FLAGS}
uv run --no-sync python scripts/update_database_config.py \
--config /app/config.yaml \
--mysql-url-env MYSQL_DATABASE_URL \
--checkpointer-sqlite "'"${SQLITE_DB_CONTAINER}"'"
'
echo
echo "MySQL switch completed."
echo " Config updated: ${SCRIPT_DIR}/config.yaml"
echo " Env updated: ${SCRIPT_DIR}/.env"
echo " Backup dir: ${backup_dir}"
echo
echo "Start backend with the unchanged script:"
echo " bash start_backend.sh"