diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,265 +1,47 @@ -import flask -from flask import Flask, render_template_string, request, redirect, url_for, session, flash, send_file, jsonify, Response -from flask_caching import Cache +import asyncio +import hashlib +import hmac import json -import os import logging +import os import threading import time +import uuid from datetime import datetime -from huggingface_hub import HfApi, hf_hub_download, utils as hf_utils -from werkzeug.utils import secure_filename -import requests from io import BytesIO -import uuid -import hmac -import hashlib -from urllib.parse import unquote +from urllib.parse import parse_qsl, unquote -app = Flask(__name__) -app.secret_key = os.getenv("FLASK_SECRET_KEY", "supersecretkey_folders_unique_telegram") - -# --- Telegram Configuration --- -BOT_API_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "6750208873:AAE2hvPlJ99dBdhGa_Brre0IIpUdOvXxHt4") -ADMIN_TELEGRAM_IDS = [int(admin_id) for admin_id in os.getenv("ADMIN_TELEGRAM_IDS", "123456789,987654321").split(',') if admin_id.strip()] # Example: "123456,789012" -# For local testing, you can mock a Telegram user. -# Example: http://localhost:7860/auth/telegram?mock_user_id=YOUR_TELEGRAM_ID (replace YOUR_TELEGRAM_ID) -MOCK_TELEGRAM_MODE = os.getenv("MOCK_TELEGRAM_MODE", "False").lower() == "true" +import requests +from flask import (Flask, Response, flash, jsonify, redirect, + render_template_string, request, send_file, session, + url_for) +from flask_caching import Cache +from huggingface_hub import HfApi, hf_hub_download, utils as hf_utils +from telegram import (InlineKeyboardButton, InlineKeyboardMarkup, Update, + WebAppInfo) +from telegram.ext import (Application, CommandHandler, ContextTypes, + MessageHandler, filters) +from werkzeug.utils import secure_filename +# --- Configuration --- +BOT_TOKEN = "6750208873:AAE2hvPlJ99dBdhGa_Brre0IIpUdOvXxHt4" # Provided Bot API token +ADMIN_TELEGRAM_ID = os.getenv("ADMIN_TELEGRAM_ID", "YOUR_ADMIN_TELEGRAM_ID_REPLACE_ME") # Replace with actual Admin Telegram ID +WEBAPP_URL = os.getenv("WEBAPP_URL", "https://example.com/launch_webapp") # Replace with your deployed WebApp URL (must be HTTPS for production) -DATA_FILE = 'cloudeng_data_tg.json' -REPO_ID = "Eluza133/Z1e1u" # Replace with your actual Repo ID +app = Flask(__name__) +app.secret_key = os.getenv("FLASK_SECRET_KEY", "supersecretkey_folders_unique_telegram_edition") +DATA_FILE = 'cloudeng_data_tg.json' # Using a new data file for TG version +REPO_ID = "Eluza133/Z1e1u" HF_TOKEN_WRITE = os.getenv("HF_TOKEN") HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") or HF_TOKEN_WRITE -UPLOAD_FOLDER = 'uploads' +UPLOAD_FOLDER = 'uploads_tg' os.makedirs(UPLOAD_FOLDER, exist_ok=True) cache = Cache(app, config={'CACHE_TYPE': 'simple'}) -logging.basicConfig(level=logging.INFO) - - -def check_telegram_authorization(init_data_str: str, bot_token: str) -> dict | None: - """ - Validates the initData string from Telegram Web App. - Returns user data dictionary if valid, None otherwise. - """ - try: - # Telegram.WebApp.initData is URL-encoded, ensure it's decoded if necessary. - # Flask's request.get_json() or request.form should handle decoding if initData is part of JSON/form. - # If initData_str is directly from JS, it might already be decoded. - # For safety, let's try to unquote it if it looks like a query string. - if '%' in init_data_str: # Basic check for URL encoding - init_data_str = unquote(init_data_str) - - params = {} - for item in init_data_str.split('&'): - key, value = item.split('=', 1) - params[key] = value - - hash_to_check = params.pop('hash', None) - if not hash_to_check: - logging.warning("No hash found in initData") - return None - - data_check_arr = [f"{k}={v}" for k, v in sorted(params.items())] - data_check_string = "\n".join(data_check_arr) - - secret_key = hmac.new("WebAppData".encode(), bot_token.encode(), hashlib.sha256).digest() - calculated_hash = hmac.new(secret_key, data_check_string.encode(), hashlib.sha256).hexdigest() - - if calculated_hash == hash_to_check: - user_data = params.get("user") - if user_data: - return json.loads(user_data) # user_data is a JSON string - return {} # Should contain user data if auth is valid - else: - logging.warning(f"Telegram auth hash mismatch. Calculated: {calculated_hash}, Received: {hash_to_check}") - return None - except Exception as e: - logging.error(f"Error in check_telegram_authorization: {e}", exc_info=True) - return None - - -def find_node_by_id(filesystem, node_id): - if not filesystem: return None, None - if filesystem.get('id') == node_id: - return filesystem, None - - queue = [(filesystem, None)] - while queue: - current_node, parent = queue.pop(0) - if current_node.get('type') == 'folder' and 'children' in current_node: - for i, child in enumerate(current_node['children']): - if child.get('id') == node_id: - return child, current_node - if child.get('type') == 'folder': - queue.append((child, current_node)) - return None, None - -def add_node(filesystem, parent_id, node_data): - parent_node, _ = find_node_by_id(filesystem, parent_id) - if parent_node and parent_node.get('type') == 'folder': - if 'children' not in parent_node: - parent_node['children'] = [] - parent_node['children'].append(node_data) - return True - return False - -def remove_node(filesystem, node_id): - node_to_remove, parent_node = find_node_by_id(filesystem, node_id) - if node_to_remove and parent_node and 'children' in parent_node: - parent_node['children'] = [child for child in parent_node['children'] if child.get('id') != node_id] - return True - # Special case: removing root's child directly if filesystem is root and parent_node is None - if node_to_remove and filesystem.get('id') == 'root' and parent_node is None: - if 'children' in filesystem: - filesystem['children'] = [child for child in filesystem['children'] if child.get('id') != node_id] - return True - return False - - -def get_node_path_string(filesystem, node_id): - path_list = [] - current_id = node_id - - while current_id: - node, parent = find_node_by_id(filesystem, current_id) - if not node: - break - if node.get('id') != 'root': - path_list.append(node.get('name', node.get('original_filename', ''))) - if not parent: - break - current_id = parent.get('id') if parent else None - return " / ".join(reversed(path_list)) or "Root" - - -def initialize_user_filesystem(user_data_storage_entry): # Expects the value part of data['users'][telegram_id_str] - if 'filesystem' not in user_data_storage_entry: - user_data_storage_entry['filesystem'] = { - "type": "folder", - "id": "root", - "name": "root", # Could be user's TG name, but "root" is fine - "children": [] - } - # Migration logic for old 'files' list, if any, could go here - # For simplicity, new TG users start with an empty root folder. - - -@cache.memoize(timeout=300) -def load_data(): - try: - if os.path.exists(DATA_FILE): # Only download if it exists on HF or local is missing - download_db_from_hf() - - if not os.path.exists(DATA_FILE): # If still not exists, create empty - with open(DATA_FILE, 'w', encoding='utf-8') as f: - json.dump({'users': {}}, f) - logging.info(f"Created empty local database file: {DATA_FILE}") - - with open(DATA_FILE, 'r', encoding='utf-8') as file: - data = json.load(file) - if not isinstance(data, dict): - logging.warning("Data is not in dict format, initializing empty database") - return {'users': {}} - data.setdefault('users', {}) - # User data is now keyed by str(telegram_user_id) - for tg_user_id_str, user_storage_entry in data['users'].items(): - initialize_user_filesystem(user_storage_entry) # Pass the dict value directly - logging.info("Data successfully loaded and initialized for Telegram users") - return data - except Exception as e: - logging.error(f"Error loading data: {e}") - return {'users': {}} - -def save_data(data): - try: - with open(DATA_FILE, 'w', encoding='utf-8') as file: - json.dump(data, file, ensure_ascii=False, indent=4) - upload_db_to_hf() - cache.clear() - logging.info("Data saved and uploaded to HF") - except Exception as e: - logging.error(f"Error saving data: {e}") - raise - -def upload_db_to_hf(): - if not HF_TOKEN_WRITE: - logging.warning("HF_TOKEN_WRITE not set, skipping database upload.") - return - try: - api = HfApi() - api.upload_file( - path_or_fileobj=DATA_FILE, - path_in_repo=DATA_FILE, - repo_id=REPO_ID, - repo_type="dataset", - token=HF_TOKEN_WRITE, - commit_message=f"Backup {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" - ) - logging.info("Database uploaded to Hugging Face") - except Exception as e: - logging.error(f"Error uploading database: {e}") - -def download_db_from_hf(): - if not HF_TOKEN_READ: - logging.warning("HF_TOKEN_READ not set, skipping database download.") - if not os.path.exists(DATA_FILE): - with open(DATA_FILE, 'w', encoding='utf-8') as f: - json.dump({'users': {}}, f) - return - try: - hf_hub_download( - repo_id=REPO_ID, - filename=DATA_FILE, - repo_type="dataset", - token=HF_TOKEN_READ, - local_dir=".", - local_dir_use_symlinks=False, - force_download=True # Ensure we get the latest if available - ) - logging.info("Database downloaded from Hugging Face") - except hf_utils.RepositoryNotFoundError: - logging.warning(f"Repository {REPO_ID} not found.") - if not os.path.exists(DATA_FILE): - with open(DATA_FILE, 'w', encoding='utf-8') as f: - json.dump({'users': {}}, f) - except hf_utils.EntryNotFoundError: - logging.warning(f"{DATA_FILE} not found in repository {REPO_ID}. Initializing empty database if local also missing.") - if not os.path.exists(DATA_FILE): - with open(DATA_FILE, 'w', encoding='utf-8') as f: - json.dump({'users': {}}, f) - except Exception as e: - logging.error(f"Error downloading database: {e}") - if not os.path.exists(DATA_FILE): - with open(DATA_FILE, 'w', encoding='utf-8') as f: - json.dump({'users': {}}, f) - -def periodic_backup(): - while True: - time.sleep(1800) # Backup every 30 minutes - logging.info("Attempting periodic backup...") - data = load_data() # Load current data before saving (or just save current state if save_data handles loading) - # save_data implicitly uploads, so this might be redundant if data is saved frequently - # The main purpose of save_data(data) is to ensure data is written to disk then uploaded. - # A direct call to upload_db_to_hf() might be better if we only want to backup the existing disk file. - # However, frequent load/save operations are handled by endpoints. This is more of a fallback. - upload_db_to_hf() - - -def get_file_type(filename): - filename_lower = filename.lower() - if filename_lower.endswith(('.mp4', '.mov', '.avi', '.webm', '.mkv')): - return 'video' - elif filename_lower.endswith(('.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg')): - return 'image' - elif filename_lower.endswith('.pdf'): - return 'pdf' - elif filename_lower.endswith('.txt'): - return 'text' - return 'other' +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +# --- Styles --- BASE_STYLE = ''' :root { --primary: #ff4d6d; --secondary: #00ddeb; --accent: #8b5cf6; @@ -270,400 +52,188 @@ BASE_STYLE = ''' --folder-color: #ffc107; } * { margin: 0; padding: 0; box-sizing: border-box; } -body { font-family: 'Inter', sans-serif; background: var(--background-light); color: var(--text-light); line-height: 1.6; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } -body.dark { background: var(--background-dark); color: var(--text-dark); } -.container { margin: 10px auto; max-width: 100%; padding: 15px; background: var(--card-bg); border-radius: 20px; box-shadow: var(--shadow); overflow-x: hidden; } +body { font-family: 'Inter', sans-serif; background: var(--background-light); color: var(--text-light); line-height: 1.6; } +body.dark { background: var(--background-dark); color: var(--text-dark); } /* Basic dark mode support */ +.container { margin: 20px auto; max-width: 1200px; padding: 25px; background: var(--card-bg); border-radius: 20px; box-shadow: var(--shadow); overflow-x: hidden; } body.dark .container { background: var(--card-bg-dark); } -h1 { font-size: 1.8em; font-weight: 800; text-align: center; margin-bottom: 20px; background: linear-gradient(135deg, var(--primary), var(--accent)); -webkit-background-clip: text; color: transparent; } -h2 { font-size: 1.3em; margin-top: 25px; color: var(--text-light); } +h1 { font-size: 2em; font-weight: 800; text-align: center; margin-bottom: 25px; background: linear-gradient(135deg, var(--primary), var(--accent)); -webkit-background-clip: text; color: transparent; } +h2 { font-size: 1.5em; margin-top: 30px; color: var(--text-light); } body.dark h2 { color: var(--text-dark); } -h4 { font-size: 1em; margin-top: 12px; margin-bottom: 4px; color: var(--accent); } -ol, ul { margin-left: 18px; margin-bottom: 12px; } -li { margin-bottom: 4px; } -input, textarea { width: 100%; padding: 12px; margin: 10px 0; border: none; border-radius: 12px; background: var(--glass-bg); color: var(--text-light); font-size: 1em; box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.1); } -body.dark input, body.dark textarea { color: var(--text-dark); background: rgba(255,255,255,0.05); } -input:focus, textarea:focus { outline: none; box-shadow: 0 0 0 3px var(--primary); } -.btn { padding: 12px 24px; background: var(--primary); color: white; border: none; border-radius: 12px; cursor: pointer; font-size: 1em; font-weight: 600; transition: var(--transition); box-shadow: var(--shadow); display: inline-block; text-decoration: none; margin-top: 4px; margin-right: 4px; } -.btn:hover { transform: scale(1.03); background: #e6415f; } +h4 { font-size: 1.1em; margin-top: 15px; margin-bottom: 5px; color: var(--accent); } +ol, ul { margin-left: 20px; margin-bottom: 15px; } +li { margin-bottom: 5px; } +input, textarea { width: 100%; padding: 14px; margin: 12px 0; border: none; border-radius: 14px; background: var(--glass-bg); color: var(--text-light); font-size: 1.1em; box-shadow: inset 0 3px 10px rgba(0, 0, 0, 0.1); } +body.dark input, body.dark textarea { color: var(--text-dark); background: rgba(0,0,0,0.2); } +input:focus, textarea:focus { outline: none; box-shadow: 0 0 0 4px var(--primary); } +.btn { padding: 14px 28px; background: var(--primary); color: white; border: none; border-radius: 14px; cursor: pointer; font-size: 1.1em; font-weight: 600; transition: var(--transition); box-shadow: var(--shadow); display: inline-block; text-decoration: none; margin-top: 5px; margin-right: 5px; } +.btn:hover { transform: scale(1.05); background: #e6415f; } .download-btn { background: var(--secondary); } .download-btn:hover { background: #00b8c5; } .delete-btn { background: var(--delete-color); } .delete-btn:hover { background: #cc3333; } .folder-btn { background: var(--folder-color); } .folder-btn:hover { background: #e6a000; } -.flash { color: var(--secondary); text-align: center; margin-bottom: 12px; padding: 8px; background: rgba(0, 221, 235, 0.1); border-radius: 8px; } +.flash { color: var(--secondary); text-align: center; margin-bottom: 15px; padding: 10px; background: rgba(0, 221, 235, 0.1); border-radius: 10px; } .flash.error { color: var(--delete-color); background: rgba(255, 68, 68, 0.1); } -.file-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 15px; margin-top: 15px; } -.user-list { margin-top: 15px; } -.user-item { padding: 12px; background: var(--card-bg); border-radius: 14px; margin-bottom: 8px; box-shadow: var(--shadow); transition: var(--transition); } +.file-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 20px; margin-top: 20px; } +.user-list { margin-top: 20px; } +.user-item { padding: 15px; background: var(--card-bg); border-radius: 16px; margin-bottom: 10px; box-shadow: var(--shadow); transition: var(--transition); } body.dark .user-item { background: var(--card-bg-dark); } -.user-item:hover { transform: translateY(-4px); } +.user-item:hover { transform: translateY(-5px); } .user-item a { color: var(--primary); text-decoration: none; font-weight: 600; } .user-item a:hover { color: var(--accent); } -.item { background: var(--card-bg); padding: 12px; border-radius: 14px; box-shadow: var(--shadow); text-align: center; transition: var(--transition); display: flex; flex-direction: column; justify-content: space-between; } +.item { background: var(--card-bg); padding: 15px; border-radius: 16px; box-shadow: var(--shadow); text-align: center; transition: var(--transition); display: flex; flex-direction: column; justify-content: space-between; } body.dark .item { background: var(--card-bg-dark); } -.item:hover { transform: translateY(-4px); } -.item-preview { max-width: 100%; height: 100px; object-fit: cover; border-radius: 8px; margin-bottom: 8px; cursor: pointer; display: block; margin-left: auto; margin-right: auto;} -.item.folder .item-preview { object-fit: contain; font-size: 50px; color: var(--folder-color); line-height: 100px; } -.item p { font-size: 0.85em; margin: 4px 0; word-break: break-all; } +.item:hover { transform: translateY(-5px); } +.item-preview { max-width: 100%; height: 130px; object-fit: cover; border-radius: 10px; margin-bottom: 10px; cursor: pointer; display: block; margin-left: auto; margin-right: auto;} +.item.folder .item-preview { object-fit: contain; font-size: 60px; color: var(--folder-color); line-height: 130px; } +.item p { font-size: 0.9em; margin: 5px 0; word-break: break-all; } .item a { color: var(--primary); text-decoration: none; } .item a:hover { color: var(--accent); } -.item-actions { margin-top: 8px; display: flex; flex-wrap: wrap; gap: 4px; justify-content: center; } -.item-actions .btn { font-size: 0.8em; padding: 4px 8px; } +.item-actions { margin-top: 10px; display: flex; flex-wrap: wrap; gap: 5px; justify-content: center; } +.item-actions .btn { font-size: 0.9em; padding: 5px 10px; } .modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.85); z-index: 2000; justify-content: center; align-items: center; } -.modal-content { max-width: 95vw; max-height: 95vh; background: #fff; padding: 8px; border-radius: 12px; overflow: auto; position: relative; } +.modal-content { max-width: 95%; max-height: 95%; background: #fff; padding: 10px; border-radius: 15px; overflow: auto; position: relative; } body.dark .modal-content { background: var(--card-bg-dark); } -.modal img, .modal video, .modal iframe, .modal pre { max-width: 100%; max-height: 80vh; display: block; margin: auto; border-radius: 8px; } -.modal iframe { width: 90vw; height: 80vh; border: none; } -.modal pre { background: #eee; color: #333; padding: 12px; border-radius: 6px; white-space: pre-wrap; word-wrap: break-word; text-align: left; max-height: 80vh; overflow-y: auto;} +.modal img, .modal video, .modal iframe, .modal pre { max-width: 100%; max-height: 85vh; display: block; margin: auto; border-radius: 10px; } +.modal iframe { width: 80vw; height: 85vh; border: none; } +.modal pre { background: #eee; color: #333; padding: 15px; border-radius: 8px; white-space: pre-wrap; word-wrap: break-word; text-align: left; max-height: 85vh; overflow-y: auto;} body.dark .modal pre { background: #2b2a33; color: var(--text-dark); } -.modal-close-btn { position: absolute; top: 10px; right: 15px; font-size: 24px; color: #aaa; cursor: pointer; background: rgba(0,0,0,0.5); border-radius: 50%; width: 24px; height: 24px; line-height: 24px; text-align: center; } +.modal-close-btn { position: absolute; top: 15px; right: 25px; font-size: 30px; color: #aaa; cursor: pointer; background: rgba(0,0,0,0.5); border-radius: 50%; width: 30px; height: 30px; line-height: 30px; text-align: center; } body.dark .modal-close-btn { color: #555; background: rgba(255,255,255,0.2); } -#progress-container { width: 100%; background: var(--glass-bg); border-radius: 8px; margin: 12px 0; display: none; position: relative; height: 18px; } -#progress-bar { width: 0%; height: 100%; background: var(--primary); border-radius: 8px; transition: width 0.3s ease; } -#progress-text { position: absolute; width: 100%; text-align: center; line-height: 18px; color: white; font-size: 0.8em; font-weight: bold; text-shadow: 1px 1px 1px rgba(0,0,0,0.5); } -.breadcrumbs { margin-bottom: 15px; font-size: 1em; } +#progress-container { width: 100%; background: var(--glass-bg); border-radius: 10px; margin: 15px 0; display: none; position: relative; height: 20px; } +#progress-bar { width: 0%; height: 100%; background: var(--primary); border-radius: 10px; transition: width 0.3s ease; } +#progress-text { position: absolute; width: 100%; text-align: center; line-height: 20px; color: white; font-size: 0.9em; font-weight: bold; text-shadow: 1px 1px 1px rgba(0,0,0,0.5); } +.breadcrumbs { margin-bottom: 20px; font-size: 1.1em; } .breadcrumbs a { color: var(--accent); text-decoration: none; } .breadcrumbs a:hover { text-decoration: underline; } -.breadcrumbs span { margin: 0 4px; color: #aaa; } -.folder-actions { margin-top: 15px; margin-bottom: 8px; display: flex; gap: 8px; align-items: center; flex-wrap: wrap; } -.folder-actions input[type=text] { width: auto; flex-grow: 1; margin: 0; min-width: 120px; } +.breadcrumbs span { margin: 0 5px; color: #aaa; } +.folder-actions { margin-top: 20px; margin-bottom: 10px; display: flex; gap: 10px; align-items: center; flex-wrap: wrap; } +.folder-actions input[type=text] { width: auto; flex-grow: 1; margin: 0; min-width: 150px; } .folder-actions .btn { margin: 0; flex-shrink: 0;} -#tg-user-info { margin-bottom: 15px; padding: 10px; background: var(--glass-bg); border-radius: 8px; text-align: center; font-size: 0.9em;} -body.dark #tg-user-info { background: rgba(255,255,255,0.05); } +@media (max-width: 768px) { + .file-grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); } + .folder-actions { flex-direction: column; align-items: stretch; } + .folder-actions input[type=text] { width: 100%; } + .item-preview { height: 100px; } + .item.folder .item-preview { font-size: 50px; line-height: 100px; } + h1 { font-size: 1.8em; } + .btn { padding: 12px 24px; font-size: 1em; } + .item-actions .btn { padding: 4px 8px; font-size: 0.8em;} +} @media (max-width: 480px) { - .container { padding: 10px; margin: 5px auto; } - .file-grid { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 10px; } + .container { padding: 15px; } + .file-grid { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 15px; } .item-preview { height: 80px; } .item.folder .item-preview { font-size: 40px; line-height: 80px; } .item p { font-size: 0.8em;} - .breadcrumbs { font-size: 0.9em; } - .btn { padding: 10px 20px; font-size: 0.9em; } - .item-actions .btn { padding: 3px 6px; font-size: 0.7em;} - h1 { font-size: 1.6em; } + .breadcrumbs { font-size: 1em; } + .btn { padding: 10px 20px; } } +/* Telegram WebApp specific styles */ +body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-rendering: optimizeLegibility; } +/* Adjust for Telegram theme */ +body.telegram-dark-theme { --background-light: #1a1625; --text-light: #e8e1ff; --card-bg: rgba(40, 35, 60, 0.95); /* ... and other dark theme vars */ } +body.telegram-light-theme { /* default vars are mostly light */ } ''' -@app.route('/') -def index_page(): - # This page will initiate Telegram authentication - # It will not require prior Flask session auth. - # After TG auth, it redirects to dashboard. - html = ''' - -Zeus Cloud Aut - -

