Eluza133 commited on
Commit
d16c245
·
verified ·
1 Parent(s): 9ffda1d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +569 -770
app.py CHANGED
@@ -1,5 +1,3 @@
1
- # --- START OF FILE app (8).py ---
2
-
3
  from flask import Flask, render_template_string, request, redirect, url_for, session, flash, send_file, jsonify
4
  from flask_caching import Cache
5
  import json
@@ -7,12 +5,13 @@ import os
7
  import logging
8
  import threading
9
  import time
10
- import uuid
11
  from datetime import datetime
12
- from huggingface_hub import HfApi, hf_hub_download, hf_hub_url
13
  from werkzeug.utils import secure_filename
14
  import requests
15
  from io import BytesIO
 
 
16
 
17
  app = Flask(__name__)
18
  app.secret_key = os.getenv("FLASK_SECRET_KEY", "supersecretkey")
@@ -35,17 +34,8 @@ def load_data():
35
  return {'users': {}, 'files': {}}
36
  data.setdefault('users', {})
37
  data.setdefault('files', {})
38
- # Ensure each user has a files list
39
- for user in data['users']:
40
- data['users'][user].setdefault('files', [])
41
  logging.info("Data successfully loaded")
42
  return data
43
- except FileNotFoundError:
44
- logging.warning(f"{DATA_FILE} not found locally after download attempt. Initializing empty database.")
45
- return {'users': {}, 'files': {}}
46
- except json.JSONDecodeError:
47
- logging.error(f"Error decoding JSON from {DATA_FILE}. Initializing empty database.")
48
- return {'users': {}, 'files': {}}
49
  except Exception as e:
50
  logging.error(f"Error loading data: {e}")
51
  return {'users': {}, 'files': {}}
@@ -62,9 +52,6 @@ def save_data(data):
62
  raise
63
 
64
  def upload_db_to_hf():
65
- if not HF_TOKEN_WRITE:
66
- logging.warning("HF_TOKEN_WRITE not set. Skipping database upload to Hugging Face.")
67
- return
68
  try:
69
  api = HfApi()
70
  api.upload_file(
@@ -80,12 +67,6 @@ def upload_db_to_hf():
80
  logging.error(f"Error uploading database: {e}")
81
 
82
  def download_db_from_hf():
83
- if not HF_TOKEN_READ:
84
- logging.warning("HF_TOKEN_READ not set. Skipping database download from Hugging Face.")
85
- if not os.path.exists(DATA_FILE):
86
- with open(DATA_FILE, 'w', encoding='utf-8') as f:
87
- json.dump({'users': {}, 'files': {}}, f)
88
- return
89
  try:
90
  hf_hub_download(
91
  repo_id=REPO_ID,
@@ -93,101 +74,250 @@ def download_db_from_hf():
93
  repo_type="dataset",
94
  token=HF_TOKEN_READ,
95
  local_dir=".",
96
- local_dir_use_symlinks=False,
97
- force_download=True # Ensure we get the latest version
98
  )
99
  logging.info("Database downloaded from Hugging Face")
100
  except Exception as e:
101
  logging.error(f"Error downloading database: {e}")
102
  if not os.path.exists(DATA_FILE):
103
- logging.info("Creating empty database file as download failed and file doesn't exist.")
104
- with open(DATA_FILE, 'w', encoding='utf-8') as f:
105
  json.dump({'users': {}, 'files': {}}, f)
106
 
107
  def periodic_backup():
108
  while True:
109
- time.sleep(1800) # Sleep for 30 minutes
110
- logging.info("Attempting periodic backup...")
111
- try:
112
- # No need to load data just for backup, save_data handles upload
113
- # Ensure data is loaded once before first potential save
114
- load_data() # Load to ensure cache is populated if needed elsewhere
115
- # Directly trigger upload which uses the current file
116
- upload_db_to_hf()
117
- except Exception as e:
118
- logging.error(f"Error during periodic backup: {e}")
119
-
120
 
121
  def get_file_type(filename):
122
- filename_lower = filename.lower()
123
- video_extensions = ('.mp4', '.mov', '.avi', '.webm', '.mkv', '.flv', '.wmv')
124
- image_extensions = ('.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.webp', '.svg')
125
- pdf_extensions = ('.pdf',)
126
- text_extensions = ('.txt', '.md', '.log', '.csv', '.json', '.xml', '.html', '.css', '.js', '.py', '.java', '.c', '.cpp', '.h', '.hpp', '.sh', '.bat')
127
-
128
- if filename_lower.endswith(video_extensions):
129
- return 'video'
130
- elif filename_lower.endswith(image_extensions):
131
- return 'image'
132
- elif filename_lower.endswith(pdf_extensions):
133
- return 'pdf'
134
- elif filename_lower.endswith(text_extensions):
135
- return 'text'
136
  return 'other'
137
 
138
  BASE_STYLE = '''
139
  :root {
140
- --primary: #ff4d6d; --secondary: #00ddeb; --accent: #8b5cf6;
141
- --background-light: #f5f6fa; --background-dark: #1a1625;
142
- --card-bg: rgba(255, 255, 255, 0.95); --card-bg-dark: rgba(40, 35, 60, 0.95);
143
- --text-light: #2a1e5a; --text-dark: #e8e1ff;
144
- --shadow: 0 10px 30px rgba(0, 0, 0, 0.2); --glass-bg: rgba(255, 255, 255, 0.15);
145
- --transition: all 0.3s ease; --delete-color: #ff4444;
 
 
 
 
 
 
 
146
  }
147
  * { margin: 0; padding: 0; box-sizing: border-box; }
148
- body { font-family: 'Inter', sans-serif; background: var(--background-light); color: var(--text-light); line-height: 1.6; }
149
- body.dark { background: var(--background-dark); color: var(--text-dark); }
150
- .container { margin: 20px auto; max-width: 1200px; padding: 25px; background: var(--card-bg); border-radius: 20px; box-shadow: var(--shadow); }
151
- body.dark .container { background: var(--card-bg-dark); }
152
- 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; }
153
- h2 { font-size: 1.5em; margin-top: 30px; color: var(--text-light); }
154
- body.dark h2 { color: var(--text-dark); }
155
- input, textarea { 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); }
156
- body.dark input, body.dark textarea { color: var(--text-dark); }
157
- input:focus, textarea:focus { outline: none; box-shadow: 0 0 0 4px var(--primary); }
158
- .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; text-align: center;}
159
- .btn:hover { transform: scale(1.05); background: #e6415f; }
160
- .download-btn { background: var(--secondary); margin-top: 10px; }
161
- .download-btn:hover { background: #00b8c5; }
162
- .delete-btn { background: var(--delete-color); margin-top: 10px; }
163
- .delete-btn:hover { background: #cc3333; }
164
- .flash { padding: 10px; margin-bottom: 15px; border-radius: 8px; background-color: var(--glass-bg); color: var(--accent); text-align: center; }
165
- .file-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 20px; margin-top: 20px; }
166
- .user-list { margin-top: 20px; }
167
- .user-item { padding: 15px; background: var(--card-bg); border-radius: 16px; margin-bottom: 10px; box-shadow: var(--shadow); transition: var(--transition); }
168
- body.dark .user-item { background: var(--card-bg-dark); }
169
- .user-item:hover { transform: translateY(-5px); }
170
- .user-item a { color: var(--primary); text-decoration: none; font-weight: 600; }
171
- .user-item a:hover { color: var(--accent); }
172
- .file-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; }
173
- body.dark .file-item { background: var(--card-bg-dark); }
174
- .file-item:hover { transform: translateY(-5px); }
175
- .file-preview-container { width: 100%; height: 200px; margin-bottom: 10px; display: flex; justify-content: center; align-items: center; overflow: hidden; background: rgba(0,0,0,0.05); border-radius: 10px; }
176
- .file-preview, .file-preview-embed { max-width: 100%; max-height: 100%; object-fit: cover; border-radius: 10px; cursor: pointer; }
177
- .file-preview-embed { width: 100%; height: 100%; border: none; }
178
- .file-item-info { margin-top: auto; } /* Pushes buttons down */
179
- .file-item p { font-size: 0.9em; margin: 5px 0; word-break: break-all; }
180
- .file-item .filename { font-weight: 600; }
181
- .file-item a { color: var(--primary); text-decoration: none; }
182
- .file-item a:hover { color: var(--accent); }
183
- .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; padding: 10px; }
184
- .modal-content { position: relative; max-width: 95%; max-height: 95%; }
185
- .modal img, .modal video { display: block; width: 100%; height: 100%; max-width: 90vw; max-height: 90vh; object-fit: contain; border-radius: 10px; box-shadow: var(--shadow); }
186
- .modal-close { position: absolute; top: -30px; right: -20px; color: white; font-size: 30px; cursor: pointer; background: rgba(0,0,0,0.5); border-radius: 50%; width: 30px; height: 30px; display: flex; justify-content: center; align-items: center; line-height: 1; }
187
- #progress-container { width: 100%; background: var(--glass-bg); border-radius: 10px; margin: 15px 0; display: none; overflow: hidden; }
188
- #progress-bar { width: 0%; height: 20px; background: var(--primary); border-radius: 10px 0 0 10px; transition: width 0.3s ease; text-align: center; color: white; line-height: 20px; font-size: 0.8em; }
189
- @media (max-width: 768px) { .file-grid { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); } }
190
- @media (max-width: 480px) { .file-grid { grid-template-columns: 1fr; } }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
  '''
192
 
193
  @app.route('/register', methods=['GET', 'POST'])
@@ -196,39 +326,25 @@ def register():
196
  username = request.form.get('username')
197
  password = request.form.get('password')
198
 
199
- if not username or not password:
200
- flash('Имя пользователя и пароль обязательны!')
201
- return redirect(url_for('register'))
202
-
203
- # Basic validation
204
- if not username.isalnum() or len(username) < 3:
205
- flash('Имя пользователя должно быть не менее 3 символов и содержать только буквы и цифры.')
206
- return redirect(url_for('register'))
207
- if len(password) < 6:
208
- flash('Пароль должен быть не менее 6 символов.')
209
- return redirect(url_for('register'))
210
-
211
-
212
  data = load_data()
213
 
214
  if username in data['users']:
215
  flash('Пользователь с таким именем уже существует!')
216
  return redirect(url_for('register'))
217
 
 
 
 
 
218
  data['users'][username] = {
219
- 'password': password, # Note: In production, hash passwords!
220
  'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
221
  'files': []
222
  }
223
- try:
224
- save_data(data)
225
- session['username'] = username
226
- flash('Регистрация прошла успешно!')
227
- return redirect(url_for('dashboard'))
228
- except Exception as e:
229
- flash('Ошибка при сохранении данных. Попробуйте еще раз.')
230
- logging.error(f"Error saving data during registration: {e}")
231
- return redirect(url_for('register'))
232
 
233
  html = '''
234
  <!DOCTYPE html>
@@ -251,8 +367,8 @@ def register():
251
  {% endif %}
252
  {% endwith %}
253
  <form method="POST" id="register-form">
254
- <input type="text" name="username" placeholder="Имя пользователя (только буквы и цифры, мин. 3)" required pattern="[a-zA-Z0-9]{3,}" title="Только буквы и цифры, минимум 3 символа">
255
- <input type="password" name="password" placeholder="Пароль (минимум 6 символов)" required minlength="6">
256
  <button type="submit" class="btn">Зарегистрироваться</button>
257
  </form>
258
  <p style="margin-top: 20px;">Уже есть аккаунт? <a href="{{ url_for('login') }}">Войти</a></p>
@@ -269,17 +385,12 @@ def login():
269
  password = request.form.get('password')
270
  data = load_data()
271
 
272
- # Note: Add password hashing and verification here in production
273
- if username in data['users'] and data['users'][username].get('password') == password:
274
  session['username'] = username
275
  return jsonify({'status': 'success', 'redirect': url_for('dashboard')})
276
  else:
277
  return jsonify({'status': 'error', 'message': 'Неверное имя пользователя или пароль!'})
278
 
279
- # If already logged in, redirect to dashboard
280
- if 'username' in session:
281
- return redirect(url_for('dashboard'))
282
-
283
  html = '''
284
  <!DOCTYPE html>
285
  <html lang="ru">
@@ -294,10 +405,10 @@ def login():
294
  <div class="container">
295
  <h1>Zeus Cloud</h1>
296
  <div id="flash-messages">
297
- {% with messages = get_flashed_messages(with_categories=true) %}
298
  {% if messages %}
299
- {% for category, message in messages %}
300
- <div class="flash flash-{{ category }}">{{ message }}</div>
301
  {% endfor %}
302
  {% endif %}
303
  {% endwith %}
@@ -310,34 +421,27 @@ def login():
310
  <p style="margin-top: 20px;">Нет аккаунта? <a href="{{ url_for('register') }}">Зарегистрируйтесь</a></p>
311
  </div>
312
  <script>
313
- // Auto-login attempt
314
  const savedCredentials = JSON.parse(localStorage.getItem('zeusCredentials'));
315
  if (savedCredentials) {
316
  fetch('/', {
317
  method: 'POST',
318
- headers: { 'Content-Type': 'application/x-www-form-urlencoded', },
 
 
319
  body: `username=${encodeURIComponent(savedCredentials.username)}&password=${encodeURIComponent(savedCredentials.password)}`
320
  })
321
  .then(response => response.json())
322
  .then(data => {
323
  if (data.status === 'success') {
324
  window.location.href = data.redirect;
325
- } else {
326
- // Clear invalid stored credentials
327
- localStorage.removeItem('zeusCredentials');
328
  }
329
  })
330
  .catch(error => console.error('Ошибка автовхода:', error));
331
  }
332
 
333
- // Manual login form submission
334
  document.getElementById('login-form').addEventListener('submit', function(e) {
335
  e.preventDefault();
336
  const formData = new FormData(this);
337
- const submitButton = this.querySelector('button[type="submit"]');
338
- submitButton.disabled = true;
339
- submitButton.textContent = 'Вход...';
340
-
341
  fetch('/', {
342
  method: 'POST',
343
  body: formData
@@ -350,16 +454,11 @@ def login():
350
  localStorage.setItem('zeusCredentials', JSON.stringify({ username, password }));
351
  window.location.href = data.redirect;
352
  } else {
353
- document.getElementById('flash-messages').innerHTML = `<div class="flash flash-error">${data.message}</div>`;
354
- submitButton.disabled = false;
355
- submitButton.textContent = 'Войти';
356
  }
357
  })
358
  .catch(error => {
359
- document.getElementById('flash-messages').innerHTML = `<div class="flash flash-error">Ошибка соединения! Попробуйте еще раз.</div>`;
360
- submitButton.disabled = false;
361
- submitButton.textContent = 'Войти';
362
- console.error('Ошибка входа:', error);
363
  });
364
  });
