diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,424 +1,39 @@ -import asyncio -import hashlib -import hmac import json -import logging import os +import logging import threading import time -import uuid from datetime import datetime -from io import BytesIO -from urllib.parse import parse_qsl, unquote - -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 +import requests +from io import BytesIO +import uuid +import hashlib +import hmac +from urllib.parse import unquote -# --- 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) +from flask import Flask, render_template_string, request, redirect, url_for, session, flash, send_file, jsonify, Response +from flask_caching import Cache 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" +app.secret_key = os.getenv("FLASK_SECRET_KEY", "supersecretkey_folders_unique_telegram_mini_app") # Changed for new app +DATA_FILE = 'cloudeng_data_tg.json' # Changed data file name +REPO_ID = os.getenv("HF_REPO_ID", "Eluza133/Z1e1u") # Ensure this is set or use a default HF_TOKEN_WRITE = os.getenv("HF_TOKEN") HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") or HF_TOKEN_WRITE -UPLOAD_FOLDER = 'uploads_tg' +UPLOAD_FOLDER = 'uploads_tg' # Changed upload folder os.makedirs(UPLOAD_FOLDER, exist_ok=True) -cache = Cache(app, config={'CACHE_TYPE': 'simple'}) -logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') - +BOT_TOKEN = os.getenv("BOT_TOKEN", "6750208873:AAE2hvPlJ99dBdhGa_Brre0IIpUdOvXxHt4") +ADMIN_TELEGRAM_USER_ID = os.getenv("ADMIN_TELEGRAM_USER_ID") -# --- Styles --- -BASE_STYLE = ''' -:root { - --primary: #ff4d6d; --secondary: #00ddeb; --accent: #8b5cf6; - --background-light: #f5f6fa; --background-dark: #1a1625; - --card-bg: rgba(255, 255, 255, 0.95); --card-bg-dark: rgba(40, 35, 60, 0.95); - --text-light: #2a1e5a; --text-dark: #e8e1ff; --shadow: 0 10px 30px rgba(0, 0, 0, 0.2); - --glass-bg: rgba(255, 255, 255, 0.15); --transition: all 0.3s ease; --delete-color: #ff4444; - --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; } -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: 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: 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: 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(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(-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: 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(-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: 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: 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: 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: 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: 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 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;} -@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: 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: 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 */ } -''' -# --- HTML Templates --- -HTML_LAUNCH_PAGE = ''' - - - - Zeus Cloud - - - - - -
-

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

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

Zeus Cloud

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

-{% with messages = get_flashed_messages(with_categories=true) %} - {% 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 %} -
- {% if item.type == 'folder' %} - 📁 -

{{ item.name }}

-
- Открыть -
- - -
-
- {% elif item.type == 'file' %} - {% set previewable = item.file_type in ['image', 'video', 'pdf', 'text'] %} - {% if item.file_type == 'image' %} - {{ item.original_filename }} - {% elif item.file_type == 'video' %} - - {% elif item.file_type == 'pdf' %} -
📄
- {% elif item.file_type == 'text' %} -
📝
- {% else %} -
- {% endif %} -

{{ item.original_filename | truncate(25, True) }}

-

{{ item.upload_date }}