Zeus Cloud

Аутентификация через Telegram...

-''' - return render_template_string(html) - -@app.route('/auth/telegram', methods=['POST']) -def auth_telegram(): - telegram_user = None - - # Mocking for local development - mock_user_id_str = request.args.get('mock_user_id') - if MOCK_TELEGRAM_MODE and mock_user_id_str: - try: - mock_user_id = int(mock_user_id_str) - telegram_user = { - "id": mock_user_id, - "first_name": "MockUser", - "username": f"mockuser{mock_user_id}", - "is_bot": False, - "language_code": "en" - } - logging.info(f"Using MOCK Telegram user: {telegram_user}") - except ValueError: - return jsonify({'status': 'error', 'message': 'Invalid mock_user_id.'}), 400 - else: - # Actual Telegram authentication - try: - payload = request.get_json() - init_data_str = payload.get('initData') - except Exception as e: - logging.error(f"Failed to parse JSON payload: {e}") - return jsonify({'status': 'error', 'message': 'Invalid request format.'}), 400 - - if not init_data_str: - return jsonify({'status': 'error', 'message': 'initData is missing.'}), 400 - - telegram_user = check_telegram_authorization(init_data_str, BOT_API_TOKEN) - - if telegram_user and telegram_user.get("id"): - telegram_user_id = telegram_user["id"] - telegram_user_id_str = str(telegram_user_id) - - session['telegram_user_id'] = telegram_user_id - session['telegram_username'] = telegram_user.get('username', telegram_user.get('first_name', f'User{telegram_user_id}')) - session['telegram_first_name'] = telegram_user.get('first_name', '') - session['telegram_last_name'] = telegram_user.get('last_name', '') - - data = load_data() - if telegram_user_id_str not in data['users']: - data['users'][telegram_user_id_str] = { - 'telegram_info': telegram_user, # Store full TG info - 'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), - 'filesystem': { - "type": "folder", "id": "root", "name": "root", "children": [] +# --- HTML Templates --- +HTML_LAUNCH_PAGE = ''' + + + + Zeus Cloud + + + + + +
+

