Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
390b938c05 | ||
|
|
ee474774e1 | ||
|
|
5bfa479236 | ||
|
|
5ab0a40b03 | ||
|
|
ab4764f521 | ||
|
|
fb5e5ff404 | ||
|
|
314c23f4c9 | ||
|
|
78fbf36c38 | ||
|
|
8568e61cf0 | ||
|
|
7a26378a09 |
+1
-1
Submodule projects/232801087/myapp updated: ded79a9a9e...a6bf56fafb
Submodule
+1
Submodule projects/78649634/bildirchin added at e109f5d777
Submodule projects/78649634/copykar updated: f8468cdd1a...6e4b7e364d
Submodule projects/78649634/gasemi_tier updated: d27520ec0f...aca18b7229
@@ -360,6 +360,13 @@ class SessionManager:
|
|||||||
"created_at": p_obj.created_at,
|
"created_at": p_obj.created_at,
|
||||||
"description": p_obj.description,
|
"description": p_obj.description,
|
||||||
"conversation_titles": p_obj.conversation_titles,
|
"conversation_titles": p_obj.conversation_titles,
|
||||||
|
"active_branch": p_obj.active_branch,
|
||||||
|
"ftp_host": p_obj.ftp_host,
|
||||||
|
"ftp_port": p_obj.ftp_port,
|
||||||
|
"ftp_user": p_obj.ftp_user,
|
||||||
|
"ftp_password": p_obj.ftp_password,
|
||||||
|
"ftp_path": p_obj.ftp_path,
|
||||||
|
"ftp_tls": p_obj.ftp_tls,
|
||||||
}
|
}
|
||||||
curr = self.get_current_project(sess.chat_id)
|
curr = self.get_current_project(sess.chat_id)
|
||||||
data[str(k)] = {
|
data[str(k)] = {
|
||||||
|
|||||||
+242
-20
@@ -109,6 +109,9 @@ def get_lang_display(lang_code_or_name: str) -> str:
|
|||||||
norm = LANG_ALIASES.get(norm, norm)
|
norm = LANG_ALIASES.get(norm, norm)
|
||||||
return AVAILABLE_LANGUAGES.get(norm, lang_code_or_name)
|
return AVAILABLE_LANGUAGES.get(norm, lang_code_or_name)
|
||||||
|
|
||||||
|
# Global active user input states for interactive prompts (e.g. FTP configuration wizards)
|
||||||
|
USER_INPUT_STATES: dict[int, dict] = {}
|
||||||
|
|
||||||
# Logging configuration
|
# Logging configuration
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||||
@@ -389,34 +392,61 @@ async def build_ftp_menu(chat_id: int) -> tuple[str, InlineKeyboardMarkup]:
|
|||||||
user=curr_proj.ftp_user,
|
user=curr_proj.ftp_user,
|
||||||
path=curr_proj.ftp_path,
|
path=curr_proj.ftp_path,
|
||||||
tls=curr_proj.ftp_tls,
|
tls=curr_proj.ftp_tls,
|
||||||
|
password=curr_proj.ftp_password,
|
||||||
lang=session.language,
|
lang=session.language,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
tls_toggle_label = f"🔒 پروتکل امن: {'روشن ✅' if curr_proj.ftp_tls else 'خاموش ❌'}" if is_fa else f"🔒 FTPS/TLS: {'ON ✅' if curr_proj.ftp_tls else 'OFF ❌'}"
|
||||||
|
|
||||||
if is_fa:
|
if is_fa:
|
||||||
keyboard = [
|
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("🔍 تست اتصال FTP", callback_data="btn_ftp_test"),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
InlineKeyboardButton("🐙 منوی گیت و برنچها", callback_data="btn_git_menu"),
|
InlineKeyboardButton("🌿 انتقال از dev به پروداکشن (Publish)", callback_data="btn_git_publish"),
|
||||||
InlineKeyboardButton("📁 مدیریت پروژهها", callback_data="proj_menu"),
|
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
InlineKeyboardButton("🌐 ویرایش هاست/پورت", callback_data="ftp_set:host"),
|
||||||
|
InlineKeyboardButton("👤 ویرایش نام کاربری", callback_data="ftp_set:user"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton("🔑 ویرایش رمز عبور", callback_data="ftp_set:pass"),
|
||||||
|
InlineKeyboardButton("📂 ویرایش مسیر مقصد", callback_data="ftp_set:path"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(tls_toggle_label, callback_data="ftp_toggle_tls"),
|
||||||
|
InlineKeyboardButton("🗑️ پاکسازی اطلاعات", callback_data="ftp_clear_conf"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton("🐙 منوی گیت و برنچها", callback_data="btn_git_menu"),
|
||||||
InlineKeyboardButton("🏠 منوی اصلی", callback_data="btn_dashboard"),
|
InlineKeyboardButton("🏠 منوی اصلی", callback_data="btn_dashboard"),
|
||||||
],
|
],
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
keyboard = [
|
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("🔍 Test FTP Connection", callback_data="btn_ftp_test"),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
InlineKeyboardButton("🐙 Git & Branches Menu", callback_data="btn_git_menu"),
|
InlineKeyboardButton("🌿 Publish dev to Production", callback_data="btn_git_publish"),
|
||||||
InlineKeyboardButton("📁 Projects Menu", callback_data="proj_menu"),
|
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
InlineKeyboardButton("🌐 Edit Host/Port", callback_data="ftp_set:host"),
|
||||||
|
InlineKeyboardButton("👤 Edit Username", callback_data="ftp_set:user"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton("🔑 Edit Password", callback_data="ftp_set:pass"),
|
||||||
|
InlineKeyboardButton("📂 Edit Remote Path", callback_data="ftp_set:path"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(tls_toggle_label, callback_data="ftp_toggle_tls"),
|
||||||
|
InlineKeyboardButton("🗑️ Clear Config", callback_data="ftp_clear_conf"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton("🐙 Git & Branches Menu", callback_data="btn_git_menu"),
|
||||||
InlineKeyboardButton("🏠 Main Dashboard", callback_data="btn_dashboard"),
|
InlineKeyboardButton("🏠 Main Dashboard", callback_data="btn_dashboard"),
|
||||||
],
|
],
|
||||||
]
|
]
|
||||||
@@ -4687,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)
|
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
|
||||||
|
|
||||||
elif data == "btn_git_publish":
|
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:
|
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:
|
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:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
text, markup = await build_git_menu(chat_id)
|
text, markup = await build_git_menu(chat_id)
|
||||||
@@ -4707,6 +4737,96 @@ async def callback_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||||||
except Exception:
|
except Exception:
|
||||||
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
|
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
|
||||||
|
|
||||||
|
elif data.startswith("ftp_set:"):
|
||||||
|
field_name = data.split(":")[1]
|
||||||
|
if not curr_proj:
|
||||||
|
await query.answer("⚠️ پروژهای یافت نشد." if is_fa else "⚠️ No project found.", show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
USER_INPUT_STATES[chat_id] = {
|
||||||
|
"type": "ftp_set",
|
||||||
|
"field": field_name,
|
||||||
|
"project": curr_proj.name,
|
||||||
|
}
|
||||||
|
|
||||||
|
field_prompts = {
|
||||||
|
"host": (
|
||||||
|
"🌐 <b>لطفاً آدرس هاست یا سرور FTP را ارسال کنید:</b>\n\n"
|
||||||
|
"<i>مثال:</i> <code>ftp.mysite.com</code> یا <code>185.120.30.40:21</code>"
|
||||||
|
) if is_fa else (
|
||||||
|
"🌐 <b>Please enter the FTP Host address:</b>\n\n"
|
||||||
|
"<i>Example:</i> <code>ftp.mysite.com</code> or <code>185.120.30.40:21</code>"
|
||||||
|
),
|
||||||
|
"user": (
|
||||||
|
"👤 <b>لطفاً نام کاربری (Username) اتصال FTP را ارسال کنید:</b>\n\n"
|
||||||
|
"<i>مثال:</i> <code>deployer@mysite.com</code> یا <code>ftpuser</code>"
|
||||||
|
) if is_fa else (
|
||||||
|
"👤 <b>Please enter the FTP Username:</b>\n\n"
|
||||||
|
"<i>Example:</i> <code>deployer@mysite.com</code> or <code>ftpuser</code>"
|
||||||
|
),
|
||||||
|
"pass": (
|
||||||
|
"🔑 <b>لطفاً رمز عبور (Password) اتصال FTP را ارسال کنید:</b>\n\n"
|
||||||
|
"<i>(پیام پس از ثبت، جهت امنیت حذف یا پوشانده میشود)</i>"
|
||||||
|
) if is_fa else (
|
||||||
|
"🔑 <b>Please enter the FTP Password:</b>\n\n"
|
||||||
|
"<i>(Message will be stored securely)</i>"
|
||||||
|
),
|
||||||
|
"path": (
|
||||||
|
"📂 <b>لطفاً مسیر پوشه ریموت مقصد روی هاست را ارسال کنید:</b>\n\n"
|
||||||
|
"<i>مثال:</i> <code>/public_html</code> یا <code>/domains/site.com/public_html</code>"
|
||||||
|
) if is_fa else (
|
||||||
|
"📂 <b>Please enter the remote target path:</b>\n\n"
|
||||||
|
"<i>Example:</i> <code>/public_html</code>"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt_text = field_prompts.get(field_name, "لطفاً مقدار جدید را ارسال کنید:")
|
||||||
|
cancel_kb = InlineKeyboardMarkup([[InlineKeyboardButton("❌ انصراف و بازگشت" if is_fa else "❌ Cancel", callback_data="btn_ftp_menu")]])
|
||||||
|
await query.edit_message_text(prompt_text, parse_mode=constants.ParseMode.HTML, reply_markup=cancel_kb)
|
||||||
|
|
||||||
|
elif data == "ftp_toggle_tls":
|
||||||
|
if curr_proj:
|
||||||
|
curr_proj.ftp_tls = not curr_proj.ftp_tls
|
||||||
|
session_manager.save()
|
||||||
|
status_str = "روشن (FTPS/TLS) 🔒" if curr_proj.ftp_tls else "خاموش (FTP معمولی) 🔓"
|
||||||
|
await query.answer(f"✅ پروتکل امن: {status_str}")
|
||||||
|
text, markup = await build_ftp_menu(chat_id)
|
||||||
|
try:
|
||||||
|
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup, disable_web_page_preview=True)
|
||||||
|
except Exception:
|
||||||
|
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
|
||||||
|
|
||||||
|
elif data == "ftp_clear_conf":
|
||||||
|
confirm_text = (
|
||||||
|
"🗑️ <b>آیا از پاکسازی کامل اطلاعات FTP این پروژه اطمینان دارید؟</b>\n\n"
|
||||||
|
"تمام مشخصات سرور، کاربر و رمز عبور حذف خواهند شد."
|
||||||
|
) if is_fa else (
|
||||||
|
"🗑️ <b>Are you sure you want to clear FTP settings for this project?</b>"
|
||||||
|
)
|
||||||
|
keyboard = [
|
||||||
|
[
|
||||||
|
InlineKeyboardButton("💥 بله، پاک شود" if is_fa else "💥 Yes, Clear", callback_data="ftp_clear_do"),
|
||||||
|
InlineKeyboardButton("❌ انصراف" if is_fa else "❌ Cancel", callback_data="btn_ftp_menu"),
|
||||||
|
]
|
||||||
|
]
|
||||||
|
await query.edit_message_text(confirm_text, parse_mode=constants.ParseMode.HTML, reply_markup=InlineKeyboardMarkup(keyboard))
|
||||||
|
|
||||||
|
elif data == "ftp_clear_do":
|
||||||
|
if curr_proj:
|
||||||
|
curr_proj.ftp_host = None
|
||||||
|
curr_proj.ftp_port = 21
|
||||||
|
curr_proj.ftp_user = None
|
||||||
|
curr_proj.ftp_password = None
|
||||||
|
curr_proj.ftp_path = "/"
|
||||||
|
curr_proj.ftp_tls = False
|
||||||
|
session_manager.save()
|
||||||
|
await query.answer("🗑️ اطلاعات FTP با موفقیت پاک شد." if is_fa else "🗑️ FTP settings cleared.")
|
||||||
|
text, markup = await build_ftp_menu(chat_id)
|
||||||
|
try:
|
||||||
|
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup, disable_web_page_preview=True)
|
||||||
|
except Exception:
|
||||||
|
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
|
||||||
|
|
||||||
elif data == "btn_ftp_test":
|
elif data == "btn_ftp_test":
|
||||||
await query.answer("🔍 در حال بررسی اتصال FTP..." if is_fa else "🔍 Testing FTP connection...")
|
await query.answer("🔍 در حال بررسی اتصال FTP..." if is_fa else "🔍 Testing FTP connection...")
|
||||||
if curr_proj and curr_proj.ftp_host:
|
if curr_proj and curr_proj.ftp_host:
|
||||||
@@ -4718,9 +4838,24 @@ async def callback_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||||||
path=curr_proj.ftp_path or "/",
|
path=curr_proj.ftp_path or "/",
|
||||||
tls=curr_proj.ftp_tls,
|
tls=curr_proj.ftp_tls,
|
||||||
)
|
)
|
||||||
|
icon = "✅" if test_ok else "❌"
|
||||||
|
res_card = (
|
||||||
|
f"{icon} <b>نتیجه تست اتصال FTP:</b>\n\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_user or '(بدون نام کاربری)')}</code>\n"
|
||||||
|
f"• 📂 <b>مسیر:</b> <code>{escape_html(curr_proj.ftp_path or '/')}</code>\n"
|
||||||
|
f"• 🔒 <b>پروتکل:</b> {'FTPS/TLS امن 🔒' if curr_proj.ftp_tls else 'FTP معمولی'}\n\n"
|
||||||
|
f"📋 <b>گزارش ارتباط:</b>\n{test_msg}"
|
||||||
|
) if is_fa else (
|
||||||
|
f"{icon} <b>FTP Connection Test Result:</b>\n\n"
|
||||||
|
f"• 🌐 <b>Host:</b> <code>{escape_html(curr_proj.ftp_host)}:{curr_proj.ftp_port}</code>\n"
|
||||||
|
f"• 👤 <b>User:</b> <code>{escape_html(curr_proj.ftp_user or '(none)')}</code>\n"
|
||||||
|
f"• 📂 <b>Path:</b> <code>{escape_html(curr_proj.ftp_path or '/')}</code>\n"
|
||||||
|
f"• 🔒 <b>Protocol:</b> {'Secure FTPS/TLS 🔒' if curr_proj.ftp_tls else 'Standard FTP'}\n\n"
|
||||||
|
f"📋 <b>Status:</b>\n{test_msg}"
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
alert_text = ("✅ اتصال FTP برقرار است" if test_ok else "❌ خطا در اتصال FTP") + f"\n{test_msg}"
|
await query.message.reply_html(res_card, disable_web_page_preview=True)
|
||||||
await query.answer(alert_text[:200], show_alert=True)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
@@ -4735,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)
|
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
|
||||||
|
|
||||||
elif data == "btn_ftp_deploy":
|
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:
|
if curr_proj and curr_proj.ftp_host:
|
||||||
# Auto publish before deploy
|
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>")
|
||||||
try:
|
|
||||||
await git_manager.git_publish(curr_proj.workspace, message="Auto-publish before UI FTP deploy", repo_name=curr_proj.name)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
res = await ftp_manager.deploy_project(
|
res = await ftp_manager.deploy_project(
|
||||||
workspace_path=curr_proj.workspace,
|
workspace_path=curr_proj.workspace,
|
||||||
host=curr_proj.ftp_host,
|
host=curr_proj.ftp_host,
|
||||||
@@ -4751,18 +4882,50 @@ async def callback_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||||||
password=curr_proj.ftp_password or "",
|
password=curr_proj.ftp_password or "",
|
||||||
remote_path=curr_proj.ftp_path or "/",
|
remote_path=curr_proj.ftp_path or "/",
|
||||||
tls=curr_proj.ftp_tls,
|
tls=curr_proj.ftp_tls,
|
||||||
|
branch="production",
|
||||||
)
|
)
|
||||||
|
|
||||||
if res.get("success"):
|
if res.get("success"):
|
||||||
files_up = res.get("files_uploaded", 0)
|
files_up = res.get("files_uploaded", 0)
|
||||||
|
files_skip = res.get("files_skipped", 0)
|
||||||
dur = res.get("duration", 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:
|
try:
|
||||||
await query.answer(f"✅ دیپلوی با موفقیت انجام شد ({files_up} فایل در {dur} ثانیه)", show_alert=True)
|
await status_msg.edit_text(report, parse_mode=constants.ParseMode.HTML)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
err = res.get("error", "Error")
|
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:
|
try:
|
||||||
await query.answer(f"❌ خطا در دیپلوی: {err}"[:200], show_alert=True)
|
await status_msg.edit_text(err_report, parse_mode=constants.ParseMode.HTML)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
@@ -6501,6 +6664,65 @@ async def message_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||||||
session = session_manager.get_or_create(chat_id)
|
session = session_manager.get_or_create(chat_id)
|
||||||
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
|
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
|
||||||
|
|
||||||
|
# 1. Check if user is in an interactive input state (e.g. configuring FTP fields)
|
||||||
|
if chat_id in USER_INPUT_STATES:
|
||||||
|
state = USER_INPUT_STATES.pop(chat_id)
|
||||||
|
if state.get("type") == "ftp_set" and curr_proj:
|
||||||
|
field = state.get("field")
|
||||||
|
val = update.message.text.strip()
|
||||||
|
|
||||||
|
if field == "host":
|
||||||
|
# Check for host:port format
|
||||||
|
if ":" in val:
|
||||||
|
h, p = val.rsplit(":", 1)
|
||||||
|
curr_proj.ftp_host = h.strip()
|
||||||
|
try:
|
||||||
|
curr_proj.ftp_port = int(p.strip())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
curr_proj.ftp_host = val
|
||||||
|
elif field == "user":
|
||||||
|
curr_proj.ftp_user = val
|
||||||
|
elif field == "pass":
|
||||||
|
curr_proj.ftp_password = val
|
||||||
|
# Delete password message from chat if possible for security
|
||||||
|
try:
|
||||||
|
await update.message.delete()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
elif field == "path":
|
||||||
|
curr_proj.ftp_path = val if val.startswith("/") else f"/{val}"
|
||||||
|
|
||||||
|
session_manager.save()
|
||||||
|
|
||||||
|
field_labels = {
|
||||||
|
"host": "هاست و پورت",
|
||||||
|
"user": "نام کاربری",
|
||||||
|
"pass": "رمز عبور",
|
||||||
|
"path": "مسیر مقصد",
|
||||||
|
}
|
||||||
|
label_fa = field_labels.get(field, field)
|
||||||
|
ack_msg = f"✅ <b>{label_fa} با موفقیت ذخیره شد.</b>" if is_fa else f"✅ <b>{field} updated successfully.</b>"
|
||||||
|
await update.message.reply_html(ack_msg)
|
||||||
|
|
||||||
|
# Auto-test connection if host, user and password are all present
|
||||||
|
if curr_proj.ftp_host and curr_proj.ftp_user and curr_proj.ftp_password:
|
||||||
|
test_ok, test_msg = await ftp_manager.test_connection(
|
||||||
|
host=curr_proj.ftp_host,
|
||||||
|
port=curr_proj.ftp_port,
|
||||||
|
user=curr_proj.ftp_user or "",
|
||||||
|
password=curr_proj.ftp_password or "",
|
||||||
|
path=curr_proj.ftp_path or "/",
|
||||||
|
tls=curr_proj.ftp_tls,
|
||||||
|
)
|
||||||
|
test_status = "✅ اتصال FTP با موفقیت برقرار و تایید شد." if test_ok else f"⚠️ هشدار در تست اتصال: {test_msg}"
|
||||||
|
await update.message.reply_html(f"<i>{test_status}</i>")
|
||||||
|
|
||||||
|
text, markup = await build_ftp_menu(chat_id)
|
||||||
|
await update.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
|
||||||
|
return
|
||||||
|
|
||||||
if not curr_proj:
|
if not curr_proj:
|
||||||
if is_fa:
|
if is_fa:
|
||||||
msg = (
|
msg = (
|
||||||
|
|||||||
@@ -1649,15 +1649,19 @@ async def execute_action(
|
|||||||
created_task,
|
created_task,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Map types: 'project' -> project tier, 'user'/'private' -> user tier, 'global' -> rejected/downgraded for AI
|
# Map types: 'project' -> project tier, 'user'/'private' -> user tier, 'global' -> global tier (if admin)
|
||||||
|
is_admin_user = settings.is_admin(chat_id)
|
||||||
if raw_type in ("project", "proj"):
|
if raw_type in ("project", "proj"):
|
||||||
mem_type = "project"
|
mem_type = "project"
|
||||||
proj_name = req_project
|
proj_name = req_project
|
||||||
elif raw_type == "global":
|
elif raw_type == "global":
|
||||||
# Global memories are strictly bot-managed by Admin. AI cannot auto-save global memories.
|
if is_admin_user:
|
||||||
logger.info("AI attempted global memory save for user %s; safely converting to user memory.", chat_id)
|
mem_type = "global"
|
||||||
mem_type = "user"
|
proj_name = None
|
||||||
proj_name = None
|
else:
|
||||||
|
logger.info("Non-admin user %s attempted global memory save; converting to user memory.", chat_id)
|
||||||
|
mem_type = "user"
|
||||||
|
proj_name = None
|
||||||
else:
|
else:
|
||||||
mem_type = "user"
|
mem_type = "user"
|
||||||
proj_name = None
|
proj_name = None
|
||||||
@@ -1667,7 +1671,7 @@ async def execute_action(
|
|||||||
type_=mem_type,
|
type_=mem_type,
|
||||||
key=key,
|
key=key,
|
||||||
content=content,
|
content=content,
|
||||||
user_id=chat_id,
|
user_id=None if mem_type == "global" else chat_id,
|
||||||
project_name=proj_name,
|
project_name=proj_name,
|
||||||
category=category,
|
category=category,
|
||||||
importance=importance,
|
importance=importance,
|
||||||
@@ -1720,28 +1724,37 @@ async def execute_action(
|
|||||||
created_task,
|
created_task,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
is_admin_user = settings.is_admin(chat_id)
|
||||||
if raw_type in ("project", "proj"):
|
if raw_type in ("project", "proj"):
|
||||||
mem_type = "project"
|
mem_type = "project"
|
||||||
proj_name = req_project
|
proj_name = req_project
|
||||||
|
elif raw_type == "global" and is_admin_user:
|
||||||
|
mem_type = "global"
|
||||||
|
proj_name = None
|
||||||
else:
|
else:
|
||||||
mem_type = "user"
|
mem_type = "user"
|
||||||
proj_name = None
|
proj_name = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
existing = memory_manager.get_by_key(type_=mem_type, key=key, user_id=chat_id, project_name=proj_name)
|
existing = memory_manager.get_by_key(type_=mem_type, key=key, user_id=None if mem_type == "global" else chat_id, project_name=proj_name)
|
||||||
cat = category or (existing.category if existing else "general")
|
cat = category or (existing.category if existing else "general")
|
||||||
imp = int(importance_raw) if importance_raw and importance_raw.isdigit() else (existing.importance if existing else 1)
|
imp = int(importance_raw) if importance_raw and importance_raw.isdigit() else (existing.importance if existing else 1)
|
||||||
item, is_created = memory_manager.save_or_update(
|
item, is_created = memory_manager.save_or_update(
|
||||||
type_=mem_type,
|
type_=mem_type,
|
||||||
key=key,
|
key=key,
|
||||||
content=content,
|
content=content,
|
||||||
user_id=chat_id,
|
user_id=None if mem_type == "global" else chat_id,
|
||||||
project_name=proj_name,
|
project_name=proj_name,
|
||||||
category=cat,
|
category=cat,
|
||||||
importance=imp,
|
importance=imp,
|
||||||
created_by=chat_id,
|
created_by=chat_id,
|
||||||
)
|
)
|
||||||
type_label = f"📁 پروژه (<code>{escape_html(proj_name or '')}</code>)" if item.is_project else "👤 کاربر (سراسری)"
|
if item.is_project:
|
||||||
|
type_label = f"📁 پروژه (<code>{escape_html(proj_name or '')}</code>)"
|
||||||
|
elif item.is_user:
|
||||||
|
type_label = "👤 کاربر (سراسری)"
|
||||||
|
else:
|
||||||
|
type_label = "🌐 عمومی (سیستم)"
|
||||||
badge = (
|
badge = (
|
||||||
f"\n\n🧠 <b>خاطره هوش مصنوعی بهروزرسانی شد:</b>\n"
|
f"\n\n🧠 <b>خاطره هوش مصنوعی بهروزرسانی شد:</b>\n"
|
||||||
f"• 🏷️ <b>بخش:</b> {type_label}\n"
|
f"• 🏷️ <b>بخش:</b> {type_label}\n"
|
||||||
@@ -1768,16 +1781,24 @@ async def execute_action(
|
|||||||
key = attrs.get("key") or attrs.get("slug") or attrs.get("_default")
|
key = attrs.get("key") or attrs.get("slug") or attrs.get("_default")
|
||||||
raw_type = (attrs.get("type") or "user").lower().strip()
|
raw_type = (attrs.get("type") or "user").lower().strip()
|
||||||
req_project = attrs.get("project") or attrs.get("proj") or current_project_name
|
req_project = attrs.get("project") or attrs.get("proj") or current_project_name
|
||||||
|
is_admin_user = settings.is_admin(chat_id)
|
||||||
|
|
||||||
mem_type = "project" if raw_type in ("project", "proj") else "user"
|
if raw_type in ("project", "proj"):
|
||||||
proj_name = req_project if mem_type == "project" else None
|
mem_type = "project"
|
||||||
|
proj_name = req_project
|
||||||
|
elif raw_type == "global" and is_admin_user:
|
||||||
|
mem_type = "global"
|
||||||
|
proj_name = None
|
||||||
|
else:
|
||||||
|
mem_type = "user"
|
||||||
|
proj_name = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if mem_id_raw and mem_id_raw.isdigit():
|
if mem_id_raw and mem_id_raw.isdigit():
|
||||||
ok = memory_manager.delete_by_id(int(mem_id_raw), user_id=chat_id, is_admin=is_admin)
|
ok = memory_manager.delete_by_id(int(mem_id_raw), user_id=chat_id, is_admin=is_admin_user)
|
||||||
elif key:
|
elif key:
|
||||||
ok = memory_manager.delete_by_key(
|
ok = memory_manager.delete_by_key(
|
||||||
type_=mem_type, key=key, user_id=chat_id, project_name=proj_name, is_admin=is_admin
|
type_=mem_type, key=key, user_id=None if mem_type == "global" else chat_id, project_name=proj_name, is_admin=is_admin_user
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return "\n⚠️ لطفاً شناسه یا کلید خاطره جهت حذف را مشخص کنید." if is_fa else "\n⚠️ Please specify memory id or key to delete.", side_effects, created_task
|
return "\n⚠️ لطفاً شناسه یا کلید خاطره جهت حذف را مشخص کنید." if is_fa else "\n⚠️ Please specify memory id or key to delete.", side_effects, created_task
|
||||||
|
|||||||
Binary file not shown.
@@ -622,6 +622,7 @@ def format_ftp_info(
|
|||||||
user: Optional[str] = None,
|
user: Optional[str] = None,
|
||||||
path: str = "/",
|
path: str = "/",
|
||||||
tls: bool = False,
|
tls: bool = False,
|
||||||
|
password: Optional[str] = None,
|
||||||
lang: str = "fa",
|
lang: str = "fa",
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Formats FTP connection and deployment status for Telegram HTML."""
|
"""Formats FTP connection and deployment status for Telegram HTML."""
|
||||||
@@ -630,18 +631,20 @@ def format_ftp_info(
|
|||||||
host_str = f"<code>{escape_html(host)}:{port}</code>" if host else ("<i>(تنظیم نشده)</i>" if is_fa else "<i>(Not configured)</i>")
|
host_str = f"<code>{escape_html(host)}:{port}</code>" if host else ("<i>(تنظیم نشده)</i>" if is_fa else "<i>(Not configured)</i>")
|
||||||
user_str = f"<code>{escape_html(user)}</code>" if user else ("<i>(ندارد)</i>" if is_fa else "<i>(None)</i>")
|
user_str = f"<code>{escape_html(user)}</code>" if user else ("<i>(ندارد)</i>" if is_fa else "<i>(None)</i>")
|
||||||
path_str = f"<code>{escape_html(path)}</code>"
|
path_str = f"<code>{escape_html(path)}</code>"
|
||||||
|
pass_str = f"<code>{'•' * min(len(password), 8)}</code> <i>(تنظیم شده)</i>" if password else ("<i>(ندارد)</i>" if is_fa else "<i>(None)</i>")
|
||||||
tls_str = "بله (FTPS/TLS امن) 🔒" if tls else "خیر (FTP معمولی)"
|
tls_str = "بله (FTPS/TLS امن) 🔒" if tls else "خیر (FTP معمولی)"
|
||||||
tls_str_en = "Yes (Secure FTPS/TLS) 🔒" if tls else "No (Standard FTP)"
|
tls_str_en = "Yes (Secure FTPS/TLS) 🔒" if tls else "No (Standard FTP)"
|
||||||
|
|
||||||
if is_fa:
|
if is_fa:
|
||||||
return (
|
return (
|
||||||
f"🚀 <b>تنظیمات استقرار و دیپلوی FTP پروژه</b>\n\n"
|
f"🚀 <b>تنظیمات استقرار و مشخصات FTP پروژه</b>\n\n"
|
||||||
f"• 📁 <b>پروژه:</b> <code>{clean_p}</code>\n"
|
f"• 📁 <b>پروژه:</b> <code>{clean_p}</code>\n"
|
||||||
f"• 🌐 <b>سرور / هاست FTP:</b> {host_str}\n"
|
f"• 🌐 <b>سرور / هاست:</b> {host_str}\n"
|
||||||
f"• 👤 <b>نام کاربری:</b> {user_str}\n"
|
f"• 👤 <b>نام کاربری:</b> {user_str}\n"
|
||||||
|
f"• 🔑 <b>رمز عبور:</b> {pass_str}\n"
|
||||||
f"• 📂 <b>مسیر مقصد روی هاست:</b> {path_str}\n"
|
f"• 📂 <b>مسیر مقصد روی هاست:</b> {path_str}\n"
|
||||||
f"• 🔒 <b>پروتکل امن (FTPS):</b> {tls_str}\n\n"
|
f"• 🔒 <b>پروتکل امن (FTPS):</b> {tls_str}\n\n"
|
||||||
f"💡 <i>در زمان دیپلوی، هوش مصنوعی فایلهای زائد، کش، سشنها و مخزن گیت (.git) را به صورت خودکار فیلتر کرده و تنها سورس اصلی و تمیز به سرور منتقل میشود.</i>"
|
f"💡 <i>میتوانید با دکمههای زیر هر فیلد را به دلخواه ویرایش، تست یا ذخیره کنید. در زمان دیپلوی، فایلهای زائد، کش و .git به صورت خودکار فیلتر میشوند.</i>"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return (
|
return (
|
||||||
@@ -649,9 +652,10 @@ def format_ftp_info(
|
|||||||
f"• 📁 <b>Project:</b> <code>{clean_p}</code>\n"
|
f"• 📁 <b>Project:</b> <code>{clean_p}</code>\n"
|
||||||
f"• 🌐 <b>FTP Host:</b> {host_str}\n"
|
f"• 🌐 <b>FTP Host:</b> {host_str}\n"
|
||||||
f"• 👤 <b>Username:</b> {user_str}\n"
|
f"• 👤 <b>Username:</b> {user_str}\n"
|
||||||
f"• 📂 <b>Remote Target Path:</b> {path_str}\n"
|
f"• 🔑 <b>Password:</b> {pass_str}\n"
|
||||||
|
f"• 📂 <b>Remote Path:</b> {path_str}\n"
|
||||||
f"• 🔒 <b>Secure FTPS/TLS:</b> {tls_str_en}\n\n"
|
f"• 🔒 <b>Secure FTPS/TLS:</b> {tls_str_en}\n\n"
|
||||||
f"💡 <i>During deployment, unnecessary cache, temp, sessions, and .git files are automatically excluded for a clean production upload.</i>"
|
f"💡 <i>Use the buttons below to edit fields, test connection, or deploy. Junk and cache files are excluded automatically.</i>"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+127
-29
@@ -8,6 +8,9 @@ from pathlib import Path
|
|||||||
from typing import Optional, Dict, Any, List, Tuple, Set
|
from typing import Optional, Dict, Any, List, Tuple, Set
|
||||||
import ftplib
|
import ftplib
|
||||||
import socket
|
import socket
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
|
||||||
logger = logging.getLogger("AGYFTPManager")
|
logger = logging.getLogger("AGYFTPManager")
|
||||||
|
|
||||||
@@ -52,6 +55,10 @@ DEFAULT_FTP_EXCLUDES = [
|
|||||||
"env",
|
"env",
|
||||||
"env/*",
|
"env/*",
|
||||||
|
|
||||||
|
# Dependencies & local packages
|
||||||
|
"node_modules",
|
||||||
|
"node_modules/*",
|
||||||
|
|
||||||
# Logs & temp files
|
# Logs & temp files
|
||||||
"*.log",
|
"*.log",
|
||||||
"tmp",
|
"tmp",
|
||||||
@@ -77,22 +84,25 @@ def should_exclude(rel_path: str, custom_excludes: Optional[List[str]] = None) -
|
|||||||
"""Checks if a relative path matches any exclusion pattern."""
|
"""Checks if a relative path matches any exclusion pattern."""
|
||||||
excludes = DEFAULT_FTP_EXCLUDES + (custom_excludes or [])
|
excludes = DEFAULT_FTP_EXCLUDES + (custom_excludes or [])
|
||||||
norm_path = rel_path.replace("\\", "/").strip("/")
|
norm_path = rel_path.replace("\\", "/").strip("/")
|
||||||
parts = norm_path.split("/")
|
parts = [p for p in norm_path.split("/") if p]
|
||||||
|
|
||||||
for pattern in excludes:
|
for pattern in excludes:
|
||||||
pat = pattern.replace("\\", "/").strip("/")
|
pat = pattern.replace("\\", "/").strip("/")
|
||||||
# Check against full relative path
|
|
||||||
if fnmatch.fnmatch(norm_path, pat):
|
|
||||||
return True
|
|
||||||
# Check against individual directory / file parts
|
|
||||||
for part in parts:
|
|
||||||
if fnmatch.fnmatch(part, pat):
|
|
||||||
return True
|
|
||||||
# If pattern is a directory (e.g. .git/* or .git), match if path starts with it
|
|
||||||
clean_pat = pat.rstrip("/*")
|
clean_pat = pat.rstrip("/*")
|
||||||
|
|
||||||
|
# Check against full relative path
|
||||||
|
if fnmatch.fnmatch(norm_path, pat) or fnmatch.fnmatch(norm_path, clean_pat):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# If path starts with directory pattern
|
||||||
if norm_path == clean_pat or norm_path.startswith(clean_pat + "/"):
|
if norm_path == clean_pat or norm_path.startswith(clean_pat + "/"):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
# Check against individual directory / file parts
|
||||||
|
for part in parts:
|
||||||
|
if fnmatch.fnmatch(part, pat) or fnmatch.fnmatch(part, clean_pat):
|
||||||
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@@ -109,17 +119,27 @@ class FTPManager:
|
|||||||
timeout: int = 15,
|
timeout: int = 15,
|
||||||
) -> ftplib.FTP:
|
) -> ftplib.FTP:
|
||||||
"""Helper to create and authenticate an FTP or FTPS client."""
|
"""Helper to create and authenticate an FTP or FTPS client."""
|
||||||
|
clean_host = host.strip()
|
||||||
|
clean_user = user.strip() if user else ""
|
||||||
|
clean_pass = password.strip() if password else ""
|
||||||
|
|
||||||
if tls:
|
if tls:
|
||||||
ftp = ftplib.FTP_TLS(timeout=timeout)
|
ftp = ftplib.FTP_TLS(timeout=timeout)
|
||||||
else:
|
else:
|
||||||
ftp = ftplib.FTP(timeout=timeout)
|
ftp = ftplib.FTP(timeout=timeout)
|
||||||
|
|
||||||
ftp.connect(host=host, port=port, timeout=timeout)
|
ftp.encoding = "utf-8"
|
||||||
ftp.login(user=user or "anonymous", passwd=password or "")
|
ftp.connect(host=clean_host, port=int(port), timeout=timeout)
|
||||||
|
|
||||||
if tls and isinstance(ftp, ftplib.FTP_TLS):
|
if tls and isinstance(ftp, ftplib.FTP_TLS):
|
||||||
|
ftp.auth()
|
||||||
|
ftp.login(user=clean_user or "anonymous", passwd=clean_pass)
|
||||||
ftp.prot_p() # Secure data connection
|
ftp.prot_p() # Secure data connection
|
||||||
|
else:
|
||||||
|
ftp.login(user=clean_user or "anonymous", passwd=clean_pass)
|
||||||
|
|
||||||
|
# Force passive mode (standard for firewalls / NAT)
|
||||||
|
ftp.set_pasv(True)
|
||||||
return ftp
|
return ftp
|
||||||
|
|
||||||
async def test_connection(
|
async def test_connection(
|
||||||
@@ -130,39 +150,72 @@ class FTPManager:
|
|||||||
password: str = "",
|
password: str = "",
|
||||||
path: str = "/",
|
path: str = "/",
|
||||||
tls: bool = False,
|
tls: bool = False,
|
||||||
timeout: int = 10,
|
timeout: int = 12,
|
||||||
) -> Tuple[bool, str]:
|
) -> Tuple[bool, str]:
|
||||||
"""Tests FTP credentials and verifies access to the target remote directory."""
|
"""Tests FTP credentials and verifies access to the target remote directory."""
|
||||||
if not host or not host.strip():
|
if not host or not host.strip():
|
||||||
return False, "آدرس هاست (Host) FTP مشخص نشده است."
|
return False, "آدرس هاست (Host) FTP مشخص نشده است."
|
||||||
|
|
||||||
def _test():
|
def _test():
|
||||||
|
ftp = None
|
||||||
try:
|
try:
|
||||||
ftp = self._create_ftp_client(host.strip(), int(port), user.strip(), password, tls, timeout)
|
ftp = self._create_ftp_client(host.strip(), int(port), user.strip(), password, tls, timeout)
|
||||||
|
|
||||||
|
# Check root PWD
|
||||||
|
init_pwd = ftp.pwd()
|
||||||
|
|
||||||
# Test CWD to target path if specified
|
# Test CWD to target path if specified
|
||||||
target_path = path.strip() if path else "/"
|
target_path = path.strip() if path else "/"
|
||||||
if target_path and target_path != "/":
|
if target_path and target_path != "/":
|
||||||
try:
|
try:
|
||||||
ftp.cwd(target_path)
|
ftp.cwd(target_path)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
pwd = ftp.pwd()
|
curr_pwd = ftp.pwd() if ftp else "/"
|
||||||
ftp.quit()
|
try:
|
||||||
return False, f"اتصال به FTP برقرار شد، اما مسیر ریموت «{target_path}» یافت نشد (مسیر فعلی: {pwd}): {e}"
|
ftp.quit()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return False, f"اتصال به سرور برقرار شد، اما مسیر ریموت «{target_path}» یافت نشد (مسیر فعلی: {curr_pwd}): {e}"
|
||||||
|
|
||||||
pwd = ftp.pwd()
|
final_pwd = ftp.pwd()
|
||||||
|
|
||||||
|
# Test directory listing
|
||||||
|
item_count = 0
|
||||||
try:
|
try:
|
||||||
listing = ftp.nlst()
|
listing = ftp.nlst()
|
||||||
count = len(listing)
|
item_count = len(listing)
|
||||||
except Exception:
|
except Exception:
|
||||||
count = 0
|
try:
|
||||||
ftp.quit()
|
lines = []
|
||||||
return True, f"اتصال با موفقیت برقرار شد. مسیر فعلی: <code>{pwd}</code> (تعداد آیتمها: {count})"
|
ftp.dir(lines.append)
|
||||||
except (socket.gaierror, socket.timeout) as e:
|
item_count = len(lines)
|
||||||
return False, f"خطای شبکه / نامعتبر بودن آدرس سرور ({host}:{port}): {e}"
|
except Exception:
|
||||||
|
item_count = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
ftp.quit()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
tls_label = " (FTPS/TLS امن)" if tls else " (FTP معمولی)"
|
||||||
|
return True, f"اتصال با موفقیت برقرار شد{tls_label} | مسیر: <code>{final_pwd}</code> | تعداد فایلها و پوشهها: {item_count}"
|
||||||
|
|
||||||
|
except socket.gaierror as e:
|
||||||
|
return False, f"نام دامنه/هاست سرور یافت نشد ({host}): {e}"
|
||||||
|
except (socket.timeout, TimeoutError) as e:
|
||||||
|
return False, f"مهلت زمانی اتصال به سرور به پایان رسید (Timeout روی پورت {port}): {e}"
|
||||||
|
except ConnectionRefusedError as e:
|
||||||
|
return False, f"اتصال توسط سرور رد شد (پورت {port} بسته است یا FTP روی آن فعال نیست): {e}"
|
||||||
except ftplib.error_perm as e:
|
except ftplib.error_perm as e:
|
||||||
return False, f"خطای احراز هویت / دسترسی FTP: {e}"
|
return False, f"خطای نام کاربری یا رمز عبور (دسترسی نامعتبر): {e}"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return False, f"خطا در برقراری ارتباط با FTP: {e}"
|
return False, f"خطا در برقراری ارتباط با FTP: {e}"
|
||||||
|
finally:
|
||||||
|
if ftp:
|
||||||
|
try:
|
||||||
|
ftp.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
return await asyncio.to_thread(_test)
|
return await asyncio.to_thread(_test)
|
||||||
|
|
||||||
@@ -177,11 +230,16 @@ class FTPManager:
|
|||||||
tls: bool = False,
|
tls: bool = False,
|
||||||
custom_excludes: Optional[List[str]] = None,
|
custom_excludes: Optional[List[str]] = None,
|
||||||
dry_run: bool = False,
|
dry_run: bool = False,
|
||||||
|
branch: str = "production",
|
||||||
) -> Dict[str, Any]:
|
) -> 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.
|
Skips all temporary, git, session, and junk files.
|
||||||
"""
|
"""
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
|
||||||
ws = Path(workspace_path).expanduser().resolve()
|
ws = Path(workspace_path).expanduser().resolve()
|
||||||
if not ws.exists() or not ws.is_dir():
|
if not ws.exists() or not ws.is_dir():
|
||||||
return {
|
return {
|
||||||
@@ -199,8 +257,42 @@ class FTPManager:
|
|||||||
uploaded_files: List[str] = []
|
uploaded_files: List[str] = []
|
||||||
skipped_files: List[str] = []
|
skipped_files: List[str] = []
|
||||||
total_bytes = 0
|
total_bytes = 0
|
||||||
|
temp_export_dir = None
|
||||||
|
|
||||||
try:
|
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)
|
ftp = self._create_ftp_client(host.strip(), int(port), user.strip(), password, tls, timeout=20)
|
||||||
|
|
||||||
# Navigate or create remote root path
|
# Navigate or create remote root path
|
||||||
@@ -230,11 +322,11 @@ class FTPManager:
|
|||||||
|
|
||||||
ensure_remote_dir(target_root)
|
ensure_remote_dir(target_root)
|
||||||
|
|
||||||
# Scan workspace
|
# Scan source directory
|
||||||
all_files_to_upload: List[Tuple[Path, str]] = [] # (local_path, rel_path)
|
all_files_to_upload: List[Tuple[Path, str]] = [] # (local_path, rel_path)
|
||||||
|
|
||||||
for root, dirs, files in os.walk(str(ws)):
|
for root, dirs, files in os.walk(str(source_dir)):
|
||||||
rel_dir = os.path.relpath(root, str(ws))
|
rel_dir = os.path.relpath(root, str(source_dir))
|
||||||
if rel_dir == ".":
|
if rel_dir == ".":
|
||||||
rel_dir = ""
|
rel_dir = ""
|
||||||
|
|
||||||
@@ -309,6 +401,12 @@ class FTPManager:
|
|||||||
"duration": round(time.time() - start_time, 2),
|
"duration": round(time.time() - start_time, 2),
|
||||||
"uploaded_list": uploaded_files,
|
"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)
|
return await asyncio.to_thread(_run_deploy)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import json
|
import json
|
||||||
|
import html
|
||||||
import logging
|
import logging
|
||||||
import asyncio
|
import asyncio
|
||||||
import urllib.request
|
import urllib.request
|
||||||
@@ -12,6 +13,11 @@ from config import settings
|
|||||||
|
|
||||||
logger = logging.getLogger("AGYGitManager")
|
logger = logging.getLogger("AGYGitManager")
|
||||||
|
|
||||||
|
def escape_html(text: str) -> str:
|
||||||
|
if not text:
|
||||||
|
return ""
|
||||||
|
return html.escape(str(text), quote=False)
|
||||||
|
|
||||||
DEFAULT_GITIGNORE = """# Byte-compiled / optimized / DLL files
|
DEFAULT_GITIGNORE = """# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
|
|||||||
+154
-45
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user