365
  </script>
@@ -371,136 +470,64 @@ def login():
371
  @app.route('/dashboard', methods=['GET', 'POST'])
372
  def dashboard():
373
  if 'username' not in session:
374
- flash('Пожалуйста, войдите в систему!', 'error')
375
  return redirect(url_for('login'))
376
 
377
  username = session['username']
378
  data = load_data()
379
  if username not in data['users']:
380
  session.pop('username', None)
381
- flash('Пользователь не найден!', 'error')
382
  return redirect(url_for('login'))
383
 
384
- if request.method == 'POST':
385
- if not HF_TOKEN_WRITE:
386
- flash('Загрузка невозможна: токен для записи на Hugging Face не настроен.', 'error')
387
- return redirect(url_for('dashboard'))
388
 
 
389
  files = request.files.getlist('files')
390
- if not files or all(not f.filename for f in files):
391
- flash('Файлы для загрузки не выбраны.', 'warning')
392
- return redirect(url_for('dashboard'))
393
-
394
- if len(files) > 20:
395
- flash('Максимум 20 файлов за раз!', 'warning')
396
- # Consider returning a JSON response if the request came from JS/XHR
397
- if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
398
- return jsonify({'status': 'error', 'message': 'Максимум 20 файлов за раз!'}), 400
399
  return redirect(url_for('dashboard'))
400
 
401
- os.makedirs('uploads', exist_ok=True)
402
- api = HfApi()
403
- uploaded_file_infos = []
404
- errors = []
405
-
406
- for file in files:
407
- if file and file.filename:
408
- original_filename = file.filename
409
- secure_original_filename = secure_filename(original_filename)
410
- if not secure_original_filename: # Handle cases like '...'
411
- secure_original_filename = 'file'
412
-
413
- unique_filename = f"{uuid.uuid4().hex}_{secure_original_filename}"
414
- temp_path = os.path.join('uploads', unique_filename) # Use unique name for temp file too
415
- hf_path = f"cloud_files/{username}/{unique_filename}"
416
 
417
- try:
 
 
 
 
418
  file.save(temp_path)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
419
 
420
- api.upload_file(
421
- path_or_fileobj=temp_path,
422
- path_in_repo=hf_path,
423
- repo_id=REPO_ID,
424
- repo_type="dataset",
425
- token=HF_TOKEN_WRITE,
426
- commit_message=f"User {username} uploaded {original_filename}"
427
- )
428
-
429
- file_info = {
430
- 'original_filename': original_filename, # Store original name for display
431
- 'hf_path': hf_path, # Store the unique path on HF
432
- 'type': get_file_type(original_filename),
433
- 'upload_date': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
434
- # Add size later if needed: 'size': os.path.getsize(temp_path)
435
- }
436
- # Ensure user exists and has 'files' list
437
- if username in data['users'] and isinstance(data['users'][username].get('files'), list):
438
- data['users'][username]['files'].append(file_info)
439
- uploaded_file_infos.append(original_filename)
440
- else:
441
- logging.error(f"User {username} or their file list not found/invalid during upload.")
442
- errors.append(f"Ошибка структуры данных для пользователя {username}")
443
-
444
- except Exception as e:
445
- logging.error(f"Error uploading file {original_filename} for user {username}: {e}")
446
- errors.append(f"Ошибка загрузки файла: {original_filename}")
447
- finally:
448
- if os.path.exists(temp_path):
449
- try:
450
- os.remove(temp_path)
451
- except Exception as e_rem:
452
- logging.error(f"Error removing temp file {temp_path}: {e_rem}")
453
-
454
- if uploaded_file_infos or errors:
455
- try:
456
- save_data(data)
457
- if uploaded_file_infos:
458
- flash(f"Успешно загружено: {', '.join(uploaded_file_infos)}", 'success')
459
- if errors:
460
- flash(f"Произошли ошибки: {'; '.join(errors)}", 'error')
461
- except Exception as e:
462
- flash('Критическая ошибка при сохранении данных после загрузки!', 'error')
463
- logging.error(f"Error saving data after upload: {e}")
464
-
465
- # For AJAX requests, return JSON status
466
- if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
467
- if not errors:
468
- return jsonify({'status': 'success'})
469
- else:
470
- # Return error even if some succeeded, let client reload to see changes/errors
471
- return jsonify({'status': 'error', 'message': '; '.join(errors)}), 500
472
-
473
- return redirect(url_for('dashboard'))
474
 
475
- # GET request part
476
- user_data = data['users'].get(username, {})
477
- user_files = sorted(user_data.get('files', []), key=lambda x: x.get('upload_date', ''), reverse=True)
478
-
479
- # Generate HF URLs for previews
480
- for file_info in user_files:
481
- try:
482
- # Use hf_hub_url for generating the correct URL (handles private repos if token is set)
483
- # We need the non-download URL for previews like images/iframes
484
- file_info['preview_url'] = hf_hub_url(
485
- repo_id=REPO_ID,
486
- filename=file_info['hf_path'],
487
- repo_type='dataset',
488
- revision='main' # or specific commit hash
489
- )
490
- # URL for modal (image/video) might need '?download=true' or token if private, handled by JS/direct link
491
- file_info['modal_url'] = file_info['preview_url'] # Start with base url
492
- # Add token parameter IF necessary and repo is private (complex logic, maybe simplify)
493
- # Simplification: Assume public or token handled by browser session/cookies if needed
494
- # OR pass token to JS if needed for fetch/modal
495
- file_info['download_url'] = url_for('download_file', hf_path=file_info['hf_path'], original_filename=file_info['original_filename'])
496
- file_info['delete_url'] = url_for('delete_file', hf_path=file_info['hf_path'])
497
- except Exception as e:
498
- logging.error(f"Error generating HF URL for {file_info.get('hf_path')}: {e}")
499
- file_info['preview_url'] = '#'
500
- file_info['modal_url'] = '#'
501
- file_info['download_url'] = '#'
502
- file_info['delete_url'] = '#'
503
 
 
504
 
