Eluza133 commited on
Commit
3d1e388
·
verified ·
1 Parent(s): 65c0890

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -1115
app.py DELETED
@@ -1,1115 +0,0 @@
1
- import flask
2
- from flask import Flask, render_template_string, request, redirect, url_for, session, flash, send_file, jsonify, Response
3
- from flask_caching import Cache
4
- import json
5
- import os
6
- import logging
7
- import threading
8
- import time
9
- from datetime import datetime
10
- from huggingface_hub import HfApi, hf_hub_download, utils as hf_utils
11
- from werkzeug.utils import secure_filename
12
- import requests
13
- from io import BytesIO
14
- import uuid
15
- from functools import wraps
16
-
17
- app = Flask(__name__)
18
- app.secret_key = os.getenv("FLASK_SECRET_KEY", "supersecretkey_folders_unique_tma")
19
- DATA_FILE = 'cloudeng_data_tma.json'
20
- REPO_ID = "Eluza133/Z1e1u"
21
- HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
22
- HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") or HF_TOKEN_WRITE
23
- TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "6750208873:AAE2hvPlJ99dBdhGa_Brre0IIpUdOvXxHt4")
24
- ADMIN_TELEGRAM_ID = os.getenv("ADMIN_TELEGRAM_ID", "YOUR_ADMIN_TELEGRAM_USER_ID_HERE")
25
-
26
- ADMIN_USERNAME = os.getenv("ADMIN_USERNAME", "admin")
27
- ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "zeusadminpass")
28
-
29
-
30
- UPLOAD_FOLDER = 'uploads_tma'
31
- os.makedirs(UPLOAD_FOLDER, exist_ok=True)
32
-
33
- cache = Cache(app, config={'CACHE_TYPE': 'simple'})
34
- logging.basicConfig(level=logging.INFO)
35
-
36
- BASE_STYLE = '''
37
- :root {
38
- --primary: #ff4d6d; --secondary: #00ddeb; --accent: #8b5cf6;
39
- --background-light: #f5f6fa; --background-dark: #1a1625;
40
- --card-bg: rgba(255, 255, 255, 0.95); --card-bg-dark: rgba(40, 35, 60, 0.95);
41
- --text-light: #2a1e5a; --text-dark: #e8e1ff; --shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
42
- --glass-bg: rgba(255, 255, 255, 0.15); --transition: all 0.3s ease; --delete-color: #ff4444;
43
- --folder-color: #ffc107;
44
- }
45
- * { margin: 0; padding: 0; box-sizing: border-box; }
46
- body { font-family: 'Inter', sans-serif; background: var(--background-light); color: var(--text-light); line-height: 1.6; }
47
- body.dark { background: var(--background-dark); color: var(--text-dark); }
48
- .container { margin: 20px auto; max-width: 1200px; padding: 25px; background: var(--card-bg); border-radius: 20px; box-shadow: var(--shadow); overflow-x: hidden; }
49
- body.dark .container { background: var(--card-bg-dark); }
50
- h1 { font-size: 2em; font-weight: 800; text-align: center; margin-bottom: 25px; background: linear-gradient(135deg, var(--primary), var(--accent)); -webkit-background-clip: text; color: transparent; }
51
- h2 { font-size: 1.5em; margin-top: 30px; color: var(--text-light); }
52
- body.dark h2 { color: var(--text-dark); }
53
- h4 { font-size: 1.1em; margin-top: 15px; margin-bottom: 5px; color: var(--accent); }
54
- ol, ul { margin-left: 20px; margin-bottom: 15px; }
55
- li { margin-bottom: 5px; }
56
- input, textarea, input[type="text"], input[type="password"], input[type="file"] { width: 100%; padding: 14px; margin: 12px 0; border: none; border-radius: 14px; background: var(--glass-bg); color: var(--text-light); font-size: 1.1em; box-shadow: inset 0 3px 10px rgba(0, 0, 0, 0.1); }
57
- body.dark input, body.dark textarea, body.dark input[type="text"], body.dark input[type="password"], body.dark input[type="file"] { color: var(--text-dark); background: rgba(255,255,255,0.05); }
58
- input:focus, textarea:focus { outline: none; box-shadow: 0 0 0 4px var(--primary); }
59
- .btn { padding: 14px 28px; background: var(--primary); color: white; border: none; border-radius: 14px; cursor: pointer; font-size: 1.1em; font-weight: 600; transition: var(--transition); box-shadow: var(--shadow); display: inline-block; text-decoration: none; margin-top: 5px; margin-right: 5px; }
60
- .btn:hover { transform: scale(1.05); background: #e6415f; }
61
- .download-btn { background: var(--secondary); }
62
- .download-btn:hover { background: #00b8c5; }
63
- .delete-btn { background: var(--delete-color); }
64
- .delete-btn:hover { background: #cc3333; }
65
- .folder-btn { background: var(--folder-color); }
66
- .folder-btn:hover { background: #e6a000; }
67
- .flash { color: var(--secondary); text-align: center; margin-bottom: 15px; padding: 10px; background: rgba(0, 221, 235, 0.1); border-radius: 10px; }
68
- .flash.error { color: var(--delete-color); background: rgba(255, 68, 68, 0.1); }
69
- .file-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 20px; margin-top: 20px; }
70
- .user-list { margin-top: 20px; }
71
- .user-item { padding: 15px; background: var(--card-bg); border-radius: 16px; margin-bottom: 10px; box-shadow: var(--shadow); transition: var(--transition); }
72
- body.dark .user-item { background: var(--card-bg-dark); }
73
- .user-item:hover { transform: translateY(-5px); }
74
- .user-item a { color: var(--primary); text-decoration: none; font-weight: 600; }
75
- .user-item a:hover { color: var(--accent); }
76
- .item { background: var(--card-bg); padding: 15px; border-radius: 16px; box-shadow: var(--shadow); text-align: center; transition: var(--transition); display: flex; flex-direction: column; justify-content: space-between; }
77
- body.dark .item { background: var(--card-bg-dark); }
78
- .item:hover { transform: translateY(-5px); }
79
- .item-preview { max-width: 100%; height: 130px; object-fit: cover; border-radius: 10px; margin-bottom: 10px; cursor: pointer; display: block; margin-left: auto; margin-right: auto;}
80
- .item.folder .item-preview { object-fit: contain; font-size: 60px; color: var(--folder-color); line-height: 130px; }
81
- .item p { font-size: 0.9em; margin: 5px 0; word-break: break-all; }
82
- .item a { color: var(--primary); text-decoration: none; }
83
- .item a:hover { color: var(--accent); }
84
- .item-actions { margin-top: 10px; display: flex; flex-wrap: wrap; gap: 5px; justify-content: center; }
85
- .item-actions .btn { font-size: 0.9em; padding: 5px 10px; }
86
- .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; }
87
- .modal-content { max-width: 95%; max-height: 95%; background: #fff; padding: 10px; border-radius: 15px; overflow: auto; position: relative; }
88
- body.dark .modal-content { background: var(--card-bg-dark); }
89
- .modal img, .modal video, .modal iframe, .modal pre { max-width: 100%; max-height: 85vh; display: block; margin: auto; border-radius: 10px; }
90
- .modal iframe { width: 80vw; height: 85vh; border: none; }
91
- .modal pre { background: #eee; color: #333; padding: 15px; border-radius: 8px; white-space: pre-wrap; word-wrap: break-word; text-align: left; max-height: 85vh; overflow-y: auto;}
92
- body.dark .modal pre { background: #2b2a33; color: var(--text-dark); }
93
- .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; }
94
- body.dark .modal-close-btn { color: #555; background: rgba(255,255,255,0.2); }
95
- #progress-container { width: 100%; background: var(--glass-bg); border-radius: 10px; margin: 15px 0; display: none; position: relative; height: 20px; }
96
- #progress-bar { width: 0%; height: 100%; background: var(--primary); border-radius: 10px; transition: width 0.3s ease; }
97
- #progress-text { position: absolute; width: 100%; text-align: center; line-height: 20px; color: white; font-size: 0.9em; font-weight: bold; text-shadow: 1px 1px 1px rgba(0,0,0,0.5); }
98
- .breadcrumbs { margin-bottom: 20px; font-size: 1.1em; }
99
- .breadcrumbs a { color: var(--accent); text-decoration: none; }
100
- .breadcrumbs a:hover { text-decoration: underline; }
101
- .breadcrumbs span { margin: 0 5px; color: #aaa; }
102
- .folder-actions { margin-top: 20px; margin-bottom: 10px; display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
103
- .folder-actions input[type=text] { width: auto; flex-grow: 1; margin: 0; min-width: 150px; }
104
- .folder-actions .btn { margin: 0; flex-shrink: 0;}
105
- @media (max-width: 768px) {
106
- .file-grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
107
- .folder-actions { flex-direction: column; align-items: stretch; }
108
- .folder-actions input[type=text] { width: 100%; }
109
- .item-preview { height: 100px; }
110
- .item.folder .item-preview { font-size: 50px; line-height: 100px; }
111
- h1 { font-size: 1.8em; }
112
- .btn { padding: 12px 24px; font-size: 1em; }
113
- .item-actions .btn { padding: 4px 8px; font-size: 0.8em;}
114
- }
115
- @media (max-width: 480px) {
116
- .container { padding: 15px; }
117
- .file-grid { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 15px; }
118
- .item-preview { height: 80px; }
119
- .item.folder .item-preview { font-size: 40px; line-height: 80px; }
120
- .item p { font-size: 0.8em;}
121
- .breadcrumbs { font-size: 1em; }
122
- .btn { padding: 10px 20px; }
123
- }
124
- '''
125
-
126
- def find_node_by_id(filesystem, node_id):
127
- if not filesystem: return None, None
128
- if filesystem.get('id') == node_id:
129
- return filesystem, None
130
- queue = [(filesystem, None)]
131
- while queue:
132
- current_node, parent = queue.pop(0)
133
- if current_node.get('type') == 'folder' and 'children' in current_node:
134
- for i, child in enumerate(current_node['children']):
135
- if child.get('id') == node_id:
136
- return child, current_node
137
- if child.get('type') == 'folder':
138
- queue.append((child, current_node))
139
- return None, None
140
-
141
- def add_node(filesystem, parent_id, node_data):
142
- parent_node, _ = find_node_by_id(filesystem, parent_id)
143
- if parent_node and parent_node.get('type') == 'folder':
144
- if 'children' not in parent_node:
145
- parent_node['children'] = []
146
- parent_node['children'].append(node_data)
147
- return True
148
- return False
149
-
150
- def remove_node(filesystem, node_id):
151
- node_to_remove, parent_node = find_node_by_id(filesystem, node_id)
152
- if node_to_remove and parent_node and 'children' in parent_node:
153
- parent_node['children'] = [child for child in parent_node['children'] if child.get('id') != node_id]
154
- return True
155
- return False
156
-
157
- def get_node_path_string(filesystem, node_id):
158
- path_list = []
159
- current_id = node_id
160
- while current_id:
161
- node, parent = find_node_by_id(filesystem, current_id)
162
- if not node: break
163
- if node.get('id') != 'root':
164
- path_list.append(node.get('name', node.get('original_filename', '')))
165
- if not parent: break
166
- current_id = parent.get('id') if parent else None
167
- return " / ".join(reversed(path_list)) or "Root"
168
-
169
- def initialize_user_filesystem_tma(user_data, tma_user_id_str):
170
- if 'filesystem' not in user_data:
171
- user_data['filesystem'] = {
172
- "type": "folder", "id": "root", "name": "root", "children": []
173
- }
174
- if 'files' in user_data and isinstance(user_data['files'], list):
175
- for old_file in user_data['files']:
176
- file_id = old_file.get('id', uuid.uuid4().hex)
177
- original_filename = old_file.get('filename', 'unknown_file')
178
- name_part, ext_part = os.path.splitext(original_filename)
179
- unique_suffix = uuid.uuid4().hex[:8]
180
- unique_filename = f"{name_part}_{unique_suffix}{ext_part}"
181
- hf_path = f"cloud_files/{tma_user_id_str}/root/{unique_filename}"
182
- file_node = {
183
- 'type': 'file', 'id': file_id, 'original_filename': original_filename,
184
- 'unique_filename': unique_filename, 'path': hf_path,
185
- 'file_type': get_file_type(original_filename),
186
- 'upload_date': old_file.get('upload_date', datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
187
- }
188
- add_node(user_data['filesystem'], 'root', file_node)
189
- del user_data['files']
190
-
191
- @cache.memoize(timeout=300)
192
- def load_data():
193
- try:
194
- download_db_from_hf()
195
- with open(DATA_FILE, 'r', encoding='utf-8') as file:
196
- data = json.load(file)
197
- if not isinstance(data, dict):
198
- data = {'users': {}}
199
- data.setdefault('users', {})
200
- for tma_user_id_str, user_data_item in data['users'].items():
201
- initialize_user_filesystem_tma(user_data_item, tma_user_id_str)
202
- return data
203
- except Exception as e:
204
- logging.error(f"Error loading data: {e}")
205
- return {'users': {}}
206
-
207
- def save_data(data):
208
- try:
209
- with open(DATA_FILE, 'w', encoding='utf-8') as file:
210
- json.dump(data, file, ensure_ascii=False, indent=4)
211
- upload_db_to_hf()
212
- cache.clear()
213
- except Exception as e:
214
- logging.error(f"Error saving data: {e}")
215
- raise
216
-
217
- def upload_db_to_hf():
218
- if not HF_TOKEN_WRITE: return
219
- try:
220
- api = HfApi()
221
- api.upload_file(
222
- path_or_fileobj=DATA_FILE, path_in_repo=DATA_FILE, repo_id=REPO_ID,
223
- repo_type="dataset", token=HF_TOKEN_WRITE,
224
- commit_message=f"Backup {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
225
- )
226
- except Exception as e:
227
- logging.error(f"Error uploading database: {e}")
228
-
229
- def download_db_from_hf():
230
- if not HF_TOKEN_READ:
231
- if not os.path.exists(DATA_FILE):
232
- with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}}, f)
233
- return
234
- try:
235
- hf_hub_download(
236
- repo_id=REPO_ID, filename=DATA_FILE, repo_type="dataset",
237
- token=HF_TOKEN_READ, local_dir=".", local_dir_use_symlinks=False
238
- )
239
- except (hf_utils.RepositoryNotFoundError, hf_utils.EntryNotFoundError):
240
- if not os.path.exists(DATA_FILE):
241
- with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}}, f)
242
- except Exception as e:
243
- logging.error(f"Error downloading database: {e}")
244
- if not os.path.exists(DATA_FILE):
245
- with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}}, f)
246
-
247
- def periodic_backup():
248
- while True:
249
- upload_db_to_hf()
250
- time.sleep(1800)
251
-
252
- def get_file_type(filename):
253
- filename_lower = filename.lower()
254
- if filename_lower.endswith(('.mp4', '.mov', '.avi', '.webm', '.mkv')): return 'video'
255
- elif filename_lower.endswith(('.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg')): return 'image'
256
- elif filename_lower.endswith('.pdf'): return 'pdf'
257
- elif filename_lower.endswith('.txt'): return 'text'
258
- return 'other'
259
-
260
- def is_admin_tma():
261
- if not ADMIN_TELEGRAM_ID or ADMIN_TELEGRAM_ID == "YOUR_ADMIN_TELEGRAM_USER_ID_HERE":
262
- return False
263
- return 'telegram_user_id' in session and str(session['telegram_user_id']) == str(ADMIN_TELEGRAM_ID)
264
-
265
-
266
- def admin_browser_login_required(f):
267
- @wraps(f)
268
- def decorated_function(*args, **kwargs):
269
- if not session.get('admin_browser_logged_in'):
270
- return redirect(url_for('admin_login', next=request.url))
271
- return f(*args, **kwargs)
272
- return decorated_function
273
-
274
- TMA_ENTRY_HTML = '''
275
- <!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
276
- <title>Zeus Cloud TMA</title><script src="https://telegram.org/js/telegram-web-app.js"></script>
277
- <style>body { font-family: sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background-color: #f0f0f0; } .loading { font-size: 1.5em; }</style>
278
- </head><body><div class="loading">Загрузка приложения...</div>
279
- <script>
280
- window.Telegram.WebApp.ready();
281
- const initData = window.Telegram.WebApp.initData;
282
- const initDataUnsafe = window.Telegram.WebApp.initDataUnsafe;
283
-
284
- if (initDataUnsafe && initDataUnsafe.user) {
285
- fetch("{{ url_for('auth_via_telegram') }}", {
286
- method: 'POST',
287
- headers: { 'Content-Type': 'application/json' },
288
- body: JSON.stringify({ user: initDataUnsafe.user, initData: initData })
289
- })
290
- .then(response => response.json())
291
- .then(data => {
292
- if (data.status === 'success') {
293
- window.location.href = data.redirect_url;
294
- } else {
295
- document.body.innerHTML = `<div class="loading">Ошибка авторизации: ${data.message}</div>`;
296
- Telegram.WebApp.showAlert(data.message || "Ошибка авторизации");
297
- }
298
- })
299
- .catch(error => {
300
- document.body.innerHTML = `<div class="loading">Ошибка сети: ${error}</div>`;
301
- Telegram.WebApp.showAlert("Ошибка сети при авторизации.");
302
- });
303
- } else {
304
- document.body.innerHTML = '<div class="loading">Не удалось получить данные пользователя Telegram. Попробуйте перезапустить приложение.</div>';
305
- Telegram.WebApp.showAlert("Не удалось получить данные пользователя Telegram.");
306
- }
307
- </script></body></html>
308
- '''
309
-
310
- @app.route('/tma')
311
- def tma_entry_page():
312
- return render_template_string(TMA_ENTRY_HTML)
313
-
314
- @app.route('/')
315
- def root_redirect():
316
- return redirect(url_for('tma_entry_page'))
317
-
318
-
319
- @app.route('/auth_via_telegram', methods=['POST'])
320
- def auth_via_telegram():
321
- try:
322
- payload = request.json
323
- tg_user_data = payload.get('user')
324
-
325
- if not tg_user_data or not tg_user_data.get('id'):
326
- return jsonify({'status': 'error', 'message': 'Отсутствуют данные пользователя Telegram.'}), 400
327
-
328
- tma_user_id_str = str(tg_user_data['id'])
329
- data = load_data()
330
-
331
- if tma_user_id_str not in data['users']:
332
- data['users'][tma_user_id_str] = {
333
- 'telegram_id': tg_user_data['id'],
334
- 'telegram_username': tg_user_data.get('username'),
335
- 'first_name': tg_user_data.get('first_name'),
336
- 'last_name': tg_user_data.get('last_name'),
337
- 'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
338
- 'filesystem': {"type": "folder", "id": "root", "name": "root", "children": []}
339
- }
340
- initialize_user_filesystem_tma(data['users'][tma_user_id_str], tma_user_id_str)
341
- try:
342
- save_data(data)
343
- except Exception as e:
344
- logging.error(f"Save data error for new TMA user {tma_user_id_str}: {e}")
345
- return jsonify({'status': 'error', 'message': 'Ошибка сохранения данных нового пользователя.'}), 500
346
-
347
- session['telegram_user_id'] = tma_user_id_str
348
- display_name = tg_user_data.get('first_name') or tg_user_data.get('username') or f"User {tma_user_id_str}"
349
- session['telegram_display_name'] = display_name
350
-
351
- return jsonify({'status': 'success', 'redirect_url': url_for('tma_dashboard')})
352
-
353
- except Exception as e:
354
- logging.error(f"Error in auth_via_telegram: {e}")
355
- return jsonify({'status': 'error', 'message': 'Внутренняя ошибка сервера при авторизации.'}), 500
356
-
357
- TMA_DASHBOARD_HTML_TEMPLATE = '''
358
- <!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
359
- <title>Zeus Cloud</title><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
360
- <script src="https://telegram.org/js/telegram-web-app.js"></script>
361
- <style>''' + BASE_STYLE + '''</style></head><body class="dark"><div class="container">
362
- <h1>Zeus Cloud</h1><p>Пользователь: {{ display_name }}</p>
363
- {% with messages = get_flashed_messages(with_categories=true) %}{% if messages %}
364
- {% for category, message in messages %}<div class="flash {{ category }}">{{ message }}</div>{% endfor %}
365
- {% endif %}{% endwith %}
366
- <div class="breadcrumbs">
367
- {% for crumb in breadcrumbs %}
368
- {% if crumb.is_link %}<a href="{{ url_for('tma_dashboard', folder_id=crumb.id) }}">{{ crumb.name if crumb.id != 'root' else 'Главная' }}</a>
369
- {% else %}<span>{{ crumb.name if crumb.id != 'root' else 'Главная' }}</span>{% endif %}
370
- {% if not loop.last %}<span>/</span>{% endif %}
371
- {% endfor %}
372
- </div>
373
- <div class="folder-actions">
374
- <form method="POST" action="{{ url_for('create_folder_tma') }}" style="display: contents;">
375
- <input type="hidden" name="parent_folder_id" value="{{ current_folder_id }}">
376
- <input type="text" name="folder_name" placeholder="Имя новой папки" required>
377
- <button type="submit" class="btn folder-btn">Создать папку</button>
378
- </form>
379
- </div>
380
- <form id="upload-form" method="POST" enctype="multipart/form-data" action="{{ url_for('tma_dashboard') }}">
381
- <input type="hidden" name="current_folder_id" value="{{ current_folder_id }}">
382
- <input type="file" name="files" id="file-input" multiple required>
383
- <button type="submit" class="btn" id="upload-btn">Загрузить файлы сюда</button>
384
- </form>
385
- <div id="progress-container"><div id="progress-bar"></div><div id="progress-text">0%</div></div>
386
- <h2>Содержимое папки: {{ current_folder.name if current_folder_id != 'root' else 'Главная' }}</h2>
387
- <div class="file-grid">
388
- {% for item in items %}
389
- <div class="item {{ item.type }}">
390
- {% if item.type == 'folder' %}
391
- <a href="{{ url_for('tma_dashboard', folder_id=item.id) }}" class="item-preview" title="Перейти в папку {{ item.name }}">📁</a>
392
- <p><b>{{ item.name }}</b></p>
393
- <div class="item-actions">
394
- <a href="{{ url_for('tma_dashboard', folder_id=item.id) }}" class="btn folder-btn">Открыть</a>
395
- <form method="POST" action="{{ url_for('delete_folder_tma', folder_id=item.id) }}" style="display: inline;" onsubmit="return confirm('Удалить папку {{ item.name }}? Папку можно удалить только если она пуста.');">
396
- <input type="hidden" name="current_view_folder_id" value="{{ current_folder_id }}">
397
- <button type="submit" class="btn delete-btn">Удалить</button>
398
- </form>
399
- </div>
400
- {% elif item.type == 'file' %}
401
- {% set previewable = item.file_type in ['image', 'video', 'pdf', 'text'] %}
402
- {% if item.file_type == 'image' %}<img class="item-preview" src="{{ hf_file_url_jinja(item.path) }}" alt="{{ item.original_filename }}" loading="lazy" onclick="openModal('{{ hf_file_url_jinja(item.path) }}', '{{ item.file_type }}', '{{ item.id }}')">
403
- {% elif item.file_type == 'video' %}<video class="item-preview" preload="metadata" muted loading="lazy" onclick="openModal('{{ hf_file_url_jinja(item.path) }}', '{{ item.file_type }}', '{{ item.id }}')"><source src="{{ hf_file_url_jinja(item.path, True) }}#t=0.5" type="video/mp4"></video>
404
- {% elif item.file_type == 'pdf' %}<div class="item-preview" style="font-size: 60px; line-height: 130px; color: var(--accent); cursor: pointer;" onclick="openModal('{{ hf_file_url_jinja(item.path, True) }}', '{{ item.file_type }}', '{{ item.id }}')">📄</div>
405
- {% elif item.file_type == 'text' %}<div class="item-preview" style="font-size: 60px; line-height: 130px; color: var(--secondary); cursor: pointer;" onclick="openModal('{{ url_for('get_text_content_tma', file_id=item.id) }}', '{{ item.file_type }}', '{{ item.id }}')">📝</div>
406
- {% else %}<div class="item-preview" style="font-size: 60px; line-height: 130px; color: #aaa;">❓</div>{% endif %}
407
- <p title="{{ item.original_filename }}">{{ item.original_filename | truncate(25, True) }}</p>
408
- <p style="font-size: 0.8em; color: #888;">{{ item.upload_date }}</p>
409
- <div class="item-actions">
410
- <button type="button" class="btn download-btn" onclick="tmaDownloadFile('{{ url_for('download_tma', file_id=item.id, _external=True) }}', '{{ item.original_filename }}')">Скачать</button>
411
- {% if previewable %}<button type="button" class="btn" style="background: var(--accent);" onclick="openModal('{{ hf_file_url_jinja(item.path) if item.file_type != 'text' else url_for('get_text_content_tma', file_id=item.id) }}', '{{ item.file_type }}', '{{ item.id }}')">Просмотр</button>{% endif %}
412
- <form method="POST" action="{{ url_for('delete_file_tma', file_id=item.id) }}" style="display: inline;" onsubmit="return confirm('Вы уверены, что хотите удалить файл {{ item.original_filename }}?');">
413
- <input type="hidden" name="current_view_folder_id" value="{{ current_folder_id }}"><button type="submit" class="btn delete-btn">Удалить</button>
414
- </form>
415
- </div>
416
- {% endif %}
417
- </div>
418
- {% endfor %}
419
- {% if not items %} <p>Эта папка пуста.</p> {% endif %}
420
- </div>
421
- <a href="{{ url_for('tma_logout') }}" class="btn" style="margin-top: 20px;" id="logout-btn">Выйти (очистить сессию)</a>
422
- {% if is_tma_user_admin_flag %}
423
- <a href="{{ url_for('admin_panel') }}" class="btn" style="margin-top: 20px; background-color: var(--accent);">Админ-панель</a>
424
- {% endif %}
425
- </div>
426
- <div class="modal" id="mediaModal" onclick="closeModal(event)"><div class="modal-content" id="modalContentContainer">
427
- <span onclick="closeModalManual()" class="modal-close-btn">×</span><div id="modalContent"></div></div></div>
428
- <script>
429
- window.Telegram.WebApp.ready();
430
- document.body.classList.add('dark');
431
- const repoId = "{{ repo_id_js }}";
432
- const hfTokenRead = "{{ HF_TOKEN_READ_js or '' }}";
433
- function hfFileUrl(path, download = false) {
434
- let url = `https://huggingface.co/datasets/${repoId}/resolve/main/${path}`;
435
- if (download) url += '?download=true'; return url;
436
- }
437
- async function openModal(srcOrUrl, type, itemId) {
438
- const modal = document.getElementById('mediaModal'); const modalContent = document.getElementById('modalContent');
439
- modalContent.innerHTML = '<p>Загрузка...</p>'; modal.style.display = 'flex';
440
- try {
441
- if (type === 'image') modalContent.innerHTML = `<img src="${srcOrUrl}" alt="Просмотр изображения">`;
442
- else if (type === 'video') modalContent.innerHTML = `<video controls autoplay style='max-width: 95%; max-height: 85vh;'><source src="${srcOrUrl}" type="video/mp4">Ваш браузер не поддерживает видео.</video>`;
443
- else if (type === 'pdf') modalContent.innerHTML = `<iframe src="${srcOrUrl}" title="Просмотр PDF"></iframe>`;
444
- else if (type === 'text') {
445
- const response = await fetch(srcOrUrl); if (!response.ok) throw new Error(`Ошибка загрузки текста: ${response.statusText}`);
446
- const text = await response.text();
447
- const escapedText = text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
448
- modalContent.innerHTML = `<pre>${escapedText}</pre>`;
449
- } else modalContent.innerHTML = '<p>Предпросмотр для этого типа файла не поддерживается.</p>';
450
- } catch (error) { modalContent.innerHTML = `<p>Не удалось загрузить содержимое для предпросмотра. ${error.message}</p>`; }
451
- }
452
- function closeModal(event) { if (event.target === document.getElementById('mediaModal')) closeModalManual(); }
453
- function closeModalManual() {
454
- const modal = document.getElementById('mediaModal'); modal.style.display = 'none';
455
- const video = modal.querySelector('video'); if (video) video.pause();
456
- const iframe = modal.querySelector('iframe'); if (iframe) iframe.src = 'about:blank';
457
- document.getElementById('modalContent').innerHTML = '';
458
- }
459
-
460
- function tmaDownloadFile(downloadUrl, filename) {
461
- if (window.Telegram && window.Telegram.WebApp && Telegram.WebApp.openLink) {
462
- console.log(`Attempting TMA download for: ${filename} via URL: ${downloadUrl}`);
463
- Telegram.WebApp.openLink(downloadUrl);
464
- } else {
465
- console.warn("Telegram.WebApp.openLink not available, or not in TMA environment. Attempting fallback download.");
466
- const link = document.createElement('a');
467
- link.href = downloadUrl;
468
- link.setAttribute('download', filename);
469
- document.body.appendChild(link);
470
- link.click();
471
- document.body.removeChild(link);
472
- }
473
- }
474
-
475
- const form = document.getElementById('upload-form'); const fileInput = document.getElementById('file-input');
476
- const progressBar = document.getElementById('progress-bar'); const progressText = document.getElementById('progress-text');
477
- const progressContainer = document.getElementById('progress-container'); const uploadBtn = document.getElementById('upload-btn');
478
- if (form) {
479
- form.addEventListener('submit', function(e) {
480
- e.preventDefault(); const files = fileInput.files;
481
- if (files.length === 0) { Telegram.WebApp.showAlert('Пожалуйста, выберите файлы для загрузки.'); return; }
482
- if (files.length > 20) { Telegram.WebApp.showAlert('Максимум 20 файлов за раз!'); return; }
483
- progressContainer.style.display = 'block'; progressBar.style.width = '0%'; progressText.textContent = '0%';
484
- uploadBtn.disabled = true; uploadBtn.textContent = 'Загрузка...';
485
- const formData = new FormData(form); const xhr = new XMLHttpRequest();
486
- xhr.upload.addEventListener('progress', function(event) {
487
- if (event.lengthComputable) {
488
- const percentComplete = Math.round((event.loaded / event.total) * 100);
489
- progressBar.style.width = percentComplete + '%'; progressText.textContent = percentComplete + '%';
490
- }
491
- });
492
- xhr.addEventListener('load', function() { uploadBtn.disabled = false; uploadBtn.textContent = 'Загрузить файлы сюда'; progressContainer.style.display = 'none'; window.location.reload(); });
493
- xhr.addEventListener('error', function() { Telegram.WebApp.showAlert('Произошла ошибка во время загрузки.'); uploadBtn.disabled = false; uploadBtn.textContent = 'Загрузит�� файлы сюда'; progressContainer.style.display = 'none'; });
494
- xhr.addEventListener('abort', function() { Telegram.WebApp.showAlert('Загрузка отменена.'); uploadBtn.disabled = false; uploadBtn.textContent = 'Загрузить файлы сюда'; progressContainer.style.display = 'none'; });
495
- xhr.open('POST', form.action, true); xhr.send(formData);
496
- });
497
- }
498
- document.getElementById('logout-btn').addEventListener('click', function(e) { e.preventDefault(); window.location.href = "{{ url_for('tma_logout') }}"; });
499
- </script></body></html>
500
- '''
501
-
502
- @app.route('/tma_dashboard', methods=['GET', 'POST'])
503
- def tma_dashboard():
504
- if 'telegram_user_id' not in session:
505
- flash('Пожалуйста, авторизуйтесь через Telegram.', 'error')
506
- return redirect(url_for('tma_entry_page'))
507
-
508
- tma_user_id = session['telegram_user_id']
509
- display_name = session.get('telegram_display_name', 'Пользователь')
510
- data = load_data()
511
-
512
- if tma_user_id not in data['users']:
513
- session.clear()
514
- flash('Пользователь не найден в системе. Пожалуйста, перезапустите приложение.', 'error')
515
- return redirect(url_for('tma_entry_page'))
516
-
517
- user_data = data['users'][tma_user_id]
518
- initialize_user_filesystem_tma(user_data, tma_user_id)
519
-
520
- current_folder_id = request.args.get('folder_id', 'root')
521
- current_folder, _ = find_node_by_id(user_data['filesystem'], current_folder_id)
522
-
523
- if not current_folder or current_folder.get('type') != 'folder':
524
- flash('Папка не найдена!', 'error')
525
- current_folder_id = 'root'
526
- current_folder, _ = find_node_by_id(user_data['filesystem'], current_folder_id)
527
- if not current_folder:
528
- flash('Критическая ошибка: корневая папка не найдена.', 'error')
529
- session.clear()
530
- return redirect(url_for('tma_entry_page'))
531
-
532
- items_in_folder = sorted(current_folder.get('children', []), key=lambda x: (x['type'] != 'folder', x.get('name', x.get('original_filename', '')).lower()))
533
-
534
- if request.method == 'POST':
535
- if not HF_TOKEN_WRITE:
536
- flash('Загрузка невозможна: токен для записи не настроен.', 'error')
537
- return redirect(url_for('tma_dashboard', folder_id=current_folder_id))
538
-
539
- files = request.files.getlist('files')
540
- if not files or all(not f.filename for f in files):
541
- flash('Файлы для загрузки не выбраны.', 'error')
542
- return redirect(url_for('tma_dashboard', folder_id=current_folder_id))
543
- if len(files) > 20:
544
- flash('Максимум 20 файлов за раз!', 'error')
545
- return redirect(url_for('tma_dashboard', folder_id=current_folder_id))
546
-
547
- target_folder_id = request.form.get('current_folder_id', 'root')
548
- target_folder_node, _ = find_node_by_id(user_data['filesystem'], target_folder_id)
549
- if not target_folder_node or target_folder_node.get('type') != 'folder':
550
- flash('Целевая папка для загрузки не найдена!', 'error')
551
- return redirect(url_for('tma_dashboard'))
552
-
553
- api = HfApi()
554
- uploaded_count = 0
555
- errors_list = []
556
- for file_obj in files:
557
- if file_obj and file_obj.filename:
558
- original_filename = secure_filename(file_obj.filename)
559
- name_part, ext_part = os.path.splitext(original_filename)
560
- unique_suffix = uuid.uuid4().hex[:8]
561
- unique_filename = f"{name_part}_{unique_suffix}{ext_part}"
562
- file_id = uuid.uuid4().hex
563
- hf_path = f"cloud_files/{tma_user_id}/{target_folder_id}/{unique_filename}"
564
- temp_path = os.path.join(UPLOAD_FOLDER, f"{file_id}_{unique_filename}")
565
- try:
566
- file_obj.save(temp_path)
567
- api.upload_file(
568
- path_or_fileobj=temp_path, path_in_repo=hf_path, repo_id=REPO_ID,
569
- repo_type="dataset", token=HF_TOKEN_WRITE,
570
- commit_message=f"UserTMA {tma_user_id} uploaded {original_filename} to folder {target_folder_id}"
571
- )
572
- file_info = {
573
- 'type': 'file', 'id': file_id, 'original_filename': original_filename,
574
- 'unique_filename': unique_filename, 'path': hf_path,
575
- 'file_type': get_file_type(original_filename),
576
- 'upload_date': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
577
- }
578
- if add_node(user_data['filesystem'], target_folder_id, file_info):
579
- uploaded_count += 1
580
- else:
581
- errors_list.append(f"Ошибка добавления метаданных дл�� {original_filename}.")
582
- try: api.delete_file(path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE)
583
- except Exception as del_err: logging.error(f"Failed to delete orphaned file {hf_path} from HF Hub: {del_err}")
584
- except Exception as e:
585
- errors_list.append(f"Ошибка загрузки файла {original_filename}: {e}")
586
- finally:
587
- if os.path.exists(temp_path): os.remove(temp_path)
588
- if uploaded_count > 0:
589
- try:
590
- save_data(data)
591
- flash(f'{uploaded_count} файл(ов) успешно загружено!')
592
- except Exception as e:
593
- flash('Файлы загружены, но ошибка сохранения метаданных.', 'error')
594
- if errors_list:
595
- for error_msg in errors_list: flash(error_msg, 'error')
596
- return redirect(url_for('tma_dashboard', folder_id=target_folder_id))
597
-
598
- breadcrumbs = []
599
- temp_id = current_folder_id
600
- while temp_id:
601
- node, parent_node_bc = find_node_by_id(user_data['filesystem'], temp_id)
602
- if not node: break
603
- is_link = (node['id'] != current_folder_id)
604
- breadcrumbs.append({'id': node['id'], 'name': node.get('name', 'Root'), 'is_link': is_link})
605
- if not parent_node_bc: break
606
- temp_id = parent_node_bc.get('id')
607
- breadcrumbs.reverse()
608
-
609
- return render_template_string(TMA_DASHBOARD_HTML_TEMPLATE,
610
- display_name=display_name, items=items_in_folder,
611
- current_folder_id=current_folder_id, current_folder=current_folder,
612
- breadcrumbs=breadcrumbs, repo_id_js=REPO_ID, HF_TOKEN_READ_js=HF_TOKEN_READ,
613
- hf_file_url_jinja=lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{path}{'?download=true' if download else ''}",
614
- is_tma_user_admin_flag=is_admin_tma())
615
-
616
-
617
- @app.route('/create_folder_tma', methods=['POST'])
618
- def create_folder_tma():
619
- if 'telegram_user_id' not in session:
620
- flash('Не авторизован', 'error')
621
- return redirect(url_for('tma_entry_page'))
622
- tma_user_id = session['telegram_user_id']
623
- data = load_data()
624
- user_data = data['users'].get(tma_user_id)
625
- if not user_data:
626
- flash('Пользователь не найден', 'error')
627
- return redirect(url_for('tma_entry_page'))
628
-
629
- parent_folder_id = request.form.get('parent_folder_id', 'root')
630
- folder_name = request.form.get('folder_name', '').strip()
631
- if not folder_name:
632
- flash('Имя папки не может быть пустым!', 'error')
633
- return redirect(url_for('tma_dashboard', folder_id=parent_folder_id))
634
-
635
- folder_id = uuid.uuid4().hex
636
- folder_data = {'type': 'folder', 'id': folder_id, 'name': folder_name, 'children': []}
637
- if add_node(user_data['filesystem'], parent_folder_id, folder_data):
638
- try:
639
- save_data(data)
640
- flash(f'Папка "{folder_name}" успешно создана.')
641
- except Exception as e: flash('Ошибка сохранения данных при создании папки.', 'error')
642
- else:
643
- flash('Не удалось найти родительскую папку.', 'error')
644
- return redirect(url_for('tma_dashboard', folder_id=parent_folder_id))
645
-
646
- @app.route('/download_tma/<file_id>')
647
- def download_tma(file_id):
648
- current_tma_user_id = session.get('telegram_user_id')
649
- is_browser_admin_session = session.get('admin_browser_logged_in', False)
650
-
651
- data = load_data()
652
- file_node = None
653
-
654
- if is_browser_admin_session:
655
- for uid_str_iter, udata_iter in data.get('users', {}).items():
656
- node, _ = find_node_by_id(udata_iter.get('filesystem', {}), file_id)
657
- if node and node.get('type') == 'file':
658
- file_node = node
659
- break
660
- elif current_tma_user_id:
661
- user_data = data['users'].get(current_tma_user_id)
662
- if user_data:
663
- file_node, _ = find_node_by_id(user_data['filesystem'], file_id)
664
-
665
- if not file_node and is_admin_tma():
666
- for uid_str_iter, udata_iter in data.get('users', {}).items():
667
- node, _ = find_node_by_id(udata_iter.get('filesystem', {}), file_id)
668
- if node and node.get('type') == 'file':
669
- file_node = node
670
- break
671
- else:
672
- flash('Пожалуйста, авторизуйтесь.', 'error')
673
- # For direct access to download URL without session, a proper response is needed
674
- if request.referrer: # if accessed via a click in-app
675
- return redirect(url_for('tma_entry_page'))
676
- else: # if direct URL access
677
- return Response("Unauthorized", status=401, mimetype='text/plain')
678
-
679
-
680
- redirect_url_fallback = url_for('tma_dashboard')
681
- if is_browser_admin_session:
682
- redirect_url_fallback = url_for('admin_panel')
683
-
684
- redirect_url = request.referrer or redirect_url_fallback
685
-
686
-
687
- if not file_node or file_node.get('type') != 'file':
688
- flash('Файл не найден или доступ запрещен!', 'error')
689
- return redirect(redirect_url)
690
-
691
- hf_path = file_node.get('path')
692
- original_filename = file_node.get('original_filename', 'downloaded_file')
693
- if not hf_path:
694
- flash('Ошибка: Путь к файлу не найден.', 'error')
695
- return redirect(redirect_url)
696
-
697
- file_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{hf_path}?download=true"
698
- try:
699
- req_headers = {}
700
- if HF_TOKEN_READ: req_headers["authorization"] = f"Bearer {HF_TOKEN_READ}"
701
-
702
- r = requests.get(file_url, headers=req_headers, stream=True)
703
- r.raise_for_status()
704
-
705
- def generate_chunks():
706
- for chunk in r.iter_content(chunk_size=8192):
707
- yield chunk
708
-
709
- mimetype = 'application/octet-stream'
710
- if '.' in original_filename:
711
- ext = original_filename.rsplit('.', 1)[1].lower()
712
- content_types = {
713
- 'pdf': 'application/pdf', 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg',
714
- 'png': 'image/png', 'gif': 'image/gif', 'txt': 'text/plain',
715
- 'mp4': 'video/mp4', 'mov': 'video/quicktime', 'avi': 'video/x-msvideo',
716
- 'zip': 'application/zip', 'rar': 'application/x-rar-compressed',
717
- }
718
- mimetype = content_types.get(ext, 'application/octet-stream')
719
-
720
- resp = Response(generate_chunks(), mimetype=mimetype)
721
- resp.headers['Content-Disposition'] = f'attachment; filename="{original_filename}"'
722
- # Add cache control headers to prevent aggressive caching if files can be updated
723
- resp.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
724
- resp.headers['Pragma'] = 'no-cache'
725
- resp.headers['Expires'] = '0'
726
- return resp
727
-
728
- except requests.exceptions.RequestException as e:
729
- logging.error(f"Error downloading file {original_filename} (ID: {file_id}) from HF: {e}")
730
- flash(f'Ошибка скачивания файла: {e}', 'error')
731
- except Exception as e:
732
- logging.error(f"Internal error during download {original_filename} (ID: {file_id}): {e}")
733
- flash(f'Внутренняя ошибка при скачивании: {e}', 'error')
734
- return redirect(redirect_url)
735
-
736
-
737
- @app.route('/delete_file_tma/<file_id>', methods=['POST'])
738
- def delete_file_tma(file_id):
739
- if 'telegram_user_id' not in session:
740
- flash('Пожалуйста, авторизуйтесь.', 'error')
741
- return redirect(url_for('tma_entry_page'))
742
- tma_user_id = session['telegram_user_id']
743
- data = load_data()
744
- user_data = data['users'].get(tma_user_id)
745
- if not user_data:
746
- session.clear(); flash('Пользователь не найден.', 'error'); return redirect(url_for('tma_entry_page'))
747
-
748
- file_node, _ = find_node_by_id(user_data['filesystem'], file_id)
749
- current_view_folder_id = request.form.get('current_view_folder_id', 'root')
750
- if not file_node or file_node.get('type') != 'file':
751
- flash('Файл не найден.', 'error')
752
- return redirect(url_for('tma_dashboard', folder_id=current_view_folder_id))
753
-
754
- hf_path = file_node.get('path')
755
- original_filename = file_node.get('original_filename', 'файл')
756
- if not hf_path:
757
- if remove_node(user_data['filesystem'], file_id):
758
- try: save_data(data); flash(f'Метаданные файла {original_filename} удалены (путь отсутствовал).')
759
- except Exception as e: flash('Ошибка сохранения данных после удаления метаданных.', 'error')
760
- return redirect(url_for('tma_dashboard', folder_id=current_view_folder_id))
761
-
762
- if not HF_TOKEN_WRITE:
763
- flash('Удаление невозможно: токен для записи не настроен.', 'error')
764
- return redirect(url_for('tma_dashboard', folder_id=current_view_folder_id))
765
- try:
766
- api = HfApi()
767
- api.delete_file(path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE)
768
- if remove_node(user_data['filesystem'], file_id):
769
- try: save_data(data); flash(f'Файл {original_filename} успешно удален!')
770
- except Exception as e: flash('Файл удален с сервера, но ошибка обновления базы.', 'error')
771
- else: flash('Файл удален с сервера, но не найден в базе для удаления.', 'error')
772
- except hf_utils.EntryNotFoundError:
773
- if remove_node(user_data['filesystem'], file_id):
774
- try: save_data(data); flash(f'Файл {original_filename} не найден на сервере, удален из базы.')
775
- except Exception as e: flash('Ошибка сохранения (файл ��е на сервере).', 'error')
776
- except Exception as e:
777
- flash(f'Ошибка удаления файла {original_filename}: {e}', 'error')
778
- return redirect(url_for('tma_dashboard', folder_id=current_view_folder_id))
779
-
780
- @app.route('/delete_folder_tma/<folder_id>', methods=['POST'])
781
- def delete_folder_tma(folder_id):
782
- if 'telegram_user_id' not in session:
783
- flash('Пожалуйста, авторизуйтесь.', 'error'); return redirect(url_for('tma_entry_page'))
784
- if folder_id == 'root':
785
- flash('Нельзя удалить корневую папку!', 'error'); return redirect(url_for('tma_dashboard'))
786
- tma_user_id = session['telegram_user_id']
787
- data = load_data()
788
- user_data = data['users'].get(tma_user_id)
789
- if not user_data:
790
- session.clear(); flash('Пользователь не найден.', 'error'); return redirect(url_for('tma_entry_page'))
791
-
792
- folder_node, parent_node = find_node_by_id(user_data['filesystem'], folder_id)
793
- current_view_folder_id = request.form.get('current_view_folder_id', 'root')
794
- if not folder_node or folder_node.get('type') != 'folder' or not parent_node:
795
- flash('Папка не найдена.', 'error')
796
- return redirect(url_for('tma_dashboard', folder_id=current_view_folder_id))
797
- if folder_node.get('children'):
798
- flash(f'Папку "{folder_node.get("name")}" можно удалить только если она пуста.', 'error')
799
- return redirect(url_for('tma_dashboard', folder_id=current_view_folder_id))
800
- if remove_node(user_data['filesystem'], folder_id):
801
- try: save_data(data); flash(f'Папка "{folder_node.get("name")}" удалена.')
802
- except Exception as e: flash('Ошибка сохранения после удаления папки.', 'error')
803
- else: flash('Не удалось удалить папку.', 'error')
804
- return redirect(url_for('tma_dashboard', folder_id=parent_node.get('id', 'root')))
805
-
806
- @app.route('/get_text_content_tma/<file_id>')
807
- def get_text_content_tma(file_id):
808
- current_tma_user_id = session.get('telegram_user_id')
809
- is_browser_admin_session = session.get('admin_browser_logged_in', False)
810
-
811
- data = load_data()
812
- file_node = None
813
-
814
- if is_browser_admin_session:
815
- for uid_str, udata_iter in data.get('users', {}).items():
816
- node, _ = find_node_by_id(udata_iter.get('filesystem', {}), file_id)
817
- if node and node.get('type') == 'file' and node.get('file_type') == 'text':
818
- file_node = node; break
819
- elif current_tma_user_id:
820
- user_data = data['users'].get(current_tma_user_id)
821
- if user_data:
822
- file_node, _ = find_node_by_id(user_data['filesystem'], file_id)
823
-
824
- if not file_node and is_admin_tma():
825
- for uid_str, udata_iter in data.get('users', {}).items():
826
- node, _ = find_node_by_id(udata_iter.get('filesystem', {}), file_id)
827
- if node and node.get('type') == 'file' and node.get('file_type') == 'text':
828
- file_node = node; break
829
- else:
830
- return Response("Не авторизован", status=401)
831
-
832
- if not file_node or file_node.get('type') != 'file' or file_node.get('file_type') != 'text':
833
- return Response("Текстовый файл не найден", status=404)
834
- hf_path = file_node.get('path')
835
- if not hf_path: return Response("Ошибка: путь к файлу отсутствует", status=500)
836
- file_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{hf_path}?download=true"
837
- try:
838
- req_headers = {};
839
- if HF_TOKEN_READ: req_headers["authorization"] = f"Bearer {HF_TOKEN_READ}"
840
- response = requests.get(file_url, headers=req_headers)
841
- response.raise_for_status()
842
- if len(response.content) > 1 * 1024 * 1024: return Response("Файл слишком большой.", status=413)
843
- try: text_content = response.content.decode('utf-8')
844
- except UnicodeDecodeError: text_content = response.content.decode('latin-1', errors='ignore')
845
- return Response(text_content, mimetype='text/plain')
846
- except Exception as e: return Response(f"Ошибка загрузки: {e}", status=502)
847
-
848
- @app.route('/tma_logout')
849
- def tma_logout():
850
- session.pop('telegram_user_id', None)
851
- session.pop('telegram_display_name', None)
852
- flash('Вы вышли из сессии приложения.')
853
- return redirect(url_for('tma_entry_page'))
854
-
855
-
856
- ADMIN_LOGIN_HTML_TEMPLATE = '''
857
- <!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
858
- <title>Admin Login</title><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
859
- <style>
860
- {{ admin_base_style_css }}
861
- body { display: flex; justify-content: center; align-items: center; height: 100vh; background: var(--background-dark); }
862
- .login-container { text-align: center; padding: 40px; background: var(--card-bg-dark); border-radius: 20px; box-shadow: var(--shadow); width: 100%; max-width: 400px; }
863
- h1 { color: var(--primary); margin-bottom: 20px; }
864
- input[type="text"], input[type="password"] { background: rgba(255,255,255,0.1); color: var(--text-dark); margin-bottom: 20px; }
865
- .btn { background: var(--accent); width: 100%; }
866
- </style>
867
- </head><body class="dark">
868
- <div class="login-container">
869
- <h1>Admin Login</h1>
870
- {% with messages = get_flashed_messages(with_categories=true) %}
871
- {% if messages %}
872
- {% for category, message in messages %}
873
- <div class="flash {{ category }}">{{ message }}</div>
874
- {% endfor %}
875
- {% endif %}
876
- {% endwith %}
877
- <form method="POST" action="{{ url_for('admin_login') }}">
878
- <input type="hidden" name="next" value="{{ request.args.get('next', '') }}">
879
- <input type="text" name="username" placeholder="Username" required>
880
- <input type="password" name="password" placeholder="Password" required>
881
- <button type="submit" class="btn">Login</button>
882
- </form>
883
- </div>
884
- </body></html>
885
- '''
886
-
887
- @app.route('/admin/login', methods=['GET', 'POST'])
888
- def admin_login():
889
- if request.method == 'POST':
890
- username = request.form.get('username')
891
- password = request.form.get('password')
892
- next_url = request.form.get('next')
893
-
894
- if username == ADMIN_USERNAME and password == ADMIN_PASSWORD:
895
- session['admin_browser_logged_in'] = True
896
- flash('Успешный вход в админ-панель.', 'success')
897
- if next_url:
898
- return redirect(next_url)
899
- return redirect(url_for('admin_panel'))
900
- else:
901
- flash('Неверное имя пользователя или пароль.', 'error')
902
- return render_template_string(ADMIN_LOGIN_HTML_TEMPLATE, admin_base_style_css=BASE_STYLE)
903
-
904
- @app.route('/admin/logout')
905
- def admin_logout():
906
- session.pop('admin_browser_logged_in', None)
907
- flash('Вы вышли из админ-панели.', 'success')
908
- return redirect(url_for('admin_login'))
909
-
910
- ADMIN_PANEL_HTML_TEMPLATE = '''
911
- <!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
912
- <title>Админ-панель</title><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
913
- <style>''' + BASE_STYLE + '''</style></head><body class="dark"><div class="container"><h1>Админ-панель</h1>
914
- <a href="{{ url_for('admin_logout') }}" class="btn" style="margin-bottom:20px; background-color: var(--accent);">Выйти из админ-панели</a>
915
- <a href="{{ url_for('tma_dashboard') }}" class="btn" style="margin-bottom:20px;">В приложение (если TMA сессия есть)</a>
916
- {% with messages = get_flashed_messages(with_categories=true) %}{% if messages %}{% for category, message in messages %}<div class="flash {{ category }}">{{ message }}</div>{% endfor %}{% endif %}{% endwith %}
917
- <h2>Пользователи</h2><div class="user-list">
918
- {% for user in user_details %}
919
- <div class="user-item">
920
- <a href="{{ url_for('admin_user_files', tma_user_id_str=user.id_key) }}">{{ user.display_name }} (ID: {{user.id_key}})</a>
921
- <p>Зарегистрирован: {{ user.created_at }}</p><p>Файлов: {{ user.file_count }}</p>
922
- <form method="POST" action="{{ url_for('admin_delete_user', tma_user_id_str=user.id_key) }}" style="display: inline; margin-left: 10px;" onsubmit="return confirm('УДАЛИТЬ пользователя {{ user.display_name }} и ВСЕ его файлы? НЕОБРАТИМО!');">
923
- <button type="submit" class="btn delete-btn" style="padding: 5px 10px; font-size: 0.9em;">Удалить</button>
924
- </form>
925
- </div>
926
- {% else %}<p>Пользователей нет.</p>{% endfor %}</div></div></body></html>'''
927
-
928
- @app.route('/admhosto')
929
- @admin_browser_login_required
930
- def admin_panel():
931
- data = load_data()
932
- users = data.get('users', {})
933
- user_details = []
934
- for tma_id_str, udata in users.items():
935
- file_count = 0
936
- q = [udata.get('filesystem', {}).get('children', [])]
937
- while q:
938
- current_level = q.pop(0)
939
- for item in current_level:
940
- if item.get('type') == 'file': file_count += 1
941
- elif item.get('type') == 'folder' and 'children' in item: q.append(item.get('children', []))
942
- user_details.append({
943
- 'id_key': tma_id_str,
944
- 'display_name': udata.get('first_name', udata.get('telegram_username', f"User {tma_id_str}")),
945
- 'created_at': udata.get('created_at', 'N/A'), 'file_count': file_count
946
- })
947
- user_details.sort(key=lambda x: x.get('created_at', ''), reverse=True)
948
- return render_template_string(ADMIN_PANEL_HTML_TEMPLATE, user_details=user_details)
949
-
950
- ADMIN_USER_FILES_HTML_TEMPLATE = '''
951
- <!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
952
- <title>Файлы {{ display_name_admin_view }}</title><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
953
- <style>''' + BASE_STYLE + '''
954
- .file-item { background: var(--card-bg-dark); padding: 15px; border-radius: 16px; box-shadow: var(--shadow); transition: var(--transition); display: flex; flex-direction: column; justify-content: space-between; }
955
- .file-preview { max-width: 100%; height: 100px; object-fit: contain; border-radius: 10px; margin-bottom: 10px; display: block; margin-left: auto; margin-right: auto; }
956
- .admin-file-actions { margin-top: 10px; display: flex; flex-wrap: wrap; gap: 5px; justify-content: center; }
957
- .admin-file-actions .btn { font-size: 0.8em; padding: 4px 8px; margin: 0; }
958
- </style></head><body class="dark"><div class="container"><h1>Файлы пользователя: {{ display_name_admin_view }} (ID: {{ tma_user_id_str_admin_view }})</h1>
959
- <a href="{{ url_for('admin_panel') }}" class="btn" style="margin-bottom: 20px;">Назад к пользователям</a>
960
- {% with messages = get_flashed_messages(with_categories=true) %}{% if messages %}{% for category, message in messages %}<div class="flash {{ category }}">{{ message }}</div>{% endfor %}{% endif %}{% endwith %}
961
- <div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 20px;">
962
- {% for file_item in files %}
963
- <div class="file-item"><div>
964
- {% if file_item.file_type == 'image' %} <img class="file-preview" src="{{ hf_file_url_jinja(file_item.path) }}" loading="lazy" onclick="openModalAdmin('{{ hf_file_url_jinja(file_item.path) }}', '{{ file_item.file_type }}', '{{ file_item.id }}')" style="cursor: pointer;">
965
- {% elif file_item.file_type == 'video' %} <video class="file-preview" preload="metadata" muted onclick="openModalAdmin('{{ hf_file_url_jinja(file_item.path) }}', '{{ file_item.file_type }}', '{{ file_item.id }}')" style="cursor: pointer;"><source src="{{ hf_file_url_jinja(file_item.path, True) }}#t=0.5"></video>
966
- {% elif file_item.file_type == 'pdf' %} <div class="file-preview" style="font-size: 40px; line-height: 100px; text-align: center; color: var(--accent); cursor: pointer;" onclick="openModalAdmin('{{ hf_file_url_jinja(file_item.path, True) }}', '{{ file_item.file_type }}', '{{ file_item.id }}')">📄</div>
967
- {% elif file_item.file_type == 'text' %} <div class="file-preview" style="font-size: 40px; line-height: 100px; text-align: center; color: var(--secondary); cursor: pointer;" onclick="openModalAdmin('{{ url_for('get_text_content_tma', file_id=file_item.id) }}', '{{ file_item.file_type }}', '{{ file_item.id }}')">📝</div>
968
- {% else %} <div class="file-preview" style="font-size: 40px; line-height: 100px; text-align: center; color: #aaa;">❓</div> {% endif %}
969
- <p title="{{ file_item.original_filename }}"><b>{{ file_item.original_filename | truncate(30) }}</b></p>
970
- <p style="font-size: 0.8em; color: #888;">В папке: {{ file_item.parent_path_str }}</p>
971
- <p style="font-size: 0.8em; color: #888;">Загружен: {{ file_item.upload_date }}</p>
972
- <p style="font-size: 0.7em; color: #ccc;">ID: {{ file_item.id }}</p>
973
- <p style="font-size: 0.7em; color: #ccc; word-break: break-all;">Path: {{ file_item.path }}</p>
974
- </div><div class="admin-file-actions">
975
- <a href="{{ url_for('download_tma', file_id=file_item.id) }}" class="btn download-btn" download="{{ file_item.original_filename }}">Скачать</a>
976
- {% set previewable = file_item.file_type in ['image', 'video', 'pdf', 'text'] %}
977
- {% if previewable %}<button type="button" class="btn" style="background: var(--accent);" onclick="openModalAdmin('{{ hf_file_url_jinja(file_item.path) if file_item.file_type != 'text' else url_for('get_text_content_tma', file_id=file_item.id) }}', '{{ file_item.file_type }}', '{{ file_item.id }}')">Просмотр</button>{% endif %}
978
- <form method="POST" action="{{ url_for('admin_delete_file', tma_user_id_str_form=tma_user_id_str_admin_view, file_id=file_item.id) }}" style="display: inline-block;" onsubmit="return confirm('Удалить файл {{ file_item.original_filename }}?');">
979
- <button type="submit" class="btn delete-btn">Удалить</button></form>
980
- </div></div>{% else %} <p>У пользователя нет файлов.</p> {% endfor %}</div></div>
981
- <div class="modal" id="mediaModalAdmin" onclick="closeModalAdminEv(event)"><div class="modal-content" id="modalContentContainerAdmin">
982
- <span onclick="closeModalAdminManual()" class="modal-close-btn">×</span><div id="modalContentAdmin"></div></div></div>
983
- <script>
984
- const repoIdJs = "{{ repo_id_js_admin }}";
985
- function hfFileUrlAdmin(path, download = false) { let url = `https://huggingface.co/datasets/${repoIdJs}/resolve/main/${path}`; if (download) url += '?download=true'; return url; }
986
- async function openModalAdmin(srcOrUrl, type, itemId) {
987
- const modal = document.getElementById('mediaModalAdmin'); const modalContent = document.getElementById('modalContentAdmin');
988
- modalContent.innerHTML = '<p>Загрузка...</p>'; modal.style.display = 'flex';
989
- try {
990
- if (type === 'image') modalContent.innerHTML = `<img src="${srcOrUrl}" alt="Просмотр изображения">`;
991
- else if (type === 'video') modalContent.innerHTML = `<video controls autoplay style='max-width: 95%; max-height: 85vh;'><source src="${srcOrUrl}" type="video/mp4"></video>`;
992
- else if (type === 'pdf') modalContent.innerHTML = `<iframe src="${srcOrUrl}" title="Просмотр PDF"></iframe>`;
993
- else if (type === 'text') { const response = await fetch(srcOrUrl); if (!response.ok) throw new Error('Network response was not ok for text file.'); const text = await response.text(); const esc = text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); modalContent.innerHTML = `<pre>${esc}</pre>`;}
994
- else modalContent.innerHTML = '<p>Предпросмотр не поддерживается.</p>';
995
- } catch (error) { modalContent.innerHTML = `<p>Не удалось загрузить содержимое для предпросмотра. ${error.message}</p>`;}
996
- }
997
- function closeModalAdminEv(event) { if (event.target === document.getElementById('mediaModalAdmin')) closeModalAdminManual(); }
998
- function closeModalAdminManual() {
999
- const modal = document.getElementById('mediaModalAdmin'); modal.style.display = 'none';
1000
- const video = modal.querySelector('video'); if (video) video.pause();
1001
- const iframe = modal.querySelector('iframe'); if (iframe) iframe.src = 'about:blank';
1002
- document.getElementById('modalContentAdmin').innerHTML = '';
1003
- }
1004
- </script></body></html>'''
1005
-
1006
- @app.route('/admhosto/user/<tma_user_id_str>')
1007
- @admin_browser_login_required
1008
- def admin_user_files(tma_user_id_str):
1009
- data = load_data()
1010
- user_data = data.get('users', {}).get(tma_user_id_str)
1011
- if not user_data:
1012
- flash(f'Пользователь ID {tma_user_id_str} не найден.', 'error'); return redirect(url_for('admin_panel'))
1013
-
1014
- all_files = []
1015
- def collect_files_admin(folder, current_path_id='root'):
1016
- parent_path_str = get_node_path_string(user_data['filesystem'], current_path_id)
1017
- for item in folder.get('children', []):
1018
- if item.get('type') == 'file': item['parent_path_str'] = parent_path_str; all_files.append(item)
1019
- elif item.get('type') == 'folder': collect_files_admin(item, item.get('id'))
1020
- collect_files_admin(user_data.get('filesystem', {}))
1021
- all_files.sort(key=lambda x: x.get('upload_date', ''), reverse=True)
1022
-
1023
- display_name_admin_view = user_data.get('first_name', user_data.get('telegram_username', f"User {tma_user_id_str}"))
1024
- return render_template_string(ADMIN_USER_FILES_HTML_TEMPLATE,
1025
- tma_user_id_str_admin_view=tma_user_id_str, display_name_admin_view=display_name_admin_view, files=all_files,
1026
- repo_id_js_admin=REPO_ID,
1027
- hf_file_url_jinja=lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{path}{'?download=true' if download else ''}")
1028
-
1029
- @app.route('/admhosto/delete_user/<tma_user_id_str>', methods=['POST'])
1030
- @admin_browser_login_required
1031
- def admin_delete_user(tma_user_id_str):
1032
- if not HF_TOKEN_WRITE:
1033
- flash('Удаление невозможно: токен для записи не настроен.', 'error'); return redirect(url_for('admin_panel'))
1034
- data = load_data()
1035
- if tma_user_id_str not in data['users']:
1036
- flash('Пользователь не найден!', 'error'); return redirect(url_for('admin_panel'))
1037
- try:
1038
- api = HfApi()
1039
- user_folder_path_on_hf = f"cloud_files/{tma_user_id_str}"
1040
- api.delete_folder(folder_path=user_folder_path_on_hf, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, ignore_patterns=[".keep"])
1041
- except hf_utils.HfHubHTTPError as e:
1042
- if e.response.status_code != 404:
1043
- flash(f'Ошибка удаления файлов пользователя {tma_user_id_str} с сервера: {e}. Пользователь из базы не удален.', 'error'); return redirect(url_for('admin_panel'))
1044
- logging.info(f"Folder {user_folder_path_on_hf} not found on HF Hub for user {tma_user_id_str} or was already empty, proceeding with DB deletion.")
1045
- except Exception as e:
1046
- flash(f'Ошибка удаления файлов пользователя {tma_user_id_str} с сервера: {e}. Пользователь из базы не удален.', 'error'); return redirect(url_for('admin_panel'))
1047
-
1048
- try:
1049
- del data['users'][tma_user_id_str]
1050
- save_data(data)
1051
- flash(f'Пользователь {tma_user_id_str} и его файлы (если были) удалены.')
1052
- except Exception as e:
1053
- flash(f'Файлы на сервере могли быть удалены, но произошла ошибка удаления пользователя из базы данных: {e}', 'error')
1054
- return redirect(url_for('admin_panel'))
1055
-
1056
-
1057
- @app.route('/admhosto/delete_file/<tma_user_id_str_form>/<file_id>', methods=['POST'])
1058
- @admin_browser_login_required
1059
- def admin_delete_file(tma_user_id_str_form, file_id):
1060
- if not HF_TOKEN_WRITE:
1061
- flash('Удаление невозможно: токен для записи не настроен.', 'error'); return redirect(url_for('admin_user_files', tma_user_id_str=tma_user_id_str_form))
1062
- data = load_data()
1063
- user_data = data.get('users', {}).get(tma_user_id_str_form)
1064
- if not user_data:
1065
- flash(f'Пользователь {tma_user_id_str_form} не найден.', 'error'); return redirect(url_for('admin_panel'))
1066
-
1067
- file_node, _ = find_node_by_id(user_data['filesystem'], file_id)
1068
- if not file_node or file_node.get('type') != 'file':
1069
- flash('Файл не найден в базе данных этого пользователя.', 'error'); return redirect(url_for('admin_user_files', tma_user_id_str=tma_user_id_str_form))
1070
-
1071
- hf_path = file_node.get('path')
1072
- original_filename = file_node.get('original_filename', 'файл')
1073
-
1074
- if not hf_path:
1075
- if remove_node(user_data['filesystem'], file_id):
1076
- try: save_data(data); flash(f'Метаданные файла {original_filename} удалены из базы (путь к файлу на сервере отсутствовал).')
1077
- except Exception as e: flash('Ошибка сохранения данных после удаления метаданных (путь отсутствовал).', 'error')
1078
- return redirect(url_for('admin_user_files', tma_user_id_str=tma_user_id_str_form))
1079
-
1080
- try:
1081
- api = HfApi()
1082
- api.delete_file(path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE)
1083
- if remove_node(user_data['filesystem'], file_id):
1084
- try: save_data(data); flash(f'Файл {original_filename} успешно удален с сервера и из базы данных!')
1085
- except Exception as e: flash('Файл удален с сервера, но произошла ошибка при обновлении базы данных.', 'error')
1086
- else:
1087
- flash(f'Файл {original_filename} удален с сервера, но не найден в структуре папок пользователя для удаления из базы.', 'error')
1088
- except hf_utils.EntryNotFoundError:
1089
- flash(f'Файл {original_filename} не найден на сервере. Удаляем из базы данных.')
1090
- if remove_node(user_data['filesystem'], file_id):
1091
- try: save_data(data); flash(f'Запись о файле {original_filename} удалена из базы.')
1092
- except Exception as e: flash('Ошибка сохранения данных после удаления записи о файле (файл не на сервере).', 'error')
1093
- except Exception as e:
1094
- flash(f'Ошибка при удалении файла {original_filename}: {e}', 'error')
1095
- return redirect(url_for('admin_user_files', tma_user_id_str=tma_user_id_str_form))
1096
-
1097
-
1098
- if __name__ == '__main__':
1099
- if not HF_TOKEN_WRITE: logging.warning("HF_TOKEN (write access) is not set. Uploads/deletions/backups will fail.")
1100
- if not HF_TOKEN_READ: logging.warning("HF_TOKEN_READ is not set. Using HF_TOKEN. Downloads/previews might fail for private repos if HF_TOKEN also lacks read.")
1101
- if ADMIN_TELEGRAM_ID == "YOUR_ADMIN_TELEGRAM_USER_ID_HERE": logging.warning("ADMIN_TELEGRAM_ID is not set. TMA admin features might not work as expected for specific users.")
1102
- if ADMIN_USERNAME == "admin" and ADMIN_PASSWORD == "zeusadminpass":
1103
- logging.warning("Using default admin username/password for browser admin panel. Please change ADMIN_USERNAME and ADMIN_PASSWORD environment variables.")
1104
-
1105
-
1106
- if HF_TOKEN_WRITE:
1107
- download_db_from_hf()
1108
- threading.Thread(target=periodic_backup, daemon=True).start()
1109
- elif HF_TOKEN_READ:
1110
- download_db_from_hf()
1111
- else:
1112
- if not os.path.exists(DATA_FILE):
1113
- with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}}, f)
1114
-
1115
- app.run(debug=False, host='0.0.0.0', port=7860)