Eluza133 commited on
Commit
69566fb
·
verified ·
1 Parent(s): e9f89f8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +529 -758
app.py CHANGED
@@ -2,18 +2,18 @@
2
  # -*- coding: utf-8 -*-
3
 
4
  import os
5
- from flask import Flask, request, Response, render_template_string, jsonify, redirect, url_for, make_response
 
6
  import hmac
7
  import hashlib
8
  import json
9
- from urllib.parse import unquote, parse_qs, quote
10
  import time
11
  from datetime import datetime
12
  import logging
13
- import threading
14
  from huggingface_hub import HfApi, hf_hub_download, list_repo_files
15
  from huggingface_hub.utils import RepositoryNotFoundError, EntryNotFoundError
16
- import io
17
 
18
  # --- Configuration ---
19
  BOT_TOKEN = os.getenv("BOT_TOKEN", "6750208873:AAE2hvPlJ99dBdhGa_Brre0IIpUdOvXxHt4")
@@ -21,44 +21,39 @@ HOST = '0.0.0.0'
21
  PORT = 7860
22
 
23
  # Hugging Face Settings
24
- REPO_ID = "Eluza133/Z1e1u"
25
- HF_UPLOAD_FOLDER = "uploads" # Base folder for user uploads within the HF repo
26
  HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE") # Token with write access
27
- HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") # Token with read access (can be same as write)
28
 
29
  app = Flask(__name__)
30
  logging.basicConfig(level=logging.INFO)
31
- app.secret_key = os.urandom(24)
32
 
33
  # --- Hugging Face API Initialization ---
34
  hf_api = None
35
  if HF_TOKEN_WRITE:
36
  hf_api = HfApi(token=HF_TOKEN_WRITE)
37
- logging.info("Hugging Face API initialized with WRITE token.")
38
  elif HF_TOKEN_READ:
39
  hf_api = HfApi(token=HF_TOKEN_READ)
40
- logging.info("Hugging Face API initialized with READ token (Uploads disabled).")
41
  else:
42
- logging.warning("HF_TOKEN_WRITE and HF_TOKEN_READ not set. Hugging Face operations will fail.")
43
 
44
 
45
  # --- Telegram Verification ---
46
  def verify_telegram_data(init_data_str):
47
- if not init_data_str:
48
- logging.warning("Verification attempt with empty initData.")
49
- return None, False, "Missing initData"
50
-
51
  try:
52
  parsed_data = parse_qs(init_data_str)
53
  received_hash = parsed_data.pop('hash', [None])[0]
54
 
55
  if not received_hash:
56
- logging.warning("Verification failed: Hash missing from initData.")
57
- return None, False, "Hash missing"
58
 
59
  data_check_list = []
60
  for key, value in sorted(parsed_data.items()):
61
- # Make sure values are handled correctly, especially if multiple exist (though unlikely for standard fields)
62
  data_check_list.append(f"{key}={value[0]}")
63
  data_check_string = "\n".join(data_check_list)
64
 
@@ -68,40 +63,40 @@ def verify_telegram_data(init_data_str):
68
  if calculated_hash == received_hash:
69
  auth_date = int(parsed_data.get('auth_date', [0])[0])
70
  current_time = int(time.time())
71
- # Check if data is reasonably fresh (e.g., within 24 hours)
72
  if current_time - auth_date > 86400:
73
- logging.warning(f"Verification Warning: Telegram InitData is older than 24 hours (Auth Date: {auth_date}, Current: {current_time}). Allowing access.")
74
- # return parsed_data, False, "Data expired" # Uncomment to enforce expiry
75
-
76
- user_data = None
77
- if 'user' in parsed_data:
78
- try:
79
- user_json_str = unquote(parsed_data['user'][0])
80
- user_data = json.loads(user_json_str)
81
- except Exception as e:
82
- logging.error(f"Could not parse user JSON from initData: {e}")
83
- return None, False, "User data parsing failed"
84
- else:
85
- logging.warning("User data missing in parsed initData.")
86
- return None, False, "User data missing"
87
-
88
- if not user_data or 'id' not in user_data:
89
- logging.error("Verification failed: User ID missing after parsing.")
90
- return None, False, "User ID missing"
91
-
92
- logging.info(f"Verification successful for user ID: {user_data.get('id')}")
93
- return user_data, True, "Verified"
94
  else:
95
- logging.warning(f"Verification failed: Hash mismatch. User: {parsed_data.get('user')}")
96
- return None, False, "Invalid hash"
97
  except Exception as e:
98
  logging.error(f"Error verifying Telegram data: {e}", exc_info=True)
99
- return None, False, "Internal verification error"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
 
101
- # --- HTML Template ---
102
  TEMPLATE = """
103
  <!DOCTYPE html>
104
- <html lang="ru">
105
  <head>
106
  <meta charset="UTF-8">
107
  <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no, user-scalable=no, viewport-fit=cover">
@@ -109,613 +104,419 @@ TEMPLATE = """
109
  <script src="https://telegram.org/js/telegram-web-app.js"></script>
110
  <link rel="preconnect" href="https://fonts.googleapis.com">
111
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
112
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
113
  <style>
114
  :root {
115
  --tg-theme-bg-color: {{ theme.bg_color | default('#ffffff') }};
116
  --tg-theme-text-color: {{ theme.text_color | default('#000000') }};
117
- --tg-theme-hint-color: {{ theme.hint_color | default('#707579') }};
118
- --tg-theme-link-color: {{ theme.link_color | default('#007aff') }};
119
- --tg-theme-button-color: {{ theme.button_color | default('#007aff') }};
120
  --tg-theme-button-text-color: {{ theme.button_text_color | default('#ffffff') }};
121
- --tg-theme-secondary-bg-color: {{ theme.secondary_bg_color | default('#f2f2f7') }};
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  --border-radius: 12px;
123
  --padding: 16px;
124
- --gap: 12px;
125
- --font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
126
  }
 
 
 
 
 
 
 
 
 
 
 
 
127
  * { box-sizing: border-box; margin: 0; padding: 0; }
128
  html {
129
- background-color: var(--tg-theme-bg-color);
130
- color-scheme: {{ 'dark' if theme.bg_color and theme.bg_color|lower != '#ffffff' and theme.bg_color|lower != '#fff' else 'light' }};
131
  }
132
  body {
133
  font-family: var(--font-family);
134
- background-color: var(--tg-theme-bg-color);
135
- color: var(--tg-theme-text-color);
136
  padding: var(--padding);
137
  overscroll-behavior-y: none;
138
- -webkit-font-smoothing: antialiased;
139
- -moz-osx-font-smoothing: grayscale;
140
- visibility: hidden;
141
  min-height: 100vh;
142
- display: flex;
143
- flex-direction: column;
144
  }
145
  .container {
146
  max-width: 700px;
147
- width: 100%;
148
  margin: 0 auto;
149
- flex-grow: 1;
150
  display: flex;
151
  flex-direction: column;
 
152
  }
153
- .header {
154
- display: flex;
155
- justify-content: space-between;
156
- align-items: center;
157
- margin-bottom: calc(var(--padding) * 1.5);
158
- padding-bottom: var(--padding);
159
- border-bottom: 1px solid var(--tg-theme-secondary-bg-color);
160
- }
161
- .header h1 {
162
  font-size: 1.8em;
163
  font-weight: 700;
164
- color: var(--tg-theme-text-color);
 
165
  }
166
- .user-info {
167
- font-size: 0.9em;
168
- color: var(--tg-theme-hint-color);
169
- text-align: right;
 
170
  }
171
- .upload-section {
 
 
172
  margin-bottom: var(--padding);
173
- padding: var(--padding);
174
- background-color: var(--tg-theme-secondary-bg-color);
175
- border-radius: var(--border-radius);
176
  }
177
- .upload-section label {
178
- display: flex;
179
- align-items: center;
180
- justify-content: center;
181
- padding: 12px 20px;
182
- background-color: var(--tg-theme-button-color);
183
- color: var(--tg-theme-button-text-color);
184
  border-radius: var(--border-radius);
185
- cursor: pointer;
186
- font-weight: 500;
187
- transition: background-color 0.2s ease;
188
  text-align: center;
 
 
 
 
189
  }
190
- .upload-section label:hover {
191
- filter: brightness(1.1);
192
  }
193
- .upload-section input[type="file"] { display: none; }
194
- .upload-section .progress-area {
195
- margin-top: var(--gap);
196
- display: none; /* Hidden initially */
 
197
  }
198
- .upload-section .progress-bar {
199
- width: 100%;
200
- height: 10px;
201
- background-color: #e0e0e0;
202
- border-radius: 5px;
203
- overflow: hidden;
204
  }
205
- .upload-section .progress-bar-inner {
206
- height: 100%;
207
- width: 0%;
208
- background-color: var(--tg-theme-link-color);
209
- transition: width 0.3s ease;
210
  }
211
- .upload-section .progress-text {
212
  font-size: 0.9em;
213
- color: var(--tg-theme-hint-color);
214
- text-align: center;
215
- margin-top: 5px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  }
217
-
218
- .file-list-section {
219
- margin-top: var(--padding);
220
- flex-grow: 1;
221
  }
222
- .file-list-section h2 {
223
- font-size: 1.3em;
224
- font-weight: 600;
225
- margin-bottom: var(--gap);
226
- color: var(--tg-theme-text-color);
 
 
 
 
 
 
227
  }
228
- #file-list {
 
 
 
 
229
  list-style: none;
230
  padding: 0;
231
- display: flex;
232
- flex-direction: column;
233
- gap: var(--gap);
234
  }
235
- #file-list li {
236
  display: flex;