505
  html = '''
506
  <!DOCTYPE html>
@@ -515,423 +542,270 @@ def dashboard():
515
  <body>
516
  <div class="container">
517
  <h1>Панель управления Zeus Cloud</h1>
518
- <p style="text-align: center;">Пользователь: <strong>{{ username }}</strong></p>
519
- <div id="flash-container">
520
- {% with messages = get_flashed_messages(with_categories=true) %}
521
- {% if messages %}
522
- {% for category, message in messages %}
523
- <div class="flash flash-{{ category }}">{{ message }}</div>
524
- {% endfor %}
525
- {% endif %}
526
- {% endwith %}
527
- </div>
528
  <form id="upload-form" method="POST" enctype="multipart/form-data">
529
- <input type="file" name="files" multiple required style="margin-bottom: 10px; display: block; background: transparent; box-shadow: none; padding: 10px; border: 1px dashed var(--accent); border-radius: 10px;">
530
- <button type="submit" class="btn" id="upload-btn">Загрузить файлы (макс 20)</button>
531
  </form>
532
  <div id="progress-container">
533
- <div id="progress-bar">0%</div>
534
  </div>
535
  <h2 style="margin-top: 30px;">Ваши файлы</h2>
536
- <p style="text-align: center; margin-top: -5px; margin-bottom: 15px; color: var(--accent); font-size: 0.9em;">Ваши файлы под надежной защитой квантовой криптографии* <br><small style="font-size: 0.8em; color: gray;">(*маркетинговое заявление)</small></p>
537
  <div class="file-grid">
538
  {% for file in user_files %}
539
  <div class="file-item">
540
- <div class="file-preview-container">
541
- {% if file['type'] == 'video' %}
542
- <video class="file-preview" preload="metadata" muted loading="lazy" onclick="openModal('{{ file.modal_url }}', true)">
543
- <source src="{{ file.preview_url }}" type="video/mp4"> {# Use basic URL for preview source #}
544
- Ваш браузер не поддерживает видео тег.
545
- </video>
546
- {% elif file['type'] == 'image' %}
547
- <img class="file-preview" src="{{ file.preview_url }}" alt="{{ file.original_filename }}" loading="lazy" onclick="openModal(this.src, false)">
548
- {% elif file['type'] == 'pdf' %}
549
- <iframe class="file-preview-embed" src="{{ file.preview_url }}" title="PDF Preview: {{ file.original_filename }}"></iframe>
550
- {% elif file['type'] == 'text' %}
551
- <iframe class="file-preview-embed" src="{{ file.preview_url }}" title="Text Preview: {{ file.original_filename }}"></iframe>
552
- {% else %}
553
- <p style="align-self: center; color: var(--accent);">Нет предпросмотра для {{ file.type }}</p>
554
- {% endif %}
555
- </div>
556
- <div class="file-item-info">
557
- <p class="filename" title="{{ file.original_filename }}">{{ file.original_filename | truncate(30, True) }}</p>
558
- <p style="font-size: 0.8em; color: grey;">{{ file.upload_date }}</p>
559
- <a href="{{ file.download_url }}" class="btn download-btn" style="font-size: 0.9em; padding: 8px 15px;">Скачать</a>
560
- <a href="{{ file.delete_url }}" class="btn delete-btn" style="font-size: 0.9em; padding: 8px 15px; margin-left: 5px;" onclick="return confirm('Вы уверены, что хотите удалить файл \\'{{ file.original_filename }}\\'?');">Удалить</a>
561
- </div>
562
  </div>
563
  {% endfor %}
564
  {% if not user_files %}
565
- <p style="grid-column: 1 / -1; text-align: center;">Вы еще не загрузили ни одного файла.</p>
566
  {% endif %}
567
  </div>
568
-
569
- <div style="margin-top: 40px; padding: 20px; background: var(--glass-bg); border-radius: 15px;">
570
- <h2>Добавить на главный экран</h2>
571
- <p>Для быстрого доступа к Zeus Cloud, вы можете добавить это приложение на главный экран вашего телефона:</p>
572
- <div style="display: flex; gap: 20px; margin-top: 15px; flex-wrap: wrap;">
573
- <div style="flex: 1; min-width: 250px;">
574
- <h4>Android (Chrome):</h4>
575
- <ol style="padding-left: 20px; font-size: 0.9em;">
576
- <li>Откройте Zeus Cloud в браузере Chrome.</li>
577
- <li>Нажмите на меню (три точки).</li>
578
- <li>Выберите <strong>"Установить приложение"</strong> или <strong>"Добавить на главный экран"</strong>.</li>
579
- <li>Подтвердите добавление.</li>
580
- </ol>
581
- </div>
582
- <div style="flex: 1; min-width: 250px;">
583
- <h4>iOS (Safari):</h4>
584
- <ol style="padding-left: 20px; font-size: 0.9em;">
585
- <li>Откройте Zeus Cloud в браузере Safari.</li>
586
- <li>Нажмите кнопку <strong>"Поделиться"</strong> (квадрат со стрелкой).</li>
587
- <li>Прокрутите вниз и выберите <strong>"На экран «Домой»"</strong>.</li>
588
- <li>Нажмите <strong>"Добавить"</strong>.</li>
589
- </ol>
590
- </div>
591
- </div>
592
  </div>
593
-
594
- <a href="{{ url_for('logout') }}" class="btn" style="margin-top: 30px; background: var(--accent);" id="logout-btn">Выйти</a>
 
 
 
 
 
 
 
 
595
  </div>
596
-
597
- <div class="modal" id="mediaModal" onclick="closeModalOnClickOutside(event)">
598
- <div class="modal-content">
599
- <span class="modal-close" onclick="closeModal()">×</span>
600
- <div id="modalBody"></div>
601
- </div>
602
  </div>
603
-
604
  <script>
605
- const HF_TOKEN_READ = "{{ HF_TOKEN_READ or '' }}"; // Pass token to JS if needed
606
-
607
- function openModal(src, isVideo) {
608
  const modal = document.getElementById('mediaModal');
609
- const modalBody = document.getElementById('modalBody');
610
- let content = '';
611
- // Note: For private repos, accessing direct URLs might require a token.
612
- // This basic implementation assumes public access or browser-handled auth.
613
- // For robust private repo access, fetch might be needed with Authorization header.
614
- if (isVideo) {
615
- // Add controls and autoplay for user convenience
616
- content = `<video controls autoplay style="max-width: 90vw; max-height: 90vh; display: block; margin: auto;"><source src="${src}" type="video/mp4">Ваш браузер не поддерживает видео.</video>`;
 
 
 
 
 
617
  } else {
618
- content = `<img src="${src}" style="max-width: 90vw; max-height: 90vh; display: block; margin: auto;">`;
619
  }
620
- modalBody.innerHTML = content;
621
  modal.style.display = 'flex';
622
  }
623
-
624
- function closeModal() {
625
  const modal = document.getElementById('mediaModal');
626
- const modalBody = document.getElementById('modalBody');
627
- const video = modalBody.querySelector('video');
628
- if (video) {
629
- video.pause();
 
 
 
630
  }
631
- modalBody.innerHTML = ''; // Clear content
632
- modal.style.display = 'none';
633
  }
634
 
635
- function closeModalOnClickOutside(event) {
636
- const modal = document.getElementById('mediaModal');
637
- if (event.target === modal) { // Close only if clicking the background overlay
638
- closeModal();
639
- }
640
- }
641
-
642
- // Upload Progress Handling
643
  const form = document.getElementById('upload-form');
644
  const progressBar = document.getElementById('progress-bar');
645
  const progressContainer = document.getElementById('progress-container');
646
  const uploadBtn = document.getElementById('upload-btn');
647
- const fileInput = form.querySelector('input[type="file"]');
648
- const flashContainer = document.getElementById('flash-container');
649
 
650
  form.addEventListener('submit', function(e) {
651
  e.preventDefault();
652
- flashContainer.innerHTML = ''; // Clear previous flashes
653
-
654
- const files = fileInput.files;
 
 
655
  if (files.length === 0) {
656
- flashContainer.innerHTML = '<div class="flash flash-warning">Пожалуйста, выберите файлы для загрузки.</div>';
657
  return;
658
  }
659
- if (files.length > 20) {
660
- flashContainer.innerHTML = '<div class="flash flash-warning">Максимум 20 файлов за раз!</div>';
661
- return;
662
- }
663
 
664
  const formData = new FormData(form);
665
  progressContainer.style.display = 'block';
666
  progressBar.style.width = '0%';
667
- progressBar.textContent = '0%';
668
  uploadBtn.disabled = true;
669
- uploadBtn.textContent = 'Загрузка...';
670
 
671
  const xhr = new XMLHttpRequest();
672
- xhr.open('POST', '{{ url_for("dashboard") }}', true);
673
- // Identify request as AJAX for backend
674
- xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
675
 
676
  xhr.upload.onprogress = function(event) {
677
  if (event.lengthComputable) {
678
- const percent = Math.round((event.loaded / event.total) * 100);
679
  progressBar.style.width = percent + '%';
680
- progressBar.textContent = percent + '%';
681
  }
682
  };
683
 
684
  xhr.onload = function() {
685
  uploadBtn.disabled = false;
686
- uploadBtn.textContent = 'Загрузить файлы (макс 20)';
687
  progressContainer.style.display = 'none';
688
  progressBar.style.width = '0%';
689
- progressBar.textContent = '0%';
690
-
691
- if (xhr.status >= 200 && xhr.status < 300) {
692
- // Success or partial success, reload to show results and flashes
693
- window.location.reload();
694
- } else {
695
- // Handle errors shown from server or network errors
696
- let errorMsg = 'Ошибка загрузки!';
697
- try {
698
- const response = JSON.parse(xhr.responseText);
699
- if (response.message) {
700
- errorMsg = response.message;
701
- }
702
- } catch (e) { /* Ignore parsing error, use default message */ }
703
- flashContainer.innerHTML = `<div class="flash flash-error">${errorMsg}</div>`;
704
  }
705
  };
706
 
707
  xhr.onerror = function() {
708
- uploadBtn.disabled = false;
709
- uploadBtn.textContent = 'Загрузить файлы (макс 20)';
710
- progressContainer.style.display = 'none';
711
- progressBar.style.width = '0%';
712
- progressBar.textContent = '0%';
713
- flashContainer.innerHTML = '<div class="flash flash-error">Ошибка сети при загрузке!</div>';
714
  };
715
 
716
  xhr.send(formData);
717
  });
718
 
719
- // Logout clears local storage
720
  document.getElementById('logout-btn').addEventListener('click', function(e) {
 
721
  localStorage.removeItem('zeusCredentials');
722
- // Allow the default link behavior to proceed to /logout
723
  });
724
  </script>
725
  </body>
726
  </html>
727
  '''
728
- # Pass username, files, repo_id and token (if needed) to template
729
- return render_template_string(html, username=username, user_files=user_files, repo_id=REPO_ID, HF_TOKEN_READ=HF_TOKEN_READ)
730
-
731
 
732
- @app.route('/download/<path:hf_path>/<original_filename>')
733
- def download_file(hf_path, original_filename):
734
  if 'username' not in session:
735
- flash('��ожалуйста, войдите в систему!', 'error')
736
  return redirect(url_for('login'))
737
 
738
  username = session['username']
739
  data = load_data()
740
- is_admin_request = request.referrer and 'admhosto' in request.referrer
741
-
742
- # Permission check: User must own the file OR it's an admin request
743
- if not is_admin_request:
744
- if username not in data['users']:
745
- session.pop('username', None)
746
- flash('Пользователь не найден!', 'error')
747
- return redirect(url_for('login'))
748
- user_files = data['users'][username].get('files', [])
749
- if not any(file.get('hf_path') == hf_path for file in user_files):
750
- flash('У вас нет доступа к этому файлу или файл не найден!', 'error')
751
- return redirect(url_for('dashboard'))
752
- else: # Admin request check if user exists
753
- owner_username = hf_path.split('/')[1] if '/' in hf_path else None
754
- if owner_username not in data['users']:
755
- flash(f'Владелец файла ({owner_username}) не найден!', 'error')
756
- return redirect(url_for('admin_panel'))
757
 
 
 
 
 
 
 
758
 
759
- # Generate the download URL (usually includes ?download=true)
760
- # Using hf_hub_url might be safer if direct URL structure changes
761
  try:
762
- # Get a URL that forces download
763
- file_url = hf_hub_url(repo_id=REPO_ID, filename=hf_path, repo_type="dataset", revision="main")
764
- # Append download=true if not already present (hf_hub_url might not add it)
765
- file_url += "?download=true"
766
-
767
  api = HfApi()
768
  headers = {}
769
  if HF_TOKEN_READ:
770
  headers["authorization"] = f"Bearer {HF_TOKEN_READ}"
771
 
772
- # Use requests to stream the download
773
  response = requests.get(file_url, headers=headers, stream=True)
774
- response.raise_for_status() # Check for HTTP errors (like 404 Not Found)
775
 
776
- # Stream the content using send_file
777
  return send_file(
778
- BytesIO(response.content), # Read content into memory (consider streaming for large files)
779
  as_attachment=True,
780
- download_name=original_filename, # Use the original filename for the user
781
- mimetype='application/octet-stream' # Generic mimetype
782
  )
783
  except requests.exceptions.RequestException as e:
784
- logging.error(f"Error downloading file from HF ({hf_path}): {e}")
785
- flash('Ошибка скачивания файла с сервера хранилища!', 'error')
 
 
 
 
786
  except Exception as e:
787
- logging.error(f"Unexpected error during download ({hf_path}): {e}")
788
- flash('Произошла непредвиденная ошибка при скачивании файла.', 'error')
789
-
790
- # Redirect back based on where the request came from
791
- if is_admin_request:
792
- owner_username = hf_path.split('/')[1] if '/' in hf_path else None
793
- if owner_username:
794
- return redirect(url_for('admin_user_files', username=owner_username))
795
- else:
796
- return redirect(url_for('admin_panel')) # Fallback
797
- else:
798
- return redirect(url_for('dashboard'))
799
-
800
 
801
- @app.route('/delete/<path:hf_path>')
802
- def delete_file(hf_path):
803
  if 'username' not in session:
804
- flash('Пожалуйста, войдите в систему!', 'error')
805
  return redirect(url_for('login'))
806
- if not HF_TOKEN_WRITE:
807
- flash('Удаление невозможна: токен для записи на Hugging Face не настроен.', 'error')
808
- return redirect(url_for('dashboard'))
809
-
810
 
811
  username = session['username']
812
  data = load_data()
813
- if username not in data['users'] or 'files' not in data['users'][username]:
814
  session.pop('username', None)
815
- flash('Пользователь или его файлы не найдены!', 'error')
816
  return redirect(url_for('login'))
817
 
818
  user_files = data['users'][username]['files']
819
- original_filename = "Неизвестный файл"
820
- file_found_in_db = False
821
- for file_info in user_files:
822
- if file_info.get('hf_path') == hf_path:
823
- original_filename = file_info.get('original_filename', original_filename)
824
- file_found_in_db = True
825
- break
826
-
827
- if not file_found_in_db:
828
- flash(f'Файл ({original_filename}) не найден в вашей базе данных!', 'warning')
829
- # Optionally try deleting from HF anyway if out of sync? For now, just redirect.
830
  return redirect(url_for('dashboard'))
