diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,1309 +1,1071 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -import os import flask -from flask import Flask, request, Response, render_template_string, jsonify, redirect, url_for, send_file -import hmac -import hashlib +from flask import Flask, render_template_string, request, redirect, url_for, session, flash, send_file, jsonify, Response +from flask_caching import Cache import json -from urllib.parse import unquote, parse_qs, quote -import time -from datetime import datetime +import os import logging import threading -from huggingface_hub import HfApi, hf_hub_download, list_repo_files -from huggingface_hub.utils import RepositoryNotFoundError, EntryNotFoundError -import mimetypes -import io -import math - -BOT_TOKEN = os.getenv("BOT_TOKEN", "6750208873:AAE2hvPlJ99dBdhGa_Brre0IIpUdOvXxHt4") -HOST = '0.0.0.0' -PORT = 7860 -DATA_FILE = 'data.json' - -REPO_ID = os.getenv("HF_REPO_ID", "Eluza133/Z1e1u") -HF_DATA_FILE_PATH = "data.json" -HF_UPLOAD_FOLDER = "uploads" -HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE") -HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") - -MAX_UPLOAD_FILES = 20 -AUTH_TIMEOUT = 86400 +import time +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 hashlib +import hmac + +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, WebAppInfo +from telegram.ext import Application as TelegramApplication, CommandHandler, ContextTypes app = Flask(__name__) -logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') -app.secret_key = os.urandom(24) +app.secret_key = os.getenv("FLASK_SECRET_KEY", "supersecretkey_folders_unique_telegram_mini_app") -_data_lock = threading.RLock() -metadata_cache = {} +DATA_FILE = 'cloudeng_data_tg.json' +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_tg' +os.makedirs(UPLOAD_FOLDER, exist_ok=True) -def get_hf_api(write=False): - token = HF_TOKEN_WRITE if write else HF_TOKEN_READ - if not token: - logging.warning(f"Hugging Face {'write' if write else 'read'} token not set.") - return None - return HfApi(token=token) - -def download_metadata_from_hf(): - global metadata_cache - api = get_hf_api(write=False) - if not api: - logging.warning("HF Read token missing. Cannot download metadata.") - return False - try: - logging.info(f"Attempting to download {HF_DATA_FILE_PATH} from {REPO_ID}...") - download_path = hf_hub_download( - repo_id=REPO_ID, - filename=HF_DATA_FILE_PATH, - repo_type="dataset", - token=api.token, - local_dir=".", - local_dir_use_symlinks=False, - force_download=True, - etag_timeout=10 - ) - logging.info("Metadata file successfully downloaded from Hugging Face.") - with _data_lock: - try: - with open(download_path, 'r', encoding='utf-8') as f: - metadata_cache = json.load(f) - logging.info("Successfully loaded downloaded metadata into cache.") - except (FileNotFoundError, json.JSONDecodeError) as e: - logging.error(f"Error reading downloaded metadata file: {e}. Resetting cache.") - metadata_cache = {} +BOT_TOKEN = "6750208873:AAE2hvPlJ99dBdhGa_Brre0IIpUdOvXxHt4" +WEB_APP_URL = os.getenv("WEB_APP_URL", "https://your-flask-app-domain.com/launch_mini_app") # IMPORTANT: Update this to your ngrok/deployed URL +ADMIN_USER = os.getenv("ADMIN_USER", "admin_zeus") +ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "admin_password_zeus") + + +cache = Cache(app, config={'CACHE_TYPE': 'simple'}) +logging.basicConfig(level=logging.INFO) +logging.getLogger("httpx").setLevel(logging.WARNING) # Reduce verbosity from httpx used by huggingface_hub + +# --- Filesystem Helper Functions --- +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 - except EntryNotFoundError: - logging.warning(f"Metadata file '{HF_DATA_FILE_PATH}' not found in repo '{REPO_ID}'. Starting fresh.") - with _data_lock: - metadata_cache = {} + 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 - except RepositoryNotFoundError: - logging.error(f"Hugging Face repository '{REPO_ID}' not found. Cannot download metadata.") - except Exception as e: - logging.error(f"Error downloading metadata from Hugging Face: {e}", exc_info=True) return False -def load_local_metadata(): - global metadata_cache - with _data_lock: - if not metadata_cache: - try: - with open(DATA_FILE, 'r', encoding='utf-8') as f: - metadata_cache = json.load(f) - logging.info("Metadata loaded from local JSON.") - except FileNotFoundError: - logging.warning(f"{DATA_FILE} not found locally. Starting with empty data.") - metadata_cache = {} - except json.JSONDecodeError: - logging.error(f"Error decoding {DATA_FILE}. Starting with empty data.") - metadata_cache = {} - except Exception as e: - logging.error(f"Unexpected error loading metadata: {e}") - metadata_cache = {} - return metadata_cache - -def save_metadata(data_to_update=None): - global metadata_cache - with _data_lock: - try: - if data_to_update: - metadata_cache.update(data_to_update) - with open(DATA_FILE, 'w', encoding='utf-8') as f: - json.dump(metadata_cache, f, ensure_ascii=False, indent=4) - logging.info(f"Metadata successfully saved locally to {DATA_FILE}.") - upload_metadata_to_hf_async() - return True - except Exception as e: - logging.error(f"Error saving metadata: {e}", exc_info=True) - return False - -def update_user_file_metadata(user_id, file_info_list): - user_id_str = str(user_id) - with _data_lock: - if user_id_str not in metadata_cache: - metadata_cache[user_id_str] = {"user_info": {}, "files": []} - if "files" not in metadata_cache[user_id_str]: - metadata_cache[user_id_str]["files"] = [] - existing_filenames = {f['filename'] for f in metadata_cache[user_id_str]["files"]} - new_files_added = 0 - for file_info in file_info_list: - if file_info['filename'] not in existing_filenames: - metadata_cache[user_id_str]["files"].append(file_info) - existing_filenames.add(file_info['filename']) - new_files_added += 1 - else: - logging.warning(f"File '{file_info['filename']}' already exists for user {user_id}. Skipping add.") - if new_files_added > 0: - logging.info(f"Added {new_files_added} file metadata entries for user {user_id}.") - if not save_metadata(): - return False - else: - logging.info(f"No new file metadata added for user {user_id}.") - return True +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 _upload_metadata_to_hf_task(): - api = get_hf_api(write=True) - if not api: - logging.warning("HF Write token missing. Skipping metadata upload.") - return - if not os.path.exists(DATA_FILE): - logging.warning(f"{DATA_FILE} does not exist locally. Skipping upload.") +def initialize_user_filesystem(user_data_param): # Renamed to avoid conflict + if 'filesystem' not in user_data_param: + user_data_param['filesystem'] = { + "type": "folder", "id": "root", "name": "root", "children": [] + } + if 'files' in user_data_param and isinstance(user_data_param['files'], list): # Migration for old structure + for old_file in user_data_param['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}" + # session['username'] would not be available here directly if called outside request context + # This part of migration might need username context if hf_path depends on it + # For new users, this block is skipped. + # Let's assume this is called when user_data_param is for current user, session is available + current_user_key = session.get('username', 'unknown_user_during_migration') + + hf_path = f"cloud_files/{current_user_key}/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_param['filesystem'], 'root', file_node) + del user_data_param['files'] + +# --- Data Persistence & HF Sync --- +@cache.memoize(timeout=300) +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): + return {'users': {}} + data.setdefault('users', {}) + # Filesystem initialization handled on login/auth if needed + 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() + except Exception as e: + logging.error(f"Error saving data: {e}") + raise + +def upload_db_to_hf(): + if not HF_TOKEN_WRITE: 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"TGApp DB Backup {datetime.now()}") + except Exception as e: + logging.error(f"Error uploading database: {e}") + +def download_db_from_hf(): + if not HF_TOKEN_READ: + if not os.path.exists(DATA_FILE): + with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}}, f) return try: - with _data_lock: - if os.path.getsize(DATA_FILE) == 0: - logging.warning(f"{DATA_FILE} is empty. Skipping upload.") - return - file_to_upload = DATA_FILE - logging.info(f"Attempting to upload {file_to_upload} to {REPO_ID}/{HF_DATA_FILE_PATH}...") - api.upload_file( - path_or_fileobj=file_to_upload, - path_in_repo=HF_DATA_FILE_PATH, - repo_id=REPO_ID, - repo_type="dataset", - commit_message=f"Update metadata {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" - ) - logging.info("Metadata successfully uploaded to Hugging Face.") + hf_hub_download(repo_id=REPO_ID, filename=DATA_FILE, repo_type="dataset", token=HF_TOKEN_READ, local_dir=".", local_dir_use_symlinks=False) + except (hf_utils.RepositoryNotFoundError, hf_utils.EntryNotFoundError): + 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 uploading metadata to Hugging Face: {e}", exc_info=True) + 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) + upload_db_to_hf() -def upload_metadata_to_hf_async(): - upload_thread = threading.Thread(target=_upload_metadata_to_hf_task, daemon=True) - upload_thread.start() +def get_file_type(filename): + ext = filename.lower().split('.')[-1] + if ext in ['mp4', 'mov', 'avi', 'webm', 'mkv']: return 'video' + if ext in ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg']: return 'image' + if ext == 'pdf': return 'pdf' + if ext == 'txt': return 'text' + return 'other' -def verify_telegram_data(init_data_str): +# --- Auth Helpers --- +def is_admin(): + return session.get('admin_logged_in', False) + +def validate_telegram_init_data(init_data_str, bot_token_to_validate): try: - parsed_data = parse_qs(init_data_str) - received_hash = parsed_data.pop('hash', [None])[0] - if not received_hash: - logging.warning("Verification failed: Hash missing from initData.") - return None, False, "Hash missing" - data_check_list = [] - for key, value in sorted(parsed_data.items()): - data_check_list.append(f"{key}={value[0]}") - data_check_string = "\n".join(data_check_list) - secret_key = hmac.new("WebAppData".encode(), BOT_TOKEN.encode(), hashlib.sha256).digest() + params = {} + for item in init_data_str.split('&'): + key, value = item.split('=', 1) + params[key] = value + + hash_received = params.pop('hash') + + data_check_string_parts = [] + for key in sorted(params.keys()): + data_check_string_parts.append(f"{key}={params[key]}") + data_check_string = "\n".join(data_check_string_parts) + + secret_key = hmac.new("WebAppData".encode(), bot_token_to_validate.encode(), hashlib.sha256).digest() calculated_hash = hmac.new(secret_key, data_check_string.encode(), hashlib.sha256).hexdigest() - if calculated_hash != received_hash: - logging.warning(f"Verification failed: Hash mismatch. Calculated: {calculated_hash}, Received: {received_hash}") - return parsed_data, False, "Invalid hash" - auth_date = int(parsed_data.get('auth_date', [0])[0]) - current_time = int(time.time()) - if current_time - auth_date > AUTH_TIMEOUT: - logging.warning(f"Verification failed: initData expired. Auth time: {auth_date}, Current time: {current_time}") - return parsed_data, False, "Data expired" - user_info_dict = None - if 'user' in parsed_data: - try: - user_json_str = unquote(parsed_data['user'][0]) - user_info_dict = json.loads(user_json_str) - except Exception as e: - logging.error(f"Could not parse user JSON from initData: {e}") - logging.info(f"Telegram data verified successfully for user ID: {user_info_dict.get('id') if user_info_dict else 'Unknown'}") - return user_info_dict, True, "Verified" + + if calculated_hash == hash_received: + user_data_str = params.get('user') + if user_data_str: + return json.loads(requests.utils.unquote(user_data_str)) + return None except Exception as e: - logging.error(f"Error during Telegram data verification: {e}", exc_info=True) - return None, False, "Verification exception" - -def authenticate_and_get_user(init_data_str): - user_info, is_valid, message = verify_telegram_data(init_data_str) - if not is_valid: - return None, message - user_id = user_info.get('id') if user_info else None - if not user_id: - logging.warning("Verification successful but user ID is missing in user data.") - return None, "User ID missing" - user_id_str = str(user_id) - with _data_lock: - should_save = False - if user_id_str not in metadata_cache: - metadata_cache[user_id_str] = { - "user_info": user_info, - "files": [] - } - logging.info(f"New user registered: {user_id}") - should_save = True - else: - if "user_info" not in metadata_cache[user_id_str] or metadata_cache[user_id_str]["user_info"] != user_info: - metadata_cache[user_id_str]["user_info"] = user_info - should_save = True - if should_save: - if not save_metadata(): - logging.error(f"Failed to save metadata after updating/adding user {user_id}") - return user_info, "Authenticated" - -USER_TEMPLATE = """ - - - - - - Zeus Cloud - - - - - - - -
-
-

