191 lines
6.9 KiB
Python
191 lines
6.9 KiB
Python
import os
|
|
from ftplib import FTP
|
|
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, DownloadColumn, TransferSpeedColumn
|
|
import lib.paramiko as paramiko
|
|
import logging
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
logging.basicConfig(
|
|
filename="sftp_debug.log",
|
|
level=logging.DEBUG,
|
|
format="%(asctime)s - %(levelname)s - %(message)s"
|
|
)
|
|
|
|
class SFTPSync:
|
|
def __init__(self, host, user, password, port=22):
|
|
self.host = host
|
|
self.user = user
|
|
self.password = password
|
|
self.port = int(port)
|
|
self.client = None
|
|
self.sftp = None
|
|
|
|
def log(self, message):
|
|
"""Helper to log to file and console if needed"""
|
|
logging.info(message)
|
|
|
|
def connect(self):
|
|
try:
|
|
self.log(f"Initiating SSH connection to {self.host}:{self.port}")
|
|
self.client = paramiko.SSHClient()
|
|
self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
|
|
self.log(f"Attempting login for user: {self.user}")
|
|
self.client.connect(
|
|
self.host,
|
|
port=self.port,
|
|
username=self.user,
|
|
password=self.password,
|
|
timeout=15,
|
|
allow_agent=False, # Prevents interference from local SSH agents
|
|
look_for_keys=False
|
|
)
|
|
|
|
self.log("SSH connection established. Opening SFTP session...")
|
|
self.sftp = self.client.open_sftp()
|
|
self.log("SFTP session opened successfully.")
|
|
return True
|
|
except paramiko.AuthenticationException:
|
|
self.log("Authentication failed: Check username or password.")
|
|
return "Auth failed: Invalid credentials."
|
|
except paramiko.SSHException as e:
|
|
self.log(f"SSH protocol error: {e}")
|
|
return f"SSH Error: {e}"
|
|
except Exception as e:
|
|
self.log(f"Unexpected connection error: {e}")
|
|
return f"Error: {e}"
|
|
|
|
def upload_with_progress(self, local_path, remote_dir):
|
|
if not os.path.exists(local_path):
|
|
self.log(f"Local error: File {local_path} not found.")
|
|
return "Local file not found."
|
|
|
|
file_size = os.path.getsize(local_path)
|
|
filename = os.path.basename(local_path)
|
|
# Ensure remote path uses forward slashes for Linux servers
|
|
remote_path = (Path(remote_dir) / filename).as_posix()
|
|
|
|
self.log(f"Starting upload: {local_path} -> {remote_path} ({file_size} bytes)")
|
|
|
|
try:
|
|
with Progress(
|
|
SpinnerColumn(),
|
|
TextColumn("[progress.description]{task.description}"),
|
|
BarColumn(),
|
|
DownloadColumn(),
|
|
TransferSpeedColumn(),
|
|
) as progress:
|
|
|
|
task = progress.add_task(f"Uploading...", total=file_size)
|
|
|
|
def callback(transferred, total):
|
|
progress.update(task, completed=transferred)
|
|
# Log every 25% to avoid bloating the log file
|
|
if transferred % (total // 4 + 1) < 8192:
|
|
logging.debug(f"Progress: {transferred}/{total} bytes")
|
|
|
|
self.sftp.put(local_path, remote_path, callback=callback)
|
|
|
|
self.log(f"Upload completed successfully: {filename}")
|
|
return True
|
|
except PermissionError:
|
|
self.log(f"Permission denied on server: Cannot write to {remote_dir}")
|
|
return "Server Error: Permission denied."
|
|
except Exception as e:
|
|
self.log(f"Upload failed mid-transfer: {e}")
|
|
return f"Upload error: {e}"
|
|
def upload_directory(self, local_dir, remote_root):
|
|
"""Recursively uploads a directory, ensuring empty folders are created."""
|
|
self.log(f"Scanning directory: {local_dir}")
|
|
|
|
for root, dirs, files in os.walk(local_dir):
|
|
rel_path = os.path.relpath(root, local_dir)
|
|
|
|
# Build the remote path
|
|
if rel_path == ".":
|
|
# This is the root folder itself (e.g., 'core')
|
|
remote_dir = (Path(remote_root) / Path(local_dir).name).as_posix()
|
|
else:
|
|
# These are subfolders (e.g., 'core/utils')
|
|
remote_dir = (Path(remote_root) / Path(local_dir).name / rel_path).as_posix()
|
|
|
|
# --- THE FIX: Create folder even if 'files' is empty ---
|
|
try:
|
|
self.sftp.mkdir(remote_dir)
|
|
self.log(f"Created remote folder: {remote_dir}")
|
|
print(f"Created: {remote_dir}") # Feedback for the user
|
|
except IOError:
|
|
# Folder likely exists
|
|
self.log(f"Folder already exists: {remote_dir}")
|
|
|
|
# Now upload files if there are any
|
|
for filename in files:
|
|
local_file = os.path.join(root, filename)
|
|
self.upload_with_progress(local_file, remote_dir)
|
|
|
|
|
|
def disconnect(self):
|
|
self.log("Closing SFTP and SSH connections.")
|
|
if self.sftp: self.sftp.close()
|
|
if self.client: self.client.close()
|
|
|
|
|
|
|
|
class FTPSync:
|
|
def __init__(self, host, user, password, port=21):
|
|
self.host = host
|
|
self.user = user
|
|
self.password = password
|
|
self.port = int(port)
|
|
self.ftp = FTP()
|
|
|
|
def connect(self):
|
|
try:
|
|
self.ftp.connect(self.host, self.port, timeout=10)
|
|
self.ftp.login(self.user, self.password)
|
|
self.ftp.set_pasv(True) # Passive mode is safer for most firewalls
|
|
return True
|
|
except Exception as e:
|
|
return f"Connection error: {e}"
|
|
|
|
def upload_with_progress(self, local_path, remote_dir):
|
|
if not os.path.exists(local_path):
|
|
return "Local file not found."
|
|
|
|
file_size = os.path.getsize(local_path)
|
|
filename = os.path.basename(local_path)
|
|
|
|
try:
|
|
self.ftp.cwd(remote_dir)
|
|
|
|
# Define the Rich Progress Bar layout
|
|
with Progress(
|
|
SpinnerColumn(),
|
|
TextColumn("[progress.description]{task.description}"),
|
|
BarColumn(),
|
|
DownloadColumn(),
|
|
TransferSpeedColumn(),
|
|
) as progress:
|
|
|
|
task = progress.add_task(f"Uploading {filename}...", total=file_size)
|
|
|
|
with open(local_path, "rb") as f:
|
|
# Callback function called every time a chunk is sent
|
|
def callback(chunk):
|
|
progress.update(task, advance=len(chunk))
|
|
|
|
# 8KB is a standard buffer size for FTP
|
|
self.ftp.storbinary(f"STOR {filename}", f, blocksize=8192, callback=callback)
|
|
|
|
return True
|
|
except Exception as e:
|
|
return f"Upload failed: {e}"
|
|
|
|
def disconnect(self):
|
|
try:
|
|
self.ftp.quit()
|
|
except:
|
|
self.ftp.close()
|