167 lines
6.5 KiB
Bash
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"
|