AI Update: تسک ها را آماده کن این ساختار عالیه
This commit is contained in:
@@ -0,0 +1,316 @@
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import fnmatch
|
||||
import logging
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any, List, Tuple, Set
|
||||
import ftplib
|
||||
import socket
|
||||
|
||||
logger = logging.getLogger("AGYFTPManager")
|
||||
|
||||
# Standard patterns for junk, temporary, confidential, and unnecessary files that must NEVER be sent to production FTP
|
||||
DEFAULT_FTP_EXCLUDES = [
|
||||
# Git and version control
|
||||
".git",
|
||||
".git/*",
|
||||
".gitignore",
|
||||
".gitattributes",
|
||||
".gitmodules",
|
||||
|
||||
# Environment and secrets (local secrets must not leak to production without explicit setup)
|
||||
".env",
|
||||
".env.*",
|
||||
"*.env",
|
||||
"*.pem",
|
||||
"*.key",
|
||||
|
||||
# AI & Bot internal session / agent configs
|
||||
".agents",
|
||||
".agents/*",
|
||||
".gemini",
|
||||
".gemini/*",
|
||||
"sessions_data",
|
||||
"sessions_data/*",
|
||||
"uploads_temp",
|
||||
"uploads_temp/*",
|
||||
"*.db-shm",
|
||||
"*.db-wal",
|
||||
|
||||
# Python cache & build
|
||||
"__pycache__",
|
||||
"__pycache__/*",
|
||||
"*.pyc",
|
||||
"*.pyo",
|
||||
"*.pyd",
|
||||
".venv",
|
||||
".venv/*",
|
||||
"venv",
|
||||
"venv/*",
|
||||
"env",
|
||||
"env/*",
|
||||
|
||||
# Logs & temp files
|
||||
"*.log",
|
||||
"tmp",
|
||||
"tmp/*",
|
||||
"temp",
|
||||
"temp/*",
|
||||
|
||||
# IDE & OS clutter
|
||||
".DS_Store",
|
||||
"Thumbs.db",
|
||||
"desktop.ini",
|
||||
".idea",
|
||||
".idea/*",
|
||||
".vscode",
|
||||
".vscode/*",
|
||||
"*.swp",
|
||||
"*.swo",
|
||||
"*~",
|
||||
]
|
||||
|
||||
|
||||
def should_exclude(rel_path: str, custom_excludes: Optional[List[str]] = None) -> bool:
|
||||
"""Checks if a relative path matches any exclusion pattern."""
|
||||
excludes = DEFAULT_FTP_EXCLUDES + (custom_excludes or [])
|
||||
norm_path = rel_path.replace("\\", "/").strip("/")
|
||||
parts = norm_path.split("/")
|
||||
|
||||
for pattern in excludes:
|
||||
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("/*")
|
||||
if norm_path == clean_pat or norm_path.startswith(clean_pat + "/"):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class FTPManager:
|
||||
"""Manages FTP connections, testing, and clean intelligent deployments."""
|
||||
|
||||
def _create_ftp_client(
|
||||
self,
|
||||
host: str,
|
||||
port: int = 21,
|
||||
user: str = "",
|
||||
password: str = "",
|
||||
tls: bool = False,
|
||||
timeout: int = 15,
|
||||
) -> ftplib.FTP:
|
||||
"""Helper to create and authenticate an FTP or FTPS client."""
|
||||
if tls:
|
||||
ftp = ftplib.FTP_TLS(timeout=timeout)
|
||||
else:
|
||||
ftp = ftplib.FTP(timeout=timeout)
|
||||
|
||||
ftp.connect(host=host, port=port, timeout=timeout)
|
||||
ftp.login(user=user or "anonymous", passwd=password or "")
|
||||
|
||||
if tls and isinstance(ftp, ftplib.FTP_TLS):
|
||||
ftp.prot_p() # Secure data connection
|
||||
|
||||
return ftp
|
||||
|
||||
async def test_connection(
|
||||
self,
|
||||
host: str,
|
||||
port: int = 21,
|
||||
user: str = "",
|
||||
password: str = "",
|
||||
path: str = "/",
|
||||
tls: bool = False,
|
||||
timeout: int = 10,
|
||||
) -> Tuple[bool, str]:
|
||||
"""Tests FTP credentials and verifies access to the target remote directory."""
|
||||
if not host or not host.strip():
|
||||
return False, "آدرس هاست (Host) FTP مشخص نشده است."
|
||||
|
||||
def _test():
|
||||
try:
|
||||
ftp = self._create_ftp_client(host.strip(), int(port), user.strip(), password, tls, timeout)
|
||||
# Test CWD to target path if specified
|
||||
target_path = path.strip() if path else "/"
|
||||
if target_path and target_path != "/":
|
||||
try:
|
||||
ftp.cwd(target_path)
|
||||
except Exception as e:
|
||||
pwd = ftp.pwd()
|
||||
ftp.quit()
|
||||
return False, f"اتصال به FTP برقرار شد، اما مسیر ریموت «{target_path}» یافت نشد (مسیر فعلی: {pwd}): {e}"
|
||||
|
||||
pwd = ftp.pwd()
|
||||
try:
|
||||
listing = ftp.nlst()
|
||||
count = len(listing)
|
||||
except Exception:
|
||||
count = 0
|
||||
ftp.quit()
|
||||
return True, f"اتصال با موفقیت برقرار شد. مسیر فعلی: <code>{pwd}</code> (تعداد آیتمها: {count})"
|
||||
except (socket.gaierror, socket.timeout) as e:
|
||||
return False, f"خطای شبکه / نامعتبر بودن آدرس سرور ({host}:{port}): {e}"
|
||||
except ftplib.error_perm as e:
|
||||
return False, f"خطای احراز هویت / دسترسی FTP: {e}"
|
||||
except Exception as e:
|
||||
return False, f"خطا در برقراری ارتباط با FTP: {e}"
|
||||
|
||||
return await asyncio.to_thread(_test)
|
||||
|
||||
async def deploy_project(
|
||||
self,
|
||||
workspace_path: str,
|
||||
host: str,
|
||||
port: int = 21,
|
||||
user: str = "",
|
||||
password: str = "",
|
||||
remote_path: str = "/",
|
||||
tls: bool = False,
|
||||
custom_excludes: Optional[List[str]] = None,
|
||||
dry_run: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Deploys files from workspace_path to the remote FTP server cleanly.
|
||||
Skips all temporary, git, session, and junk files.
|
||||
"""
|
||||
ws = Path(workspace_path).expanduser().resolve()
|
||||
if not ws.exists() or not ws.is_dir():
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"دایرکتوری پروژه در مسیر {workspace_path} یافت نشد.",
|
||||
"files_uploaded": 0,
|
||||
"files_skipped": 0,
|
||||
"bytes_transferred": 0,
|
||||
"duration": 0.0,
|
||||
"uploaded_list": [],
|
||||
}
|
||||
|
||||
def _run_deploy():
|
||||
start_time = time.time()
|
||||
uploaded_files: List[str] = []
|
||||
skipped_files: List[str] = []
|
||||
total_bytes = 0
|
||||
|
||||
try:
|
||||
ftp = self._create_ftp_client(host.strip(), int(port), user.strip(), password, tls, timeout=20)
|
||||
|
||||
# Navigate or create remote root path
|
||||
target_root = (remote_path or "/").strip().replace("\\", "/")
|
||||
if not target_root.startswith("/"):
|
||||
target_root = "/" + target_root
|
||||
target_root = target_root.rstrip("/")
|
||||
if not target_root:
|
||||
target_root = "/"
|
||||
|
||||
def ensure_remote_dir(r_dir: str):
|
||||
if r_dir in ("/", ""):
|
||||
ftp.cwd("/")
|
||||
return
|
||||
parts = [p for p in r_dir.split("/") if p]
|
||||
curr = ""
|
||||
for p in parts:
|
||||
curr += "/" + p
|
||||
try:
|
||||
ftp.cwd(curr)
|
||||
except Exception:
|
||||
try:
|
||||
ftp.mkd(curr)
|
||||
ftp.cwd(curr)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
ensure_remote_dir(target_root)
|
||||
|
||||
# Scan workspace
|
||||
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))
|
||||
if rel_dir == ".":
|
||||
rel_dir = ""
|
||||
|
||||
# Filter out directories in-place to avoid descending into ignored dirs
|
||||
dirs_to_keep = []
|
||||
for d in dirs:
|
||||
dir_rel = f"{rel_dir}/{d}".strip("/")
|
||||
if should_exclude(dir_rel, custom_excludes):
|
||||
skipped_files.append(dir_rel + "/")
|
||||
else:
|
||||
dirs_to_keep.append(d)
|
||||
dirs[:] = dirs_to_keep
|
||||
|
||||
for f in files:
|
||||
file_rel = f"{rel_dir}/{f}".strip("/")
|
||||
if should_exclude(file_rel, custom_excludes):
|
||||
skipped_files.append(file_rel)
|
||||
else:
|
||||
all_files_to_upload.append((Path(root) / f, file_rel))
|
||||
|
||||
if dry_run:
|
||||
ftp.quit()
|
||||
return {
|
||||
"success": True,
|
||||
"dry_run": True,
|
||||
"files_to_upload": len(all_files_to_upload),
|
||||
"files_skipped": len(skipped_files),
|
||||
"bytes_transferred": sum(p.stat().st_size for p, _ in all_files_to_upload if p.exists()),
|
||||
"duration": round(time.time() - start_time, 2),
|
||||
"uploaded_list": [r for _, r in all_files_to_upload[:50]],
|
||||
"remote_path": target_root,
|
||||
}
|
||||
|
||||
# Upload files
|
||||
for local_p, rel_p in all_files_to_upload:
|
||||
if not local_p.exists():
|
||||
continue
|
||||
file_size = local_p.stat().st_size
|
||||
rel_dir = str(Path(rel_p).parent).replace("\\", "/").strip(".")
|
||||
target_dir = f"{target_root}/{rel_dir}".rstrip("/") if rel_dir else target_root
|
||||
|
||||
ensure_remote_dir(target_dir)
|
||||
|
||||
file_name = local_p.name
|
||||
with open(local_p, "rb") as fp:
|
||||
ftp.storbinary(f"STOR {file_name}", fp)
|
||||
|
||||
uploaded_files.append(rel_p)
|
||||
total_bytes += file_size
|
||||
|
||||
ftp.quit()
|
||||
duration = round(time.time() - start_time, 2)
|
||||
return {
|
||||
"success": True,
|
||||
"dry_run": False,
|
||||
"files_uploaded": len(uploaded_files),
|
||||
"files_skipped": len(skipped_files),
|
||||
"bytes_transferred": total_bytes,
|
||||
"duration": duration,
|
||||
"uploaded_list": uploaded_files,
|
||||
"remote_path": target_root,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"FTP Deploy error: {e}", exc_info=True)
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"files_uploaded": len(uploaded_files),
|
||||
"files_skipped": len(skipped_files),
|
||||
"bytes_transferred": total_bytes,
|
||||
"duration": round(time.time() - start_time, 2),
|
||||
"uploaded_list": uploaded_files,
|
||||
}
|
||||
|
||||
return await asyncio.to_thread(_run_deploy)
|
||||
|
||||
|
||||
ftp_manager = FTPManager()
|
||||
Reference in New Issue
Block a user