diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -12,33 +12,48 @@ from werkzeug.utils import secure_filename import requests from io import BytesIO import uuid -import hashlib import hmac +import hashlib -from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, WebAppInfo -from telegram.ext import Application as TelegramApplication, CommandHandler, ContextTypes app = Flask(__name__) -app.secret_key = os.getenv("FLASK_SECRET_KEY", "supersecretkey_folders_unique_telegram_mini_app") - +app.secret_key = os.getenv("FLASK_SECRET_KEY", "supersecretkey_folders_unique_telegram") 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 +REPO_ID = os.getenv("HF_REPO_ID", "Eluza133/Z1e1u") # Ensure this is set in your environment +HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE") # Ensure this is set +HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") or HF_TOKEN_WRITE # Ensure this is set +ADMIN_TELEGRAM_IDS = os.getenv("ADMIN_TELEGRAM_IDS", "").split(',') # Comma-separated string of admin Telegram IDs +BOT_TOKEN = "6750208873:AAE2hvPlJ99dBdhGa_Brre0IIpUdOvXxHt4" + UPLOAD_FOLDER = 'uploads_tg' os.makedirs(UPLOAD_FOLDER, exist_ok=True) -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 verify_telegram_auth(init_data_str, bot_token): + try: + params = dict(item.split("=", 1) for item in init_data_str.split("&")) + hash_received = params.pop("hash", None) + if not hash_received: + return None + + data_check_arr = [] + for key, value in sorted(params.items()): + data_check_arr.append(f"{key}={value}") + data_check_string = "\n".join(data_check_arr) + + secret_key = hmac.new("WebAppData".encode(), bot_token.encode(), hashlib.sha256).digest() + calculated_hash = hmac.new(secret_key, data_check_string.encode(), hashlib.sha256).hexdigest() + + if calculated_hash == hash_received: + user_data = json.loads(params.get("user", "{}")) + return user_data + return None + except Exception as e: + logging.error(f"Telegram auth verification error: {e}") + return None + def find_node_by_id(filesystem, node_id): if not filesystem: return None, None if filesystem.get('id') == node_id: @@ -82,35 +97,19 @@ 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 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": [] +def initialize_user_filesystem_if_needed(user_data, user_id_for_path_unused): + if 'filesystem' not in user_data or not isinstance(user_data['filesystem'], dict): + user_data['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 --- + # Removed old 'files' list migration logic as it's complex with TG ID change + # and relied on session['username'] which is not available globally in load_data. + # New users will get the basic structure. Existing data might need manual migration if format is very old. + + @cache.memoize(timeout=300) def load_data(): try: @@ -118,9 +117,12 @@ def load_data(): with open(DATA_FILE, 'r', encoding='utf-8') as file: data = json.load(file) if not isinstance(data, dict): + logging.warning("Data is not in dict format, initializing empty database") return {'users': {}} data.setdefault('users', {}) - # Filesystem initialization handled on login/auth if needed + for user_id_str, user_data_item in data['users'].items(): + initialize_user_filesystem_if_needed(user_data_item, user_id_str) + logging.info("Data successfully loaded and initialized for TG app") return data except Exception as e: logging.error(f"Error loading data: {e}") @@ -132,77 +134,60 @@ def save_data(data): json.dump(data, file, ensure_ascii=False, indent=4) upload_db_to_hf() cache.clear() + logging.info("Data saved and uploaded to HF for TG app") except Exception as e: logging.error(f"Error saving data: {e}") raise def upload_db_to_hf(): - if not HF_TOKEN_WRITE: 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"TGApp DB Backup {datetime.now()}") + api.upload_file( + path_or_fileobj=DATA_FILE, + path_in_repo=DATA_FILE, + repo_id=REPO_ID, + repo_type="dataset", + token=HF_TOKEN_WRITE, + commit_message=f"Backup TG App {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + ) + logging.info("Database uploaded to Hugging Face (TG App)") except Exception as e: - logging.error(f"Error uploading database: {e}") + logging.error(f"Error uploading database (TG App): {e}") def download_db_from_hf(): if not HF_TOKEN_READ: + logging.warning("HF_TOKEN_READ not set, skipping database download.") if not os.path.exists(DATA_FILE): with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}}, f) return try: - hf_hub_download(repo_id=REPO_ID, filename=DATA_FILE, repo_type="dataset", token=HF_TOKEN_READ, local_dir=".", local_dir_use_symlinks=False) - 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) + 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 App)") except Exception as e: - logging.error(f"Error downloading database: {e}") + logging.error(f"Error downloading database (TG App): {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) + time.sleep(1800) # Backup every 30 minutes upload_db_to_hf() + 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' + 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' -# --- 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: - 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 == 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 validating Telegram initData: {e}, Data: {init_data_str[:200]}") - return None - -# --- CSS --- BASE_STYLE = ''' :root { --primary: #ff4d6d; --secondary: #00ddeb; --accent: #8b5cf6; @@ -293,368 +278,185 @@ body.dark .modal-close-btn { color: #555; background: rgba(255,255,255,0.2); } } ''' -# --- HTML Templates --- -LAUNCH_MINI_APP_HTML = ''' - -Zeus Cloud Mini App - - -