831
 
832
  try:
833
  api = HfApi()
834
  api.delete_file(
835
- path_in_repo=hf_path,
836
  repo_id=REPO_ID,
837
  repo_type="dataset",
838
  token=HF_TOKEN_WRITE,
839
- commit_message=f"User {username} deleted file {original_filename}"
840
  )
841
- # Remove from database only after successful HF deletion
842
- data['users'][username]['files'] = [f for f in user_files if f.get('hf_path') != hf_path]
843
  save_data(data)
844
- flash(f'Файл "{original_filename}" успешно удален!', 'success')
845
- logging.info(f"User {username} deleted file {hf_path}")
846
-
847
  except Exception as e:
848
- # Check if file not found error means it was already deleted
849
- if "404" in str(e) or "not found" in str(e).lower():
850
- logging.warning(f"File {hf_path} not found on HF during delete attempt by {username}, possibly already deleted. Removing from DB.")
851
- data['users'][username]['files'] = [f for f in user_files if f.get('hf_path') != hf_path]
852
- try:
853
- save_data(data)
854
- flash(f'Файл "{original_filename}" не найден в хранилище, удален из списка.', 'warning')
855
- except Exception as save_e:
856
- flash('Ошибка при обновлении базы данных после обнаружения отсутствующего файла.', 'error')
857
- logging.error(f"Error saving data after failed delete (file not found): {save_e}")
858
-
859
- else:
860
- logging.error(f"Error deleting file {hf_path} for user {username}: {e}")
861
- flash(f'Ошибка при удалении файла "{original_filename}"!', 'error')
862
 
863
  return redirect(url_for('dashboard'))
864
 
865
  @app.route('/logout')
866
  def logout():
867
  session.pop('username', None)
868
- flash('Вы успешно вышли из системы.', 'success')
869
- # JS on login page handles localStorage clearing
870
  return redirect(url_for('login'))
871
 
872
-
873
- # --- Admin Routes ---
874
-
875
- # Simple password protection for admin routes (replace with proper auth)
876
- ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "adminpass123")
877
-
878
- def check_admin():
879
- """Checks if admin is logged in via session."""
880
- return session.get('is_admin')
881
-
882
- @app.route('/admhosto/login', methods=['GET', 'POST'])
883
- def admin_login():
884
- if request.method == 'POST':
885
- password = request.form.get('password')
886
- if password == ADMIN_PASSWORD:
887
- session['is_admin'] = True
888
- flash('Успешный вход в админ-панель.', 'success')
889
- return redirect(url_for('admin_panel'))
890
- else:
891
- flash('Неверный пароль администратора.', 'error')
892
- return redirect(url_for('admin_login'))
893
-
894
- if check_admin():
895
- return redirect(url_for('admin_panel'))
896
-
897
- html = f'''
898
- <!DOCTYPE html>
899
- <html lang="ru">
900
- <head>
901
- <meta charset="UTF-8"><title>Вход в Админ-панель</title>
902
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
903
- <style>{BASE_STYLE}</style>
904
- </head>
905
- <body>
906
- <div class="container">
907
- <h1>Вход в Админ-панель</h1>
908
- {% with messages = get_flashed_messages(with_categories=true) %}
909
- {% if messages %}{% for category, message in messages %}
910
- <div class="flash flash-{{ category }}">{{ message }}</div>
911
- {% endfor %}{% endif %}
912
- {% endwith %}
913
- <form method="POST">
914
- <input type="password" name="password" placeholder="Пароль администратора" required>
915
- <button type="submit" class="btn">Войти</button>
916
- </form>
917
- </div>
918
- </body></html>'''
919
- return render_template_string(html)
920
-
921
- @app.route('/admhosto/logout')
922
- def admin_logout():
923
- session.pop('is_admin', None)
924
- flash('Вы вышли из админ-панели.', 'info')
925
- return redirect(url_for('admin_login'))
926
-
927
-
928
  @app.route('/admhosto')
929
  def admin_panel():
930
- if not check_admin():
931
- return redirect(url_for('admin_login'))
932
-
933
  data = load_data()
934
- users = data.get('users', {})
935
 
936
  html = '''
937
  <!DOCTYPE html>
@@ -946,32 +820,31 @@ def admin_panel():
946
  <body>
947
  <div class="container">
948
  <h1>Админ-панель Zeus Cloud</h1>
949
- <a href="{{ url_for('admin_logout') }}" class="btn delete-btn" style="position: absolute; top: 30px; right: 30px; padding: 10px 15px; font-size: 0.9em;">Выйти</a>
950
- {% with messages = get_flashed_messages(with_categories=true) %}
951
- {% if messages %}<div style="margin-top: 15px;">
952
- {% for category, message in messages %}
953
- <div class="flash flash-{{ category }}">{{ message }}</div>
954
- {% endfor %}
955
- </div>{% endif %}
956
- {% endwith %}
957
- <h2>Список пользователей ({{ users | length }})</h2>
958
  <div class="user-list">
959
- {% for username, user_data in users.items() | sort %}
960
  <div class="user-item">
961
- <strong style="font-size: 1.1em;"><a href="{{ url_for('admin_user_files', username=username) }}">{{ username }}</a></strong>
962
- <p style="font-size: 0.85em; color: grey;">Дата регистрации: {{ user_data.get('created_at', 'N/A') }}</p>
963
- <p style="font-size: 0.85em; color: grey;">Количество файлов: {{ user_data.get('files', []) | length }}</p>
964
- <div style="margin-top: 10px;">
965
- <form method="POST" action="{{ url_for('admin_delete_user', username=username) }}" style="display: inline;" onsubmit="return confirm('Вы уверены, что хотите УДАЛИТЬ пользователя {{ username }} и ВСЕ его файлы? Это действие НЕОБРАТИМО!');">
966
- <button type="submit" class="btn delete-btn" style="padding: 5px 10px; font-size: 0.9em;">Удалить пользователя</button>
967
- </form>
968
- </div>
969
  </div>
970
  {% endfor %}
971
  {% if not users %}
972
  <p>Пользователей пока нет.</p>
973
  {% endif %}
974
  </div>
 
 
 
 
 
 
 
 
 
975
  </div>
976
  </body>
977
  </html>
@@ -980,31 +853,13 @@ def admin_panel():
980
 
981
  @app.route('/admhosto/user/<username>')
982
  def admin_user_files(username):
983
- if not check_admin():
984
- return redirect(url_for('admin_login'))
985
-
986
  data = load_data()
987
- if username not in data.get('users', {}):
988
- flash(f'Пользователь "{username}" не найден!', 'error')
989
  return redirect(url_for('admin_panel'))
990
 
991
  user_files = sorted(data['users'][username].get('files', []), key=lambda x: x.get('upload_date', ''), reverse=True)
992
 
993
- # Generate HF URLs for previews
994
- for file_info in user_files:
995
- try:
996
- file_info['preview_url'] = hf_hub_url(repo_id=REPO_ID, filename=file_info['hf_path'], repo_type='dataset')
997
- file_info['modal_url'] = file_info['preview_url'] # Use same base URL
998
- file_info['download_url'] = url_for('download_file', hf_path=file_info['hf_path'], original_filename=file_info['original_filename'])
999
- file_info['admin_delete_url'] = url_for('admin_delete_file', username=username, hf_path=file_info['hf_path'])
1000
- except Exception as e:
1001
- logging.error(f"Admin: Error generating HF URL for {file_info.get('hf_path')}: {e}")
1002
- file_info['preview_url'] = '#'
1003
- file_info['modal_url'] = '#'
1004
- file_info['download_url'] = '#'
1005
- file_info['admin_delete_url'] = '#'
1006
-
1007
-
1008
  html = '''
1009
  <!DOCTYPE html>
1010
  <html lang="ru">
@@ -1017,237 +872,181 @@ def admin_user_files(username):
1017
  </head>
1018
  <body>
1019
  <div class="container">
1020
- <a href="{{ url_for('admin_panel') }}" class="btn" style="position: absolute; top: 30px; left: 30px; padding: 10px 15px; font-size: 0.9em; background: var(--accent);">← Назад</a>
1021
- <a href="{{ url_for('admin_logout') }}" class="btn delete-btn" style="position: absolute; top: 30px; right: 30px; padding: 10px 15px; font-size: 0.9em;">Выйти</a>
1022
  <h1>Файлы пользователя: {{ username }}</h1>
1023
- {% with messages = get_flashed_messages(with_categories=true) %}
1024
  {% if messages %}
1025
- {% for category, message in messages %}
1026
- <div class="flash flash-{{ category }}">{{ message }}</div>
1027
  {% endfor %}
1028
  {% endif %}
1029
  {% endwith %}
1030
  <div class="file-grid">
1031
  {% for file in user_files %}
1032
  <div class="file-item">
1033
- <div class="file-preview-container">
1034
- {% if file['type'] == 'video' %}
1035
- <video class="file-preview" preload="metadata" muted loading="lazy" onclick="openModal('{{ file.modal_url }}', true)">
1036
- <source src="{{ file.preview_url }}" type="video/mp4">
1037
- </video>
1038
- {% elif file['type'] == 'image' %}
1039
- <img class="file-preview" src="{{ file.preview_url }}" alt="{{ file.original_filename }}" loading="lazy" onclick="openModal(this.src, false)">
1040
- {% elif file['type'] == 'pdf' %}
1041
- <iframe class="file-preview-embed" src="{{ file.preview_url }}" title="PDF Preview: {{ file.original_filename }}"></iframe>
1042
- {% elif file['type'] == 'text' %}
1043
- <iframe class="file-preview-embed" src="{{ file.preview_url }}" title="Text Preview: {{ file.original_filename }}"></iframe>
1044
- {% else %}
1045
- <p style="align-self: center; color: var(--accent);">Нет предпросмотра для {{ file.type }}</p>
1046
- {% endif %}
1047
- </div>
1048
- <div class="file-item-info">
1049
- <p class="filename" title="{{ file.original_filename }}">{{ file.original_filename | truncate(30, True) }}</p>
1050
- <p style="font-size: 0.8em; color: grey;">{{ file.get('upload_date', 'N/A') }}</p>
1051
- <a href="{{ file.download_url }}" class="btn download-btn" style="font-size: 0.9em; padding: 8px 15px;">Скачать</a>
1052
- <form method="POST" action="{{ file.admin_delete_url }}" style="display: inline; margin-left: 5px;" onsubmit="return confirm('Вы уверены, что хотите удалить файл \\'{{ file.original_filename }}\\' пользователя {{ username }}?');">
1053
- <button type="submit" class="btn delete-btn" style="padding: 8px 15px; font-size: 0.9em;">Удалить</button>
1054
- </form>
1055
- </div>
1056
  </div>
1057
  {% endfor %}
1058
  {% if not user_files %}
1059
- <p style="grid-column: 1 / -1; text-align: center;">У этого пользователя пока нет файлов.</p>
1060
  {% endif %}
1061
  </div>
 
1062
  </div>
1063
- <div class="modal" id="mediaModal" onclick="closeModalOnClickOutside(event)">
1064
- <div class="modal-content">
1065
- <span class="modal-close" onclick="closeModal()">×</span>
1066
- <div id="modalBody"></div>
1067
- </div>
1068
  </div>
1069
  <script>
1070
- // Re-use modal functions from dashboard
1071
- function openModal(src, isVideo) {
1072
  const modal = document.getElementById('mediaModal');
1073
- const modalBody = document.getElementById('modalBody');
1074
- let content = '';
1075
- if (isVideo) {
1076
- content = `<video controls autoplay style="max-width: 90vw; max-height: 90vh; display: block; margin: auto;"><source src="${src}" type="video/mp4">Видео не поддерживается.</video>`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1077
  } else {
1078
- content = `<img src="${src}" style="max-width: 90vw; max-height: 90vh; display: block; margin: auto;">`;
1079
  }
1080
- modalBody.innerHTML = content;
1081
  modal.style.display = 'flex';
1082
  }