Zeus Cloud

- -
-
-

Загрузить файлы

- - -
Файлы не выбраны
- -
-
-
-
-

Ваши файлы

-
- -
-
- - +

Zeus Cloud

Инициализация...

+ +''' - async function handleUpload() { - if (currentFiles.length === 0 || !userInitData) { - uploadStatusDiv.textContent = 'Выберите файлы для загрузки.'; - return; - } - uploadButton.disabled = true; - uploadButton.classList.remove('enabled'); - uploadStatusDiv.textContent = 'Загрузка началась...'; - uploadStatusDiv.style.color = 'var(--tg-theme-hint-color)'; - progressBar.style.display = 'block'; - progressBarInner.style.width = '0%'; - const formData = new FormData(); - currentFiles.forEach(file => { - formData.append('files', file); - }); - formData.append('initData', userInitData); - - try { - const responseText = await new Promise((resolve, reject) => { - const xhr = new XMLHttpRequest(); - xhr.open('POST', '/upload', true); - xhr.upload.onprogress = function(event) { - if (event.lengthComputable) { - const percentComplete = (event.loaded / event.total) * 100; - progressBarInner.style.width = percentComplete + '%'; - uploadStatusDiv.textContent = `Загрузка... ${Math.round(percentComplete)}%`; - } - }; - xhr.onload = function() { - if (xhr.status >= 200 && xhr.status < 300) { - resolve(xhr.responseText); - } else { - let errorMessage = `Ошибка загрузки (Статус: ${xhr.status})`; - try { - const errorData = JSON.parse(xhr.responseText); - errorMessage = errorData.message || errorMessage; - } catch (e) { /* use default */ } - reject(new Error(errorMessage)); - } - }; - xhr.onerror = function() { - reject(new Error('Сетевая ошибка при загрузке.')); - }; - xhr.send(formData); - }); - - const data = JSON.parse(responseText); - uploadStatusDiv.textContent = data.message || 'Загрузка успешно завершена!'; - uploadStatusDiv.style.color = 'green'; - fileInput.value = ''; - currentFiles = []; - selectedFilesDiv.textContent = 'Файлы не выбраны'; - fetchFiles(); - } catch (error) { - console.error('Upload error:', error); - uploadStatusDiv.textContent = `Ошибка: ${error.message}`; - uploadStatusDiv.style.color = 'red'; - } finally { - progressBar.style.display = 'none'; - if (currentFiles.length > 0) { // Re-check currentFiles for enabling upload button - uploadButton.classList.add('enabled'); - uploadButton.disabled = false; - } else { - uploadButton.classList.remove('enabled'); - uploadButton.disabled = true; - selectedFilesDiv.textContent = 'Файлы не выбраны'; - } - } - } +ADMIN_LOGIN_HTML = ''' + +Admin Login - Zeus Cloud +