-
- Скачать - {% if previewable %}{% endif %} -
- - -
-
- {% endif %} -
- {% 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 %} -
- -''' +cache = Cache(app, config={'CACHE_TYPE': 'simple'}) +logging.basicConfig(level=logging.INFO) -# --- Core Logic Helper Functions (Filesystem) --- +# --- Helper Functions (largely unchanged, but context might change) --- def find_node_by_id(filesystem, node_id): if not filesystem: return None, None if filesystem.get('id') == node_id: @@ -427,11 +42,11 @@ def find_node_by_id(filesystem, node_id): 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']: + 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)) # current_node is parent here + queue.append((child, current_node)) return None, None def add_node(filesystem, parent_id, node_data): @@ -448,11 +63,6 @@ def remove_node(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): @@ -467,70 +77,61 @@ def get_node_path_string(filesystem, node_id): 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): +def initialize_user_filesystem(user_data): # user_data is data['users'][tg_user_id_str] 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'] + user_data['filesystem'] = { + "type": "folder", + "id": "root", + "name": "root", + "children": [] + } + # Removed old file migration logic, assuming new users or already structured data for TG users @cache.memoize(timeout=300) -def load_data_tg(): +def load_data(): 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 + if not isinstance(data, dict): + logging.warning("Data is not in dict format, initializing empty database") + return {'users': {}} + data.setdefault('users', {}) + # Filesystem initialization now happens upon user login/first access if needed + # or when admin views a user that hasn't logged in yet. + for user_id_str, user_data_val in data['users'].items(): + initialize_user_filesystem(user_data_val) # Ensure all loaded users have fs structure + logging.info("Data successfully loaded and initialized") + return data except Exception as e: - logging.error(f"Error loading data (TG version): {e}") + logging.error(f"Error loading data: {e}") return {'users': {}} -def save_data_tg(data): +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 (TG version)") + logging.info("Data saved and uploaded to HF") except Exception as e: - logging.error(f"Error saving data (TG version): {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 + 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}") + 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"TGMA 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: @@ -539,160 +140,300 @@ def download_db_from_hf(): 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)") + 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") except (hf_utils.RepositoryNotFoundError, hf_utils.EntryNotFoundError): - logging.warning(f"{DATA_FILE} or repo not found. Initializing empty database.") + logging.warning(f"DB not found in repo {REPO_ID}. 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}") + 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 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") +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' + +# --- Telegram Auth --- +def check_telegram_authorization(auth_data_dict): + if not BOT_TOKEN: + logging.error("BOT_TOKEN is not set. Cannot verify Telegram authorization.") return None - received_hash = parsed_data.pop('hash') + check_hash = auth_data_dict.pop('hash', None) + if not check_hash: return None + + data_check_arr = [] + for key, value in sorted(auth_data_dict.items()): + data_check_arr.append(f"{key}={value}") + data_check_string = "\n".join(data_check_arr) - data_check_string_parts = [] - for key, value in sorted(parsed_data.items()): - data_check_string_parts.append(f"{key}={value}") + secret_key = hashlib.sha256(BOT_TOKEN.encode()).digest() + calculated_hash = hmac.new(secret_key, data_check_string.encode(), hashlib.sha256).hexdigest() - data_check_string = "\n".join(data_check_string_parts) + if calculated_hash == check_hash: + try: + user_data = json.loads(auth_data_dict['user']) + return user_data + except (KeyError, json.JSONDecodeError) as e: + logging.error(f"Error parsing user data from Telegram auth: {e}") + return None + return None - secret_key = hmac.new("WebAppData".encode('utf-8'), bot_token_val.encode('utf-8'), hashlib.sha256).digest() - calculated_hash = hmac.new(secret_key, data_check_string.encode('utf-8'), hashlib.sha256).hexdigest() +# --- Admin Check --- +def is_admin(): + if not ADMIN_TELEGRAM_USER_ID: return False + return 'telegram_user_id' in session and str(session['telegram_user_id']) == ADMIN_TELEGRAM_USER_ID - if calculated_hash == received_hash: - user_data_json_str = parsed_data.get('user') - if user_data_json_str: - try: - return json.loads(user_data_json_str) - except json.JSONDecodeError as e: - logging.error(f"Failed to parse user JSON: {e}") - return None - else: - logging.error("User data not found in parsed_data") - return None - else: - logging.warning(f"Hash mismatch. Calculated: {calculated_hash}, Received: {received_hash}") - return None +# --- HTML Shell for Mini App --- +MINI_APP_SHELL_HTML = """ + + + + + + Zeus Cloud + + + + +
+

Загрузка Zeus Cloud...

+
+
+ + + +""" -# --- Flask Routes --- +# --- Base Style (remains largely the same) --- +BASE_STYLE = ''' +:root { + --primary: #ff4d6d; --secondary: #00ddeb; --accent: #8b5cf6; + --background-light: #f5f6fa; --background-dark: #1a1625; + --card-bg: rgba(255, 255, 255, 0.95); --card-bg-dark: rgba(40, 35, 60, 0.95); + --text-light: #2a1e5a; --text-dark: #e8e1ff; --shadow: 0 10px 30px rgba(0, 0, 0, 0.2); + --glass-bg: rgba(255, 255, 255, 0.15); --transition: all 0.3s ease; --delete-color: #ff4444; + --folder-color: #ffc107; +} +* { margin: 0; padding: 0; box-sizing: border-box; } +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + background: var(--tg-theme-bg-color, var(--background-light)); + color: var(--tg-theme-text-color, var(--text-light)); + line-height: 1.6; + padding-bottom: 70px; /* Space for potential MainButton */ +} +/* body.dark specific styles are less relevant as Telegram handles theme */ +.container { margin: 10px auto; max-width: 1200px; padding: 15px; background: var(--tg-theme-secondary-bg-color, var(--card-bg)); border-radius: 12px; box-shadow: var(--shadow); overflow-x: hidden; } +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.4em; margin-top: 25px; color: var(--tg-theme-text-color, var(--text-light)); } +h4 { font-size: 1.0em; margin-top: 12px; margin-bottom: 4px; color: var(--tg-theme-link-color, var(--accent)); } +ol, ul { margin-left: 20px; margin-bottom: 12px; } +li { margin-bottom: 4px; } +input, textarea { width: 100%; padding: 12px; margin: 10px 0; border: 1px solid var(--tg-theme-hint-color, #ccc); border-radius: 10px; background: var(--tg-theme-bg-color, var(--glass-bg)); color: var(--tg-theme-text-color, var(--text-light)); font-size: 1em; } +input:focus, textarea:focus { outline: none; border-color: var(--tg-theme-link-color, var(--primary)); box-shadow: 0 0 0 2px var(--tg-theme-link-color, var(--primary)); } +.btn { padding: 12px 24px; background: var(--tg-theme-button-color, var(--primary)); color: var(--tg-theme-button-text-color, white); border: none; border-radius: 10px; cursor: pointer; font-size: 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 { opacity: 0.9; transform: scale(1.03); } +.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(--tg-theme-link-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(--tg-theme-destructive-text-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(--tg-theme-secondary-bg-color, var(--card-bg)); border-radius: 12px; margin-bottom: 8px; box-shadow: var(--shadow); transition: var(--transition); } +.user-item:hover { transform: translateY(-3px); } +.user-item a { color: var(--tg-theme-link-color, var(--primary)); text-decoration: none; font-weight: 600; } +.item { background: var(--tg-theme-secondary-bg-color, var(--card-bg)); padding: 12px; border-radius: 12px; box-shadow: var(--shadow); text-align: center; transition: var(--transition); display: flex; flex-direction: column; justify-content: space-between; } +.item:hover { transform: translateY(-3px); } +.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(--tg-theme-link-color, var(--folder-color)); line-height: 100px; } +.item p { font-size: 0.85em; margin: 4px 0; word-break: break-all; } +.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; } +.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: 95%; max-height: 95%; background: var(--tg-theme-secondary-bg-color, #fff); padding: 10px; border-radius: 15px; overflow: auto; position: relative; } +.modal img, .modal video, .modal iframe, .modal pre { max-width: 100%; max-height: 85vh; display: block; margin: auto; border-radius: 10px; } +.modal iframe { width: 90vw; height: 85vh; border: none; } /* Adjusted for TWA */ +.modal pre { background: var(--tg-theme-bg-color, #eee); color: var(--tg-theme-text-color, #333); padding: 15px; border-radius: 8px; white-space: pre-wrap; word-wrap: break-word; text-align: left; max-height: 85vh; overflow-y: auto;} +.modal-close-btn { position: absolute; top: 10px; right: 15px; font-size: 24px; color: var(--tg-theme-hint-color, #aaa); cursor: pointer; background: rgba(0,0,0,0.3); border-radius: 50%; width: 28px; height: 28px; line-height: 28px; text-align: center; } +#progress-container { width: 100%; background: var(--tg-theme-hint-color, var(--glass-bg)); border-radius: 10px; margin: 15px 0; display: none; position: relative; height: 20px; } +#progress-bar { width: 0%; height: 100%; background: var(--tg-theme-button-color, var(--primary)); border-radius: 10px; transition: width 0.3s ease; } +#progress-text { position: absolute; width: 100%; text-align: center; line-height: 20px; color: var(--tg-theme-button-text-color, white); font-size: 0.9em; font-weight: bold; text-shadow: 1px 1px 1px rgba(0,0,0,0.5); } +.breadcrumbs { margin-bottom: 15px; font-size: 1em; } +.breadcrumbs a { color: var(--tg-theme-link-color, var(--accent)); text-decoration: none; } +.breadcrumbs a:hover { text-decoration: underline; } +.breadcrumbs span { margin: 0 5px; color: var(--tg-theme-hint-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: 150px; } +.folder-actions .btn { margin: 0; flex-shrink: 0;} +@media (max-width: 768px) { /* These might need less adjustment for TWA */ + .file-grid { grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); } + .item-preview { height: 90px; } + .item.folder .item-preview { font-size: 40px; line-height: 90px; } +} +@media (max-width: 480px) { + .container { padding: 10px; } + .file-grid { grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); gap: 10px; } + .item-preview { height: 70px; } + .item.folder .item-preview { font-size: 35px; line-height: 70px; } + .item p { font-size: 0.8em;} + .breadcrumbs { font-size: 0.9em; } +} +''' + +# --- Routes --- @app.route('/') -def index(): - return redirect(url_for('launch_webapp')) # Redirect to the Mini App launch page +def index_redirect(): + # Redirect to the Mini App entry point or an info page + return redirect(url_for('mini_app_entry')) -@app.route('/launch_webapp') -def launch_webapp(): - return render_template_string(HTML_LAUNCH_PAGE) +@app.route('/app') +def mini_app_entry(): + return render_template_string(MINI_APP_SHELL_HTML) -@app.route('/auth_telegram', methods=['POST']) +@app.route('/auth/telegram', methods=['POST']) def auth_telegram(): try: - payload = request.get_json() - init_data_str = payload.get('init_data') + init_data_str = request.form.get('initData') if not init_data_str: - return jsonify({'status': 'error', 'message': 'init_data отсутствует'}), 400 - - user_tg_data = validate_telegram_data(init_data_str, BOT_TOKEN) - - if user_tg_data and 'id' in user_tg_data: - telegram_user_id = str(user_tg_data['id']) - display_name = user_tg_data.get('first_name', '') - if user_tg_data.get('last_name'): - display_name += f" {user_tg_data['last_name']}" - if not display_name and user_tg_data.get('username'): - display_name = user_tg_data['username'] - if not display_name: - display_name = f"User {telegram_user_id}" - - session['telegram_user_id'] = telegram_user_id - session['display_name'] = display_name - - # Initialize user in database if new - data = load_data_tg() - if telegram_user_id not in data['users']: - data['users'][telegram_user_id] = { - 'telegram_raw': user_tg_data, # Store raw TG data if needed - 'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), - 'filesystem': {"type": "folder", "id": "root", "name": "root", "children": []} + return jsonify({'status': 'error', 'message': 'No initData received'}), 400 + + auth_data_dict = dict(param.split('=', 1) for param in unquote(init_data_str).split('&')) + + tg_user = check_telegram_authorization(auth_data_dict.copy()) + + if tg_user: + user_id_str = str(tg_user['id']) + session['telegram_user_id'] = tg_user['id'] + session['telegram_user_info'] = tg_user + + data = load_data() + if user_id_str not in data['users']: + data['users'][user_id_str] = { + 'tg_username': tg_user.get('username'), + 'tg_first_name': tg_user.get('first_name'), + 'tg_last_name': tg_user.get('last_name'), + 'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S') + # Filesystem initialized by initialize_user_filesystem below } - try: - save_data_tg(data) - except Exception as e: - logging.error(f"Error saving new user data for {telegram_user_id}: {e}") - return jsonify({'status': 'error', 'message': 'Ошибка сохранения данных пользователя'}), 500 - # Ensure filesystem exists for existing users too (idempotent init) - user_db_entry = data['users'][telegram_user_id] - initialize_user_filesystem_tg(user_db_entry, telegram_user_id) - - - return jsonify({'status': 'success', 'redirect_url': url_for('dashboard')}) + # Ensure filesystem structure exists for the user + initialize_user_filesystem(data['users'][user_id_str]) + + try: + save_data(data) # Save if new user or if filesystem was just initialized + except Exception as e: + logging.error(f"Error saving user data for TGID {user_id_str}: {e}") + return jsonify({'status': 'error', 'message': 'Error saving user data'}), 500 + + return jsonify({'status': 'success', 'user': tg_user, 'redirect_url': url_for('dashboard')}) else: - logging.error("Telegram data validation failed or user ID missing.") - return jsonify({'status': 'error', 'message': 'Ошибка валидации данных Telegram'}), 403 + return jsonify({'status': 'error', 'message': 'Invalid Telegram authorization'}), 403 except Exception as e: - logging.error(f"Error in /auth_telegram: {e}") - return jsonify({'status': 'error', 'message': f'Внутренняя ошибка сервера: {str(e)}'}), 500 + logging.error(f"Error in Telegram auth: {e}") + return jsonify({'status': 'error', 'message': str(e)}), 500 + @app.route('/dashboard', methods=['GET', 'POST']) def dashboard(): if 'telegram_user_id' not in session: - flash('Пожалуйста, авторизуйтесь через Telegram.') - return redirect(url_for('launch_webapp')) + flash('Пожалуйста, авторизуйтесь через Telegram.', 'error') + # In a mini app, this usually means redirecting to the auth flow or showing an error. + # Since /app handles auth, direct access to /dashboard shouldn't happen unauthenticated. + return render_template_string("

Доступ запрещен. Пожалуйста, откройте приложение через Telegram.

"), 403 + + tg_user_id_str = str(session['telegram_user_id']) + tg_user_info = session.get('telegram_user_info', {}) + user_display_name = tg_user_info.get('first_name', tg_user_info.get('username', f"User {tg_user_id_str}")) - tg_user_id = session['telegram_user_id'] - data = load_data_tg() - if tg_user_id not in data['users']: + data = load_data() + if tg_user_id_str not in data['users']: session.clear() - flash('Пользователь не найден в базе данных!') - return redirect(url_for('launch_webapp')) + flash('Данные пользователя не найдены. Пожалуйста, перезайдите.', 'error') + return render_template_string("

Данные пользователя не найдены. Пожалуйста, перезайдите через Telegram.

"), 403 - user_data = data['users'][tg_user_id] - initialize_user_filesystem_tg(user_data, tg_user_id) # Ensure filesystem structure + user_data = data['users'][tg_user_id_str] + initialize_user_filesystem(user_data) # Ensure filesystem exists current_folder_id = request.args.get('folder_id', 'root') current_folder, parent_folder = find_node_by_id(user_data['filesystem'], current_folder_id) if not current_folder or current_folder.get('type') != 'folder': - flash('Папка не найдена!', 'error'); current_folder_id = 'root' + flash('Папка не найдена!', 'error') + current_folder_id = 'root' current_folder, parent_folder = find_node_by_id(user_data['filesystem'], current_folder_id) - if not current_folder: # Should not happen if root is always initialized - logging.error(f"CRITICAL: Root folder not found for user {tg_user_id}") - flash('Критическая ошибка: корневая папка не найдена.', 'error'); session.clear() - return redirect(url_for('launch_webapp')) - + if not current_folder: # Should not happen if initialized + logging.error(f"CRITICAL: Root folder not found for user TGID {tg_user_id_str}") + flash('Критическая ошибка: корневая папка не найдена.', 'error') + session.clear() + return redirect(url_for('mini_app_entry')) # Re-auth + items_in_folder = sorted(current_folder.get('children', []), key=lambda x: (x['type'] != 'folder', x.get('name', x.get('original_filename', '')).lower())) if request.method == 'POST': @@ -704,17 +445,23 @@ def dashboard(): 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: + + if len(files) > 20: # Limit number of files 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')) - api = HfApi(); uploaded_count = 0; errors = [] + 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) @@ -722,29 +469,43 @@ def dashboard(): 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}" + + hf_path = f"cloud_files/{tg_user_id_str}/{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')} + 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 TGID:{tg_user_id_str} 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 + else: # Should not happen if target_folder_node is valid 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}") + logging.error(f"Failed to add node metadata for file {file_id} to folder {target_folder_id} for user TGID {tg_user_id_str}") 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}") + logging.error(f"Error uploading file {original_filename} for TGID {tg_user_id_str}: {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}") + try: + save_data(data) + flash(f'{uploaded_count} файл(ов) успешно загружено!') + except Exception as e: + flash('Файлы загружены на сервер, но произошла ошибка сохранения метаданных.', 'error') + logging.error(f"Error saving data after upload for TGID {tg_user_id_str}: {e}") if errors: for error_msg in errors: flash(error_msg, 'error') return redirect(url_for('dashboard', folder_id=target_folder_id)) @@ -754,393 +515,466 @@ def dashboard(): 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)}) + is_link = (node['id'] != current_folder_id) + breadcrumbs.append({'id': node['id'], 'name': node.get('name', 'Root'), 'is_link': is_link}) if not parent: break temp_id = parent.get('id') breadcrumbs.reverse() - 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()) + html = ''' + +Zeus Cloud + +
+

