diff --git "a/app (50).py" "b/app (50).py"
new file mode 100644--- /dev/null
+++ "b/app (50).py"
@@ -0,0 +1,1713 @@
+import flask
+from flask import Flask, render_template_string, request, redirect, url_for, session, flash, send_file, jsonify, Response, stream_with_context
+from flask_caching import Cache
+import json
+import os
+import logging
+import threading
+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
+from functools import wraps
+from urllib.parse import quote
+import zipfile
+import tempfile
+import pytz
+from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
+
+app = Flask(__name__)
+app.secret_key = os.getenv("FLASK_SECRET_KEY", "supersecretkey_folders_unique_tma")
+DATA_FILE = 'cloudeng_data_tma.json'
+REPO_ID = "Eluza133/Z1e1u"
+HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
+HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") or HF_TOKEN_WRITE
+TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "6750208873:AAE2hvPlJ99dBdhGa_Brre0IIpUdOvXxHt4")
+ADMIN_TELEGRAM_ID = os.getenv("ADMIN_TELEGRAM_ID", "YOUR_ADMIN_TELEGRAM_USER_ID_HERE")
+ADMIN_USERNAME = os.getenv("ADMIN_USERNAME", "admin")
+ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "zeusadminpass")
+UPLOAD_FOLDER = 'uploads_tma'
+os.makedirs(UPLOAD_FOLDER, exist_ok=True)
+cache = Cache(app, config={'CACHE_TYPE': 'simple'})
+logging.basicConfig(level=logging.INFO)
+
+BASE_STYLE = '''
+:root {
+ --primary: #ff4d6d; --secondary: #00ddeb; --accent: #8b5cf6;
+ --background-dark: #121212; --card-bg-dark: #1e1e1e;
+ --text-dark: #e0e0e0; --text-muted: #888; --shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
+ --glass-bg: rgba(30, 30, 30, 0.7); --transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
+ --delete-color: #ff4444; --folder-color: #ffc107; --selection-color: rgba(139, 92, 246, 0.3);
+ --note-color: #6a5acd;
+}
+@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
+* { margin: 0; padding: 0; box-sizing: border-box; }
+html { scroll-behavior: smooth; }
+body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background: var(--background-dark); color: var(--text-dark); line-height: 1.6; -webkit-tap-highlight-color: transparent; }
+.container { margin: 0 auto; max-width: 1200px; padding: 75px 15px 15px 15px; }
+.app-header { position: fixed; top: 0; left: 0; right: 0; background: var(--glass-bg); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); border-bottom: 1px solid rgba(255,255,255,0.1); z-index: 1000; padding: 10px 20px; display: flex; justify-content: space-between; align-items: center; }
+.user-info { font-weight: 600; }
+.view-toggle { display: flex; align-items: center; gap: 5px; }
+.view-toggle button { background: none; border: none; color: var(--text-muted); font-size: 1.2em; padding: 5px; cursor: pointer; transition: var(--transition); }
+.view-toggle button:hover, .view-toggle button.active { color: var(--primary); }
+h2 { font-size: 1.3em; margin-bottom: 15px; margin-top: 15px; color: var(--text-dark); }
+.breadcrumbs { font-size: 1em; margin-bottom: 20px; white-space: nowrap; overflow-x: auto; -webkit-overflow-scrolling: touch; }
+.breadcrumbs a { color: var(--accent); text-decoration: none; }
+.breadcrumbs span { margin: 0 5px; color: var(--text-muted); }
+input, select, textarea { width: 100%; padding: 14px; margin: 8px 0; border: 1px solid #333; border-radius: 12px; background: #2a2a2a; color: var(--text-dark); font-size: 1em; }
+.btn { padding: 12px 24px; background: var(--primary); color: white; border: none; border-radius: 12px; cursor: pointer; font-size: 1em; font-weight: 600; transition: var(--transition); text-decoration: none; display: inline-block; text-align: center; }
+.btn:hover { filter: brightness(1.2); }
+.btn:active { transform: scale(0.98); }
+.download-btn { background: var(--secondary); }
+.delete-btn { background: var(--delete-color); }
+.folder-btn { background: var(--folder-color); }
+.flash { text-align: center; margin-bottom: 15px; padding: 12px; border-radius: 10px; background: rgba(0, 221, 235, 0.1); color: var(--secondary); }
+.flash.error { background: rgba(255, 68, 68, 0.1); color: var(--delete-color); }
+.file-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 15px; }
+.item { background: var(--card-bg-dark); border-radius: 16px; text-align: center; transition: var(--transition); position: relative; border: 2px solid transparent; user-select: none; padding: 10px; display: flex; flex-direction: column; cursor: pointer; }
+.item:hover { transform: translateY(-5px); box-shadow: var(--shadow); }
+.item:active { transform: scale(0.97); }
+.item.selected { border-color: var(--accent); background-color: var(--selection-color); }
+.item-preview-wrapper { position: relative; width: 100%; padding-top: 75%; border-radius: 10px; overflow: hidden; margin-bottom: 10px; background: #2a2a2a; }
+.item-preview { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; }
+.item.folder .item-preview { object-fit: contain; font-size: 3.5em; color: var(--folder-color); }
+.item.note .item-preview { object-fit: contain; font-size: 3.5em; color: var(--note-color); }
+.item-name { font-size: 0.9em; font-weight: 500; word-break: break-all; margin: 5px 0; flex-grow: 1; }
+.item-info { font-size: 0.75em; color: var(--text-muted); }
+.item-actions { display: none; }
+.file-grid.list-view { display: flex; flex-direction: column; gap: 8px; }
+.file-grid.list-view .item { flex-direction: row; align-items: center; text-align: left; padding: 8px; }
+.file-grid.list-view .item:hover { transform: translateY(0); }
+.file-grid.list-view .item-preview-wrapper { width: 45px; height: 45px; padding-top: 0; margin-bottom: 0; margin-right: 15px; flex-shrink: 0; }
+.file-grid.list-view .item.folder .item-preview, .file-grid.list-view .item.note .item-preview { font-size: 1.8em; }
+.file-grid.list-view .item-name-info { flex-grow: 1; }
+.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.85); z-index: 2000; justify-content: center; align-items: center; }
+.modal-content { display: flex; flex-direction: column; max-width: 95%; max-height: 95%; background: var(--card-bg-dark); padding: 10px; border-radius: 15px; overflow: hidden; position: relative; }
+.modal-main-content { flex-grow: 1; overflow-y: auto; }
+.modal-main-content img, .modal-main-content video, .modal-main-content iframe, .modal-main-content pre { max-width: 100%; max-height: 85vh; display: block; margin: auto; border-radius: 10px; }
+.modal-main-content iframe { width: 80vw; height: 85vh; border: none; }
+.modal-main-content pre { background: #121212; padding: 15px; border-radius: 8px; white-space: pre-wrap; word-wrap: break-word; max-height: 85vh; color: var(--text-dark); }
+.modal-actions { padding: 10px; text-align: center; border-top: 1px solid #333; }
+.modal-close-btn { position: absolute; top: 15px; right: 25px; font-size: 30px; color: #aaa; cursor: pointer; background: rgba(0,0,0,0.5); border-radius: 50%; width: 30px; height: 30px; line-height: 30px; text-align: center; z-index: 2001;}
+#progress-container { width: 100%; background: #333; border-radius: 10px; margin: 15px 0; display: none; height: 10px; }
+#progress-bar { width: 0%; height: 100%; background: var(--primary); border-radius: 10px; transition: width 0.3s ease; }
+#selection-bar { position: fixed; bottom: -120px; left: 10px; right: 10px; background: var(--glass-bg); backdrop-filter: blur(10px); padding: 10px; border-radius: 15px; box-shadow: var(--shadow); z-index: 1000; display: flex; justify-content: space-around; align-items: center; transition: bottom 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); }
+#selection-bar.visible { bottom: 10px; }
+#selection-bar .btn { margin: 0 5px; padding: 10px 15px; font-size: 0.9em; flex-grow: 1; }
+#move-modal .modal-content, #fab-modal .modal-content { padding: 20px; max-width: 400px; }
+.fab-container { position: fixed; bottom: 20px; right: 20px; z-index: 1050; }
+.fab { width: 56px; height: 56px; background: var(--accent); border-radius: 50%; border: none; box-shadow: var(--shadow); color: white; font-size: 24px; display: flex; justify-content: center; align-items: center; cursor: pointer; transition: transform 0.3s; }
+.fab:active { transform: scale(0.9); }
+.loading-spinner { border: 4px solid #f3f3f3; border-top: 4px solid var(--primary); border-radius: 50%; width: 40px; height: 40px; animation: spin 1s linear infinite; margin: auto; }
+.reminder-item { display: flex; justify-content: space-between; align-items: center; padding: 8px; border-bottom: 1px solid #333; }
+.reminder-item:last-child { border-bottom: none; }
+.reminder-item button { background: var(--delete-color); color: white; border: none; border-radius: 50%; width: 24px; height: 24px; cursor: pointer; }
+'''
+
+def find_node_by_id(filesystem, node_id):
+ if not filesystem: return None, None
+ if filesystem.get('id') == node_id:
+ return filesystem, None
+ queue = [(filesystem, None)]
+ while queue:
+ current_node, parent = queue.pop(0)
+ if current_node.get('type') == 'folder' and 'children' in current_node:
+ for i, child in enumerate(current_node['children']):
+ if child.get('id') == node_id:
+ return child, current_node
+ if child.get('type') == 'folder':
+ queue.append((child, current_node))
+ return None, None
+
+def add_node(filesystem, parent_id, node_data):
+ parent_node, _ = find_node_by_id(filesystem, parent_id)
+ if parent_node and parent_node.get('type') == 'folder':
+ if 'children' not in parent_node:
+ parent_node['children'] = []
+ parent_node['children'].append(node_data)
+ return True
+ return False
+
+def remove_node(filesystem, node_id):
+ node_to_remove, parent_node = find_node_by_id(filesystem, node_id)
+ if node_to_remove and parent_node and 'children' in parent_node:
+ parent_node['children'] = [child for child in parent_node['children'] if child.get('id') != node_id]
+ return True, node_to_remove
+ return False, None
+
+def get_node_path_string(filesystem, node_id):
+ path_list = []
+ current_id = node_id
+ while current_id:
+ node, parent = find_node_by_id(filesystem, current_id)
+ if not node: break
+ if node.get('id') != 'root':
+ path_list.append(node.get('name', node.get('original_filename', '')))
+ if not parent: break
+ current_id = parent.get('id') if parent else None
+ return " / ".join(reversed(path_list)) or "Root"
+
+def get_all_folders(filesystem, exclude_ids=None):
+ if exclude_ids is None:
+ exclude_ids = set()
+ folders = []
+
+ def traverse(node, path_prefix):
+ if node.get('type') == 'folder':
+ if node.get('id') not in exclude_ids:
+ folder_name = f"{path_prefix}{node.get('name', 'Unnamed')}" if path_prefix else (node.get('name') if node.get('id') != 'root' else 'Главная (Root)')
+ folders.append({'id': node.get('id'), 'name': folder_name})
+ new_prefix = f"{path_prefix}{node.get('name', '')}/" if node.get('id') != 'root' else ""
+ for child in node.get('children', []):
+ traverse(child, new_prefix)
+
+ traverse(filesystem, "")
+ return sorted(folders, key=lambda x: x['name'].lower())
+
+def count_items_recursive(node):
+ if not node or not isinstance(node, dict):
+ return 0
+ count = 0
+ if node.get('type') in ['file', 'note']:
+ count += 1
+ if node.get('type') == 'folder' and 'children' in node:
+ for child in node.get('children', []):
+ count += count_items_recursive(child)
+ return count
+
+def initialize_user_filesystem_tma(user_data, tma_user_id_str):
+ if 'filesystem' not in user_data:
+ user_data['filesystem'] = {"type": "folder", "id": "root", "name": "root", "children": []}
+ if 'files' in user_data and isinstance(user_data['files'], list):
+ for old_file in user_data['files']:
+ file_id = old_file.get('id', uuid.uuid4().hex)
+ original_filename = old_file.get('filename', 'unknown_file')
+ name_part, ext_part = os.path.splitext(original_filename)
+ unique_suffix = uuid.uuid4().hex[:8]
+ unique_filename = f"{name_part}_{unique_suffix}{ext_part}"
+ hf_path = f"cloud_files/{tma_user_id_str}/root/{unique_filename}"
+ file_node = {
+ 'type': 'file', 'id': file_id, 'original_filename': original_filename,
+ 'unique_filename': unique_filename, 'path': hf_path,
+ 'file_type': get_file_type(original_filename),
+ 'upload_date': old_file.get('upload_date', datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
+ }
+ add_node(user_data['filesystem'], 'root', file_node)
+ del user_data['files']
+
+@cache.memoize(timeout=300)
+def load_data():
+ try:
+ download_db_from_hf()
+ with open(DATA_FILE, 'r', encoding='utf-8') as file: data = json.load(file)
+ if not isinstance(data, dict): data = {'users': {}}
+ data.setdefault('users', {})
+ for tma_user_id_str, user_data_item in data['users'].items():
+ initialize_user_filesystem_tma(user_data_item, tma_user_id_str)
+ user_data_item.setdefault('reminders', [])
+ 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"Backup {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
+ 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:
+ 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 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:
+ upload_db_to_hf()
+ time.sleep(1800)
+
+def send_telegram_message(chat_id, text):
+ if not TELEGRAM_BOT_TOKEN:
+ logging.warning("TELEGRAM_BOT_TOKEN is not set. Cannot send message.")
+ return
+ url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
+ payload = {"chat_id": chat_id, "text": text, "parse_mode": "HTML"}
+ try:
+ response = requests.post(url, json=payload)
+ response.raise_for_status()
+ logging.info(f"Sent message to {chat_id}")
+ except requests.exceptions.RequestException as e:
+ logging.error(f"Failed to send Telegram message to {chat_id}: {e}")
+
+def check_reminders():
+ while True:
+ try:
+ data = load_data()
+ now_utc = datetime.now(pytz.utc)
+ made_changes = False
+ for user_id, user_data in data.get('users', {}).items():
+ if 'reminders' in user_data:
+ for reminder in user_data['reminders']:
+ if not reminder.get('notified', False):
+ due_time_str = reminder.get('due_datetime_utc')
+ if due_time_str:
+ due_time_utc = datetime.fromisoformat(due_time_str.replace('Z', '+00:00')).replace(tzinfo=pytz.utc)
+ if now_utc >= due_time_utc:
+ telegram_id = user_data.get('telegram_id')
+ if telegram_id:
+ message_text = f"🔔 Напоминание:\n\n{reminder['text']}"
+ send_telegram_message(telegram_id, message_text)
+ reminder['notified'] = True
+ made_changes = True
+ if made_changes:
+ save_data(data)
+ except Exception as e:
+ logging.error(f"Error in check_reminders thread: {e}")
+ time.sleep(60)
+
+def get_file_type(filename):
+ filename_lower = filename.lower()
+ if filename_lower.endswith(('.mp4', '.mov', '.avi', '.webm', '.mkv')): return 'video'
+ elif filename_lower.endswith(('.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg')): return 'image'
+ elif filename_lower.endswith('.pdf'): return 'pdf'
+ elif filename_lower.endswith('.txt'): return 'text'
+ return 'other'
+
+def is_admin_tma():
+ if not ADMIN_TELEGRAM_ID or ADMIN_TELEGRAM_ID == "YOUR_ADMIN_TELEGRAM_USER_ID_HERE":
+ return False
+ return 'telegram_user_id' in session and str(session['telegram_user_id']) == str(ADMIN_TELEGRAM_ID)
+
+def admin_browser_login_required(f):
+ @wraps(f)
+ def decorated_function(*args, **kwargs):
+ if not session.get('admin_browser_logged_in'):
+ return redirect(url_for('admin_login', next=request.url))
+ return f(*args, **kwargs)
+ return decorated_function
+
+TMA_ENTRY_HTML = '''
+
+Zeus Cloud TMA
+
+Загрузка приложения...
+'''
+
+@app.route('/tma')
+def tma_entry_page():
+ return render_template_string(TMA_ENTRY_HTML)
+
+@app.route('/')
+def root_redirect():
+ return redirect(url_for('tma_entry_page'))
+
+@app.route('/auth_via_telegram', methods=['POST'])
+def auth_via_telegram():
+ try:
+ payload = request.json
+ tg_user_data = payload.get('user')
+ if not tg_user_data or not tg_user_data.get('id'):
+ return jsonify({'status': 'error', 'message': 'Отсутствуют данные пользователя Telegram.'}), 400
+ tma_user_id_str = str(tg_user_data['id'])
+ data = load_data()
+
+ user_info = {
+ 'telegram_id': tg_user_data['id'],
+ 'telegram_username': tg_user_data.get('username'),
+ 'first_name': tg_user_data.get('first_name'),
+ 'last_name': tg_user_data.get('last_name'),
+ 'photo_url': tg_user_data.get('photo_url')
+ }
+
+ if tma_user_id_str not in data['users']:
+ user_info['created_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+ user_info['filesystem'] = {"type": "folder", "id": "root", "name": "root", "children": []}
+ user_info['reminders'] = []
+ data['users'][tma_user_id_str] = user_info
+ initialize_user_filesystem_tma(data['users'][tma_user_id_str], tma_user_id_str)
+ else:
+ data['users'][tma_user_id_str].update(user_info)
+
+ try: save_data(data)
+ except Exception as e:
+ logging.error(f"Save data error for TMA user {tma_user_id_str}: {e}")
+ return jsonify({'status': 'error', 'message': 'Ошибка сохранения данных пользователя.'}), 500
+
+ session['telegram_user_id'] = tma_user_id_str
+ display_name = tg_user_data.get('first_name') or tg_user_data.get('username') or f"User {tma_user_id_str}"
+ session['telegram_display_name'] = display_name
+ return jsonify({'status': 'success', 'redirect_url': url_for('tma_dashboard')})
+ except Exception as e:
+ logging.error(f"Error in auth_via_telegram: {e}")
+ return jsonify({'status': 'error', 'message': 'Внутренняя ошибка сервера при авторизации.'}), 500
+
+TMA_DASHBOARD_HTML_TEMPLATE = '''
+
+Zeus Cloud
+
+
+
+
+
+
+
+{% with messages = get_flashed_messages(with_categories=true) %}{% if messages %}
+ {% for category, message in messages %}
{{ message }}
{% endfor %}
+{% endif %}{% endwith %}
+
+
{{ current_folder.name if current_folder_id != 'root' else 'Главная' }}
+
+
+ {% for item in items %}
+
+
+ {% if item.type == 'folder' %}
+
+ {% elif item.type == 'note' %}
+
+ {% elif item.type == 'file' %}
+ {% if item.file_type == 'image' %}
 }})
+ {% elif item.file_type == 'video' %}
+ {% elif item.file_type == 'pdf' %}
+ {% elif item.file_type == 'text' %}
+ {% else %}
+ {% endif %}
+ {% endif %}
+
+
+
{{ (item.title if item.type == 'note' else item.name if item.type == 'folder' else item.original_filename) | truncate(30, True) }}
+ {% if item.type == 'file' %}
{{ item.upload_date }}
+ {% elif item.type == 'note' %}
{{ item.modified_date }}
{% endif %}
+
+
+ {% endfor %}
+ {% if not items %}
Эта папка пуста.
{% endif %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Переместить в:
+
+
+
+
+
+
+
Действия
+
+
+
+
+
+
+
+
Новая заметка
+
+
+
+
+
+
+
+
+
+
+
Напоминания
+
+
Новое напоминание
+
+
+
+
+
+
+
+
+
+
+
+'''
+
+@app.route('/tma_dashboard', methods=['GET', 'POST'])
+def tma_dashboard():
+ if 'telegram_user_id' not in session:
+ flash('Пожалуйста, авторизуйтесь через Telegram.', 'error')
+ return redirect(url_for('tma_entry_page'))
+ tma_user_id = session['telegram_user_id']
+ display_name = session.get('telegram_display_name', 'Пользователь')
+ data = load_data()
+ if tma_user_id not in data['users']:
+ session.clear()
+ flash('Пользователь не найден. Пожалуйста, перезапустите приложение.', 'error')
+ return redirect(url_for('tma_entry_page'))
+ user_data = data['users'][tma_user_id]
+ initialize_user_filesystem_tma(user_data, tma_user_id)
+ 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'], 'root')
+
+ parent_folder_id = parent_folder.get('id', 'root') if parent_folder else 'root'
+
+ items_in_folder = sorted(current_folder.get('children', []), key=lambda x: (x['type'] not in ['folder', 'note'], x.get('name', x.get('original_filename', x.get('title', ''))).lower()))
+ if request.method == 'POST':
+ if not HF_TOKEN_WRITE:
+ flash('Загрузка невозможна: токен для записи не настроен.', 'error')
+ return redirect(url_for('tma_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('tma_dashboard', folder_id=current_folder_id))
+ target_folder_id = request.form.get('current_folder_id', 'root')
+ uploaded_count = 0
+ errors_list = []
+ api = HfApi()
+ for file_obj in files:
+ if file_obj and file_obj.filename:
+ original_filename = secure_filename(file_obj.filename)
+ name_part, ext_part = os.path.splitext(original_filename)
+ unique_suffix = uuid.uuid4().hex[:8]
+ unique_filename = f"{name_part}_{unique_suffix}{ext_part}"
+ file_id = uuid.uuid4().hex
+ hf_path = f"cloud_files/{tma_user_id}/{target_folder_id}/{unique_filename}"
+ temp_path = os.path.join(UPLOAD_FOLDER, f"{file_id}_{unique_filename}")
+ try:
+ file_obj.save(temp_path)
+ api.upload_file(path_or_fileobj=temp_path, path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE)
+ 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')}
+ if add_node(user_data['filesystem'], target_folder_id, file_info):
+ uploaded_count += 1
+ except Exception as e:
+ errors_list.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: flash('Файлы загружены, но ошибка сохранения метаданных.', 'error')
+ if errors_list:
+ for error_msg in errors_list: flash(error_msg, 'error')
+ return redirect(url_for('tma_dashboard', folder_id=target_folder_id))
+
+ breadcrumbs = []
+ temp_id = current_folder_id
+ while temp_id:
+ node, parent_node_bc = 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_node_bc: break
+ temp_id = parent_node_bc.get('id')
+ breadcrumbs.reverse()
+
+ all_folders_for_move = get_all_folders(user_data['filesystem'])
+
+ return render_template_string(TMA_DASHBOARD_HTML_TEMPLATE, display_name=display_name, items=items_in_folder, current_folder_id=current_folder_id, current_folder=current_folder, parent_folder_id=parent_folder_id, breadcrumbs=breadcrumbs, hf_file_url_jinja=lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{quote(path)}{'?download=true' if download else ''}", is_tma_user_admin_flag=is_admin_tma(), all_folders_for_move=all_folders_for_move)
+
+@app.route('/create_folder_tma', methods=['POST'])
+def create_folder_tma():
+ if 'telegram_user_id' not in session: return redirect(url_for('tma_entry_page'))
+ tma_user_id = session['telegram_user_id']
+ data = load_data()
+ user_data = data['users'].get(tma_user_id)
+ if not user_data: return redirect(url_for('tma_entry_page'))
+
+ parent_folder_id = request.form.get('parent_folder_id', 'root')
+ folder_name = request.form.get('folder_name', '').strip()
+ if not folder_name:
+ flash('Имя папки не может быть пустым!', 'error')
+ else:
+ folder_id = uuid.uuid4().hex
+ folder_data = {'type': 'folder', 'id': folder_id, 'name': folder_name, 'children': []}
+ if add_node(user_data['filesystem'], parent_folder_id, folder_data):
+ try: save_data(data); flash(f'Папка "{folder_name}" создана.')
+ except Exception: flash('Ошибка сохранения данных.', 'error')
+ else: flash('Не удалось найти родительскую папку.', 'error')
+ return redirect(url_for('tma_dashboard', folder_id=parent_folder_id))
+
+def get_item_node_for_user(item_id):
+ if not (session.get('telegram_user_id') or session.get('admin_browser_logged_in')):
+ return None
+ data = load_data()
+ if session.get('admin_browser_logged_in'):
+ for uid, udata in data.get('users', {}).items():
+ node, _ = find_node_by_id(udata.get('filesystem', {}), item_id)
+ if node: return node
+ else:
+ user_data = data['users'].get(session['telegram_user_id'])
+ if user_data:
+ node, _ = find_node_by_id(user_data.get('filesystem', {}), item_id)
+ if node: return node
+ return None
+
+def get_file_node_for_admin(tma_user_id_str, file_id):
+ if not session.get('admin_browser_logged_in'):
+ return None
+ data = load_data()
+ user_data = data['users'].get(tma_user_id_str)
+ if user_data:
+ node, _ = find_node_by_id(user_data.get('filesystem', {}), file_id)
+ if node and node.get('type') == 'file':
+ return node
+ return None
+
+@app.route('/download_tma/')
+def download_tma(file_id):
+ file_node = get_item_node_for_user(file_id)
+ if not file_node or file_node.get('type') != 'file':
+ return jsonify({'status': 'error', 'message': 'Файл не найден или доступ запрещен'}), 404
+
+ token = uuid.uuid4().hex
+ cache.set(f"download_token_{token}", file_node, timeout=300)
+ public_url = url_for('public_download', token=token, _external=True)
+ return jsonify({'status': 'success', 'url': public_url})
+
+@app.route('/public_download/')
+def public_download(token):
+ file_node = cache.get(f"download_token_{token}")
+ if not file_node:
+ return Response("Ссылка для скачивания недействительна или истекла.", status=404)
+
+ hf_path = file_node.get('path')
+ original_filename = file_node.get('original_filename', 'downloaded_file')
+ if not hf_path:
+ return Response("Ошибка: Путь к файлу не найден.", status=500)
+
+ try:
+ hf_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{quote(hf_path)}"
+ headers = {}
+ if HF_TOKEN_READ:
+ headers["Authorization"] = f"Bearer {HF_TOKEN_READ}"
+
+ req = requests.get(hf_url, headers=headers, stream=True, allow_redirects=True)
+ req.raise_for_status()
+
+ encoded_filename = quote(original_filename)
+ response_headers = {
+ 'Content-Type': req.headers.get('Content-Type', 'application/octet-stream'),
+ 'Content-Disposition': f"attachment; filename*=UTF-8''{encoded_filename}",
+ 'Content-Length': req.headers.get('Content-Length')
+ }
+ response_headers = {k: v for k, v in response_headers.items() if v is not None}
+
+ return Response(stream_with_context(req.iter_content(chunk_size=8192)), headers=response_headers)
+
+ except requests.exceptions.HTTPError as e:
+ if e.response.status_code == 404:
+ logging.error(f"File not found on Hugging Face during streaming: {hf_path}")
+ return Response("Файл не найден на удаленном хранилище.", status=404)
+ else:
+ logging.error(f"HTTP error downloading from HF via streaming: {e}")
+ return Response(f'Ошибка HTTP при скачивании файла: {e}', status=502)
+ except Exception as e:
+ logging.error(f"Error streaming with token {token} from HF: {e}")
+ return Response(f'Ошибка скачивания файла: {e}', status=502)
+
+@app.route('/batch_download_tma')
+def batch_download_tma():
+ if 'telegram_user_id' not in session: return Response("Unauthorized", 401)
+ file_ids_str = request.args.get('file_ids')
+ if not file_ids_str: return Response("No file IDs provided", 400)
+
+ file_ids = file_ids_str.split(',')
+
+ temp_zip_file = tempfile.NamedTemporaryFile(delete=False, suffix=".zip")
+
+ try:
+ with zipfile.ZipFile(temp_zip_file.name, 'w', zipfile.ZIP_DEFLATED) as zf:
+ for file_id in file_ids:
+ file_node = get_item_node_for_user(file_id)
+ if file_node and file_node.get('path'):
+ hf_path = file_node['path']
+ original_filename = file_node.get('original_filename', file_id)
+ try:
+ local_file_path = hf_hub_download(
+ repo_id=REPO_ID,
+ filename=hf_path,
+ repo_type="dataset",
+ token=HF_TOKEN_READ,
+ cache_dir=os.path.join(UPLOAD_FOLDER, 'hf_download_cache')
+ )
+ zf.write(local_file_path, arcname=original_filename)
+ except Exception as e:
+ logging.error(f"Failed to download and add {original_filename} to zip: {e}")
+
+ return send_file(temp_zip_file.name, as_attachment=True, download_name='archive.zip', mimetype='application/zip')
+ finally:
+ if os.path.exists(temp_zip_file.name):
+ os.unlink(temp_zip_file.name)
+
+@app.route('/batch_delete_tma', methods=['POST'])
+def batch_delete_tma():
+ if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Не авторизован.'}), 401
+ tma_user_id = session['telegram_user_id']
+ data = load_data()
+ user_data = data['users'].get(tma_user_id)
+ if not user_data: return jsonify({'status': 'error', 'message': 'Пользователь не найден.'}), 404
+ item_ids = request.json.get('item_ids', [])
+ if not item_ids: return jsonify({'status': 'error', 'message': 'Не выбраны элементы.'}), 400
+
+ api = HfApi()
+ success_count = 0; errors = []
+ for item_id in item_ids:
+ node, _ = find_node_by_id(user_data['filesystem'], item_id)
+ if not node:
+ errors.append(f"Элемент {item_id} не найден.")
+ continue
+ if node.get('type') == 'folder':
+ if node.get('children'): errors.append(f'Папка "{node.get("name")}" не пуста.'); continue
+ if remove_node(user_data['filesystem'], item_id)[0]: success_count += 1
+ else: errors.append(f'Ошибка удаления папки "{node.get("name")}".')
+ elif node.get('type') == 'note':
+ if remove_node(user_data['filesystem'], item_id)[0]: success_count += 1
+ else: errors.append(f'Ошибка удаления "{node.get("title")}".')
+ elif node.get('type') == 'file':
+ try:
+ if node.get('path') and HF_TOKEN_WRITE: api.delete_file(path_in_repo=node['path'], repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE)
+ except hf_utils.EntryNotFoundError: pass
+ except Exception as e: errors.append(f'Ошибка удаления "{node.get("original_filename")}" с сервера: {e}'); continue
+ if remove_node(user_data['filesystem'], item_id)[0]: success_count += 1
+ else: errors.append(f'Ошибка удаления "{node.get("original_filename")}" из базы.')
+ if success_count > 0:
+ try: save_data(data)
+ except Exception as e: return jsonify({'status': 'error', 'message': f'Ошибка сохранения: {e}'})
+ if errors: return jsonify({'status': 'error', 'message': f'Удалено {success_count}. Ошибки: ' + "; ".join(errors)})
+ return jsonify({'status': 'success', 'message': f'Удалено {success_count} элемент(ов).'})
+
+@app.route('/batch_move_tma', methods=['POST'])
+def batch_move_tma():
+ if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Не авторизован.'}), 401
+ tma_user_id = session['telegram_user_id']
+ data = load_data()
+ user_data = data['users'].get(tma_user_id)
+ if not user_data: return jsonify({'status': 'error', 'message': 'Пользователь не найден.'}), 404
+ item_ids = request.json.get('item_ids', [])
+ destination_id = request.json.get('destination_id')
+ if not item_ids or not destination_id: return jsonify({'status': 'error', 'message': 'Не указаны файлы или папка.'}), 400
+ destination_node, _ = find_node_by_id(user_data['filesystem'], destination_id)
+ if not destination_node or destination_node.get('type') != 'folder': return jsonify({'status': 'error', 'message': 'Папка назначения не найдена.'}), 404
+
+ descendant_ids = set()
+ for item_id in item_ids:
+ node, _ = find_node_by_id(user_data['filesystem'], item_id)
+ if node and node.get('type') == 'folder':
+ queue = [node]
+ while queue:
+ curr = queue.pop(0)
+ descendant_ids.add(curr.get('id'))
+ if 'children' in curr: queue.extend(curr['children'])
+ if destination_id in descendant_ids: return jsonify({'status': 'error', 'message': 'Нельзя переместить папку в саму себя.'})
+
+ moved_count = 0; errors = []
+ for item_id in item_ids:
+ if item_id == destination_id: continue
+ removed, node_to_move = remove_node(user_data['filesystem'], item_id)
+ if removed and node_to_move:
+ if add_node(user_data['filesystem'], destination_id, node_to_move): moved_count += 1
+ else: errors.append(f'Ошибка добавления {item_id} в новую папку.')
+ else: errors.append(f'Не удалось извлечь {item_id}.')
+
+ if moved_count > 0:
+ try: save_data(data)
+ except Exception as e: return jsonify({'status': 'error', 'message': f'Ошибка сохранения: {e}'})
+ if errors: return jsonify({'status': 'error', 'message': f'Перемещено {moved_count}. Ошибки: ' + "; ".join(errors)})
+ return jsonify({'status': 'success', 'message': f'Перемещено {moved_count} элемент(ов).'})
+
+@app.route('/get_text_content_tma/')
+def get_text_content_tma(file_id):
+ file_node = get_item_node_for_user(file_id)
+ if not file_node or file_node.get('file_type') != 'text': return Response("Текстовый файл не найден", 404)
+ hf_path = file_node.get('path')
+ if not hf_path: return Response("Ошибка: путь к файлу отсутствует", 500)
+ file_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{quote(hf_path)}?download=true"
+ try:
+ req_headers = {};
+ if HF_TOKEN_READ: req_headers["authorization"] = f"Bearer {HF_TOKEN_READ}"
+ response = requests.get(file_url, headers=req_headers)
+ response.raise_for_status()
+ if len(response.content) > 1 * 1024 * 1024: return Response("Файл слишком большой для предпросмотра.", 413)
+ try: text_content = response.content.decode('utf-8')
+ 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}", 502)
+
+@app.route('/get_note_tma/')
+def get_note_tma(note_id):
+ if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Unauthorized'}), 401
+ note_node = get_item_node_for_user(note_id)
+ if not note_node or note_node.get('type') != 'note':
+ return jsonify({'status': 'error', 'message': 'Note not found'}), 404
+ return jsonify({'status': 'success', 'note': note_node})
+
+@app.route('/create_or_update_note_tma', methods=['POST'])
+def create_or_update_note_tma():
+ if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Unauthorized'}), 401
+ tma_user_id = session['telegram_user_id']
+ data = load_data()
+ user_data = data['users'].get(tma_user_id)
+ if not user_data: return jsonify({'status': 'error', 'message': 'User not found'}), 404
+
+ payload = request.json
+ note_id = payload.get('note_id')
+ title = payload.get('title', '').strip()
+ content = payload.get('content', '')
+ parent_folder_id = payload.get('parent_folder_id', 'root')
+ now_str = datetime.now().strftime('%Y-%m-%d %H:%M')
+
+ if not title: return jsonify({'status': 'error', 'message': 'Title cannot be empty.'}), 400
+
+ if note_id:
+ node, _ = find_node_by_id(user_data['filesystem'], note_id)
+ if not node or node.get('type') != 'note':
+ return jsonify({'status': 'error', 'message': 'Note not found'}), 404
+ node['title'] = title
+ node['content'] = content
+ node['modified_date'] = now_str
+ else:
+ new_note_id = uuid.uuid4().hex
+ note_data = {
+ 'type': 'note', 'id': new_note_id, 'title': title, 'content': content,
+ 'created_date': now_str, 'modified_date': now_str
+ }
+ if not add_node(user_data['filesystem'], parent_folder_id, note_data):
+ return jsonify({'status': 'error', 'message': 'Parent folder not found'}), 404
+
+ try:
+ save_data(data)
+ return jsonify({'status': 'success', 'message': 'Note saved.'})
+ except Exception as e:
+ return jsonify({'status': 'error', 'message': f'Failed to save data: {e}'}), 500
+
+@app.route('/get_reminders_tma')
+def get_reminders_tma():
+ if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Unauthorized'}), 401
+ user_data = load_data()['users'].get(session['telegram_user_id'])
+ if not user_data: return jsonify({'status': 'error', 'message': 'User not found'}), 404
+
+ reminders = sorted(user_data.get('reminders', []), key=lambda r: r.get('due_datetime_utc', ''))
+ return jsonify({'status': 'success', 'reminders': reminders})
+
+@app.route('/create_reminder_tma', methods=['POST'])
+def create_reminder_tma():
+ if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Unauthorized'}), 401
+ tma_user_id = session['telegram_user_id']
+ data = load_data()
+ user_data = data['users'].get(tma_user_id)
+ if not user_data: return jsonify({'status': 'error', 'message': 'User not found'}), 404
+
+ payload = request.json
+ text = payload.get('text', '').strip()
+ dt_local_str = payload.get('datetime_local')
+ user_tz_str = payload.get('user_timezone', 'UTC')
+ if not text or not dt_local_str:
+ return jsonify({'status': 'error', 'message': 'Missing required fields'}), 400
+
+ try:
+ user_tz = ZoneInfo(user_tz_str)
+ except ZoneInfoNotFoundError:
+ user_tz = pytz.timezone('UTC')
+
+ dt_local = datetime.fromisoformat(dt_local_str)
+ dt_aware = dt_local.astimezone(user_tz)
+ dt_utc = dt_aware.astimezone(pytz.utc)
+
+ new_reminder = {
+ 'id': uuid.uuid4().hex, 'text': text,
+ 'due_datetime_utc': dt_utc.isoformat().replace('+00:00', 'Z'),
+ 'due_datetime_local': dt_local.isoformat(),
+ 'user_timezone': user_tz_str, 'notified': False
+ }
+ user_data.setdefault('reminders', []).append(new_reminder)
+ try:
+ save_data(data)
+ return jsonify({'status': 'success'})
+ except Exception as e:
+ return jsonify({'status': 'error', 'message': f'Failed to save: {e}'}), 500
+
+@app.route('/delete_reminder_tma/', methods=['POST'])
+def delete_reminder_tma(reminder_id):
+ if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Unauthorized'}), 401
+ tma_user_id = session['telegram_user_id']
+ data = load_data()
+ user_data = data['users'].get(tma_user_id)
+ if not user_data: return jsonify({'status': 'error', 'message': 'User not found'}), 404
+
+ reminders = user_data.get('reminders', [])
+ user_data['reminders'] = [r for r in reminders if r.get('id') != reminder_id]
+
+ try:
+ save_data(data)
+ return jsonify({'status': 'success'})
+ except Exception as e:
+ return jsonify({'status': 'error', 'message': f'Failed to save: {e}'}), 500
+
+@app.route('/tma_logout')
+def tma_logout():
+ session.clear()
+ flash('Вы вышли из сессии приложения.')
+ return redirect(url_for('tma_entry_page'))
+
+ADMIN_LOGIN_HTML = '''
+Admin Login
+
+
+
+
Admin Login
+{% with messages = get_flashed_messages(with_categories=true) %}{% if messages %}
+ {% for category, message in messages %}
{{ message }}
{% endfor %}
+{% endif %}{% endwith %}
+
+
+'''
+
+ADMIN_PANEL_HTML = '''
+Admin Panel
+
+
+
+
+
+
+{% for user_id, user in users %}
+ -
+
+ {% if user.photo_url %}

+ {% else %}
{{ user.get('first_name', 'U')[0] }}
{% endif %}
+
+ {{ user.get('first_name', 'N/A') }} {{ user.get('last_name', '') }} (@{{ user.get('telegram_username', 'N/A') }})
+ ID: {{ user_id }}
+ Created: {{ user.get('created_at', 'N/A') }}
+ Items: {{ user.get('item_count', 0) }}
+ Reminders: {{ user.get('reminders', [])|length }}
+
+
+
+
+{% else %}
+ - No users found.
+{% endfor %}
+
+
+'''
+
+ADMIN_USER_FILES_HTML = '''
+
+Admin - User Files
+
+
+
+
+
+{% with messages = get_flashed_messages(with_categories=true) %}{% if messages %}
+ {% for category, message in messages %}
{{ message }}
{% endfor %}
+{% endif %}{% endwith %}
+
+
{{ current_folder.name if current_folder_id != 'root' else 'Root Folder' }}
+
+ {% for item in items %}
+
+
+ {% if item.type == 'folder' %}
+
+ {% elif item.type == 'note' %}
+
+ {% elif item.type == 'file' %}
+ {% if item.file_type == 'image' %}
 }})
+ {% elif item.file_type == 'video' %}
+ {% elif item.file_type == 'pdf' %}
+ {% elif item.file_type == 'text' %}
+ {% else %}
+ {% endif %}
+ {% endif %}
+
+
+
{{ (item.title if item.type == 'note' else item.name if item.type == 'folder' else item.original_filename) | truncate(30, True) }}
+ {% if item.type == 'file' %}
{{ item.upload_date }}
+ {% elif item.type == 'note' %}
{{ item.modified_date }}
{% endif %}
+
+
+
+
+
+ {% endfor %}
+ {% if not items %}
This folder is empty.
{% endif %}
+
+
+
+
+'''
+
+ADMIN_USER_REMINDERS_HTML = '''
+Admin - User Reminders
+
+
+
+
+
+
+{% with messages = get_flashed_messages(with_categories=true) %}{% if messages %}
+ {% for category, message in messages %}
{{ message }}
{% endfor %}
+{% endif %}{% endwith %}
+
+
+'''
+
+@app.route('/admin')
+def admin_redirect():
+ return redirect(url_for('admin_login'))
+
+@app.route('/admhosto/login', methods=['GET', 'POST'])
+def admin_login():
+ if session.get('admin_browser_logged_in'):
+ return redirect(url_for('admin_panel'))
+ if request.method == 'POST':
+ if request.form.get('username') == ADMIN_USERNAME and request.form.get('password') == ADMIN_PASSWORD:
+ session['admin_browser_logged_in'] = True
+ next_url = request.form.get('next') or url_for('admin_panel')
+ return redirect(next_url)
+ else:
+ flash('Invalid credentials.', 'error')
+ return render_template_string(ADMIN_LOGIN_HTML)
+
+@app.route('/admhosto/logout')
+def admin_logout():
+ session.pop('admin_browser_logged_in', None)
+ flash('You have been logged out.')
+ return redirect(url_for('admin_login'))
+
+@app.route('/admhosto')
+@admin_browser_login_required
+def admin_panel():
+ data = load_data()
+ all_users = data.get('users', {})
+ search_query = request.args.get('q', '').lower()
+
+ processed_users = {}
+ for user_id, user_data in all_users.items():
+ user_data['item_count'] = count_items_recursive(user_data.get('filesystem'))
+ processed_users[user_id] = user_data
+
+ if search_query:
+ filtered_users = {}
+ for user_id, user_data in processed_users.items():
+ full_name = f"{user_data.get('first_name', '')} {user_data.get('last_name', '')}".lower()
+ username = user_data.get('telegram_username', '').lower()
+ if search_query in full_name or search_query in username:
+ filtered_users[user_id] = user_data
+ users_to_display = filtered_users
+ else:
+ users_to_display = processed_users
+
+ sorted_users = sorted(
+ users_to_display.items(),
+ key=lambda item: item[1].get('created_at', '0000-00-00 00:00:00'),
+ reverse=True
+ )
+
+ return render_template_string(ADMIN_PANEL_HTML, users=sorted_users, search_query=request.args.get('q', ''))
+
+@app.route('/admhosto/user/')
+@admin_browser_login_required
+def admin_user_files(tma_user_id_str):
+ data = load_data()
+ user_data = data['users'].get(tma_user_id_str)
+ if not user_data:
+ flash('User not found.', 'error')
+ return redirect(url_for('admin_panel'))
+
+ initialize_user_filesystem_tma(user_data, tma_user_id_str)
+ current_folder_id = request.args.get('folder_id', 'root')
+ current_folder, _ = find_node_by_id(user_data['filesystem'], current_folder_id)
+
+ if not current_folder or current_folder.get('type') != 'folder':
+ flash('Folder not found!', 'error')
+ current_folder_id = 'root'
+ current_folder, _ = find_node_by_id(user_data['filesystem'], 'root')
+
+ items_in_folder = sorted(current_folder.get('children', []), key=lambda x: (x['type'] not in ['folder', 'note'], x.get('name', x.get('original_filename', x.get('title', ''))).lower()))
+
+ breadcrumbs = []
+ temp_id = current_folder_id
+ while temp_id:
+ node, parent_node_bc = 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_node_bc: break
+ temp_id = parent_node_bc.get('id')
+ breadcrumbs.reverse()
+
+ return render_template_string(ADMIN_USER_FILES_HTML,
+ user_id=tma_user_id_str,
+ user=user_data,
+ 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/{quote(path)}{'?download=true' if download else ''}")
+
+@app.route('/admhosto/user//reminders')
+@admin_browser_login_required
+def admin_user_reminders(tma_user_id_str):
+ data = load_data()
+ user_data = data['users'].get(tma_user_id_str)
+ if not user_data:
+ flash('User not found.', 'error')
+ return redirect(url_for('admin_panel'))
+
+ reminders = sorted(user_data.get('reminders', []), key=lambda r: r.get('due_datetime_utc', ''), reverse=True)
+ return render_template_string(ADMIN_USER_REMINDERS_HTML, user_id=tma_user_id_str, user=user_data, reminders=reminders)
+
+@app.route('/admhosto/download//')
+@admin_browser_login_required
+def admin_download_file(tma_user_id_str, file_id):
+ file_node = get_file_node_for_admin(tma_user_id_str, file_id)
+ if not file_node:
+ return jsonify({'status': 'error', 'message': 'File not found or access denied!'}), 404
+
+ token = uuid.uuid4().hex
+ cache.set(f"download_token_{token}", file_node, timeout=300)
+ public_url = url_for('public_download', token=token, _external=True)
+ return jsonify({'status': 'success', 'url': public_url})
+
+@app.route('/admhosto/text//')
+@admin_browser_login_required
+def admin_get_text_content(tma_user_id_str, file_id):
+ file_node = get_file_node_for_admin(tma_user_id_str, file_id)
+ if not file_node or file_node.get('file_type') != 'text':
+ return Response("Text file not found", 404)
+ hf_path = file_node.get('path')
+ if not hf_path:
+ return Response("Error: file path is missing", 500)
+ file_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{quote(hf_path)}?download=true"
+ try:
+ req_headers = {}
+ if HF_TOKEN_READ: req_headers["authorization"] = f"Bearer {HF_TOKEN_READ}"
+ response = requests.get(file_url, headers=req_headers)
+ response.raise_for_status()
+ if len(response.content) > 1 * 1024 * 1024:
+ return Response("File too large for preview.", 413)
+ try:
+ text_content = response.content.decode('utf-8')
+ 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"Download error: {e}", 502)
+
+@app.route('/admhosto/note//')
+@admin_browser_login_required
+def admin_get_note(tma_user_id_str, note_id):
+ data = load_data()
+ user_data = data['users'].get(tma_user_id_str)
+ if not user_data: return jsonify({'status': 'error', 'message': 'User not found'}), 404
+ node, _ = find_node_by_id(user_data['filesystem'], note_id)
+ if not node or node.get('type') != 'note':
+ return jsonify({'status': 'error', 'message': 'Note not found'}), 404
+ return jsonify({'status': 'success', 'note': node})
+
+@app.route('/admhosto/delete_item//', methods=['POST'])
+@admin_browser_login_required
+def admin_delete_item(tma_user_id_str, item_id):
+ data = load_data()
+ user_data = data['users'].get(tma_user_id_str)
+ current_folder_id = request.form.get('current_folder_id', 'root')
+ if not user_data:
+ flash('User not found.', 'error'); return redirect(url_for('admin_panel'))
+
+ node, _ = find_node_by_id(user_data['filesystem'], item_id)
+ if not node:
+ flash('Item not found.', 'error')
+ elif node.get('type') == 'file':
+ hf_path = node.get('path')
+ if not HF_TOKEN_WRITE: flash('Deletion not possible: write token not configured.', 'error')
+ else:
+ try:
+ api = HfApi()
+ if hf_path: 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'], item_id)[0]:
+ try: save_data(data); flash('File deleted.')
+ except Exception: flash('File deleted from server, but DB update failed.', 'error')
+ except hf_utils.EntryNotFoundError:
+ if remove_node(user_data['filesystem'], item_id)[0]:
+ try: save_data(data); flash('File not found on server, removed from DB.')
+ except Exception: flash('DB save error (file not on server).', 'error')
+ except Exception as e: flash(f'Deletion error: {e}', 'error')
+ elif node.get('type') == 'note':
+ if remove_node(user_data['filesystem'], item_id)[0]:
+ try: save_data(data); flash('Note deleted.')
+ except Exception: flash('DB update failed after note deletion.', 'error')
+ elif node.get('type') == 'folder':
+ if node.get('children'): flash('Folder is not empty.', 'error')
+ else:
+ if remove_node(user_data['filesystem'], item_id)[0]:
+ try: save_data(data); flash('Folder deleted.')
+ except Exception: flash('DB update failed after folder deletion.', 'error')
+ return redirect(url_for('admin_user_files', tma_user_id_str=tma_user_id_str, folder_id=current_folder_id))
+
+@app.route('/admhosto/user//delete_reminder/', methods=['POST'])
+@admin_browser_login_required
+def admin_delete_reminder(tma_user_id_str, reminder_id):
+ data = load_data()
+ user_data = data['users'].get(tma_user_id_str)
+ if not user_data:
+ flash('User not found.', 'error'); return redirect(url_for('admin_panel'))
+
+ reminders = user_data.get('reminders', [])
+ initial_len = len(reminders)
+ user_data['reminders'] = [r for r in reminders if r.get('id') != reminder_id]
+ if len(user_data['reminders']) < initial_len:
+ try:
+ save_data(data)
+ flash('Reminder deleted successfully.', 'success')
+ except Exception as e:
+ flash(f'Failed to save data: {e}', 'error')
+ else:
+ flash('Reminder not found.', 'error')
+ return redirect(url_for('admin_user_reminders', tma_user_id_str=tma_user_id_str))
+
+if __name__ == '__main__':
+ if not HF_TOKEN_WRITE: logging.warning("HF_TOKEN (write) is not set. Uploads/deletions will fail.")
+ if not HF_TOKEN_READ: logging.warning("HF_TOKEN_READ is not set. Downloads/previews might fail.")
+ if ADMIN_TELEGRAM_ID == "YOUR_ADMIN_TELEGRAM_USER_ID_HERE": logging.warning("ADMIN_TELEGRAM_ID is not set.")
+ if ADMIN_USERNAME == "admin" and ADMIN_PASSWORD == "zeusadminpass":
+ logging.warning("Using default admin credentials. Please change them.")
+
+ if HF_TOKEN_WRITE or HF_TOKEN_READ:
+ download_db_from_hf()
+ else:
+ if not os.path.exists(DATA_FILE):
+ with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}}, f)
+
+ if HF_TOKEN_WRITE:
+ threading.Thread(target=periodic_backup, daemon=True).start()
+
+ threading.Thread(target=check_reminders, daemon=True).start()
+
+ app.run(debug=False, host='0.0.0.0', port=7860)
\ No newline at end of file