Admin Login

+{% with messages = get_flashed_messages(with_categories=true) %}{% if messages %}{% for category, message in messages %}
{{ message }}
{% endfor %}{% endif %}{% endwith %} +
+
+
''' - function openViewer(file) { - modal.style.display = 'block'; - modalContent.innerHTML = ''; - modalCaption.textContent = file.filename; - const mimeType = file.content_type || ''; - const downloadUrl = `/download/${encodeURIComponent(file.filename)}?initData=${encodeURIComponent(userInitData)}`; - let element; - if (mimeType.startsWith('image/')) { - element = document.createElement('img'); - element.src = downloadUrl; - element.alt = file.filename; - } else if (mimeType.startsWith('video/')) { - element = document.createElement('video'); - element.src = downloadUrl; - element.controls = true; - element.autoplay = true; - } else if (mimeType.startsWith('audio/')) { - element = document.createElement('audio'); - element.src = downloadUrl; - element.controls = true; - element.autoplay = true; - element.style.padding = '20px'; - } - if (element) { - modalContent.appendChild(element); - if (tg.HapticFeedback) { - tg.HapticFeedback.impactOccurred('light'); - } - } else { - modalCaption.textContent = 'Предпросмотр недоступен для этого типа файла.'; - } - } - - function closeViewer() { - modal.style.display = 'none'; - const mediaElement = modalContent.querySelector('video, audio'); - if (mediaElement) { - mediaElement.pause(); - mediaElement.src = ''; - } - modalContent.innerHTML = ''; - } - - modalCloseBtn.onclick = closeViewer; - modal.onclick = function(event) { - if (event.target === modal) { - closeViewer(); - } - }; - - function setupTelegram() { - if (!tg || !tg.initData) { - console.error("Telegram WebApp script not loaded or initData is missing."); - userGreeting.textContent = 'Ошибка: Не удалось инициализировать Telegram.'; - document.body.style.visibility = 'visible'; - return; - } - tg.ready(); - tg.expand(); - applyTheme(tg.themeParams); - tg.onEvent('themeChanged', () => applyTheme(tg.themeParams)); - userInitData = tg.initData; - const user = tg.initDataUnsafe?.user; - if (user) { - const name = user.first_name || user.username || 'Пользователь'; - userGreeting.textContent = `Привет, ${name}!`; - } else { - userGreeting.textContent = 'Привет!'; - } - fetchFiles(); - fileInput.addEventListener('change', handleFileSelection); - uploadButton.addEventListener('click', handleUpload); - document.body.style.visibility = 'visible'; - } - - if (window.Telegram && window.Telegram.WebApp) { - setupTelegram(); - } else { - console.warn("Telegram WebApp script not immediately available, waiting for window.onload"); - window.addEventListener('load', setupTelegram); - setTimeout(() => { - if (document.body.style.visibility !== 'visible') { - console.error("Telegram WebApp script fallback timeout triggered."); - userGreeting.textContent = 'Ошибка загрузки интерфейса Telegram.'; - document.body.style.visibility = 'visible'; - } - }, 3500); - } - - - -""" - -ADMIN_TEMPLATE = """ - - - - - - Admin - Zeus Cloud - - - - - - -
-

Zeus Cloud - Админ Панель

-
ВНИМАНИЕ: Этот раздел не защищен! Добавьте аутентификацию для реального использования.
-
-

Управление метаданными

- -
-
-
- {% if users %} -
- {% for user_id, data in users.items() %} -
-
- User Avatar - -
-
-
Язык: {{ data.user_info.language_code or 'N/A' }}
-
Premium: {{ 'Да' if data.user_info and data.user_info.is_premium else 'Нет' }}
-
-
- Ф��йлов загружено: {{ data.files|length if data.files else 0 }} -
- Просмотреть файлы -
- {% endfor %} -
- {% else %} -

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

- {% endif %} -
- - - -""" - -ADMIN_USER_FILES_TEMPLATE = """ - - - - - - Файлы пользователя {{ user_info.first_name or user_id }} - Admin - - - - - - -
- ← Назад к списку пользователей -

Файлы пользователя

-
{{ user_info.first_name or '' }} {{ user_info.last_name or '' }} (ID: {{ user_id }})
- - {% if files %} - - - - - - - - - - - - {% for file_item in files|sort(attribute='uploaded_at_ts', reverse=true) %} - - - - - - - - {% endfor %} - -
Имя файлаРазмерДата загрузкиТипДействия
{{ file_item.filename }}{{ file_item.size | filesizeformat if file_item.size else 'N/A' }}{{ file_item.uploaded_at_str or 'N/A' }}{{ file_item.content_type or 'N/A' }} - {% set mime_type = file_item.content_type or '' %} - {% if mime_type.startswith('image/') or mime_type.startswith('video/') or mime_type.startswith('audio/') %} - - {% endif %} - Скачать -
- {% else %} -

У этого пользователя нет загруженных файлов.

- {% endif %} -
- - - - +
+

Zeus Cloud

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

+{% 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 (element) { - adminModalContent.appendChild(element); - } else { - adminModalCaption.textContent = 'Предпросмотр недоступен для этого типа файла.'; - } - } +ADMIN_PANEL_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.username }} +

Зарегистрирован: {{ user.created_at }} (TG ID: {{ user.tg_id if user.tg_id else 'N/A' }})

+

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

+
+ +
+
+{% else %}

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

{% endfor %}
+Выйти из админки +
''' - function closeAdminViewer() { - adminModal.style.display = 'none'; - const mediaElement = adminModalContent.querySelector('video, audio'); - if (mediaElement) { - mediaElement.pause(); - mediaElement.src = ''; - } - adminModalContent.innerHTML = ''; - } +ADMIN_USER_FILES_HTML = ''' +Файлы {{ username }} + +

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

+Назад к пользователям +{% 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 %}
+ +''' - if(adminModalCloseBtn) adminModalCloseBtn.onclick = closeAdminViewer; - if(adminModal) { - adminModal.onclick = function(event) { - if (event.target === adminModal) { - closeAdminViewer(); - } - }; - } - - - -""" +# --- Telegram Bot Setup --- +async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE): + keyboard = [[InlineKeyboardButton("☁️ Открыть Zeus Cloud ☁️", web_app=WebAppInfo(url=WEB_APP_URL))]] + reply_markup = InlineKeyboardMarkup(keyboard) + await update.message.reply_text('Добро пожаловать в Zeus Cloud! Нажмите кнопку ниже, чтобы открыть веб-приложение.', reply_markup=reply_markup) -@app.template_filter('filesizeformat') -def filesizeformat(value): - try: - bytes_val = int(value) - if bytes_val == 0: return '0 Bytes' - k = 1024 - sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'] - i = min(int(math.floor(math.log(bytes_val) / math.log(k))), len(sizes) - 1) - return f"{bytes_val / math.pow(k, i):.2f} {sizes[i]}" - except (ValueError, TypeError): - return value - except Exception: - return 'N/A' +bot_app = TelegramApplication.builder().token(BOT_TOKEN).build() +bot_app.add_handler(CommandHandler("start", start_command)) +# --- Flask Routes --- @app.route('/') def index(): - return render_template_string(USER_TEMPLATE, theme={}, max_files=MAX_UPLOAD_FILES) - -@app.route('/files', methods=['POST']) -def get_user_files(): - req_data = request.get_json() - init_data_str = req_data.get('initData') - if not init_data_str: - return jsonify({"status": "error", "message": "Missing initData"}), 400 - user_info, message = authenticate_and_get_user(init_data_str) - if not user_info: - return jsonify({"status": "error", "message": message}), 403 - user_id_str = str(user_info['id']) - with _data_lock: - user_data = metadata_cache.get(user_id_str, {}) - files = user_data.get('files', []) - return jsonify({"status": "ok", "files": files}), 200 - -@app.route('/upload', methods=['POST']) -def upload_files(): - init_data_str = request.form.get('initData') - if not init_data_str: - return jsonify({"status": "error", "message": "Missing initData"}), 400 - user_info, message = authenticate_and_get_user(init_data_str) - if not user_info: - return jsonify({"status": "error", "message": message}), 403 - user_id = user_info['id'] - user_id_str = str(user_id) - uploaded_files = request.files.getlist('files') - if not uploaded_files or len(uploaded_files) == 0: - return jsonify({"status": "error", "message": "No files selected for upload."}), 400 - if len(uploaded_files) > MAX_UPLOAD_FILES: - return jsonify({"status": "error", "message": f"Cannot upload more than {MAX_UPLOAD_FILES} files at once."}), 400 - api = get_hf_api(write=True) - if not api: - return jsonify({"status": "error", "message": "Server error: Cannot connect to storage."}), 500 - successful_uploads_metadata = [] - errors = [] - for file_storage in uploaded_files: - filename = file_storage.filename - if not filename: - errors.append("Received a file without a name.") - continue - path_in_repo = f"{HF_UPLOAD_FOLDER}/{user_id_str}/{filename}" - file_content = file_storage.read() - file_size = len(file_content) - content_type, _ = mimetypes.guess_type(filename) - try: - logging.info(f"Uploading '{filename}' for user {user_id} to {path_in_repo}...") - file_obj = io.BytesIO(file_content) - api.upload_file( - path_or_fileobj=file_obj, - path_in_repo=path_in_repo, - repo_id=REPO_ID, - repo_type="dataset", - commit_message=f"User {user_id} uploaded {filename}" - ) - logging.info(f"Successfully uploaded '{filename}' for user {user_id}.") - now = time.time() - successful_uploads_metadata.append({ - "filename": filename, - "hf_path": path_in_repo, - "uploaded_at_ts": now, - "uploaded_at_str": datetime.fromtimestamp(now).strftime('%Y-%m-%d %H:%M:%S'), - "size": file_size, - "content_type": content_type - }) - except Exception as e: - logging.error(f"Failed to upload '{filename}' for user {user_id}: {e}", exc_info=True) - errors.append(f"Ошибка загрузки {filename}: {str(e)}") - if successful_uploads_metadata: - if not update_user_file_metadata(user_id, successful_uploads_metadata): - errors.append("Ошибка обновления списка файлов после загрузки.") - if not errors: - return jsonify({"status": "ok", "message": f"Загружено {len(successful_uploads_metadata)} файл(ов)."}), 200 - else: - return jsonify({ - "status": "error" if not successful_uploads_metadata else "partial_success", - "message": f"Загружено {len(successful_uploads_metadata)} из {len(uploaded_files)}. Ошибки: {'; '.join(errors)}", - "uploaded_files": [f['filename'] for f in successful_uploads_metadata], - "errors": errors - }), 207 - -@app.route('/download/', methods=['GET']) -def download_file(filename): - init_data_str = request.args.get('initData') - if not init_data_str: - return "Authentication required.", 401 - user_info, message = authenticate_and_get_user(init_data_str) - if not user_info: - return f"Access denied: {message}", 403 - user_id = user_info['id'] - user_id_str = str(user_id) - with _data_lock: - user_data = metadata_cache.get(user_id_str, {}) - user_files = user_data.get('files', []) - file_metadata = next((f for f in user_files if f['filename'] == filename), None) - if not file_metadata: - logging.warning(f"User {user_id} attempted to download unlisted/unowned file: {filename}") - return "File not found or access denied.", 404 - api = get_hf_api(write=False) - if not api: - return "Server error: Cannot connect to storage.", 500 - path_in_repo = file_metadata.get('hf_path', f"{HF_UPLOAD_FOLDER}/{user_id_str}/{filename}") + return redirect(url_for('launch_mini_app')) + +@app.route('/launch_mini_app') +def launch_mini_app(): + return render_template_string(LAUNCH_MINI_APP_HTML) + +@app.route('/telegram_auth_callback', methods=['POST']) +def telegram_auth_callback(): + try: + payload = request.get_json() + init_data_str = payload.get('initData') + if not init_data_str: + return jsonify({'status': 'error', 'message': 'initData отсутствует'}), 400 + + tg_user_data = validate_telegram_init_data(init_data_str, BOT_TOKEN) + + if tg_user_data: + user_id_str = str(tg_user_data.get('id')) + username = tg_user_data.get('username', f"tg_{user_id_str}") + + session['telegram_user_id'] = user_id_str + session['telegram_username'] = tg_user_data.get('username') # Store original TG username if exists + session['username'] = username # This will be the key in data['users'] + session.permanent = True + + + data = load_data() + if username not in data['users']: + data['users'][username] = { + 'password': 'telegram_auth', + 'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + 'telegram_id': user_id_str, + 'telegram_profile': tg_user_data + } + + user_account_data = data['users'][username] + if 'filesystem' not in user_account_data: + initialize_user_filesystem(user_account_data) + + if user_account_data.get('password') == 'telegram_auth': # Ensure TG ID is stored + user_account_data['telegram_id'] = user_id_str + user_account_data['telegram_profile'] = tg_user_data + + + try: + save_data(data) + except Exception as e: + logging.error(f"Error saving data after Telegram auth for {username}: {e}") + return jsonify({'status': 'error', 'message': 'Ошибка сохранения данных пользователя'}), 500 + + return jsonify({'status': 'success', 'redirect': url_for('dashboard')}) + else: + return jsonify({'status': 'error', 'message': 'Неверные данные аутентификации Telegram'}), 403 + except Exception as e: + logging.error(f"Exception in telegram_auth_callback: {e}") + return jsonify({'status': 'error', 'message': f'Внутренняя ошибка сервера: {str(e)}'}), 500 + +@app.route('/dashboard', methods=['GET', 'POST']) +def dashboard(): + if 'telegram_user_id' not in session and not is_admin(): # Admin can view dashboard after form login + flash('Пожалуйста, авторизуйтесь через Telegram.') + return redirect(url_for('launch_mini_app')) + + username = session.get('username') + if not username: # Should not happen if TG auth worked or admin logged in + session.clear() + return redirect(url_for('launch_mini_app')) + + data = load_data() + if username not in data['users']: + session.clear() + flash('Пользователь не найден!') + return redirect(url_for('launch_mini_app')) + + user_data = data['users'][username] + if 'filesystem' not in user_data: + initialize_user_filesystem(user_data) # Pass user_data itself + + 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' + current_folder, parent_folder = find_node_by_id(user_data['filesystem'], current_folder_id) + if not current_folder: + flash('Критическая ошибка: корневая папка не найдена.', 'error') + session.clear() + return redirect(url_for('launch_mini_app')) + + 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': + 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')) + + api = HfApi() + uploaded_count = 0; errors = [] + for file in files: + if file and file.filename: + original_filename = secure_filename(file.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 + hf_path = f"cloud_files/{username}/{target_folder_id}/{unique_filename}" + temp_path = os.path.join(UPLOAD_FOLDER, f"{file_id}_{unique_filename}") + try: + file.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 {username} 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: + errors.append(f"Ошибка добавления метаданных для {original_filename}.") + 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: errors.append(f"Ошибка загрузки файла {original_filename}: {e}") + finally: + if os.path.exists(temp_path): os.remove(temp_path) + if uploaded_count > 0: + try: save_data(data); flash(f'{uploaded_count} файл(ов) успешно загружено!') + except Exception as e: flash('Файлы загружены на сервер, но произошла ошибка сохранения метаданных.', 'error') + 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 + 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(DASHBOARD_HTML, username=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) + +@app.route('/create_folder', methods=['POST']) +def create_folder(): + if 'telegram_user_id' not in session and not is_admin(): + return jsonify({'status': 'error', 'message': 'Не авторизован'}), 401 + + username = session['username'] + data = load_data() + user_data = data['users'].get(username) + 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 broader range of characters for folder names + # if not folder_name.isalnum() and '_' not in folder_name and ' ' not 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(data); flash(f'Папка "{folder_name}" успешно создана.') + except Exception as e: flash('Ошибка сохранения данных при создании папки.', 'error') + else: flash('Не удалось найти родительскую папку.', 'error') + return redirect(url_for('dashboard', folder_id=parent_folder_id)) + +@app.route('/download/') +def download_file(file_id): + is_tg_user = 'telegram_user_id' in session + is_form_admin = is_admin() + + if not is_tg_user and not is_form_admin: + flash('Пожалуйста, войдите в систему!') + if request.referrer and 'admhosto' in request.referrer: + return redirect(url_for('god_mode_login')) + return redirect(url_for('launch_mini_app')) + + data = load_data() + file_node = None; username_context = None + + if session.get('username'): # TG user or form admin + username_context = session['username'] + user_data = data['users'].get(username_context) + if user_data: file_node, _ = find_node_by_id(user_data.get('filesystem',{}), file_id) + + if not file_node and is_form_admin: # Admin cross-user search + for uname, udata in data.get('users', {}).items(): + node, _ = find_node_by_id(udata.get('filesystem', {}), file_id) + if node and node.get('type') == 'file': + file_node = node; username_context = uname; break + + if not file_node or file_node.get('type') != 'file': + flash('Файл не найден!', 'error') + return redirect(request.referrer or url_for('dashboard' if is_tg_user else 'admin_panel')) + + 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 is_tg_user else 'admin_panel')) + + file_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{hf_path}?download=true" try: - logging.info(f"User {user_id} requesting download of {path_in_repo}") - local_file_path = hf_hub_download( - repo_id=REPO_ID, - filename=path_in_repo, - repo_type="dataset", - token=api.token, - force_download=False, - etag_timeout=10 - ) - logging.info(f"File {path_in_repo} downloaded to cache: {local_file_path}") - content_type = file_metadata.get('content_type') or mimetypes.guess_type(filename)[0] or 'application/octet-stream' - return send_file( - local_file_path, - mimetype=content_type, - as_attachment=False, - download_name=filename - ) - except EntryNotFoundError: - logging.error(f"File not found on Hugging Face: {path_in_repo}") - return "File not found on storage.", 404 - except RepositoryNotFoundError: - logging.error(f"Repository not found: {REPO_ID}") - return "Storage repository not found.", 500 + 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') except Exception as e: - logging.error(f"Error downloading file {path_in_repo} for user {user_id}: {e}", exc_info=True) - return "Server error during download.", 500 + flash(f'Ошибка скачивания файла {original_filename}! ({e})', 'error') + return redirect(request.referrer or url_for('dashboard' if is_tg_user else 'admin_panel')) + + +@app.route('/delete_file/', methods=['POST']) +def delete_file(file_id): + if 'telegram_user_id' not in session and not is_admin(): + flash('Пожалуйста, войдите в систему!') + return redirect(url_for('launch_mini_app')) + + username = session['username'] + data = load_data() + user_data = data['users'].get(username) + if not user_data: session.clear(); return redirect(url_for('launch_mini_app')) + + file_node, parent_node = find_node_by_id(user_data['filesystem'], file_id) + current_view_folder_id = request.form.get('current_view_folder_id', 'root') + + if not file_node or file_node.get('type') != 'file' or not parent_node: + 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: + if remove_node(user_data['filesystem'], file_id): + try: save_data(data); flash(f'Метаданные файла {original_filename} удалены.') + except Exception as e: 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(); api.delete_file(path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE) + if remove_node(user_data['filesystem'], file_id): + try: save_data(data); flash(f'Файл {original_filename} успешно удален!') + except Exception as e: flash('Файл удален с сервера, но ошибка обновления БД.', 'error') + else: flash('Файл удален с сервера, но не найден в БД.', 'error') + except hf_utils.EntryNotFoundError: + if remove_node(user_data['filesystem'], file_id): + try: save_data(data); flash(f'Файл {original_filename} не найден на сервере, удален из базы.') + except Exception as e: flash('Ошибка сохранения (файл не найден на сервере).', 'error') + else: flash('Файл не найден ни на сервере, ни в базе данных.', 'error') + except Exception as 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 and not is_admin(): + flash('Пожалуйста, войдите в систему!') + return redirect(url_for('launch_mini_app')) + if folder_id == 'root': + flash('Нельзя удалить корневую папку!', 'error') + return redirect(url_for('dashboard')) + + username = session['username'] + data = load_data() + user_data = data['users'].get(username) + if not user_data: session.clear(); return redirect(url_for('launch_mini_app')) + + folder_node, parent_node = find_node_by_id(user_data['filesystem'], folder_id) + current_view_folder_id = request.form.get('current_view_folder_id', '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)) + + if remove_node(user_data['filesystem'], folder_id): + try: save_data(data); flash(f'Пустая папка "{folder_name}" успешно удалена.') + except Exception as e: flash('Ошибка сохранения данных после удаления папки.', 'error') + else: flash('Не удалось удалить папку из базы данных.', 'error') + return redirect(url_for('dashboard', folder_id=parent_node.get('id', 'root'))) -@app.route('/admin') + +@app.route('/get_text_content/') +def get_text_content(file_id): + is_tg_user = 'telegram_user_id' in session + is_form_admin = is_admin() + if not is_tg_user and not is_form_admin: return Response("Не авторизован", status=401) + + data = load_data() + file_node = None; username_context = session.get('username') + + if username_context: + user_data = data['users'].get(username_context) + if user_data: file_node, _ = find_node_by_id(user_data.get('filesystem',{}), file_id) + + if not file_node and is_form_admin: + for uname, udata in data.get('users', {}).items(): + 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; 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) + + 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.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 Exception as e: return Response(f"Ошибка загрузки: {e}", status=502) + +@app.route('/logout') +def logout(): + session.clear() + flash('Вы успешно вышли из системы.') + return redirect(url_for('launch_mini_app')) # Redirect to Mini App launcher + +# --- Admin Routes --- +@app.route('/god_mode_login', methods=['GET', 'POST']) # Admin login page +def god_mode_login(): + if request.method == 'POST': + username = request.form.get('username') + password = request.form.get('password') + if username == ADMIN_USER and password == ADMIN_PASSWORD: + session['admin_logged_in'] = True + session['username'] = username + session.permanent = True + flash('Admin login successful!') + return redirect(url_for('admin_panel')) + else: + flash('Invalid admin credentials.', 'error') + return render_template_string(ADMIN_LOGIN_HTML) + +@app.route('/admhosto') def admin_panel(): - current_data = load_local_metadata() - return render_template_string(ADMIN_TEMPLATE, users=current_data) + if not is_admin(): + flash('Доступ запрещен.', 'error') + return redirect(url_for('god_mode_login')) -@app.route('/admin/user/') -def admin_user_files(user_id): - current_data = load_local_metadata() - user_data = current_data.get(str(user_id)) + data = load_data() + users = data.get('users', {}) + user_details = [] + for uname, 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', [])) + user_details.append({'username': uname, 'created_at': udata.get('created_at', 'N/A'), 'file_count': file_count, 'tg_id': udata.get('telegram_id')}) + return render_template_string(ADMIN_PANEL_HTML, user_details=user_details) + +@app.route('/admhosto/user/') +def admin_user_files(username): + if not is_admin(): return redirect(url_for('god_mode_login')) + data = load_data() + user_data = data.get('users', {}).get(username) if not user_data: - return "User not found", 404 - user_info = user_data.get("user_info", {"id": user_id}) - files = user_data.get("files", []) - return render_template_string(ADMIN_USER_FILES_TEMPLATE, - user_id=user_id, - user_info=user_info, - files=files) - -def _admin_serve_file(user_id, filename, as_attachment): - user_id_str = str(user_id) - logging.info(f"Admin serving file '{filename}' for user {user_id_str}, as_attachment={as_attachment}") - api = get_hf_api(write=False) - if not api: - return "Server error: Cannot connect to storage.", 500 - path_in_repo = f"{HF_UPLOAD_FOLDER}/{user_id_str}/{filename}" - with _data_lock: - user_data = metadata_cache.get(user_id_str, {}) - user_files = user_data.get('files', []) - file_metadata = next((f for f in user_files if f['filename'] == filename), None) - if file_metadata and 'hf_path' in file_metadata: - path_in_repo = file_metadata['hf_path'] + flash(f'Пользователь {username} не найден.', 'error') + return redirect(url_for('admin_panel')) + + all_files = [] + def collect_files_recursive(folder_content, current_path_id='root'): + parent_path_str = get_node_path_string(user_data['filesystem'], current_path_id) + for item in folder_content.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_recursive(item, item.get('id')) + collect_files_recursive(user_data.get('filesystem', {})) + all_files.sort(key=lambda x: x.get('upload_date', ''), reverse=True) + return render_template_string(ADMIN_USER_FILES_HTML, username=username, 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(username): + if not is_admin(): return redirect(url_for('god_mode_login')) + if not HF_TOKEN_WRITE: + flash('Удаление невозможно: токен для записи не настроен.', 'error') + return redirect(url_for('admin_panel')) + + data = load_data() + if username not in data['users']: + flash('Пользователь не найден!', 'error'); return redirect(url_for('admin_panel')) try: - local_file_path = hf_hub_download( - repo_id=REPO_ID, - filename=path_in_repo, - repo_type="dataset", - token=api.token, - force_download=False, - etag_timeout=10 - ) - content_type = mimetypes.guess_type(filename)[0] or 'application/octet-stream' - if file_metadata and 'content_type' in file_metadata: - content_type = file_metadata['content_type'] or content_type - return send_file( - local_file_path, - mimetype=content_type, - as_attachment=as_attachment, - download_name=filename if as_attachment else None - ) - except EntryNotFoundError: - logging.error(f"Admin serving: File not found on Hugging Face: {path_in_repo}") - return "File not found on storage.", 404 - except Exception as e: - logging.error(f"Admin serving: Error for file {path_in_repo}: {e}", exc_info=True) - return "Server error during file serving.", 500 + api = HfApi(); user_folder_path_on_hf = f"cloud_files/{username}" + api.delete_folder(folder_path=user_folder_path_on_hf, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE) + except hf_utils.HfHubHTTPError as e: + if e.response.status_code != 404: + flash(f'Ошибка удаления файлов {username} с сервера: {e}.', 'error'); return redirect(url_for('admin_panel')) + except Exception as e: flash(f'Ошибка удаления файлов {username} с сервера: {e}.', 'error'); return redirect(url_for('admin_panel')) + try: + del data['users'][username]; save_data(data) + flash(f'Пользователь {username} и его файлы удалены!') + except Exception as e: flash(f'Ошибка удаления пользователя {username} из БД: {e}', 'error') + return redirect(url_for('admin_panel')) -@app.route('/admin/download//', methods=['GET']) -def admin_download_file(user_id, filename): - return _admin_serve_file(user_id, filename, as_attachment=True) +@app.route('/admhosto/delete_file//', methods=['POST']) +def admin_delete_file(username, file_id): + if not is_admin(): return redirect(url_for('god_mode_login')) + if not HF_TOKEN_WRITE: + flash('Удаление невозможно: токен для записи не настроен.', 'error') + return redirect(url_for('admin_user_files', username=username)) -@app.route('/admin/view//', methods=['GET']) -def admin_view_file(user_id, filename): - return _admin_serve_file(user_id, filename, as_attachment=False) + data = load_data() + user_data = data.get('users', {}).get(username) + if not user_data: flash(f'Пользователь {username} не найден.', 'error'); return redirect(url_for('admin_panel')) + file_node, _ = find_node_by_id(user_data['filesystem'], file_id) + if not file_node or file_node.get('type') != 'file': + flash('Файл не найден.', 'error'); return redirect(url_for('admin_user_files', username=username)) + + hf_path = file_node.get('path'); original_filename = file_node.get('original_filename', 'файл') + if not hf_path: + if remove_node(user_data['filesystem'], file_id): + try: save_data(data); flash(f'Метаданные {original_filename} удалены (путь отсутствовал).') + except Exception as e: flash('Ошибка сохранения (путь отсутствовал).', 'error') + return redirect(url_for('admin_user_files', username=username)) + try: + api = HfApi(); api.delete_file(path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE) + if remove_node(user_data['filesystem'], file_id): + try: save_data(data); flash(f'Файл {original_filename} удален!') + except Exception as e: flash('Файл удален с сервера, ошибка обновления БД.', 'error') + else: flash('Файл удален с сервера, но не найден в БД.', 'error') + except hf_utils.EntryNotFoundError: + if remove_node(user_data['filesystem'], file_id): + try: save_data(data); flash(f'Файл {original_filename} не найден на сервере, удален из базы.') + except Exception as e: flash('Ошибка сохранения (файл не найден на сервере).', 'error') + else: flash('Файл не найден ни на сервере, ни в БД.', 'error') + except Exception as e: flash(f'Ошибка удаления файла {original_filename}: {e}', 'error') + return redirect(url_for('admin_user_files', username=username)) -@app.route('/admin/download_metadata', methods=['POST']) -def admin_trigger_download_metadata(): - success = download_metadata_from_hf() - if success: - return jsonify({"status": "ok", "message": "Скачивание data.json с Hugging Face завершено. Обновите страницу."}) - else: - return jsonify({"status": "error", "message": "Ошибка скачивания data.json. Проверьте логи."}), 500 +# --- Main Execution --- if __name__ == '__main__': - print("---") - print("--- ZEUS CLOUD MINI APP SERVER ---") - print("---") - print(f"Starting Flask server on http://{HOST}:{PORT}") - print(f"Using Bot Token ID: {BOT_TOKEN.split(':')[0]}") - print(f"Metadata file (local): {DATA_FILE}") - print(f"Hugging Face Repo: {REPO_ID}") - print(f"HF Metadata Path: {HF_DATA_FILE_PATH}") - print(f"HF Upload Folder: {HF_UPLOAD_FOLDER}//") - if not HF_TOKEN_READ or not HF_TOKEN_WRITE: - print("---") - print("--- WARNING: HUGGING FACE TOKEN(S) NOT SET ---") - print("--- Storage functionality requires HF_TOKEN_READ and HF_TOKEN_WRITE env vars.") - print("---") + if not HF_TOKEN_WRITE: logging.warning("HF_TOKEN_WRITE not set. Uploads/deletions/backups will fail.") + if not HF_TOKEN_READ: logging.warning("HF_TOKEN_READ not set. Using HF_TOKEN_WRITE. Downloads might fail for private repos if HF_TOKEN_WRITE is also not set.") + + if not BOT_TOKEN: logging.critical("BOT_TOKEN is not set. Telegram Mini App functionality will not work.") + if WEB_APP_URL == "https://your-flask-app-domain.com/launch_mini_app": + logging.warning("WEB_APP_URL is default. Update it to your actual ngrok or deployed URL for the Mini App to work.") + + if HF_TOKEN_WRITE: + download_db_from_hf() + threading.Thread(target=periodic_backup, daemon=True).start() + elif HF_TOKEN_READ: + download_db_from_hf() else: - print("--- Hugging Face tokens found.") - print("--- Attempting initial metadata download from Hugging Face...") - download_metadata_from_hf() - load_local_metadata() - print(f"--- Initial metadata cache loaded with {len(metadata_cache)} user(s).") - print("---") - print("--- SECURITY WARNING ---") - print("--- The /admin routes are NOT protected by authentication.") - print("--- Implement proper auth before any production deployment.") - print("---") - print("--- Server Ready ---") - app.run(host=HOST, port=PORT, debug=False, threaded=True) \ No newline at end of file + if not os.path.exists(DATA_FILE): + with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}}, f) + + def run_bot(): + logging.info("Starting Telegram bot polling...") + bot_app.run_polling(allowed_updates=Update.ALL_TYPES) + + if BOT_TOKEN: + threading.Thread(target=run_bot, daemon=True).start() + + logging.info(f"Flask app starting. Mini App URL (set in bot): {WEB_APP_URL}") + logging.info(f"Admin login at: /god_mode_login") + app.run(debug=False, host='0.0.0.0', port=7860) \ No newline at end of file