Zeus Cloud

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

- -''' -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 %} -
-
-
''' - -DASHBOARD_HTML = ''' - -Панель управления - Zeus Cloud - - -
-

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 %} -
-Выйти -
- - -''' +''' -ADMIN_PANEL_HTML = ''' +@app.route('/') +def index_page(): + html = f''' -Админ-панель -

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

-{% 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 }}

-
- -
+Zeus Cloud - Telegram App + + +

Zeus Cloud

+

Это приложение предназначено для использования внутри Telegram.

+

Пожалуйста, откройте его через вашего Telegram бота.

+
Пытаемся авторизоваться...
-{% else %}

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

{% endfor %}
-Выйти из админки -
''' - -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 %}
- ''' - -# --- 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) - -bot_app = TelegramApplication.builder().token(BOT_TOKEN).build() -bot_app.add_handler(CommandHandler("start", start_command)) - -# --- Flask Routes --- -@app.route('/') -def index(): - 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 - - + window.Telegram.WebApp.ready(); + const initData = window.Telegram.WebApp.initData; + if (initData) {{ + fetch('/perform_telegram_auth', {{ + method: 'POST', + headers: {{ 'Content-Type': 'application/json' }}, + body: JSON.stringify({{ init_data: initData }}) + }}) + .then(response => response.json()) + .then(data => {{ + if (data.status === 'success') {{ + document.getElementById('auth-status').innerText = 'Авторизация успешна! Перенаправление...'; + window.location.href = '{url_for('dashboard')}'; + }} else {{ + document.getElementById('auth-status').innerText = 'Ошибка авторизации: ' + data.message; + window.Telegram.WebApp.showAlert('Ошибка авторизации: ' + data.message); + }} + }}) + .catch(error => {{ + document.getElementById('auth-status').innerText = 'Ошибка сети при авторизации: ' + error; + window.Telegram.WebApp.showAlert('Ошибка сети при авторизации: ' + error); + }}); + }} else {{ + // Check if already logged in via Flask session (e.g. navigated back) + fetch('/check_auth_status') + .then(response => response.json()) + .then(data => {{ + if (data.authenticated) {{ + window.location.href = "{url_for('dashboard')}"; + }} else {{ + document.getElementById('auth-status').innerText = 'Telegram initData не найдено. Откройте через бота.'; + if(window.Telegram.WebApp.platform === "unknown"){{ + // Not in Telegram environment + }} else {{ + window.Telegram.WebApp.showAlert('Пожалуйста, перезапустите приложение из Telegram.'); + }} + }} + }}); + }} + +''' + return render_template_string(html) + +@app.route('/check_auth_status') +def check_auth_status(): + if 'telegram_user_id' in session: + return jsonify({'authenticated': True}) + return jsonify({'authenticated': False}) + + +@app.route('/perform_telegram_auth', methods=['POST']) +def perform_telegram_auth(): + auth_data = request.json + init_data_str = auth_data.get('init_data') + + if not init_data_str: + return jsonify({'status': 'error', 'message': 'Отсутствуют данные для авторизации (initData).'}), 400 + + tg_user = verify_telegram_auth(init_data_str, BOT_TOKEN) + + if tg_user and 'id' in tg_user: + tg_user_id = str(tg_user['id']) + tg_username = tg_user.get('username', f"user{tg_user_id}") + tg_first_name = tg_user.get('first_name', '') + tg_last_name = tg_user.get('last_name', '') + + data = load_data() + if tg_user_id not in data['users']: + data['users'][tg_user_id] = { + 'telegram_username': tg_username, + 'telegram_first_name': tg_first_name, + 'telegram_last_name': tg_last_name, + 'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + 'filesystem': {"type": "folder", "id": "root", "name": "root", "children": []} + } 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 + logging.error(f"Error saving new TG user data {tg_user_id}: {e}") + return jsonify({'status': 'error', 'message': 'Ошибка сохранения данных нового пользователя.'}), 500 + + session['telegram_user_id'] = tg_user_id + session['telegram_username'] = tg_username + session['telegram_first_name'] = tg_first_name + session['telegram_last_name'] = tg_last_name + + logging.info(f"User {tg_username} (ID: {tg_user_id}) authenticated successfully.") + return jsonify({'status': 'success', 'message': 'Авторизация успешна.'}) + else: + logging.warning(f"Telegram authentication failed. Invalid initData or verification issue.") + return jsonify({'status': 'error', 'message': 'Неверные данные авторизации Telegram.'}), 403 + @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')) + if 'telegram_user_id' not in session: + flash('Пожалуйста, авторизуйтесь через Telegram!') + return redirect(url_for('index_page')) + current_tg_user_id = session['telegram_user_id'] + current_tg_username = session['telegram_username'] + data = load_data() - if username not in data['users']: + if current_tg_user_id not in data['users']: session.clear() - flash('Пользователь не найден!') - return redirect(url_for('launch_mini_app')) + flash('Пользователь не найден! Пожалуйста, перезайдите.') + return redirect(url_for('index_page')) - user_data = data['users'][username] - if 'filesystem' not in user_data: - initialize_user_filesystem(user_data) # Pass user_data itself + user_data = data['users'][current_tg_user_id] + initialize_user_filesystem_if_needed(user_data, current_tg_user_id) # Ensure filesystem key exists current_folder_id = request.args.get('folder_id', 'root') current_folder, parent_folder = find_node_by_id(user_data['filesystem'], current_folder_id) @@ -664,10 +466,11 @@ def dashboard(): current_folder_id = 'root' current_folder, parent_folder = find_node_by_id(user_data['filesystem'], current_folder_id) if not current_folder: + logging.error(f"CRITICAL: Root folder not found for user {current_tg_user_id}") flash('Критическая ошибка: корневая папка не найдена.', 'error') session.clear() - return redirect(url_for('launch_mini_app')) - + return redirect(url_for('index_page')) + 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': @@ -679,42 +482,64 @@ 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: 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) + uploaded_count = 0 + errors = [] + + for file_item in files: + if file_item and file_item.filename: + original_filename = secure_filename(file_item.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}" + + hf_path = f"cloud_files/{current_tg_user_id}/{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 + file_item.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 {current_tg_username} (ID: {current_tg_user_id}) uploaded {original_filename} to folder {target_folder_id}" + ) + file_info = { + 'type': 'file', 'id': file_id, 'original_filename': original_filename, + 'unique_filename': unique_filename, 'path': hf_path, 'file_type': get_file_type(original_filename), + 'upload_date': datetime.now().strftime('%Y-%m-%d %H:%M:%S') + } + if add_node(user_data['filesystem'], target_folder_id, file_info): + uploaded_count += 1 else: errors.append(f"Ошибка добавления метаданных для {original_filename}.") + logging.error(f"Failed to add node metadata for file {file_id} for user {current_tg_user_id}") try: api.delete_file(path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE) except Exception as del_err: logging.error(f"Failed to delete orphaned file {hf_path} from HF Hub: {del_err}") - except Exception as e: errors.append(f"Ошибка загрузки файла {original_filename}: {e}") + except Exception as e: + logging.error(f"Error uploading file {original_filename} for {current_tg_user_id}: {e}") + errors.append(f"Ошибка загрузки файла {original_filename}: {e}") finally: if os.path.exists(temp_path): os.remove(temp_path) + if uploaded_count > 0: - try: save_data(data); flash(f'{uploaded_count} файл(ов) успешно загружено!') - except Exception as e: flash('Файлы загружены на сервер, но произошла ошибка сохранения метаданных.', 'error') + try: + save_data(data) + flash(f'{uploaded_count} файл(ов) успешно загружено!') + except Exception as e: + flash('Файлы загружены, но ошибка сохранения метаданных.', 'error') + logging.error(f"Error saving data after upload for {current_tg_user_id}: {e}") if errors: for error_msg in errors: flash(error_msg, 'error') return redirect(url_for('dashboard', folder_id=target_folder_id)) @@ -730,16 +555,201 @@ def dashboard(): 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) + html = f''' + +Панель управления - Zeus Cloud + +
+

