⬅ חזרה לתיקייה
✏️ עריכת admin_server_backup_20260701_105541_1.py
C:\TripServer\backups\admin_server\admin_server_backup_20260701_105541_1.py
# -*- coding: utf-8 -*- from flask import Flask, request, redirect, send_from_directory, make_response from werkzeug.utils import secure_filename import os import time import sys import shutil import py_compile import threading import html as html_lib import glob import zipfile import subprocess import hashlib import json app = Flask(__name__) BASE_DIR = r"C:\TripServer" FOLDERS = { "JAPAN": os.path.join(BASE_DIR, "JAPAN"), "USA": os.path.join(BASE_DIR, "USA"), "USA_TV": os.path.join(BASE_DIR, "USA_TV"), "Moneyapp": os.path.join(BASE_DIR, "Moneyapp"), "MoneyHome": os.path.join(BASE_DIR, "MoneyHome"), } APP_FOLDERS = {"JAPAN", "USA", "USA_TV", "Moneyapp", "MoneyHome"} JAPAN_PRESENTATION_FILES = { "flight_intro": "japan-flight-intro.html", "japan_experience": "japan-experience.html", } USA_PRESENTATION_FILENAME = "usa2027-presentation.html" USA_MEDIA_IMAGE_DIR = os.path.join("media", "usa-presentation", "images") USA_MEDIA_MUSIC_DIR = os.path.join("media", "usa-presentation", "music") USA_MUSIC_FILES = { "usa_music": os.path.join(USA_MEDIA_MUSIC_DIR, "usa-roadtrip-theme.mp3"), "usa_music_part2": os.path.join(USA_MEDIA_MUSIC_DIR, "usa-part2-theme.mp3"), } USA_TARGETS = { "phone": { "folder": "USA", "title": "📱 אייפון / שרת פרטי רגיל", "short": "אייפון", "public": "/USA/", "desc": r"C:\TripServer\USA", }, "tv": { "folder": "USA_TV", "title": "📺 טלוויזיה / TV 77OLED", "short": "TV", "public": "/USA_TV/", "desc": r"C:\TripServer\USA_TV", }, } USA_IMAGE_KEYS = ({"intro", "budget", "roadtrip-map", "summary", "extra-big-sur", "extra-diner", "extra-death-valley", "extra-mono-lake"} | {f"day-{i:02d}" for i in range(1, 37)}) USA_IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp"} USA_IMAGE_META = [ ("intro", "פתיחה", "אמריקה על פול ווליום / תמונת Road Trip כללית"), ("roadtrip-map", "מפת המסלול", "מפת Road Trip / מבט כללי על המסלול"), ("day-01", "יום 1", "סן פרנסיסקו / נחיתה ופתיחה"), ("day-02", "יום 2", "סן פרנסיסקו + Alcatraz / מפרץ, אלקטרז, גשר הזהב"), ("day-03", "יום 3", "סן פרנסיסקו → מונטריי / כביש 1, חוף קליפורניה"), ("day-04", "יום 4", "Monterey Bay Whale Watch + Big Sur"), ("extra-big-sur", "שקף Big Sur", "Bixby Bridge / Big Sur / McWay Falls"), ("day-05", "יום 5", "Pismo → לוס אנג׳לס"), ("day-06", "יום 6", "Warner Bros + Beverly Hills + Griffith"), ("day-07", "יום 7", "לוס אנג׳לס → Zion"), ("extra-diner", "שקף Peggy Sue’s Diner", "דיינר אמריקאי קלאסי / עצירת Road Trip"), ("day-08", "יום 8", "Zion → Bryce → Page"), ("day-09", "יום 9", "Antelope Canyon + Horseshoe Bend"), ("day-10", "יום 10", "Page → Grand Canyon South Rim"), ("day-11", "יום 11", "Grand Canyon → Las Vegas"), ("day-12", "יום 12", "Vegas Outlets + Sphere"), ("day-13", "יום 13", "Las Vegas / יום חופשי"), ("day-14", "יום 14", "Las Vegas → Death Valley"), ("extra-death-valley", "שקף Death Valley", "Badwater Basin / מדבר / נופים דרמטיים"), ("day-15", "יום 15", "Death Valley → Mammoth Lakes"), ("extra-mono-lake", "שקף Mono Lake", "Mono Lake / Tioga Road / מעבר ליוסמיטי"), ("day-16", "יום 16", "Yosemite"), ("day-17", "יום 17", "Yosemite → San Francisco"), ("day-18", "יום 18", "טיסה לפלורידה / Orlando"), ("day-19", "יום 19", "Universal Orlando"), ("day-20", "יום 20", "Universal / פארקים"), ("day-21", "יום 21", "Orlando / מנוחה וקניות"), ("day-22", "יום 22", "Disney / פארק"), ("day-23", "יום 23", "Orlando"), ("day-24", "יום 24", "Orlando"), ("day-25", "יום 25", "Orlando → Miami"), ("day-26", "יום 26", "Miami"), ("day-27", "יום 27", "עלייה לקרוז"), ("day-28", "יום 28", "קרוז"), ("day-29", "יום 29", "קרוז / יום ים"), ("day-30", "יום 30", "קרוז / יום ים"), ("day-31", "יום 31", "CocoCay"), ("day-32", "יום 32", "Nassau"), ("day-33", "יום 33", "קרוז / יום ים"), ("day-34", "יום 34", "ירידה מהקרוז / Miami"), ("day-35", "יום 35", "Miami / קניות וסיום"), ("day-36", "יום 36", "חזרה הביתה"), ("budget", "תקציב", "שקף תקציב"), ("summary", "סיום", "תמונת סיום משפחתית"), ] # Canonical admin path: future self-updates must always replace the real server file # under C:\TripServer, not an accidental copy determined by the current working directory. CANONICAL_ADMIN_SERVER_PATH = os.path.join(BASE_DIR, "admin_server.py") RUNNING_SCRIPT_PATH = os.path.abspath(__file__) ARGV_SCRIPT_PATH = os.path.abspath(sys.argv[0]) if sys.argv and sys.argv[0] else RUNNING_SCRIPT_PATH ADMIN_SERVER_PATH = CANONICAL_ADMIN_SERVER_PATH ADMIN_BACKUP_DIR = os.path.join(BASE_DIR, "backups", "admin_server") ADMIN_UPLOAD_TMP_DIR = os.path.join(BASE_DIR, "tmp", "admin_server_uploads") ADMIN_RESTART_CMD_PATH = os.path.join(BASE_DIR, "restart_admin_server.cmd") ADMIN_RESTART_LOG_PATH = os.path.join(BASE_DIR, "admin_restart.log") ADMIN_UPDATE_MARKER_PATH = os.path.join(BASE_DIR, "admin_update_marker.json") ADMIN_PANEL_VERSION = "multi-page USA phone TV separated v5 canonical self-update" ADMIN_MAX_UPLOAD_BYTES = 2 * 1024 * 1024 USA_IMAGES_ZIP_MAX_BYTES = 80 * 1024 * 1024 USA_IMAGES_ZIP_MAX_FILES = 180 USA_IMAGES_ZIP_MAX_UNCOMPRESSED_BYTES = 160 * 1024 * 1024 USA_IMAGES_ZIP_MAX_SINGLE_FILE_BYTES = 12 * 1024 * 1024 USA_PACKAGE_ZIP_MAX_BYTES = 220 * 1024 * 1024 USA_PACKAGE_ZIP_MAX_FILES = 360 USA_PACKAGE_ZIP_MAX_UNCOMPRESSED_BYTES = 320 * 1024 * 1024 for folder_path in FOLDERS.values(): os.makedirs(folder_path, exist_ok=True) for target in ("USA", "USA_TV"): os.makedirs(os.path.join(FOLDERS[target], USA_MEDIA_IMAGE_DIR), exist_ok=True) os.makedirs(os.path.join(FOLDERS[target], USA_MEDIA_MUSIC_DIR), exist_ok=True) os.makedirs(ADMIN_BACKUP_DIR, exist_ok=True) os.makedirs(ADMIN_UPLOAD_TMP_DIR, exist_ok=True) def no_cache_response(response): response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0" response.headers["Pragma"] = "no-cache" response.headers["Expires"] = "0" return response def find_html_file(folder_path): index_path = os.path.join(folder_path, "index.html") if os.path.exists(index_path): return "index.html" if not os.path.isdir(folder_path): return None html_files = [f for f in os.listdir(folder_path) if f.lower().endswith((".html", ".htm"))] return html_files[0] if html_files else None def clean_wrong_main_files(folder_path): if not os.path.isdir(folder_path): return wrong_names = {"html", "htm", "index", "index.htm", "download", "unknown"} for name in os.listdir(folder_path): full_path = os.path.join(folder_path, name) if os.path.isfile(full_path) and name.lower() in wrong_names: try: os.remove(full_path) except Exception: pass def save_uploaded_file(folder, uploaded_file): if folder not in FOLDERS: return None folder_path = FOLDERS[folder] os.makedirs(folder_path, exist_ok=True) filename = "index.html" uploaded_file.save(os.path.join(folder_path, filename)) clean_wrong_main_files(folder_path) return filename def normalize_usa_mode(mode): mode = (mode or "").strip().lower() if mode in ("phone", "usa", "iphone", "server", "private", "usa_presentation"): return "phone" if mode in ("tv", "usa_tv", "television", "oled"): return "tv" return None def usa_target_folder(mode): mode = normalize_usa_mode(mode) if not mode: return None return FOLDERS[USA_TARGETS[mode]["folder"]] def save_japan_presentation(presentation_key, uploaded_file): if presentation_key not in JAPAN_PRESENTATION_FILES: return None folder = FOLDERS["JAPAN"] os.makedirs(folder, exist_ok=True) filename = JAPAN_PRESENTATION_FILES[presentation_key] uploaded_file.save(os.path.join(folder, filename)) return filename def save_usa_presentation(mode, uploaded_file): folder = usa_target_folder(mode) if not folder: return None os.makedirs(folder, exist_ok=True) uploaded_file.save(os.path.join(folder, USA_PRESENTATION_FILENAME)) return USA_PRESENTATION_FILENAME def save_usa_music(mode, music_key, uploaded_file): folder = usa_target_folder(mode) if not folder or music_key not in USA_MUSIC_FILES: return None rel_path = USA_MUSIC_FILES[music_key] full_path = os.path.join(folder, rel_path) os.makedirs(os.path.dirname(full_path), exist_ok=True) uploaded_file.save(full_path) return rel_path def save_usa_presentation_image(mode, image_key, uploaded_file): folder = usa_target_folder(mode) if not folder or image_key not in USA_IMAGE_KEYS: return None image_dir = os.path.join(folder, USA_MEDIA_IMAGE_DIR) os.makedirs(image_dir, exist_ok=True) target_ext = ".jpg" for old_ext in USA_IMAGE_EXTENSIONS: old_path = os.path.join(image_dir, image_key + old_ext) if os.path.exists(old_path): try: os.remove(old_path) except Exception: pass target_path = os.path.join(image_dir, image_key + target_ext) try: from PIL import Image uploaded_file.stream.seek(0) img = Image.open(uploaded_file.stream) if img.mode not in ("RGB", "L"): img = img.convert("RGB") elif img.mode == "L": img = img.convert("RGB") img.save(target_path, "JPEG", quality=90, optimize=True) except Exception: try: uploaded_file.stream.seek(0) except Exception: pass uploaded_file.save(target_path) return os.path.join(USA_MEDIA_IMAGE_DIR, image_key + target_ext) def save_usa_presentation_images_zip(mode, uploaded_file): folder = usa_target_folder(mode) if not folder: raise ValueError("bad_usa_mode") image_dir = os.path.join(folder, USA_MEDIA_IMAGE_DIR) os.makedirs(image_dir, exist_ok=True) stamp = time.strftime("%Y%m%d_%H%M%S") tmp_zip = os.path.join(ADMIN_UPLOAD_TMP_DIR, f"usa_images_{mode}_{stamp}.zip") os.makedirs(ADMIN_UPLOAD_TMP_DIR, exist_ok=True) uploaded_file.save(tmp_zip) zip_size = os.path.getsize(tmp_zip) if zip_size > USA_IMAGES_ZIP_MAX_BYTES: try: os.remove(tmp_zip) except Exception: pass raise ValueError(f"zip_size_exceeded:{zip_size}") imported, skipped, seen = [], [], set() try: with zipfile.ZipFile(tmp_zip, "r") as zf: infos = [info for info in zf.infolist() if not info.is_dir()] if len(infos) > USA_IMAGES_ZIP_MAX_FILES: raise ValueError(f"too_many_files:{len(infos)}") total_uncompressed = sum(max(0, info.file_size) for info in infos) if total_uncompressed > USA_IMAGES_ZIP_MAX_UNCOMPRESSED_BYTES: raise ValueError(f"zip_uncompressed_size_exceeded:{total_uncompressed}") for info in infos: raw_name = info.filename.replace("\\", "/") base = os.path.basename(raw_name) if not base or base.startswith(".") or raw_name.startswith("__MACOSX/"): continue if info.file_size > USA_IMAGES_ZIP_MAX_SINGLE_FILE_BYTES: skipped.append(base + " — גדול מדי") continue stem, ext = os.path.splitext(base) stem = stem.strip() ext = ext.lower() if ext not in USA_IMAGE_EXTENSIONS: skipped.append(base + " — סיומת לא נתמכת") continue if stem not in USA_IMAGE_KEYS: skipped.append(base + " — שם לא שייך למצגת") continue if stem in seen: skipped.append(base + " — כפילות ב־ZIP") continue seen.add(stem) for old_ext in USA_IMAGE_EXTENSIONS: old_path = os.path.join(image_dir, stem + old_ext) if os.path.exists(old_path): try: os.remove(old_path) except Exception: pass target = os.path.join(image_dir, stem + ".jpg") try: from PIL import Image with zf.open(info, "r") as source: img = Image.open(source) img.verify() with zf.open(info, "r") as source: img = Image.open(source) if img.mode not in ("RGB", "L"): img = img.convert("RGB") elif img.mode == "L": img = img.convert("RGB") img.save(target, "JPEG", quality=90, optimize=True, progressive=True) except Exception: with zf.open(info, "r") as source, open(target, "wb") as dest: shutil.copyfileobj(source, dest) imported.append(stem + ".jpg") finally: try: os.remove(tmp_zip) except Exception: pass imported.sort() skipped.sort() missing = sorted(key + ".jpg" for key in USA_IMAGE_KEYS if key + ".jpg" not in imported) return imported, skipped, missing def validate_admin_server_upload(tmp_path): size = os.path.getsize(tmp_path) if size <= 0: return False, "הקובץ ריק." if size > ADMIN_MAX_UPLOAD_BYTES: return False, f"הקובץ גדול מדי: {size:,} bytes. המגבלה היא {ADMIN_MAX_UPLOAD_BYTES:,} bytes." try: py_compile.compile(tmp_path, doraise=True) except py_compile.PyCompileError as exc: return False, "בדיקת קומפילציה נכשלה:\n" + str(exc) except Exception as exc: return False, "שגיאה בבדיקת הקובץ:\n" + repr(exc) try: with open(tmp_path, "r", encoding="utf-8", errors="ignore") as fh: content = fh.read(500000) except Exception as exc: return False, "לא ניתן לקרוא את הקובץ:\n" + repr(exc) required_tokens = ["Flask", "@app.route", "app.run"] missing = [token for token in required_tokens if token not in content] if missing: return False, "הקובץ עבר קומפילציה, אבל לא נראה כמו admin_server.py של TripServer. חסרים סימנים: " + ", ".join(missing) return True, "OK" def admin_status_page(title, message, ok=True, details="", back="/admin"): color = "#16a34a" if ok else "#dc2626" bg = "#052e1b" if ok else "#3f1d1d" safe_title = html_lib.escape(str(title)) safe_message = html_lib.escape(str(message)).replace("\n", "<br>") safe_details = html_lib.escape(str(details)).replace("\n", "<br>") if details else "" safe_back = html_lib.escape(back or "/admin") details_html = f"<pre>{safe_details}</pre>" if safe_details else "" return f""" <!DOCTYPE html><html lang="he" dir="rtl"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>{safe_title}</title> <style>body{{margin:0;font-family:Arial;background:#0f172a;color:white;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}}.card{{width:min(860px,94vw);background:#1e293b;border-radius:24px;padding:26px;box-shadow:0 18px 70px rgba(0,0,0,.35)}}.badge{{display:inline-block;background:{bg};border:1px solid {color};color:white;border-radius:999px;padding:8px 14px;font-weight:bold;margin-bottom:12px}}h1{{margin:0 0 12px;font-size:30px}}p{{line-height:1.7;color:#e2e8f0}}pre{{white-space:pre-wrap;direction:ltr;text-align:left;background:#020617;color:#fde68a;border:1px solid #334155;border-radius:14px;padding:14px;overflow:auto;max-height:360px}}a,button{{display:inline-block;margin:8px 0 0 8px;background:#2563eb;color:white;border:0;border-radius:12px;padding:12px 16px;text-decoration:none;font-weight:bold;cursor:pointer}}button.restart{{background:#ea580c}}form{{display:inline}}</style></head> <body><div class="card"><div class="badge">TripServer Admin</div><h1>{safe_title}</h1><p>{safe_message}</p>{details_html}<a href="{safe_back}">חזרה</a><a href="/admin">דף ראשי</a><form action="/restart_admin_server" method="post" onsubmit="return confirm('לאתחל את הפאנל עכשיו?')"><button class="restart">אתחל פאנל עכשיו</button></form></div></body></html>""" def file_sha256(path): try: h = hashlib.sha256() with open(path, "rb") as fh: for chunk in iter(lambda: fh.read(1024 * 1024), b""): h.update(chunk) return h.hexdigest() except Exception: return "" def admin_script_paths_to_update(): """Return every plausible admin script path that should be replaced. The old panel used os.path.abspath(__file__), which can point to the file that happened to start the process. For reliable future self-updates we update both the canonical TripServer admin file and the currently running script path when they differ. """ candidates = [CANONICAL_ADMIN_SERVER_PATH, RUNNING_SCRIPT_PATH, ARGV_SCRIPT_PATH] out = [] for path in candidates: if not path: continue path = os.path.abspath(path) if not path.lower().endswith(".py"): continue if path not in out: out.append(path) return out def write_admin_update_marker(upload_hash, updated_paths, backup_paths): try: payload = { "ok": True, "version": ADMIN_PANEL_VERSION, "time": time.strftime("%Y-%m-%d %H:%M:%S"), "pid_that_updated": os.getpid(), "cwd": os.getcwd(), "running_script_path": RUNNING_SCRIPT_PATH, "argv_script_path": ARGV_SCRIPT_PATH, "canonical_admin_server_path": CANONICAL_ADMIN_SERVER_PATH, "upload_sha256": upload_hash, "updated_paths": updated_paths, "backup_paths": backup_paths, } with open(ADMIN_UPDATE_MARKER_PATH, "w", encoding="utf-8") as fh: json.dump(payload, fh, ensure_ascii=False, indent=2) except Exception as exc: log_admin_restart("failed writing admin_update_marker: " + repr(exc)) def backup_current_admin_server(prefix="admin_server_backup"): os.makedirs(ADMIN_BACKUP_DIR, exist_ok=True) stamp = time.strftime("%Y%m%d_%H%M%S") source = ADMIN_SERVER_PATH if os.path.exists(ADMIN_SERVER_PATH) else RUNNING_SCRIPT_PATH backup_path = os.path.join(ADMIN_BACKUP_DIR, f"{prefix}_{stamp}.py") if os.path.exists(source): shutil.copy2(source, backup_path) else: with open(backup_path, "w", encoding="utf-8") as fh: fh.write("# no previous admin_server.py file was found during backup\n") return backup_path def latest_admin_backup(): os.makedirs(ADMIN_BACKUP_DIR, exist_ok=True) backups = glob.glob(os.path.join(ADMIN_BACKUP_DIR, "admin_server_backup_*.py")) return max(backups, key=os.path.getmtime) if backups else None def log_admin_restart(message): try: os.makedirs(BASE_DIR, exist_ok=True) with open(ADMIN_RESTART_LOG_PATH, "a", encoding="utf-8") as fh: fh.write(time.strftime("%Y-%m-%d %H:%M:%S") + " | " + str(message) + "\n") except Exception: pass def restart_process_later(): time.sleep(0.8) script_path = os.path.abspath(ADMIN_SERVER_PATH) python_exe = sys.executable or "python" work_dir = os.path.dirname(script_path) or BASE_DIR try: log_admin_restart(f"restart requested | script={script_path} | python={python_exe} | argv={sys.argv}") if os.name == "nt": cmd = "\r\n".join([ "@echo off", 'cd /d "{}"'.format(work_dir), "timeout /t 2 /nobreak >nul", 'start "TripServer Admin" /min "{}" "{}"'.format(python_exe, script_path), "" ]) with open(ADMIN_RESTART_CMD_PATH, "w", encoding="utf-8") as fh: fh.write(cmd) subprocess.Popen(["cmd", "/c", ADMIN_RESTART_CMD_PATH], cwd=work_dir, close_fds=True) os._exit(0) else: subprocess.Popen([python_exe, script_path], cwd=work_dir, close_fds=True, start_new_session=True) os._exit(0) except Exception as exc: log_admin_restart("restart failed: " + repr(exc)) try: os.execv(python_exe, [python_exe, script_path]) except Exception as exc2: log_admin_restart("execv fallback failed: " + repr(exc2)) os._exit(1) def serve_app(folder): file_name = find_html_file(FOLDERS[folder]) if not file_name: return f"No HTML file found in {folder} folder", 404 response = make_response(send_from_directory(FOLDERS[folder], file_name)) return no_cache_response(response) def css(): return """ body{margin:0;font-family:Arial,Helvetica,sans-serif;background:#0f172a;color:white;min-height:100vh}.wrap{width:min(1180px,92%);margin:auto;padding:34px 0 60px}.top{display:flex;justify-content:space-between;gap:14px;align-items:center;margin-bottom:22px}.crumbs a,.back{color:#93c5fd;text-decoration:none;font-weight:900}.hero{text-align:center;margin:18px 0 28px}.hero h1{font-size:clamp(30px,5vw,52px);margin:0 0 8px}.hero p{color:#cbd5e1;line-height:1.65;margin:0}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:18px}.card{display:block;background:linear-gradient(180deg,#1e293b,#111827);border:1px solid #334155;border-radius:24px;padding:24px;text-decoration:none;color:white;box-shadow:0 14px 38px rgba(0,0,0,.25);min-height:145px}.card:hover{border-color:#60a5fa;transform:translateY(-2px)}.icon{font-size:40px;margin-bottom:12px}.card h2{margin:0 0 10px;font-size:24px}.card p{margin:0;color:#cbd5e1;line-height:1.5}.box{background:#1e293b;border:1px solid #334155;border-radius:22px;padding:24px;margin-bottom:22px}.box h2{margin-top:0}.box h3{margin-bottom:10px}.note{color:#cbd5e1;font-size:14px;line-height:1.6}.warning{background:#3f1d1d;border:1px solid #7f1d1d;color:#fecaca;border-radius:14px;padding:12px;line-height:1.6;margin-bottom:14px}.good{background:#143326;border:1px solid #1f7a54;color:#bbf7d0;border-radius:14px;padding:12px;line-height:1.6;margin:12px 0}.path{direction:ltr;text-align:left;background:#020617;color:#fde68a;border:1px solid #334155;border-radius:12px;padding:10px;display:block;margin:10px 0;overflow:auto}input[type=file],select{width:100%;box-sizing:border-box;padding:14px;background:#0f172a;color:white;border-radius:12px;border:1px solid #334155}button,.btn{display:inline-block;margin-top:14px;border:0;background:#2563eb;color:white;padding:14px 20px;border-radius:12px;font-size:16px;font-weight:900;cursor:pointer;text-decoration:none}.btn.secondary{background:#334155}.btn.orange,button.orange{background:#ea580c}.preview-links{display:flex;gap:10px;flex-wrap:wrap;margin-top:14px}.preview-links a{display:inline-block;color:white;background:#334155;border:1px solid #475569;border-radius:12px;padding:10px 14px;text-decoration:none;font-weight:bold}.image-upload-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:12px;margin-top:16px}.image-upload-card{margin:0}.image-file-input{position:absolute;opacity:0;width:1px;height:1px;pointer-events:none}.image-upload-label{display:block;height:100%;cursor:pointer;background:#0f172a;border:1px solid #334155;border-radius:18px;padding:15px;transition:.18s transform,.18s border-color,.18s background;box-shadow:0 8px 22px rgba(0,0,0,.14)}.image-upload-label:hover{transform:translateY(-2px);border-color:#60a5fa;background:#13213a}.image-key{display:inline-flex;background:#1d4ed8;color:#dbeafe;border-radius:999px;padding:4px 9px;font-size:12px;font-weight:900;margin-bottom:8px}.image-title{display:block;color:#fff;font-size:17px;font-weight:950;margin-bottom:7px;line-height:1.25}.image-desc{display:block;color:#cbd5e1;font-size:14px;line-height:1.45;min-height:42px}.upload-status{display:block;margin-top:12px;color:#fde68a;font-size:13px;font-weight:900;border-top:1px solid #334155;padding-top:10px}.split{display:grid;grid-template-columns:1fr 1fr;gap:16px}@media(max-width:740px){.split{grid-template-columns:1fr}.top{display:block}.image-upload-grid{grid-template-columns:1fr}.image-title{font-size:16px}.image-desc{min-height:auto}} """ def page(title, content, subtitle="", back="/admin"): safe_title = html_lib.escape(title) safe_subtitle = html_lib.escape(subtitle) back_html = f'<a class="back" href="{html_lib.escape(back)}">⬅ חזרה</a>' if back else "" return f""" <!DOCTYPE html><html lang="he" dir="rtl"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>{safe_title}</title><style>{css()}</style></head> <body><div class="wrap"><div class="top"><div>{back_html}</div><div class="crumbs"><a href="/admin">דף ראשי</a></div></div><div class="hero"><h1>{safe_title}</h1><p>{safe_subtitle}</p></div>{content}</div></body></html>""" def card(icon, title, text, href): return f'<a class="card" href="{html_lib.escape(href)}"><div class="icon">{icon}</div><h2>{html_lib.escape(title)}</h2><p>{html_lib.escape(text)}</p></a>' def upload_box(title, action, button, accept=".html,.htm", note="", extra=""): return f"""<div class="box"><h2>{title}</h2>{extra}<form action="{html_lib.escape(action)}" method="post" enctype="multipart/form-data"><input type="file" name="file" accept="{html_lib.escape(accept)}"><button>{html_lib.escape(button)}</button></form>{('<p class="note">'+note+'</p>') if note else ''}</div>""" def usa_preview_links(mode): t = USA_TARGETS[mode] public = t["public"] return f"""<div class="preview-links"><a href="{public}" target="_blank">פתיחת קובץ טיול</a><a href="{public}{USA_PRESENTATION_FILENAME}?from=admin" target="_blank">בדיקת מצגת</a></div>""" def image_cards(mode): cards = [] for key, title, desc in USA_IMAGE_META: safe_key = html_lib.escape(key) safe_title = html_lib.escape(title) safe_desc = html_lib.escape(desc) input_id = f"image-file-{mode}-{key}" cards.append(f"""<form class="image-upload-card" action="/upload_usa_image/{mode}" method="post" enctype="multipart/form-data"><input type="hidden" name="image_key" value="{safe_key}"><input class="image-file-input" id="{input_id}" type="file" name="file" accept=".jpg,.jpeg,.png,.webp,image/*" onchange="if(this.files && this.files.length){{this.closest('form').querySelector('.upload-status').textContent='מעלה תמונה...';this.closest('form').submit();}}"><label for="{input_id}" class="image-upload-label"><span class="image-key">{safe_key}</span><span class="image-title">{safe_title}</span><span class="image-desc">{safe_desc}</span><span class="upload-status">לחץ כאן ובחר תמונה</span></label></form>""") return "<div class=\"image-upload-grid\">" + "".join(cards) + "</div>" def admin_main_html(): content = '<div class="grid">' + "".join([ card("🇺🇸", "טיול ארצות הברית 27", "כניסה לדף משנה ייעודי לאייפון, TV, מצגות, תמונות ושירים.", "/admin/usa"), card("🇯🇵", "טיול יפן 26", "קובץ הטיול ומצגות יפן.", "/admin/japan"), card("💰", "אפליקציית תקציב חופשה", "עדכון אפליקציית תקציב הטיולים.", "/admin/moneyapp"), card("🏠", "אפליקציית תקציב חודשי משפחתי", "עדכון אפליקציית התקציב המשפחתי.", "/admin/moneyhome"), card("🛠️", "עדכון פנל החכם", "העלאת admin_server.py, גיבוי, שחזור ואתחול.", "/admin/update"), ]) + '</div>' return page("פנל חכם", content, "דף ראשי נקי — בחר אזור ניהול", back="/") @app.route("/admin") @app.route("/admin/") def admin(): return admin_main_html() @app.route("/") def home(): content = '<div class="grid">' + "".join([ card("📁", "פנל חכם", "ניהול ועדכון קבצים.", "/admin"), card("🇺🇸", "טיול ארה״ב — אייפון", "פתיחת קובץ הטיול הרגיל.", "/USA/"), card("📺", "טיול ארה״ב — TV", "פתיחת גרסת מסך טלוויזיה.", "/USA_TV/"), card("🇯🇵", "טיול יפן", "לוז, ניווטים ומסמכי הטיול.", "/JAPAN/"), card("💰", "תקציב טיולים", "ניהול תקציב חופשות.", "/Moneyapp/"), card("🏠", "תקציב משפחתי", "ניהול תקציב חודשי.", "/MoneyHome/"), ]) + '</div>' return page("TripServer", content, "מסך פתיחה", back="") @app.route("/admin/usa") def admin_usa(): content = '<div class="grid">' + "".join([ card("📱", "עדכון קובץ טיול ארצות הברית למסך אייפון", r"יעד: C:\TripServer\USA\index.html", "/admin/usa/trip/phone"), card("🎬", "עדכון מצגת ארצות הברית למסך אייפון", r"יעד: C:\TripServer\USA\usa2027-presentation.html", "/admin/usa/presentation/phone"), card("📺", "עדכון קובץ טיול ארצות הברית למסך טלוויזיה", r"יעד: C:\TripServer\USA_TV\index.html", "/admin/usa/trip/tv"), card("🎞️", "עדכון מצגת ארצות הברית למסך טלוויזיה", r"יעד: C:\TripServer\USA_TV\usa2027-presentation.html", "/admin/usa/presentation/tv"), ]) + '</div>' return page("ארצות הברית 2027", content, "בחירה נפרדת לפי יעד תצוגה — אייפון מול TV", back="/admin") @app.route("/admin/usa/trip/<mode>") def admin_usa_trip(mode): mode = normalize_usa_mode(mode) if not mode: return "Bad USA mode", 404 t = USA_TARGETS[mode] action = "/upload/" + t["folder"] target_path = t["desc"] + r"\index.html" extra = f'<div class="good">זה דף עדכון קובץ הטיול בלבד עבור <strong>{t["title"]}</strong>.</div><code class="path">{target_path}</code>' content = upload_box(f"{t['title']} — קובץ טיול", action, "העלה קובץ טיול", note="הקובץ יישמר בשם index.html בנתיב שמופיע למעלה.", extra=extra) content += usa_preview_links(mode) return page(f"קובץ טיול ארה״ב — {t['short']}", content, "אין כאן מצגות, תמונות או שירים — רק קובץ הטיול", back="/admin/usa") @app.route("/admin/usa/presentation/<mode>") def admin_usa_presentation(mode): mode = normalize_usa_mode(mode) if not mode: return "Bad USA mode", 404 t = USA_TARGETS[mode] target_root = t["desc"] content = f"""<div class="box"><h2>{t['title']} — מצגת, תמונות ושירים</h2><div class="good">כל מה שנמצא בדף הזה נשמר רק תחת היעד הזה, ללא ערבוב בין אייפון לבין TV.</div><code class="path">{target_root}</code>{usa_preview_links(mode)}</div>""" content += upload_box("קובץ מצגת", f"/upload_usa_presentation/{mode}", "העלה קובץ מצגת", note=f"יישמר בשם {USA_PRESENTATION_FILENAME}") content += upload_box("ZIP תמונות מלא", f"/upload_usa_images_zip/{mode}", "העלה ZIP תמונות", accept=".zip,application/zip,application/x-zip-compressed", note="התמונות יישמרו בתיקיית media/usa-presentation/images של היעד הנוכחי בלבד.") content += """<div class="box"><h2>תמונות בודדות לפי שקף</h2><p class="note">אפשר להחליף נקודתית כל תמונה. הקובץ יישמר כ־JPG בשם השקף.</p>""" + image_cards(mode) + "</div>" content += f"""<div class="box"><h2>שירים למצגת</h2><div class="split"><div><h3>שיר חלק 1</h3><form action="/upload_usa_music/{mode}/usa_music" method="post" enctype="multipart/form-data"><input type="file" name="file" accept=".mp3,audio/mpeg"><button>העלה שיר חלק 1</button></form><p class="note">usa-roadtrip-theme.mp3</p></div><div><h3>שיר חלק 2</h3><form action="/upload_usa_music/{mode}/usa_music_part2" method="post" enctype="multipart/form-data"><input type="file" name="file" accept=".mp3,audio/mpeg"><button>העלה שיר חלק 2</button></form><p class="note">usa-part2-theme.mp3</p></div></div></div>""" if mode == "tv": content += upload_box("ZIP מלא של תיקיית USA_TV", "/upload_usa_tv_package_zip", "העלה ZIP מלא של USA_TV", accept=".zip,application/zip,application/x-zip-compressed", note="מחליף את כל תיקיית USA_TV לאחר יצירת גיבוי. ה־ZIP חייב לכלול index.html ו־usa2027-presentation.html.", extra="<div class='warning'>זה אזור TV בלבד. הוא לא משנה את תיקיית USA של האייפון.</div>") return page(f"מצגת ארה״ב — {t['short']}", content, "מצגת + מדיה ביעד נפרד", back="/admin/usa") @app.route("/admin/japan") def admin_japan(): content = upload_box("קובץ טיול יפן", "/upload/JAPAN", "העלה קובץ טיול יפן", note="יישמר כ־index.html בתיקיית JAPAN.") content += upload_box("פתיח טיסה יפן", "/upload_presentation/flight_intro", "העלה פתיח טיסה", note="יישמר כ־japan-flight-intro.html") content += upload_box("מצגת יפן", "/upload_presentation/japan_experience", "העלה מצגת יפן", note="יישמר כ־japan-experience.html") content += '<div class="preview-links"><a href="/JAPAN/" target="_blank">פתיחת יפן</a><a href="/JAPAN/japan-flight-intro.html" target="_blank">בדיקת פתיח</a><a href="/JAPAN/japan-experience.html" target="_blank">בדיקת מצגת</a></div>' return page("טיול יפן 2026", content, "קובץ טיול ומצגות", back="/admin") @app.route("/admin/moneyapp") def admin_moneyapp(): content = upload_box("אפליקציית תקציב חופשה", "/upload/Moneyapp", "העלה אפליקציית תקציב חופשה", note="יישמר כ־index.html בתיקיית Moneyapp.") content += '<div class="preview-links"><a href="/Moneyapp/" target="_blank">פתיחת אפליקציית תקציב חופשה</a></div>' return page("אפליקציית תקציב חופשה", content, "עדכון קובץ האפליקציה", back="/admin") @app.route("/admin/moneyhome") def admin_moneyhome(): content = upload_box("אפליקציית תקציב חודשי משפחתי", "/upload/MoneyHome", "העלה אפליקציית תקציב משפחתי", note="יישמר כ־index.html בתיקיית MoneyHome.") content += '<div class="preview-links"><a href="/MoneyHome/" target="_blank">פתיחת אפליקציית תקציב משפחתי</a></div>' return page("אפליקציית תקציב חודשי משפחתי", content, "עדכון קובץ האפליקציה", back="/admin") @app.route("/admin/update") def admin_update(): content = """<div class="box"><h2>עדכון פנל החכם</h2><div class="warning">העלאת admin_server.py מחליפה את הפאנל הפעיל רק לאחר בדיקת קומפילציה ויצירת גיבוי.</div><form action="/upload_admin_server" method="post" enctype="multipart/form-data"><input type="file" name="file" accept=".py"><button class="orange">העלה admin_server.py</button></form><div class="preview-links"><form action="/restart_admin_server" method="post"><button class="orange">אתחל פאנל</button></form><form action="/restore_admin_server_backup" method="post"><button>שחזר גיבוי אחרון</button></form></div></div>""" return page("עדכון פנל החכם", content, "עדכון עצמי, גיבויים ואתחול", back="/admin") @app.route("/health") def health(): marker = None try: if os.path.exists(ADMIN_UPDATE_MARKER_PATH): with open(ADMIN_UPDATE_MARKER_PATH, "r", encoding="utf-8") as fh: marker = json.load(fh) except Exception as exc: marker = {"read_error": repr(exc)} return { "ok": True, "service": "TripServer Admin", "version": ADMIN_PANEL_VERSION, "usa_tv": True, "pid": os.getpid(), "cwd": os.getcwd(), "admin_server_path": ADMIN_SERVER_PATH, "running_script_path": RUNNING_SCRIPT_PATH, "argv_script_path": ARGV_SCRIPT_PATH, "admin_sha256": file_sha256(ADMIN_SERVER_PATH), "running_sha256": file_sha256(RUNNING_SCRIPT_PATH), "update_marker": marker, } @app.route("/upload_admin_server", methods=["POST"]) def upload_admin_server(): file = request.files.get("file") if not file or file.filename == "": return admin_status_page("לא נבחר קובץ", "בחר קובץ admin_server.py להעלאה.", ok=False, back="/admin/update"), 400 original_name = file.filename or "" if not original_name.lower().endswith(".py"): return admin_status_page("קובץ לא תקין", "ניתן להעלות כאן רק קובץ Python עם סיומת .py", ok=False, back="/admin/update"), 400 if request.content_length and request.content_length > ADMIN_MAX_UPLOAD_BYTES + 250000: return admin_status_page("קובץ גדול מדי", f"הקובץ גדול מהמותר. המגבלה היא בערך {ADMIN_MAX_UPLOAD_BYTES:,} bytes.", ok=False, back="/admin/update"), 413 os.makedirs(ADMIN_UPLOAD_TMP_DIR, exist_ok=True) stamp = time.strftime("%Y%m%d_%H%M%S") tmp_path = os.path.join(ADMIN_UPLOAD_TMP_DIR, f"admin_server_upload_{stamp}.py") try: file.save(tmp_path) valid, details = validate_admin_server_upload(tmp_path) if not valid: try: os.remove(tmp_path) except Exception: pass return admin_status_page("העלאת admin_server.py נדחתה", "הקובץ הקיים לא הוחלף. נמצאה בעיה בקובץ שהעלית.", ok=False, details=details, back="/admin/update"), 400 upload_hash = file_sha256(tmp_path) paths_to_update = admin_script_paths_to_update() backup_paths = [] updated_paths = [] errors = [] os.makedirs(ADMIN_BACKUP_DIR, exist_ok=True) for target_path in paths_to_update: try: os.makedirs(os.path.dirname(target_path), exist_ok=True) if os.path.exists(target_path): backup_path = os.path.join(ADMIN_BACKUP_DIR, f"admin_server_backup_{stamp}_{len(backup_paths)+1}.py") shutil.copy2(target_path, backup_path) backup_paths.append(backup_path) shutil.copy2(tmp_path, target_path) written_hash = file_sha256(target_path) if written_hash != upload_hash: errors.append(f"{target_path}: sha256 mismatch after copy") else: updated_paths.append(target_path) except Exception as exc: errors.append(f"{target_path}: {repr(exc)}") try: os.remove(tmp_path) except Exception: pass if errors or not updated_paths: return admin_status_page( "שגיאה בעדכון admin_server.py", "הקובץ החדש עבר קומפילציה, אבל לא הצלחנו לוודא שהוא נכתב לכל נתיבי ההפעלה הדרושים.", ok=False, details="נתיבי יעד:\n" + "\n".join(paths_to_update) + "\n\nשגיאות:\n" + "\n".join(errors), back="/admin/update" ), 500 write_admin_update_marker(upload_hash, updated_paths, backup_paths) log_admin_restart("admin upload completed | hash=" + upload_hash + " | updated=" + repr(updated_paths)) return admin_status_page( "admin_server.py עודכן בהצלחה", "הקובץ החדש עבר קומפילציה, נכתב לנתיב הקנוני וגם לנתיב שמריץ כרגע את הפאנל, וה־SHA256 אומת. לחץ אתחל פאנל כדי להפעיל אותו.", ok=True, details=( "גרסה חדשה:\n" + ADMIN_PANEL_VERSION + "\n\nSHA256:\n" + upload_hash + "\n\nנתיבים שעודכנו:\n" + "\n".join(updated_paths) + "\n\nגיבויים:\n" + ("\n".join(backup_paths) if backup_paths else "לא היה קובץ קודם לגיבוי") + "\n\nקובץ סימון:\n" + ADMIN_UPDATE_MARKER_PATH ), back="/admin/update" ) except Exception as exc: try: if os.path.exists(tmp_path): os.remove(tmp_path) except Exception: pass return admin_status_page("שגיאה בעדכון admin_server.py", "הקובץ הקיים לא הוחלף.", ok=False, details=repr(exc), back="/admin/update"), 500 @app.route("/restart_admin_server", methods=["POST"]) def restart_admin_server(): threading.Thread(target=restart_process_later, daemon=True).start() return admin_status_page("הפאנל מופעל מחדש", "המתן 5–10 שניות ואז רענן את הפאנל.", ok=True, back="/admin/update") @app.route("/restore_admin_server_backup", methods=["POST"]) def restore_admin_server_backup(): backup_path = latest_admin_backup() if not backup_path: return admin_status_page("אין גיבוי לשחזור", "לא נמצא קובץ גיבוי בתיקיית הגיבויים.", ok=False, back="/admin/update"), 404 try: valid, details = validate_admin_server_upload(backup_path) if not valid: return admin_status_page("הגיבוי האחרון לא תקין", "הגיבוי לא שוחזר כי הוא לא עבר בדיקת תקינות.", ok=False, details=details, back="/admin/update"), 400 safety_backup = backup_current_admin_server("admin_server_before_restore") shutil.copy2(backup_path, ADMIN_SERVER_PATH) return admin_status_page("שחזור גיבוי הסתיים", "הגיבוי האחרון שוחזר אל admin_server.py. לחץ אתחל פאנל כדי להפעיל אותו.", ok=True, details=f"שוחזר מתוך:\n{backup_path}\n\nגיבוי בטיחות:\n{safety_backup}", back="/admin/update") except Exception as exc: return admin_status_page("שגיאה בשחזור גיבוי", "השחזור לא הושלם.", ok=False, details=repr(exc), back="/admin/update"), 500 @app.route("/upload/<folder>", methods=["POST"]) def upload(folder): if folder not in FOLDERS: return "Folder not found", 404 file = request.files.get("file") if not file or file.filename == "": return "No file selected", 400 save_uploaded_file(folder, file) return redirect(request.referrer or "/admin") @app.route("/upload_presentation/<presentation_key>", methods=["POST"]) def upload_presentation(presentation_key): file = request.files.get("file") if not file or file.filename == "": return "No file selected", 400 filename = save_japan_presentation(presentation_key, file) if not filename: return "Presentation type not found", 404 return redirect(request.referrer or "/admin/japan") @app.route("/upload_usa_presentation/<mode>", methods=["POST"]) def upload_usa_presentation(mode): mode = normalize_usa_mode(mode) if not mode: return "USA presentation target not found", 404 file = request.files.get("file") if not file or file.filename == "": return "No file selected", 400 filename = save_usa_presentation(mode, file) if not filename: return "USA presentation type not found", 404 return redirect(request.referrer or f"/admin/usa/presentation/{mode}") @app.route("/upload_usa_image/<mode>", methods=["POST"]) def upload_usa_image_mode(mode): mode = normalize_usa_mode(mode) if not mode: return "USA image target not found", 404 image_key = request.form.get("image_key", "").strip() file = request.files.get("file") if not file or file.filename == "": return "No file selected", 400 filename = save_usa_presentation_image(mode, image_key, file) if not filename: return "USA image key not found", 404 return redirect(request.referrer or f"/admin/usa/presentation/{mode}") @app.route("/upload_usa_image", methods=["POST"]) def upload_usa_image_legacy(): return upload_usa_image_mode("phone") @app.route("/upload_usa_images_zip/<mode>", methods=["POST"]) def upload_usa_images_zip_mode(mode): mode = normalize_usa_mode(mode) if not mode: return "USA image target not found", 404 file = request.files.get("file") if not file or file.filename == "": return "No file selected", 400 original_name = file.filename or "" if not original_name.lower().endswith(".zip"): return admin_status_page("קובץ לא תקין", "ניתן להעלות כאן רק קובץ ZIP של תמונות מצגת.", ok=False, back=f"/admin/usa/presentation/{mode}"), 400 if request.content_length and request.content_length > USA_IMAGES_ZIP_MAX_BYTES + 1024 * 1024: return admin_status_page("קובץ גדול מדי", f"קובץ התמונות גדול מהמגבלה: {USA_IMAGES_ZIP_MAX_BYTES:,} bytes.", ok=False, back=f"/admin/usa/presentation/{mode}"), 413 try: imported, skipped, missing = save_usa_presentation_images_zip(mode, file) details = "נשמרו בתיקיית התמונות:\n" + ("\n".join(imported) if imported else "לא נשמרו תמונות.") if skipped: details += "\n\nדולגו:\n" + "\n".join(skipped[:120]) if missing: details += "\n\nלא נמצאו ב־ZIP, ולכן לא הוחלפו:\n" + "\n".join(missing[:120]) return admin_status_page("ZIP תמונות ארה״ב נטען", f"נשמרו {len(imported)} תמונות למצגת {USA_TARGETS[mode]['short']}.", ok=True, details=details, back=f"/admin/usa/presentation/{mode}") except zipfile.BadZipFile: return admin_status_page("ZIP לא תקין", "הקובץ שהועלה אינו ZIP תקין או שלא ניתן לפתוח אותו.", ok=False, back=f"/admin/usa/presentation/{mode}"), 400 except ValueError as exc: return admin_status_page("ZIP לא תקין", "הקובץ לא עומד במגבלות העלאה של הפאנל.", ok=False, details=str(exc), back=f"/admin/usa/presentation/{mode}"), 400 except Exception as exc: return admin_status_page("שגיאה בהעלאת ZIP", "לא ניתן היה לעבד את קובץ התמונות.", ok=False, details=repr(exc), back=f"/admin/usa/presentation/{mode}"), 500 @app.route("/upload_usa_images_zip", methods=["POST"]) def upload_usa_images_zip_legacy(): return upload_usa_images_zip_mode("phone") @app.route("/upload_usa_music/<mode>/<music_key>", methods=["POST"]) def upload_usa_music_mode(mode, music_key): mode = normalize_usa_mode(mode) if not mode: return "USA music target not found", 404 file = request.files.get("file") if not file or file.filename == "": return "No file selected", 400 filename = save_usa_music(mode, music_key, file) if not filename: return "USA music type not found", 404 return redirect(request.referrer or f"/admin/usa/presentation/{mode}") @app.route("/upload_usa_music/<music_key>", methods=["POST"]) def upload_usa_music_legacy(music_key): return upload_usa_music_mode("phone", music_key) def safe_extract_zip_to_dir(zip_path, dest_dir, strip_root_folder=None): os.makedirs(dest_dir, exist_ok=True) with zipfile.ZipFile(zip_path, "r") as zf: infos = [info for info in zf.infolist() if not info.is_dir()] if len(infos) > USA_PACKAGE_ZIP_MAX_FILES: raise ValueError(f"too_many_files:{len(infos)}") total = sum(max(0, i.file_size) for i in infos) if total > USA_PACKAGE_ZIP_MAX_UNCOMPRESSED_BYTES: raise ValueError(f"zip_uncompressed_size_exceeded:{total}") for info in infos: raw = info.filename.replace("\\", "/") if raw.startswith("__MACOSX/") or "/.." in raw or raw.startswith("../") or raw.startswith("/"): continue rel = raw if strip_root_folder and rel.startswith(strip_root_folder.rstrip("/") + "/"): rel = rel[len(strip_root_folder.rstrip("/")) + 1:] if not rel or rel.endswith("/"): continue target = os.path.abspath(os.path.join(dest_dir, rel)) dest_abs = os.path.abspath(dest_dir) if not target.startswith(dest_abs + os.sep) and target != dest_abs: continue os.makedirs(os.path.dirname(target), exist_ok=True) with zf.open(info, "r") as source, open(target, "wb") as dest: shutil.copyfileobj(source, dest) def zip_has_file(zip_path, name): name = name.replace("\\", "/").strip("/") with zipfile.ZipFile(zip_path, "r") as zf: names = {n.replace("\\", "/").strip("/") for n in zf.namelist() if not n.endswith("/")} return name in names @app.route("/upload_usa_tv_package_zip", methods=["POST"]) def upload_usa_tv_package_zip(): file = request.files.get("file") if not file or file.filename == "": return "No file selected", 400 original_name = file.filename or "" if not original_name.lower().endswith(".zip"): return admin_status_page("קובץ לא תקין", "ניתן להעלות כאן רק ZIP מלא של USA_TV.", ok=False, back="/admin/usa/presentation/tv"), 400 if request.content_length and request.content_length > USA_PACKAGE_ZIP_MAX_BYTES + 1024 * 1024: return admin_status_page("קובץ גדול מדי", f"קובץ ה־ZIP גדול מהמגבלה: {USA_PACKAGE_ZIP_MAX_BYTES:,} bytes.", ok=False, back="/admin/usa/presentation/tv"), 413 stamp = time.strftime("%Y%m%d_%H%M%S") tmp_zip = os.path.join(ADMIN_UPLOAD_TMP_DIR, f"usa_tv_full_package_{stamp}.zip") tmp_extract = os.path.join(ADMIN_UPLOAD_TMP_DIR, f"usa_tv_extract_{stamp}") os.makedirs(ADMIN_UPLOAD_TMP_DIR, exist_ok=True) try: file.save(tmp_zip) if os.path.getsize(tmp_zip) > USA_PACKAGE_ZIP_MAX_BYTES: raise ValueError("zip_size_exceeded") with zipfile.ZipFile(tmp_zip, "r") as zf: names = {n.replace("\\", "/").strip("/") for n in zf.namelist() if not n.endswith("/")} strip = "USA_TV" if "USA_TV/index.html" in names else None if strip: required = {"USA_TV/index.html", "USA_TV/usa2027-presentation.html"} else: required = {"index.html", "usa2027-presentation.html"} missing = sorted(required - names) if missing: return admin_status_page("ZIP חסר קבצים", "ה־ZIP לא הוחלף כי חסרים קבצי בסיס.", ok=False, details="\n".join(missing), back="/admin/usa/presentation/tv"), 400 safe_extract_zip_to_dir(tmp_zip, tmp_extract, strip_root_folder=strip) target = FOLDERS["USA_TV"] backup_dir = os.path.join(BASE_DIR, "backups", "USA_TV", f"USA_TV_backup_{stamp}") if os.path.exists(target): os.makedirs(os.path.dirname(backup_dir), exist_ok=True) if os.path.exists(backup_dir): shutil.rmtree(backup_dir) shutil.copytree(target, backup_dir) shutil.rmtree(target) shutil.copytree(tmp_extract, target) return admin_status_page("חבילת USA_TV הועלתה", "תיקיית USA_TV הוחלפה אחרי יצירת גיבוי.", ok=True, details=f"יעד:\n{target}\n\nגיבוי:\n{backup_dir}", back="/admin/usa/presentation/tv") except zipfile.BadZipFile: return admin_status_page("ZIP לא תקין", "הקובץ שהועלה אינו ZIP תקין.", ok=False, back="/admin/usa/presentation/tv"), 400 except Exception as exc: return admin_status_page("שגיאה בהעלאת USA_TV", "תיקיית USA_TV לא הוחלפה.", ok=False, details=repr(exc), back="/admin/usa/presentation/tv"), 500 finally: for p in (tmp_zip,): try: if os.path.exists(p): os.remove(p) except Exception: pass try: if os.path.exists(tmp_extract): shutil.rmtree(tmp_extract) except Exception: pass @app.route("/JAPAN") @app.route("/JAPAN/") def japan(): return serve_app("JAPAN") @app.route("/USA") @app.route("/USA/") def usa(): return serve_app("USA") @app.route("/USA_TV") @app.route("/USA_TV/") def usa_tv(): return serve_app("USA_TV") @app.route("/Moneyapp") @app.route("/Moneyapp/") def moneyapp(): return serve_app("Moneyapp") @app.route("/MoneyHome") @app.route("/MoneyHome/") def moneyhome(): return serve_app("MoneyHome") @app.route("/JAPAN/<path:path>") def japan_static(path): return no_cache_response(make_response(send_from_directory(FOLDERS["JAPAN"], path))) @app.route("/USA/<path:path>") def usa_static(path): return no_cache_response(make_response(send_from_directory(FOLDERS["USA"], path))) @app.route("/USA_TV/<path:path>") def usa_tv_static(path): return no_cache_response(make_response(send_from_directory(FOLDERS["USA_TV"], path))) @app.route("/Moneyapp/<path:path>") def money_static(path): return no_cache_response(make_response(send_from_directory(FOLDERS["Moneyapp"], path))) @app.route("/MoneyHome/<path:path>") def moneyhome_static(path): return no_cache_response(make_response(send_from_directory(FOLDERS["MoneyHome"], path))) if __name__ == "__main__": app.run(host="0.0.0.0", port=8000, threaded=True, use_reloader=False)
שמור קובץ