Загрузка приложения Zeus Cloud...

+
+ + + +''' - html = ''' +HTML_DASHBOARD = ''' Панель управления - Zeus Cloud - -
-

Zeus Cloud

-
Пользователь: {{ telegram_username }} (ID: {{ telegram_user_id }})
- +
+

Zeus Cloud

Пользователь: {{ display_name }}

{% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} - {% for category, message in messages %} -
{{ message }}
- {% endfor %} - {% endif %} + {% if messages %}{% for category, message in messages %}
{{ message }}
{% endfor %}{% endif %} {% endwith %} - -
- +
-
0%
-

Содержимое папки: {{ current_folder.name if current_folder_id != 'root' else 'Главная' }}

{% for item in items %} @@ -673,7 +243,7 @@ def dashboard():

{{ item.name }}

Открыть -
+
@@ -681,18 +251,13 @@ def dashboard(): {% elif item.type == 'file' %} {% set previewable = item.file_type in ['image', 'video', 'pdf', 'text'] %} {% if item.file_type == 'image' %} - {{ item.original_filename }} + {{ item.original_filename }} {% elif item.file_type == 'video' %} - + {% elif item.file_type == 'pdf' %} -
📄
+
📄
{% elif item.file_type == 'text' %} -
📝
+
📝
{% else %}
{% endif %} @@ -700,11 +265,8 @@ def dashboard():

{{ item.upload_date }}

Скачать - {% if previewable %} - - {% endif %} -
+ {% if previewable %}{% endif %} +
@@ -714,930 +276,871 @@ def dashboard(): {% endfor %} {% if not items %}

Эта папка пуста.

{% endif %}
+Выйти (очистить сессию) +{% if is_admin %} Админ-панель {% endif %} +
+ +''' + +HTML_ADMIN_PANEL = ''' + +Админ-панель + +

Админ-панель

+Назад в приложение +{% with messages = get_flashed_messages(with_categories=true) %}{% if messages %}{% for category, message in messages %}
{{ message }}
{% endfor %}{% endif %}{% endwith %} +

Пользователи

+{% for user in user_details %} +
+ {{ user.display_name }} (ID: {{ user.telegram_id_str }}) +

Зарегистрирован: {{ user.created_at }}

+

Файлов: {{ user.file_count }}

+
+ +
+
+{% else %}

Пользователей нет.

{% endfor %}
+ +''' + +HTML_ADMIN_USER_FILES = ''' +Файлы {{ display_name }} + + +

Файлы пользователя: {{ display_name }}

+Назад к пользователям +{% with messages = get_flashed_messages(with_categories=true) %}{% if messages %}{% for category, message in messages %}
{{ message }}
{% endfor %}{% endif %}{% endwith %} +
+{% for file in files %} +
+
+ {% if file.file_type == 'image' %} + {% elif file.file_type == 'video' %} + {% elif file.file_type == 'pdf' %}
📄
+ {% elif file.file_type == 'text' %}
📝
+ {% else %}
{% endif %} +

{{ file.original_filename | truncate(30) }}

+

В папке: {{ file.parent_path_str }}

+

Загружен: {{ file.upload_date }}

+

ID: {{ file.id }}

+

Path: {{ file.path }}

+
+
+ Скачать + {% set previewable = file.file_type in ['image', 'video', 'pdf', 'text'] %} + {% if previewable %}{% endif %} +
+ +
+
+
+{% else %}

У пользователя нет файлов.

{% endfor %} +
+ +''' + + +# --- Core Logic Helper Functions (Filesystem) --- +def find_node_by_id(filesystem, node_id): + if not filesystem: return None, None + if filesystem.get('id') == node_id: + return filesystem, None + queue = [(filesystem, None)] + while queue: + current_node, parent = queue.pop(0) + if current_node.get('type') == 'folder' and 'children' in current_node: + for child in current_node['children']: + if child.get('id') == node_id: + return child, current_node + if child.get('type') == 'folder': + queue.append((child, current_node)) # current_node is parent here + return None, None + +def add_node(filesystem, parent_id, node_data): + parent_node, _ = find_node_by_id(filesystem, parent_id) + if parent_node and parent_node.get('type') == 'folder': + if 'children' not in parent_node: + parent_node['children'] = [] + parent_node['children'].append(node_data) + return True + return False + +def remove_node(filesystem, node_id): + node_to_remove, parent_node = find_node_by_id(filesystem, node_id) + if node_to_remove and parent_node and 'children' in parent_node: + parent_node['children'] = [child for child in parent_node['children'] if child.get('id') != node_id] + return True + # Special case: removing root's child if filesystem itself is parent (parent_node is None) + if node_to_remove and not parent_node and filesystem.get('id') == 'root' and 'children' in filesystem: + if node_to_remove in filesystem['children']: # Check if node_to_remove is a direct child + filesystem['children'] = [child for child in filesystem['children'] if child.get('id') != node_id] + return True + return False + +def get_node_path_string(filesystem, node_id): + path_list = [] + current_id = node_id + while current_id: + node, parent = find_node_by_id(filesystem, current_id) + if not node: break + if node.get('id') != 'root': + path_list.append(node.get('name', node.get('original_filename', ''))) + if not parent: break + current_id = parent.get('id') if parent else None + return " / ".join(reversed(path_list)) or "Root" + +def get_file_type(filename): + filename_lower = filename.lower() + if filename_lower.endswith(('.mp4', '.mov', '.avi', '.webm', '.mkv')): return 'video' + elif filename_lower.endswith(('.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg')): return 'image' + elif filename_lower.endswith('.pdf'): return 'pdf' + elif filename_lower.endswith('.txt'): return 'text' + return 'other' + +# --- Data Management (TG Adapted) --- +def initialize_user_filesystem_tg(user_data, user_identifier_for_path): + if 'filesystem' not in user_data: + user_data['filesystem'] = {"type": "folder", "id": "root", "name": "root", "children": []} + if 'files' in user_data and isinstance(user_data['files'], list): # Legacy migration + for old_file in user_data['files']: + file_id = old_file.get('id', uuid.uuid4().hex) + original_filename = old_file.get('filename', 'unknown_file') + name_part, ext_part = os.path.splitext(original_filename) + unique_suffix = uuid.uuid4().hex[:8] + unique_filename = f"{name_part}_{unique_suffix}{ext_part}" + # Use user_identifier_for_path for legacy data, could be old username + hf_path = f"cloud_files/{user_identifier_for_path}/root/{unique_filename}" + file_node = { + 'type': 'file', 'id': file_id, 'original_filename': original_filename, + 'unique_filename': unique_filename, 'path': hf_path, + 'file_type': get_file_type(original_filename), + 'upload_date': old_file.get('upload_date', datetime.now().strftime('%Y-%m-%d %H:%M:%S')) + } + add_node(user_data['filesystem'], 'root', file_node) + del user_data['files'] + +@cache.memoize(timeout=300) +def load_data_tg(): + try: + download_db_from_hf() + with open(DATA_FILE, 'r', encoding='utf-8') as file: + data = json.load(file) + if not isinstance(data, dict): data = {'users': {}} + data.setdefault('users', {}) + for user_key, user_data_entry in data['users'].items(): # user_key is str(telegram_id) or old username + initialize_user_filesystem_tg(user_data_entry, user_key) + logging.info("Data successfully loaded and initialized (TG version)") + return data + except Exception as e: + logging.error(f"Error loading data (TG version): {e}") + return {'users': {}} + +def save_data_tg(data): + try: + with open(DATA_FILE, 'w', encoding='utf-8') as file: + json.dump(data, file, ensure_ascii=False, indent=4) + upload_db_to_hf() + cache.clear() + logging.info("Data saved and uploaded to HF (TG version)") + except Exception as e: + logging.error(f"Error saving data (TG version): {e}") + raise + +def upload_db_to_hf(): + if not HF_TOKEN_WRITE: logging.warning("HF_TOKEN_WRITE not set, skipping database upload."); return + try: + api = HfApi() + api.upload_file(path_or_fileobj=DATA_FILE, path_in_repo=DATA_FILE, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, commit_message=f"TG App Backup {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + logging.info("Database uploaded to Hugging Face (TG version)") + except Exception as e: logging.error(f"Error uploading database (TG version): {e}") + +def download_db_from_hf(): + if not HF_TOKEN_READ: + logging.warning("HF_TOKEN_READ not set, skipping database download.") + if not os.path.exists(DATA_FILE): + with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}}, f) + return + try: + hf_hub_download(repo_id=REPO_ID, filename=DATA_FILE, repo_type="dataset", token=HF_TOKEN_READ, local_dir=".", local_dir_use_symlinks=False) + logging.info("Database downloaded from Hugging Face (TG version)") + except (hf_utils.RepositoryNotFoundError, hf_utils.EntryNotFoundError): + logging.warning(f"{DATA_FILE} or repo not found. Initializing empty database.") + if not os.path.exists(DATA_FILE): + with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}}, f) + except Exception as e: + logging.error(f"Error downloading database (TG version): {e}") + if not os.path.exists(DATA_FILE): + with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}}, f) + +def periodic_backup(): + while True: + upload_db_to_hf() + time.sleep(1800) # 30 minutes -Выйти (очистить сессию) -
+# --- Auth Helpers --- +def validate_telegram_data(init_data_str, bot_token_val): + try: + # Telegram sends initData as a query string + # parse_qsl correctly handles URL-encoded values + parsed_data = dict(parse_qsl(unquote(init_data_str))) + except Exception as e: + logging.error(f"Failed to parse init_data_str: {e}") + return None - + if 'hash' not in parsed_data: + logging.error("Hash not found in parsed_data") + return None + + received_hash = parsed_data.pop('hash') + + data_check_string_parts = [] + for key, value in sorted(parsed_data.items()): + data_check_string_parts.append(f"{key}={value}") + + data_check_string = "\n".join(data_check_string_parts) - -''' - template_context = { - 'telegram_user_id': telegram_user_id, - 'telegram_username': telegram_username, - 'items': items_in_folder, - 'current_folder_id': current_folder_id, - 'current_folder': current_folder, - 'breadcrumbs': breadcrumbs, - 'repo_id': REPO_ID, - 'HF_TOKEN_READ': HF_TOKEN_READ, - 'hf_file_url': lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{path}{'?download=true' if download else ''}", - 'os': os # os is not used in template, can remove - } - return render_template_string(html, **template_context) + if request.method == 'POST': + if not HF_TOKEN_WRITE: + flash('Загрузка невозможна: токен для записи не настроен.', 'error') + return redirect(url_for('dashboard', folder_id=current_folder_id)) + + files = request.files.getlist('files') + if not files or all(not f.filename for f in files): + flash('Файлы для загрузки не выбраны.', 'error') + return redirect(url_for('dashboard', folder_id=current_folder_id)) + if len(files) > 20: + flash('Максимум 20 файлов за раз!', 'error') + return redirect(url_for('dashboard', folder_id=current_folder_id)) + target_folder_id = request.form.get('current_folder_id', 'root') + target_folder_node, _ = find_node_by_id(user_data['filesystem'], target_folder_id) + if not target_folder_node or target_folder_node.get('type') != 'folder': + flash('Целевая папка для загрузки не найдена!', 'error') + return redirect(url_for('dashboard')) -@app.route('/create_folder', methods=['POST']) -def create_folder(): - if 'telegram_user_id' not in session: - return jsonify({'status': 'error', 'message': 'Не авторизован'}), 401 + api = HfApi(); uploaded_count = 0; errors = [] + for file_obj in files: + if file_obj and file_obj.filename: + original_filename = secure_filename(file_obj.filename) + name_part, ext_part = os.path.splitext(original_filename) + unique_suffix = uuid.uuid4().hex[:8] + unique_filename = f"{name_part}_{unique_suffix}{ext_part}" + file_id = uuid.uuid4().hex + # Path on HF includes telegram_user_id and target_folder_id for organization + hf_path = f"cloud_files/{tg_user_id}/{target_folder_id}/{unique_filename}" + temp_path = os.path.join(UPLOAD_FOLDER, f"{file_id}_{unique_filename}") + try: + file_obj.save(temp_path) + api.upload_file(path_or_fileobj=temp_path, path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, commit_message=f"User {tg_user_id} uploaded {original_filename} to folder {target_folder_id}") + file_info = {'type': 'file', 'id': file_id, 'original_filename': original_filename, 'unique_filename': unique_filename, 'path': hf_path, 'file_type': get_file_type(original_filename), 'upload_date': datetime.now().strftime('%Y-%m-%d %H:%M:%S')} + if add_node(user_data['filesystem'], target_folder_id, file_info): + uploaded_count += 1 + else: # Should not happen if target_folder_node check passed + errors.append(f"Ошибка добавления метаданных для {original_filename}.") + logging.error(f"Failed to add node metadata for file {file_id} to folder {target_folder_id} for user {tg_user_id}") + try: api.delete_file(path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE) + except Exception as del_err: logging.error(f"Failed to delete orphaned file {hf_path} from HF Hub: {del_err}") + except Exception as e: + logging.error(f"Error uploading file {original_filename} for {tg_user_id}: {e}") + errors.append(f"Ошибка загрузки файла {original_filename}: {e}") + finally: + if os.path.exists(temp_path): os.remove(temp_path) + + if uploaded_count > 0: + try: save_data_tg(data); flash(f'{uploaded_count} файл(ов) успешно загружено!') + except Exception as e: flash('Файлы загружены на сервер, но произошла ошибка сохранения метаданных.', 'error'); logging.error(f"Error saving data after upload for {tg_user_id}: {e}") + if errors: + for error_msg in errors: flash(error_msg, 'error') + return redirect(url_for('dashboard', folder_id=target_folder_id)) + + breadcrumbs = [] + temp_id = current_folder_id + while temp_id: + node, parent = find_node_by_id(user_data['filesystem'], temp_id) + if not node: break + breadcrumbs.append({'id': node['id'], 'name': node.get('name', 'Root'), 'is_link': (node['id'] != current_folder_id)}) + if not parent: break + temp_id = parent.get('id') + breadcrumbs.reverse() - telegram_user_id_str = str(session['telegram_user_id']) - data = load_data() - user_storage_entry = data['users'].get(telegram_user_id_str) - if not user_storage_entry: - # This case should ideally not happen if session is valid - session.clear() - flash('Пользователь не найден, пожалуйста, авторизуйтесь снова.', 'error') - return redirect(url_for('index_page')) + return render_template_string(HTML_DASHBOARD, display_name=session['display_name'], items=items_in_folder, current_folder_id=current_folder_id, current_folder=current_folder, breadcrumbs=breadcrumbs, repo_id=REPO_ID, HF_TOKEN_READ=HF_TOKEN_READ, hf_file_url=lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{path}{'?download=true' if download else ''}", is_admin=is_current_user_admin()) +@app.route('/create_folder', methods=['POST']) +def create_folder(): + if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Не авторизован'}), 401 + tg_user_id = session['telegram_user_id'] + data = load_data_tg(); user_data = data['users'].get(tg_user_id) + if not user_data: return jsonify({'status': 'error', 'message': 'Пользователь не найден'}), 404 parent_folder_id = request.form.get('parent_folder_id', 'root') folder_name = request.form.get('folder_name', '').strip() - - if not folder_name: - flash('Имя папки не может быть пусты��!', 'error') - return redirect(url_for('dashboard', folder_id=parent_folder_id)) - - # Basic validation for folder name (alphanumeric, spaces, underscores) - if not all(c.isalnum() or c.isspace() or c == '_' for c in folder_name) or len(folder_name) > 50: - flash('Имя папки может содержать буквы, цифры, пробелы, подчеркивания (макс 50 симв).', 'error') - return redirect(url_for('dashboard', folder_id=parent_folder_id)) - + if not folder_name: flash('Имя папки не может быть пустым!', 'error'); return redirect(url_for('dashboard', folder_id=parent_folder_id)) + # Allow more characters in folder names + # if not all(c.isalnum() or c in [' ', '_', '-'] for c in folder_name): + # flash('Имя папки может содержать буквы, цифры, пробелы, дефисы и подчеркивания.', 'error') + # return redirect(url_for('dashboard', folder_id=parent_folder_id)) folder_id = uuid.uuid4().hex - folder_data = { - 'type': 'folder', - 'id': folder_id, - 'name': folder_name, - 'children': [] - } - - if add_node(user_storage_entry['filesystem'], parent_folder_id, folder_data): - try: - save_data(data) - flash(f'Папка "{folder_name}" успешно создана.') - except Exception as e: - flash('Ошибка сохранения данных при создании папки.', 'error') - logging.error(f"Create folder save error for {telegram_user_id_str}: {e}") - else: - flash('Не удалось найти родительскую папку.', 'error') - + folder_data = {'type': 'folder', 'id': folder_id, 'name': folder_name, 'children': []} + if add_node(user_data['filesystem'], parent_folder_id, folder_data): + try: save_data_tg(data); flash(f'Папка "{folder_name}" успешно создана.') + except Exception as e: flash('Ошибка сохранения данных при создании папки.', 'error'); logging.error(f"Create folder save error: {e}") + else: flash('Не удалось найти родительскую папку.', 'error') return redirect(url_for('dashboard', folder_id=parent_folder_id)) -def is_admin_session(): - return 'telegram_user_id' in session and session['telegram_user_id'] in ADMIN_TELEGRAM_IDS - @app.route('/download/') def download_file(file_id): - is_admin_req = is_admin_session() # Check if current session holder is admin - - # User must be logged in (TG auth) OR be an admin accessing via admhosto (checked by is_admin_req) - if 'telegram_user_id' not in session and not is_admin_req: - # For TG Mini App context, usually means direct access without session. - # If admin panel is the referrer, is_admin_req would be true. - # If not, it's an unauthorized direct access. + is_admin_access = is_current_user_admin() + if 'telegram_user_id' not in session and not is_admin_access: flash('Пожалуйста, авторизуйтесь.') - return redirect(url_for('index_page')) + return redirect(url_for('launch_webapp')) - data = load_data() - file_node = None - # file_owner_telegram_id_str = None # Not strictly needed for download if path is known + data = load_data_tg() + file_node = None; owner_tg_user_id = None - # If accessed by a regular user (not necessarily admin) if 'telegram_user_id' in session: - current_telegram_user_id_str = str(session['telegram_user_id']) - user_storage_entry = data['users'].get(current_telegram_user_id_str) - if user_storage_entry: - file_node, _ = find_node_by_id(user_storage_entry.get('filesystem', {}), file_id) - - # If admin is making the request, and file not found for current user (if admin is also a user) - # or if admin is accessing directly (e.g. from admin panel link) - if not file_node and is_admin_req: - logging.info(f"Admin (ID: {session.get('telegram_user_id')}) searching for file ID {file_id} across all users.") - for tg_id_str, u_storage_entry in data.get('users', {}).items(): - node, _ = find_node_by_id(u_storage_entry.get('filesystem', {}), file_id) + current_tg_user_id = session['telegram_user_id'] + user_data = data['users'].get(current_tg_user_id) + if user_data: + file_node, _ = find_node_by_id(user_data['filesystem'], file_id) + if file_node: owner_tg_user_id = current_tg_user_id + + if not file_node and is_admin_access: # Admin search across users + logging.info(f"Admin download: searching for file ID {file_id}") + for tg_id, u_data in data.get('users', {}).items(): + node, _ = find_node_by_id(u_data.get('filesystem', {}), file_id) if node and node.get('type') == 'file': - file_node = node - # file_owner_telegram_id_str = tg_id_str - logging.info(f"Admin found file ID {file_id} belonging to user ID {tg_id_str}") + file_node = node; owner_tg_user_id = tg_id + logging.info(f"Admin found file ID {file_id} belonging to user {owner_tg_user_id}") break if not file_node or file_node.get('type') != 'file': flash('Файл не найден!', 'error') - # Redirect to dashboard if in session, else to index (auth) page - # Admin panel might have its own referrer logic - # For simplicity, always redirect to dashboard if session, else to index. Admin can navigate back. - return redirect(request.referrer or (url_for('dashboard') if 'telegram_user_id' in session else url_for('index_page'))) - + return redirect(request.referrer or url_for('dashboard')) hf_path = file_node.get('path') original_filename = file_node.get('original_filename', 'downloaded_file') - if not hf_path: flash('Ошибка: Путь к файлу не найден в метаданных.', 'error') - return redirect(request.referrer or (url_for('dashboard') if 'telegram_user_id' in session else url_for('index_page'))) + return redirect(request.referrer or url_for('dashboard')) file_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{hf_path}?download=true" - try: - headers = {} - if HF_TOKEN_READ: - headers["authorization"] = f"Bearer {HF_TOKEN_READ}" - + headers = {}; + if HF_TOKEN_READ: headers["authorization"] = f"Bearer {HF_TOKEN_READ}" response = requests.get(file_url, headers=headers, stream=True) response.raise_for_status() - file_content = BytesIO(response.content) - return send_file( - file_content, - as_attachment=True, - download_name=original_filename, - mimetype='application/octet-stream' # Generic mimetype - ) + return send_file(file_content, as_attachment=True, download_name=original_filename, mimetype='application/octet-stream') except requests.exceptions.RequestException as e: logging.error(f"Error downloading file from HF ({hf_path}): {e}") flash(f'Ошибка скачивания файла {original_filename}! ({e})', 'error') except Exception as e: logging.error(f"Unexpected error during download ({hf_path}): {e}") flash('Произошла непредвиденная ошибка при скачивании файла.', 'error') - - return redirect(request.referrer or (url_for('dashboard') if 'telegram_user_id' in session else url_for('index_page'))) + return redirect(request.referrer or url_for('dashboard')) @app.route('/delete_file/', methods=['POST']) def delete_file(file_id): - if 'telegram_user_id' not in session: - flash('Пожалуйста, авторизуйтесь.') - return redirect(url_for('index_page')) - - telegram_user_id_str = str(session['telegram_user_id']) - telegram_username = session.get('telegram_username', f'User {telegram_user_id_str}') - data = load_data() - user_storage_entry = data['users'].get(telegram_user_id_str) - - if not user_storage_entry: - session.clear() - flash('Пользователь не найден!', 'error') - return redirect(url_for('index_page')) + if 'telegram_user_id' not in session: flash('Пожалуйста, авторизуйтесь.'); return redirect(url_for('launch_webapp')) + tg_user_id = session['telegram_user_id'] + data = load_data_tg(); user_data = data['users'].get(tg_user_id) + if not user_data: flash('Пользователь не найден!', 'error'); session.clear(); return redirect(url_for('launch_webapp')) - file_node, parent_node = find_node_by_id(user_storage_entry['filesystem'], file_id) - current_view_folder_id = request.form.get('current_view_folder_id', 'root') + file_node, parent_node = find_node_by_id(user_data['filesystem'], file_id) + current_view_folder_id = request.form.get('current_view_folder_id', parent_node.get('id', 'root') if parent_node else 'root') - if not file_node or file_node.get('type') != 'file' or not parent_node: + if not file_node or file_node.get('type') != 'file' or not parent_node: # parent_node must exist for a non-root file flash('Файл не найден или не может быть удален.', 'error') return redirect(url_for('dashboard', folder_id=current_view_folder_id)) hf_path = file_node.get('path') original_filename = file_node.get('original_filename', 'файл') - if not hf_path: # Should not happen if file was uploaded correctly + if not hf_path: flash(f'Ошибка: Путь к файлу {original_filename} не найден. Удаление только из базы.', 'error') - if remove_node(user_storage_entry['filesystem'], file_id): - try: - save_data(data) - flash(f'Метаданные файла {original_filename} удалены.') - except Exception as e: - flash('Ошибка сохранения данных после удаления метаданных.', 'error') - logging.error(f"Delete file metadata save error (no hf_path): {e}") + if remove_node(user_data['filesystem'], file_id): + try: save_data_tg(data); flash(f'Метаданные файла {original_filename} удалены.') + except Exception as e: flash('Ошибка сохранения данных.', 'error'); logging.error(f"Delete file metadata save error: {e}") return redirect(url_for('dashboard', folder_id=current_view_folder_id)) - - if not HF_TOKEN_WRITE: - flash('Удаление невозможно: токен для записи не настроен.', 'error') - return redirect(url_for('dashboard', folder_id=current_view_folder_id)) - + if not HF_TOKEN_WRITE: flash('Удаление невозможно: токен для записи не настроен.', 'error'); return redirect(url_for('dashboard', folder_id=current_view_folder_id)) + try: api = HfApi() - commit_msg = f"User {telegram_username} (ID: {telegram_user_id_str}) deleted file {original_filename} (NodeID: {file_id})" - api.delete_file( - path_in_repo=hf_path, - repo_id=REPO_ID, - repo_type="dataset", - token=HF_TOKEN_WRITE, - commit_message=commit_msg - ) - logging.info(f"Deleted file {hf_path} from HF Hub for user {telegram_user_id_str}") - - if remove_node(user_storage_entry['filesystem'], file_id): - try: - save_data(data) - flash(f'Файл {original_filename} успешно удален!') - except Exception as e: - flash('Файл удален с сервера, но произошла ошибка обновления базы данных.', 'error') - logging.error(f"Delete file DB update error: {e}") - else: - flash('Файл удален с сервера, но не найден в локальной базе данных для удаления.', 'error') # Should not happen - + api.delete_file(path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, commit_message=f"User {tg_user_id} deleted file {original_filename}") + logging.info(f"Deleted file {hf_path} from HF Hub for user {tg_user_id}") + if remove_node(user_data['filesystem'], file_id): + try: save_data_tg(data); flash(f'Файл {original_filename} успешно удален!') + except Exception as e: flash('Файл удален с сервера, но ошибка обновления БД.', 'error'); logging.error(f"Delete file DB update error: {e}") + else: flash('Файл удален с сервера, но не найден в БД.', 'error') except hf_utils.EntryNotFoundError: - logging.warning(f"File {hf_path} not found on HF Hub during delete attempt for user {telegram_user_id_str}. Removing from DB.") - if remove_node(user_storage_entry['filesystem'], file_id): - try: - save_data(data) - flash(f'Файл {original_filename} не найден на сервере, удален из базы.') - except Exception as e: - flash('Ошибка сохранения данных после удаления метаданных (файл не найден на сервере).', 'error') - logging.error(f"Delete file metadata save error (HF not found): {e}") - else: - flash('Файл не найден ни на сервере, ни в базе данных.', 'error') + logging.warning(f"File {hf_path} not found on HF Hub for user {tg_user_id}. Removing from DB.") + if remove_node(user_data['filesystem'], file_id): + try: save_data_tg(data); flash(f'Файл {original_filename} не найден на сервере, удален из базы.') + except Exception as e: flash('Ошибка сохранения данных.', 'error'); logging.error(f"Delete file metadata save error (HF not found): {e}") + else: flash('Файл не найден ни на сервере, ни в базе данных.', 'error') except Exception as e: - logging.error(f"Error deleting file {hf_path} for {telegram_user_id_str}: {e}") + logging.error(f"Error deleting file {hf_path} for {tg_user_id}: {e}") flash(f'Ошибка удаления файла {original_filename}: {e}', 'error') - return redirect(url_for('dashboard', folder_id=current_view_folder_id)) @app.route('/delete_folder/', methods=['POST']) def delete_folder(folder_id): - if 'telegram_user_id' not in session: - flash('Пожалуйста, авторизуйтесь.') - return redirect(url_for('index_page')) - - if folder_id == 'root': - flash('Нельзя удалить корневую папку!', 'error') - return redirect(url_for('dashboard')) - - telegram_user_id_str = str(session['telegram_user_id']) - data = load_data() - user_storage_entry = data['users'].get(telegram_user_id_str) + if 'telegram_user_id' not in session: flash('Пожалуйста, авторизуйтесь.'); return redirect(url_for('launch_webapp')) + if folder_id == 'root': flash('Нельзя удалить корневую папку!', 'error'); return redirect(url_for('dashboard')) + + tg_user_id = session['telegram_user_id'] + data = load_data_tg(); user_data = data['users'].get(tg_user_id) + if not user_data: flash('Пользователь не найден!', 'error'); session.clear(); return redirect(url_for('launch_webapp')) - if not user_storage_entry: - session.clear() - flash('Пользователь не найден!', 'error') - return redirect(url_for('index_page')) + folder_node, parent_node = find_node_by_id(user_data['filesystem'], folder_id) + current_view_folder_id = request.form.get('current_view_folder_id', parent_node.get('id', 'root') if parent_node else 'root') - folder_node, parent_node = find_node_by_id(user_storage_entry['filesystem'], folder_id) - current_view_folder_id = request.form.get('current_view_folder_id', 'root') # Where to redirect back if not folder_node or folder_node.get('type') != 'folder' or not parent_node: - # If parent_node is None, it means we are trying to delete a direct child of root. - # This is valid if folder_node exists. - if not (folder_node and folder_node.get('type') == 'folder' and parent_node is None and user_storage_entry['filesystem']['id'] == 'root'): - flash('Папка не найдена или не может быть удалена.', 'error') - return redirect(url_for('dashboard', folder_id=current_view_folder_id)) - - + flash('Папка не найдена или не может быть удалена.', 'error') + return redirect(url_for('dashboard', folder_id=current_view_folder_id)) + folder_name = folder_node.get('name', 'папка') - - if folder_node.get('children'): # Check if folder has children + if folder_node.get('children'): flash(f'Папку "{folder_name}" можно удалить только если она пуста.', 'error') - return redirect(url_for('dashboard', folder_id=current_view_folder_id)) - - # Attempt to remove the node from the filesystem structure - # For children of root, parent_node from find_node_by_id might be None, - # so we pass user_storage_entry['filesystem'] to remove_node. - removed_from_db = False - if parent_node: # If parent_node is not None, it's a regular nested folder - removed_from_db = remove_node(parent_node, folder_id) # This is incorrect logic - # remove_node expects the parent's children list effectively - # Correct way: remove_node(user_storage_entry['filesystem'], folder_id) + return redirect(url_for('dashboard', folder_id=current_view_folder_id)) # Redirect back to the folder containing the one we tried to delete - # Corrected remove_node call - if remove_node(user_storage_entry['filesystem'], folder_id): - try: - save_data(data) - flash(f'Пустая папка "{folder_name}" успешно удалена.') - except Exception as e: - flash('Ошибка сохранения данных после удаления папки.', 'error') - logging.error(f"Delete empty folder save error: {e}") - else: - flash('Не удалось удалить папку из базы данных (возможно, она уже удалена или не существовала).', 'error') - logging.warning(f"Failed to remove folder {folder_id} for user {telegram_user_id_str} from DB structure.") - - # Redirect to the parent of the deleted folder, or root if parent was root - redirect_to_folder_id = parent_node.get('id', 'root') if parent_node else 'root' + if remove_node(user_data['filesystem'], folder_id): + try: save_data_tg(data); flash(f'Пустая папка "{folder_name}" успешно удалена.') + except Exception as e: flash('Ошибка сохранения данных после удаления папки.', 'error'); logging.error(f"Delete empty folder save error: {e}") + else: flash('Не удалось удалить папку из базы данных.', 'error') + + # Redirect to the parent of the deleted folder + redirect_to_folder_id = parent_node.get('id', 'root') return redirect(url_for('dashboard', folder_id=redirect_to_folder_id)) @app.route('/get_text_content/') def get_text_content(file_id): - is_admin_req = is_admin_session() + is_admin_access = is_current_user_admin() + if 'telegram_user_id' not in session and not is_admin_access: + return Response("Не авторизован", status=401) - if 'telegram_user_id' not in session and not is_admin_req: - return Response("Не авторизован", status=401) - - data = load_data() - file_node = None - # file_owner_telegram_id_str = None + data = load_data_tg() + file_node = None; owner_tg_user_id = None if 'telegram_user_id' in session: - current_telegram_user_id_str = str(session['telegram_user_id']) - user_storage_entry = data['users'].get(current_telegram_user_id_str) - if user_storage_entry: - file_node, _ = find_node_by_id(user_storage_entry.get('filesystem', {}), file_id) - - if not file_node and is_admin_req: - logging.info(f"Admin (ID: {session.get('telegram_user_id')}) searching for text file ID {file_id} across users.") - for tg_id_str, u_storage_entry in data.get('users', {}).items(): - node, _ = find_node_by_id(u_storage_entry.get('filesystem', {}), file_id) + current_tg_user_id = session['telegram_user_id'] + user_data = data['users'].get(current_tg_user_id) + if user_data: + file_node, _ = find_node_by_id(user_data['filesystem'], file_id) + if file_node and file_node.get('file_type') == 'text': owner_tg_user_id = current_tg_user_id + + if not file_node and is_admin_access: + logging.info(f"Admin text content: searching for file ID {file_id}") + for tg_id, u_data in data.get('users', {}).items(): + node, _ = find_node_by_id(u_data.get('filesystem', {}), file_id) if node and node.get('type') == 'file' and node.get('file_type') == 'text': - file_node = node - # file_owner_telegram_id_str = tg_id_str - logging.info(f"Admin found text file ID {file_id} belonging to user ID {tg_id_str}") + file_node = node; owner_tg_user_id = tg_id + logging.info(f"Admin found text file ID {file_id} belonging to user {owner_tg_user_id}") break if not file_node or file_node.get('type') != 'file' or file_node.get('file_type') != 'text': return Response("Текстовый файл не найден", status=404) - + hf_path = file_node.get('path') - if not hf_path: - return Response("Ошибка: путь к файлу отсутствует", status=500) - + if not hf_path: return Response("Ошибка: путь к файлу отсутствует", status=500) file_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{hf_path}?download=true" - try: - headers = {} - if HF_TOKEN_READ: - headers["authorization"] = f"Bearer {HF_TOKEN_READ}" - + headers = {}; + if HF_TOKEN_READ: headers["authorization"] = f"Bearer {HF_TOKEN_READ}" response = requests.get(file_url, headers=headers) response.raise_for_status() - - # Limit text preview size - MAX_TEXT_PREVIEW_SIZE = 1 * 1024 * 1024 # 1 MB - if len(response.content) > MAX_TEXT_PREVIEW_SIZE: - return Response(f"Файл слишком большой для предпросмотра (>{MAX_TEXT_PREVIEW_SIZE // 1024 // 1024}MB).", status=413) - - try: - text_content = response.content.decode('utf-8') - except UnicodeDecodeError: - try: - text_content = response.content.decode('latin-1') # Try common fallback - except Exception: - return Response("Не удалось определить кодировку файла.", status=500) - - return Response(text_content, mimetype='text/plain; charset=utf-8') - - except requests.exceptions.RequestException as e: - logging.error(f"Error fetching text content from HF ({hf_path}): {e}") - return Response(f"Ошибка загрузки содержимого: {e}", status=502) - except Exception as e: - logging.error(f"Unexpected error fetching text content ({hf_path}): {e}") - return Response("Внутренняя ошибка сервера", status=500) - + if len(response.content) > 1 * 1024 * 1024: return Response("Файл слишком большой для предпросмотра.", status=413) + try: text_content = response.content.decode('utf-8') + except UnicodeDecodeError: text_content = response.content.decode('latin-1', errors='replace') + return Response(text_content, mimetype='text/plain') + except requests.exceptions.RequestException as e: logging.error(f"Error fetching text content from HF ({hf_path}): {e}"); return Response(f"Ошибка загрузки содержимого: {e}", status=502) + except Exception as e: logging.error(f"Unexpected error fetching text content ({hf_path}): {e}"); return Response("Внутренняя ошибка сервера", status=500) @app.route('/logout') def logout(): session.clear() - flash('Вы успешно вышли из сессии Telegram Mini App.') - # In Mini Apps, redirecting to auth trigger might be desired, or just show a message. - return redirect(url_for('index_page')) + flash('Вы успешно вышли из сессии. Перезапустите приложение из Telegram.') + # Redirect to launch page, which will re-auth or show message + return redirect(url_for('launch_webapp')) +# --- Admin Panel Routes --- @app.route('/admhosto') def admin_panel(): - if not is_admin_session(): - flash('Доступ запрещен (Admin).', 'error') - return redirect(url_for('index_page')) - - data = load_data() - users_data_from_db = data.get('users', {}) # Keyed by telegram_user_id_str - + if not is_current_user_admin(): flash('Доступ запрещен.', 'error'); return redirect(url_for('dashboard')) + data = load_data_tg(); users = data.get('users', {}) user_details = [] - for tg_id_str, u_storage_entry in users_data_from_db.items(): + for tg_id_str, u_data in users.items(): file_count = 0 - # u_storage_entry is the dict containing 'telegram_info', 'filesystem', etc. - # Filesystem is directly under u_storage_entry['filesystem'] - - # Correctly count files within this user's filesystem - q = [u_storage_entry.get('filesystem', {}).get('children', [])] - processed_folders = set() # To avoid infinite loops with malformed data (though unlikely with UUIDs) - + q = [u_data.get('filesystem', {}).get('children', [])] while q: - current_level_children = q.pop(0) - for item in current_level_children: - if item.get('type') == 'file': - file_count += 1 - elif item.get('type') == 'folder' and 'children' in item and item.get('id') not in processed_folders: - q.append(item.get('children', [])) - processed_folders.add(item.get('id')) + current_level = q.pop(0) + for item in current_level: + if item.get('type') == 'file': file_count += 1 + elif item.get('type') == 'folder' and 'children' in item: q.append(item.get('children', [])) - tg_info = u_storage_entry.get('telegram_info', {}) - display_name = tg_info.get('username', tg_info.get('first_name', f'User_{tg_id_str}')) + display_name = u_data.get('telegram_raw', {}).get('first_name', f"User {tg_id_str}") + if u_data.get('telegram_raw', {}).get('last_name'): + display_name += f" {u_data.get('telegram_raw', {}).get('last_name')}" + if not display_name.strip() and u_data.get('telegram_raw', {}).get('username'): + display_name = u_data.get('telegram_raw', {}).get('username') user_details.append({ 'telegram_id_str': tg_id_str, 'display_name': display_name, - 'created_at': u_storage_entry.get('created_at', 'N/A'), + 'created_at': u_data.get('created_at', 'N/A'), 'file_count': file_count }) - user_details.sort(key=lambda x: x['display_name'].lower()) - - html = ''' - -Админ-панель - -