237
- align-items: center;
238
- padding: var(--padding);
239
- background-color: var(--tg-theme-secondary-bg-color);
240
- border-radius: var(--border-radius);
241
  justify-content: space-between;
242
- word-break: break-all;
243
- }
244
- #file-list li .file-info {
245
- display: flex;
246
  align-items: center;
247
- gap: 10px;
248
- flex-grow: 1;
249
- min-width: 0; /* Prevent overflow */
250
  }
251
- #file-list li .file-icon {
252
- font-size: 1.4em;
253
- color: var(--tg-theme-hint-color);
254
- min-width: 25px;
255
- text-align: center;
 
256
  }
257
- #file-list li .file-name {
258
- font-weight: 500;
259
- color: var(--tg-theme-text-color);
260
  text-decoration: none;
 
261
  white-space: nowrap;
262
- overflow: hidden;
263
- text-overflow: ellipsis;
264
  }
265
- #file-list li .file-name:hover {
266
- color: var(--tg-theme-link-color);
267
- text-decoration: underline;
268
- }
269
- #file-list li .file-actions button {
270
- background: none;
271
- border: none;
272
- color: var(--tg-theme-link-color);
273
- cursor: pointer;
274
- font-size: 1.1em;
275
- margin-left: 10px;
276
- padding: 5px;
277
- }
278
- #file-list li .file-actions button:hover {
279
- opacity: 0.7;
280
- }
281
-
282
- #loading-indicator, #error-message, #no-files-message {
283
  text-align: center;
 
284
  padding: var(--padding);
285
- color: var(--tg-theme-hint-color);
286
- font-size: 1.1em;
287
- display: none; /* Hidden initially */
288
- }
289
- #error-message {
290
- color: #dc3545; /* Standard error color */
291
- background-color: rgba(220, 53, 69, 0.1);
292
- border: 1px solid rgba(220, 53, 69, 0.2);
293
- border-radius: var(--border-radius);
294
- }
295
- /* Modal for media */
296
- .media-modal {
297
- display: none; position: fixed; z-index: 1001;
298
- left: 0; top: 0; width: 100%; height: 100%;
299
- overflow: auto; background-color: rgba(0,0,0,0.8);
300
- backdrop-filter: blur(5px); -webkit-backdrop-filter: blur(5px);
301
- animation: fadeIn 0.3s ease-out;
302
- }
303
- @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
304
- .media-modal-content {
305
- margin: auto; display: block;
306
- max-width: 90%; max-height: 85%;
307
- position: absolute; top: 50%; left: 50%;
308
- transform: translate(-50%, -50%);
309
- }
310
- .media-modal img, .media-modal video {
311
- display: block; width: auto; height: auto;
312
- max-width: 100%; max-height: 100%;
313
- margin: 0 auto;
314
- border-radius: var(--border-radius);
315
- }
316
- .media-modal-close {
317
- position: absolute; top: 15px; right: 35px;
318
- color: #f1f1f1; font-size: 40px; font-weight: bold;
319
- transition: 0.3s; cursor: pointer;
320
- }
321
- .media-modal-close:hover, .media-modal-close:focus { color: #bbb; text-decoration: none; }
322
-
323
- /* Footer */
324
- .footer {
325
- text-align: center;
326
- margin-top: calc(var(--padding) * 2);
327
- padding-top: var(--padding);
328
- border-top: 1px solid var(--tg-theme-secondary-bg-color);
329
- font-size: 0.85em;
330
- color: var(--tg-theme-hint-color);
331
- }
332
-
333
  </style>
334
  </head>
335
  <body>
336
  <div class="container">
337
- <div class="header">
338
- <h1>☁️ Zeus Cloud</h1>
339
- <div class="user-info" id="user-info-greeting">Загрузка...</div>
340
- </div>
341
-
342
- <div class="upload-section">
343
- <label for="file-upload">
344
- 📤 Выбрать файлы (до 20 шт.)
345
- </label>
346
- <input type="file" id="file-upload" multiple accept="*/*">
347
- <div class="progress-area" id="progress-area">
348
- <div class="progress-bar">
349
- <div class="progress-bar-inner" id="progress-bar-inner"></div>
350
- </div>
351
- <div class="progress-text" id="progress-text"></div>
352
- </div>
353
- </div>
354
-
355
- <div class="file-list-section">
356
- <h2>Мои файлы</h2>
357
- <div id="loading-indicator">Загрузка списка файлов...</div>
358
- <div id="error-message"></div>
359
- <ul id="file-list"></ul>
360
- <div id="no-files-message">У вас пока нет загруженных файлов.</div>
361
- </div>
362
-
363
- <div class="footer">
364
- Powered by Hugging Face & Telegram
365
- </div>
366
-
367
- </div>
368
 
369
- <!-- The Modal -->
370
- <div id="mediaModal" class="media-modal">
371
- <span class="media-modal-close" id="media-modal-close-btn">×</span>
372
- <div id="media-modal-content-container">
373
- </div>
374
  </div>
375
 
376
-
377
  <script>
378
  const tg = window.Telegram.WebApp;
379
- const fileInput = document.getElementById('file-upload');
380
- const fileList = document.getElementById('file-list');
381
- const loadingIndicator = document.getElementById('loading-indicator');
382
- const errorMessage = document.getElementById('error-message');
383
- const noFilesMessage = document.getElementById('no-files-message');
384
- const progressArea = document.getElementById('progress-area');
385
- const progressBarInner = document.getElementById('progress-bar-inner');
386
- const progressText = document.getElementById('progress-text');
387
- const userInfoGreeting = document.getElementById('user-info-greeting');
388
- const mediaModal = document.getElementById('mediaModal');
389
- const mediaModalContentContainer = document.getElementById('media-modal-content-container');
390
- const mediaModalCloseBtn = document.getElementById('media-modal-close-btn');
391
-
392
-
393
  let currentUserId = null;
394
  let currentInitData = null;
395
- const HF_REPO_ID = "{{ repo_id }}";
396
- const HF_UPLOAD_FOLDER_JS = "{{ upload_folder }}";
397
- const MAX_FILES_UPLOAD = 20;
398
 
399
  function applyTheme(themeParams) {
400
  const root = document.documentElement;
401
- const isDark = themeParams.bg_color && themeParams.bg_color.toLowerCase() !== '#ffffff' && themeParams.bg_color.toLowerCase() !== '#fff';
402
- root.style.setProperty('--tg-theme-bg-color', themeParams.bg_color || (isDark ? '#1c1c1d' : '#ffffff'));
403
- root.style.setProperty('--tg-theme-text-color', themeParams.text_color || (isDark ? '#ffffff' : '#000000'));
404
- root.style.setProperty('--tg-theme-hint-color', themeParams.hint_color || (isDark ? '#999999' : '#707579'));
405
- root.style.setProperty('--tg-theme-link-color', themeParams.link_color || '#007aff');
406
- root.style.setProperty('--tg-theme-button-color', themeParams.button_color || '#007aff');
407
- root.style.setProperty('--tg-theme-button-text-color', themeParams.button_text_color || '#ffffff');
408
- root.style.setProperty('--tg-theme-secondary-bg-color', themeParams.secondary_bg_color || (isDark ? '#2c2c2e' : '#f2f2f7'));
409
- root.style.setProperty('color-scheme', isDark ? 'dark' : 'light');
410
- }
411
-
412
- function showMessage(element, message) {
413
- element.textContent = message;
414
- element.style.display = 'block';
415
- }
416
-
417
- function hideMessage(element) {
418
- element.style.display = 'none';
419
- }
420
-
421
- function getFileIcon(filename) {
422
- const extension = filename.split('.').pop().toLowerCase();
423
- if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(extension)) return '🖼️';
424
- if (['mp4', 'avi', 'mov', 'wmv', 'mkv', 'webm'].includes(extension)) return '🎬';
425
- if (['mp3', 'wav', 'ogg', 'aac', 'flac'].includes(extension)) return '🎵';
426
- if (['pdf'].includes(extension)) return '📄';
427
- if (['doc', 'docx', 'odt'].includes(extension)) return '📝';
428
- if (['xls', 'xlsx', 'ods'].includes(extension)) return '📊';
429
- if (['ppt', 'pptx', 'odp'].includes(extension)) return '📽️';
430
- if (['zip', 'rar', '7z', 'tar', 'gz'].includes(extension)) return '📦';
431
- if (['txt', 'md', 'log'].includes(extension)) return '📜';
432
- if (['html', 'css', 'js', 'py', 'java', 'c', 'cpp'].includes(extension)) return '💻';
433
- return '📁'; // Default icon
434
- }
435
-
436
- function isViewableMedia(filename) {
437
- const extension = filename.split('.').pop().toLowerCase();
438
- return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'mp4', 'webm', 'ogg'].includes(extension); // Add video/audio types TWA can reasonably play
439
- }
440
-
441
- function getMediaType(filename) {
442
- const extension = filename.split('.').pop().toLowerCase();
443
- if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(extension)) return 'image';
444
- if (['mp4', 'webm', 'ogg'].includes(extension)) return 'video';
445
- // Add audio later if needed
446
- return null;
447
- }
448
-
449
 