1083
- function closeModal() {
1084
  const modal = document.getElementById('mediaModal');
1085
- const modalBody = document.getElementById('modalBody');
1086
- const video = modalBody.querySelector('video');
1087
- if (video) { video.pause(); }
1088
- modalBody.innerHTML = '';
1089
- modal.style.display = 'none';
 
 
 
1090
  }
1091
- function closeModalOnClickOutside(event) {
1092
- if (event.target === document.getElementById('mediaModal')) { closeModal(); }
1093
- }
1094
  </script>
1095
  </body>
1096
  </html>
1097
  '''
1098
- return render_template_string(html, username=username, user_files=user_files, repo_id=REPO_ID)
1099
-
1100
 
1101
  @app.route('/admhosto/delete_user/<username>', methods=['POST'])
1102
  def admin_delete_user(username):
1103
- if not check_admin():
1104
- flash('Требуется авторизация администратора.', 'error')
1105
- return redirect(url_for('admin_login'))
1106
- if not HF_TOKEN_WRITE:
1107
- flash('Удаление невозможно: токен для записи на Hugging Face не настроен.', 'error')
1108
- return redirect(url_for('admin_panel'))
1109
-
1110
  data = load_data()
1111
- if username not in data.get('users', {}):
1112
- flash(f'Пользователь "{username}" не найден!', 'error')
1113
  return redirect(url_for('admin_panel'))
1114
 
1115
  user_files_to_delete = data['users'][username].get('files', [])
1116
- hf_paths_to_delete = [file['hf_path'] for file in user_files_to_delete if 'hf_path' in file]
1117
- folder_path = f"cloud_files/{username}"
1118
 
1119
  try:
1120
  api = HfApi()
1121
- deleted_hf = False
1122
- # Try deleting the whole folder first (more efficient)
1123
- try:
1124
- logging.info(f"Admin attempting to delete folder: {folder_path}")
1125
- api.delete_folder(
1126
- folder_path=folder_path,
1127
- repo_id=REPO_ID,
1128
- repo_type="dataset",
1129
- token=HF_TOKEN_WRITE,
1130
- commit_message=f"Admin deleted folder for user {username}"
1131
- )
1132
- logging.info(f"Admin successfully deleted folder {folder_path}")
1133
- deleted_hf = True
1134
- except Exception as folder_del_err:
1135
- logging.warning(f"Admin failed to delete folder {folder_path} ({folder_del_err}). Attempting individual file deletion.")
1136
- # Fallback: delete files individually if folder deletion fails or isn't supported well
1137
- if hf_paths_to_delete:
1138
- delete_results = api.delete_files(
1139
- paths_in_repo=hf_paths_to_delete,
1140
- repo_id=REPO_ID,
1141
- repo_type="dataset",
1142
- token=HF_TOKEN_WRITE,
1143
- commit_message=f"Admin deleted files for user {username}",
1144
- # Consider adding ignore_patterns=None, ignore_regex=None if needed
1145
- )
1146
- # Note: delete_files might not raise errors for non-existent files, just logs them.
1147
- # We assume success if no exception is raised here.
1148
- logging.info(f"Admin deleted individual files for user {username}. Results (may vary): {delete_results}")
1149
- deleted_hf = True # Assume deletion attempted/succeeded for DB removal
1150
-
1151
-
1152
- # Delete user from database AFTER attempting HF deletion
1153
  del data['users'][username]
1154
  save_data(data)
1155
- flash(f'Пользователь "{username}" и его файлы (попытка удаления из хранилища) успешно удалены из базы данных!', 'success')
1156
- logging.info(f"Admin deleted user {username} entry from database.")
1157
 
1158
  except Exception as e:
1159
- logging.error(f"Error during admin deletion of user {username}: {e}")
1160
- flash(f'Произошла ошибка при удалении пользователя "{username}"!', 'error')
1161
 
1162
  return redirect(url_for('admin_panel'))
1163
 
 
 
 
 
 
 
1164
 
1165
- @app.route('/admhosto/delete_file/<username>/<path:hf_path>', methods=['POST'])
1166
- def admin_delete_file(username, hf_path):
1167
- if not check_admin():
1168
- flash('Требуется авторизация администратора.', 'error')
1169
- return redirect(url_for('admin_login'))
1170
- if not HF_TOKEN_WRITE:
1171
- flash('Удаление невозможно: токен для записи на Hugging Face не настроен.', 'error')
1172
- return redirect(url_for('admin_user_files', username=username))
1173
-
1174
- data = load_data()
1175
- if username not in data.get('users', {}):
1176
- flash(f'Пользователь "{username}" не найден!', 'error')
1177
- return redirect(url_for('admin_panel'))
1178
-
1179
- user_files = data['users'][username].get('files', [])
1180
- original_filename = "Неизвестный файл"
1181
- file_found_in_db = False
1182
- for file_info in user_files:
1183
- if file_info.get('hf_path') == hf_path:
1184
- original_filename = file_info.get('original_filename', original_filename)
1185
- file_found_in_db = True
1186
- break
1187
-
1188
- if not file_found_in_db:
1189
- flash(f'Файл ({original_filename}) не найден в базе данных пользователя "{username}"!', 'warning')
1190
- # Proceed with HF deletion attempt anyway in case DB is out of sync
1191
 
1192
- try:
1193
- api = HfApi()
1194
- api.delete_file(
1195
- path_in_repo=hf_path,
1196
- repo_id=REPO_ID,
1197
- repo_type="dataset",
1198
- token=HF_TOKEN_WRITE,
1199
- commit_message=f"Admin deleted file {original_filename} for user {username}"
1200
- )
1201
 
1202
- # Remove from DB after successful HF deletion (or if it wasn't in DB initially)
1203
- data['users'][username]['files'] = [f for f in user_files if f.get('hf_path') != hf_path]
1204
- save_data(data)
1205
- flash(f'Файл "{original_filename}" пользователя "{username}" успешно удален!', 'success')
1206
- logging.info(f"Admin deleted file {hf_path} for user {username}")
 
 
 
 
1207
 
1208
- except Exception as e:
1209
- # Check if file not found error means it was already deleted
1210
- if "404" in str(e) or "not found" in str(e).lower():
1211
- logging.warning(f"Admin: File {hf_path} not found on HF during delete attempt for {username}. Removing from DB.")
1212
- data['users'][username]['files'] = [f for f in user_files if f.get('hf_path') != hf_path]
1213
- try:
1214
- save_data(data)
1215
- flash(f'Файл "{original_filename}" не найден в хранилище, удален из списка пользователя "{username}".', 'warning')
1216
- except Exception as save_e:
1217
- flash('Ошибка при обновлении базы данных после обнаружения отсутствующего файла.', 'error')
1218
- logging.error(f"Admin: Error saving data after failed delete (file not found): {save_e}")
1219
- else:
1220
- logging.error(f"Admin error deleting file {hf_path} for user {username}: {e}")
1221
- flash(f'Ошибка при удалении файла "{original_filename}" пользователя "{username}"!', 'error')
1222
 
1223
- return redirect(url_for('admin_user_files', username=username))
 
 
1224
 
 
1225
 
1226
  if __name__ == '__main__':
1227
  if not HF_TOKEN_WRITE:
1228
  logging.warning("HF_TOKEN (write access) is not set. File uploads and deletions will fail.")
1229
  if not HF_TOKEN_READ:
1230
- logging.warning("HF_TOKEN_READ is not set. Falling back to HF_TOKEN (if set). File downloads/previews might fail for private repos if HF_TOKEN is also not set.")
1231
- if not os.getenv("ADMIN_PASSWORD"):
1232
- logging.warning("ADMIN_PASSWORD environment variable not set. Using default weak password 'adminpass123'.")
1233
-
1234
 
1235
- # Initial data load and potential download from HF
1236
- logging.info("Performing initial data load...")
1237
- load_data()
1238
- logging.info("Initial data load complete.")
1239
-
1240
-
1241
- # Start periodic backup only if write token is available
1242
  if HF_TOKEN_WRITE:
1243
- backup_thread = threading.Thread(target=periodic_backup, daemon=True)
1244
- backup_thread.start()
1245
- logging.info("Periodic backup thread started.")
1246
  else:
1247
  logging.warning("Periodic backup disabled because HF_TOKEN (write access) is not set.")
1248
 
1249
- # Use waitress or gunicorn in production instead of Flask's built-in server
1250
- # Example using Flask's server for development:
1251
- app.run(debug=False, host='0.0.0.0', port=int(os.getenv("PORT", 7860)))
1252
-
1253
- # --- END OF FILE app (8).py ---
 
 
 
1
  from flask import Flask, render_template_string, request, redirect, url_for, session, flash, send_file, jsonify
2
  from flask_caching import Cache
3
  import json
 
5
  import logging
6
  import threading
7
  import time
 
8
  from datetime import datetime
9
+ from huggingface_hub import HfApi, hf_hub_download
10
  from werkzeug.utils import secure_filename
11
  import requests
12
  from io import BytesIO
13
+ import uuid
14
+ import mimetypes
15
 
16
  app = Flask(__name__)
17
  app.secret_key = os.getenv("FLASK_SECRET_KEY", "supersecretkey")
 
34
  return {'users': {}, 'files': {}}
35
  data.setdefault('users', {})
36
  data.setdefault('files', {})
 
 
 
37
  logging.info("Data successfully loaded")
38
  return data
 
 
 
 
 
 
39
  except Exception as e:
40
  logging.error(f"Error loading data: {e}")
41
  return {'users': {}, 'files': {}}
 
52
  raise
53
 
54
  def upload_db_to_hf():
 
 
 
55
  try:
56
  api = HfApi()
57
  api.upload_file(
 
67
  logging.error(f"Error uploading database: {e}")
68
 
69
  def download_db_from_hf():
 
 
 
 
 
 
70
  try:
71
  hf_hub_download(
72
  repo_id=REPO_ID,
 
74
  repo_type="dataset",
75
  token=HF_TOKEN_READ,
76
  local_dir=".",
77
+ local_dir_use_symlinks=False
 
78
  )
79
  logging.info("Database downloaded from Hugging Face")
80
  except Exception as e:
81
  logging.error(f"Error downloading database: {e}")
82
  if not os.path.exists(DATA_FILE):
83
+ with open(DATA_FILE, 'w', encoding='utf-8') as f:
 
84
  json.dump({'users': {}, 'files': {}}, f)
85
 
86
  def periodic_backup():
87
  while True:
88
+ upload_db_to_hf()
89
+ time.sleep(1800)
 
 
 
 
 
 
 
 
 
90
 
91
  def get_file_type(filename):
92
+ mime_type, _ = mimetypes.guess_type(filename)
93
+ if mime_type:
94
+ if mime_type.startswith('image'):
95
+ return 'image'
96
+ elif mime_type.startswith('video'):
97
+ return 'video'
98
+ elif mime_type == 'application/pdf':
99
+ return 'pdf'
100
+ elif mime_type == 'text/plain':
101
+ return 'text'
 
 
 
 
102
  return 'other'
103
 
104
  BASE_STYLE = '''
105
  :root {
106
+ --primary: #ff4d6d;
107
+ --secondary: #00ddeb;
108
+ --accent: #8b5cf6;
109
+ --background-light: #f5f6fa;
110
+ --background-dark: #1a1625;
111
+ --card-bg: rgba(255, 255, 255, 0.95);
112
+ --card-bg-dark: rgba(40, 35, 60, 0.95);
113
+ --text-light: #2a1e5a;
114
+ --text-dark: #e8e1ff;
115
+ --shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
116
+ --glass-bg: rgba(255, 255, 255, 0.15);
117
+ --transition: all 0.3s ease;
118
+ --delete-color: #ff4444;
119
  }
120
  * { margin: 0; padding: 0; box-sizing: border-box; }