Zeus Cloud

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

+{% with messages = get_flashed_messages(with_categories=true) %}{% 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 %}
+ {% if item.type == 'folder' %}📁

{{ item.name }}

+
Открыть +
+
+ {% elif item.type == 'file' %}{% set previewable = item.file_type in ['image', 'video', 'pdf', 'text'] %} + {% if item.file_type == 'image' %}{{ item.original_filename|e }} + {% elif item.file_type == 'video' %} + {% elif item.file_type == 'pdf' %}
📄
+ {% elif item.file_type == 'text' %}
📝
+ {% else %}
{% endif %} +

{{ item.original_filename | truncate(25, True) }}

{{ item.upload_date }}

+
+ Скачать + {% if previewable %}{% endif %} +
+
{% endif %}
+ {% endfor %}{% if not items %}

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

{% endif %}
+ +
+ +''' + template_context = { + 'user_display_name': user_display_name, 'items': items_in_folder, 'current_folder_id': current_folder_id, + 'current_folder': current_folder, 'breadcrumbs': breadcrumbs, '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, **template_context) @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) + tg_user_id_str = str(session['telegram_user_id']) + data = load_data() + user_data = data['users'].get(tg_user_id_str) 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)) - # 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_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') + + if not folder_name: + flash('Имя папки не может быть пустым!', 'error') + elif not all(c.isalnum() or c.isspace() or c in '_-.' for c in folder_name) or len(folder_name) > 50 : # Basic validation + flash('Имя папки содержит недопустимые символы или слишком длинное.', 'error') + else: + folder_id = uuid.uuid4().hex + 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(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)) + @app.route('/download/') def download_file(file_id): - is_admin_access = is_current_user_admin() - if 'telegram_user_id' not in session and not is_admin_access: - flash('Пожалуйста, авторизуйтесь.') - return redirect(url_for('launch_webapp')) + allow_access = False + current_user_tg_id_str = str(session.get('telegram_user_id')) if 'telegram_user_id' in session else None + + data = load_data() + file_node = None + # file_owner_tg_id_str = None # Not strictly needed here, but good for logging if implemented - data = load_data_tg() - file_node = None; owner_tg_user_id = None + # Check if current user is admin + is_current_user_admin = is_admin() - if 'telegram_user_id' in session: - current_tg_user_id = session['telegram_user_id'] - user_data = data['users'].get(current_tg_user_id) + if current_user_tg_id_str: + user_data = data['users'].get(current_user_tg_id_str) 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) + _file_node, _ = find_node_by_id(user_data.get('filesystem', {}), file_id) + if _file_node and _file_node.get('type') == 'file': + file_node = _file_node + allow_access = True # Owner has access + + if not file_node and is_current_user_admin: # If admin and file not found under their own ID (or they are searching) + for tg_id, udata in data.get('users', {}).items(): + if tg_id == current_user_tg_id_str: continue # Already checked + node, _ = find_node_by_id(udata.get('filesystem', {}), file_id) if node and node.get('type') == 'file': - 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 + file_node = node; allow_access = True; break - if not file_node or file_node.get('type') != 'file': - flash('Файл не найден!', 'error') - return redirect(request.referrer or url_for('dashboard')) + if not allow_access or not file_node: + flash('Файл не найден или доступ запрещен!', 'error') + return redirect(url_for('dashboard') if current_user_tg_id_str else url_for('mini_app_entry')) 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')) + flash('Ошибка: Путь к файлу не найден.', 'error') + return redirect(url_for('dashboard', folder_id=request.args.get('folder_id', 'root'))) # Stay in current folder file_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{hf_path}?download=true" try: - headers = {}; + 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') + return send_file(BytesIO(response.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') + flash(f'Ошибка скачивания файла: {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')) + flash('Непредвиденная ошибка при скачивании.', 'error') + return redirect(url_for('dashboard', folder_id=request.args.get('folder_id', 'root'))) @app.route('/delete_file/', methods=['POST']) def delete_file(file_id): - 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')) + if 'telegram_user_id' not in session: flash('Пожалуйста, авторизуйтесь.'); return redirect(url_for('mini_app_entry')) + tg_user_id_str = str(session['telegram_user_id']) + data = load_data() + user_data = data['users'].get(tg_user_id_str) + if not user_data: flash('Пользователь не найден!'); session.clear(); return redirect(url_for('mini_app_entry')) 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: # 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: - 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"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)) - - try: - api = HfApi() - 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 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 {tg_user_id}: {e}") - flash(f'Ошибка удаления файла {original_filename}: {e}', 'error') + if not file_node or file_node.get('type') != 'file' or not parent_node: + flash('Файл не найден или не может быть удален.', 'error') + elif not HF_TOKEN_WRITE: + flash('Удаление невозможно: токен для записи не настроен.', 'error') + else: + hf_path = file_node.get('path') + original_filename = file_node.get('original_filename', 'файл') + try: + if hf_path: + api = HfApi() + api.delete_file(path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, + commit_message=f"User TGID:{tg_user_id_str} deleted file {original_filename}") + if remove_node(user_data['filesystem'], file_id): save_data(data) + flash(f'Файл {original_filename} успешно удален!') + except hf_utils.EntryNotFoundError: # File not on HF, remove from DB + if remove_node(user_data['filesystem'], file_id): save_data(data) + flash(f'Файл {original_filename} не найден на сервере, удален из базы.') + except Exception as e: + logging.error(f"Error deleting file {hf_path} for TGID {tg_user_id_str}: {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('launch_webapp')) + if 'telegram_user_id' not in session: flash('Пожалуйста, авторизуйтесь.'); return redirect(url_for('mini_app_entry')) 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')) + tg_user_id_str = str(session['telegram_user_id']) + data = load_data() + user_data = data['users'].get(tg_user_id_str) + if not user_data: flash('Пользователь не найден!'); session.clear(); return redirect(url_for('mini_app_entry')) 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') - if not folder_node or folder_node.get('type') != 'folder' or not parent_node: flash('Папка не найдена или не может быть удалена.', 'error') - return redirect(url_for('dashboard', folder_id=current_view_folder_id)) - - folder_name = folder_node.get('name', 'папка') - if folder_node.get('children'): - flash(f'Папку "{folder_name}" можно удалить только если она пуста.', 'error') - return redirect(url_for('dashboard', folder_id=current_view_folder_id)) # Redirect back to the folder containing the one we tried to delete - - 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)) + elif folder_node.get('children'): + flash(f'Папку "{folder_node.get("name", "папка")}" можно удалить только если она пуста.', 'error') + else: + if remove_node(user_data['filesystem'], folder_id): + try: save_data(data); flash(f'Папка "{folder_node.get("name", "папка")}" удалена.') + except Exception as e: flash('Ошибка сохранения данных.', 'error'); logging.error(f"Delete folder save error: {e}") + else: flash('Не удалось удалить папку.', 'error') + return redirect(url_for('dashboard', folder_id=current_view_folder_id)) @app.route('/get_text_content/') def get_text_content(file_id): - is_admin_access = is_current_user_admin() - if 'telegram_user_id' not in session and not is_admin_access: - return Response("Не авторизован", status=401) - - data = load_data_tg() - file_node = None; owner_tg_user_id = None + allow_access = False + current_user_tg_id_str = str(session.get('telegram_user_id')) if 'telegram_user_id' in session else None + + data = load_data() + file_node = None + is_current_user_admin = is_admin() - if 'telegram_user_id' in session: - current_tg_user_id = session['telegram_user_id'] - user_data = data['users'].get(current_tg_user_id) + if current_user_tg_id_str: + user_data = data['users'].get(current_user_tg_id_str) 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) + _file_node, _ = find_node_by_id(user_data.get('filesystem', {}), file_id) + if _file_node and _file_node.get('type') == 'file' and _file_node.get('file_type') == 'text': + file_node = _file_node; allow_access = True + + if not file_node and is_current_user_admin: + for tg_id, udata in data.get('users', {}).items(): + if tg_id == current_user_tg_id_str: continue + node, _ = find_node_by_id(udata.get('filesystem', {}), file_id) if node and node.get('type') == 'file' and node.get('file_type') == 'text': - 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) + file_node = node; allow_access = True; break + if not allow_access or not file_node: return Response("Текстовый файл не найден или доступ запрещен", status=404) + hf_path = file_node.get('path') 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}" - response = requests.get(file_url, headers=headers) + response = requests.get(file_url, headers=headers, timeout=10) response.raise_for_status() 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.') - # Redirect to launch page, which will re-auth or show message - return redirect(url_for('launch_webapp')) - + except requests.exceptions.RequestException as e: return Response(f"Ошибка загрузки: {e}", status=502) + except Exception as e: return Response("Внутренняя ошибка", status=500) -# --- Admin Panel Routes --- +# --- Admin Routes --- @app.route('/admhosto') def admin_panel(): - if not is_current_user_admin(): flash('Доступ запрещен.', 'error'); return redirect(url_for('dashboard')) - data = load_data_tg(); users = data.get('users', {}) + if not is_admin(): flash('Доступ запрещен.', 'error'); return redirect(url_for('mini_app_entry')) + data = load_data() + users = data.get('users', {}) user_details = [] - for tg_id_str, u_data in users.items(): - file_count = 0 - q = [u_data.get('filesystem', {}).get('children', [])] + for tg_id_str, udata in users.items(): + file_count = 0; q = [udata.get('filesystem', {}).get('children', [])] while q: 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', [])) - - 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_data.get('created_at', 'N/A'), - 'file_count': file_count + 'telegram_user_id': tg_id_str, + 'display_name': udata.get('tg_first_name', udata.get('tg_username', f"TGID: {tg_id_str}")), + 'created_at': udata.get('created_at', 'N/A'), 'file_count': file_count }) - 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_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')) - - 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')}" + html = ''' +Админ-панель +

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

