رفع الملفات إلى "/"
هذا الالتزام موجود في:
223
app.py
Normal file
223
app.py
Normal file
@@ -0,0 +1,223 @@
|
||||
import os, sys, json, subprocess
|
||||
import boto3
|
||||
from urllib.parse import urlparse
|
||||
from flask import Flask, request, jsonify
|
||||
from flask_cors import CORS
|
||||
|
||||
app = Flask(__name__, static_folder="../dist", static_url_path="")
|
||||
CORS(app)
|
||||
|
||||
def parse_connection_string(conn_str, default_user='', default_pass=''):
|
||||
if '://' in conn_str:
|
||||
parsed = urlparse(conn_str)
|
||||
return {
|
||||
'host': parsed.hostname,
|
||||
'user': parsed.username or default_user,
|
||||
'pass': parsed.password or default_pass,
|
||||
'db': parsed.path.lstrip('/') if parsed.path else ''
|
||||
}
|
||||
else:
|
||||
return {
|
||||
'host': conn_str,
|
||||
'user': default_user,
|
||||
'pass': default_pass,
|
||||
'db': ''
|
||||
}
|
||||
|
||||
@app.after_request
|
||||
def add_no_cache_headers(response):
|
||||
return response
|
||||
|
||||
# =========================
|
||||
# React
|
||||
# =========================
|
||||
@app.route("/")
|
||||
def frontend():
|
||||
return app.send_static_file("index.html")
|
||||
|
||||
@app.route("/<path:path>")
|
||||
def assets(path):
|
||||
return app.send_static_file(path)
|
||||
|
||||
# =========================
|
||||
# CONFIG
|
||||
# =========================
|
||||
MIGRATORS = {
|
||||
"mysql_mysql": {
|
||||
"script": "mysql_migrator.py",
|
||||
"progress": "mysql_progress.json"
|
||||
},
|
||||
"psql_psql": {
|
||||
"script": "psql_psql_migrator.py",
|
||||
"progress": "pg_progress.json"
|
||||
},
|
||||
"psql_s3": {
|
||||
"script": "pg_s3_migrator.py",
|
||||
"progress": "psql_progress.json"
|
||||
},
|
||||
"s3_s3": {
|
||||
"script": "s3_s3_migrator.py",
|
||||
"progress": "migration_progress.json"
|
||||
}
|
||||
}
|
||||
|
||||
# =========================
|
||||
# GET SCHEMAS
|
||||
# =========================
|
||||
@app.route("/api/get_schemas", methods=["POST"])
|
||||
def get_schemas():
|
||||
data = request.json
|
||||
mtype = data.get("type")
|
||||
|
||||
if mtype == "mysql_mysql":
|
||||
conn = parse_connection_string(data['host'], data['user'], data['pass'])
|
||||
cmd = f"mysql -h {conn['host']} -u {conn['user']} -p{conn['pass']} -e \"SHOW DATABASES;\""
|
||||
try:
|
||||
res = subprocess.check_output(cmd, shell=True).decode().splitlines()
|
||||
ignore = ("Database","information_schema","mysql","performance_schema","sys")
|
||||
return jsonify({"schemas": [r for r in res if r and r not in ignore], "error": None})
|
||||
except Exception as e:
|
||||
return jsonify({"schemas": [], "error": str(e)})
|
||||
|
||||
if mtype == "psql_psql":
|
||||
conn = parse_connection_string(data['host'], data['user'], data['pass'])
|
||||
cmd = (
|
||||
f"PGPASSWORD='{conn['pass']}' psql -h {conn['host']} -U {conn['user']} -t -c "
|
||||
"\"SELECT schema_name FROM information_schema.schemata "
|
||||
"WHERE schema_name NOT IN ('information_schema','pg_catalog');\""
|
||||
)
|
||||
try:
|
||||
res = subprocess.check_output(cmd, shell=True).decode().splitlines()
|
||||
return jsonify({"schemas": [r.strip() for r in res if r.strip()], "error": None})
|
||||
except Exception as e:
|
||||
return jsonify({"schemas": [], "error": str(e)})
|
||||
|
||||
return jsonify([])
|
||||
|
||||
# =========================
|
||||
# GET TABLES
|
||||
# =========================
|
||||
@app.route("/api/get_tables", methods=["POST"])
|
||||
def get_tables():
|
||||
data = request.json
|
||||
mtype = data.get("type")
|
||||
tables = []
|
||||
|
||||
if mtype == "mysql_mysql":
|
||||
conn = parse_connection_string(data['host'], data['user'], data['pass'])
|
||||
for db in data["schemas"]:
|
||||
cmd = f"mysql -h {conn['host']} -u {conn['user']} -p{conn['pass']} -D {db} -e \"SHOW TABLES;\""
|
||||
try:
|
||||
res = subprocess.check_output(cmd, shell=True).decode().splitlines()
|
||||
tables += [f"{db}.{t}" for t in res if not t.startswith("Tables")]
|
||||
except:
|
||||
pass
|
||||
|
||||
if mtype == "psql_psql":
|
||||
conn = parse_connection_string(data['host'], data['user'], data['pass'])
|
||||
for schema in data["schemas"]:
|
||||
cmd = (
|
||||
f"PGPASSWORD='{conn['pass']}' psql -h {conn['host']} -U {conn['user']} -t -c "
|
||||
f"\"SELECT table_name FROM information_schema.tables WHERE table_schema='{schema}';\""
|
||||
)
|
||||
try:
|
||||
res = subprocess.check_output(cmd, shell=True).decode().splitlines()
|
||||
tables += [f"{schema}.{t.strip()}" for t in res if t.strip()]
|
||||
except:
|
||||
pass
|
||||
|
||||
return jsonify({"tables": tables, "error": None})
|
||||
|
||||
# =========================
|
||||
# LIST S3 BUCKETS
|
||||
# =========================
|
||||
@app.route("/api/list_buckets", methods=["POST"])
|
||||
def list_buckets():
|
||||
data = request.json
|
||||
if not data:
|
||||
return jsonify({"success": False, "error": "No JSON data provided"}), 400
|
||||
required_keys = ["AWS_SRC_ACCESS_KEY", "AWS_SRC_SECRET_KEY"]
|
||||
for key in required_keys:
|
||||
if key not in data or not data[key]:
|
||||
return jsonify({"success": False, "error": f"Missing or empty required field: {key}"}), 400
|
||||
try:
|
||||
s3 = boto3.client(
|
||||
"s3",
|
||||
aws_access_key_id=data["AWS_SRC_ACCESS_KEY"],
|
||||
aws_secret_access_key=data["AWS_SRC_SECRET_KEY"],
|
||||
region_name=data.get("AWS_SRC_REGION", "us-east-1")
|
||||
)
|
||||
return jsonify({"success": True, "buckets": [b["Name"] for b in s3.list_buckets()["Buckets"]]})
|
||||
except Exception as e:
|
||||
return jsonify({"success": False, "error": str(e)}), 400
|
||||
|
||||
# =========================
|
||||
# MIGRATE
|
||||
# =========================
|
||||
@app.route("/api/migrate", methods=["POST"])
|
||||
def migrate():
|
||||
data = request.json
|
||||
mtype = data.get("type")
|
||||
|
||||
if mtype not in MIGRATORS:
|
||||
return jsonify({"error": "Invalid migrator type"}), 400
|
||||
|
||||
# Parse connection string for mysql_mysql
|
||||
if mtype == "mysql_mysql":
|
||||
if 'SRC_HOST' in data:
|
||||
conn = parse_connection_string(data['SRC_HOST'], data.get('SRC_USER', ''), data.get('SRC_PASS', ''))
|
||||
data['SRC_HOST'] = conn['host']
|
||||
if conn['user']: data['SRC_USER'] = conn['user']
|
||||
if conn['pass']: data['SRC_PASS'] = conn['pass']
|
||||
if 'DEST_HOST' in data:
|
||||
conn = parse_connection_string(data['DEST_HOST'], data.get('DEST_USER', ''), data.get('DEST_PASS', ''))
|
||||
data['DEST_HOST'] = conn['host']
|
||||
if conn['user']: data['DEST_USER'] = conn['user']
|
||||
if conn['pass']: data['DEST_PASS'] = conn['pass']
|
||||
|
||||
# Parse connection string for psql_psql
|
||||
if mtype == "psql_psql":
|
||||
if 'DB_HOST' in data:
|
||||
conn = parse_connection_string(data['DB_HOST'], data.get('DB_USER', ''), data.get('DB_PASS', ''))
|
||||
data['DB_HOST'] = conn['host']
|
||||
if conn['user']: data['DB_USER'] = conn['user']
|
||||
if conn['pass']: data['DB_PASS'] = conn['pass']
|
||||
if conn['db']: data['DB_NAME'] = conn['db']
|
||||
if 'DEST_HOST' in data:
|
||||
conn = parse_connection_string(data['DEST_HOST'], data.get('DEST_USER', ''), data.get('DEST_PASS', ''))
|
||||
data['DEST_HOST'] = conn['host']
|
||||
if conn['user']: data['DEST_USER'] = conn['user']
|
||||
if conn['pass']: data['DEST_PASS'] = conn['pass']
|
||||
if conn['db']: data['DEST_NAME'] = conn['db']
|
||||
|
||||
env = os.environ.copy()
|
||||
for k, v in data.items():
|
||||
env[k] = ",".join(v) if isinstance(v, list) else str(v)
|
||||
|
||||
progress_file = MIGRATORS[mtype]["progress"]
|
||||
with open(progress_file, "w") as f:
|
||||
json.dump({"percent": 0, "message": "بانتظار البدء...", "status": "idle"}, f)
|
||||
|
||||
subprocess.Popen([sys.executable, MIGRATORS[mtype]["script"]], env=env)
|
||||
|
||||
return jsonify({"status": "started", "type": mtype})
|
||||
|
||||
# =========================
|
||||
# PROGRESS
|
||||
# =========================
|
||||
@app.route("/api/progress/<mtype>")
|
||||
def progress(mtype):
|
||||
if mtype not in MIGRATORS:
|
||||
return jsonify({"error": "Invalid type"}), 400
|
||||
|
||||
try:
|
||||
with open(MIGRATORS[mtype]["progress"]) as f:
|
||||
return jsonify(json.load(f))
|
||||
except:
|
||||
return jsonify({"percent": 0, "message": "بانتظار البدء...", "status": "waiting"})
|
||||
|
||||
# =========================
|
||||
# MAIN
|
||||
# =========================
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=int(os.getenv("PORT", 8001)))
|
||||
13
index.html
Normal file
13
index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Database Migrator</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
4563
package-lock.json
مولّد
Normal file
4563
package-lock.json
مولّد
Normal file
تم حذف اختلاف الملف لأن الملف كبير جداً
تحميل الاختلاف
31
package.json
Normal file
31
package.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "universal-migrator-ui",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint . --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
"@types/react": "^18.2.15",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@vitejs/plugin-react": "^4.0.3",
|
||||
"autoprefixer": "^10.4.23",
|
||||
"eslint": "^8.45.0",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.3",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"vite": "^4.4.5"
|
||||
}
|
||||
}
|
||||
5
requirements.txt
Normal file
5
requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
Flask
|
||||
Flask-CORS
|
||||
boto3
|
||||
gunicorn
|
||||
tailwindcss
|
||||
المرجع في مشكلة جديدة
حظر مستخدم