121
+ body {
122
+ font-family: 'Inter', sans-serif;
123
+ background: var(--background-light);
124
+ color: var(--text-light);
125
+ line-height: 1.6;
126
+ }
127
+ body.dark {
128
+ background: var(--background-dark);
129
+ color: var(--text-dark);
130
+ }
131
+ .container {
132
+ margin: 20px auto;
133
+ max-width: 1200px;
134
+ padding: 25px;
135
+ background: var(--card-bg);
136
+ border-radius: 20px;
137
+ box-shadow: var(--shadow);
138
+ }
139
+ body.dark .container {
140
+ background: var(--card-bg-dark);
141
+ }
142
+ h1 {
143
+ font-size: 2em;
144
+ font-weight: 800;
145
+ text-align: center;
146
+ margin-bottom: 25px;
147
+ background: linear-gradient(135deg, var(--primary), var(--accent));
148
+ -webkit-background-clip: text;
149
+ color: transparent;
150
+ }
151
+ h2 {
152
+ font-size: 1.5em;
153
+ margin-top: 30px;
154
+ color: var(--text-light);
155
+ }
156
+ body.dark h2 {
157
+ color: var(--text-dark);
158
+ }
159
+ input, textarea {
160
+ width: 100%;
161
+ padding: 14px;
162
+ margin: 12px 0;
163
+ border: none;
164
+ border-radius: 14px;
165
+ background: var(--glass-bg);
166
+ color: var(--text-light);
167
+ font-size: 1.1em;
168
+ box-shadow: inset 0 3px 10px rgba(0, 0, 0, 0.1);
169
+ }
170
+ body.dark input, body.dark textarea {
171
+ color: var(--text-dark);
172
+ }
173
+ input:focus, textarea:focus {
174
+ outline: none;
175
+ box-shadow: 0 0 0 4px var(--primary);
176
+ }
177
+ .btn {
178
+ padding: 14px 28px;
179
+ background: var(--primary);
180
+ color: white;
181
+ border: none;
182
+ border-radius: 14px;
183
+ cursor: pointer;
184
+ font-size: 1.1em;
185
+ font-weight: 600;
186
+ transition: var(--transition);
187
+ box-shadow: var(--shadow);
188
+ display: inline-block;
189
+ text-decoration: none;
190
+ }
191
+ .btn:hover {
192
+ transform: scale(1.05);
193
+ background: #e6415f;
194
+ }
195
+ .download-btn {
196
+ background: var(--secondary);
197
+ margin-top: 10px;
198
+ }
199
+ .download-btn:hover {
200
+ background: #00b8c5;
201
+ }
202
+ .delete-btn {
203
+ background: var(--delete-color);
204
+ margin-top: 10px;
205
+ }
206
+ .delete-btn:hover {
207
+ background: #cc3333;
208
+ }
209
+ .flash {
210
+ color: var(--secondary);
211
+ text-align: center;
212
+ margin-bottom: 15px;
213
+ }
214
+ .file-grid {
215
+ display: grid;
216
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
217
+ gap: 20px;
218
+ margin-top: 20px;
219
+ }
220
+ .user-list {
221
+ margin-top: 20px;
222
+ }
223
+ .user-item {
224
+ padding: 15px;
225
+ background: var(--card-bg);
226
+ border-radius: 16px;
227
+ margin-bottom: 10px;
228
+ box-shadow: var(--shadow);
229
+ transition: var(--transition);
230
+ }
231
+ body.dark .user-item {
232
+ background: var(--card-bg-dark);
233
+ }
234
+ .user-item:hover {
235
+ transform: translateY(-5px);
236
+ }
237
+ .user-item a {
238
+ color: var(--primary);
239
+ text-decoration: none;
240
+ font-weight: 600;
241
+ }
242
+ .user-item a:hover {
243
+ color: var(--accent);
244
+ }
245
+ @media (max-width: 768px) {
246
+ .file-grid {
247
+ grid-template-columns: repeat(2, 1fr);
248
+ }
249
+ }
250
+ @media (max-width: 480px) {
251
+ .file-grid {
252
+ grid-template-columns: 1fr;
253
+ }
254
+ }
255
+ .file-item {
256
+ background: var(--card-bg);
257
+ padding: 15px;
258
+ border-radius: 16px;
259
+ box-shadow: var(--shadow);
260
+ text-align: center;
261
+ transition: var(--transition);
262
+ }
263
+ body.dark .file-item {
264
+ background: var(--card-bg-dark);
265
+ }
266
+ .file-item:hover {
267
+ transform: translateY(-5px);
268
+ }
269
+ .file-preview {
270
+ max-width: 100%;
271
+ max-height: 200px;
272
+ object-fit: cover;
273
+ border-radius: 10px;
274
+ margin-bottom: 10px;
275
+ loading: lazy;
276
+ }
277
+ .file-item p {
278
+ font-size: 0.9em;
279
+ margin: 5px 0;
280
+ }
281
+ .file-item a {
282
+ color: var(--primary);
283
+ text-decoration: none;
284
+ }
285
+ .file-item a:hover {
286
+ color: var(--accent);
287
+ }
288
+ .modal {
289
+ display: none;
290
+ position: fixed;
291
+ top: 0;
292
+ left: 0;
293
+ width: 100%;
294
+ height: 100%;
295
+ background: rgba(0, 0, 0, 0.85);
296
+ z-index: 2000;
297
+ justify-content: center;
298
+ align-items: center;
299
+ }
300
+ .modal img, .modal video, .modal iframe, .modal pre {
301
+ max-width: 95%;
302
+ max-height: 95%;
303
+ object-fit: contain;
304
+ border-radius: 20px;
305
+ box-shadow: var(--shadow);
306
+ }
307
+ #progress-container {
308
+ width: 100%;
309
+ background: var(--glass-bg);
310
+ border-radius: 10px;
311
+ margin: 15px 0;
312
+ display: none;
313
+ }
314
+ #progress-bar {
315
+ width: 0%;
316
+ height: 20px;
317
+ background: var(--primary);
318
+ border-radius: 10px;
319
+ transition: width 0.3s ease;
320
+ }
321
  '''
322
 
323
  @app.route('/register', methods=['GET', 'POST'])
 
326
  username = request.form.get('username')
327
  password = request.form.get('password')
328
 
 
 
 
 
 
 
 
 
 
 
 
 
 
329
  data = load_data()
330
 
331
  if username in data['users']:
332
  flash('Пользователь с таким именем уже существует!')
333
  return redirect(url_for('register'))
334
 
335
+ if not username or not password:
336
+ flash('Имя пользователя и пароль обязательны!')
337
+ return redirect(url_for('register'))
338
+
339
  data['users'][username] = {
340
+ 'password': password,
341
  'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
342
  'files': []
343
  }
344
+ save_data(data)
345
+ session['username'] = username
346
+ flash('Регистрация прошла успешно!')
347
+ return redirect(url_for('dashboard'))
 
 
 
 
 
348
 
349
  html = '''
350
  <!DOCTYPE html>
 
367
  {% endif %}
368
  {% endwith %}
369
  <form method="POST" id="register-form">
370
+ <input type="text" name="username" placeholder="Введите имя пользователя" required>
371
+ <input type="password" name="password" placeholder="Введите пароль" required>
372
  <button type="submit" class="btn">Зарегистрироваться</button>
373
  </form>
374
  <p style="margin-top: 20px;">Уже есть аккаунт? <a href="{{ url_for('login') }}">Войти</a></p>
 
385
  password = request.form.get('password')
386
  data = load_data()
387
 
388
+ if username in data['users'] and data['users'][username]['password'] == password:
 
389
  session['username'] = username
390
  return jsonify({'status': 'success', 'redirect': url_for('dashboard')})
391
  else:
392
  return jsonify({'status': 'error', 'message': 'Неверное имя пользователя или пароль!'})
393
 
 
 
 
 
394
  html = '''
395
  <!DOCTYPE html>
396
  <html lang="ru">
 
405
  <div class="container">
406
  <h1>Zeus Cloud</h1>
407
  <div id="flash-messages">
408
+ {% with messages = get_flashed_messages() %}
409
  {% if messages %}
410
+ {% for message in messages %}
411
+ <div class="flash">{{ message }}</div>
412
  {% endfor %}
413
  {% endif %}
414
  {% endwith %}
 
421
  <p style="margin-top: 20px;">Нет аккаунта? <a href="{{ url_for('register') }}">Зарегистрируйтесь</a></p>
422
  </div>
423
  <script>
 
424
  const savedCredentials = JSON.parse(localStorage.getItem('zeusCredentials'));
425
  if (savedCredentials) {
426
  fetch('/', {
427
  method: 'POST',
428
+ headers: {
429
+ 'Content-Type': 'application/x-www-form-urlencoded',
430
+ },
431
  body: `username=${encodeURIComponent(savedCredentials.username)}&password=${encodeURIComponent(savedCredentials.password)}`
432
  })
433
  .then(response => response.json())
434
  .then(data => {
435
  if (data.status === 'success') {
436
  window.location.href = data.redirect;
 
 
 
437
  }
438
  })
439
  .catch(error => console.error('Ошибка автовхода:', error));
440
  }
441
 
 
442
  document.getElementById('login-form').addEventListener('submit', function(e) {
443
  e.preventDefault();
444
  const formData = new FormData(this);
 
 
 
 
445
  fetch('/', {
446
  method: 'POST',
447
  body: formData
 
454
  localStorage.setItem('zeusCredentials', JSON.stringify({ username, password }));
455
  window.location.href = data.redirect;
456
  } else {
457
+ document.getElementById('flash-messages').innerHTML = `<div class="flash">${data.message}</div>`;
 
 
458
  }
459
  })
460
  .catch(error => {
461
+ document.getElementById('flash-messages').innerHTML = `<div class="flash">Ошибка соединения!</div>`;
 
 
 
462
  });
463
  });
464
  </script>
 
470
  @app.route('/dashboard', methods=['GET', 'POST'])
471
  def dashboard():
472
  if 'username' not in session:
473
+ flash('Пожалуйста, войдите в систему!')
474
  return redirect(url_for('login'))
475
 
476
  username = session['username']
477
  data = load_data()
478
  if username not in data['users']:
479
  session.pop('username', None)
480
+ flash('Пользователь не найден!')
481
  return redirect(url_for('login'))
482
 
483
+ user_files = sorted(data['users'][username]['files'], key=lambda x: x['upload_date'], reverse=True)
 
 
 
484
 
485
+ if request.method == 'POST':
486
  files = request.files.getlist('files')
487
+ if files and len(files) > 20:
488
+ flash('Максимум 20 файлов за раз!')
 
 
 
 
 
 
 
489
  return redirect(url_for('dashboard'))
490
 
491
+ if files:
492
+ os.makedirs('uploads', exist_ok=True)
493
+ api = HfApi()
494
+ temp_files = []
 
 
 
 
 
 
 
 
 
 
 
495
 
496
+ for file in files:
497
+ if file and file.filename:
498
+ original_filename = secure_filename(file.filename)
499
+ unique_filename = f"{uuid.uuid4()}{os.path.splitext(original_filename)[1]}"
500
+ temp_path = os.path.join('uploads', unique_filename)
501
  file.save(temp_path)
502
+ temp_files.append((temp_path, unique_filename, original_filename))
503
+
504
+ for temp_path, unique_filename, original_filename in temp_files:
505
+ file_path = f"cloud_files/{username}/{unique_filename}"
506
+ api.upload_file(
507
+ path_or_fileobj=temp_path,
508
+ path_in_repo=file_path,
509
+ repo_id=REPO_ID,
510
+ repo_type="dataset",
511
+ token=HF_TOKEN_WRITE,
512
+ commit_message=f"Uploaded file for {username}"
513
+ )
514
+
515
+ file_info = {
516
+ 'filename': original_filename,
517
+ 'unique_filename': unique_filename,
518
+ 'path': file_path,
519
+ 'type': get_file_type(unique_filename),
520
+ 'upload_date': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
521
+ }
522
+ data['users'][username]['files'].append(file_info)
523
 
524
+ if os.path.exists(temp_path):
525
+ os.remove(temp_path)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
526
 
527
+ save_data(data)
528
+ flash('Файлы успешно загружены!')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
529
 
530
+ return redirect(url_for('dashboard'))
531
 
532
  html = '''