450
- function displayFiles(files) {
451
- fileList.innerHTML = ''; // Clear existing list
452
- hideMessage(loadingIndicator);
453
- hideMessage(errorMessage);
454
 
455
- if (!files || files.length === 0) {
456
- showMessage(noFilesMessage, вас пока нет загруженных файлов.');
457
- return;
 
458
  }
 
459
 
460
- hideMessage(noFilesMessage);
461
-
462
- files.sort().forEach(filePath => {
463
- // filePath is like "uploads/USER_ID/filename.ext"
464
- const filename = filePath.split('/').pop();
465
- const li = document.createElement('li');
466
-
467
- const fileInfoDiv = document.createElement('div');
468
- fileInfoDiv.className = 'file-info';
469
-
470
- const iconSpan = document.createElement('span');
471
- iconSpan.className = 'file-icon';
472
- iconSpan.textContent = getFileIcon(filename);
473
-
474
- const link = document.createElement('a');
475
- link.className = 'file-name';
476
- // Construct the direct download URL
477
- const downloadUrl = `https://huggingface.co/datasets/${HF_REPO_ID}/resolve/main/${filePath}`;
478
- link.href = downloadUrl;
479
- link.textContent = filename;
480
- link.target = '_blank'; // Open in new tab by default
481
-
482
- fileInfoDiv.appendChild(iconSpan);
483
- fileInfoDiv.appendChild(link);
484
- li.appendChild(fileInfoDiv);
485
-
486
- const actionsDiv = document.createElement('div');
487
- actionsDiv.className = 'file-actions';
488
-
489
- if (isViewableMedia(filename)) {
490
- const viewButton = document.createElement('button');
491
- viewButton.innerHTML = '👁️'; // Eye icon
492
- viewButton.title = 'Предпросмотр';
493
- viewButton.onclick = (e) => {
494
- e.preventDefault(); // Prevent default link navigation
495
- openMediaModal(downloadUrl, getMediaType(filename));
496
- };
497
- actionsDiv.appendChild(viewButton);
498
- }
499
-
500
- // Add download button explicitly if needed, though the link itself works
501
- // const downloadButton = document.createElement('button');
502
- // downloadButton.innerHTML = '💾'; // Save icon
503
- // downloadButton.title = 'Скачать';
504
- // downloadButton.onclick = () => window.open(downloadUrl, '_blank');
505
- // actionsDiv.appendChild(downloadButton);
506
-
507
- // Add delete button
508
- const deleteButton = document.createElement('button');
509
- deleteButton.innerHTML = '🗑️'; // Trash icon
510
- deleteButton.title = 'Удалить';
511
- deleteButton.onclick = () => confirmDeleteFile(filePath, filename, li);
512
- actionsDiv.appendChild(deleteButton);
513
-
514
-
515
- li.appendChild(actionsDiv);
516
- fileList.appendChild(li);
517
- });
518
  }
519
 
520
- function fetchFiles() {
521
- if (!currentInitData || !currentUserId) {
522
- showMessage(errorMessage, 'Ошибка: Не удалось получить данные пользователя.');
 
523
  return;
524
  }
525
- showMessage(loadingIndicator, 'Загрузка списка файлов...');
526
- hideMessage(errorMessage);
527
- hideMessage(noFilesMessage);
528
- fileList.innerHTML = '';
529
-
530
- fetch('/list_files', {
531
- method: 'POST',
532
- headers: { 'Content-Type': 'application/json' },
533
- body: JSON.stringify({ initData: currentInitData })
534
- })
535
- .then(response => {
536
  if (!response.ok) {
537
- return response.json().then(err => { throw new Error(err.message || `HTTP error ${response.status}`) });
 
538
  }
539
- return response.json();
540
- })
541
- .then(data => {
542
  if (data.status === 'ok') {
543
  displayFiles(data.files);
544
  } else {
545
- throw new Error(data.message || 'Не удалось загрузить файлы.');
546
  }
547
- })
548
- .catch(error => {
549
  console.error('Error fetching files:', error);
550
- hideMessage(loadingIndicator);
551
- showMessage(errorMessage, `Ошибка загрузки списка: ${error.message}`);
552
- });
553
  }
554
 
555
- function uploadFiles(files) {
556
- if (!HF_TOKEN_WRITE) {
557
- showMessage(errorMessage, "Загрузка невозможна: отсутствует токен записи на сервере.");
558
- tg.showAlert("Загрузка временно недоступна.");
559
- return;
560
- }
561
- if (!currentInitData) {
562
- showMessage(errorMessage, 'Ошибка: Не удалось получить данные для аутентификации.');
563
- return;
564
- }
565
- if (files.length === 0) return;
566
- if (files.length > MAX_FILES_UPLOAD) {
567
- tg.showAlert(`Можно загрузить не более ${MAX_FILES_UPLOAD} файлов за раз.`);
568
  return;
569
  }
570
 
571
- const formData = new FormData();
572
- for (let i = 0; i < files.length; i++) {
573
- formData.append('files', files[i]);
574
- }
575
- formData.append('initData', currentInitData);
576
-
577
- progressArea.style.display = 'block';
578
- progressBarInner.style.width = '0%';
579
- progressText.textContent = `Загрузка ${files.length} файла(ов)... 0%`;
580
-
581
- const xhr = new XMLHttpRequest();
582
- xhr.open('POST', '/upload', true);
583
-
584
- xhr.upload.onprogress = function(event) {
585
- if (event.lengthComputable) {
586
- const percentComplete = Math.round((event.loaded / event.total) * 100);
587
- progressBarInner.style.width = percentComplete + '%';
588
- progressText.textContent = `Загрузка ${files.length} файла(ов)... ${percentComplete}%`;
589
- }
590
- };
591
-
592
- xhr.onload = function() {
593
- progressText.textContent = 'Обработка...';
594
- setTimeout(() => { // Give a small delay for visual feedback
595
- progressArea.style.display = 'none';
596
- try {
597
- const response = JSON.parse(xhr.responseText);
598
- if (xhr.status === 200 && response.status === 'ok') {
599
- tg.showAlert(response.message || `${files.length} файл(ов) успешно загружено!`);
600
- fetchFiles(); // Refresh the list
601
- } else {
602
- const errorMsg = response.message || `Ошибка сервера (${xhr.status})`;
603
- showMessage(errorMessage, `Ошибка загрузки: ${errorMsg}`);
604
- tg.showAlert(`Ошибка загрузки: ${errorMsg}`);
605
- }
606
- } catch (e) {
607
- showMessage(errorMessage, `Ошибка обработки ответа сервера: ${xhr.responseText}`);
608
- tg.showAlert('Произошла ошибка при загрузке.');
609
- }
610
- }, 300);
611
- };
612
-
613
- xhr.onerror = function() {
614
- progressArea.style.display = 'none';
615
- const errorMsg = 'Ошибка сети или сервер недоступен.';
616
- showMessage(errorMessage, errorMsg);
617
- tg.showAlert(errorMsg);
618
- };
619
-
620
- xhr.send(formData);
621
- }
622
-
623
- function openMediaModal(url, type) {
624
- mediaModalContentContainer.innerHTML = ''; // Clear previous content
625
- let mediaElement;
626
- if (type === 'image') {
627
- mediaElement = document.createElement('img');
628
- mediaElement.src = url;
629
- mediaElement.alt = 'Предпросмотр изображения';
630
- } else if (type === 'video') {
631
- mediaElement = document.createElement('video');
632
- mediaElement.src = url;
633
- mediaElement.controls = true;
634
- mediaElement.autoplay = false; // Consider autoplay settings
635
- mediaElement.alt = 'Предпросмотр видео';
636
- } else {
637
- console.error("Unsupported media type for modal:", type);
638
- return;
639
- }
640
- mediaElement.className = 'media-modal-content';
641
- mediaModalContentContainer.appendChild(mediaElement);
642
- mediaModal.style.display = "block";
643
- if (tg.HapticFeedback) tg.HapticFeedback.impactOccurred('light');
644
- }
645
 
646
- function closeMediaModal() {
647
- mediaModal.style.display = "none";
648
- // Important: Stop video/audio playback if any
649
- const video = mediaModalContentContainer.querySelector('video');
650
- if (video) {
651
- video.pause();
652
- video.src = ''; // Release resources
653
- }
654
- mediaModalContentContainer.innerHTML = ''; // Clear content
 
 
 
 
 
 
 
 
 
655
  }
656
 
657
- function confirmDeleteFile(filePath, filename, listItemElement) {
658
- tg.showConfirm(`Вы уверены, что хотите удалить файл "${filename}"? Это действие необратимо.`, (confirmed) => {
659
- if (confirmed) {
660
- deleteFile(filePath, listItemElement);
661
- }
662
- });
663
- }
664
 
665
- function deleteFile(filePath, listItemElement) {
666
- if (!currentInitData) {
667
- tg.showAlert('Ошибка: Не удалось выполнить аутентификацию для удаления.');
668
- return;
669
- }
670
- if (!HF_TOKEN_WRITE) {
671
- tg.showAlert('Ошибка: Удаление файлов недоступно (требуется разрешение на запись).');
672
- return;
673
  }
674
 
675
- // Show visual indication of deletion in progress
676
- listItemElement.style.opacity = '0.5';
677
- const deleteButton = listItemElement.querySelector('.file-actions button[title="Удалить"]');
678
- if (deleteButton) deleteButton.disabled = true;
679
 
680
 
681
- fetch('/delete_file', {
682
- method: 'POST',
683
- headers: { 'Content-Type': 'application/json' },
684
- body: JSON.stringify({ initData: currentInitData, filePath: filePath })
685
- })
686
- .then(response => {
687
- if (!response.ok) {
688
- return response.json().then(err => { throw new Error(err.message || `HTTP error ${response.status}`) });
689
- }
690
- return response.json();
691
- })
692
- .then(data => {
693
- if (data.status === 'ok') {
694
- tg.showAlert(data.message || `Файл "${filePath.split('/').pop()}" удален.`);
695
- listItemElement.remove(); // Remove from list immediately
696
- // Check if the list is now empty
697
- if (fileList.children.length === 0) {
698
- showMessage(noFilesMessage, 'У вас пока нет загр��женных файлов.');
699
- }
700
- } else {
701
- throw new Error(data.message || 'Не удалось удалить файл.');
702
- }
703
- })
704
- .catch(error => {
705
- console.error('Error deleting file:', error);
706
- tg.showAlert(`Ошибка удаления: ${error.message}`);
707
- // Restore visual state on error
708
- listItemElement.style.opacity = '1';
709
- if (deleteButton) deleteButton.disabled = false;
710
- });
711
- }
 
 
 
 
 
 
 
 
 
712
 
713
- // --- Initialization ---
714
  function setupTelegram() {
715
  if (!tg || !tg.initData) {
716
  console.error("Telegram WebApp script not loaded or initData is missing.");
717
- document.body.style.backgroundColor = '#f8d7da'; // Error background
718
- document.body.innerHTML = '<div style="padding: 20px; color: #721c24;">Ошибка: Не удалось инициализировать приложение Telegram. Попробуйте перезапустить.</div>';
719
  document.body.style.visibility = 'visible';
720
  return;
721
  }
@@ -723,68 +524,63 @@ TEMPLATE = """
723
  tg.ready();
724
  tg.expand();
725
 
726
- currentInitData = tg.initData; // Store for later use in requests
727
-
728
  applyTheme(tg.themeParams);
729
  tg.onEvent('themeChanged', () => applyTheme(tg.themeParams));
730
 
731
- // Get user info for greeting and ID
732
- const user = tg.initDataUnsafe?.user;
733
- if (user && user.id) {
734
- currentUserId = user.id;
735
- const name = user.first_name || user.username || 'Пользователь';
736
- userInfoGreeting.textContent = `Привет, ${name}! 👋`;
737
- // Enable upload button only if write token is potentially present
738
- if (!HF_TOKEN_WRITE) {
739
- const uploadLabel = document.querySelector('.upload-section label');
740
- uploadLabel.style.backgroundColor = 'var(--tg-theme-hint-color)';
741
- uploadLabel.style.cursor = 'not-allowed';
742
- uploadLabel.title = 'Загрузка недоступна (нет прав на запись)';
743
- fileInput.disabled = true;
 
 
 
 
 
744
  }
745
- } else {
746
- showMessage(errorMessage, 'Ошибка: Не удалось определить пользователя Telegram.');
747
- userInfoGreeting.textContent = 'Ошибка ID';
748
- // Disable functionality if no user ID
749
- fileInput.disabled = true;
750
- return; // Stop further execution like fetching files
751
- }
752
 
753
- // Fetch initial file list
754
- fetchFiles();
755
 
756
- // Event listeners
757
- fileInput.addEventListener('change', (event) => {
758
- uploadFiles(event.target.files);
759
- // Clear the input after selection to allow re-uploading the same file
760
- event.target.value = null;
761
- });
762
 
763
- // Modal listeners
764
- mediaModalCloseBtn.addEventListener('click', closeMediaModal);
765
- mediaModal.addEventListener('click', (event) => {
766
- // Close if clicked outside the content area
767
- if (event.target === mediaModal) {
768
- closeMediaModal();
769
- }
770
- });
 
 
771
 
772
 
773
  document.body.style.visibility = 'visible';
774
- }
775
 
776
- // --- Run ---
777
  if (window.Telegram && window.Telegram.WebApp) {
778
  setupTelegram();
779
  } else {
780
  console.warn("Telegram WebApp script not immediately available, waiting for window.onload");
781
  window.addEventListener('load', setupTelegram);
782
- // Fallback timeout
783
  setTimeout(() => {
784
- if (document.body.style.visibility !== 'visible' && (!window.Telegram || !window.Telegram.WebApp)) {
785
  console.error("Telegram WebApp script fallback timeout triggered.");
786
- document.body.style.backgroundColor = '#f8d7da';
787
- document.body.innerHTML = '<div style="padding: 20px; color: #721c24;">Ошибка загрузки интерфейса Telegram. Проверьте соединение или попробуйте позже.</div>';
788
  document.body.style.visibility = 'visible';
789
  }
790
  }, 3500);
@@ -798,186 +594,155 @@ TEMPLATE = """
798
  # --- Flask Routes ---
799
  @app.route('/')
800
  def index():
801
- theme_params = {} # Let JS handle theme application after tg.ready()
802
- # Pass essential config to JS template
803
- return render_template_string(
804
- TEMPLATE,
805
- theme=theme_params,
806
- repo_id=REPO_ID,
807
- upload_folder=HF_UPLOAD_FOLDER
808
- )
809
-
810
- @app.route('/list_files', methods=['POST'])
811
- def list_files_route():
812
- req_data = request.get_json()
813
- init_data_str = req_data.get('initData')
814
-
815
- user_data, is_valid, message = verify_telegram_data(init_data_str)
816
-
817
- if not is_valid or not user_data:
818
- return jsonify({"status": "error", "message": f"Аутентификация не удалась: {message}"}), 403
819
-
820
- user_id = user_data.get('id')
821
- if not user_id:
822
- return jsonify({"status": "error", "message": "Не удалось определить ID пользователя."}), 403
823
-
824
- if not hf_api:
825
- return jsonify({"status": "error", "message": "Hugging Face API не настроен на сервере."}), 500
826
-
827
- user_folder_path = f"{HF_UPLOAD_FOLDER}/{user_id}"
828
  try:
829
- logging.info(f"Listing files for user {user_id} in repo {REPO_ID} path {user_folder_path}/")
830
- # Use allow_patterns to list only files directly within the user's folder
831
- repo_files = list_repo_files(
832
- repo_id=REPO_ID,
833
- repo_type="dataset",
834
- token=HF_TOKEN_READ or HF_TOKEN_WRITE, # Use read or write token
835
- paths=[user_folder_path], # Specify the directory
836
- recursive=False # List only top-level files in the user dir
837
- )
838
- # The result includes the full path, which is what we want
839
- user_files = [f for f in repo_files if f.startswith(user_folder_path + '/')]
 
 
840
 
841
- logging.info(f"Found {len(user_files)} files for user {user_id}.")
842
- return jsonify({"status": "ok", "files": user_files})
843
 
844
- except EntryNotFoundError:
845
- logging.info(f"User folder '{user_folder_path}' not found for user {user_id}. Returning empty list.")
846
- return jsonify({"status": "ok", "files": []}) # Folder doesn't exist yet = no files
847
- except RepositoryNotFoundError:
848
- logging.error(f"Hugging Face repository '{REPO_ID}' not found.")
849
- return jsonify({"status": "error", "message": f"Ошибка сервера: Репозиторий '{REPO_ID}' не найден."}), 500
850
- except Exception as e:
851
- logging.exception(f"Error listing files for user {user_id} from Hugging Face:")
852
- return jsonify({"status": "error", "message": f"Ошибка сервера при получении списка файлов: {str(e)}"}), 500
853
 
 
 
 
854
 
855
  @app.route('/upload', methods=['POST'])
856
- def upload_file_route():
857
- if not HF_TOKEN_WRITE:
858
- return jsonify({"status": "error", "message": "Загрузка не разрешена: отсутствует токен записи."}), 403
859
- if not hf_api:
860
- return jsonify({"status": "error", "message": "Hugging Face API не настроен для записи."}), 500
861
-
862
- init_data_str = request.form.get('initData')
863
- user_data, is_valid, message = verify_telegram_data(init_data_str)
864
 
865
- if not is_valid or not user_data:
866
- return jsonify({"status": "error", "message": f"Аутентификация не удалась: {message}"}), 403
 
867
 
868
- user_id = user_data.get('id')
869
  if not user_id:
870
- return jsonify({"status": "error", "message": "Не удалось определить ID пользователя."}), 403
871
-
872
- uploaded_files = request.files.getlist('files')
873
- if not uploaded_files:
874
- return jsonify({"status": "error", "message": "Файлы не найдены в запросе."}), 400
875
 
876
- if len(uploaded_files) > 20:
877
- return jsonify({"status": "error", "message": "Слишком много файлов. Максимум: 20."}), 400
 
 
 
878
 
 
 
879
  user_folder_path = f"{HF_UPLOAD_FOLDER}/{user_id}"
880
- upload_success_count = 0
881
- upload_errors = []
882
-
883
- for file in uploaded_files:
884
- if file.filename == '':
885
- logging.warning(f"Skipping empty filename upload for user {user_id}.")
886
- continue
887
-
888
- # Sanitize filename potentially? For now, use original.
889
- filename = file.filename
890
- path_in_repo = f"{user_folder_path}/{filename}"
891
-
892
- try:
893
- # Check if file already exists to prevent accidental overwrite?
894
- # Or just let upload_file handle it (it overwrites by default)
895
- logging.info(f"Uploading '{filename}' for user {user_id} to {REPO_ID}/{path_in_repo}")
896
-
897
- # Use a file-like object directly
898
- file_content = file.read() # Read content into memory
899
- file_obj = io.BytesIO(file_content)
900
-
901
- hf_api.upload_file(
902
- path_or_fileobj=file_obj,
903
- path_in_repo=path_in_repo,
904
- repo_id=REPO_ID,
905
- repo_type="dataset",
906
- token=HF_TOKEN_WRITE, # Explicitly use write token
907
- commit_message=f"User {user_id} uploaded {filename}"
908
- # create_pr=False # Direct commit is usually desired here
909
- )
910
- upload_success_count += 1
911
- logging.info(f"Successfully uploaded '{filename}' for user {user_id}.")
912
-
913
- except Exception as e:
914
- logging.exception(f"Error uploading file '{filename}' for user {user_id} to Hugging Face:")
915
- upload_errors.append(f"{filename}: {str(e)}")
916
-
917
- if not upload_errors and upload_success_count > 0:
918
- return jsonify({"status": "ok", "message": f"{upload_success_count} файл(ов) успешно загружено."})
919
- elif upload_success_count > 0:
920
- return jsonify({
921
- "status": "partial_error",
922
- "message": f"{upload_success_count} файл(ов) загружено. Ошибки: {'; '.join(upload_errors)}",
923
- "errors": upload_errors
924
- }), 207 # Multi-Status
925
- else:
926
- return jsonify({
927
- "status": "error",
928
- "message": f"Не удалось загрузить файлы. Ошибки: {'; '.join(upload_errors)}",
929
- "errors": upload_errors
930
- }), 500
931
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
932
 
933
- @app.route('/delete_file', methods=['POST'])
934
- def delete_file_route():
935
- if not HF_TOKEN_WRITE:
936
- return jsonify({"status": "error", "message": "Удаление не разрешено: отсутствует токен записи."}), 403
937
- if not hf_api:
938
- return jsonify({"status": "error", "message": "Hugging Face API не настроен для записи."}), 500
939
 
940
- req_data = request.get_json()
941
- init_data_str = req_data.get('initData')
942
- file_path_to_delete = req_data.get('filePath') # e.g., "uploads/USER_ID/filename.ext"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
943
 
944
- if not file_path_to_delete:
945
- return jsonify({"status": "error", "message": "Не указан путь к файлу для удаления."}), 400
946
 
947
- user_data, is_valid, message = verify_telegram_data(init_data_str)
 
 
 
948
 
949
- if not is_valid or not user_data:
950
- return jsonify({"status": "error", "message": f"Аутентификация не удалась: {message}"}), 403
 
951
 
952
- user_id = user_data.get('id')
953
  if not user_id:
954
- return jsonify({"status": "error", "message": "Не удалось определить ID пользователя."}), 403
955
 
956
- # Security Check: Ensure the file path belongs to the authenticated user
957
- expected_prefix = f"{HF_UPLOAD_FOLDER}/{user_id}/"
958
- if not file_path_to_delete.startswith(expected_prefix):
959
- logging.warning(f"User {user_id} attempted to delete unauthorized path: {file_path_to_delete}")
960
- return jsonify({"status": "error", "message": "Ошибка доступа к файлу."}), 403
961
 
962
  try:
963
- logging.info(f"Attempting to delete file '{file_path_to_delete}' for user {user_id} from repo {REPO_ID}")
964
- hf_api.delete_file(
965
- path_in_repo=file_path_to_delete,
966
  repo_id=REPO_ID,
967
  repo_type="dataset",
968
- token=HF_TOKEN_WRITE,
969
- commit_message=f"User {user_id} deleted {file_path_to_delete.split('/')[-1]}"
970
  )
971
- logging.info(f"Successfully deleted '{file_path_to_delete}' for user {user_id}.")
972
- return jsonify({"status": "ok", "message": "Файл успешно удален."})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
973
 
974
  except EntryNotFoundError:
975
- logging.warning(f"File '{file_path_to_delete}' not found for deletion by user {user_id}.")
976
- # Could argue whether this is an error or success (file is gone)
977
- return jsonify({"status": "error", "message": "Файл не найден на сервере."}), 404
 
 
978
  except Exception as e:
979
- logging.exception(f"Error deleting file '{file_path_to_delete}' for user {user_id} from Hugging Face:")
980
- return jsonify({"status": "error", "message": f"Ошибка сервера при удалении файла: {str(e)}"}), 500
981
 
982
 
983
  # --- App Initialization ---
@@ -987,24 +752,30 @@ if __name__ == '__main__':
987
  print("---")
988
  print(f"Flask server starting on http://{HOST}:{PORT}")
989
  print(f"Using Bot Token ID: {BOT_TOKEN.split(':')[0]}")
990
- print(f"Hugging Face Repo: {REPO_ID}")
991
- print(f"HF Upload Folder: {HF_UPLOAD_FOLDER}")
992
- if not hf_api:
993
  print("---")
994
- print("--- WARNING: HUGGING FACE API NOT INITIALIZED ---")
995
- print("--- Set HF_TOKEN_READ and/or HF_TOKEN_WRITE environment variables.")
996
- print("--- File listing requires READ, Upload/Delete require WRITE.")
997
  print("---")
998
  elif not HF_TOKEN_WRITE:
999
  print("---")
1000
  print("--- WARNING: HF_TOKEN_WRITE NOT SET ---")
1001
- print("--- File uploads and deletions will be disabled.")
1002
  print("---")
1003
  else:
1004
- print("--- Hugging Face WRITE token found. Uploads/Deletes enabled.")
 
 
 
 
 
 
1005
 
1006
  print("--- Server Ready ---")
1007
- # Use a production server like Waitress or Gunicorn instead of app.run() for deployment
 
1008
  # from waitress import serve
1009
  # serve(app, host=HOST, port=PORT)
1010
- app.run(host=HOST, port=PORT, debug=False) # debug=False recommended for production
 
2
  # -*- coding: utf-8 -*-
3
 
4
  import os
5
+ import io
6
+ from flask import Flask, request, Response, render_template_string, jsonify, session
7
  import hmac
8
  import hashlib
9
  import json
10
+ from urllib.parse import unquote, parse_qs
11
  import time
12
  from datetime import datetime
13
  import logging
 
14
  from huggingface_hub import HfApi, hf_hub_download, list_repo_files
15
  from huggingface_hub.utils import RepositoryNotFoundError, EntryNotFoundError
16
+ from werkzeug.utils import secure_filename
17
 
18
  # --- Configuration ---
19
  BOT_TOKEN = os.getenv("BOT_TOKEN", "6750208873:AAE2hvPlJ99dBdhGa_Brre0IIpUdOvXxHt4")
 
21
  PORT = 7860
22
 
23
  # Hugging Face Settings
24
+ REPO_ID = os.getenv("HF_REPO_ID", "Eluza133/Z1e1u")
25
+ HF_UPLOAD_FOLDER = "uploads" # Base folder within the HF repo
26
  HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE") # Token with write access
27
+ HF_TOKEN_READ = os.getenv("HF_TOKEN_READ", HF_TOKEN_WRITE) # Read token (can be same as write)
28
 
29
  app = Flask(__name__)
30
  logging.basicConfig(level=logging.INFO)
31
+ app.secret_key = os.getenv("FLASK_SECRET_KEY", os.urandom(24)) # Use env var or random
32
 
33
  # --- Hugging Face API Initialization ---
34
  hf_api = None
35
  if HF_TOKEN_WRITE:
36
  hf_api = HfApi(token=HF_TOKEN_WRITE)
37
+ logging.info(f"Hugging Face API initialized for repo: {REPO_ID} with Write Token.")
38
  elif HF_TOKEN_READ:
39
  hf_api = HfApi(token=HF_TOKEN_READ)
40
+ logging.info(f"Hugging Face API initialized for repo: {REPO_ID} with Read-Only Token.")
41
  else:
42
+ logging.warning("HF_TOKEN_WRITE or HF_TOKEN_READ not set. Hugging Face operations will fail.")
43
 
44
 
45
  # --- Telegram Verification ---
46
  def verify_telegram_data(init_data_str):
 
 
 
 
47
  try:
48
  parsed_data = parse_qs(init_data_str)
49
  received_hash = parsed_data.pop('hash', [None])[0]
50
 
51
  if not received_hash:
52
+ logging.warning("Verification failed: Hash missing.")
53
+ return None, False
54
 
55
  data_check_list = []
56
  for key, value in sorted(parsed_data.items()):
 
57
  data_check_list.append(f"{key}={value[0]}")
58
  data_check_string = "\n".join(data_check_list)
59
 
 
63
  if calculated_hash == received_hash:
64
  auth_date = int(parsed_data.get('auth_date', [0])[0])
65
  current_time = int(time.time())
66
+ # Allow data up to 24 hours old, adjust as needed
67
  if current_time - auth_date > 86400:
68
+ logging.warning(f"Verification warning: Telegram InitData is older than 24 hours (Auth Date: {auth_date}, Current: {current_time}).")
69
+ # Decide if this should be a failure: return parsed_data, False
70
+ return parsed_data, True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  else:
72
+ logging.warning(f"Verification failed: Hash mismatch. Calculated: {calculated_hash}, Received: {received_hash}")
73
+ return parsed_data, False
74
  except Exception as e:
75
  logging.error(f"Error verifying Telegram data: {e}", exc_info=True)
76
+ return None, False
77
+
78
+ def get_user_id_from_initdata(init_data_str):
79
+ parsed_data, is_valid = verify_telegram_data(init_data_str)
80
+ if not is_valid or not parsed_data or 'user' not in parsed_data:
81
+ return None
82
+
83
+ try:
84
+ user_json_str = unquote(parsed_data['user'][0])
85
+ user_info_dict = json.loads(user_json_str)
86
+ user_id = user_info_dict.get('id')
87
+ if user_id:
88
+ return str(user_id) # Return as string
89
+ else:
90
+ logging.error("User ID not found in parsed user data.")
91
+ return None
92
+ except Exception as e:
93
+ logging.error(f"Could not parse user JSON or get ID: {e}", exc_info=True)
94
+ return None
95
 
96
+ # --- HTML Templates ---
97
  TEMPLATE = """
98
  <!DOCTYPE html>
99
+ <html lang="en">
100
  <head>
101
  <meta charset="UTF-8">
102
  <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no, user-scalable=no, viewport-fit=cover">
 
104
  <script src="https://telegram.org/js/telegram-web-app.js"></script>
105
  <link rel="preconnect" href="https://fonts.googleapis.com">
106
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
107
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
108
  <style>
109
  :root {
110
  --tg-theme-bg-color: {{ theme.bg_color | default('#ffffff') }};
111
  --tg-theme-text-color: {{ theme.text_color | default('#000000') }};
112
+ --tg-theme-hint-color: {{ theme.hint_color | default('#999999') }};
113
+ --tg-theme-link-color: {{ theme.link_color | default('#2481cc') }};
114
+ --tg-theme-button-color: {{ theme.button_color | default('#2481cc') }};
115
  --tg-theme-button-text-color: {{ theme.button_text_color | default('#ffffff') }};
116
+ --tg-theme-secondary-bg-color: {{ theme.secondary_bg_color | default('#f1f1f1') }};
117
+ --tg-color-scheme: {{ color_scheme | default('light') }};
118
+
119
+ --bg-color: var(--tg-theme-bg-color);
120
+ --text-color: var(--tg-theme-text-color);
121
+ --hint-color: var(--tg-theme-hint-color);
122
+ --button-bg: var(--tg-theme-button-color);
123
+ --button-text: var(--tg-theme-button-text-color);
124
+ --secondary-bg: var(--tg-theme-secondary-bg-color);
125
+ --border-color: var(--tg-theme-hint-color);
126
+ --link-color: var(--tg-theme-link-color);
127
+ --error-color: #dc3545;
128
+ --success-color: #198754;
129
+ --font-family: 'Inter', sans-serif;
130
  --border-radius: 12px;
131
  --padding: 16px;
 
 
132
  }
133
+
134
+ [data-theme="dark"] {
135
+ --bg-color: #121212;
136
+ --text-color: #ffffff;
137
+ --hint-color: #aaaaaa;
138
+ --link-color: #62bcf9;
139
+ --button-bg: #31a5f5;
140
+ --button-text: #ffffff;
141
+ --secondary-bg: #1e1e1e;
142
+ --border-color: #333333;
143
+ }
144
+
145
  * { box-sizing: border-box; margin: 0; padding: 0; }
146
  html {
147
+ background-color: var(--bg-color);
148
+ color-scheme: var(--tg-color-scheme);
149
  }
150
  body {
151
  font-family: var(--font-family);
152
+ background-color: var(--bg-color);
153
+ color: var(--text-color);
154
  padding: var(--padding);
155
  overscroll-behavior-y: none;
156
+ line-height: 1.6;
157
+ visibility: hidden; /* Hide until ready */
 
158
  min-height: 100vh;
 
 
159
  }
160
  .container {
161
  max-width: 700px;
 
162
  margin: 0 auto;
 
163
  display: flex;
164
  flex-direction: column;
165
+ gap: calc(var(--padding) * 1.5);
166
  }
167
+ h1 {
168
+ text-align: center;
 
 
 
 
 
 
 
169
  font-size: 1.8em;
170
  font-weight: 700;
171
+ color: var(--link-color);
172
+ margin-bottom: var(--padding);
173
  }
174
+ .upload-section, .files-section {
175
+ background-color: var(--secondary-bg);
176
+ border-radius: var(--border-radius);
177
+ padding: var(--padding);
178
+ border: 1px solid var(--border-color);
179
  }
180
+ h2 {
181
+ font-size: 1.3em;
182
+ font-weight: 600;
183
  margin-bottom: var(--padding);
184
+ border-bottom: 1px solid var(--border-color);
185
+ padding-bottom: 8px;
 
186
  }
187
+ .file-input-wrapper {
188
+ position: relative;
189
+ overflow: hidden;
190
+ display: inline-block;
191
+ border: 2px dashed var(--hint-color);
 
 
192
  border-radius: var(--border-radius);
193
+ padding: var(--padding);
 
 
194
  text-align: center;
195
+ cursor: pointer;
196
+ transition: border-color 0.3s ease;
197
+ width: 100%;
198
+ margin-bottom: var(--padding);
199
  }
200
+ .file-input-wrapper:hover {
201
+ border-color: var(--link-color);
202
  }
203
+ .file-input-wrapper input[type=file] {
204
+ position: absolute;
205
+ left: 0; top: 0; opacity: 0;
206
+ width: 100%; height: 100%;
207
+ cursor: pointer;
208
  }
209
+ .file-input-label {
210
+ color: var(--hint-color);
211
+ font-weight: 500;
 
 
 
212
  }
213
+ .file-input-label span {
214
+ display: block;
215
+ font-size: 1.5em;
216
+ margin-bottom: 8px;
 
217
  }
218
+ #file-list-preview {
219
  font-size: 0.9em;
220
+ color: var(--text-color);
221
+ margin-top: 10px;
222
+ max-height: 100px;
223
+ overflow-y: auto;
224
+ text-align: left;
225
+ }
226
+ #file-list-preview div {
227
+ padding: 2px 0;
228
+ white-space: nowrap;
229
+ overflow: hidden;
230
+ text-overflow: ellipsis;
231
+ }
232
+ .btn {
233
+ display: block; width: 100%;
234
+ padding: 12px var(--padding); border-radius: var(--border-radius);
235
+ background: var(--button-bg); color: var(--button-text);
236
+ text-decoration: none; font-weight: 600;
237
+ border: none; cursor: pointer;
238
+ transition: background-color 0.2s ease;
239
+ font-size: 1em; text-align: center;
240
  }
241
+ .btn:hover {
242
+ opacity: 0.9;
 
 
243
  }
244
+ .btn:disabled {
245
+ background-color: var(--hint-color);
246
+ cursor: not-allowed;
247
+ }
248
+ #status-message {
249
+ margin-top: var(--padding);
250
+ padding: 10px;
251
+ border-radius: var(--border-radius);
252
+ text-align: center;
253
+ font-weight: 500;
254
+ display: none; /* Hidden by default */
255
  }
256
+ #status-message.success { background-color: var(--success-color); color: white; }
257
+ #status-message.error { background-color: var(--error-color); color: white; }
258
+ #status-message.loading { background-color: var(--secondary-bg); color: var(--text-color); border: 1px solid var(--border-color); }
259
+
260
+ .file-list-container {
261
  list-style: none;
262
  padding: 0;
 
 
 
263
  }
264
+ .file-item {
265
  display: flex;
 
 
 
 
266
  justify-content: space-between;
 
 
 
 
267
  align-items: center;
268
+ padding: 10px;
269
+ border-bottom: 1px solid var(--border-color);
270
+ transition: background-color 0.2s ease;
271
  }
272
+ .file-item:last-child { border-bottom: none; }
273
+ .file-item:hover { background-color: rgba(128, 128, 128, 0.1); }
274
+ .file-name {
275
+ flex-grow: 1;
276
+ margin-right: 15px;
277
+ word-break: break-all; /* Wrap long names */
278
  }
279
+ .file-link {
280
+ color: var(--link-color);
 
281
  text-decoration: none;
282
+ font-weight: 500;
283
  white-space: nowrap;
 
 
284
  }
285
+ .file-link:hover { text-decoration: underline; }
286
+ .loading-files, .no-files {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
287
  text-align: center;
288
+ color: var(--hint-color);
289
  padding: var(--padding);
290
+ font-style: italic;
291
+ }
292
+ .loader {
293
+ border: 4px solid var(--secondary-bg); border-radius: 50%; border-top: 4px solid var(--link-color);
294
+ width: 20px; height: 20px; animation: spin 1s linear infinite; display: inline-block; margin-left: 10px; vertical-align: middle;
295
+ }
296
+ @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
  </style>
298
  </head>
299
  <body>
300
  <div class="container">
301
+ <h1>☁️ Zeus Cloud</h1>
302
+
303
+ <section class="upload-section">
304
+ <h2>Upload Files</h2>
305
+ <div class="file-input-wrapper">
306
+ <input type="file" id="fileInput" multiple accept="*/*">
307
+ <label for="fileInput" class="file-input-label">
308
+ <span>📤</span>
309
+ Click or Drag & Drop<br>
310
+ (Max 20 files)
311
+ </label>
312
+ </div>
313
+ <div id="file-list-preview">No files selected.</div>
314
+ <button id="uploadButton" class="btn" disabled>Upload</button>
315
+ <div id="status-message"></div>
316
+ </section>
317
+
318
+ <section class="files-section">
319
+ <h2>My Files</h2>
320
+ <ul class="file-list-container" id="myFilesList">
321
+ <li class="loading-files">Loading files...</li>
322
+ </ul>
323
+ </section>
 
 
 
 
 
 
 
 
324
 
 
 
 
 
 
325
  </div>
326
 
 
327
  <script>
328
  const tg = window.Telegram.WebApp;
329
+ const fileInput = document.getElementById('fileInput');
330
+ const uploadButton = document.getElementById('uploadButton');
331
+ const statusMessage = document.getElementById('status-message');
332
+ const myFilesList = document.getElementById('myFilesList');
333
+ const fileListPreview = document.getElementById('fileList-preview');
 
 
 
 
 
 
 
 
 
334
  let currentUserId = null;
335
  let currentInitData = null;
 
 
 
336
 
337
  function applyTheme(themeParams) {
338
  const root = document.documentElement;
339
+ const colorScheme = themeParams.color_scheme || 'light';
340
+ root.dataset.theme = colorScheme;
341
+ root.style.setProperty('--tg-theme-bg-color', themeParams.bg_color || (colorScheme === 'dark' ? '#121212' : '#ffffff'));
342
+ root.style.setProperty('--tg-theme-text-color', themeParams.text_color || (colorScheme === 'dark' ? '#ffffff' : '#000000'));
343
+ root.style.setProperty('--tg-theme-hint-color', themeParams.hint_color || (colorScheme === 'dark' ? '#aaaaaa' : '#999999'));
344
+ root.style.setProperty('--tg-theme-link-color', themeParams.link_color || (colorScheme === 'dark' ? '#62bcf9' : '#2481cc'));
345
+ root.style.setProperty('--tg-theme-button-color', themeParams.button_color || (colorScheme === 'dark' ? '#31a5f5' : '#2481cc'));
346
+ root.style.setProperty('--tg-theme-button-text-color', themeParams.button_text_color || (colorScheme === 'dark' ? '#ffffff' : '#ffffff'));
347
+ root.style.setProperty('--tg-theme-secondary-bg-color', themeParams.secondary_bg_color || (colorScheme === 'dark' ? '#1e1e1e' : '#f1f1f1'));
348
+ root.style.setProperty('--tg-color-scheme', colorScheme);
349
+ }
350
+
351
+ function showStatus(message, type = 'loading', duration = null) {
352
+ statusMessage.textContent = message;
353
+ statusMessage.className = type; // 'success', 'error', 'loading'
354
+ statusMessage.style.display = 'block';
355
+
356
+ // Add loader only for loading type
357
+ const loader = statusMessage.querySelector('.loader');
358
+ if (type === 'loading' && !loader) {
359
+ const loaderSpan = document.createElement('span');
360
+ loaderSpan.className = 'loader';
361
+ statusMessage.appendChild(loaderSpan);
362
+ } else if (type !== 'loading' && loader) {
363
+ loader.remove();
364
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
365
 
 
 
 
 
366
 
367
+ if (duration) {
368
+ setTimeout(() => {
369
+ statusMessage.style.display = 'none';
370
+ }, duration);
371
  }
372
+ }
373
 
374
+ function hideStatus() {
375
+ statusMessage.style.display = 'none';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
376
  }
377
 
378
+ async function fetchFiles() {
379
+ if (!currentInitData) {
380
+ console.error("Cannot fetch files: initData not available.");
381
+ myFilesList.innerHTML = '<li class="no-files">Error: Could not verify user.</li>';
382
  return;
383
  }
384
+ myFilesList.innerHTML = '<li class="loading-files">Loading files...</li>';
385
+
386
+ try {
387
+ const response = await fetch('/list_files', {
388
+ method: 'GET',
389
+ headers: {
390
+ 'X-Telegram-Init-Data': currentInitData,
391
+ 'Accept': 'application/json'
392
+ }
393
+ });
394
+
395
  if (!response.ok) {
396
+ const errorData = await response.json().catch(() => ({ message: `HTTP error! status: ${response.status}` }));
397
+ throw new Error(errorData.message || `Failed to fetch files (${response.status})`);
398
  }
399
+
400
+ const data = await response.json();
401
+
402
  if (data.status === 'ok') {
403
  displayFiles(data.files);
404
  } else {
405
+ throw new Error(data.message || 'Failed to list files from server.');
406
  }
407
+ } catch (error) {
 
408
  console.error('Error fetching files:', error);
409
+ myFilesList.innerHTML = `<li class="no-files">Error loading files: ${error.message}</li>`;
410
+ }
 
411
  }
412
 
413
+ function displayFiles(files) {
414
+ myFilesList.innerHTML = ''; // Clear previous list
415
+ if (!files || files.length === 0) {
416
+ myFilesList.innerHTML = '<li class="no-files">No files uploaded yet.</li>';
 
 
 
 
 
 
 
 
 
417
  return;
418
  }
419
 
420
+ files.sort().forEach(fileInfo => { // Sort alphabetically by path
421
+ const li = document.createElement('li');
422
+ li.className = 'file-item';
423
+
424
+ const fileNameSpan = document.createElement('span');
425
+ fileNameSpan.className = 'file-name';
426
+ // Display only the filename part
427
+ fileNameSpan.textContent = fileInfo.name.split('/').pop();
428
+
429
+ const fileLink = document.createElement('a');
430
+ fileLink.className = 'file-link';
431
+ fileLink.href = fileInfo.url;
432
+ fileLink.textContent = 'Open';
433
+ fileLink.target = '_blank'; // Open in new tab
434
+
435
+ li.appendChild(fileNameSpan);
436
+ li.appendChild(fileLink);
437
+ myFilesList.appendChild(li);
438
+ });
439
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
440
 
441
+ function handleFileSelection() {
442
+ const files = fileInput.files;
443
+ if (files.length === 0) {
444
+ fileListPreview.textContent = 'No files selected.';
445
+ uploadButton.disabled = true;
446
+ } else if (files.length > 20) {
447
+ fileListPreview.textContent = `Error: Too many files selected (${files.length}). Maximum is 20.`;
448
+ uploadButton.disabled = true;
449
+ tg.HapticFeedback.notificationOccurred('error');
450
+ } else {
451
+ let previewHTML = `Selected ${files.length} file(s):<div>`;
452
+ for (let i = 0; i < files.length; i++) {
453
+ previewHTML += `<div>${files[i].name} (${(files[i].size / 1024 / 1024).toFixed(2)} MB)</div>`;
454
+ }
455
+ previewHTML += '</div>';
456
+ fileListPreview.innerHTML = previewHTML;
457
+ uploadButton.disabled = false;
458
+ }
459
  }
460
 
 
 
 
 
 
 
 
461
 
462
+ async function handleUpload() {
463
+ const files = fileInput.files;
464
+ if (files.length === 0 || files.length > 20 || !currentInitData) {
465
+ showStatus('Invalid selection or missing user data.', 'error', 3000);
466
+ return;
 
 
 
467
  }
468
 
469
+ uploadButton.disabled = true;
470
+ showStatus('Uploading...', 'loading');
471
+ tg.MainButton.showProgress();
472
+ tg.MainButton.setParams({ text: "UPLOADING...", is_active: false });
473
 
474
 
475
+ const formData = new FormData();
476
+ for (let i = 0; i < files.length; i++) {
477
+ formData.append('files', files[i]);
478
+ }
479
+ // Send initData in header for verification
480
+ // formData.append('initData', currentInitData); // Avoid sending in body if using header
481
+
482
+ try {
483
+ const response = await fetch('/upload', {
484
+ method: 'POST',
485
+ headers: {
486
+ 'X-Telegram-Init-Data': currentInitData // Send initData securely in header
487
+ },
488
+ body: formData,
489
+ });
490
+
491
+ const data = await response.json();
492
+
493
+ if (response.ok && data.status === 'ok') {
494
+ showStatus(`Successfully uploaded ${data.uploaded_count} file(s)!`, 'success', 3000);
495
+ fileInput.value = ''; // Clear the input
496
+ handleFileSelection(); // Update preview and button state
497
+ await fetchFiles(); // Refresh the file list
498
+ tg.HapticFeedback.notificationOccurred('success');
499
+ } else {
500
+ throw new Error(data.message || `Upload failed (${response.status})`);
501
+ }
502
+ } catch (error) {
503
+ console.error('Upload error:', error);
504
+ showStatus(`Upload Error: ${error.message}`, 'error', 5000);
505
+ tg.HapticFeedback.notificationOccurred('error');
506
+ } finally {
507
+ uploadButton.disabled = false; // Re-enable after success/error, unless no files selected
508
+ handleFileSelection(); // Ensure button state is correct based on selection
509
+ tg.MainButton.hideProgress();
510
+ tg.MainButton.setParams({ text: "UPLOAD COMPLETE", is_active: true }); // Or revert to original text
511
+ // Maybe hide main button after completion? tg.MainButton.hide();
512
+ setTimeout(() => tg.MainButton.setParams({text: "UPLOAD", is_active: true }), 2000); // Revert button text
513
+ }
514
+ }
515
 
 
516
  function setupTelegram() {
517
  if (!tg || !tg.initData) {
518
  console.error("Telegram WebApp script not loaded or initData is missing.");
519
+ document.body.innerHTML = '<p style="color: red; text-align: center; padding-top: 50px;">Error: Could not initialize Telegram Mini App interface. Please try reopening.</p>';
 
520
  document.body.style.visibility = 'visible';
521
  return;
522
  }
 
524
  tg.ready();
525
  tg.expand();
526
 
 
 
527
  applyTheme(tg.themeParams);
528
  tg.onEvent('themeChanged', () => applyTheme(tg.themeParams));
529
 
530
+ currentInitData = tg.initData;
531
+
532
+ // Send initData for initial verification (optional, can rely on upload/list requests)
533
+ fetch('/verify', {
534
+ method: 'POST',
535
+ headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
536
+ body: JSON.stringify({ initData: currentInitData }),
537
+ })
538
+ .then(response => response.json())
539
+ .then(data => {
540
+ if (data.status === 'ok' && data.verified && data.user_id) {
541
+ currentUserId = data.user_id;
542
+ console.log('Initial verification successful. User ID:', currentUserId);
543
+ fetchFiles(); // Fetch files now that we potentially have the user ID context
544
+ } else {
545
+ console.warn('Initial verification failed or user ID missing:', data.message);
546
+ // Handle case where initial verify fails - maybe show error?
547
+ myFilesList.innerHTML = `<li class="no-files">Error: Could not verify your Telegram account. ${data.message || ''}</li>`;
548
  }
549
+ })
550
+ .catch(error => {
551
+ console.error('Error during initial verification:', error);
552
+ myFilesList.innerHTML = `<li class="no-files">Error verifying account: ${error.message}</li>`;
553
+ });
 
 
554
 
 
 
555
 
556
+ fileInput.addEventListener('change', handleFileSelection);
557
+ uploadButton.addEventListener('click', handleUpload);
 
 
 
 
558
 
559
+ // Configure Main Button (Optional)
560
+ // tg.MainButton.setText("Upload Selected");
561
+ // tg.MainButton.onClick(handleUpload); // Trigger upload via main button
562
+ // fileInput.addEventListener('change', () => {
563
+ // if (fileInput.files.length > 0 && fileInput.files.length <= 20) {
564
+ // tg.MainButton.show().enable();
565
+ // } else {
566
+ // tg.MainButton.hide(); // Or disable: tg.MainButton.disable();
567
+ // }
568
+ // });
569
 
570
 
571
  document.body.style.visibility = 'visible';
572
+ }
573
 
 
574
  if (window.Telegram && window.Telegram.WebApp) {
575
  setupTelegram();
576
  } else {
577
  console.warn("Telegram WebApp script not immediately available, waiting for window.onload");
578
  window.addEventListener('load', setupTelegram);
579
+ // Fallback if load event doesn't fire or script fails
580
  setTimeout(() => {
581
+ if (document.body.style.visibility !== 'visible') {
582
  console.error("Telegram WebApp script fallback timeout triggered.");
583
+ document.body.innerHTML = '<p style="color: red; text-align: center; padding-top: 50px;">Error: Failed to load Telegram interface components.</p>';
 
584
  document.body.style.visibility = 'visible';
585
  }
586
  }, 3500);
 
594
  # --- Flask Routes ---
595
  @app.route('/')
596
  def index():
597
+ theme_params_json = request.args.get('themeParams', '{}')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
598
  try:
599
+ theme_params = json.loads(theme_params_json)
600
+ except json.JSONDecodeError:
601
+ theme_params = {}
602
+ color_scheme = request.args.get('colorScheme', 'light')
603
+ return render_template_string(TEMPLATE, theme=theme_params, color_scheme=color_scheme)
604
+
605
+ @app.route('/verify', methods=['POST'])
606
+ def verify_route():
607
+ try:
608
+ req_data = request.get_json()
609
+ init_data_str = req_data.get('initData')
610
+ if not init_data_str:
611
+ return jsonify({"status": "error", "verified": False, "message": "Missing initData"}), 400
612
 
613
+ user_id = get_user_id_from_initdata(init_data_str)
 
614
 
615
+ if user_id:
616
+ return jsonify({"status": "ok", "verified": True, "user_id": user_id}), 200
617
+ else:
618
+ logging.warning(f"Verification failed for initData: {init_data_str[:50]}...")
619
+ return jsonify({"status": "error", "verified": False, "message": "Invalid or unverifiable data"}), 403
 
 
 
 
620
 
621
+ except Exception as e:
622
+ logging.exception("Error in /verify endpoint")
623
+ return jsonify({"status": "error", "verified": False, "message": "Internal server error"}), 500
624
 
625
  @app.route('/upload', methods=['POST'])
626
+ def upload_files():
627
+ if not hf_api or not HF_TOKEN_WRITE:
628
+ return jsonify({"status": "error", "message": "Server configuration error: Hugging Face write access not available."}), 503
 
 
 
 
 
629
 
630
+ init_data_str = request.headers.get('X-Telegram-Init-Data')
631
+ if not init_data_str:
632
+ return jsonify({"status": "error", "message": "Authentication required (Missing InitData Header)."}), 401
633
 
634
+ user_id = get_user_id_from_initdata(init_data_str)
635
  if not user_id:
636
+ return jsonify({"status": "error", "message": "Authentication failed (Invalid InitData)."}), 403
 
 
 
 
637
 
638
+ files = request.files.getlist('files')
639
+ if not files:
640
+ return jsonify({"status": "error", "message": "No files provided in the request."}), 400
641
+ if len(files) > 20:
642
+ return jsonify({"status": "error", "message": "Too many files. Maximum is 20."}), 400
643
 
644
+ uploaded_count = 0
645
+ errors = []
646
  user_folder_path = f"{HF_UPLOAD_FOLDER}/{user_id}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
647
 
648
+ for file in files:
649
+ if file and file.filename:
650
+ filename = secure_filename(file.filename)
651
+ path_in_repo = f"{user_folder_path}/{filename}"
652
+ logging.info(f"Attempting to upload '{filename}' for user {user_id} to {REPO_ID}/{path_in_repo}")
653
+
654
+ try:
655
+ # Read file content into memory for upload
656
+ file_content = file.read()
657
+ file_like_object = io.BytesIO(file_content)
658
+
659
+ hf_api.upload_file(
660
+ path_or_fileobj=file_like_object,
661
+ path_in_repo=path_in_repo,
662
+ repo_id=REPO_ID,
663
+ repo_type="dataset",
664
+ commit_message=f"Upload {filename} by user {user_id}"
665
+ # token=HF_TOKEN_WRITE is implicitly used by hf_api instance
666
+ )
667
+ uploaded_count += 1
668
+ logging.info(f"Successfully uploaded '{filename}' for user {user_id}.")
669
+ except Exception as e:
670
+ error_msg = f"Failed to upload {filename}: {str(e)}"
671
+ logging.error(error_msg, exc_info=True)
672
+ errors.append(error_msg)
673
+ else:
674
+ errors.append("Received an empty file part.")
675
 
 
 
 
 
 
 
676
 
677
+ if errors:
678
+ if uploaded_count > 0:
679
+ return jsonify({
680
+ "status": "partial_error",
681
+ "message": f"Uploaded {uploaded_count} file(s) with errors on others.",
682
+ "errors": errors,
683
+ "uploaded_count": uploaded_count
684
+ }), 207 # Multi-Status
685
+ else:
686
+ return jsonify({
687
+ "status": "error",
688
+ "message": "Failed to upload any files.",
689
+ "errors": errors,
690
+ "uploaded_count": 0
691
+ }), 500
692
+ else:
693
+ return jsonify({"status": "ok", "message": f"Successfully uploaded {uploaded_count} files.", "uploaded_count": uploaded_count}), 200
694
 
 
 
695
 
696
+ @app.route('/list_files', methods=['GET'])
697
+ def list_user_files():
698
+ if not hf_api:
699
+ return jsonify({"status": "error", "message": "Server configuration error: Hugging Face access not available."}), 503
700
 
701
+ init_data_str = request.headers.get('X-Telegram-Init-Data')
702
+ if not init_data_str:
703
+ return jsonify({"status": "error", "message": "Authentication required (Missing InitData Header)."}), 401
704
 
705
+ user_id = get_user_id_from_initdata(init_data_str)
706
  if not user_id:
707
+ return jsonify({"status": "error", "message": "Authentication failed (Invalid InitData)."}), 403
708
 
709
+ user_folder_path = f"{HF_UPLOAD_FOLDER}/{user_id}"
710
+ logging.info(f"Listing files for user {user_id} in path: {user_folder_path}")
 
 
 
711
 
712
  try:
713
+ # Use list_repo_files which is efficient for listing
714
+ repo_files = list_repo_files(
 
715
  repo_id=REPO_ID,
716
  repo_type="dataset",
717
+ token=HF_TOKEN_READ or HF_TOKEN_WRITE, # Use read or write token
718
+ paths=[user_folder_path] # Specify the directory to list
719
  )
720
+
721
+ # Filter out potential directory markers if any, though list_repo_files usually doesn't return them for subdirs
722
+ user_files_info = []
723
+ base_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main"
724
+
725
+ for file_path in repo_files:
726
+ # Ensure we only list files directly under the user's folder
727
+ if file_path.startswith(user_folder_path + "/") and file_path != user_folder_path + "/":
728
+ file_info = {
729
+ "name": file_path, # Full path from repo root
730
+ "url": f"{base_url}/{file_path}"
731
+ }
732
+ user_files_info.append(file_info)
733
+
734
+ logging.info(f"Found {len(user_files_info)} file(s) for user {user_id}.")
735
+ return jsonify({"status": "ok", "files": user_files_info})
736
 
737
  except EntryNotFoundError:
738
+ logging.info(f"No files found or directory does not exist for user {user_id} at path {user_folder_path}.")
739
+ return jsonify({"status": "ok", "files": []}) # Return empty list if user folder doesn't exist yet
740
+ except RepositoryNotFoundError:
741
+ logging.error(f"Repository '{REPO_ID}' not found.")
742
+ return jsonify({"status": "error", "message": "Server configuration error: Target repository not found."}), 500
743
  except Exception as e:
744
+ logging.error(f"Error listing files for user {user_id}: {e}", exc_info=True)
745
+ return jsonify({"status": "error", "message": "Internal server error while listing files."}), 500
746
 
747
 
748
  # --- App Initialization ---
 
752
  print("---")
753
  print(f"Flask server starting on http://{HOST}:{PORT}")
754
  print(f"Using Bot Token ID: {BOT_TOKEN.split(':')[0]}")
755
+ print(f"Target Hugging Face Repo: {REPO_ID}")
756
+ print(f"Upload Base Folder: {HF_UPLOAD_FOLDER}")
757
+ if not HF_TOKEN_WRITE and not HF_TOKEN_READ:
758
  print("---")
759
+ print("--- CRITICAL WARNING: NO HUGGING FACE TOKEN SET ---")
760
+ print("--- File operations WILL FAIL. Set HF_TOKEN_WRITE or HF_TOKEN_READ environment variables.")
 
761
  print("---")
762
  elif not HF_TOKEN_WRITE:
763
  print("---")
764
  print("--- WARNING: HF_TOKEN_WRITE NOT SET ---")
765
+ print("--- File uploads will be disabled. Read-only mode.")
766
  print("---")
767
  else:
768
+ print("--- Hugging Face Write Token found. Uploads enabled.")
769
+
770
+ if not app.secret_key or app.secret_key == b'default_secret':
771
+ print("---")
772
+ print("--- WARNING: Using default or insecure FLASK_SECRET_KEY ---")
773
+ print("--- Set a strong FLASK_SECRET_KEY environment variable for production.")
774
+ print("---")
775
 
776
  print("--- Server Ready ---")
777
+ # For production, use a proper WSGI server like Gunicorn or Waitress
778
+ # Example using waitress:
779
  # from waitress import serve
780
  # serve(app, host=HOST, port=PORT)
781
+ app.run(host=HOST, port=PORT, debug=False) # debug=False is crucial for production