Eluza133 commited on
Commit
fb4a82a
·
verified ·
1 Parent(s): 2ea90d9

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -1839
app.py DELETED
@@ -1,1839 +0,0 @@
1
- # --- START OF FILE app.py ---
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, timedelta, timezone
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
- import hmac
16
- import hashlib
17
- import urllib.parse
18
- from functools import wraps
19
-
20
- app = Flask(__name__)
21
- app.secret_key = os.getenv("FLASK_SECRET_KEY", "supersecretkey_folders_tma_unique")
22
- DATA_FILE = 'cloudeng_tma_data.json'
23
- REPO_ID = "Eluza133/Z1e1u"
24
- HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
25
- HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") or HF_TOKEN_WRITE
26
- TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
27
- ADMIN_TELEGRAM_IDS_STR = os.getenv("ADMIN_TELEGRAM_IDS", "")
28
- ADMIN_TELEGRAM_IDS = {int(admin_id.strip()) for admin_id in ADMIN_TELEGRAM_IDS_STR.split(',') if admin_id.strip().isdigit()}
29
-
30
- UPLOAD_FOLDER = 'uploads'
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
- # --- Data Handling ---
37
-
38
- def find_node_by_id(filesystem, node_id):
39
- if not filesystem: return None, None
40
- if filesystem.get('id') == node_id:
41
- return filesystem, None
42
-
43
- queue = [(filesystem, None)]
44
- while queue:
45
- current_node, parent = queue.pop(0)
46
- if current_node.get('type') == 'folder' and 'children' in current_node:
47
- for i, child in enumerate(current_node['children']):
48
- if child.get('id') == node_id:
49
- return child, current_node
50
- if child.get('type') == 'folder':
51
- queue.append((child, current_node))
52
- return None, None
53
-
54
- def add_node(filesystem, parent_id, node_data):
55
- parent_node, _ = find_node_by_id(filesystem, parent_id)
56
- if parent_node and parent_node.get('type') == 'folder':
57
- if 'children' not in parent_node:
58
- parent_node['children'] = []
59
- parent_node['children'].append(node_data)
60
- return True
61
- return False
62
-
63
- def remove_node(filesystem, node_id):
64
- node_to_remove, parent_node = find_node_by_id(filesystem, node_id)
65
- if node_to_remove and parent_node and 'children' in parent_node:
66
- parent_node['children'] = [child for child in parent_node['children'] if child.get('id') != node_id]
67
- return True
68
- # Handle removing from root if parent is root
69
- elif node_to_remove and filesystem and filesystem.get('id') == 'root' and node_id != 'root':
70
- if 'children' in filesystem:
71
- original_len = len(filesystem['children'])
72
- filesystem['children'] = [child for child in filesystem['children'] if child.get('id') != node_id]
73
- return len(filesystem['children']) < original_len
74
- return False
75
-
76
-
77
- def get_node_path_string(filesystem, node_id):
78
- path_list = []
79
- current_id = node_id
80
-
81
- while current_id:
82
- node, parent = find_node_by_id(filesystem, current_id)
83
- if not node: break
84
- if node.get('id') != 'root':
85
- path_list.append(node.get('name', node.get('original_filename', '')))
86
- if not parent and node.get('id') != 'root': # Reached root's child without finding root explicitly
87
- break
88
- if not parent and node.get('id') == 'root': # Is root
89
- break
90
- current_id = parent.get('id') if parent else None
91
-
92
- return " / ".join(reversed(path_list)) or "Root"
93
-
94
-
95
- def initialize_user_filesystem(user_data):
96
- if 'filesystem' not in user_data:
97
- user_data['filesystem'] = {
98
- "type": "folder",
99
- "id": "root",
100
- "name": "root",
101
- "children": []
102
- }
103
-
104
- @cache.memoize(timeout=300)
105
- def load_data():
106
- try:
107
- download_db_from_hf()
108
- with open(DATA_FILE, 'r', encoding='utf-8') as file:
109
- data = json.load(file)
110
- if not isinstance(data, dict):
111
- logging.warning("Data is not in dict format, initializing empty database")
112
- return {'users': {}}
113
- data.setdefault('users', {})
114
- # Ensure keys are strings for JSON, but represent integers
115
- data['users'] = {str(k): v for k, v in data['users'].items()}
116
- for user_id_str, user_data in data['users'].items():
117
- initialize_user_filesystem(user_data)
118
- logging.info("Data successfully loaded and initialized")
119
- return data
120
- except Exception as e:
121
- logging.error(f"Error loading data: {e}")
122
- return {'users': {}}
123
-
124
- def save_data(data):
125
- try:
126
- # Ensure user keys are strings for JSON dump
127
- data['users'] = {str(k): v for k, v in data['users'].items()}
128
- with open(DATA_FILE, 'w', encoding='utf-8') as file:
129
- json.dump(data, file, ensure_ascii=False, indent=4)
130
- upload_db_to_hf()
131
- cache.clear()
132
- logging.info("Data saved and uploaded to HF")
133
- except Exception as e:
134
- logging.error(f"Error saving data: {e}")
135
- raise
136
-
137
- def upload_db_to_hf():
138
- if not HF_TOKEN_WRITE:
139
- logging.warning("HF_TOKEN_WRITE not set, skipping database upload.")
140
- return
141
- try:
142
- api = HfApi()
143
- api.upload_file(
144
- path_or_fileobj=DATA_FILE,
145
- path_in_repo=DATA_FILE,
146
- repo_id=REPO_ID,
147
- repo_type="dataset",
148
- token=HF_TOKEN_WRITE,
149
- commit_message=f"Backup {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
150
- )
151
- logging.info("Database uploaded to Hugging Face")
152
- except Exception as e:
153
- logging.error(f"Error uploading database: {e}")
154
-
155
- def download_db_from_hf():
156
- if not HF_TOKEN_READ:
157
- logging.warning("HF_TOKEN_READ not set, skipping database download.")
158
- if not os.path.exists(DATA_FILE):
159
- with open(DATA_FILE, 'w', encoding='utf-8') as f:
160
- json.dump({'users': {}}, f)
161
- return
162
- try:
163
- hf_hub_download(
164
- repo_id=REPO_ID,
165
- filename=DATA_FILE,
166
- repo_type="dataset",
167
- token=HF_TOKEN_READ,
168
- local_dir=".",
169
- local_dir_use_symlinks=False
170
- )
171
- logging.info("Database downloaded from Hugging Face")
172
- except (hf_utils.RepositoryNotFoundError, hf_utils.EntryNotFoundError) as e:
173
- logging.warning(f"Database file not found on HF Hub ({e}). Initializing empty database if local file doesn't exist.")
174
- if not os.path.exists(DATA_FILE):
175
- with open(DATA_FILE, 'w', encoding='utf-8') as f:
176
- json.dump({'users': {}}, f)
177
- except Exception as e:
178
- logging.error(f"Error downloading database: {e}")
179
- if not os.path.exists(DATA_FILE):
180
- with open(DATA_FILE, 'w', encoding='utf-8') as f:
181
- json.dump({'users': {}}, f)
182
-
183
- def periodic_backup():
184
- while True:
185
- time.sleep(1800)
186
- try:
187
- # Reload data before backup to ensure consistency if save_data wasn't called recently
188
- current_data = load_data()
189
- upload_db_to_hf() # Uses the local file which might be slightly older, but save_data triggers upload anyway
190
- logging.info("Periodic backup check complete.")
191
- except Exception as e:
192
- logging.error(f"Error during periodic backup: {e}")
193
-
194
-
195
- def get_file_type(filename):
196
- filename_lower = filename.lower()
197
- if filename_lower.endswith(('.mp4', '.mov', '.avi', '.webm', '.mkv')):
198
- return 'video'
199
- elif filename_lower.endswith(('.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg')):
200
- return 'image'
201
- elif filename_lower.endswith('.pdf'):
202
- return 'pdf'
203
- elif filename_lower.endswith('.txt'):
204
- return 'text'
205
- return 'other'
206
-
207
- # --- Telegram Auth ---
208
-
209
- def verify_telegram_data(init_data_str, bot_token):
210
- if not init_data_str or not bot_token:
211
- return None, "No initData or bot token provided"
212
-
213
- try:
214
- parsed_data = urllib.parse.parse_qs(init_data_str)
215
- except Exception as e:
216
- return None, f"Could not parse initData: {e}"
217
-
218
- if 'hash' not in parsed_data or 'user' not in parsed_data:
219
- return None, "initData missing 'hash' or 'user' field"
220
-
221
- received_hash = parsed_data['hash'][0]
222
- data_check_list = []
223
-
224
- for key, value in sorted(parsed_data.items()):
225
- if key != 'hash':
226
- data_check_list.append(f"{key}={value[0]}")
227
-
228
- data_check_string = "\n".join(data_check_list)
229
-
230
- secret_key = hmac.new("WebAppData".encode(), bot_token.encode(), hashlib.sha256).digest()
231
- calculated_hash = hmac.new(secret_key, data_check_string.encode(), hashlib.sha256).hexdigest()
232
-
233
- if calculated_hash != received_hash:
234
- return None, "Hash verification failed"
235
-
236
- # Optional: Check auth_date freshness (e.g., within 1 hour)
237
- if 'auth_date' in parsed_data:
238
- auth_timestamp = int(parsed_data['auth_date'][0])
239
- current_timestamp = int(time.time())
240
- if current_timestamp - auth_timestamp > 3600: # 1 hour validity
241
- logging.warning(f"Stale auth_date received: {auth_timestamp}")
242
- # return None, "Authentication data is outdated" # Can be strict if needed
243
-
244
- try:
245
- user_data = json.loads(parsed_data['user'][0])
246
- # Ensure essential fields are present
247
- if 'id' not in user_data:
248
- return None, "User data missing 'id'"
249
- # Make sure id is an integer
250
- user_data['id'] = int(user_data['id'])
251
- return user_data, None # Success
252
- except (json.JSONDecodeError, KeyError, ValueError) as e:
253
- return None, f"Could not parse user data: {e}"
254
-
255
- # --- Decorators ---
256
-
257
- def login_required(f):
258
- @wraps(f)
259
- def decorated_function(*args, **kwargs):
260
- if 'telegram_user' not in session:
261
- return jsonify({"status": "error", "message": "Authentication required", "action": "reload"}), 401
262
- return f(*args, **kwargs)
263
- return decorated_function
264
-
265
- def admin_required(f):
266
- @wraps(f)
267
- def decorated_function(*args, **kwargs):
268
- if 'telegram_user' not in session:
269
- flash('Authentication required.', 'error')
270
- return redirect(url_for('index')) # Redirect to main page for re-auth attempt
271
- user_id = session['telegram_user'].get('id')
272
- if not user_id or user_id not in ADMIN_TELEGRAM_IDS:
273
- flash('Admin access required.', 'error')
274
- return redirect(url_for('dashboard')) # Redirect non-admins to their dashboard
275
- return f(*args, **kwargs)
276
- return decorated_function
277
-
278
-
279
- # --- Styling ---
280
- BASE_STYLE = '''
281
- :root {
282
- --primary: #ff4d6d; --secondary: #00ddeb; --accent: #8b5cf6;
283
- --background-light: #f5f6fa; --background-dark: #1a1625;
284
- --card-bg: rgba(255, 255, 255, 0.95); --card-bg-dark: rgba(40, 35, 60, 0.95);
285
- --text-light: #2a1e5a; --text-dark: #e8e1ff; --shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
286
- --glass-bg: rgba(255, 255, 255, 0.15); --transition: all 0.3s ease; --delete-color: #ff4444;
287
- --folder-color: #ffc107;
288
- --tg-theme-bg-color: var(--background-light);
289
- --tg-theme-text-color: var(--text-light);
290
- --tg-theme-button-color: var(--primary);
291
- --tg-theme-button-text-color: #ffffff;
292
- --tg-theme-hint-color: #aaa;
293
- }
294
- html.dark-mode {
295
- --tg-theme-bg-color: var(--background-dark);
296
- --tg-theme-text-color: var(--text-dark);
297
- }
298
-
299
- * { margin: 0; padding: 0; box-sizing: border-box; }
300
- body { font-family: 'Inter', sans-serif; background: var(--tg-theme-bg-color); color: var(--tg-theme-text-color); line-height: 1.6; padding-bottom: 60px; /* Space for potential main button */ }
301
- .container { margin: 10px auto; max-width: 1200px; padding: 15px; background: var(--card-bg); border-radius: 15px; box-shadow: var(--shadow); overflow-x: hidden; }
302
- html.dark-mode .container { background: var(--card-bg-dark); }
303
- h1 { font-size: 1.8em; font-weight: 800; text-align: center; margin-bottom: 20px; background: linear-gradient(135deg, var(--primary), var(--accent)); -webkit-background-clip: text; color: transparent; }
304
- h2 { font-size: 1.4em; margin-top: 25px; margin-bottom: 10px; color: var(--tg-theme-text-color); }
305
- h4 { font-size: 1.1em; margin-top: 15px; margin-bottom: 5px; color: var(--accent); }
306
- ol, ul { margin-left: 20px; margin-bottom: 15px; }
307
- li { margin-bottom: 5px; }
308
- input, textarea { width: 100%; padding: 12px; margin: 10px 0; border: none; border-radius: 12px; background: var(--glass-bg); color: var(--tg-theme-text-color); font-size: 1em; box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.1); }
309
- html.dark-mode input, html.dark-mode textarea { background: rgba(255, 255, 255, 0.1); }
310
- input:focus, textarea:focus { outline: none; box-shadow: 0 0 0 3px var(--primary); }
311
- .btn { padding: 12px 24px; background: var(--tg-theme-button-color); color: var(--tg-theme-button-text-color); border: none; border-radius: 12px; cursor: pointer; font-size: 1em; font-weight: 600; transition: var(--transition); box-shadow: var(--shadow); display: inline-block; text-decoration: none; margin-top: 5px; margin-right: 5px; }
312
- .btn:hover { filter: brightness(1.1); transform: scale(1.03); }
313
- .download-btn { background: var(--secondary); color: #fff; }
314
- .download-btn:hover { background: #00b8c5; }
315
- .delete-btn { background: var(--delete-color); color: #fff; }
316
- .delete-btn:hover { background: #cc3333; }
317
- .folder-btn { background: var(--folder-color); color: #fff; }
318
- .folder-btn:hover { background: #e6a000; }
319
- .flash { color: var(--secondary); text-align: center; margin-bottom: 15px; padding: 10px; background: rgba(0, 221, 235, 0.1); border-radius: 10px; font-size: 0.9em; }
320
- .flash.error { color: var(--delete-color); background: rgba(255, 68, 68, 0.1); }
321
- .file-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 15px; margin-top: 15px; }
322
- .user-list { margin-top: 20px; }
323
- .user-item { padding: 15px; background: var(--card-bg); border-radius: 16px; margin-bottom: 10px; box-shadow: var(--shadow); transition: var(--transition); display: flex; align-items: center; gap: 15px;}
324
- html.dark-mode .user-item { background: var(--card-bg-dark); }
325
- .user-item:hover { transform: translateY(-3px); }
326
- .user-item img.avatar { width: 50px; height: 50px; border-radius: 50%; object-fit: cover; }
327
- .user-item .user-info { flex-grow: 1; }
328
- .user-item a { color: var(--primary); text-decoration: none; font-weight: 600; }
329
- .user-item a:hover { color: var(--accent); }
330
- .item { background: var(--card-bg); padding: 12px; border-radius: 14px; box-shadow: var(--shadow); text-align: center; transition: var(--transition); display: flex; flex-direction: column; justify-content: space-between; }
331
- html.dark-mode .item { background: var(--card-bg-dark); }
332
- .item:hover { transform: translateY(-4px); }
333
- .item-preview { max-width: 100%; height: 110px; object-fit: cover; border-radius: 8px; margin-bottom: 8px; cursor: pointer; display: block; margin-left: auto; margin-right: auto;}
334
- .item.folder .item-preview { object-fit: contain; font-size: 50px; color: var(--folder-color); line-height: 110px; }
335
- .item p { font-size: 0.85em; margin: 4px 0; word-break: break-all; }
336
- .item a { color: var(--primary); text-decoration: none; }
337
- .item a:hover { color: var(--accent); }
338
- .item-actions { margin-top: 8px; display: flex; flex-wrap: wrap; gap: 4px; justify-content: center; }
339
- .item-actions .btn { font-size: 0.8em; padding: 4px 8px; }
340
- .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; }
341
- .modal-content { max-width: 95%; max-height: 95%; background: var(--tg-theme-bg-color); padding: 10px; border-radius: 15px; overflow: auto; position: relative; }
342
- .modal img, .modal video, .modal iframe, .modal pre { max-width: 100%; max-height: 85vh; display: block; margin: auto; border-radius: 10px; }
343
- .modal iframe { width: 90vw; height: 85vh; border: none; background: #fff; }
344
- html.dark-mode .modal iframe { background: #333; }
345
- .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; font-size: 0.9em;}
346
- html.dark-mode .modal pre { background: #2b2a33; color: var(--text-dark); }
347
- .modal-close-btn { position: absolute; top: 10px; right: 15px; font-size: 24px; color: #aaa; cursor: pointer; background: rgba(0,0,0,0.5); border-radius: 50%; width: 25px; height: 25px; line-height: 25px; text-align: center; }
348
- html.dark-mode .modal-close-btn { color: #555; background: rgba(255,255,255,0.2); }
349
- #progress-container { width: 100%; background: var(--glass-bg); border-radius: 8px; margin: 12px 0; display: none; position: relative; height: 18px; }
350
- html.dark-mode #progress-container { background: rgba(255, 255, 255, 0.1); }
351
- #progress-bar { width: 0%; height: 100%; background: var(--primary); border-radius: 8px; transition: width 0.3s ease; }
352
- #progress-text { position: absolute; width: 100%; text-align: center; line-height: 18px; color: white; font-size: 0.8em; font-weight: bold; text-shadow: 1px 1px 1px rgba(0,0,0,0.5); }
353
- .breadcrumbs { margin-bottom: 15px; font-size: 1em; color: var(--tg-theme-hint-color); }
354
- .breadcrumbs a { color: var(--accent); text-decoration: none; }
355
- .breadcrumbs a:hover { text-decoration: underline; }
356
- .breadcrumbs span { margin: 0 4px; }
357
- .folder-actions { margin-top: 15px; margin-bottom: 10px; display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
358
- .folder-actions input[type=text] { width: auto; flex-grow: 1; margin: 0; min-width: 150px; }
359
- .folder-actions .btn { margin: 0; flex-shrink: 0;}
360
- #auth-status { text-align: center; padding: 20px; font-size: 1.1em; }
361
- .user-info-header { display: flex; align-items: center; gap: 10px; margin-bottom: 15px; padding: 10px; background: var(--glass-bg); border-radius: 10px;}
362
- html.dark-mode .user-info-header { background: rgba(255, 255, 255, 0.1); }
363
- .user-info-header img.avatar { width: 40px; height: 40px; border-radius: 50%; }
364
- .user-info-header p { margin: 0; font-weight: 600; }
365
-
366
- @media (max-width: 768px) {
367
- .file-grid { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 12px; }
368
- .item-preview { height: 90px; }
369
- .item.folder .item-preview { font-size: 40px; line-height: 90px; }
370
- h1 { font-size: 1.6em; }
371
- .btn { padding: 10px 20px; font-size: 0.95em; }
372
- .item-actions .btn { padding: 3px 6px; font-size: 0.75em;}
373
- }
374
- @media (max-width: 480px) {
375
- .container { padding: 10px; }
376
- .file-grid { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 10px; }
377
- .item-preview { height: 80px; }
378
- .item.folder .item-preview { font-size: 35px; line-height: 80px; }
379
- .item p { font-size: 0.8em;}
380
- .breadcrumbs { font-size: 0.9em; }
381
- .btn { padding: 9px 18px; }
382
- .folder-actions { flex-direction: column; align-items: stretch; }
383
- .folder-actions input[type=text] { width: 100%; }
384
- }
385
- '''
386
-
387
- # --- Routes ---
388
-
389
- @app.route('/')
390
- def index():
391
- init_script = '''
392
- <script src="https://telegram.org/js/telegram-web-app.js"></script>
393
- <script>
394
- window.onload = function() {
395
- Telegram.WebApp.ready();
396
- if (Telegram.WebApp.colorScheme === 'dark') {
397
- document.documentElement.classList.add('dark-mode');
398
- }
399
-
400
- const initData = Telegram.WebApp.initData;
401
- const statusDiv = document.getElementById('auth-status');
402
- const mainContent = document.getElementById('main-content');
403
-
404
- if (!initData) {
405
- statusDiv.innerHTML = '<p style="color: red;">Ошибка: Не удалось получить данные Telegram. Попробуйте открыть приложение через Telegram.</p>';
406
- mainContent.style.display = 'none';
407
- return;
408
- }
409
-
410
- statusDiv.innerHTML = '<p>Проверка авторизации...</p>';
411
-
412
- fetch('/verify_telegram_auth', {
413
- method: 'POST',
414
- headers: { 'Content-Type': 'application/json' },
415
- body: JSON.stringify({ initData: initData })
416
- })
417
- .then(response => response.json())
418
- .then(data => {
419
- if (data.status === 'success') {
420
- statusDiv.style.display = 'none'; // Hide status message
421
- mainContent.style.display = 'block'; // Show content
422
- // Inject user info if needed, or reload to let Flask handle session
423
- window.location.reload(); // Easiest way to let Flask use the new session
424
- } else {
425
- statusDiv.innerHTML = `<p style="color: red;">Ошибка авторизации: ${data.message}. Попробуйте перезапустить приложение.</p>`;
426
- mainContent.style.display = 'none';
427
- Telegram.WebApp.showAlert('Ошибка авторизации: ' + data.message);
428
- }
429
- })
430
- .catch(error => {
431
- console.error('Auth verification error:', error);
432
- statusDiv.innerHTML = '<p style="color: red;">Ошибка соединения с сервером для авторизации. Проверьте интернет и попробуйте снова.</p>';
433
- mainContent.style.display = 'none';
434
- Telegram.WebApp.showAlert('Ошибка соединения с сервером.');
435
- });
436
- };
437
- </script>
438
- '''
439
- html_base = '''
440
- <!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
441
- <title>Zeus Cloud</title><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
442
- <style>''' + BASE_STYLE + '''</style>''' + init_script + '''</head><body>
443
- <div id="auth-status"><p>Загрузка приложения...</p></div>
444
- <div id="main-content" style="display: none;">
445
- <!-- Dashboard content will be loaded here by Flask if auth succeeds -->
446
- <div class="container">
447
- <p>Загрузка панели управления...</p>
448
- </div>
449
- </div>
450
- </body></html>'''
451
-
452
- # If user is already authenticated via session, redirect to dashboard directly
453
- if 'telegram_user' in session:
454
- return redirect(url_for('dashboard'))
455
-
456
- # Otherwise, show the initial loading/auth page
457
- return render_template_string(html_base)
458
-
459
-
460
- @app.route('/verify_telegram_auth', methods=['POST'])
461
- def verify_auth():
462
- if not TELEGRAM_BOT_TOKEN:
463
- logging.error("TELEGRAM_BOT_TOKEN is not set!")
464
- return jsonify({"status": "error", "message": "Server configuration error (no bot token)."}), 500
465
-
466
- req_data = request.get_json()
467
- init_data_str = req_data.get('initData')
468
-
469
- if not init_data_str:
470
- return jsonify({"status": "error", "message": "No initData received."}), 400
471
-
472
- user_data, error_message = verify_telegram_data(init_data_str, TELEGRAM_BOT_TOKEN)
473
-
474
- if user_data:
475
- session['telegram_user'] = user_data
476
- session.permanent = True # Make session persistent
477
-
478
- # --- User creation/update in DB ---
479
- data = load_data()
480
- user_id = user_data['id']
481
- user_id_str = str(user_id)
482
-
483
- if user_id_str not in data['users']:
484
- logging.info(f"New user detected: {user_id}, Username: {user_data.get('username', 'N/A')}")
485
- data['users'][user_id_str] = {
486
- 'telegram_id': user_id,
487
- 'username': user_data.get('username'),
488
- 'first_name': user_data.get('first_name'),
489
- 'last_name': user_data.get('last_name'),
490
- 'photo_url': user_data.get('photo_url'),
491
- 'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
492
- 'filesystem': {
493
- "type": "folder", "id": "root", "name": "root", "children": []
494
- }
495
- }
496
- else:
497
- # Update user info if changed
498
- data['users'][user_id_str]['username'] = user_data.get('username')
499
- data['users'][user_id_str]['first_name'] = user_data.get('first_name')
500
- data['users'][user_id_str]['last_name'] = user_data.get('last_name')
501
- data['users'][user_id_str]['photo_url'] = user_data.get('photo_url')
502
- initialize_user_filesystem(data['users'][user_id_str]) # Ensure filesystem exists
503
-
504
- try:
505
- save_data(data)
506
- return jsonify({"status": "success", "user": user_data})
507
- except Exception as e:
508
- logging.error(f"Failed to save data after user verification/creation for {user_id}: {e}")
509
- # Decide if auth should fail if DB save fails. For now, let auth succeed but log error.
510
- return jsonify({"status": "success", "user": user_data, "warning": "DB save failed"})
511
-
512
- else:
513
- logging.warning(f"Telegram auth verification failed: {error_message}")
514
- return jsonify({"status": "error", "message": error_message}), 403
515
-
516
- @app.route('/dashboard', methods=['GET', 'POST'])
517
- @login_required
518
- def dashboard():
519
- telegram_user = session['telegram_user']
520
- user_id = telegram_user['id']
521
- user_id_str = str(user_id)
522
-
523
- data = load_data()
524
- if user_id_str not in data['users']:
525
- session.pop('telegram_user', None)
526
- flash('Пользователь не найден в базе данных! Попробуйте перезапустить приложение.', 'error')
527
- # In TMA context, can't easily redirect to login, show error or trigger reload
528
- return render_template_string('<body>Ошибка: Пользователь не найден. <a href="/">Перезагрузить</a></body>'), 404
529
-
530
- user_data = data['users'][user_id_str]
531
- # Filesystem should be initialized on login/verify, but check again just in case
532
- initialize_user_filesystem(user_data)
533
-
534
- current_folder_id = request.args.get('folder_id', 'root')
535
- current_folder, parent_folder = find_node_by_id(user_data['filesystem'], current_folder_id)
536
-
537
- if not current_folder or current_folder.get('type') != 'folder':
538
- flash('Папка не найдена!', 'error')
539
- current_folder_id = 'root'
540
- # Refetch root node
541
- current_folder, parent_folder = find_node_by_id(user_data['filesystem'], current_folder_id)
542
- if not current_folder:
543
- logging.error(f"CRITICAL: Root folder not found for user {user_id}")
544
- flash('Критическая ошибка: корневая папка не найдена.', 'error')
545
- session.pop('telegram_user', None)
546
- return render_template_string('<body>Критическая ошибка файловой системы. <a href="/">Перезагрузить</a></body>'), 500
547
-
548
-
549
- items_in_folder = sorted(current_folder.get('children', []), key=lambda x: (x['type'] != 'folder', x.get('name', x.get('original_filename', '')).lower()))
550
-
551
- if request.method == 'POST':
552
- if not HF_TOKEN_WRITE:
553
- flash('Загрузка невозможна: токен для записи не настроен.', 'error')
554
- return redirect(url_for('dashboard', folder_id=current_folder_id))
555
-
556
- files = request.files.getlist('files')
557
- if not files or all(not f.filename for f in files):
558
- # This case might be handled by JS now, but keep server-side check
559
- # flash('Файлы для загрузки не выбраны.', 'error') # Avoid flash, return JSON for XHR
560
- return jsonify({'status': 'error', 'message': 'Файлы для загрузки не выбраны.'}), 400
561
-
562
- if len(files) > 20:
563
- # flash('Максимум 20 файлов за раз!', 'error')
564
- return jsonify({'status': 'error', 'message': 'Максимум 20 файлов за раз!'}), 400
565
-
566
-
567
- target_folder_id = request.form.get('current_folder_id', 'root')
568
- target_folder_node, _ = find_node_by_id(user_data['filesystem'], target_folder_id)
569
-
570
- if not target_folder_node or target_folder_node.get('type') != 'folder':
571
- # flash('Целевая папка для загрузки не найдена!', 'error')
572
- return jsonify({'status': 'error', 'message': 'Целевая папка для загрузки не найдена!'}), 404
573
-
574
-
575
- api = HfApi()
576
- uploaded_count = 0
577
- errors = []
578
-
579
- for file in files:
580
- if file and file.filename:
581
- original_filename = secure_filename(file.filename)
582
- name_part, ext_part = os.path.splitext(original_filename)
583
- unique_suffix = uuid.uuid4().hex[:8]
584
- unique_filename = f"{name_part}_{unique_suffix}{ext_part}"
585
- file_id = uuid.uuid4().hex
586
-
587
- # Use telegram_id in the path
588
- hf_path = f"cloud_files/{user_id}/{target_folder_id}/{unique_filename}"
589
- temp_path = os.path.join(UPLOAD_FOLDER, f"{file_id}_{unique_filename}")
590
-
591
- try:
592
- file.save(temp_path)
593
-
594
- api.upload_file(
595
- path_or_fileobj=temp_path,
596
- path_in_repo=hf_path,
597
- repo_id=REPO_ID,
598
- repo_type="dataset",
599
- token=HF_TOKEN_WRITE,
600
- commit_message=f"User {user_id} uploaded {original_filename} to folder {target_folder_id}"
601
- )
602
-
603
- file_info = {
604
- 'type': 'file',
605
- 'id': file_id,
606
- 'original_filename': original_filename,
607
- 'unique_filename': unique_filename,
608
- 'path': hf_path,
609
- 'file_type': get_file_type(original_filename),
610
- 'upload_date': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
611
- }
612
-
613
- if add_node(user_data['filesystem'], target_folder_id, file_info):
614
- uploaded_count += 1
615
- else:
616
- errors.append(f"Ошибка добавления метаданных для {original_filename}.")
617
- logging.error(f"Failed to add node metadata for file {file_id} to folder {target_folder_id} for user {user_id}")
618
- try:
619
- api.delete_file(path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE)
620
- except Exception as del_err:
621
- logging.error(f"Failed to delete orphaned file {hf_path} from HF Hub: {del_err}")
622
-
623
- except Exception as e:
624
- logging.error(f"Error uploading file {original_filename} for {user_id}: {e}")
625
- errors.append(f"Ошибка загрузки файла {original_filename}: {e}")
626
- finally:
627
- if os.path.exists(temp_path):
628
- os.remove(temp_path)
629
-
630
- response_message = ""
631
- final_status = "success"
632
-
633
- if uploaded_count > 0:
634
- try:
635
- save_data(data)
636
- response_message += f'{uploaded_count} файл(ов) успешно загружено! '
637
- except Exception as e:
638
- response_message += 'Файлы загружены на сервер, но произошла ошибка сохранения метаданных. '
639
- logging.error(f"Error saving data after upload for {user_id}: {e}")
640
- final_status = "warning" # Or "error" depending on severity
641
-
642
- if errors:
643
- response_message += "Ошибки: " + "; ".join(errors)
644
- final_status = "error" if uploaded_count == 0 else "warning"
645
-
646
- # Return JSON response for XHR request
647
- return jsonify({'status': final_status, 'message': response_message.strip()})
648
-
649
-
650
- # --- Breadcrumbs Calculation (for GET request) ---
651
- breadcrumbs = []
652
- temp_id = current_folder_id
653
- # Limit depth to avoid infinite loops in case of data corruption
654
- for _ in range(20): # Max 20 levels deep
655
- node, parent = find_node_by_id(user_data['filesystem'], temp_id)
656
- if not node: break
657
- is_link = (node['id'] != current_folder_id)
658
- breadcrumbs.append({'id': node['id'], 'name': node.get('name', 'Root'), 'is_link': is_link})
659
- if not parent and node.get('id') == 'root': break # Stop at root
660
- if not parent: break # Stop if parent somehow not found before root
661
- temp_id = parent.get('id')
662
- if not temp_id: break # Stop if parent has no id
663
- breadcrumbs.reverse()
664
-
665
- # --- HTML Template for GET request ---
666
- html = '''
667
- <!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
668
- <title>Панель управления - Zeus Cloud</title><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
669
- <style>''' + BASE_STYLE + '''</style>
670
- <script src="https://telegram.org/js/telegram-web-app.js"></script>
671
- </head>
672
- <body>
673
- <div class="container">
674
-
675
- <div class="user-info-header">
676
- {% if telegram_user.photo_url %}
677
- <img src="{{ telegram_user.photo_url }}" alt="Avatar" class="avatar">
678
- {% endif %}
679
- <p>Привет, {{ telegram_user.first_name or telegram_user.username or 'Пользователь' }}!</p>
680
- {% if is_admin %}
681
- <a href="{{ url_for('admin_panel') }}" class="btn" style="margin-left: auto; padding: 5px 10px; font-size: 0.8em;">Админ</a>
682
- {% endif %}
683
- </div>
684
-
685
- <h1>Zeus Cloud</h1>
686
- <div id="flash-container">
687
- {% with messages = get_flashed_messages(with_categories=true) %}
688
- {% if messages %}
689
- {% for category, message in messages %}
690
- <div class="flash {{ category }}">{{ message }}</div>
691
- {% endfor %}
692
- {% endif %}
693
- {% endwith %}
694
- </div>
695
-
696
- <div class="breadcrumbs">
697
- {% for crumb in breadcrumbs %}
698
- {% if crumb.is_link %}
699
- <a href="{{ url_for('dashboard', folder_id=crumb.id) }}">{{ crumb.name if crumb.id != 'root' else 'Главная' }}</a>
700
- {% else %}
701
- <span>{{ crumb.name if crumb.id != 'root' else 'Главная' }}</span>
702
- {% endif %}
703
- {% if not loop.last %}<span>/</span>{% endif %}
704
- {% endfor %}
705
- </div>
706
-
707
- <div class="folder-actions">
708
- <form method="POST" action="{{ url_for('create_folder') }}" style="display: contents;">
709
- <input type="hidden" name="parent_folder_id" value="{{ current_folder_id }}">
710
- <input type="text" name="folder_name" placeholder="Имя новой папки" required>
711
- <button type="submit" class="btn folder-btn">Создать папку</button>
712
- </form>
713
- </div>
714
-
715
- <form id="upload-form" method="POST" enctype="multipart/form-data" action="{{ url_for('dashboard') }}">
716
- <input type="hidden" name="current_folder_id" value="{{ current_folder_id }}">
717
- <input type="file" name="files" id="file-input" multiple required style="margin-bottom: 10px; display: block;">
718
- <button type="submit" class="btn" id="upload-btn">Загрузить файлы сюда</button>
719
- </form>
720
- <div id="progress-container"><div id="progress-bar"></div><div id="progress-text">0%</div></div>
721
-
722
- <h2>Содержимое папки: {{ current_folder.name if current_folder_id != 'root' else 'Главная' }}</h2>
723
- <div class="file-grid">
724
- {% for item in items %}
725
- <div class="item {{ item.type }}">
726
- {% if item.type == 'folder' %}
727
- <a href="{{ url_for('dashboard', folder_id=item.id) }}" class="item-preview" title="Перейти в папку {{ item.name }}">📁</a>
728
- <p><b>{{ item.name | truncate(25, True) }}</b></p>
729
- <div class="item-actions">
730
- <a href="{{ url_for('dashboard', folder_id=item.id) }}" class="btn folder-btn">Открыть</a>
731
- <form method="POST" action="{{ url_for('delete_folder', folder_id=item.id) }}" style="display: inline;" onsubmit="return confirmDelete('Удалить папку {{ item.name }}? Папку можно удалить только если она пуста.');">
732
- <input type="hidden" name="current_view_folder_id" value="{{ current_folder_id }}">
733
- <button type="submit" class="btn delete-btn">Удалить</button>
734
- </form>
735
- </div>
736
- {% elif item.type == 'file' %}
737
- {% set previewable = item.file_type in ['image', 'video', 'pdf', 'text'] %}
738
- {% if item.file_type == 'image' %}
739
- <img class="item-preview" src="{{ hf_file_url(item.path) }}" alt="{{ item.original_filename }}" loading="lazy"
740
- onclick="openModal('{{ hf_file_url(item.path) }}', '{{ item.file_type }}', '{{ item.id }}')">
741
- {% elif item.file_type == 'video' %}
742
- <video class="item-preview" preload="metadata" muted loading="lazy"
743
- onclick="openModal('{{ hf_file_url(item.path) }}', '{{ item.file_type }}', '{{ item.id }}')">
744
- <source src="{{ hf_file_url(item.path, True) }}#t=0.5" type="video/mp4">No video support.</video>
745
- {% elif item.file_type == 'pdf' %}
746
- <div class="item-preview" style="font-size: 50px; line-height: 110px; color: var(--accent); cursor: pointer;"
747
- onclick="openModal('{{ hf_file_url(item.path, True) }}', '{{ item.file_type }}', '{{ item.id }}')">📄</div>
748
- {% elif item.file_type == 'text' %}
749
- <div class="item-preview" style="font-size: 50px; line-height: 110px; color: var(--secondary); cursor: pointer;"
750
- onclick="openModal('{{ url_for('get_text_content', file_id=item.id) }}', '{{ item.file_type }}', '{{ item.id }}')">📝</div>
751
- {% else %}
752
- <div class="item-preview" style="font-size: 50px; line-height: 110px; color: #aaa;">❓</div>
753
- {% endif %}
754
- <p title="{{ item.original_filename }}">{{ item.original_filename | truncate(25, True) }}</p>
755
- <p style="font-size: 0.8em; color: var(--tg-theme-hint-color);">{{ item.upload_date }}</p>
756
- <div class="item-actions">
757
- <a href="{{ url_for('download_file', file_id=item.id) }}" class="btn download-btn">Скачать</a>
758
- {% if previewable %}
759
- <button class="btn" style="background: var(--accent);"
760
- onclick="openModal('{{ hf_file_url(item.path) if item.file_type != 'text' else url_for('get_text_content', file_id=item.id) }}', '{{ item.file_type }}', '{{ item.id }}')">Просмотр</button>
761
- {% endif %}
762
- <form method="POST" action="{{ url_for('delete_file', file_id=item.id) }}" style="display: inline;" onsubmit="return confirmDelete('Вы уверены, что хотите удалить файл {{ item.original_filename }}?');">
763
- <input type="hidden" name="current_view_folder_id" value="{{ current_folder_id }}">
764
- <button type="submit" class="btn delete-btn">Удалить</button>
765
- </form>
766
- </div>
767
- {% endif %}
768
- </div>
769
- {% endfor %}
770
- {% if not items %} <p>Эта папка пуста.</p> {% endif %}
771
- </div>
772
-
773
- </div>
774
-
775
- <div class="modal" id="mediaModal" onclick="closeModal(event)">
776
- <div class="modal-content" id="modalContentContainer">
777
- <span onclick="closeModalManual()" class="modal-close-btn">×</span>
778
- <div id="modalContent"></div>
779
- </div>
780
- </div>
781
-
782
- <script>
783
- const repoId = "{{ repo_id }}";
784
- const hfTokenRead = "{{ HF_TOKEN_READ or '' }}";
785
- const tg = window.Telegram.WebApp;
786
- tg.ready();
787
- if (tg.colorScheme === 'dark') {
788
- document.documentElement.classList.add('dark-mode');
789
- }
790
-
791
- function hfFileUrl(path, download = false) {
792
- let url = `https://huggingface.co/datasets/${repoId}/resolve/main/${path}`;
793
- if (download) url += '?download=true';
794
- // Add token header simulation if needed (usually not for direct src/href)
795
- // However, for fetch/XHR, headers are needed if repo is private
796
- return url;
797
- }
798
-
799
- async function openModal(srcOrUrl, type, itemId) {
800
- const modal = document.getElementById('mediaModal');
801
- const modalContent = document.getElementById('modalContent');
802
- modalContent.innerHTML = '<p>Загрузка...</p>';
803
- modal.style.display = 'flex';
804
- tg.expand(); // Expand webapp for better view
805
-
806
- try {
807
- const headers = {};
808
- if (hfTokenRead) {
809
- // Headers mainly needed for fetch/XHR if repo is private
810
- // For img src, video src, iframe src, browser handles auth if logged into HF,
811
- // or token needs embedding if public URL access isn't enough.
812
- // Here we assume public read or direct URL access works.
813
- // If not, PDF/Text might need fetch with headers.
814
- // headers['Authorization'] = `Bearer ${hfTokenRead}`;
815
- }
816
-
817
- if (type === 'image') {
818
- modalContent.innerHTML = `<img src="${srcOrUrl}" alt="Просмотр изображения">`;
819
- } else if (type === 'video') {
820
- // Using the direct URL for video source
821
- modalContent.innerHTML = `<video controls autoplay style='max-width: 95%; max-height: 85vh;'><source src="${srcOrUrl}" type="video/mp4">Ваш браузер не поддерживает видео.</video>`;
822
- } else if (type === 'pdf') {
823
- // Using the direct URL for iframe source
824
- // Note: PDF viewing might be blocked by Cross-Origin policies depending on HF setup
825
- modalContent.innerHTML = `<iframe src="${srcOrUrl}" title="Просмотр PDF"></iframe>`;
826
- } else if (type === 'text') {
827
- // Fetch text content using URL which might need auth header if private
828
- const response = await fetch(srcOrUrl, { headers: headers });
829
- if (!response.ok) throw new Error(`Ошибка загрузки текста: ${response.status} ${response.statusText}`);
830
- const text = await response.text();
831
- // Basic escaping for display in <pre>
832
- const escapedText = text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
833
- modalContent.innerHTML = `<pre>${escapedText}</pre>`;
834
- } else {
835
- modalContent.innerHTML = '<p>Предпросмотр для этого типа файла не поддерживается.</p>';
836
- }
837
- } catch (error) {
838
- console.error("Error loading modal content:", error);
839
- modalContent.innerHTML = `<p>Не удалось загрузить содержимое для предпросмотра. ${error.message}</p>`;
840
- tg.showAlert(`Ошибка предпросмотра: ${error.message}`);
841
- }
842
- }
843
-
844
- function closeModal(event) {
845
- const modal = document.getElementById('mediaModal');
846
- if (event.target === modal) {
847
- closeModalManual();
848
- }
849
- }
850
-
851
- function closeModalManual() {
852
- const modal = document.getElementById('mediaModal');
853
- modal.style.display = 'none';
854
- const video = modal.querySelector('video');
855
- if (video) video.pause();
856
- const iframe = modal.querySelector('iframe');
857
- if (iframe) iframe.src = 'about:blank'; // Clear iframe content
858
- document.getElementById('modalContent').innerHTML = '';
859
- }
860
-
861
- function confirmDelete(message) {
862
- return new Promise((resolve) => {
863
- tg.showConfirm(message, (confirmed) => {
864
- resolve(confirmed);
865
- });
866
- });
867
- }
868
-
869
- document.querySelectorAll('form[onsubmit^="return confirmDelete"]').forEach(form => {
870
- form.addEventListener('submit', async function(e) {
871
- e.preventDefault();
872
- const message = this.getAttribute('onsubmit').match(/confirmDelete\('(.*)'\)/)[1];
873
- const confirmed = await confirmDelete(message);
874
- if (confirmed) {
875
- this.submit(); // Submit the form if confirmed
876
- }
877
- });
878
- // Remove the inline handler attribute as it's now handled by the listener
879
- // form.removeAttribute('onsubmit'); // Optional: keeps HTML cleaner
880
- });
881
-
882
-
883
- const form = document.getElementById('upload-form');
884
- const fileInput = document.getElementById('file-input');
885
- const progressBar = document.getElementById('progress-bar');
886
- const progressText = document.getElementById('progress-text');
887
- const progressContainer = document.getElementById('progress-container');
888
- const uploadBtn = document.getElementById('upload-btn');
889
- const flashContainer = document.getElementById('flash-container');
890
-
891
- form.addEventListener('submit', function(e) {
892
- e.preventDefault();
893
-
894
- const files = fileInput.files;
895
- if (files.length === 0) {
896
- tg.showAlert('Пожалуйста, выберите файлы для загрузки.');
897
- return;
898
- }
899
- if (files.length > 20) {
900
- tg.showAlert('Максимум 20 файлов за раз!');
901
- return;
902
- }
903
-
904
- progressContainer.style.display = 'block';
905
- progressBar.style.width = '0%';
906
- progressText.textContent = '0%';
907
- uploadBtn.disabled = true;
908
- uploadBtn.textContent = 'Загрузка...';
909
- tg.MainButton.showProgress();
910
- tg.MainButton.setText('Загрузка...');
911
- tg.MainButton.disable();
912
-
913
-
914
- const formData = new FormData(form);
915
- const xhr = new XMLHttpRequest();
916
-
917
- xhr.upload.addEventListener('progress', function(event) {
918
- if (event.lengthComputable) {
919
- const percentComplete = Math.round((event.loaded / event.total) * 100);
920
- progressBar.style.width = percentComplete + '%';
921
- progressText.textContent = percentComplete + '%';
922
- // Update main button progress if needed, but simple text/disable is often enough
923
- }
924
- });
925
-
926
- xhr.addEventListener('load', function() {
927
- uploadBtn.disabled = false;
928
- uploadBtn.textContent = 'Загрузить файлы сюда';
929
- progressContainer.style.display = 'none';
930
- tg.MainButton.hideProgress();
931
- tg.MainButton.setText('Загрузить файлы сюда'); // Reset if main button was used
932
- tg.MainButton.enable();
933
-
934
- flashContainer.innerHTML = ''; // Clear previous flashes
935
- try {
936
- const response = JSON.parse(xhr.responseText);
937
- const flashClass = response.status === 'success' ? 'flash' : (response.status === 'warning' ? 'flash warning' : 'flash error');
938
- const flashMessage = `<div class="${flashClass}">${response.message}</div>`;
939
- flashContainer.innerHTML = flashMessage;
940
- if (response.status === 'success' || response.status === 'warning') {
941
- // Optionally reload or update UI without full reload
942
- setTimeout(() => window.location.reload(), 1500); // Reload after message display
943
- } else {
944
- tg.showAlert('Ошибка загрузки: ' + response.message);
945
- }
946
- } catch (e) {
947
- console.error("Failed to parse upload response:", e, xhr.responseText);
948
- flashContainer.innerHTML = `<div class="flash error">Неожиданный ответ от сервера.</div>`;
949
- tg.showAlert('Неожиданный ответ от сервера после загрузки.');
950
- }
951
- // Clear file input after upload
952
- fileInput.value = '';
953
- });
954
-
955
- xhr.addEventListener('error', function() {
956
- handleUploadEnd('Произошла ошибка во время загрузки.');
957
- });
958
-
959
- xhr.addEventListener('abort', function() {
960
- handleUploadEnd('Загрузка отменена.');
961
- });
962
-
963
- function handleUploadEnd(message) {
964
- uploadBtn.disabled = false;
965
- uploadBtn.textContent = 'Загрузить файлы сюда';
966
- progressContainer.style.display = 'none';
967
- tg.MainButton.hideProgress();
968
- tg.MainButton.setText('Загрузить файлы сюда');
969
- tg.MainButton.enable();
970
- tg.showAlert(message);
971
- flashContainer.innerHTML = `<div class="flash error">${message}</div>`;
972
- fileInput.value = '';
973
- }
974
-
975
- xhr.open('POST', form.action, true);
976
- // Add headers if needed, e.g., CSRF token if implemented
977
- xhr.send(formData);
978
- });
979
-
980
- </script>
981
- </body></html>'''
982
-
983
- template_context = {
984
- 'telegram_user': telegram_user,
985
- 'items': items_in_folder,
986
- 'current_folder_id': current_folder_id,
987
- 'current_folder': current_folder,
988
- 'breadcrumbs': breadcrumbs,
989
- 'repo_id': REPO_ID,
990
- 'HF_TOKEN_READ': HF_TOKEN_READ,
991
- 'hf_file_url': lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{path}{'?download=true' if download else ''}",
992
- 'os': os,
993
- 'is_admin': user_id in ADMIN_TELEGRAM_IDS
994
- }
995
- return render_template_string(html, **template_context)
996
-
997
-
998
- @app.route('/create_folder', methods=['POST'])
999
- @login_required
1000
- def create_folder():
1001
- telegram_user = session['telegram_user']
1002
- user_id = telegram_user['id']
1003
- user_id_str = str(user_id)
1004
-
1005
- data = load_data()
1006
- user_data = data['users'].get(user_id_str)
1007
- if not user_data:
1008
- flash('Пользователь не найден!', 'error')
1009
- return redirect(url_for('dashboard')) # Redirect to root dashboard
1010
-
1011
- parent_folder_id = request.form.get('parent_folder_id', 'root')
1012
- folder_name = request.form.get('folder_name', '').strip()
1013
-
1014
- if not folder_name:
1015
- flash('Имя папки не может быть пустым!', 'error')
1016
- return redirect(url_for('dashboard', folder_id=parent_folder_id))
1017
- # Allow more characters, sanitize later if needed
1018
- # if not folder_name.isalnum() and '_' not in folder_name and ' ' not in folder_name:
1019
- # flash('Имя папки может содержать буквы, цифры, пробелы и подчеркивания.', 'error')
1020
- # return redirect(url_for('dashboard', folder_id=parent_folder_id))
1021
- folder_name = secure_filename(folder_name.replace(' ', '_')) # Basic sanitization
1022
-
1023
- if not folder_name: # If secure_filename removes everything
1024
- flash('Недопустимое имя папки.', 'error')
1025
- return redirect(url_for('dashboard', folder_id=parent_folder_id))
1026
-
1027
-
1028
- folder_id = uuid.uuid4().hex
1029
- folder_data = {
1030
- 'type': 'folder',
1031
- 'id': folder_id,
1032
- 'name': folder_name,
1033
- 'children': []
1034
- }
1035
-
1036
- if add_node(user_data['filesystem'], parent_folder_id, folder_data):
1037
- try:
1038
- save_data(data)
1039
- flash(f'Папка "{folder_name}" успешно создана.')
1040
- except Exception as e:
1041
- flash('Ошибка сохранения данных при создании папки.', 'error')
1042
- logging.error(f"Create folder save error for user {user_id}: {e}")
1043
- # Attempt to remove the added node if save failed? Complex.
1044
- else:
1045
- flash('Не удалось найти родительскую папку для создания новой.', 'error')
1046
-
1047
- return redirect(url_for('dashboard', folder_id=parent_folder_id))
1048
-
1049
-
1050
- @app.route('/download/<file_id>')
1051
- @login_required # Require login even for download link access initially
1052
- def download_file(file_id):
1053
- current_user_id = session['telegram_user']['id']
1054
- is_current_user_admin = current_user_id in ADMIN_TELEGRAM_IDS
1055
-
1056
- data = load_data()
1057
- file_node = None
1058
- owner_user_id_str = None
1059
-
1060
- # 1. Check if the file belongs to the current user
1061
- current_user_data = data['users'].get(str(current_user_id))
1062
- if current_user_data:
1063
- file_node, _ = find_node_by_id(current_user_data.get('filesystem', {}), file_id)
1064
- if file_node and file_node.get('type') == 'file':
1065
- owner_user_id_str = str(current_user_id)
1066
-
1067
- # 2. If not found for current user AND current user is admin, search all users
1068
- if not file_node and is_current_user_admin:
1069
- logging.info(f"Admin {current_user_id} searching for file ID {file_id} across all users.")
1070
- for uid_str, udata in data.get('users', {}).items():
1071
- node, _ = find_node_by_id(udata.get('filesystem', {}), file_id)
1072
- if node and node.get('type') == 'file':
1073
- file_node = node
1074
- owner_user_id_str = uid_str
1075
- logging.info(f"Admin {current_user_id} found file ID {file_id} belonging to user {owner_user_id_str}")
1076
- break
1077
-
1078
- if not file_node:
1079
- flash('Файл не найден!', 'error')
1080
- # Redirect back to user's dashboard or admin panel depending on who requested
1081
- if is_current_user_admin and request.referrer and 'admhosto' in request.referrer:
1082
- return redirect(request.referrer)
1083
- return redirect(url_for('dashboard'))
1084
-
1085
-
1086
- hf_path = file_node.get('path')
1087
- original_filename = file_node.get('original_filename', 'downloaded_file')
1088
-
1089
- if not hf_path:
1090
- flash('Ошибка: Путь к файлу не найден в метаданных.', 'error')
1091
- if is_current_user_admin and request.referrer and 'admhosto' in request.referrer:
1092
- return redirect(request.referrer)
1093
- return redirect(url_for('dashboard'))
1094
-
1095
- file_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{hf_path}?download=true"
1096
-
1097
- try:
1098
- headers = {}
1099
- if HF_TOKEN_READ:
1100
- headers["authorization"] = f"Bearer {HF_TOKEN_READ}"
1101
-
1102
- response = requests.get(file_url, headers=headers, stream=True, timeout=60) # Add timeout
1103
- response.raise_for_status()
1104
-
1105
- # Stream download if needed for large files, but send_file handles BytesIO well too
1106
- file_content = BytesIO(response.content)
1107
-
1108
- return send_file(
1109
- file_content,
1110
- as_attachment=True,
1111
- download_name=original_filename,
1112
- mimetype='application/octet-stream'
1113
- )
1114
- except requests.exceptions.Timeout:
1115
- logging.error(f"Timeout downloading file from HF ({hf_path})")
1116
- flash(f'Ошибка скачивания файла {original_filename}: Тайм-аут соединения с сервером.', 'error')
1117
- except requests.exceptions.RequestException as e:
1118
- logging.error(f"Error downloading file from HF ({hf_path}): {e}")
1119
- flash(f'Ошибка скачивания файла {original_filename}! ({e})', 'error')
1120
- except Exception as e:
1121
- logging.error(f"Unexpected error during download ({hf_path}): {e}")
1122
- flash('Произошла непредвиденная ошибка при скачивании файла.', 'error')
1123
-
1124
- # Fallback redirect
1125
- if is_current_user_admin and request.referrer and 'admhosto' in request.referrer:
1126
- return redirect(request.referrer)
1127
- return redirect(url_for('dashboard'))
1128
-
1129
-
1130
- @app.route('/delete_file/<file_id>', methods=['POST'])
1131
- @login_required
1132
- def delete_file(file_id):
1133
- telegram_user = session['telegram_user']
1134
- user_id = telegram_user['id']
1135
- user_id_str = str(user_id)
1136
-
1137
- data = load_data()
1138
- user_data = data['users'].get(user_id_str)
1139
- if not user_data:
1140
- flash('Пользователь не найден!', 'error')
1141
- session.pop('telegram_user', None)
1142
- return redirect(url_for('index')) # Force re-auth
1143
-
1144
- file_node, parent_node = find_node_by_id(user_data['filesystem'], file_id)
1145
- # Determine the folder to redirect back to
1146
- current_view_folder_id = request.form.get('current_view_folder_id')
1147
- if not current_view_folder_id and parent_node:
1148
- current_view_folder_id = parent_node.get('id', 'root')
1149
- elif not current_view_folder_id:
1150
- current_view_folder_id = 'root'
1151
-
1152
-
1153
- if not file_node or file_node.get('type') != 'file': # Parent check removed, root files possible
1154
- flash('Файл не найден или не может быть удален.', 'error')
1155
- return redirect(url_for('dashboard', folder_id=current_view_folder_id))
1156
-
1157
- hf_path = file_node.get('path')
1158
- original_filename = file_node.get('original_filename', 'файл')
1159
-
1160
- # Attempt to remove from DB first
1161
- if remove_node(user_data['filesystem'], file_id):
1162
- try:
1163
- save_data(data)
1164
- logging.info(f"Removed file {file_id} ({original_filename}) from DB for user {user_id}.")
1165
- # Now attempt to delete from HF
1166
- if hf_path and HF_TOKEN_WRITE:
1167
- try:
1168
- api = HfApi()
1169
- api.delete_file(
1170
- path_in_repo=hf_path,
1171
- repo_id=REPO_ID,
1172
- repo_type="dataset",
1173
- token=HF_TOKEN_WRITE,
1174
- commit_message=f"User {user_id} deleted file {original_filename} (ID: {file_id})"
1175
- )
1176
- logging.info(f"Deleted file {hf_path} from HF Hub for user {user_id}")
1177
- flash(f'Файл {original_filename} успешно удален!')
1178
- except hf_utils.EntryNotFoundError:
1179
- logging.warning(f"File {hf_path} not found on HF Hub during delete for user {user_id}, but removed from DB.")
1180
- flash(f'Файл {original_filename} удален из базы (не найден на сервере).')
1181
- except Exception as e:
1182
- logging.error(f"Error deleting file {hf_path} from HF Hub for user {user_id} (DB entry removed): {e}")
1183
- flash(f'Файл {original_filename} удален из базы, но ошибка при удалении с сервера: {e}', 'error')
1184
- elif not hf_path:
1185
- flash(f'Файл {original_filename} удален из базы (путь не найден).')
1186
- elif not HF_TOKEN_WRITE:
1187
- flash(f'Файл {original_filename} удален из базы (удаление с сервера невозможно - токен отсутствует).', 'warning')
1188
-
1189
- except Exception as e:
1190
- # This is bad - removed from structure in memory, but failed to save
1191
- logging.critical(f"CRITICAL: Failed to save DB after removing file {file_id} for user {user_id}. Data inconsistency possible! Error: {e}")
1192
- flash('Критическая ошибка: не удалось сохранить базу данных после удаления файла. Перезагрузите данные.', 'error')
1193
- # Force cache clear and maybe reload?
1194
- cache.clear()
1195
- # Don't attempt HF delete if DB save failed
1196
- else:
1197
- flash('Не удалось найти файл в структуре для удаления.', 'error')
1198
-
1199
- return redirect(url_for('dashboard', folder_id=current_view_folder_id))
1200
-
1201
-
1202
- @app.route('/delete_folder/<folder_id>', methods=['POST'])
1203
- @login_required
1204
- def delete_folder(folder_id):
1205
- telegram_user = session['telegram_user']
1206
- user_id = telegram_user['id']
1207
- user_id_str = str(user_id)
1208
-
1209
- if folder_id == 'root':
1210
- flash('Нельзя удалить корневую папку!', 'error')
1211
- return redirect(url_for('dashboard'))
1212
-
1213
- data = load_data()
1214
- user_data = data['users'].get(user_id_str)
1215
- if not user_data:
1216
- flash('Пользователь не найден!', 'error')
1217
- session.pop('telegram_user', None)
1218
- return redirect(url_for('index'))
1219
-
1220
- folder_node, parent_node = find_node_by_id(user_data['filesystem'], folder_id)
1221
- current_view_folder_id = request.form.get('current_view_folder_id') # Where user was viewing from
1222
- redirect_to_folder_id = 'root' # Default redirect target
1223
-
1224
- if parent_node:
1225
- redirect_to_folder_id = parent_node.get('id', 'root')
1226
- elif current_view_folder_id: # Fallback to where user clicked delete
1227
- redirect_to_folder_id = current_view_folder_id
1228
-
1229
- if not folder_node or folder_node.get('type') != 'folder':
1230
- flash('Папка не найдена или не может быть удалена.', 'error')
1231
- return redirect(url_for('dashboard', folder_id=redirect_to_folder_id))
1232
-
1233
- folder_name = folder_node.get('name', 'папка')
1234
-
1235
- if folder_node.get('children'):
1236
- flash(f'Папку "{folder_name}" можно удалить только если она пуста.', 'error')
1237
- return redirect(url_for('dashboard', folder_id=current_view_folder_id or folder_id)) # Stay in current view or folder itself
1238
-
1239
- # Proceed with deletion
1240
- if remove_node(user_data['filesystem'], folder_id):
1241
- try:
1242
- save_data(data)
1243
- flash(f'Пустая папка "{folder_name}" успешно удалена.')
1244
- except Exception as e:
1245
- flash('Ошибка сохранения данных после удаления папки.', 'error')
1246
- logging.error(f"Delete empty folder save error for user {user_id}: {e}")
1247
- # Data inconsistency - folder removed in memory, not saved.
1248
- cache.clear() # Clear cache to force reload on next request
1249
- else:
1250
- flash('Не удалось удалить папку из базы данных (не найдена?).', 'error')
1251
-
1252
- return redirect(url_for('dashboard', folder_id=redirect_to_folder_id))
1253
-
1254
-
1255
- @app.route('/get_text_content/<file_id>')
1256
- @login_required # Require login
1257
- def get_text_content(file_id):
1258
- current_user_id = session['telegram_user']['id']
1259
- is_current_user_admin = current_user_id in ADMIN_TELEGRAM_IDS
1260
-
1261
- data = load_data()
1262
- file_node = None
1263
- owner_user_id_str = None
1264
-
1265
- # 1. Check current user's files
1266
- current_user_data = data['users'].get(str(current_user_id))
1267
- if current_user_data:
1268
- node, _ = find_node_by_id(current_user_data.get('filesystem', {}), file_id)
1269
- if node and node.get('type') == 'file' and node.get('file_type') == 'text':
1270
- file_node = node
1271
- owner_user_id_str = str(current_user_id)
1272
-
1273
- # 2. If admin and not found, check others
1274
- if not file_node and is_current_user_admin:
1275
- for uid_str, udata in data.get('users', {}).items():
1276
- node, _ = find_node_by_id(udata.get('filesystem', {}), file_id)
1277
- if node and node.get('type') == 'file' and node.get('file_type') == 'text':
1278
- file_node = node
1279
- owner_user_id_str = uid_str
1280
- break
1281
-
1282
- if not file_node:
1283
- return Response("Текстовый файл не найден или доступ запрещен", status=404)
1284
-
1285
- hf_path = file_node.get('path')
1286
- if not hf_path:
1287
- return Response("Ошибка: путь к файлу отсутствует в метаданных", status=500)
1288
-
1289
- file_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{hf_path}?download=true"
1290
-
1291
- try:
1292
- headers = {}
1293
- if HF_TOKEN_READ:
1294
- headers["authorization"] = f"Bearer {HF_TOKEN_READ}"
1295
-
1296
- response = requests.get(file_url, headers=headers, timeout=15) # Timeout for text files
1297
- response.raise_for_status()
1298
-
1299
- # Limit preview size
1300
- max_preview_size = 1 * 1024 * 1024 # 1MB
1301
- if len(response.content) > max_preview_size:
1302
- # Return truncated content with a warning
1303
- text_content_bytes = response.content[:max_preview_size]
1304
- warning_message = "\n\n[Файл слишком большой, показана только первая 1MB]"
1305
- try:
1306
- text_content = text_content_bytes.decode('utf-8', errors='ignore') + warning_message
1307
- except UnicodeDecodeError: # Should be caught by errors='ignore'
1308
- text_content = "[Не удалось декодировать начало файла]" + warning_message
1309
-
1310
- return Response(text_content, mimetype='text/plain')
1311
- # Or return an error:
1312
- # return Response("Файл слишком большой для предпросмотра ( > 1MB).", status=413)
1313
-
1314
-
1315
- # Try decoding
1316
- try:
1317
- text_content = response.content.decode('utf-8')
1318
- except UnicodeDecodeError:
1319
- try:
1320
- # Fallback to latin-1 or common windows encoding
1321
- text_content = response.content.decode('latin-1')
1322
- except UnicodeDecodeError:
1323
- try:
1324
- text_content = response.content.decode('cp1251')
1325
- except Exception:
1326
- return Response("Не удалось определить кодировку файла.", status=500)
1327
-
1328
- return Response(text_content, mimetype='text/plain; charset=utf-8') # Specify charset
1329
-
1330
- except requests.exceptions.Timeout:
1331
- logging.warning(f"Timeout fetching text content from HF ({hf_path})")
1332
- return Response("Тайм-аут при загрузке содержимого файла.", status=504)
1333
- except requests.exceptions.RequestException as e:
1334
- logging.error(f"Error fetching text content from HF ({hf_path}): {e}")
1335
- return Response(f"Ошибка загрузки содержимого: {e}", status=502)
1336
- except Exception as e:
1337
- logging.error(f"Unexpected error fetching text content ({hf_path}): {e}")
1338
- return Response("Внутренняя ошибка сервера", status=500)
1339
-
1340
-
1341
- # --- Admin Routes ---
1342
-
1343
- @app.route('/admhosto')
1344
- @admin_required
1345
- def admin_panel():
1346
- data = load_data()
1347
- users = data.get('users', {})
1348
-
1349
- user_details = []
1350
- for user_id_str, udata in users.items():
1351
- file_count = 0
1352
- folder_count = 0
1353
- q = [(udata.get('filesystem', {}))] # Start with root object
1354
- visited_ids = set()
1355
-
1356
- while q:
1357
- current_node = q.pop(0)
1358
- if not current_node or not isinstance(current_node, dict) or current_node.get('id') in visited_ids:
1359
- continue
1360
- visited_ids.add(current_node.get('id'))
1361
-
1362
- if current_node.get('type') == 'file':
1363
- file_count += 1
1364
- elif current_node.get('type') == 'folder':
1365
- if current_node.get('id') != 'root': # Don't count root itself
1366
- folder_count += 1
1367
- if 'children' in current_node and isinstance(current_node['children'], list):
1368
- for child in current_node['children']:
1369
- if isinstance(child, dict):
1370
- q.append(child)
1371
-
1372
-
1373
- user_details.append({
1374
- 'telegram_id': int(user_id_str),
1375
- 'username': udata.get('username', 'N/A'),
1376
- 'first_name': udata.get('first_name', ''),
1377
- 'last_name': udata.get('last_name', ''),
1378
- 'photo_url': udata.get('photo_url'),
1379
- 'created_at': udata.get('created_at', 'N/A'),
1380
- 'file_count': file_count,
1381
- 'folder_count': folder_count
1382
- })
1383
-
1384
- user_details.sort(key=lambda x: x.get('created_at', ''), reverse=True)
1385
-
1386
- html = '''
1387
- <!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
1388
- <title>Админ-панель</title>
1389
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
1390
- <style>''' + BASE_STYLE + '''
1391
- .user-item .actions { margin-left: auto; display: flex; gap: 5px; }
1392
- .user-item .actions .btn { padding: 5px 10px; font-size: 0.85em; }
1393
- </style>
1394
- <script src="https://telegram.org/js/telegram-web-app.js"></script>
1395
- </head><body>
1396
- <div class="container"><h1>Админ-панель</h1>
1397
- <a href="{{ url_for('dashboard') }}" class="btn" style="margin-bottom: 20px;">Назад в мой кабинет</a>
1398
- {% with messages = get_flashed_messages(with_categories=true) %}{% if messages %}{% for category, message in messages %}<div class="flash {{ category }}">{{ message }}</div>{% endfor %}{% endif %}{% endwith %}
1399
- <h2>Пользователи ({{ user_details|length }})</h2><div class="user-list">
1400
- {% for user in user_details %}
1401
- <div class="user-item">
1402
- <img src="{{ user.photo_url or 'https://via.placeholder.com/50/cccccc/969696?text=?' }}" alt="Avatar" class="avatar">
1403
- <div class="user-info">
1404
- <a href="{{ url_for('admin_user_files', user_id=user.telegram_id) }}">
1405
- <b>{{ user.first_name or '' }} {{ user.last_name or '' }}</b>
1406
- {% if user.username %}(@{{ user.username }}){% endif %}
1407
- </a>
1408
- <p style="font-size: 0.8em; color: var(--tg-theme-hint-color);">ID: {{ user.telegram_id }}</p>
1409
- <p style="font-size: 0.8em; color: var(--tg-theme-hint-color);">Зарегистрирован: {{ user.created_at }}</p>
1410
- <p style="font-size: 0.8em;">Файлов: {{ user.file_count }}, Папок: {{ user.folder_count }}</p>
1411
- </div>
1412
- <div class="actions">
1413
- <a href="{{ url_for('admin_user_files', user_id=user.telegram_id) }}" class="btn folder-btn">Файлы</a>
1414
- <form method="POST" action="{{ url_for('admin_delete_user', user_id=user.telegram_id) }}" style="display: inline;" onsubmit="return confirmAdminDelete('УДАЛИТЬ пользователя {{ user.username or user.telegram_id }} и ВСЕ его данные? НЕОБРАТИМО!');">
1415
- <button type="submit" class="btn delete-btn">Удалить</button>
1416
- </form>
1417
- </div>
1418
- </div>
1419
- {% else %}<p>Пользователей нет.</p>{% endfor %}</div></div>
1420
- <script>
1421
- window.Telegram.WebApp.ready();
1422
- function confirmAdminDelete(message) {
1423
- return new Promise((resolve) => {
1424
- window.Telegram.WebApp.showConfirm(message, (confirmed) => {
1425
- resolve(confirmed);
1426
- });
1427
- });
1428
- }
1429
- document.querySelectorAll('form[onsubmit^="return confirmAdminDelete"]').forEach(form => {
1430
- form.addEventListener('submit', async function(e) {
1431
- e.preventDefault();
1432
- const message = this.getAttribute('onsubmit').match(/confirmAdminDelete\('(.*)'\)/)[1];
1433
- const confirmed = await confirmAdminDelete(message);
1434
- if (confirmed) {
1435
- this.submit();
1436
- }
1437
- });
1438
- });
1439
- if (window.Telegram.WebApp.colorScheme === 'dark') {
1440
- document.documentElement.classList.add('dark-mode');
1441
- }
1442
- </script>
1443
- </body></html>'''
1444
- return render_template_string(html, user_details=user_details)
1445
-
1446
- @app.route('/admhosto/user/<int:user_id>')
1447
- @admin_required
1448
- def admin_user_files(user_id):
1449
- user_id_str = str(user_id)
1450
- data = load_data()
1451
- user_data = data.get('users', {}).get(user_id_str)
1452
- if not user_data:
1453
- flash(f'Пользователь с ID {user_id} не найден.', 'error')
1454
- return redirect(url_for('admin_panel'))
1455
-
1456
- user_info = {
1457
- 'telegram_id': user_id,
1458
- 'username': user_data.get('username', 'N/A'),
1459
- 'first_name': user_data.get('first_name', ''),
1460
- 'last_name': user_data.get('last_name', ''),
1461
- 'photo_url': user_data.get('photo_url'),
1462
- }
1463
-
1464
- all_files = []
1465
- def collect_files_recursive(folder_node, current_path_str="Root"):
1466
- if not folder_node or folder_node.get('type') != 'folder': return
1467
- for item in folder_node.get('children', []):
1468
- if item.get('type') == 'file':
1469
- item['parent_path_str'] = current_path_str
1470
- all_files.append(item)
1471
- elif item.get('type') == 'folder':
1472
- folder_name = item.get('name', 'Unnamed Folder')
1473
- new_path = f"{current_path_str} / {folder_name}"
1474
- collect_files_recursive(item, new_path)
1475
-
1476
- collect_files_recursive(user_data.get('filesystem', {}))
1477
- all_files.sort(key=lambda x: x.get('upload_date', ''), reverse=True)
1478
-
1479
-
1480
- html = '''
1481
- <!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"><title>Файлы {{ user_info.username or user_info.telegram_id }}</title>
1482
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
1483
- <style>''' + BASE_STYLE + '''
1484
- .file-item { background: var(--card-bg); padding: 12px; border-radius: 14px; box-shadow: var(--shadow); transition: var(--transition); display: flex; flex-direction: column; justify-content: space-between; }
1485
- html.dark-mode .file-item { background: var(--card-bg-dark); }
1486
- .file-item:hover { transform: translateY(-4px); }
1487
- .file-preview { max-width: 100%; height: 100px; object-fit: contain; border-radius: 8px; margin-bottom: 8px; display: block; margin-left: auto; margin-right: auto; cursor: pointer; }
1488
- .admin-file-actions { margin-top: 8px; display: flex; flex-wrap: wrap; gap: 4px; justify-content: center; }
1489
- .admin-file-actions .btn { font-size: 0.75em; padding: 3px 6px; margin: 0; }
1490
- .user-header { display: flex; align-items: center; gap: 15px; margin-bottom: 20px; }
1491
- .user-header img.avatar { width: 60px; height: 60px; border-radius: 50%; }
1492
- </style>
1493
- <script src="https://telegram.org/js/telegram-web-app.js"></script>
1494
- </head><body><div class="container">
1495
-
1496
- <div class="user-header">
1497
- <img src="{{ user_info.photo_url or 'https://via.placeholder.com/60/cccccc/969696?text=?' }}" alt="Avatar" class="avatar">
1498
- <div>
1499
- <h1>Файлы пользователя: {{ user_info.first_name or '' }} {{ user_info.last_name or '' }}</h1>
1500
- <p style="color: var(--tg-theme-hint-color);">{% if user_info.username %}@{{ user_info.username }} | {% endif %}ID: {{ user_info.telegram_id }}</p>
1501
- </div>
1502
- </div>
1503
-
1504
- <a href="{{ url_for('admin_panel') }}" class="btn" style="margin-bottom: 20px;">Назад к пользователям</a>
1505
- {% with messages = get_flashed_messages(with_categories=true) %}{% if messages %}{% for category, message in messages %}<div class="flash {{ category }}">{{ message }}</div>{% endfor %}{% endif %}{% endwith %}
1506
-
1507
- <div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 15px;">
1508
- {% for file in files %}
1509
- <div class="file-item">
1510
- <div>
1511
- {% if file.file_type == 'image' %} <img class="file-preview" src="{{ hf_file_url(file.path) }}" loading="lazy" onclick="openModal('{{ hf_file_url(file.path) }}', '{{ file.file_type }}', '{{ file.id }}')">
1512
- {% elif file.file_type == 'video' %} <video class="file-preview" preload="metadata" muted onclick="openModal('{{ hf_file_url(file.path) }}', '{{ file.file_type }}', '{{ file.id }}')"><source src="{{ hf_file_url(file.path, True) }}#t=0.5"></video>
1513
- {% elif file.file_type == 'pdf' %} <div class="file-preview" style="font-size: 40px; line-height: 100px; text-align: center; color: var(--accent);" onclick="openModal('{{ hf_file_url(file.path, True) }}', '{{ file.file_type }}', '{{ file.id }}')">📄</div>
1514
- {% elif file.file_type == 'text' %} <div class="file-preview" style="font-size: 40px; line-height: 100px; text-align: center; color: var(--secondary);" onclick="openModal('{{ url_for('get_text_content', file_id=file.id) }}', '{{ file.file_type }}', '{{ file.id }}')">📝</div>
1515
- {% else %} <div class="file-preview" style="font-size: 40px; line-height: 100px; text-align: center; color: var(--tg-theme-hint-color);">❓</div> {% endif %}
1516
- <p title="{{ file.original_filename }}"><b>{{ file.original_filename | truncate(30) }}</b></p>
1517
- <p style="font-size: 0.8em; color: var(--tg-theme-hint-color);">В папке: {{ file.parent_path_str }}</p>
1518
- <p style="font-size: 0.8em; color: var(--tg-theme-hint-color);">Загружен: {{ file.upload_date }}</p>
1519
- <p style="font-size: 0.7em; color: var(--tg-theme-hint-color);">ID: {{ file.id }}</p>
1520
- <p style="font-size: 0.7em; color: var(--tg-theme-hint-color); word-break: break-all;" title="{{file.path}}">Path: {{ file.path | truncate(40) }}</p>
1521
- </div>
1522
- <div class="admin-file-actions">
1523
- <a href="{{ url_for('download_file', file_id=file.id) }}" class="btn download-btn">Скачать</a>
1524
- {% set previewable = file.file_type in ['image', 'video', 'pdf', 'text'] %}
1525
- {% if previewable %}
1526
- <button class="btn" style="background: var(--accent);"
1527
- onclick="openModal('{{ hf_file_url(file.path) if file.file_type != 'text' else url_for('get_text_content', file_id=file.id) }}', '{{ file.file_type }}', '{{ file.id }}')">Просмотр</button>
1528
- {% endif %}
1529
- <form method="POST" action="{{ url_for('admin_delete_file', user_id=user_info.telegram_id, file_id=file.id) }}" style="display: inline-block;" onsubmit="return confirmAdminDeleteFile('Удалить файл {{ file.original_filename }}?');">
1530
- <button type="submit" class="btn delete-btn">Удалить</button>
1531
- </form>
1532
- </div>
1533
- </div>
1534
- {% else %} <p>У пользователя нет файлов.</p> {% endfor %}
1535
- </div></div>
1536
-
1537
- <div class="modal" id="mediaModal" onclick="closeModal(event)">
1538
- <div class="modal-content" id="modalContentContainer">
1539
- <span onclick="closeModalManual()" class="modal-close-btn">×</span>
1540
- <div id="modalContent"></div>
1541
- </div>
1542
- </div>
1543
-
1544
- <script>
1545
- const repoId = "{{ repo_id }}";
1546
- const tg = window.Telegram.WebApp;
1547
- tg.ready();
1548
- if (tg.colorScheme === 'dark') {
1549
- document.documentElement.classList.add('dark-mode');
1550
- }
1551
-
1552
- function hfFileUrl(path, download = false) {
1553
- let url = `https://huggingface.co/datasets/${repoId}/resolve/main/${path}`;
1554
- if (download) url += '?download=true';
1555
- return url;
1556
- }
1557
- async function openModal(srcOrUrl, type, itemId) {
1558
- // Same modal logic as in dashboard
1559
- const modal = document.getElementById('mediaModal');
1560
- const modalContent = document.getElementById('modalContent');
1561
- modalContent.innerHTML = '<p>Загрузка...</p>';
1562
- modal.style.display = 'flex';
1563
- tg.expand();
1564
-
1565
- try {
1566
- const headers = {}; // Add HF_TOKEN_READ header if needed for fetch
1567
- // if (hfTokenRead) { headers['Authorization'] = `Bearer ${hfTokenRead}`; }
1568
-
1569
- if (type === 'image') {
1570
- modalContent.innerHTML = `<img src="${srcOrUrl}" alt="Просмотр изображения">`;
1571
- } else if (type === 'video') {
1572
- modalContent.innerHTML = `<video controls autoplay style='max-width: 95%; max-height: 85vh;'><source src="${srcOrUrl}" type="video/mp4">Ваш браузер не поддерживает видео.</video>`;
1573
- } else if (type === 'pdf') {
1574
- modalContent.innerHTML = `<iframe src="${srcOrUrl}" title="Просмотр PDF"></iframe>`;
1575
- } else if (type === 'text') {
1576
- const response = await fetch(srcOrUrl, { headers: headers });
1577
- if (!response.ok) throw new Error(`Ошибка загрузки текста: ${response.statusText}`);
1578
- const text = await response.text();
1579
- const escapedText = text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
1580
- modalContent.innerHTML = `<pre>${escapedText}</pre>`;
1581
- } else {
1582
- modalContent.innerHTML = '<p>Предпросмотр для этого типа файла не поддерживается.</p>';
1583
- }
1584
- } catch (error) {
1585
- console.error("Error loading modal content:", error);
1586
- modalContent.innerHTML = `<p>Не удалось загрузить содержимое для предпросмотра. ${error.message}</p>`;
1587
- tg.showAlert('Ошибка предпросмотра: ' + error.message);
1588
- }
1589
- }
1590
-
1591
- function closeModal(event) {
1592
- const modal = document.getElementById('mediaModal');
1593
- if (event.target === modal) {
1594
- closeModalManual();
1595
- }
1596
- }
1597
-
1598
- function closeModalManual() {
1599
- const modal = document.getElementById('mediaModal');
1600
- modal.style.display = 'none';
1601
- const video = modal.querySelector('video');
1602
- if (video) video.pause();
1603
- const iframe = modal.querySelector('iframe');
1604
- if (iframe) iframe.src = 'about:blank';
1605
- document.getElementById('modalContent').innerHTML = '';
1606
- }
1607
-
1608
- function confirmAdminDeleteFile(message) {
1609
- return new Promise((resolve) => {
1610
- tg.showConfirm(message, (confirmed) => {
1611
- resolve(confirmed);
1612
- });
1613
- });
1614
- }
1615
- document.querySelectorAll('form[onsubmit^="return confirmAdminDeleteFile"]').forEach(form => {
1616
- form.addEventListener('submit', async function(e) {
1617
- e.preventDefault();
1618
- const message = this.getAttribute('onsubmit').match(/confirmAdminDeleteFile\('(.*)'\)/)[1];
1619
- const confirmed = await confirmAdminDeleteFile(message);
1620
- if (confirmed) {
1621
- this.submit();
1622
- }
1623
- });
1624
- });
1625
-
1626
- </script>
1627
- </body></html>'''
1628
- return render_template_string(html,
1629
- user_info=user_info,
1630
- files=all_files,
1631
- repo_id=REPO_ID,
1632
- hf_file_url=lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{path}{'?download=true' if download else ''}",
1633
- HF_TOKEN_READ=HF_TOKEN_READ)
1634
-
1635
-
1636
- @app.route('/admhosto/delete_user/<int:user_id>', methods=['POST'])
1637
- @admin_required
1638
- def admin_delete_user(user_id):
1639
- admin_user_id = session['telegram_user']['id']
1640
- user_id_str = str(user_id)
1641
-
1642
- if not HF_TOKEN_WRITE:
1643
- flash('Удаление невозможно: токен для записи Hugging Face не настроен.', 'error')
1644
- return redirect(url_for('admin_panel'))
1645
-
1646
- data = load_data()
1647
- if user_id_str not in data['users']:
1648
- flash('Пользователь не найден!', 'error')
1649
- return redirect(url_for('admin_panel'))
1650
-
1651
- user_data_to_delete = data['users'][user_id_str]
1652
- username_for_log = user_data_to_delete.get('username', user_id_str)
1653
- logging.warning(f"ADMIN ACTION by {admin_user_id}: Attempting to delete user {username_for_log} (ID: {user_id_str}) and all their data.")
1654
-
1655
- # --- Attempt to delete from Hugging Face first ---
1656
- hf_delete_successful = False
1657
- try:
1658
- api = HfApi()
1659
- # Path uses user ID
1660
- user_folder_path_on_hf = f"cloud_files/{user_id_str}"
1661
-
1662
- logging.info(f"Attempting to delete HF Hub folder: {user_folder_path_on_hf} for user {user_id_str}")
1663
- # Note: delete_folder might require listing files first if it's not empty.
1664
- # A safer approach might be listing and deleting files individually, then the folder.
1665
- # However, let's try delete_folder directly first. It might handle non-empty folders.
1666
- # Update: delete_folder usually expects an *empty* folder. Robust deletion needs listing+deleting files.
1667
- # Let's try deleting the folder path prefix, which might work better.
1668
- objects_to_delete = api.list_repo_tree(repo_id=REPO_ID, repo_type="dataset", path_in_repo=user_folder_path_on_hf, token=HF_TOKEN_READ, recursive=True)
1669
-
1670
- paths_to_delete = [obj.path for obj in objects_to_delete]
1671
-
1672
- if paths_to_delete:
1673
- logging.info(f"Found {len(paths_to_delete)} items in {user_folder_path_on_hf} to delete.")
1674
- # Delete files first
1675
- for path in paths_to_delete:
1676
- if not path.endswith('/'): # Assuming paths ending in / are folders handled implicitly or need separate deletion
1677
- try:
1678
- api.delete_file(path_in_repo=path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE)
1679
- logging.info(f"Admin deleted HF file: {path}")
1680
- except hf_utils.EntryNotFoundError:
1681
- logging.warning(f"File {path} not found during bulk delete, skipping.")
1682
- except Exception as file_del_e:
1683
- logging.error(f"Error deleting file {path} during user cleanup: {file_del_e}")
1684
- # Optionally decide whether to abort the whole user deletion
1685
- # Try deleting the folder itself after files are gone (might still fail if structure complex)
1686
- try:
1687
- api.delete_folder(folder_path=user_folder_path_on_hf, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE,
1688
- commit_message=f"ADMIN ACTION by {admin_user_id}: Deleted folder for user {user_id_str}")
1689
- logging.info(f"Successfully deleted folder {user_folder_path_on_hf} on HF Hub.")
1690
- hf_delete_successful = True # Mark as successful if folder delete works
1691
- except hf_utils.HfHubHTTPError as e:
1692
- if e.response.status_code == 404 or "is not empty" in str(e): # Folder might be gone or implicitly deleted
1693
- logging.warning(f"Folder {user_folder_path_on_hf} possibly already gone or non-empty after file deletion attempt. Assuming HF cleanup done.")
1694
- hf_delete_successful = True # Count as success if folder seems gone
1695
- else: raise e # Re-raise other HF errors
1696
- except Exception as folder_del_e:
1697
- logging.error(f"Error deleting folder {user_folder_path_on_hf} after file deletion: {folder_del_e}")
1698
- # Don't mark hf_delete_successful = True
1699
- else:
1700
- logging.info(f"No objects found in HF path {user_folder_path_on_hf}. Assuming HF cleanup not needed or already done.")
1701
- hf_delete_successful = True # No files to delete = success
1702
-
1703
-
1704
- except hf_utils.HfHubHTTPError as e:
1705
- if e.response.status_code == 404: # Initial listing failed (folder never existed)
1706
- logging.warning(f"User folder {user_folder_path_on_hf} not found on HF Hub for user {user_id_str}. Skipping HF deletion.")
1707
- hf_delete_successful = True # Consider it success as there's nothing to delete
1708
- else:
1709
- logging.error(f"Error during HF cleanup for {user_id_str}: {e}")
1710
- flash(f'Ошибка при удалении файлов пользователя {username_for_log} с сервера: {e}. Пользователь НЕ удален из базы.', 'error')
1711
- return redirect(url_for('admin_panel'))
1712
- except Exception as e:
1713
- logging.error(f"Unexpected error during HF Hub data deletion for {user_id_str}: {e}")
1714
- flash(f'Неожиданная ошибка при удалении файлов {username_for_log} с сервера: {e}. Пользователь НЕ удален из базы.', 'error')
1715
- return redirect(url_for('admin_panel'))
1716
-
1717
- # --- Proceed with DB deletion only if HF deletion was deemed successful or skipped ---
1718
- if hf_delete_successful:
1719
- try:
1720
- del data['users'][user_id_str]
1721
- save_data(data)
1722
- flash(f'Пользователь {username_for_log} (ID: {user_id_str}) и его данные успешно удалены из базы данных!')
1723
- logging.info(f"ADMIN ACTION by {admin_user_id}: Successfully deleted user {user_id_str} from database.")
1724
- except Exception as e:
1725
- logging.error(f"CRITICAL: Error saving data after deleting user {user_id_str} from dict. DB MIGHT BE INCONSISTENT. HF data likely deleted. Error: {e}")
1726
- flash(f'Данные пользователя {username_for_log} удалены с сервера, но произошла КРИТИЧЕСКАЯ ОШИБКА при удалении пользователя из базы данных: {e}', 'error')
1727
- cache.clear()
1728
- else:
1729
- flash(f'Удаление пользователя {username_for_log} из базы отменено из-за ошибки при удалении файлов с сервера.', 'error')
1730
-
1731
-
1732
- return redirect(url_for('admin_panel'))
1733
-
1734
-
1735
- @app.route('/admhosto/delete_file/<int:user_id>/<file_id>', methods=['POST'])
1736
- @admin_required
1737
- def admin_delete_file(user_id, file_id):
1738
- admin_user_id = session['telegram_user']['id']
1739
- user_id_str = str(user_id)
1740
-
1741
- if not HF_TOKEN_WRITE:
1742
- flash('Удаление невозможно: токен для записи Hugging Face не настроен.', 'error')
1743
- return redirect(url_for('admin_user_files', user_id=user_id))
1744
-
1745
- data = load_data()
1746
- user_data = data.get('users', {}).get(user_id_str)
1747
- if not user_data:
1748
- flash(f'Пользователь {user_id_str} не найден.', 'error')
1749
- return redirect(url_for('admin_panel'))
1750
-
1751
- file_node, parent_node = find_node_by_id(user_data.get('filesystem',{}), file_id)
1752
-
1753
- if not file_node or file_node.get('type') != 'file':
1754
- flash('Файл не найден в структуре пользователя.', 'error')
1755
- return redirect(url_for('admin_user_files', user_id=user_id))
1756
-
1757
- hf_path = file_node.get('path')
1758
- original_filename = file_node.get('original_filename', 'файл')
1759
- username_for_log = user_data.get('username', user_id_str)
1760
-
1761
- # Try removing from DB first
1762
- if remove_node(user_data['filesystem'], file_id):
1763
- try:
1764
- save_data(data)
1765
- logging.info(f"ADMIN ACTION by {admin_user_id}: Removed file {file_id} ({original_filename}) from DB for user {username_for_log} ({user_id_str}).")
1766
-
1767
- # Now delete from HF
1768
- if hf_path:
1769
- try:
1770
- api = HfApi()
1771
- api.delete_file(
1772
- path_in_repo=hf_path,
1773
- repo_id=REPO_ID,
1774
- repo_type="dataset",
1775
- token=HF_TOKEN_WRITE,
1776
- commit_message=f"ADMIN ACTION by {admin_user_id}: Deleted file {original_filename} (ID: {file_id}) for user {user_id_str}"
1777
- )
1778
- logging.info(f"ADMIN ACTION by {admin_user_id}: Deleted file {hf_path} from HF Hub for user {user_id_str}")
1779
- flash(f'Файл {original_filename} успешно удален (админ)!')
1780
- except hf_utils.EntryNotFoundError:
1781
- logging.warning(f"ADMIN ACTION: File {hf_path} not found on HF Hub during delete for user {user_id_str}, but removed from DB.")
1782
- flash(f'Файл {original_filename} удален из базы (не найден на сервере) (админ).')
1783
- except Exception as e:
1784
- logging.error(f"ADMIN ACTION: Error deleting file {hf_path} from HF Hub for user {user_id_str} (DB entry removed): {e}")
1785
- flash(f'Файл {original_filename} удален из базы, но ошибка при удалении с сервера: {e} (админ)', 'error')
1786
- else:
1787
- flash(f'Файл {original_filename} удален из базы (путь не найден) (админ).')
1788
-
1789
- except Exception as e:
1790
- logging.critical(f"CRITICAL ADMIN ACTION: Failed to save DB after removing file {file_id} for user {user_id_str}. Data inconsistency possible! Error: {e}")
1791
- flash('Критическая ошибка: не удалось сохранить базу данных после удаления файла (админ).', 'error')
1792
- cache.clear()
1793
- else:
1794
- flash('Не удалось найти файл в структуре для удаления (админ).', 'error')
1795
-
1796
-
1797
- return redirect(url_for('admin_user_files', user_id=user_id))
1798
-
1799
-
1800
- # --- Main Execution ---
1801
-
1802
- if __name__ == '__main__':
1803
- app.permanent_session_lifetime = timedelta(days=30) # Extend session lifetime
1804
-
1805
- if not TELEGRAM_BOT_TOKEN:
1806
- logging.critical("FATAL: TELEGRAM_BOT_TOKEN environment variable is not set. Application cannot verify users.")
1807
- exit(1)
1808
- if not ADMIN_TELEGRAM_IDS:
1809
- logging.warning("ADMIN_TELEGRAM_IDS environment variable is not set or empty. Admin panel will not be accessible.")
1810
- else:
1811
- logging.info(f"Admin users configured: {ADMIN_TELEGRAM_IDS}")
1812
-
1813
- if not HF_TOKEN_WRITE:
1814
- logging.warning("HF_TOKEN (write access) is not set. File uploads, deletions, and backups will fail.")
1815
- if not HF_TOKEN_READ:
1816
- logging.warning("HF_TOKEN_READ is not set. Falling back to HF_TOKEN. File downloads/previews might fail for private repos if HF_TOKEN is also not set.")
1817
-
1818
- if HF_TOKEN_WRITE:
1819
- logging.info("Performing initial database download before starting background backup.")
1820
- download_db_from_hf() # Download before starting backup thread
1821
- backup_thread = threading.Thread(target=periodic_backup, daemon=True)
1822
- backup_thread.start()
1823
- logging.info("Periodic backup thread started.")
1824
- elif HF_TOKEN_READ:
1825
- logging.info("Write token not found. Performing initial database download (read-only mode). Backups disabled.")
1826
- download_db_from_hf()
1827
- else:
1828
- logging.critical("Neither HF_TOKEN nor HF_TOKEN_READ is set. Hugging Face operations disabled. Loading/creating local DB only.")
1829
- if not os.path.exists(DATA_FILE):
1830
- with open(DATA_FILE, 'w', encoding='utf-8') as f:
1831
- json.dump({'users': {}}, f)
1832
- logging.info(f"Created empty local database file: {DATA_FILE}")
1833
- else:
1834
- logging.info(f"Using existing local database file: {DATA_FILE}")
1835
-
1836
-
1837
- app.run(debug=False, host='0.0.0.0', port=7860)
1838
-
1839
- # --- END OF FILE app.py ---