Zeus Cloud

Пользователь: {current_tg_username} (ID: {current_tg_user_id})

+{{% 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 %}} +
+ +Выйти +
+ + + + +''' + template_context = { + 'current_tg_user_id': current_tg_user_id, + 'current_tg_username': current_tg_username, + 'items': items_in_folder, + 'current_folder_id': current_folder_id, + 'current_folder': current_folder, + 'breadcrumbs': breadcrumbs, + 'hf_file_url_jinja': lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{path}{'?download=true' if download else ''}", + 'os': os + } + return render_template_string(html, **template_context) @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'] + if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Не авторизован'}), 401 + current_tg_user_id = session['telegram_user_id'] data = load_data() - user_data = data['users'].get(username) + user_data = data['users'].get(current_tg_user_id) if not user_data: return jsonify({'status': 'error', 'message': 'Пользователь не найден'}), 404 parent_folder_id = request.form.get('parent_folder_id', 'root') @@ -748,78 +758,98 @@ def create_folder(): if not folder_name: flash('Имя папки не может быть пустым!', 'error') return redirect(url_for('dashboard', folder_id=parent_folder_id)) - - # Allow broader range of characters for folder names + # Allow more characters in 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') + try: + save_data(data) + flash(f'Папка "{folder_name}" успешно создана.') + except Exception as e: + flash('Ошибка сохранения данных при создании папки.', 'error') + logging.error(f"Create folder save error for {current_tg_user_id}: {e}") + else: + flash('Не удалось найти родительскую папку.', 'error') return redirect(url_for('dashboard', folder_id=parent_folder_id)) +def is_current_user_admin(): + return 'telegram_user_id' in session and str(session['telegram_user_id']) in ADMIN_TELEGRAM_IDS + @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')) - + user_is_admin_for_this_op = False + if 'telegram_user_id' not in session: + if not is_current_user_admin(): # Admin check even if no session, but this path is unlikely + flash('Пожалуйста, авторизуйтесь через Telegram!') + return redirect(url_for('index_page')) + user_is_admin_for_this_op = True # Admin trying to access without being a regular user + + current_tg_user_id = session.get('telegram_user_id') 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) + file_node = None + owner_tg_user_id = None + + if not user_is_admin_for_this_op and current_tg_user_id: + user_data = data['users'].get(current_tg_user_id) + if user_data: + file_node, _ = find_node_by_id(user_data['filesystem'], file_id) + if file_node: owner_tg_user_id = current_tg_user_id - if not file_node and is_form_admin: # Admin cross-user search - for uname, udata in data.get('users', {}).items(): + if not file_node and is_current_user_admin(): # If current user is admin and didn't find in own files (or isn't owner) + user_is_admin_for_this_op = True # Confirm admin powers are invoked + logging.info(f"Admin {session.get('telegram_user_id')} searching for file ID {file_id} across all users.") + for uid, 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 + file_node = node + owner_tg_user_id = uid + logging.info(f"Admin found file ID {file_id} belonging to user {owner_tg_user_id}") + break if not file_node or file_node.get('type') != 'file': flash('Файл не найден!', 'error') - return redirect(request.referrer or url_for('dashboard' if is_tg_user else 'admin_panel')) + return redirect(request.referrer or url_for('dashboard' if current_tg_user_id else 'index_page')) 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')) + flash('Ошибка: Путь к файлу не найден в метаданных.', 'error') + return redirect(request.referrer or url_for('dashboard' if current_tg_user_id else 'index_page')) 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') except Exception as e: + logging.error(f"Error downloading file from HF ({hf_path}): {e}") flash(f'Ошибка скачивания файла {original_filename}! ({e})', 'error') - return redirect(request.referrer or url_for('dashboard' if is_tg_user else 'admin_panel')) - + return redirect(request.referrer or url_for('dashboard' if current_tg_user_id else 'index_page')) @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')) + if 'telegram_user_id' not in session: + flash('Пожалуйста, авторизуйтесь!') + return redirect(url_for('index_page')) - username = session['username'] + current_tg_user_id = session['telegram_user_id'] + current_tg_username = session['telegram_username'] data = load_data() - user_data = data['users'].get(username) - if not user_data: session.clear(); return redirect(url_for('launch_mini_app')) + user_data = data['users'].get(current_tg_user_id) + if not user_data: + session.clear() + flash('Пользователь не найден!') + return redirect(url_for('index_page')) 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') @@ -827,44 +857,51 @@ def delete_file(file_id): 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', 'файл') - 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(data); flash(f'Метаданные файла {original_filename} удалены.') - except Exception as e: flash('Ошибка сохранения данных после удаления метаданных.', 'error') + 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) + 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 {current_tg_username} (ID: {current_tg_user_id}) deleted file {original_filename} (ID: {file_id})") + logging.info(f"Deleted file {hf_path} from HF Hub for user {current_tg_user_id}") if remove_node(user_data['filesystem'], file_id): try: save_data(data); flash(f'Файл {original_filename} успешно удален!') - except Exception as e: flash('Файл удален с сервера, но ошибка обновления БД.', 'error') + 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 {current_tg_user_id}. Removing from DB.") if remove_node(user_data['filesystem'], file_id): try: save_data(data); flash(f'Файл {original_filename} не найден на сервере, удален из базы.') - except Exception as e: flash('Ошибка сохранения (файл не найден на сервере).', 'error') + 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: flash(f'Ошибка удаления файла {original_filename}: {e}', 'error') + except Exception as e: + logging.error(f"Error deleting file {hf_path} for {current_tg_user_id}: {e}") + flash(f'Ошибка удаления файла {original_filename}: {e}', 'error') return redirect(url_for('dashboard', folder_id=current_view_folder_id)) + @app.route('/delete_folder/', methods=['POST']) def delete_folder(folder_id): - if 'telegram_user_id' not in session 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'] + if 'telegram_user_id' not in session: flash('Пожалуйста, авторизуйтесь!'); return redirect(url_for('index_page')) + if folder_id == 'root': flash('Нельзя удалить корневую папку!', 'error'); return redirect(url_for('dashboard')) + + current_tg_user_id = session['telegram_user_id'] data = load_data() - user_data = data['users'].get(username) - if not user_data: session.clear(); return redirect(url_for('launch_mini_app')) + user_data = data['users'].get(current_tg_user_id) + if not user_data: session.clear(); flash('Пользователь не найден!'); return redirect(url_for('index_page')) folder_node, parent_node = find_node_by_id(user_data['filesystem'], folder_id) current_view_folder_id = request.form.get('current_view_folder_id', 'root') @@ -880,192 +917,304 @@ def delete_folder(folder_id): if remove_node(user_data['filesystem'], folder_id): try: save_data(data); flash(f'Пустая папка "{folder_name}" успешно удалена.') - except Exception as e: flash('Ошибка сохранения данных после удаления папки.', 'error') + except Exception as e: flash('Ошибка сохранения данных после удаления папки.', 'error'); logging.error(f"Delete empty folder save error: {e}") else: flash('Не удалось удалить папку из базы данных.', 'error') - return redirect(url_for('dashboard', folder_id=parent_node.get('id', 'root'))) - + + redirect_to_folder_id = parent_node.get('id', 'root') + return redirect(url_for('dashboard', folder_id=redirect_to_folder_id)) @app.route('/get_text_content/') def get_text_content(file_id): - is_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) - + user_is_admin_for_this_op = False + if 'telegram_user_id' not in session: + if not is_current_user_admin(): return Response("Не авторизован", status=401) + user_is_admin_for_this_op = True + + current_tg_user_id = session.get('telegram_user_id') data = load_data() - file_node = None; username_context = session.get('username') + file_node = None + owner_tg_user_id = None - 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 user_is_admin_for_this_op and current_tg_user_id: + user_data = data['users'].get(current_tg_user_id) + if user_data: + node, _ = find_node_by_id(user_data['filesystem'], file_id) + if node and node.get('type') == 'file' and node.get('file_type') == 'text': + file_node = node + owner_tg_user_id = current_tg_user_id - if not file_node and is_form_admin: - for uname, udata in data.get('users', {}).items(): + if not file_node and is_current_user_admin(): + user_is_admin_for_this_op = True + for uid, 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) + file_node = node + owner_tg_user_id = uid + break + if 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.raise_for_status() - if len(response.content) > 1 * 1024 * 1024: return Response("Файл слишком большой", status=413) + 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') + except UnicodeDecodeError: text_content = response.content.decode('latin-1', errors='ignore') return Response(text_content, mimetype='text/plain') - except Exception as e: return Response(f"Ошибка загрузки: {e}", status=502) + except Exception as e: + logging.error(f"Error fetching text content from HF ({hf_path}): {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) + return redirect(url_for('index_page')) @app.route('/admhosto') def admin_panel(): - if not is_admin(): + if not is_current_user_admin(): flash('Доступ запрещен.', 'error') - return redirect(url_for('god_mode_login')) + return redirect(url_for('index_page')) data = load_data() users = data.get('users', {}) user_details = [] - for uname, udata in users.items(): + for tg_id_str, udata in users.items(): file_count = 0 - q = [udata.get('filesystem', {}).get('children', [])] + q = [(udata.get('filesystem', {}))] while q: - current_level = q.pop(0) - for item in current_level: + current_folder_node = q.pop(0) + if not current_folder_node: continue + for item in current_folder_node.get('children', []): 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) + elif item.get('type') == 'folder': q.append(item) + user_details.append({ + 'telegram_id_str': tg_id_str, + 'display_name': udata.get('telegram_username', f"user_{tg_id_str}"), + 'first_name': udata.get('telegram_first_name', ''), + 'last_name': udata.get('telegram_last_name', ''), + 'created_at': udata.get('created_at', 'N/A'), + 'file_count': file_count + }) + + html = f''' + +Админ-панель + +

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