Админ-панель

-

Вернуться в приложение

-{% with messages = get_flashed_messages(with_categories=true) %}{% if messages %}{% for category, message in messages %}
{{ message }}
{% endfor %}{% endif %}{% endwith %} -

Пользователи (Telegram)

-{% for user in user_details %} -
- {{ user.display_name }} (ID: {{ user.telegram_id_str }}) -

Зарегистрирован: {{ user.created_at }}

-

Файлов: {{ user.file_count }}

-
- -
-
-{% else %}

Пользователей нет.

{% endfor %}
''' - return render_template_string(html, user_details=user_details) + return render_template_string(HTML_ADMIN_PANEL, user_details=user_details) @app.route('/admhosto/user/') def admin_user_files(telegram_user_id_str): - if not is_admin_session(): - flash('Доступ запрещен.', 'error') - return redirect(url_for('index_page')) + if not is_current_user_admin(): flash('Доступ запрещен.', 'error'); return redirect(url_for('dashboard')) + data = load_data_tg(); user_data = data.get('users', {}).get(telegram_user_id_str) + if not user_data: flash(f'Пользователь {telegram_user_id_str} не найден.', 'error'); return redirect(url_for('admin_panel')) - data = load_data() - user_storage_entry = data.get('users', {}).get(telegram_user_id_str) - if not user_storage_entry: - flash(f'Пользователь с ID {telegram_user_id_str} не найден.', 'error') - return redirect(url_for('admin_panel')) - - tg_info = user_storage_entry.get('telegram_info', {}) - display_name = tg_info.get('username', tg_info.get('first_name', f'User_{telegram_user_id_str}')) + display_name = user_data.get('telegram_raw', {}).get('first_name', f"User {telegram_user_id_str}") + if user_data.get('telegram_raw', {}).get('last_name'): display_name += f" {user_data.get('telegram_raw', {}).get('last_name')}" all_files = [] - # Filesystem is u_storage_entry['filesystem'] - user_filesystem = user_storage_entry.get('filesystem', {}) - - def collect_files_recursive(folder_node, current_path_id='root'): - # Get path string for the parent of items in folder_node.children - parent_path_str = get_node_path_string(user_filesystem, folder_node.get('id', 'root')) - # Path to the current folder_node itself - - for item in folder_node.get('children', []): - if item.get('type') == 'file': - item_copy = dict(item) # Avoid modifying original data - item_copy['parent_path_str'] = parent_path_str # Path of the containing folder - all_files.append(item_copy) - elif item.get('type') == 'folder': - collect_files_recursive(item, item.get('id')) # Recurse into subfolder - - collect_files_recursive(user_filesystem) # Start with the root filesystem object + def collect_files_admin(folder_fs, current_path_id='root'): + parent_path_str = get_node_path_string(user_data['filesystem'], current_path_id) + for item in folder_fs.get('children', []): + if item.get('type') == 'file': item['parent_path_str'] = parent_path_str; all_files.append(item) + elif item.get('type') == 'folder': collect_files_admin(item, item.get('id')) + collect_files_admin(user_data.get('filesystem', {})) all_files.sort(key=lambda x: x.get('upload_date', ''), reverse=True) - - - html = ''' -Файлы {{ display_name }} - -

