Eluza133 commited on
Commit
a3d651f
·
verified ·
1 Parent(s): d127bf1

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -1398
app.py DELETED
@@ -1,1398 +0,0 @@
1
- import os
2
- import hmac
3
- import hashlib
4
- import json
5
- from urllib.parse import unquote, parse_qsl, urlencode
6
- from flask import Flask, request, jsonify, Response, send_file
7
- from flask_caching import Cache
8
- import logging
9
- import threading
10
- import time
11
- from datetime import datetime
12
- from huggingface_hub import HfApi, hf_hub_download, utils as hf_utils
13
- from werkzeug.utils import secure_filename
14
- import requests
15
- from io import BytesIO
16
- import uuid
17
- from typing import Union, Optional # <-- Импорт добавлен
18
-
19
- # --- Configuration ---
20
- app = Flask(__name__)
21
- app.secret_key = os.getenv("FLASK_SECRET_KEY", "supersecretkey_mini_app_unique")
22
- BOT_TOKEN = os.getenv('TELEGRAM_BOT_TOKEN', '6750208873:AAE2hvPlJ99dBdhGa_Brre0IIpUdOvXxHt4') # MUST be set
23
- DATA_FILE = 'cloudeng_mini_app_data.json'
24
- REPO_ID = "Eluza133/Z1e1u" # Same HF Repo
25
- HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
26
- HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") or HF_TOKEN_WRITE
27
- UPLOAD_FOLDER = 'uploads_mini_app'
28
- os.makedirs(UPLOAD_FOLDER, exist_ok=True)
29
-
30
- # --- Caching and Logging ---
31
- cache = Cache(app, config={'CACHE_TYPE': 'simple'})
32
- logging.basicConfig(level=logging.INFO)
33
-
34
- # --- Constants ---
35
- AUTH_DATA_LIFETIME = 3600 # 1 hour validity for initData
36
-
37
- # --- Filesystem Utilities ---
38
- def find_node_by_id(filesystem, node_id):
39
- if not filesystem or not isinstance(filesystem, dict):
40
- return None, None
41
- if filesystem.get('id') == node_id:
42
- return filesystem, None
43
-
44
- queue = [(filesystem, None)]
45
- visited = {filesystem.get('id')}
46
-
47
- while queue:
48
- current_node, parent = queue.pop(0)
49
- if current_node.get('type') == 'folder' and 'children' in current_node:
50
- for child in current_node.get('children', []):
51
- child_id = child.get('id')
52
- if not child_id: continue # Skip nodes without id
53
-
54
- if child_id == node_id:
55
- return child, current_node
56
- if child_id not in visited and child.get('type') == 'folder':
57
- visited.add(child_id)
58
- queue.append((child, current_node))
59
- return None, None
60
-
61
- def add_node(filesystem, parent_id, node_data):
62
- parent_node, _ = find_node_by_id(filesystem, parent_id)
63
- if parent_node and parent_node.get('type') == 'folder':
64
- if 'children' not in parent_node:
65
- parent_node['children'] = []
66
- existing_ids = {child.get('id') for child in parent_node['children']}
67
- if node_data.get('id') not in existing_ids:
68
- parent_node['children'].append(node_data)
69
- return True
70
- return False
71
-
72
- def remove_node(filesystem, node_id):
73
- node_to_remove, parent_node = find_node_by_id(filesystem, node_id)
74
- if node_to_remove and parent_node and 'children' in parent_node:
75
- original_length = len(parent_node['children'])
76
- parent_node['children'] = [child for child in parent_node['children'] if child.get('id') != node_id]
77
- return len(parent_node['children']) < original_length
78
- if node_to_remove and node_id == filesystem.get('id'):
79
- logging.warning("Attempted to remove root node directly.")
80
- return False
81
- return False
82
-
83
- def get_node_path_list(filesystem, node_id):
84
- path_list = []
85
- current_id = node_id
86
- processed_ids = set()
87
- while current_id and current_id not in processed_ids:
88
- processed_ids.add(current_id)
89
- node, parent = find_node_by_id(filesystem, current_id)
90
- if not node:
91
- break
92
- path_list.append({
93
- 'id': node.get('id'),
94
- 'name': node.get('name', node.get('original_filename', 'Unknown'))
95
- })
96
- if not parent:
97
- break
98
- parent_id = parent.get('id')
99
- if parent_id == current_id:
100
- logging.error(f"Filesystem loop detected at node {current_id}")
101
- break
102
- current_id = parent_id
103
- if not any(p['id'] == 'root' for p in path_list):
104
- path_list.append({'id': 'root', 'name': 'Root'})
105
- final_path = []
106
- seen_ids = set()
107
- for item in reversed(path_list):
108
- if item['id'] not in seen_ids:
109
- final_path.append(item)
110
- seen_ids.add(item['id'])
111
- return final_path
112
-
113
- def initialize_user_filesystem(user_data):
114
- if 'filesystem' not in user_data or not isinstance(user_data['filesystem'], dict):
115
- user_data['filesystem'] = {
116
- "type": "folder",
117
- "id": "root",
118
- "name": "Root",
119
- "children": []
120
- }
121
-
122
- # --- Data Loading/Saving ---
123
- @cache.memoize(timeout=120)
124
- def load_data():
125
- try:
126
- download_db_from_hf()
127
- with open(DATA_FILE, 'r', encoding='utf-8') as file:
128
- data = json.load(file)
129
- if not isinstance(data, dict):
130
- logging.warning("Data file is not a dict, initializing empty.")
131
- return {'users': {}}
132
- data.setdefault('users', {})
133
- for user_id, user_data in data['users'].items():
134
- initialize_user_filesystem(user_data)
135
- logging.info("Data loaded and filesystems checked/initialized.")
136
- return data
137
- except FileNotFoundError:
138
- logging.warning(f"{DATA_FILE} not found locally. Initializing empty data.")
139
- return {'users': {}}
140
- except json.JSONDecodeError:
141
- logging.error(f"Error decoding JSON from {DATA_FILE}. Returning empty data.")
142
- return {'users': {}}
143
- except Exception as e:
144
- logging.error(f"Error loading data: {e}")
145
- return {'users': {}}
146
-
147
- def save_data(data):
148
- try:
149
- with open(DATA_FILE, 'w', encoding='utf-8') as file:
150
- json.dump(data, file, ensure_ascii=False, indent=4)
151
- upload_db_to_hf()
152
- cache.clear()
153
- logging.info("Data saved locally and upload to HF initiated.")
154
- except Exception as e:
155
- logging.error(f"Error saving data: {e}")
156
-
157
- def upload_db_to_hf():
158
- if not HF_TOKEN_WRITE:
159
- logging.warning("HF_TOKEN_WRITE not set, skipping database upload.")
160
- return
161
- try:
162
- api = HfApi()
163
- api.upload_file(
164
- path_or_fileobj=DATA_FILE,
165
- path_in_repo=DATA_FILE,
166
- repo_id=REPO_ID,
167
- repo_type="dataset",
168
- token=HF_TOKEN_WRITE,
169
- commit_message=f"Backup MiniApp {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
170
- run_as_future=True
171
- )
172
- logging.info("Database upload to Hugging Face scheduled.")
173
- except Exception as e:
174
- logging.error(f"Error scheduling database upload: {e}")
175
-
176
- def download_db_from_hf():
177
- if not HF_TOKEN_READ:
178
- logging.warning("HF_TOKEN_READ not set, skipping database download.")
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
- logging.info(f"Created empty local database file: {DATA_FILE}")
183
- return
184
- try:
185
- hf_hub_download(
186
- repo_id=REPO_ID,
187
- filename=DATA_FILE,
188
- repo_type="dataset",
189
- token=HF_TOKEN_READ,
190
- local_dir=".",
191
- local_dir_use_symlinks=False,
192
- force_download=True,
193
- etag_timeout=10
194
- )
195
- logging.info("Database downloaded from Hugging Face")
196
- except hf_utils.RepositoryNotFoundError:
197
- logging.error(f"Repository {REPO_ID} not found.")
198
- if not os.path.exists(DATA_FILE):
199
- with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}}, f)
200
- except hf_utils.EntryNotFoundError:
201
- logging.warning(f"{DATA_FILE} not found in repo {REPO_ID}. Using/Creating local.")
202
- if not os.path.exists(DATA_FILE):
203
- with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}}, f)
204
- except requests.exceptions.ConnectionError as e:
205
- logging.error(f"Connection error downloading DB from HF: {e}. Using local version if available.")
206
- except Exception as e:
207
- logging.error(f"Error downloading database: {e}")
208
- if not os.path.exists(DATA_FILE):
209
- with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}}, f)
210
-
211
- # --- File Type Helper ---
212
- def get_file_type(filename):
213
- if not filename or '.' not in filename: return 'other'
214
- ext = filename.lower().split('.')[-1]
215
- if ext in ['mp4', 'mov', 'avi', 'webm', 'mkv']: return 'video'
216
- if ext in ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg']: return 'image'
217
- if ext == 'pdf': return 'pdf'
218
- if ext == 'txt': return 'text'
219
- return 'other'
220
-
221
- # --- Telegram Validation ---
222
- # Use Optional[dict] which is equivalent to Union[dict, None]
223
- def check_telegram_authorization(auth_data: str, bot_token: str) -> Optional[dict]:
224
- if not auth_data or not bot_token or bot_token == 'YOUR_BOT_TOKEN':
225
- logging.warning("Validation skipped: Missing auth_data or valid BOT_TOKEN.")
226
- return None
227
- try:
228
- parsed_data = dict(parse_qsl(unquote(auth_data)))
229
- if "hash" not in parsed_data:
230
- logging.error("Hash not found in auth data")
231
- return None
232
-
233
- telegram_hash = parsed_data.pop('hash')
234
- auth_date_ts = int(parsed_data.get('auth_date', 0))
235
- current_ts = int(time.time())
236
-
237
- if abs(current_ts - auth_date_ts) > AUTH_DATA_LIFETIME:
238
- logging.warning(f"Auth data expired (Auth: {auth_date_ts}, Now: {current_ts}, Diff: {current_ts - auth_date_ts})")
239
- return None
240
-
241
- data_check_string = "\n".join(sorted([f"{k}={v}" for k, v in parsed_data.items()]))
242
- secret_key = hmac.new("WebAppData".encode(), bot_token.encode(), hashlib.sha256).digest()
243
- calculated_hash = hmac.new(secret_key, data_check_string.encode(), hashlib.sha256).hexdigest()
244
-
245
- if calculated_hash == telegram_hash:
246
- user_data_str = parsed_data.get('user')
247
- if user_data_str:
248
- try:
249
- user_info = json.loads(user_data_str)
250
- if 'id' not in user_info:
251
- logging.error("Validated user data missing 'id'")
252
- return None
253
- return user_info # Success
254
- except json.JSONDecodeError:
255
- logging.error("Failed to decode user JSON from auth data")
256
- return None
257
- else:
258
- logging.warning("No 'user' field in validated auth data")
259
- return None # Require user field
260
- else:
261
- logging.warning("Hash mismatch during validation")
262
- return None
263
- except Exception as e:
264
- logging.error(f"Exception during validation: {e}")
265
- return None
266
-
267
-
268
- # --- HTML, CSS, JS Template ---
269
- HTML_TEMPLATE = """
270
- <!DOCTYPE html>
271
- <html lang="ru">
272
- <head>
273
- <meta charset="UTF-8">
274
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
275
- <title>Zeus Cloud Mini App</title>
276
- <script src="https://telegram.org/js/telegram-web-app.js"></script>
277
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
278
- <style>
279
- :root {
280
- --primary: #ff4d6d; --secondary: #00ddeb; --accent: #8b5cf6;
281
- --tg-theme-bg-color: var(--tg-bg-color, #ffffff);
282
- --tg-theme-text-color: var(--tg-text-color, #000000);
283
- --tg-theme-hint-color: var(--tg-hint-color, #999999);
284
- --tg-theme-link-color: var(--tg-link-color, #2481cc);
285
- --tg-theme-button-color: var(--tg-button-color, #5288c1);
286
- --tg-theme-button-text-color: var(--tg-button-text-color, #ffffff);
287
- --tg-theme-secondary-bg-color: var(--tg-secondary-bg-color, #f1f1f1);
288
- --tg-viewport-height: var(--tg-viewport-stable-height, 100vh);
289
- --card-bg: var(--tg-theme-secondary-bg-color);
290
- --text-light: var(--tg-theme-text-color);
291
- --text-dark: var(--tg-theme-text-color); /* Simplified for TG theme */
292
- --shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
293
- --glass-bg: rgba(128, 128, 128, 0.1);
294
- --transition: all 0.2s ease-out;
295
- --delete-color: #ff4444;
296
- --folder-color: #ffc107;
297
- }
298
- html { box-sizing: border-box; }
299
- *, *:before, *:after { box-sizing: inherit; }
300
- body {
301
- font-family: 'Inter', sans-serif;
302
- background: var(--tg-theme-bg-color);
303
- color: var(--text-light);
304
- line-height: 1.5;
305
- margin: 0;
306
- padding: 15px;
307
- min-height: var(--tg-viewport-height);
308
- display: flex; flex-direction: column;
309
- -webkit-font-smoothing: antialiased;
310
- -moz-osx-font-smoothing: grayscale;
311
- }
312
- .container {
313
- width: 100%;
314
- max-width: 800px; /* Limit width on larger screens */
315
- margin: 0 auto; /* Center container */
316
- flex-grow: 1;
317
- display: flex;
318
- flex-direction: column;
319
- }
320
- #loading, #error-view { padding: 30px; text-align: center; font-size: 1.2em; }
321
- #app-content { display: none; flex-grow: 1; flex-direction: column; }
322
-
323
- h1 { font-size: 1.8em; font-weight: 800; text-align: center; margin-bottom: 15px; background: linear-gradient(135deg, var(--primary), var(--accent)); -webkit-background-clip: text; color: transparent; }
324
- h2 { font-size: 1.3em; margin-top: 20px; margin-bottom: 10px; color: var(--text-light); border-bottom: 1px solid var(--tg-theme-hint-color); padding-bottom: 5px; }
325
- p { margin-bottom: 10px; }
326
- input[type=text], input[type=file] {
327
- width: 100%; padding: 12px; margin: 8px 0; border: 1px solid var(--tg-theme-hint-color); border-radius: 10px; background: var(--tg-theme-bg-color); color: var(--text-light); font-size: 1em;
328
- box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05); transition: border-color 0.2s ease;
329
- }
330
- input[type=text]:focus { outline: none; border-color: var(--primary); }
331
- input[type=file] { border: none; padding: 5px; }
332
-
333
- .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: 0 2px 5px rgba(0,0,0,0.1); display: inline-block; text-decoration: none; margin-top: 5px; margin-right: 5px; text-align: center; }
334
- .btn:hover { opacity: 0.85; }
335
- .btn:active { transform: scale(0.98); }
336
- .btn[disabled] { background-color: var(--tg-theme-hint-color); cursor: not-allowed; opacity: 0.7; }
337
- .download-btn { background: var(--secondary); color: black; }
338
- .delete-btn { background: var(--delete-color); }
339
- .folder-btn { background: var(--folder-color); color: black; }
340
- .view-btn { background: var(--accent); }
341
-
342
- .flash { color: var(--tg-theme-button-text-color); text-align: center; margin-bottom: 15px; padding: 10px; border-radius: 10px; font-weight: 500;}
343
- .flash.success { background: #28a745; }
344
- .flash.error { background: var(--delete-color); }
345
-
346
- .file-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); gap: 15px; margin-top: 15px; padding-bottom: 20px;}
347
-
348
- .item { background: var(--card-bg); padding: 10px; border-radius: 12px; box-shadow: var(--shadow); text-align: center; transition: var(--transition); display: flex; flex-direction: column; justify-content: space-between; }
349
- .item:hover { transform: translateY(-3px); box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12); }
350
- .item-preview { width: 100%; height: 90px; object-fit: cover; border-radius: 8px; margin-bottom: 8px; cursor: pointer; display: block; background-color: rgba(128,128,128, 0.1); }
351
- .item.folder .item-preview { object-fit: contain; font-size: 50px; color: var(--folder-color); line-height: 90px; text-decoration: none; }
352
- .item p { font-size: 0.85em; margin: 4px 0; word-break: break-all; line-height: 1.3; height: 2.6em; overflow: hidden;} /* Show 2 lines */
353
- .item p.filename { font-weight: 600; height: auto; max-height: 2.6em;} /* Allow filename more space */
354
- .item p.details { font-size: 0.75em; color: var(--tg-theme-hint-color); height: auto; }
355
- .item a { color: var(--tg-theme-link-color); text-decoration: none; }
356
- .item a:hover { text-decoration: underline; }
357
- .item-actions { margin-top: 8px; display: flex; flex-wrap: wrap; gap: 5px; justify-content: center; }
358
- .item-actions .btn { font-size: 0.8em; padding: 4px 8px; }
359
-
360
- .modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.8); z-index: 2000; justify-content: center; align-items: center; padding: 10px;}
361
- .modal-content { width: 100%; height: 100%; background: var(--tg-theme-bg-color); padding: 10px; border-radius: 15px; overflow: hidden; position: relative; display: flex; flex-direction: column; }
362
- .modal-body { flex-grow: 1; overflow: auto; display: flex; justify-content: center; align-items: center; }
363
- .modal img, .modal video, .modal iframe, .modal pre { max-width: 100%; max-height: 95%; display: block; margin: auto; border-radius: 10px; }
364
- .modal iframe { width: 100%; height: 100%; border: none; }
365
- .modal pre { background: var(--tg-theme-secondary-bg-color); color: var(--text-light); padding: 15px; border-radius: 8px; white-space: pre-wrap; word-wrap: break-word; text-align: left; max-height: 95%; overflow-y: auto; width: 100%;}
366
- .modal-close-btn { position: absolute; top: 15px; right: 15px; font-size: 24px; color: var(--tg-theme-hint-color); cursor: pointer; background: rgba(128,128,128,0.2); border-radius: 50%; width: 30px; height: 30px; line-height: 30px; text-align: center; z-index: 2001;}
367
-
368
- #progress-container { width: 100%; background: var(--tg-theme-secondary-bg-color); border-radius: 10px; margin: 15px 0; display: none; position: relative; height: 20px; overflow: hidden; }
369
- #progress-bar { width: 0%; height: 100%; background: var(--primary); border-radius: 10px; transition: width 0.3s ease; }
370
- #progress-text { position: absolute; width: 100%; text-align: center; line-height: 20px; color: var(--tg-theme-button-text-color); font-size: 0.9em; font-weight: bold; text-shadow: 1px 1px 1px rgba(0,0,0,0.5); }
371
-
372
- .breadcrumbs { margin-bottom: 15px; font-size: 1em; background: var(--card-bg); padding: 8px 12px; border-radius: 8px; box-shadow: var(--shadow); }
373
- .breadcrumbs a { color: var(--tg-theme-link-color); text-decoration: none; font-weight: 500; }
374
- .breadcrumbs a:hover { text-decoration: underline; }
375
- .breadcrumbs span { margin: 0 5px; color: var(--tg-theme-hint-color); }
376
- .breadcrumbs span.current-folder { font-weight: 600; color: var(--text-light);}
377
-
378
- .folder-actions { margin-top: 10px; margin-bottom: 15px; display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
379
- .folder-actions input[type=text] { width: auto; flex-grow: 1; margin: 0; min-width: 150px; }
380
- .folder-actions .btn { margin: 0; flex-shrink: 0;}
381
-
382
- #user-info-header { text-align: center; margin-bottom: 15px; font-size: 0.9em; color: var(--tg-theme-hint-color); }
383
-
384
- @media (max-width: 480px) {
385
- body { padding: 10px; }
386
- .file-grid { grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); gap: 10px;}
387
- .item-preview { height: 80px; }
388
- .item.folder .item-preview { font-size: 40px; line-height: 80px; }
389
- h1 { font-size: 1.6em; }
390
- .btn { padding: 10px 18px; }
391
- .item-actions .btn { padding: 3px 6px; }
392
- .breadcrumbs { font-size: 0.9em; padding: 6px 10px; }
393
- }
394
- </style>
395
- </head>
396
- <body>
397
- <div id="loading">Загрузка и проверка данных Telegram...</div>
398
- <div id="error-view" style="display: none;"></div>
399
- <div id="app-content" class="container">
400
- <h1>Zeus Cloud</h1>
401
- <div id="user-info-header"></div>
402
- <div id="flash-container"></div>
403
-
404
- <div class="breadcrumbs" id="breadcrumbs-container"></div>
405
-
406
- <div class="folder-actions">
407
- <input type="text" id="new-folder-name" placeholder="Имя новой папки" required>
408
- <button id="create-folder-btn" class="btn folder-btn">Создать папку</button>
409
- </div>
410
-
411
- <form id="upload-form">
412
- <input type="file" name="files" id="file-input" multiple required>
413
- <button type="submit" class="btn" id="upload-btn">Загрузить файлы сюда</button>
414
- </form>
415
- <div id="progress-container"><div id="progress-bar"></div><div id="progress-text">0%</div></div>
416
-
417
- <h2 id="current-folder-title">Содержимое папки</h2>
418
- <div class="file-grid" id="file-grid-container">
419
- <!-- Items will be loaded here -->
420
- </div>
421
- </div>
422
-
423
- <div class="modal" id="mediaModal" onclick="closeModal(event)">
424
- <div class="modal-content">
425
- <span onclick="closeModalManual()" class="modal-close-btn">×</span>
426
- <div class="modal-body" id="modalContent"></div>
427
- </div>
428
- </div>
429
-
430
- <script>
431
- const tg = window.Telegram.WebApp;
432
- const loadingEl = document.getElementById('loading');
433
- const errorViewEl = document.getElementById('error-view');
434
- const appContentEl = document.getElementById('app-content');
435
- const userInfoHeaderEl = document.getElementById('user-info-header');
436
- const flashContainerEl = document.getElementById('flash-container');
437
- const breadcrumbsContainerEl = document.getElementById('breadcrumbs-container');
438
- const fileGridContainerEl = document.getElementById('file-grid-container');
439
- const currentFolderTitleEl = document.getElementById('current-folder-title');
440
- const uploadForm = document.getElementById('upload-form');
441
- const fileInput = document.getElementById('file-input');
442
- const uploadBtn = document.getElementById('upload-btn');
443
- const progressContainer = document.getElementById('progress-container');
444
- const progressBar = document.getElementById('progress-bar');
445
- const progressText = document.getElementById('progress-text');
446
- const newFolderInput = document.getElementById('new-folder-name');
447
- const createFolderBtn = document.getElementById('create-folder-btn');
448
-
449
- let currentFolderId = 'root';
450
- let validatedInitData = null;
451
- let currentUser = null;
452
- let currentItems = [];
453
-
454
- // --- API Communication ---
455
- async function apiCall(endpoint, method = 'POST', body = {}) {
456
- if (!validatedInitData) {
457
- showError("Ошибка: Данные авторизации отсутствуют.");
458
- throw new Error("Not authenticated");
459
- }
460
- body.initData = validatedInitData;
461
-
462
- try {
463
- const response = await fetch(endpoint, {
464
- method: method,
465
- headers: { 'Content-Type': 'application/json' },
466
- body: JSON.stringify(body)
467
- });
468
- if (!response.ok) {
469
- let errorMsg = `HTTP error ${response.status}`;
470
- try {
471
- const errData = await response.json();
472
- errorMsg = errData.message || errorMsg;
473
- } catch (e) { /* Ignore if error body is not JSON */ }
474
- throw new Error(errorMsg);
475
- }
476
- return await response.json();
477
- } catch (error) {
478
- console.error(`API call to ${endpoint} failed:`, error);
479
- showFlash(`Ошибка сети или сервера: ${error.message}`, 'error');
480
- throw error;
481
- }
482
- }
483
-
484
- // --- UI Rendering ---
485
- function showLoadingScreen() {
486
- loadingEl.style.display = 'block';
487
- errorViewEl.style.display = 'none';
488
- appContentEl.style.display = 'none';
489
- }
490
-
491
- function showError(message) {
492
- loadingEl.style.display = 'none';
493
- errorViewEl.innerHTML = `<h2>Ошибка</h2><p>${message}</p><button class='btn' onclick='window.location.reload()'>Перезагрузить</button>`;
494
- errorViewEl.style.display = 'block';
495
- appContentEl.style.display = 'none';
496
- if (tg.HapticFeedback) tg.HapticFeedback.notificationOccurred('error');
497
- }
498
-
499
- function showAppContent() {
500
- loadingEl.style.display = 'none';
501
- errorViewEl.style.display = 'none';
502
- appContentEl.style.display = 'flex'; // Use flex for container layout
503
- }
504
-
505
- function showFlash(message, type = 'success') {
506
- const flashDiv = document.createElement('div');
507
- flashDiv.className = `flash ${type}`;
508
- flashDiv.textContent = message;
509
- flashContainerEl.innerHTML = ''; // Clear previous messages
510
- flashContainerEl.appendChild(flashDiv);
511
- setTimeout(() => {
512
- if (flashDiv.parentNode === flashContainerEl) {
513
- flashContainerEl.removeChild(flashDiv);
514
- }
515
- }, 5000);
516
- if (tg.HapticFeedback) tg.HapticFeedback.notificationOccurred(type);
517
- }
518
-
519
- function renderBreadcrumbs(breadcrumbs) {
520
- breadcrumbsContainerEl.innerHTML = '';
521
- breadcrumbs.forEach((crumb, index) => {
522
- if (index > 0) {
523
- breadcrumbsContainerEl.appendChild(document.createTextNode(' / '));
524
- }
525
- if (index === breadcrumbs.length - 1) {
526
- const span = document.createElement('span');
527
- span.className = 'current-folder';
528
- span.textContent = crumb.name;
529
- breadcrumbsContainerEl.appendChild(span);
530
- currentFolderTitleEl.textContent = `Содержимое: ${crumb.name}`;
531
- } else {
532
- const link = document.createElement('a');
533
- link.href = '#';
534
- link.textContent = crumb.name;
535
- link.onclick = (e) => { e.preventDefault(); loadFolderContent(crumb.id); };
536
- breadcrumbsContainerEl.appendChild(link);
537
- }
538
- });
539
- }
540
-
541
- function renderItems(items) {
542
- fileGridContainerEl.innerHTML = ''; // Clear previous items
543
- if (!items || items.length === 0) {
544
- fileGridContainerEl.innerHTML = '<p>Эта папка пуста.</p>';
545
- return;
546
- }
547
- items.forEach(item => {
548
- const itemDiv = document.createElement('div');
549
- itemDiv.className = `item ${item.type}`;
550
- let previewHtml = '';
551
- let actionsHtml = '';
552
- let filenameDisplay = item.original_filename || item.name || 'Unnamed';
553
- const maxLen = 25;
554
- let truncatedFilename = filenameDisplay.length > maxLen
555
- ? filenameDisplay.substring(0, maxLen - 3) + '...'
556
- : filenameDisplay;
557
-
558
- if (item.type === 'folder') {
559
- previewHtml = `<a href="#" class="item-preview" title="Перейти в папку ${item.name}" onclick="event.preventDefault(); loadFolderContent('${item.id}')">📁</a>`;
560
- actionsHtml = `
561
- <button class="btn folder-btn" onclick="loadFolderContent('${item.id}')">Открыть</button>
562
- <button class="btn delete-btn" onclick="deleteFolder('${item.id}', '${item.name}')">Удалить</button>
563
- `;
564
- truncatedFilename = item.name;
565
- } else if (item.type === 'file') {
566
- const previewable = ['image', 'video', 'pdf', 'text'].includes(item.file_type);
567
- const dlUrl = `/download/${item.id}`;
568
- let viewFuncCall = '';
569
-
570
- if (item.file_type === 'image') {
571
- previewHtml = `<img class="item-preview" src="/preview_thumb/${item.id}" alt="${filenameDisplay}" loading="lazy" onerror="this.style.display='none'" onclick="openModal('/download/${item.id}', 'image', '${item.id}')">`;
572
- viewFuncCall = `openModal('/download/${item.id}', 'image', '${item.id}')`;
573
- } else if (item.file_type === 'video') {
574
- previewHtml = `<div class="item-preview" style="font-size: 50px; line-height: 90px; color: var(--accent);" onclick="openModal('/download/${item.id}', 'video', '${item.id}')">▶️</div>`;
575
- viewFuncCall = `openModal('/download/${item.id}', 'video', '${item.id}')`;
576
- } else if (item.file_type === 'pdf') {
577
- previewHtml = `<div class="item-preview" style="font-size: 50px; line-height: 90px; color: var(--accent);" onclick="openModal('/download/${item.id}', 'pdf', '${item.id}')">📄</div>`;
578
- viewFuncCall = `openModal('/download/${item.id}', 'pdf', '${item.id}')`;
579
- } else if (item.file_type === 'text') {
580
- previewHtml = `<div class="item-preview" style="font-size: 50px; line-height: 90px; color: var(--secondary);" onclick="openModal('/get_text_content/${item.id}', 'text', '${item.id}')">📝</div>`;
581
- viewFuncCall = `openModal('/get_text_content/${item.id}', 'text', '${item.id}')`;
582
- } else {
583
- previewHtml = '<div class="item-preview" style="font-size: 50px; line-height: 90px; color: #aaa;">❓</div>';
584
- }
585
-
586
- actionsHtml = `<a href="${dlUrl}" class="btn download-btn" target="_blank" rel="noopener noreferrer">Скачать</a>`;
587
- if (previewable) {
588
- actionsHtml += `<button class="btn view-btn" onclick="${viewFuncCall}">Просмотр</button>`;
589
- }
590
- actionsHtml += `<button class="btn delete-btn" onclick="deleteFile('${item.id}', '${filenameDisplay}')">Удалить</button>`;
591
- }
592
-
593
- itemDiv.innerHTML = `
594
- ${previewHtml}
595
- <p class="filename" title="${filenameDisplay}">${truncatedFilename}</p>
596
- ${item.upload_date ? `<p class="details">${item.upload_date.split(' ')[0]}</p>` : ''}
597
- <div class="item-actions">${actionsHtml}</div>
598
- `;
599
- fileGridContainerEl.appendChild(itemDiv);
600
- });
601
- }
602
-
603
- // --- Modal Logic ---
604
- async function openModal(srcOrUrl, type, itemId) {
605
- const modal = document.getElementById('mediaModal');
606
- const modalContent = document.getElementById('modalContent');
607
- modalContent.innerHTML = '<p>Загрузка...</p>';
608
- modal.style.display = 'flex';
609
-
610
- try {
611
- if (type === 'pdf') {
612
- // Check if running inside Telegram, if so, maybe open externally?
613
- // Or use a viewer that works well in iframe like PDF.js (more complex setup)
614
- // Using Google Docs viewer as a common fallback:
615
- modalContent.innerHTML = `<iframe src="https://docs.google.com/gview?url=${encodeURIComponent(window.location.origin + srcOrUrl)}&embedded=true" title="Просмотр PDF"></iframe>`;
616
- } else if (type === 'image') {
617
- modalContent.innerHTML = `<img src="${srcOrUrl}" alt="Просмотр изображения">`;
618
- } else if (type === 'video') {
619
- modalContent.innerHTML = `<video controls autoplay style='max-width: 100%; max-height: 100%;'><source src="${srcOrUrl}">Ваш браузер не поддерживает видео.</video>`;
620
- } else if (type === 'text') {
621
- const response = await fetch(srcOrUrl); // Use the dedicated text route
622
- if (!response.ok) throw new Error(`Ошибка загрузки текста: ${response.statusText}`);
623
- const text = await response.text();
624
- const escapedText = text.replace(/</g, "<").replace(/>/g, ">"); // Basic escaping
625
- modalContent.innerHTML = `<pre>${escapedText}</pre>`;
626
- } else {
627
- modalContent.innerHTML = '<p>Предпросмотр для этого типа файла не поддерживается.</p>';
628
- }
629
- } catch (error) {
630
- console.error("Error loading modal content:", error);
631
- modalContent.innerHTML = `<p>Не удалось загрузить содержимое для предпросмотра. ${error.message}</p>`;
632
- if (tg.HapticFeedback) tg.HapticFeedback.notificationOccurred('error');
633
- }
634
- }
635
-
636
- function closeModal(event) {
637
- const modal = document.getElementById('mediaModal');
638
- if (event.target === modal) {
639
- closeModalManual();
640
- }
641
- }
642
-
643
- function closeModalManual() {
644
- const modal = document.getElementById('mediaModal');
645
- modal.style.display = 'none';
646
- const video = modal.querySelector('video');
647
- if (video) { video.pause(); video.src = ''; }
648
- const iframe = modal.querySelector('iframe');
649
- if (iframe) iframe.src = 'about:blank';
650
- document.getElementById('modalContent').innerHTML = '';
651
- }
652
-
653
- // --- Folder Operations ---
654
- async function loadFolderContent(folderId) {
655
- currentFolderId = folderId;
656
- console.log(`Loading folder: ${folderId}`);
657
- try {
658
- const data = await apiCall('/get_dashboard_data', 'POST', { folder_id: folderId });
659
- if (data.status === 'ok') {
660
- currentItems = data.items || [];
661
- renderBreadcrumbs(data.breadcrumbs || [{'id': 'root', 'name': 'Root'}]);
662
- renderItems(currentItems.sort((a, b) => (a.type !== 'folder') - (b.type !== 'folder') || (a.name || a.original_filename || '').localeCompare(b.name || b.original_filename || '')));
663
- } else {
664
- showFlash(data.message || 'Не удалось загрузить содержимое папки.', 'error');
665
- }
666
- } catch (error) {
667
- // Error already handled by apiCall
668
- }
669
- }
670
-
671
- async function handleCreateFolder() {
672
- const folderName = newFolderInput.value.trim();
673
- if (!folderName) {
674
- showFlash('Введите имя папки.', 'error');
675
- return;
676
- }
677
- if (!/^[a-zA-Z0-9 _\-]+$/.test(folderName)) {
678
- showFlash('Имя папки может содержать буквы, цифры, пробелы, тире и подчеркивания.', 'error');
679
- return;
680
- }
681
-
682
- createFolderBtn.disabled = true;
683
- createFolderBtn.textContent = 'Создание...';
684
-
685
- try {
686
- const data = await apiCall('/create_folder', 'POST', {
687
- parent_folder_id: currentFolderId,
688
- folder_name: folderName
689
- });
690
- if (data.status === 'ok') {
691
- showFlash(`Папка "${folderName}" создана.`);
692
- newFolderInput.value = '';
693
- loadFolderContent(currentFolderId); // Refresh content
694
- } else {
695
- showFlash(data.message || 'Не удалось создать папку.', 'error');
696
- }
697
- } catch (error) {
698
- // Error handled by apiCall
699
- } finally {
700
- createFolderBtn.disabled = false;
701
- createFolderBtn.textContent = 'Создать папку';
702
- }
703
- }
704
-
705
- async function deleteFolder(folderId, folderName) {
706
- // Check if folder is empty client-side (optional optimization)
707
- // const folderIsEmpty = !currentItems.some(item => item.parent_id === folderId);
708
- // if (!folderIsEmpty) { showFlash('Папку можно удалить только если она пуста (клиентская проверка).', 'error'); return; }
709
-
710
- tg.showConfirm(`Вы уверены, что хотите удалить папку "${folderName}"? Папку можно удалить только если она пуста.`, async (confirmed) => {
711
- if (confirmed) {
712
- try {
713
- const data = await apiCall(`/delete_folder/${folderId}`, 'POST', { current_folder_id: currentFolderId });
714
- if (data.status === 'ok') {
715
- showFlash(`Папка "${folderName}" удалена.`);
716
- loadFolderContent(currentFolderId); // Refresh
717
- } else {
718
- showFlash(data.message || 'Не удалось удалить папку.', 'error');
719
- }
720
- } catch (error) {
721
- // Error handled by apiCall
722
- }
723
- }
724
- });
725
- }
726
-
727
- async function deleteFile(fileId, fileName) {
728
- tg.showConfirm(`Вы уверены, что хотите удалить файл "${fileName}"?`, async (confirmed) => {
729
- if (confirmed) {
730
- try {
731
- const data = await apiCall(`/delete_file/${fileId}`, 'POST', { current_folder_id: currentFolderId });
732
- if (data.status === 'ok') {
733
- showFlash(`Файл "${fileName}" удален.`);
734
- loadFolderContent(currentFolderId); // Refresh
735
- } else {
736
- showFlash(data.message || 'Не удалось удалить файл.', 'error');
737
- }
738
- } catch (error) {
739
- // Error handled by apiCall
740
- }
741
- }
742
- });
743
- }
744
-
745
- // --- File Upload ---
746
- function handleFileUpload(event) {
747
- event.preventDefault();
748
- const files = fileInput.files;
749
- if (files.length === 0) {
750
- showFlash('Выберите файлы для загрузки.', 'error');
751
- return;
752
- }
753
- if (files.length > 20) {
754
- showFlash('Максимум 20 файлов за раз!', 'error');
755
- return;
756
- }
757
-
758
- progressContainer.style.display = 'block';
759
- progressBar.style.width = '0%';
760
- progressText.textContent = '0%';
761
- uploadBtn.disabled = true;
762
- uploadBtn.textContent = 'Загрузка...';
763
-
764
- const formData = new FormData();
765
- for (let i = 0; i < files.length; i++) {
766
- formData.append('files', files[i]);
767
- }
768
- formData.append('current_folder_id', currentFolderId);
769
- formData.append('initData', validatedInitData);
770
-
771
- const xhr = new XMLHttpRequest();
772
-
773
- xhr.upload.addEventListener('progress', function(event) {
774
- if (event.lengthComputable) {
775
- const percentComplete = Math.round((event.loaded / event.total) * 100);
776
- progressBar.style.width = percentComplete + '%';
777
- progressText.textContent = percentComplete + '%';
778
- }
779
- });
780
-
781
- xhr.addEventListener('load', function() {
782
- uploadBtn.disabled = false;
783
- uploadBtn.textContent = 'Загрузить файлы сюда';
784
- progressContainer.style.display = 'none';
785
- fileInput.value = '';
786
-
787
- if (xhr.status >= 200 && xhr.status < 300) {
788
- try {
789
- const data = JSON.parse(xhr.responseText);
790
- if (data.status === 'ok') {
791
- showFlash(data.message || `${files.length} файл(ов) загружено.`);
792
- loadFolderContent(currentFolderId); // Refresh
793
- } else {
794
- showFlash(data.message || 'Ошибка при обработке загрузки на сервере.', 'error');
795
- }
796
- } catch (e) {
797
- showFlash('Некорректный ответ от сервера после загрузки.', 'error');
798
- }
799
- } else {
800
- showFlash(`Ошибка загрузки: ${xhr.statusText || xhr.status}`, 'error');
801
- }
802
- });
803
-
804
- xhr.addEventListener('error', function() {
805
- showFlash('Ошибка сети во время загрузки.', 'error');
806
- uploadBtn.disabled = false;
807
- uploadBtn.textContent = 'Загрузить файлы сюда';
808
- progressContainer.style.display = 'none';
809
- });
810
-
811
- xhr.addEventListener('abort', function() {
812
- showFlash('Загрузка отменена.', 'error');
813
- uploadBtn.disabled = false;
814
- uploadBtn.textContent = 'Загрузить файлы сюда';
815
- progressContainer.style.display = 'none';
816
- });
817
-
818
- xhr.open('POST', '/upload', true);
819
- xhr.send(formData);
820
- }
821
-
822
-
823
- // --- Initialization ---
824
- function initializeApp() {
825
- tg.ready();
826
- tg.expand();
827
- document.body.style.backgroundColor = tg.themeParams.bg_color || '#ffffff';
828
- tg.setHeaderColor(tg.themeParams.secondary_bg_color || '#f1f1f1');
829
-
830
- if (!tg.initData) {
831
- showError("Ошибка: Не удалось получить данные авторизации Telegram (initData). Попробуйте перезапустить Mini App.");
832
- return;
833
- }
834
- validatedInitData = tg.initData;
835
-
836
- fetch('/validate_init_data', {
837
- method: 'POST',
838
- headers: { 'Content-Type': 'application/json' },
839
- body: JSON.stringify({ initData: validatedInitData })
840
- })
841
- .then(response => response.json())
842
- .then(data => {
843
- if (data.status === 'ok' && data.user) {
844
- currentUser = data.user;
845
- userInfoHeaderEl.textContent = `User: ${currentUser.first_name || ''} ${currentUser.last_name || ''} (@${currentUser.username || currentUser.id})`;
846
- showAppContent();
847
- loadFolderContent('root');
848
- } else {
849
- throw new Error(data.message || 'Не удалось верифицировать пользователя.');
850
- }
851
- })
852
- .catch(error => {
853
- console.error("Validation failed:", error);
854
- showError(`Ошибка авторизации: ${error.message}. Попробуйте перезапустить.`);
855
- validatedInitData = null;
856
- });
857
-
858
- uploadForm.addEventListener('submit', handleFileUpload);
859
- createFolderBtn.addEventListener('click', handleCreateFolder);
860
-
861
- // Optional: Back button handling
862
- // tg.BackButton.onClick(() => {
863
- // const currentPath = getPathFromBreadcrumbs(); // Need function to get path array
864
- // if (currentPath.length > 1) {
865
- // const parentFolder = currentPath[currentPath.length - 2];
866
- // loadFolderContent(parentFolder.id);
867
- // } else {
868
- // // Optionally close app or do nothing at root
869
- // }
870
- // });
871
- // Show/hide back button based on current folder depth
872
- }
873
-
874
- // --- Start the App ---
875
- initializeApp();
876
-
877
- </script>
878
- </body>
879
- </html>
880
- """
881
-
882
-
883
- # --- Flask Routes ---
884
-
885
- @app.route('/')
886
- def index():
887
- """ Serves the main Mini App HTML shell. """
888
- return Response(HTML_TEMPLATE, mimetype='text/html')
889
-
890
- @app.route('/validate_init_data', methods=['POST'])
891
- def validate_init_data():
892
- """ Validates Telegram initData and ensures user exists in DB. """
893
- data = request.get_json()
894
- if not data or 'initData' not in data:
895
- return jsonify({"status": "error", "message": "Missing initData"}), 400
896
-
897
- init_data = data['initData']
898
- user_info = check_telegram_authorization(init_data, BOT_TOKEN)
899
-
900
- if user_info and 'id' in user_info:
901
- tg_user_id = str(user_info['id'])
902
- db_data = load_data()
903
- users = db_data.setdefault('users', {})
904
-
905
- if tg_user_id not in users:
906
- logging.info(f"New user detected: {tg_user_id}. Initializing filesystem.")
907
- users[tg_user_id] = {
908
- 'user_info': user_info,
909
- 'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
910
- }
911
- initialize_user_filesystem(users[tg_user_id])
912
- try:
913
- save_data(db_data)
914
- except Exception as e:
915
- logging.error(f"Failed to save data for new user {tg_user_id}: {e}")
916
- return jsonify({"status": "error", "message": "Ошибка сохранения данных нового пользователя."}), 500
917
- elif 'user_info' not in users[tg_user_id] or users[tg_user_id]['user_info'].get('username') != user_info.get('username'): # Update user info if missing or changed
918
- users[tg_user_id]['user_info'] = user_info
919
- try:
920
- save_data(db_data)
921
- except Exception as e:
922
- logging.warning(f"Failed to update user_info for {tg_user_id}: {e}")
923
-
924
- if 'filesystem' not in users[tg_user_id]:
925
- initialize_user_filesystem(users[tg_user_id])
926
- try:
927
- save_data(db_data)
928
- except Exception as e:
929
- logging.warning(f"Failed to initialize filesystem for existing user {tg_user_id}: {e}")
930
-
931
- return jsonify({"status": "ok", "user": user_info})
932
- else:
933
- logging.warning(f"Validation failed for initData: {init_data[:100]}...")
934
- return jsonify({"status": "error", "message": "Недействительные данные авторизации."}), 403
935
-
936
-
937
- @app.route('/get_dashboard_data', methods=['POST'])
938
- def get_dashboard_data():
939
- """ Returns folder content and breadcrumbs for the validated user. """
940
- data = request.get_json()
941
- if not data or 'initData' not in data or 'folder_id' not in data:
942
- return jsonify({"status": "error", "message": "Неполный запрос"}), 400
943
-
944
- user_info = check_telegram_authorization(data['initData'], BOT_TOKEN)
945
- if not user_info or 'id' not in user_info:
946
- return jsonify({"status": "error", "message": "Не авторизован"}), 403
947
-
948
- tg_user_id = str(user_info['id'])
949
- folder_id = data['folder_id']
950
- db_data = load_data()
951
- user_data = db_data.get('users', {}).get(tg_user_id)
952
-
953
- if not user_data or 'filesystem' not in user_data:
954
- logging.error(f"User data or filesystem missing for validated user {tg_user_id}")
955
- return jsonify({"status": "error", "message": "Ошибка данных пользователя"}), 500
956
-
957
- current_folder, _ = find_node_by_id(user_data['filesystem'], folder_id)
958
-
959
- if not current_folder or current_folder.get('type') != 'folder':
960
- logging.warning(f"Folder {folder_id} not found for user {tg_user_id}. Defaulting to root.")
961
- folder_id = 'root'
962
- current_folder, _ = find_node_by_id(user_data['filesystem'], folder_id)
963
- if not current_folder:
964
- logging.error(f"CRITICAL: Root folder not found for user {tg_user_id}")
965
- return jsonify({"status": "error", "message": "Критическая ошибка: Корневая папка отсутствует"}), 500
966
-
967
- items_in_folder = current_folder.get('children', [])
968
- breadcrumbs = get_node_path_list(user_data['filesystem'], folder_id)
969
-
970
- current_folder_info = {
971
- 'id': current_folder.get('id'),
972
- 'name': current_folder.get('name', 'Root')
973
- }
974
-
975
- return jsonify({
976
- "status": "ok",
977
- "items": items_in_folder,
978
- "breadcrumbs": breadcrumbs,
979
- "current_folder": current_folder_info
980
- })
981
-
982
-
983
- @app.route('/upload', methods=['POST'])
984
- def upload_files():
985
- """ Handles file uploads for the validated user. """
986
- init_data = request.form.get('initData')
987
- current_folder_id = request.form.get('current_folder_id', 'root')
988
- files = request.files.getlist('files')
989
-
990
- user_info = check_telegram_authorization(init_data, BOT_TOKEN)
991
- if not user_info or 'id' not in user_info:
992
- return jsonify({"status": "error", "message": "Не авторизован"}), 403
993
-
994
- tg_user_id = str(user_info['id'])
995
-
996
- if not HF_TOKEN_WRITE:
997
- return jsonify({'status': 'error', 'message': 'Загрузка невозможна: токен HF не настроен.'}), 500
998
-
999
- if not files or all(not f.filename for f in files):
1000
- return jsonify({'status': 'error', 'message': 'Файлы для загрузки не выбраны.'}), 400
1001
-
1002
- if len(files) > 20:
1003
- return jsonify({'status': 'error', 'message': 'Максимум 20 файлов за раз!'}), 400
1004
-
1005
- db_data = load_data()
1006
- user_data = db_data.get('users', {}).get(tg_user_id)
1007
- if not user_data or 'filesystem' not in user_data:
1008
- return jsonify({"status": "error", "message": "Ошибка данных пользователя"}), 500
1009
-
1010
- target_folder_node, _ = find_node_by_id(user_data['filesystem'], current_folder_id)
1011
- if not target_folder_node or target_folder_node.get('type') != 'folder':
1012
- return jsonify({'status': 'error', 'message': 'Целевая папка не найдена!'}), 404
1013
-
1014
- api = HfApi()
1015
- uploaded_count = 0
1016
- errors = []
1017
- needs_save = False
1018
-
1019
- for file in files:
1020
- if file and file.filename:
1021
- original_filename = secure_filename(file.filename)
1022
- name_part, ext_part = os.path.splitext(original_filename)
1023
- unique_suffix = uuid.uuid4().hex[:8]
1024
- unique_filename = f"{name_part}_{unique_suffix}{ext_part}"
1025
- file_id = uuid.uuid4().hex
1026
-
1027
- hf_path = f"cloud_files/{tg_user_id}/{current_folder_id}/{unique_filename}"
1028
- temp_path = os.path.join(UPLOAD_FOLDER, f"{file_id}_{unique_filename}")
1029
-
1030
- try:
1031
- file.save(temp_path)
1032
- api.upload_file(
1033
- path_or_fileobj=temp_path, path_in_repo=hf_path,
1034
- repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE,
1035
- commit_message=f"User {tg_user_id} uploaded {original_filename} to {current_folder_id}"
1036
- )
1037
-
1038
- file_info = {
1039
- 'type': 'file', 'id': file_id,
1040
- 'original_filename': original_filename, 'unique_filename': unique_filename,
1041
- 'path': hf_path, 'file_type': get_file_type(original_filename),
1042
- 'upload_date': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
1043
- }
1044
-
1045
- if add_node(user_data['filesystem'], current_folder_id, file_info):
1046
- uploaded_count += 1
1047
- needs_save = True
1048
- else:
1049
- errors.append(f"Ошибка добавления метаданных для {original_filename}.")
1050
- logging.error(f"Failed add_node for {file_id} to {current_folder_id} for {tg_user_id}")
1051
- try:
1052
- api.delete_file(path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE)
1053
- except Exception as del_err:
1054
- logging.error(f"Failed deleting orphaned HF file {hf_path}: {del_err}")
1055
-
1056
- except Exception as e:
1057
- logging.error(f"Upload error for {original_filename} ({tg_user_id}): {e}")
1058
- errors.append(f"Ошибка загрузки {original_filename}: {e}")
1059
- finally:
1060
- if os.path.exists(temp_path):
1061
- try: os.remove(temp_path)
1062
- except OSError as e: logging.error(f"Error removing temp file {temp_path}: {e}")
1063
-
1064
- if needs_save:
1065
- try:
1066
- save_data(db_data)
1067
- except Exception as e:
1068
- logging.error(f"Error saving DB after upload for {tg_user_id}: {e}")
1069
- errors.append("Ошибка сохранения метаданных после загрузки.")
1070
-
1071
- final_message = f"{uploaded_count} файл(ов) загружено."
1072
- if errors:
1073
- final_message += " Ошибки: " + "; ".join(errors)
1074
-
1075
- return jsonify({
1076
- "status": "ok" if not errors else "error",
1077
- "message": final_message
1078
- })
1079
-
1080
-
1081
- @app.route('/create_folder', methods=['POST'])
1082
- def create_folder():
1083
- """ Creates a new folder for the validated user. """
1084
- data = request.get_json()
1085
- if not data or 'initData' not in data or 'parent_folder_id' not in data or 'folder_name' not in data:
1086
- return jsonify({"status": "error", "message": "Неполный запрос"}), 400
1087
-
1088
- user_info = check_telegram_authorization(data['initData'], BOT_TOKEN)
1089
- if not user_info or 'id' not in user_info:
1090
- return jsonify({"status": "error", "message": "Не авторизован"}), 403
1091
-
1092
- tg_user_id = str(user_info['id'])
1093
- parent_folder_id = data['parent_folder_id']
1094
- folder_name = data['folder_name'].strip()
1095
-
1096
- if not folder_name:
1097
- return jsonify({'status': 'error', 'message': 'Имя папки не может быть пустым!'}), 400
1098
- # Relaxed validation - check disallowed chars instead? Let's keep simpler for now.
1099
- if not folder_name.replace(' ', '').replace('-', '').replace('_', '').isalnum():
1100
- if '/' in folder_name or '\\' in folder_name or ':' in folder_name: # Basic check
1101
- return jsonify({'status': 'error', 'message': 'Имя папки содержит недопустимые символы.'}), 400
1102
- # Allow broader names if basic check passes
1103
- logging.warning(f"Folder name '{folder_name}' contains non-alphanumeric/space/dash/underscore characters, but allowing.")
1104
-
1105
-
1106
- db_data = load_data()
1107
- user_data = db_data.get('users', {}).get(tg_user_id)
1108
- if not user_data or 'filesystem' not in user_data:
1109
- return jsonify({"status": "error", "message": "Ошибка данных пользователя"}), 500
1110
-
1111
- folder_id = uuid.uuid4().hex
1112
- folder_data = {
1113
- 'type': 'folder', 'id': folder_id,
1114
- 'name': folder_name, 'children': []
1115
- }
1116
-
1117
- if add_node(user_data['filesystem'], parent_folder_id, folder_data):
1118
- try:
1119
- save_data(db_data)
1120
- return jsonify({'status': 'ok', 'message': f'Папка "{folder_name}" создана.'})
1121
- except Exception as e:
1122
- logging.error(f"Create folder save error ({tg_user_id}): {e}")
1123
- return jsonify({'status': 'error', 'message': 'Ошибка сохранения данных.'}), 500
1124
- else:
1125
- return jsonify({'status': 'error', 'message': 'Не удалось найти родительскую папку или добавить узел.'}), 400
1126
-
1127
-
1128
- @app.route('/download/<file_id>')
1129
- def download_file_route(file_id):
1130
- """ Serves the file for download. NO AUTH HERE. """
1131
- db_data = load_data()
1132
- file_node = None
1133
- owner_user_id = None
1134
-
1135
- for user_id, user_data in db_data.get('users', {}).items():
1136
- if 'filesystem' in user_data:
1137
- node, _ = find_node_by_id(user_data['filesystem'], file_id)
1138
- if node and node.get('type') == 'file':
1139
- file_node = node
1140
- owner_user_id = user_id
1141
- break
1142
-
1143
- if not file_node:
1144
- return Response("Файл не найден", status=404)
1145
-
1146
- hf_path = file_node.get('path')
1147
- original_filename = file_node.get('original_filename', f'{file_id}_download')
1148
-
1149
- if not hf_path:
1150
- logging.error(f"Missing HF path for file ID {file_id} (owner: {owner_user_id})")
1151
- return Response("Ошибка: Путь к файлу не найден", status=500)
1152
-
1153
- file_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{hf_path}?download=true"
1154
-
1155
- try:
1156
- headers = {}
1157
- if HF_TOKEN_READ:
1158
- headers["authorization"] = f"Bearer {HF_TOKEN_READ}"
1159
-
1160
- response = requests.get(file_url, headers=headers, stream=True, timeout=30)
1161
- response.raise_for_status()
1162
-
1163
- # Use Content-Disposition header for filename
1164
- encoded_filename = urlencode({'filename': original_filename})[9:] # Crude but often works
1165
- # A more robust way involves RFC 6266 encoding, but keep it simple
1166
- disposition = f"attachment; filename=\"{original_filename}\"; filename*=UTF-8''{encoded_filename}"
1167
-
1168
- return Response(response.iter_content(chunk_size=8192),
1169
- mimetype=response.headers.get('Content-Type', 'application/octet-stream'),
1170
- headers={"Content-Disposition": disposition})
1171
-
1172
- except requests.exceptions.RequestException as e:
1173
- logging.error(f"Error downloading file from HF ({hf_path}, owner: {owner_user_id}): {e}")
1174
- status_code = e.response.status_code if e.response is not None else 502
1175
- return Response(f"Ошибка скачивания файла ({status_code})", status=status_code)
1176
- except Exception as e:
1177
- logging.error(f"Unexpected error during download ({hf_path}, owner: {owner_user_id}): {e}")
1178
- return Response("Внутренняя ошибка сервера при скачивании", status=500)
1179
-
1180
-
1181
- @app.route('/delete_file/<file_id>', methods=['POST'])
1182
- def delete_file_route(file_id):
1183
- """ Deletes a file for the validated user. """
1184
- data = request.get_json()
1185
- if not data or 'initData' not in data or 'current_folder_id' not in data:
1186
- return jsonify({"status": "error", "message": "Неполный запрос"}), 400
1187
-
1188
- user_info = check_telegram_authorization(data['initData'], BOT_TOKEN)
1189
- if not user_info or 'id' not in user_info:
1190
- return jsonify({"status": "error", "message": "Не авторизован"}), 403
1191
-
1192
- tg_user_id = str(user_info['id'])
1193
-
1194
- if not HF_TOKEN_WRITE:
1195
- return jsonify({'status': 'error', 'message': 'Удаление невозможно: токен HF не настроен.'}), 500
1196
-
1197
- db_data = load_data()
1198
- user_data = db_data.get('users', {}).get(tg_user_id)
1199
- if not user_data or 'filesystem' not in user_data:
1200
- return jsonify({"status": "error", "message": "Ошибка данных пользователя"}), 500
1201
-
1202
- file_node, parent_node = find_node_by_id(user_data['filesystem'], file_id)
1203
-
1204
- if not file_node or file_node.get('type') != 'file' or not parent_node:
1205
- return jsonify({'status': 'error', 'message': 'Файл не найден или не может быть удален.'}), 404
1206
-
1207
- hf_path = file_node.get('path')
1208
- original_filename = file_node.get('original_filename', 'файл')
1209
- needs_save = False
1210
-
1211
- if hf_path:
1212
- try:
1213
- api = HfApi()
1214
- api.delete_file(
1215
- path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE,
1216
- commit_message=f"User {tg_user_id} deleted {original_filename}"
1217
- )
1218
- logging.info(f"Deleted file {hf_path} from HF Hub for user {tg_user_id}")
1219
- except hf_utils.EntryNotFoundError:
1220
- logging.warning(f"File {hf_path} not found on HF Hub for delete attempt ({tg_user_id}).")
1221
- except Exception as e:
1222
- logging.error(f"Error deleting file from HF Hub ({hf_path}, {tg_user_id}): {e}")
1223
- # Don't stop here, still try to remove from DB
1224
- # return jsonify({'status': 'error', 'message': f'Ошибка удаления файла с сервера: {e}'}), 500
1225
-
1226
- if remove_node(user_data['filesystem'], file_id):
1227
- needs_save = True
1228
- logging.info(f"Removed file node {file_id} from DB for user {tg_user_id}")
1229
- else:
1230
- logging.error(f"Failed to remove file node {file_id} from DB structure for {tg_user_id} after HF delete.")
1231
-
1232
- if needs_save:
1233
- try:
1234
- save_data(db_data)
1235
- return jsonify({'status': 'ok', 'message': f'Файл {original_filename} удален.'})
1236
- except Exception as e:
1237
- logging.error(f"Delete file DB save error ({tg_user_id}): {e}")
1238
- return jsonify({'status': 'error', 'message': 'Файл удален с сервера, но ошибка сохранения базы данных.'}), 500
1239
- else:
1240
- return jsonify({'status': 'error', 'message': 'Файл не найден в базе данных для удаления.'}), 404
1241
-
1242
-
1243
- @app.route('/delete_folder/<folder_id>', methods=['POST'])
1244
- def delete_folder_route(folder_id):
1245
- """ Deletes an empty folder for the validated user. """
1246
- if folder_id == 'root':
1247
- return jsonify({'status': 'error', 'message': 'Нельзя удалить корневую папку!'}), 400
1248
-
1249
- data = request.get_json()
1250
- if not data or 'initData' not in data or 'current_folder_id' not in data:
1251
- return jsonify({"status": "error", "message": "Неполный запрос"}), 400
1252
-
1253
- user_info = check_telegram_authorization(data['initData'], BOT_TOKEN)
1254
- if not user_info or 'id' not in user_info:
1255
- return jsonify({"status": "error", "message": "Не авторизован"}), 403
1256
-
1257
- tg_user_id = str(user_info['id'])
1258
-
1259
- db_data = load_data()
1260
- user_data = db_data.get('users', {}).get(tg_user_id)
1261
- if not user_data or 'filesystem' not in user_data:
1262
- return jsonify({"status": "error", "message": "Ошибка данных пользователя"}), 500
1263
-
1264
- folder_node, parent_node = find_node_by_id(user_data['filesystem'], folder_id)
1265
-
1266
- if not folder_node or folder_node.get('type') != 'folder' or not parent_node:
1267
- return jsonify({'status': 'error', 'message': 'Папка не найдена или не может быть удалена.'}), 404
1268
-
1269
- folder_name = folder_node.get('name', 'папка')
1270
-
1271
- if folder_node.get('children'):
1272
- return jsonify({'status': 'error', 'message': f'Папку "{folder_name}" можно удалить только если она пуста.'}), 400
1273
-
1274
- if remove_node(user_data['filesystem'], folder_id):
1275
- try:
1276
- save_data(db_data)
1277
- return jsonify({'status': 'ok', 'message': f'Папка "{folder_name}" удалена.'})
1278
- except Exception as e:
1279
- logging.error(f"Delete folder save error ({tg_user_id}): {e}")
1280
- return jsonify({'status': 'error', 'message': 'Ошибка сохранения базы данных после удаления папки.'}), 500
1281
- else:
1282
- logging.error(f"Failed to remove empty folder node {folder_id} from DB for {tg_user_id}")
1283
- return jsonify({'status': 'error', 'message': 'Не удалось удалить папку из базы данных.'}), 500
1284
-
1285
-
1286
- @app.route('/get_text_content/<file_id>')
1287
- def get_text_content_route(file_id):
1288
- """ Serves text file content. NO AUTH HERE. """
1289
- db_data = load_data()
1290
- file_node = None
1291
- owner_user_id = None
1292
-
1293
- for user_id, user_data in db_data.get('users', {}).items():
1294
- if 'filesystem' in user_data:
1295
- node, _ = find_node_by_id(user_data['filesystem'], file_id)
1296
- if node and node.get('type') == 'file' and node.get('file_type') == 'text':
1297
- file_node = node
1298
- owner_user_id = user_id
1299
- break
1300
-
1301
- if not file_node:
1302
- return Response("Текстовый файл не найден", status=404)
1303
-
1304
- hf_path = file_node.get('path')
1305
- if not hf_path:
1306
- return Response("Ошибка: путь к файлу отсутствует", status=500)
1307
-
1308
- file_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{hf_path}?download=true"
1309
-
1310
- try:
1311
- headers = {}
1312
- if HF_TOKEN_READ:
1313
- headers["authorization"] = f"Bearer {HF_TOKEN_READ}"
1314
-
1315
- response = requests.get(file_url, headers=headers, timeout=15)
1316
- response.raise_for_status()
1317
-
1318
- max_preview_size = 1 * 1024 * 1024 # 1 MB
1319
- if len(response.content) > max_preview_size:
1320
- return Response("Файл слишком большой для предпросмотра (>1MB).", status=413)
1321
-
1322
- text_content = None
1323
- encodings_to_try = ['utf-8', 'cp1251', 'latin-1']
1324
- for enc in encodings_to_try:
1325
- try:
1326
- text_content = response.content.decode(enc)
1327
- break
1328
- except UnicodeDecodeError:
1329
- continue
1330
-
1331
- if text_content is None:
1332
- return Response("Не удалось определить кодировку файла.", status=500)
1333
-
1334
- return Response(text_content, mimetype='text/plain; charset=utf-8')
1335
-
1336
- except requests.exceptions.RequestException as e:
1337
- logging.error(f"Error fetching text content from HF ({hf_path}, owner {owner_user_id}): {e}")
1338
- status_code = e.response.status_code if e.response is not None else 502
1339
- return Response(f"Ошибка загрузки содержимого ({status_code})", status=status_code)
1340
- except Exception as e:
1341
- logging.error(f"Unexpected error fetching text content ({hf_path}, owner {owner_user_id}): {e}")
1342
- return Response("Внутренняя ошибка сервера", status=500)
1343
-
1344
-
1345
- @app.route('/preview_thumb/<file_id>')
1346
- def preview_thumb_route(file_id):
1347
- """ Serves image previews. NO AUTH HERE. """
1348
- db_data = load_data()
1349
- file_node = None
1350
- owner_user_id = None
1351
-
1352
- for user_id, user_data in db_data.get('users', {}).items():
1353
- if 'filesystem' in user_data:
1354
- node, _ = find_node_by_id(user_data['filesystem'], file_id)
1355
- if node and node.get('type') == 'file' and node.get('file_type') == 'image':
1356
- file_node = node
1357
- owner_user_id = user_id
1358
- break
1359
-
1360
- if not file_node: return Response("Изображение не найдено", status=404)
1361
- hf_path = file_node.get('path')
1362
- if not hf_path: return Response("Путь к файлу не найден", status=500)
1363
-
1364
- file_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{hf_path}"
1365
-
1366
- try:
1367
- headers = {}
1368
- if HF_TOKEN_READ: headers["authorization"] = f"Bearer {HF_TOKEN_READ}"
1369
- response = requests.get(file_url, headers=headers, stream=True, timeout=20)
1370
- response.raise_for_status()
1371
- # Return directly, let browser handle image rendering
1372
- return Response(response.iter_content(chunk_size=8192), mimetype=response.headers.get('Content-Type', 'image/jpeg'))
1373
- except requests.exceptions.RequestException as e:
1374
- logging.error(f"Error fetching preview from HF ({hf_path}, owner: {owner_user_id}): {e}")
1375
- status_code = e.response.status_code if e.response is not None else 502
1376
- return Response(f"Ошибка загрузки превью ({status_code})", status=status_code)
1377
- except Exception as e:
1378
- logging.error(f"Unexpected error during preview ({hf_path}, owner: {owner_user_id}): {e}")
1379
- return Response("Внутренняя ошибка сервера при загрузке превью", status=500)
1380
-
1381
-
1382
- # --- Main Execution ---
1383
- if __name__ == '__main__':
1384
- if BOT_TOKEN == 'YOUR_BOT_TOKEN':
1385
- logging.critical("\n" + "*"*60 +
1386
- "\n CRITICAL: TELEGRAM_BOT_TOKEN is not set or is 'YOUR_BOT_TOKEN'. " +
1387
- "\n Telegram authentication WILL FAIL. Set the environment variable." +
1388
- "\n" + "*"*60)
1389
- if not HF_TOKEN_WRITE:
1390
- logging.warning("HF_TOKEN (write access) is not set. File uploads, deletions will fail.")
1391
- if not HF_TOKEN_READ:
1392
- logging.warning("HF_TOKEN_READ is not set. File downloads/previews might fail for private repos.")
1393
-
1394
- logging.info("Performing initial database download...")
1395
- download_db_from_hf()
1396
- logging.info("Initial download attempt complete.")
1397
-
1398
- app.run(debug=False, host='0.0.0.0', port=7860)