+Вернуться в приложение +{{% 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.first_name}}}} {{{{user.last_name}}}}) - ID: {{{{user.telegram_id_str}}}} +

Зарегистрирован: {{{{ 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(username): - if not is_admin(): return redirect(url_for('god_mode_login')) + +@app.route('/admhosto/user/') +def admin_user_files(telegram_id_str): + if not is_current_user_admin(): flash('Доступ запрещен.', 'error'); return redirect(url_for('index_page')) data = load_data() - user_data = data.get('users', {}).get(username) - if not user_data: - flash(f'Пользователь {username} не найден.', 'error') - return redirect(url_for('admin_panel')) + user_data = data.get('users', {}).get(telegram_id_str) + if not user_data: flash(f'Пользователь ID {telegram_id_str} не найден.', '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')) + def collect_files_recursive(folder_node, current_path_id='root'): + parent_path_str = get_node_path_string(user_data['filesystem'], current_path_id) + for item in folder_node.get('children', []): + if item.get('type') == 'file': + item_copy = item.copy() + item_copy['parent_path_str'] = parent_path_str + all_files.append(item_copy) + 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 ''}") + + display_username = user_data.get('telegram_username', f"user_{telegram_id_str}") -@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')) + html = f''' +Файлы {display_username} + + +

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

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

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

+

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

+

Загружен: {{{{ 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, current_telegram_id_str_for_route=telegram_id_str, display_username=display_username, files=all_files, + hf_file_url_jinja=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_id_str): + if not is_current_user_admin(): flash('Доступ запрещен.', 'error'); return redirect(url_for('index_page')) + if not HF_TOKEN_WRITE: flash('Удаление невозможно: токен для записи не настроен.', 'error'); return redirect(url_for('admin_panel')) data = load_data() - if username not in data['users']: - flash('Пользователь не найден!', 'error'); return redirect(url_for('admin_panel')) + if telegram_id_str not in data['users']: flash('Пользователь не найден!', 'error'); return redirect(url_for('admin_panel')) + + user_to_delete_data = data['users'][telegram_id_str] + user_to_delete_username = user_to_delete_data.get('telegram_username', f"user_{telegram_id_str}") + logging.warning(f"ADMIN ACTION: Attempting to delete user {user_to_delete_username} (ID: {telegram_id_str}) and all their data.") + try: - 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) + api = HfApi() + user_folder_path_on_hf = f"cloud_files/{telegram_id_str}" + logging.info(f"Attempting to delete HF Hub folder: {user_folder_path_on_hf} for user {telegram_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 {user_to_delete_username} (ID: {telegram_id_str})") + logging.info(f"Successfully initiated deletion of folder {user_folder_path_on_hf} on HF Hub.") except hf_utils.HfHubHTTPError as e: - if e.response.status_code != 404: - 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')) + if e.response.status_code == 404: logging.warning(f"User folder {user_folder_path_on_hf} not found on HF Hub. Skipping HF deletion.") + else: + logging.error(f"Error deleting user folder {user_folder_path_on_hf} from HF Hub: {e}") + flash(f'Ошибка при удалении файлов пользователя {user_to_delete_username} с сервера: {e}. Пользователь НЕ удален из базы.', 'error') + return redirect(url_for('admin_panel')) + except Exception as e: + logging.error(f"Unexpected error during HF Hub folder deletion for {user_to_delete_username}: {e}") + flash(f'Неожиданная ошибка при удалении файлов {user_to_delete_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') + del data['users'][telegram_id_str] + save_data(data) + flash(f'Пользователь {user_to_delete_username} (ID: {telegram_id_str}) и его файлы (запрос на удаление отправлен) успешно удалены из базы данных!') + logging.info(f"ADMIN ACTION: Successfully deleted user {user_to_delete_username} (ID: {telegram_id_str}) from database.") + except Exception as e: + logging.error(f"Error saving data after deleting user {user_to_delete_username}: {e}") + flash(f'Файлы пользователя {user_to_delete_username} удалены с сервера, но ошибка при удалении из базы: {e}', 'error') return redirect(url_for('admin_panel')) -@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('/admhosto/delete_file//', methods=['POST']) +def admin_delete_file(telegram_id_str, file_id): + if not is_current_user_admin(): flash('Доступ запрещен.', 'error'); return redirect(url_for('index_page')) + if not HF_TOKEN_WRITE: flash('Удаление невозможно: токен для записи не настроен.', 'error'); return redirect(url_for('admin_user_files', telegram_id_str=telegram_id_str)) data = load_data() - user_data = data.get('users', {}).get(username) - if not user_data: flash(f'Пользователь {username} не найден.', 'error'); return redirect(url_for('admin_panel')) + user_data = data.get('users', {}).get(telegram_id_str) + if not user_data: flash(f'Пользователь ID {telegram_id_str} не найден.', 'error'); return redirect(url_for('admin_panel')) + + user_display_name = user_data.get('telegram_username', f"user_{telegram_id_str}") + file_node, parent_node = find_node_by_id(user_data['filesystem'], file_id) - 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)) + if not file_node or file_node.get('type') != 'file' or not parent_node: + flash('Файл не найден в структуре пользователя.', 'error') + return redirect(url_for('admin_user_files', telegram_id_str=telegram_id_str)) + + hf_path = file_node.get('path') + original_filename = file_node.get('original_filename', 'файл') - 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(data); flash(f'Метаданные {original_filename} удалены (путь отсутствовал).') - except Exception as e: flash('Ошибка сохранения (путь отсутствовал).', 'error') - return redirect(url_for('admin_user_files', username=username)) + try: save_data(data); flash(f'Метаданные файла {original_filename} удалены (путь отсутствовал).') + except Exception as e: flash('Ошибка БД (путь отсутствовал).', 'error'); logging.error(f"Admin delete file metadata save error (no path): {e}") + return redirect(url_for('admin_user_files', telegram_id_str=telegram_id_str)) + try: - api = HfApi(); api.delete_file(path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE) + 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} (ID: {file_id}) for user {user_display_name} (ID: {telegram_id_str})") + logging.info(f"ADMIN ACTION: Deleted file {hf_path} from HF Hub for user {telegram_id_str}") 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') + try: save_data(data); flash(f'Файл {original_filename} успешно удален!') + except Exception as e: flash('Файл удален с сервера, но ошибка обновления БД.', 'error'); logging.error(f"Admin delete file DB update error: {e}") + else: flash('Файл удален с сервера, но не найден в БД для удаления метаданных.', 'error') except hf_utils.EntryNotFoundError: + logging.warning(f"ADMIN ACTION: File {hf_path} not found on HF Hub for user {telegram_id_str}. Removing from DB.") 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)) + 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_id_str}: {e}") + flash(f'Ошибка удаления файла {original_filename}: {e}', 'error') + return redirect(url_for('admin_user_files', telegram_id_str=telegram_id_str)) -# --- Main Execution --- if __name__ == '__main__': - 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 REPO_ID: logging.critical("HF_REPO_ID is not set. Application cannot function properly."); exit(1) + if not HF_TOKEN_WRITE: logging.warning("HF_TOKEN_WRITE is not set. File uploads, deletions, and backups will fail.") + if not HF_TOKEN_READ: logging.warning("HF_TOKEN_READ is not set (or HF_TOKEN_WRITE as fallback). File downloads/previews might fail for private repos.") + if not BOT_TOKEN: logging.critical("BOT_TOKEN is not set. Telegram authentication will fail."); exit(1) - 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 not ADMIN_TELEGRAM_IDS or ADMIN_TELEGRAM_IDS == ['']: + logging.warning("ADMIN_TELEGRAM_IDS is not set. Admin panel will be inaccessible.") + else: + logging.info(f"Admin Telegram IDs configured: {ADMIN_TELEGRAM_IDS}") if HF_TOKEN_WRITE: - download_db_from_hf() + logging.info("Performing initial database download before starting background backup.") + download_db_from_hf() # Download once at start if possible threading.Thread(target=periodic_backup, daemon=True).start() - elif HF_TOKEN_READ: - download_db_from_hf() + logging.info("Periodic backup thread started.") else: - 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.warning("Periodic backup disabled because HF_TOKEN_WRITE is not set.") + if HF_TOKEN_READ: + logging.info("Performing initial database download (read-only mode).") + download_db_from_hf() + else: + logging.warning("No read or write token. Database operations with Hugging Face Hub are disabled.") + 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}") - 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 + app.run(debug=False, host='0.0.0.0', port=int(os.getenv("PORT", 7860))) \ No newline at end of file