#!/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"