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

Create app.py

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