+ +{% 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 }} ({{user.telegram_user_id}}) +

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

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

+
+
+{% else %}

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

{% endfor %}
+''' + return render_template_string(html, user_details=user_details) + +@app.route('/admhosto/user/') +def admin_user_files(telegram_user_id): + if not is_admin(): flash('Доступ запрещен.', 'error'); return redirect(url_for('mini_app_entry')) + data = load_data() + user_data = data.get('users', {}).get(telegram_user_id) + if not user_data: flash(f'Пользователь {telegram_user_id} не найден.', 'error'); return redirect(url_for('admin_panel')) + + initialize_user_filesystem(user_data) # Ensure fs for this user if accessed by admin first time + user_display_name = user_data.get('tg_first_name', user_data.get('tg_username', f"TGID: {telegram_user_id}")) all_files = [] - 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) + def collect_files_recursive(folder_node, current_path_str_list): + for item in folder_node.get('children', []): + if item.get('type') == 'file': + item_copy = item.copy() + item_copy['parent_path_str'] = " / ".join(current_path_str_list) or "Root" + all_files.append(item_copy) + elif item.get('type') == 'folder': + collect_files_recursive(item, current_path_str_list + [item.get('name', 'Unnamed Folder')]) - 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 ''}") + collect_files_recursive(user_data.get('filesystem', {}), []) + all_files.sort(key=lambda x: x.get('upload_date', ''), reverse=True) + html = ''' +Файлы {{ user_display_name }} +