533
  <!DOCTYPE html>
 
542
  <body>
543
  <div class="container">
544
  <h1>Панель управления Zeus Cloud</h1>
545
+ <p>Пользователь: {{ username }}</p>
546
+ {% with messages = get_flashed_messages() %}
547
+ {% if messages %}
548
+ {% for message in messages %}
549
+ <div class="flash">{{ message }}</div>
550
+ {% endfor %}
551
+ {% endif %}
552
+ {% endwith %}
 
 
553
  <form id="upload-form" method="POST" enctype="multipart/form-data">
554
+ <input type="file" name="files" multiple required>
555
+ <button type="submit" class="btn" id="upload-btn">Загрузить файлы</button>
556
  </form>
557
  <div id="progress-container">
558
+ <div id="progress-bar"></div>
559
  </div>
560
  <h2 style="margin-top: 30px;">Ваши файлы</h2>
561
+ <p style="text-align: center; margin-top: 10px; color: var(--accent);">Ваши файлы под надежной защитой квантовой криптографии</p>
562
  <div class="file-grid">
563
  {% for file in user_files %}
564
  <div class="file-item">
565
+ {% if file['type'] == 'video' %}
566
+ <video class="file-preview" preload="metadata" muted loading="lazy" onclick="openModal('https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ file['path'] }}', true)">
567
+ <source src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ file['path'] }}" type="video/mp4">
568
+ </video>
569
+ {% elif file['type'] == 'image' %}
570
+ <img class="file-preview" src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ file['path'] }}" alt="{{ file['filename'] }}" loading="lazy" onclick="openModal(this.src, false)">
571
+ {% elif file['type'] == 'pdf' %}
572
+ <iframe class="file-preview" src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ file['path'] }}#toolbar=0" loading="lazy" onclick="openModal('https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ file['path'] }}', 'pdf')"></iframe>
573
+ {% elif file['type'] == 'text' %}
574
+ <p class="file-preview" style="overflow: auto; white-space: pre-wrap;" onclick="openModal('https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ file['path'] }}', 'text')">Text file</p>
575
+ {% else %}
576
+ <p>{{ file['filename'] }}</p>
577
+ {% endif %}
578
+ <p>{{ file['filename'] }}</p>
579
+ <p>{{ file['upload_date'] }}</p>
580
+ <a href="{{ url_for('download_file', file_path=file['path'], filename=file['filename']) }}" class="btn download-btn">Скачать</a>
581
+ <a href="{{ url_for('delete_file', file_path=file['path']) }}" class="btn delete-btn" onclick="return confirm('Вы уверены, что хотите удалить этот файл?');">Удалить</a>
 
 
 
 
 
582
  </div>
583
  {% endfor %}
584
  {% if not user_files %}
585
+ <p>Вы еще не загрузили ни одного файла.</p>
586
  {% endif %}
587
  </div>
588
+ <h2 style="margin-top: 30px;">Добавить на главный экран</h2>
589
+ <p>Для быстрого доступа к Zeus Cloud, вы можете добавить это приложение на главный экран вашего телефона:</p>
590
+ <div style="margin-top: 10px;">
591
+ <h3>Для пользователей Android:</h3>
592
+ <ol>
593
+ <li>Откройте Zeus Cloud в браузере Chrome.</li>
594
+ <li>Нажмите на меню браузера (обычно три точки вверху справа).</li>
595
+ <li>Выберите <strong>"Добавить на главный экран"</strong>.</li>
596
+ <li>Подтвердите добавление, и иконка приложения появится на вашем главном экране.</li>
597
+ </ol>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
598
  </div>
599
+ <div style="margin-top: 10px;">
600
+ <h3>Для пользователей iOS (iPhone/iPad):</h3>
601
+ <ol>
602
+ <li>Откройте Zeus Cloud в браузере Safari.</li>
603
+ <li>Нажмите кнопку <strong>"Поделиться"</strong> (квадрат со стрелкой вверх) в нижней части экрана.</li>
604
+ <li>Прокрутите список опций вниз и выберите <strong>"Добавить на экран «Домой»"</strong>.</li>
605
+ <li>В правом верхнем углу нажмите <strong>"Добавить"</strong>. Иконка приложения появится на вашем главном экране.</li>
606
+ </ol>
607
+ </div>
608
+ <a href="{{ url_for('logout') }}" class="btn" style="margin-top: 20px;" id="logout-btn">Выйти</a>
609
  </div>
610
+ <div class="modal" id="mediaModal" onclick="closeModal(event)">
611
+ <div id="modalContent"></div>
 
 
 
 
612
  </div>
 
613
  <script>
614
+ async function openModal(src, type) {
 
 
615
  const modal = document.getElementById('mediaModal');
616
+ const modalContent = document.getElementById('modalContent');
617
+ if (type === true) {
618
+ modalContent.innerHTML = `<video controls autoplay><source src="${src}" type="video/mp4"></video>`;
619
+ } else if (type === 'pdf') {
620
+ modalContent.innerHTML = `<iframe src="${src}#toolbar=0" style="width: 95%; height: 95vh;"></iframe>`;
621
+ } else if (type === 'text') {
622
+ try {
623
+ const response = await fetch(src);
624
+ const text = await response.text();
625
+ modalContent.innerHTML = `<pre style="white-space: pre-wrap; overflow: auto;">${text}</pre>`;
626
+ } catch (e) {
627
+ modalContent.innerHTML = `<p>Ошибка загрузки текстового файла</p>`;
628
+ }
629
  } else {
630
+ modalContent.innerHTML = `<img src="${src}">`;
631
  }
 
632
  modal.style.display = 'flex';
633
  }
634
+ function closeModal(event) {
 
635
  const modal = document.getElementById('mediaModal');
636
+ if (event.target === modal) {
637
+ modal.style.display = 'none';
638
+ const video = modal.querySelector('video');
639
+ if (video) {
640
+ video.pause();
641
+ }
642
+ modalContent.innerHTML = '';
643
  }
 
 
644
  }
645
 
 
 
 
 
 
 
 
 
646
  const form = document.getElementById('upload-form');
647
  const progressBar = document.getElementById('progress-bar');
648
  const progressContainer = document.getElementById('progress-container');
649
  const uploadBtn = document.getElementById('upload-btn');
 
 
650
 
651
  form.addEventListener('submit', function(e) {
652
  e.preventDefault();
653
+ const files = form.querySelector('input[type="file"]').files;
654
+ if (files.length > 20) {
655
+ alert('Максимум 20 файлов за раз!');
656
+ return;
657
+ }
658
  if (files.length === 0) {
659
+ alert('Пожалуйста, выберите файлы для загрузки.');
660
  return;
661
  }
 
 
 
 
662
 
663
  const formData = new FormData(form);
664
  progressContainer.style.display = 'block';
665
  progressBar.style.width = '0%';
 
666
  uploadBtn.disabled = true;
 
667
 
668
  const xhr = new XMLHttpRequest();
669
+ xhr.open('POST', '/dashboard', true);
 
 
670
 
671
  xhr.upload.onprogress = function(event) {
672
  if (event.lengthComputable) {
673
+ const percent = (event.loaded / event.total) * 100;
674
  progressBar.style.width = percent + '%';
 
675
  }
676
  };
677
 
678
  xhr.onload = function() {
679
  uploadBtn.disabled = false;
 
680
  progressContainer.style.display = 'none';
681
  progressBar.style.width = '0%';
682
+ if (xhr.status === 200) {
683
+ location.reload();
684
+ } else {
685
+ alert('Ошибка загрузки!');
 
 
 
 
 
 
 
 
 
 
 
686
  }
687
  };
688
 
689
  xhr.onerror = function() {
690
+ alert('Ошибка соединения!');
691
+ progressContainer.style.display = 'none';
692
+ progressBar.style.width = '0%';
693
+ uploadBtn.disabled = false;
 
 
694
  };
695
 
696
  xhr.send(formData);
697
  });
698
 
 
699
  document.getElementById('logout-btn').addEventListener('click', function(e) {
700
+ e.preventDefault();
701
  localStorage.removeItem('zeusCredentials');
702
+ window.location.href = '/logout';
703
  });
704
  </script>
705
  </body>
706
  </html>
707
  '''
708
+ return render_template_string(html, username=username, user_files=user_files, repo_id=REPO_ID)
 
 
709
 
710
+ @app.route('/download/<path:file_path>/<filename>')
711
+ def download_file(file_path, filename):
712
  if 'username' not in session:
713
+ flash('Пожалуйста, войдите в систему!')
714
  return redirect(url_for('login'))
715
 
716
  username = session['username']
717
  data = load_data()
718
+ if username not in data['users']:
719
+ session.pop('username', None)
720
+ flash('Пользователь не найден!')
721
+ return redirect(url_for('login'))
 
 
 
 
 
 
 
 
 
 
 
 
 
722
 
723
+ user_files = data['users'][username]['files']
724
+ if not any(file['path'] == file_path for file in user_files):
725
+ is_admin_route = request.referrer and 'admhosto' in request.referrer
726
+ if not is_admin_route:
727
+ flash('У вас нет доступа к этому файлу!')
728
+ return redirect(url_for('dashboard'))
729
 
730
+ file_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{file_path}?download=true"
 
731
  try:
 
 
 
 
 
732
  api = HfApi()
733
  headers = {}
734
  if HF_TOKEN_READ:
735
  headers["authorization"] = f"Bearer {HF_TOKEN_READ}"
736
 
 
737
  response = requests.get(file_url, headers=headers, stream=True)
738
+ response.raise_for_status()
739
 
740
+ file_content = BytesIO(response.content)
741
  return send_file(
742
+ file_content,
743
  as_attachment=True,
744
+ download_name=filename,
745
+ mimetype='application/octet-stream'
746
  )
747
  except requests.exceptions.RequestException as e:
748
+ logging.error(f"Error downloading file from HF: {e}")
749
+ flash('Ошибка скачивания файла!')
750
+ if request.referrer and 'admhosto' in request.referrer:
751
+ return redirect(url_for('admin_user_files', username=file_path.split('/')[1]))
752
+ else:
753
+ return redirect(url_for('dashboard'))
754
  except Exception as e:
755
+ logging.error(f"Unexpected error during download: {e}")
756
+ flash('Произошла непредвиденная ошибка при скачивании файла.')
757
+ if request.referrer and 'admhosto' in request.referrer:
758
+ return redirect(url_for('admin_user_files', username=file_path.split('/')[1]))
759
+ else:
760
+ return redirect(url_for('dashboard'))
 
 
 
 
 
 
 
761
 
762
+ @app.route('/delete/<path:file_path>')
763
+ def delete_file(file_path):
764
  if 'username' not in session:
765
+ flash('Пожалуйста, войдите в систему!')
766
  return redirect(url_for('login'))
 
 
 
 
767
 
768
  username = session['username']
769
  data = load_data()
770
+ if username not in data['users']:
771
  session.pop('username', None)
772
+ flash('Пользователь не найден!')
773
  return redirect(url_for('login'))
774
 
775
  user_files = data['users'][username]['files']
776
+ file_to_delete = next((file for file in user_files if file['path'] == file_path), None)
777
+
778
+ if not file_to_delete:
779
+ flash('Файл не найден!')
 
 
 
 
 
 
 
780
  return redirect(url_for('dashboard'))
781
 
782
  try:
783
  api = HfApi()
784
  api.delete_file(
785
+ path_in_repo=file_path,
786
  repo_id=REPO_ID,
787
  repo_type="dataset",
788
  token=HF_TOKEN_WRITE,
789
+ commit_message=f"Deleted file {file_path} for {username}"
790
  )
791
+ data['users'][username]['files'] = [f for f in user_files if f['path'] != file_path]
 
792
  save_data(data)
793
+ flash('Файл успешно удален!')
 
 
794
  except Exception as e:
795
+ logging.error(f"Error deleting file: {e}")
796
+ flash('Ошибка удаления файла!')
 
 
 
 
 
 
 
 
 
 
 
 
797
 
798
  return redirect(url_for('dashboard'))
799
 
800
  @app.route('/logout')
801
  def logout():
802
  session.pop('username', None)
 
 
803
  return redirect(url_for('login'))
804
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
805
  @app.route('/admhosto')
806
  def admin_panel():
 
 
 
807
  data = load_data()
808
+ users = data['users']
809
 
810
  html = '''
