155 lines
5.3 KiB
Python
155 lines
5.3 KiB
Python
import os
|
|
import sys
|
|
import threading
|
|
from ftplib import FTP, error_perm
|
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
|
|
thread_local = threading.local()
|
|
|
|
def load_env():
|
|
env = {}
|
|
if os.path.exists('.env'):
|
|
with open('.env', 'r', encoding='utf-8') as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if line and not line.startswith('#'):
|
|
if '=' in line:
|
|
key, val = line.split('=', 1)
|
|
env[key.strip()] = val.strip()
|
|
return env
|
|
|
|
def get_ftp_connection(ftp_host, ftp_user, ftp_pass):
|
|
if not hasattr(thread_local, "ftp") or thread_local.ftp is None:
|
|
ftp = FTP(ftp_host, timeout=30)
|
|
ftp.login(ftp_user, ftp_pass)
|
|
ftp.passive = True
|
|
thread_local.ftp = ftp
|
|
return thread_local.ftp
|
|
|
|
def close_thread_connection():
|
|
if hasattr(thread_local, "ftp") and thread_local.ftp is not None:
|
|
try:
|
|
thread_local.ftp.quit()
|
|
except:
|
|
try:
|
|
thread_local.ftp.close()
|
|
except:
|
|
pass
|
|
thread_local.ftp = None
|
|
|
|
def ensure_remote_dir(ftp, path):
|
|
parts = path.replace('\\', '/').strip('/').split('/')
|
|
current = ""
|
|
for part in parts:
|
|
if not part:
|
|
continue
|
|
current += "/" + part
|
|
try:
|
|
ftp.mkd(current)
|
|
print(f"\nCreated remote dir: {current}")
|
|
except error_perm:
|
|
# Directory already exists or permission error (which is normal if it exists)
|
|
pass
|
|
|
|
def upload_file_task(local_path, rel_path, ftp_host, ftp_user, ftp_pass, created_dirs, created_dirs_lock):
|
|
ftp_path = rel_path.replace('\\', '/')
|
|
remote_dir = os.path.dirname(ftp_path)
|
|
|
|
# Ensure remote directory exists
|
|
if remote_dir:
|
|
with created_dirs_lock:
|
|
if remote_dir not in created_dirs:
|
|
try:
|
|
ftp = get_ftp_connection(ftp_host, ftp_user, ftp_pass)
|
|
ensure_remote_dir(ftp, remote_dir)
|
|
created_dirs.add(remote_dir)
|
|
except Exception as e:
|
|
thread_local.ftp = None
|
|
try:
|
|
ftp = get_ftp_connection(ftp_host, ftp_user, ftp_pass)
|
|
ensure_remote_dir(ftp, remote_dir)
|
|
created_dirs.add(remote_dir)
|
|
except Exception as err2:
|
|
return False, ftp_path, f"Failed directory creation: {err2}"
|
|
|
|
# Upload the file
|
|
for attempt in range(3):
|
|
try:
|
|
ftp = get_ftp_connection(ftp_host, ftp_user, ftp_pass)
|
|
with open(local_path, 'rb') as f:
|
|
ftp.storbinary(f"STOR {ftp_path}", f)
|
|
return True, ftp_path, None
|
|
except Exception as e:
|
|
thread_local.ftp = None # Force reconnect on next attempt
|
|
if attempt == 2:
|
|
return False, ftp_path, str(e)
|
|
return False, ftp_path, "Unknown error"
|
|
|
|
def main():
|
|
env = load_env()
|
|
ftp_host = "ftp.canticristiani.it"
|
|
ftp_user = "canticristiani.it"
|
|
ftp_pass = env.get("FTP_PASSWORD")
|
|
|
|
if not ftp_pass:
|
|
print("❌ Error: FTP_PASSWORD not found in .env")
|
|
sys.exit(1)
|
|
|
|
local_dir = "www"
|
|
if not os.path.isdir(local_dir):
|
|
print(f"❌ Error: Local dir '{local_dir}' not found.")
|
|
sys.exit(1)
|
|
|
|
files_to_upload = []
|
|
for root, dirs, files in os.walk(local_dir):
|
|
for file in files:
|
|
local_path = os.path.join(root, file)
|
|
rel_path = os.path.relpath(local_path, local_dir)
|
|
files_to_upload.append((local_path, rel_path))
|
|
|
|
total = len(files_to_upload)
|
|
uploaded = 0
|
|
errors = []
|
|
|
|
created_dirs = set()
|
|
created_dirs_lock = threading.Lock()
|
|
progress_lock = threading.Lock()
|
|
|
|
# Thread limit: default to 100 as requested
|
|
num_threads = int(os.environ.get("FTP_THREADS", env.get("FTP_THREADS", "100")))
|
|
print(f"🚀 Starting parallel upload of {total} files using {num_threads} threads...")
|
|
|
|
def run_task(item):
|
|
nonlocal uploaded
|
|
local_path, rel_path = item
|
|
success, ftp_path, err_msg = upload_file_task(
|
|
local_path, rel_path, ftp_host, ftp_user, ftp_pass, created_dirs, created_dirs_lock
|
|
)
|
|
with progress_lock:
|
|
uploaded += 1
|
|
print(f"\rUploading [{uploaded}/{total}] {ftp_path}...", end="", flush=True)
|
|
if not success:
|
|
errors.append((ftp_path, err_msg))
|
|
|
|
with ThreadPoolExecutor(max_workers=num_threads) as executor:
|
|
futures = [executor.submit(run_task, item) for item in files_to_upload]
|
|
for future in as_completed(futures):
|
|
pass
|
|
|
|
# Cleanup connections
|
|
with ThreadPoolExecutor(max_workers=num_threads) as cleanup_executor:
|
|
cleanups = [cleanup_executor.submit(close_thread_connection) for _ in range(num_threads)]
|
|
for future in as_completed(cleanups):
|
|
pass
|
|
|
|
if errors:
|
|
print(f"\n⚠️ Deploy completed with {len(errors)} errors:")
|
|
for path, err in errors:
|
|
print(f" {path}: {err}")
|
|
sys.exit(1)
|
|
else:
|
|
print("\n\n✅ Deploy completed successfully with 100% files uploaded without errors!")
|
|
|
|
if __name__ == '__main__':
|
|
main()
|