Файлы пользователя: {{ display_name }} (ID: {{ telegram_user_id_str }})

-Назад к пользователям -{% with messages = get_flashed_messages(with_categories=true) %}{% if messages %}{% for category, message in messages %}
{{ message }}
{% endfor %}{% endif %}{% endwith %} -
-{% for file in files %} -
-
- {% if file.file_type == 'image' %} - {% elif file.file_type == 'video' %} - {% elif file.file_type == 'pdf' %}
📄
- {% elif file.file_type == 'text' %}
📝
- {% else %}
{% endif %} -

{{ file.original_filename | truncate(30) }}

-

В папке: {{ file.parent_path_str if file.parent_path_str else 'Root' }}

-

Загружен: {{ file.upload_date }}

-

ID: {{ file.id }}

-

Path: {{ file.path }}

-
-
- Скачать - {% set previewable = file.file_type in ['image', 'video', 'pdf', 'text'] %} - {% if previewable %} - - {% endif %} -
- -
-
-
-{% else %}

У пользователя нет файлов.

{% endfor %} -
- - - - -''' - return render_template_string(html, telegram_user_id_str=telegram_user_id_str, display_name=display_name, files=all_files, repo_id=REPO_ID, hf_file_url=lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{path}{'?download=true' if download else ''}") + + return render_template_string(HTML_ADMIN_USER_FILES, display_name=display_name, owner_telegram_id_str=telegram_user_id_str, files=all_files, repo_id=REPO_ID, hf_file_url=lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{path}{'?download=true' if download else ''}") @app.route('/admhosto/delete_user/', methods=['POST']) def admin_delete_user(telegram_user_id_str): - if not is_admin_session(): - flash('Доступ запрещен.', 'error') - return redirect(url_for('index_page')) - if not HF_TOKEN_WRITE: - flash('Удаление невозможно: токен для записи не настроен.', 'error') - return redirect(url_for('admin_panel')) - - data = load_data() - if telegram_user_id_str not in data['users']: - flash('Пользователь не найден!', 'error') - return redirect(url_for('admin_panel')) - - user_storage_entry = data['users'][telegram_user_id_str] - tg_info = user_storage_entry.get('telegram_info', {}) - display_name = tg_info.get('username', tg_info.get('first_name', f'User_{telegram_user_id_str}')) - logging.warning(f"ADMIN ACTION: Attempting to delete user {display_name} (ID: {telegram_user_id_str}) and all their data.") + if not is_current_user_admin(): flash('Доступ запрещен.', 'error'); return redirect(url_for('dashboard')) + if not HF_TOKEN_WRITE: flash('Удаление невозможно: токен для записи не настроен.', 'error'); return redirect(url_for('admin_panel')) + data = load_data_tg() + if telegram_user_id_str not in data['users']: flash('Пользователь не найден!', 'error'); return redirect(url_for('admin_panel')) + + logging.warning(f"ADMIN ACTION: Attempting to delete user {telegram_user_id_str} and all their data.") try: api = HfApi() - # Path on HF is based on telegram_user_id_str - user_folder_path_on_hf = f"cloud_files/{telegram_user_id_str}" - + user_folder_path_on_hf = f"cloud_files/{telegram_user_id_str}" # Path on HF based on TG ID logging.info(f"Attempting to delete HF Hub folder: {user_folder_path_on_hf} for user {telegram_user_id_str}") - # Deleting a folder might require deleting all files individually if API doesn't support recursive well - # For now, assume api.delete_folder works or handles non-empty state appropriately. - # Or, list all files and delete them first. This example uses delete_folder directly. - api.delete_folder( # This might fail if folder is not empty and API requires it - folder_path=user_folder_path_on_hf, - repo_id=REPO_ID, - repo_type="dataset", - token=HF_TOKEN_WRITE, - commit_message=f"ADMIN ACTION: Deleted all files/folders for user {telegram_user_id_str}" - ) + api.delete_folder(folder_path=user_folder_path_on_hf, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, commit_message=f"ADMIN ACTION: Deleted all files/folders for user {telegram_user_id_str}") logging.info(f"Successfully initiated deletion of folder {user_folder_path_on_hf} on HF Hub.") - except hf_utils.HfHubHTTPError as e: - if e.response.status_code == 404: # Not Found - logging.warning(f"User folder {user_folder_path_on_hf} not found on HF Hub for user {telegram_user_id_str}. Skipping HF deletion of folder.") - else: - logging.error(f"Error deleting user folder {user_folder_path_on_hf} from HF Hub for {telegram_user_id_str}: {e}") - flash(f'Ошибка при удалении файлов пользователя {display_name} с сервера: {e}. Пользователь НЕ удален из базы.', 'error') - return redirect(url_for('admin_panel')) - except Exception as e: # Other exceptions - logging.error(f"Unexpected error during HF Hub folder deletion for {telegram_user_id_str}: {e}") - flash(f'Неожиданная ошибка при удалении файлов {display_name} с сервера: {e}. Пользователь НЕ удален из базы.', 'error') - return redirect(url_for('admin_panel')) - - # If HF deletion was successful or skipped (404), proceed to delete from DB + if e.response.status_code == 404: logging.warning(f"User folder {user_folder_path_on_hf} not found on HF Hub for user {telegram_user_id_str}. Skipping HF deletion.") + else: logging.error(f"Error deleting user folder {user_folder_path_on_hf} from HF Hub for {telegram_user_id_str}: {e}"); flash(f'Ошибка при удалении файлов пользователя {telegram_user_id_str} с сервера: {e}. Пользователь НЕ удален из базы.', 'error'); return redirect(url_for('admin_panel')) + except Exception as e: logging.error(f"Unexpected error during HF Hub folder deletion for {telegram_user_id_str}: {e}"); flash(f'Неожиданная ошибка при удалении файлов {telegram_user_id_str} с сервера: {e}. Пользователь НЕ удален из базы.', 'error'); return redirect(url_for('admin_panel')) + try: del data['users'][telegram_user_id_str] - save_data(data) - flash(f'Пользователь {display_name} (ID: {telegram_user_id_str}) и его файлы (запрос на удаление с сервера отправлен) успешно удалены из базы данных!') + save_data_tg(data) + flash(f'Пользователь {telegram_user_id_str} и его файлы (запрос на удаление отправлен) успешно удалены из базы данных!') logging.info(f"ADMIN ACTION: Successfully deleted user {telegram_user_id_str} from database.") - except Exception as e: - logging.error(f"Error saving data after deleting user {telegram_user_id_str}: {e}") - # This is problematic: user might be partially deleted. - flash(f'Файлы пользователя {display_name} могли быть удалены с сервера, но произошла ошибка при удалении пользователя из базы данных: {e}', 'error') - + except Exception as e: logging.error(f"Error saving data after deleting user {telegram_user_id_str}: {e}"); flash(f'Файлы пользователя {telegram_user_id_str} удалены с сервера, но произошла ошибка при удалении пользователя из базы данных: {e}', 'error') return redirect(url_for('admin_panel')) +@app.route('/admhosto/delete_file//', methods=['POST']) +def admin_delete_file(telegram_user_id_str, file_id): + if not is_current_user_admin(): flash('Доступ запрещен.', 'error'); return redirect(url_for('dashboard')) + if not HF_TOKEN_WRITE: flash('Удаление невозможно: токен для записи не настроен.', 'error'); return redirect(url_for('admin_user_files', telegram_user_id_str=telegram_user_id_str)) -@app.route('/admhosto/delete_file//', methods=['POST']) -def admin_delete_file(telegram_user_id_str_for_file, file_id): - if not is_admin_session(): - flash('Доступ запрещен.', 'error') - return redirect(url_for('index_page')) # Or login if that's the admin entry - if not HF_TOKEN_WRITE: - flash('Удаление невозможно: токен для записи не настроен.', 'error') - return redirect(url_for('admin_user_files', telegram_user_id_str=telegram_user_id_str_for_file)) - - data = load_data() - user_storage_entry = data.get('users', {}).get(telegram_user_id_str_for_file) - if not user_storage_entry: - flash(f'Пользователь с ID {telegram_user_id_str_for_file} не найден.', 'error') - return redirect(url_for('admin_panel')) - - # Filesystem for the specific user - file_node, parent_node = find_node_by_id(user_storage_entry['filesystem'], file_id) - - if not file_node or file_node.get('type') != 'file': # parent_node check is implicitly handled by remove_node - flash('Файл не найден в структуре пользователя.', 'error') - return redirect(url_for('admin_user_files', telegram_user_id_str=telegram_user_id_str_for_file)) + data = load_data_tg(); user_data = data.get('users', {}).get(telegram_user_id_str) + if not user_data: flash(f'Пользователь {telegram_user_id_str} не найден.', 'error'); return redirect(url_for('admin_panel')) + file_node, parent_node = find_node_by_id(user_data['filesystem'], file_id) + if not file_node or file_node.get('type') != 'file' or not parent_node: flash('Файл не найден в структуре пользователя.', 'error'); return redirect(url_for('admin_user_files', telegram_user_id_str=telegram_user_id_str)) + hf_path = file_node.get('path') original_filename = file_node.get('original_filename', 'файл') - if not hf_path: - flash(f'Ошибка: Путь к файлу {original_filename} не найден в метаданных. Удаление только из базы.', 'error') - if remove_node(user_storage_entry['filesystem'], file_id): - try: - save_data(data) - flash(f'Метаданные файла {original_filename} удалены (путь отсутствовал).') - except Exception as e: - flash('Ошибка сохранения данных после удаления метаданных (путь отсутствовал).', 'error') - logging.error(f"Admin delete file metadata save error (no path): {e}") - return redirect(url_for('admin_user_files', telegram_user_id_str=telegram_user_id_str_for_file)) + flash(f'Ошибка: Путь к файлу {original_filename} не найден. Удаление только из базы.', 'error') + if remove_node(user_data['filesystem'], file_id): + try: save_data_tg(data); flash(f'Метаданные файла {original_filename} удалены (путь отсутствовал).') + except Exception as e: flash('Ошибка сохранения данных.', 'error'); logging.error(f"Admin delete file metadata save error (no path): {e}") + return redirect(url_for('admin_user_files', telegram_user_id_str=telegram_user_id_str)) try: api = HfApi() - admin_tg_username = session.get('telegram_username', 'UnknownAdmin') - commit_msg = f"ADMIN ACTION (by {admin_tg_username}): Deleted file {original_filename} (NodeID: {file_id}) for user {telegram_user_id_str_for_file}" - api.delete_file( - path_in_repo=hf_path, - repo_id=REPO_ID, - repo_type="dataset", - token=HF_TOKEN_WRITE, - commit_message=commit_msg - ) - logging.info(f"ADMIN ACTION: Deleted file {hf_path} from HF Hub for user {telegram_user_id_str_for_file}") - - if remove_node(user_storage_entry['filesystem'], file_id): - try: - save_data(data) - flash(f'Файл {original_filename} успешно удален!') - except Exception as e: - flash('Файл удален с сервера, но произошла ошибка обновления базы данных.', 'error') - logging.error(f"Admin delete file DB update error: {e}") - else: - # This state implies an inconsistency, should be rare - flash('Файл удален с сервера, но не найден в базе данных для удаления метаданных.', 'error') - + api.delete_file(path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, commit_message=f"ADMIN ACTION: Deleted file {original_filename} for user {telegram_user_id_str}") + logging.info(f"ADMIN ACTION: Deleted file {hf_path} from HF Hub for user {telegram_user_id_str}") + if remove_node(user_data['filesystem'], file_id): + try: save_data_tg(data); flash(f'Файл {original_filename} успешно удален!') + except Exception as e: flash('Файл удален с сервера, но ошибка обновления БД.', 'error'); logging.error(f"Admin delete file DB update error: {e}") + else: flash('Файл удален с сервера, но не найден в БД.', 'error') except hf_utils.EntryNotFoundError: - logging.warning(f"ADMIN ACTION: File {hf_path} not found on HF Hub during delete for user {telegram_user_id_str_for_file}. Removing from DB.") - if remove_node(user_storage_entry['filesystem'], file_id): - try: - save_data(data) - flash(f'Файл {original_filename} не найден на сервере, удален из базы.') - except Exception as e: - flash('Ошибка сохранения данных после удаления метаданных (файл не найден на сервере).', 'error') - logging.error(f"Admin delete file metadata save error (HF not found): {e}") - else: - flash('Файл не найден ни на сервере, ни в базе данных.', 'error') - + logging.warning(f"ADMIN ACTION: File {hf_path} not found on HF Hub for user {telegram_user_id_str}. Removing from DB.") + if remove_node(user_data['filesystem'], file_id): + try: save_data_tg(data); flash(f'Файл {original_filename} не найден на сервере, удален из базы.') + except Exception as e: flash('Ошибка сохранения данных.', 'error'); logging.error(f"Admin delete file metadata save error (HF not found): {e}") + else: flash('Файл не найден ни на сервере, ни в базе данных.', 'error') + except Exception as e: logging.error(f"ADMIN ACTION: Error deleting file {hf_path} for {telegram_user_id_str}: {e}"); flash(f'Ошибка удаления файла {original_filename}: {e}', 'error') + return redirect(url_for('admin_user_files', telegram_user_id_str=telegram_user_id_str)) + + +# --- Main Execution & Bot Runner --- +async def run_bot(application_bot): + logging.info("Initializing Telegram bot...") + await application_bot.initialize() + logging.info("Starting Telegram bot polling...") + await application_bot.start() + await application_bot.updater.start_polling() + logging.info("Telegram bot is running.") + +def bot_thread_target(bot_app_instance): + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: + loop.run_until_complete(run_bot(bot_app_instance)) + except KeyboardInterrupt: + logging.info("Bot polling interrupted by user.") except Exception as e: - logging.error(f"ADMIN ACTION: Error deleting file {hf_path} for {telegram_user_id_str_for_file}: {e}") - flash(f'Ошибка удаления файла {original_filename}: {e}', 'error') - - return redirect(url_for('admin_user_files', telegram_user_id_str=telegram_user_id_str_for_file)) + logging.error(f"Exception in bot thread: {e}", exc_info=True) + finally: + # Cleanly stop the bot if polling was started + if bot_app_instance.updater and bot_app_instance.updater.running: + loop.run_until_complete(bot_app_instance.updater.stop()) + loop.run_until_complete(bot_app_instance.stop()) # Ensure bot stops + loop.close() + logging.info("Bot thread finished.") if __name__ == '__main__': - if not BOT_API_TOKEN or BOT_API_TOKEN == "YOUR_TELEGRAM_BOT_TOKEN_HERE": - logging.error("CRITICAL: TELEGRAM_BOT_TOKEN is not set or is a placeholder. Telegram authentication will fail.") - if not ADMIN_TELEGRAM_IDS: - logging.warning("ADMIN_TELEGRAM_IDS is not set. Admin panel will not be accessible by any Telegram user.") - - if not HF_TOKEN_WRITE: - logging.warning("HF_TOKEN (write access) is not set. File uploads, deletions, and DB backups to HF will fail.") - if not HF_TOKEN_READ: - logging.warning("HF_TOKEN_READ is not set. Falling back to HF_TOKEN. File downloads/previews might fail for private repos if HF_TOKEN is also not set or invalid.") - - # Initial DB download: - # Perform this once at startup before starting the backup thread or serving requests. - logging.info("Performing initial database download/check before starting.") - load_data() # This will trigger download_db_from_hf if needed and initialize local file + if ADMIN_TELEGRAM_ID == "YOUR_ADMIN_TELEGRAM_ID_REPLACE_ME": + logging.warning("ADMIN_TELEGRAM_ID is not set. Admin panel functionality will not work correctly for any user.") + if WEBAPP_URL == "https://example.com/launch_webapp": + logging.warning("WEBAPP_URL is not set to your actual deployment URL. Telegram bot button might not work.") + if not HF_TOKEN_WRITE: logging.warning("HF_TOKEN (write access) is not set. File uploads, deletions, and backups will fail.") + if not HF_TOKEN_READ: logging.warning("HF_TOKEN_READ is not set. Falling back to HF_TOKEN. File downloads/previews might fail for private repos if HF_TOKEN is also not set.") + + # Initial DB download + if HF_TOKEN_WRITE or HF_TOKEN_READ: + logging.info("Performing initial database download before starting.") + download_db_from_hf() + else: # No tokens, create local empty if not exists + if not os.path.exists(DATA_FILE): + with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}}, f) + logging.info(f"Created empty local database file: {DATA_FILE} (no HF tokens provided).") - if HF_TOKEN_WRITE: # Only run backup thread if we can write to HF + # Start periodic backup if write token exists + if HF_TOKEN_WRITE: backup_thread = threading.Thread(target=periodic_backup, daemon=True) backup_thread.start() - logging.info("Periodic database backup thread to Hugging Face Hub started.") + logging.info("Periodic backup thread started.") else: - logging.warning("Periodic database backup to Hugging Face Hub is DISABLED because HF_TOKEN_WRITE is not set.") + logging.warning("Periodic backup disabled because HF_TOKEN (write access) is not set.") - app.run(debug=MOCK_TELEGRAM_MODE, host='0.0.0.0', port=7860) # Enable debug if MOCK_TELEGRAM_MODE is true \ No newline at end of file + # Setup and start Telegram bot + ptb_application = Application.builder().token(BOT_TOKEN).build() + ptb_application.add_handler(CommandHandler("start", start_command_handler)) + ptb_application.add_handler(MessageHandler(filters.COMMAND, unknown_command_handler)) # Handles any other command + + bot_main_thread = threading.Thread(target=bot_thread_target, args=(ptb_application,), daemon=True) + bot_main_thread.start() + + logging.info("Starting Flask application...") + # Use a production-ready WSGI server like Gunicorn or Waitress instead of app.run() for production + app.run(debug=False, host='0.0.0.0', port=7860) \ No newline at end of file