AI Update: دیپلوی با اف تی پی باید به این صورت انجام شود نسخه ی موجود د

This commit is contained in:
Antigravity Bot
2026-08-30 21:37:58 +03:30
parent 5ab0a40b03
commit 5bfa479236
3 changed files with 188 additions and 36 deletions
+48 -14
View File
@@ -401,9 +401,12 @@ async def build_ftp_menu(chat_id: int) -> tuple[str, InlineKeyboardMarkup]:
if is_fa:
keyboard = [
[
InlineKeyboardButton("🚀 دیپلوی روی FTP (Clean Deploy)", callback_data="btn_ftp_deploy"),
InlineKeyboardButton("🚀 دیپلوی نسخه پروداکشن روی FTP", callback_data="btn_ftp_deploy"),
InlineKeyboardButton("🔍 تست اتصال FTP", callback_data="btn_ftp_test"),
],
[
InlineKeyboardButton("🌿 انتقال از dev به پروداکشن (Publish)", callback_data="btn_git_publish"),
],
[
InlineKeyboardButton("🌐 ویرایش هاست/پورت", callback_data="ftp_set:host"),
InlineKeyboardButton("👤 ویرایش نام کاربری", callback_data="ftp_set:user"),
@@ -424,9 +427,12 @@ async def build_ftp_menu(chat_id: int) -> tuple[str, InlineKeyboardMarkup]:
else:
keyboard = [
[
InlineKeyboardButton("🚀 Deploy via FTP (Clean)", callback_data="btn_ftp_deploy"),
InlineKeyboardButton("🚀 Deploy Production to FTP", callback_data="btn_ftp_deploy"),
InlineKeyboardButton("🔍 Test FTP Connection", callback_data="btn_ftp_test"),
],
[
InlineKeyboardButton("🌿 Publish dev to Production", callback_data="btn_git_publish"),
],
[
InlineKeyboardButton("🌐 Edit Host/Port", callback_data="ftp_set:host"),
InlineKeyboardButton("👤 Edit Username", callback_data="ftp_set:user"),
@@ -4711,11 +4717,11 @@ async def callback_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
elif data == "btn_git_publish":
await query.answer("🚀 در حال انتشار به پروداکشن..." if is_fa else "🚀 Publishing to production...")
await query.answer("🚀 در حال انتقال تغییرات از dev به production..." if is_fa else "🚀 Publishing to production...")
if curr_proj:
ok, res_msg, _ = await git_manager.git_publish(curr_proj.workspace, message="Publish via bot button", repo_name=curr_proj.name)
ok, res_msg, _ = await git_manager.git_publish(curr_proj.workspace, message="Publish dev to production via bot button", repo_name=curr_proj.name)
try:
await query.answer("✅ انتشار به پروداکشن با موفقیت انجام شد!" if ok else "⚠️ خطا در انتشار", show_alert=not ok)
await query.message.reply_html(res_msg, disable_web_page_preview=True)
except Exception:
pass
text, markup = await build_git_menu(chat_id)
@@ -4864,14 +4870,10 @@ async def callback_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
elif data == "btn_ftp_deploy":
await query.answer("🚀 در حال استقرار هوشمند روی FTP..." if is_fa else "🚀 Deploying to FTP...")
await query.answer("🚀 در حال استقرار نسخه پروداکشن روی FTP..." if is_fa else "🚀 Deploying production to FTP...")
if curr_proj and curr_proj.ftp_host:
# Auto publish before deploy
try:
await git_manager.git_publish(curr_proj.workspace, message="Auto-publish before UI FTP deploy", repo_name=curr_proj.name)
except Exception:
pass
status_msg = await query.message.reply_html("🚀 <i>در حال آماده‌سازی و ارسال نسخه شاخه production به سرور FTP...</i>" if is_fa else "🚀 <i>Preparing and uploading production branch files to FTP...</i>")
res = await ftp_manager.deploy_project(
workspace_path=curr_proj.workspace,
host=curr_proj.ftp_host,
@@ -4880,18 +4882,50 @@ async def callback_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
password=curr_proj.ftp_password or "",
remote_path=curr_proj.ftp_path or "/",
tls=curr_proj.ftp_tls,
branch="production",
)
if res.get("success"):
files_up = res.get("files_uploaded", 0)
files_skip = res.get("files_skipped", 0)
dur = res.get("duration", 0)
b_trans = res.get("bytes_transferred", 0)
size_mb = f"{b_trans / (1024 * 1024):.2f} MB" if b_trans > 1024 * 1024 else f"{b_trans / 1024:.1f} KB"
report = (
f"✅ <b>استقرار روی FTP با موفقیت انجام شد!</b>\n\n"
f"• 📁 <b>پروژه:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• 🌿 <b>شاخه سورس دیپلوی:</b> <code>production</code>\n"
f"• 🌐 <b>سرور مقصد:</b> <code>{escape_html(curr_proj.ftp_host)}:{curr_proj.ftp_port}</code>\n"
f"• 📂 <b>مسیر هاست:</b> <code>{escape_html(curr_proj.ftp_path or '/')}</code>\n"
f"• 📦 <b>تعداد فایل‌های آپلود شده:</b> {files_up}\n"
f"• 🧹 <b>فایل‌های زائد و فیلتر شده:</b> {files_skip}\n"
f"• 📊 <b>حجم کل منتقل شده:</b> {size_mb}\n"
f"• ⏱️ <b>مدت زمان دیپلوی:</b> {dur} ثانیه"
) if is_fa else (
f"✅ <b>FTP Deployment Completed Successfully!</b>\n\n"
f"• 📁 <b>Project:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• 🌿 <b>Source Branch:</b> <code>production</code>\n"
f"• 🌐 <b>Target Server:</b> <code>{escape_html(curr_proj.ftp_host)}:{curr_proj.ftp_port}</code>\n"
f"• 📂 <b>Remote Path:</b> <code>{escape_html(curr_proj.ftp_path or '/')}</code>\n"
f"• 📦 <b>Files Uploaded:</b> {files_up}\n"
f"• 🧹 <b>Excluded/Skipped:</b> {files_skip}\n"
f"• 📊 <b>Transferred:</b> {size_mb}\n"
f"• ⏱️ <b>Duration:</b> {dur}s"
)
try:
await query.answer(f"✅ دیپلوی با موفقیت انجام شد ({files_up} فایل در {dur} ثانیه)", show_alert=True)
await status_msg.edit_text(report, parse_mode=constants.ParseMode.HTML)
except Exception:
pass
else:
err = res.get("error", "Error")
err_report = (
f"❌ <b>خطا در استقرار روی FTP:</b>\n\n"
f"• ⚠️ <b>پیام خطا:</b> {escape_html(str(err))}\n"
f"• 💡 <i>پیشنهاد: ابتدا دکمه تست اتصال را بزنید یا مشخصات هاست و مسیر را بررسی کنید.</i>"
) if is_fa else f"❌ <b>FTP Deployment Error:</b>\n{escape_html(str(err))}"
try:
await query.answer(f"❌ خطا در دیپلوی: {err}"[:200], show_alert=True)
await status_msg.edit_text(err_report, parse_mode=constants.ParseMode.HTML)
except Exception:
pass
else:
+52 -4
View File
@@ -8,6 +8,9 @@ from pathlib import Path
from typing import Optional, Dict, Any, List, Tuple, Set
import ftplib
import socket
import subprocess
import tempfile
import shutil
logger = logging.getLogger("AGYFTPManager")
@@ -227,11 +230,16 @@ class FTPManager:
tls: bool = False,
custom_excludes: Optional[List[str]] = None,
dry_run: bool = False,
branch: str = "production",
) -> Dict[str, Any]:
"""
Deploys files from workspace_path to the remote FTP server cleanly.
Deploys files from the specified git branch (default: production) to the remote FTP server cleanly.
Extracts files directly from the git branch to ensure exact production code is deployed.
Skips all temporary, git, session, and junk files.
"""
import tempfile
import shutil
ws = Path(workspace_path).expanduser().resolve()
if not ws.exists() or not ws.is_dir():
return {
@@ -249,8 +257,42 @@ class FTPManager:
uploaded_files: List[str] = []
skipped_files: List[str] = []
total_bytes = 0
temp_export_dir = None
try:
# 1. Determine source directory: if git exists, export the exact production branch
source_dir = ws
if (ws / ".git").exists():
try:
# Check if branch exists
branches_proc = subprocess.run(
["git", "branch", "--list", branch],
cwd=str(ws),
capture_output=True,
text=True,
)
target_branch = branch if branch in branches_proc.stdout else "HEAD"
temp_export_dir = Path(tempfile.mkdtemp(prefix="ftp_deploy_prod_"))
# Use git archive to cleanly extract files from the target branch
archive_proc = subprocess.Popen(
["git", "archive", target_branch],
cwd=str(ws),
stdout=subprocess.PIPE,
)
tar_proc = subprocess.Popen(
["tar", "-x", "-C", str(temp_export_dir)],
stdin=archive_proc.stdout,
)
archive_proc.stdout.close()
tar_proc.communicate()
if tar_proc.returncode == 0:
source_dir = temp_export_dir
except Exception as ge:
logger.warning(f"Git archive export failed for {branch} (falling back to workspace): {ge}")
source_dir = ws
ftp = self._create_ftp_client(host.strip(), int(port), user.strip(), password, tls, timeout=20)
# Navigate or create remote root path
@@ -280,11 +322,11 @@ class FTPManager:
ensure_remote_dir(target_root)
# Scan workspace
# Scan source directory
all_files_to_upload: List[Tuple[Path, str]] = [] # (local_path, rel_path)
for root, dirs, files in os.walk(str(ws)):
rel_dir = os.path.relpath(root, str(ws))
for root, dirs, files in os.walk(str(source_dir)):
rel_dir = os.path.relpath(root, str(source_dir))
if rel_dir == ".":
rel_dir = ""
@@ -359,6 +401,12 @@ class FTPManager:
"duration": round(time.time() - start_time, 2),
"uploaded_list": uploaded_files,
}
finally:
if temp_export_dir and Path(temp_export_dir).exists():
try:
shutil.rmtree(temp_export_dir, ignore_errors=True)
except Exception:
pass
return await asyncio.to_thread(_run_deploy)
File diff suppressed because one or more lines are too long