Файлы: {{ user_display_name }} ({{telegram_user_id}})

+Назад +{% 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(25) }}

+

В папке: {{ 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 %}
+ +''' + return render_template_string(html, telegram_user_id=telegram_user_id, user_display_name=user_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 ''}") -@app.route('/admhosto/delete_user/', methods=['POST']) -def admin_delete_user(telegram_user_id_str): - 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')) +@app.route('/admhosto/delete_user/', methods=['POST']) +def admin_delete_user(telegram_user_id): + if not is_admin(): flash('Доступ запрещен.', 'error'); return redirect(url_for('mini_app_entry')) + 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')) + data = load_data() + if telegram_user_id 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.") + logging.warning(f"ADMIN ACTION: Attempting to delete user TGID {telegram_user_id} and all their data.") try: - api = HfApi() - 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}") - 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}") + api = HfApi(); user_folder_path_on_hf = f"cloud_files/{telegram_user_id}" + logging.info(f"Attempting to delete HF Hub folder: {user_folder_path_on_hf} for user TGID {telegram_user_id}") + 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 TGID {telegram_user_id}") 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: 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')) + if e.response.status_code == 404: logging.warning(f"User folder {user_folder_path_on_hf} not found on HF Hub.") + else: logging.error(f"Error deleting user folder {user_folder_path_on_hf} from HF Hub: {e}"); flash(f'Ошибка удаления файлов с сервера: {e}. Пользователь НЕ удален из базы.', 'error'); return redirect(url_for('admin_panel')) + except Exception as e: logging.error(f"Unexpected error during HF folder deletion: {e}"); flash(f'Неожиданная ошибка удаления файлов с сервера: {e}. Пользователь НЕ удален из базы.', 'error'); return redirect(url_for('admin_panel')) try: - del data['users'][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}"); flash(f'Файлы пользователя {telegram_user_id_str} удалены с сервера, но произошла ошибка при удалении пользователя из базы данных: {e}', 'error') + del data['users'][telegram_user_id]; save_data(data) + flash(f'Пользователь TGID {telegram_user_id} и его файлы удалены/запрошены к удалению!') + logging.info(f"ADMIN ACTION: Successfully deleted user TGID {telegram_user_id} from database.") + except Exception as e: logging.error(f"Error saving data after deleting user {telegram_user_id}: {e}"); flash(f'Файлы удалены с сервера, но ошибка удаления из базы: {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, file_id): + if not is_admin(): flash('Доступ запрещен.', 'error'); return redirect(url_for('mini_app_entry')) + if not HF_TOKEN_WRITE: flash('Удаление невозможно: токен не настроен.', 'error'); return redirect(url_for('admin_user_files', telegram_user_id=telegram_user_id)) - 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_data = data.get('users', {}).get(telegram_user_id) + if not user_data: flash(f'Пользователь {telegram_user_id} не найден.', 'error'); return redirect(url_for('admin_panel')) + + file_node, _ = find_node_by_id(user_data.get('filesystem',{}), file_id) + if not file_node or file_node.get('type') != 'file': flash('Файл не найден.', 'error'); return redirect(url_for('admin_user_files', telegram_user_id=telegram_user_id)) - 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_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() - 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') + if hf_path: + api = HfApi() + 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 TGID {telegram_user_id}") + if remove_node(user_data['filesystem'], file_id): save_data(data) + flash(f'Файл {original_filename} удален!') except hf_utils.EntryNotFoundError: - 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.") + if remove_node(user_data['filesystem'], file_id): save_data(data) + flash(f'Файл {original_filename} не найден на сервере, удален из базы.') except Exception as e: - 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.") - + logging.error(f"ADMIN ACTION: Error deleting file {hf_path} for TGID {telegram_user_id}: {e}") + flash(f'Ошибка удаления файла {original_filename}: {e}', 'error') + return redirect(url_for('admin_user_files', telegram_user_id=telegram_user_id)) if __name__ == '__main__': - 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 BOT_TOKEN: logging.critical("BOT_TOKEN is not set. Telegram authentication will FAIL.") + if not ADMIN_TELEGRAM_USER_ID: logging.warning("ADMIN_TELEGRAM_USER_ID is not set. Admin panel will be inaccessible.") 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 not HF_TOKEN_READ: logging.warning("HF_TOKEN_READ is not set (or HF_TOKEN). Downloads/previews might fail for private repos.") - # 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("Performing initial database download before starting background backup.") + download_db_from_hf() # Download once at start + threading.Thread(target=periodic_backup, daemon=True).start() logging.info("Periodic backup thread started.") else: - logging.warning("Periodic backup disabled because HF_TOKEN (write access) is not set.") - - # 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 + logging.warning("Periodic backup disabled (HF_TOKEN_WRITE not set).") + if HF_TOKEN_READ: download_db_from_hf() + elif not os.path.exists(DATA_FILE): # No tokens and no local 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}") + + app.run(debug=False, host='0.0.0.0', port=int(os.getenv("PORT", 7860))) \ No newline at end of file