811
  <!DOCTYPE html>
 
820
  <body>
821
  <div class="container">
822
  <h1>Админ-панель Zeus Cloud</h1>
823
+ <h2>Список пользователей</h2>
 
 
 
 
 
 
 
 
824
  <div class="user-list">
825
+ {% for username, user_data in users.items() %}
826
  <div class="user-item">
827
+ <a href="{{ url_for('admin_user_files', username=username) }}">{{ username }}</a>
828
+ <p>Дата регистрации: {{ user_data.get('created_at', 'N/A') }}</p>
829
+ <p>Количество файлов: {{ user_data.get('files', []) | length }}</p>
830
+ <form method="POST" action="{{ url_for('admin_delete_user', username=username) }}" style="display: inline; margin-left: 10px;" onsubmit="return confirm('Вы уверены, что хотите удалить пользователя {{ username }} и все его файлы? Это действие необратимо!');">
831
+ <button type="submit" class="btn delete-btn" style="padding: 5px 10px; font-size: 0.9em;">Удалить</button>
832
+ </form>
 
 
833
  </div>
834
  {% endfor %}
835
  {% if not users %}
836
  <p>Пользователей пока нет.</p>
837
  {% endif %}
838
  </div>
839
+ {% with messages = get_flashed_messages() %}
840
+ {% if messages %}
841
+ <div style="margin-top: 20px;">
842
+ {% for message in messages %}
843
+ <div class="flash">{{ message }}</div>
844
+ {% endfor %}
845
+ </div>
846
+ {% endif %}
847
+ {% endwith %}
848
  </div>
849
  </body>
850
  </html>
 
853
 
854
  @app.route('/admhosto/user/<username>')
855
  def admin_user_files(username):
 
 
 
856
  data = load_data()
857
+ if username not in data['users']:
858
+ flash('Пользователь не найден!')
859
  return redirect(url_for('admin_panel'))
860
 
861
  user_files = sorted(data['users'][username].get('files', []), key=lambda x: x.get('upload_date', ''), reverse=True)
862
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
863
  html = '''
864
  <!DOCTYPE html>
865
  <html lang="ru">
 
872
  </head>
873
  <body>
874
  <div class="container">
 
 
875
  <h1>Файлы пользователя: {{ username }}</h1>
876
+ {% with messages = get_flashed_messages() %}
877
  {% if messages %}
878
+ {% for message in messages %}
879
+ <div class="flash">{{ message }}</div>
880
  {% endfor %}
881
  {% endif %}
882
  {% endwith %}
883
  <div class="file-grid">
884
  {% for file in user_files %}
885
  <div class="file-item">
886
+ {% if file.get('type') == 'video' %}
887
+ <video class="file-preview" preload="metadata" muted loading="lazy" onclick="openModal('https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ file['path'] }}?download=true', true)">
888
+ <source src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ file['path'] }}?download=true" type="video/mp4">
889
+ </video>
890
+ {% elif file.get('type') == 'image' %}
891
+ <img class="file-preview" src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ file['path'] }}?download=true" alt="{{ file['filename'] }}" loading="lazy" onclick="openModal(this.src, false)">
892
+ {% elif file.get('type') == 'pdf' %}
893
+ <iframe class="file-preview" src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ file['path'] }}?download=true#toolbar=0" loading="lazy" onclick="openModal('https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ file['path'] }}?download=true', 'pdf')"></iframe>
894
+ {% elif file.get('type') == 'text' %}
895
+ <p class="file-preview" style="overflow: auto; white-space: pre-wrap;" onclick="openModal('https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ file['path'] }}?download=true', 'text')">Text file</p>
896
+ {% else %}
897
+ <p>{{ file.get('filename', 'N/A') }}</p>
898
+ {% endif %}
899
+ <p>{{ file.get('filename', 'N/A') }}</p>
900
+ <p>{{ file.get('upload_date', 'N/A') }}</p>
901
+ <a href="{{ url_for('download_file', file_path=file['path'], filename=file['filename']) }}" class="btn download-btn">Скачать</a>
902
+ <form method="POST" action="{{ url_for('admin_delete_file', username=username, file_path=file['path']) }}" style="display: inline; margin-left: 5px;" onsubmit="return confirm('Вы уверены, что хотите удалить этот файл?');">
903
+ <button type="submit" class="btn delete-btn" style="padding: 5px 10px; font-size: 0.9em;">Удалить</button>
904
+ </form>
 
 
 
 
905
  </div>
906
  {% endfor %}
907
  {% if not user_files %}
908
+ <p>У этого пользователя пока нет файлов.</p>
909
  {% endif %}
910
  </div>
911
+ <a href="{{ url_for('admin_panel') }}" class="btn" style="margin-top: 20px;">Назад к списку пользователей</a>
912
  </div>
913
+ <div class="modal" id="mediaModal" onclick="closeModal(event)">
914
+ <div id="modalContent"></div>
 
 
 
915
  </div>
916
  <script>
917
+ async function openModal(src, type) {
 
918
  const modal = document.getElementById('mediaModal');
919
+ const modalContent = document.getElementById('modalContent');
920
+ const tokenParam = HF_TOKEN_READ ? `?token=${HF_TOKEN_READ}` : "";
921
+ let finalSrc = src;
922
+ if (!finalSrc.includes('?download=true')) {
923
+ finalSrc = finalSrc.includes('?') ? finalSrc.replace('?', '?download=true&') : finalSrc + '?download=true';
924
+ }
925
+ finalSrc += tokenParam;
926
+ if (type === true) {
927
+ modalContent.innerHTML = `<video controls autoplay style='max-width: 95%; max-height: 95vh;'><source src="${finalSrc}" type="video/mp4"></video>`;
928
+ } else if (type === 'pdf') {
929
+ modalContent.innerHTML = `<iframe src="${finalSrc}#toolbar=0" style="width: 95%; height: 95vh;"></iframe>`;
930
+ } else if (type === 'text') {
931
+ try {
932
+ const response = await fetch(finalSrc);
933
+ const text = await response.text();
934
+ modalContent.innerHTML = `<pre style="white-space: pre-wrap; overflow: auto;">${text}</pre>`;
935
+ } catch (e) {
936
+ modalContent.innerHTML = `<p>Ошибка загрузки текстового файла</p>`;
937
+ }
938
  } else {
939
+ modalContent.innerHTML = `<img src="${finalSrc}" style='max-width: 95%; max-height: 95vh;'>`;
940
  }
 
941
  modal.style.display = 'flex';
942
  }
943
+ function closeModal(event) {
944
  const modal = document.getElementById('mediaModal');
945
+ if (event.target === modal) {
946
+ modal.style.display = 'none';
947
+ const video = modal.querySelector('video');
948
+ if (video) {
949
+ video.pause();
950
+ }
951
+ document.getElementById('modalContent').innerHTML = '';
952
+ }
953
  }
954
+ const HF_TOKEN_READ = "{{ os.getenv('HF_TOKEN_READ') or os.getenv('HF_TOKEN') }}";
 
 
955
  </script>
956
  </body>
957
  </html>
958
  '''
959
+ return render_template_string(html, username=username, user_files=user_files, repo_id=REPO_ID, os=os)
 
960
 
961
  @app.route('/admhosto/delete_user/<username>', methods=['POST'])
962
  def admin_delete_user(username):
 
 
 
 
 
 
 
963
  data = load_data()
964
+ if username not in data['users']:
965
+ flash('Пользователь не найден!')
966
  return redirect(url_for('admin_panel'))
967
 
968
  user_files_to_delete = data['users'][username].get('files', [])
 
 
969
 
970
  try:
971
  api = HfApi()
972
+ paths_to_delete = [file['path'] for file in user_files_to_delete]
973
+ if paths_to_delete:
974
+ try:
975
+ api.delete_folder(
976
+ folder_path=f"cloud_files/{username}",
977
+ repo_id=REPO_ID,
978
+ repo_type="dataset",
979
+ token=HF_TOKEN_WRITE,
980
+ commit_message=f"Deleted all files for user {username} by admin"
981
+ )
982
+ logging.info(f"Successfully deleted folder for user {username}")
983
+ except Exception as folder_delete_error:
984
+ logging.warning(f"Could not delete folder for {username}, attempting individual file deletion: {folder_delete_error}")
985
+ for file_path in paths_to_delete:
986
+ try:
987
+ api.delete_file(
988
+ path_in_repo=file_path,
989
+ repo_id=REPO_ID,
990
+ repo_type="dataset",
991
+ token=HF_TOKEN_WRITE
992
+ )
993
+ except Exception as file_delete_error:
994
+ logging.error(f"Error deleting file {file_path} for user {username}: {file_delete_error}")
995
+
 
 
 
 
 
 
 
 
996
  del data['users'][username]
997
  save_data(data)
998
+ flash(f'Пользователь {username} и его файлы успешно удалены!')
999
+ logging.info(f"Admin deleted user {username} and their files.")
1000
 
1001
  except Exception as e:
1002
+ logging.error(f"Error deleting user {username}: {e}")
1003
+ flash(f'Ошибка при удалении пользователя {username}!')
1004
 
1005
  return redirect(url_for('admin_panel'))
1006
 
1007
+ @app.route('/admhosto/delete_file/<username>/<path:file_path>', methods=['POST'])
1008
+ def admin_delete_file(username, file_path):
1009
+ data = load_data()
1010
+ if username not in data['users']:
1011
+ flash('Пользователь не найден!')
1012
+ return redirect(url_for('admin_panel'))
1013
 
1014
+ user_files = data['users'][username].get('files', [])
1015
+ file_exists_in_db = any(f['path'] == file_path for f in user_files)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1016
 
1017
+ if not file_exists_in_db:
1018
+ flash('Файл не найден в базе данных пользователя!')
 
 
 
 
 
 
 
1019
 
1020
+ try:
1021
+ api = HfApi()
1022
+ api.delete_file(
1023
+ path_in_repo=file_path,
1024
+ repo_id=REPO_ID,
1025
+ repo_type="dataset",
1026
+ token=HF_TOKEN_WRITE,
1027
+ commit_message=f"Admin deleted file {file_path} for user {username}"
1028
+ )
1029
 
1030
+ data['users'][username]['files'] = [f for f in user_files if f['path'] != file_path]
1031
+ save_data(data)
1032
+ flash('Файл успешно удален!')
1033
+ logging.info(f"Admin deleted file {file_path} for user {username}")
 
 
 
 
 
 
 
 
 
 
1034
 
1035
+ except Exception as e:
1036
+ logging.error(f"Error deleting file {file_path} by admin: {e}")
1037
+ flash('Ошибка удаления файла!')
1038
 
1039
+ return redirect(url_for('admin_user_files', username=username))
1040
 
1041
  if __name__ == '__main__':
1042
  if not HF_TOKEN_WRITE:
1043
  logging.warning("HF_TOKEN (write access) is not set. File uploads and deletions will fail.")
1044
  if not HF_TOKEN_READ:
1045
+ logging.warning("HF_TOKEN_READ is not set. Falling back to HF_TOKEN. File downloads might fail for private repos if HF_TOKEN is not set.")
 
 
 
1046
 
 
 
 
 
 
 
 
1047
  if HF_TOKEN_WRITE:
1048
+ threading.Thread(target=periodic_backup, daemon=True).start()
 
 
1049
  else:
1050
  logging.warning("Periodic backup disabled because HF_TOKEN (write access) is not set.")
1051
 
1052
+ app.run(debug=False, host='0.0.0.0', port=7860)