Eluza133 commited on
Commit
32b1f85
·
verified ·
1 Parent(s): f3a5554

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +257 -186
app.py CHANGED
@@ -1,5 +1,4 @@
1
  # --- START OF FILE app (9).py ---
2
-
3
  from flask import Flask, render_template_string, request, redirect, url_for, session, flash, send_file, jsonify, Response
4
  from flask_caching import Cache
5
  import json
@@ -26,6 +25,7 @@ os.makedirs(UPLOAD_FOLDER, exist_ok=True)
26
  cache = Cache(app, config={'CACHE_TYPE': 'simple'})
27
  logging.basicConfig(level=logging.INFO)
28
 
 
29
  def find_node_by_id(filesystem, node_id):
30
  if filesystem.get('id') == node_id:
31
  return filesystem, None
@@ -214,13 +214,14 @@ BASE_STYLE = '''
214
  * { margin: 0; padding: 0; box-sizing: border-box; }
215
  body { font-family: 'Inter', sans-serif; background: var(--background-light); color: var(--text-light); line-height: 1.6; }
216
  body.dark { background: var(--background-dark); color: var(--text-dark); }
217
- .container { margin: 20px auto; max-width: 1200px; padding: 25px; background: var(--card-bg); border-radius: 20px; box-shadow: var(--shadow); }
218
  body.dark .container { background: var(--card-bg-dark); }
219
  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; }
220
  h2 { font-size: 1.5em; margin-top: 30px; color: var(--text-light); }
221
- h3 { font-size: 1.2em; margin-top: 15px; color: var(--primary); }
222
- body.dark h2, body.dark h3 { color: var(--text-dark); }
223
- body.dark h3 { color: var(--secondary); }
 
224
  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); }
225
  body.dark input, body.dark textarea { color: var(--text-dark); }
226
  input:focus, textarea:focus { outline: none; box-shadow: 0 0 0 4px var(--primary); }
@@ -232,7 +233,8 @@ input:focus, textarea:focus { outline: none; box-shadow: 0 0 0 4px var(--primary
232
  .delete-btn:hover { background: #cc3333; }
233
  .folder-btn { background: var(--folder-color); }
234
  .folder-btn:hover { background: #e6a000; }
235
- .flash { color: var(--secondary); text-align: center; margin-bottom: 15px; }
 
236
  .file-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 20px; margin-top: 20px; }
237
  .user-list { margin-top: 20px; }
238
  .user-item { padding: 15px; background: var(--card-bg); border-radius: 16px; margin-bottom: 10px; box-shadow: var(--shadow); transition: var(--transition); }
@@ -243,20 +245,22 @@ body.dark .user-item { background: var(--card-bg-dark); }
243
  .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; }
244
  body.dark .item { background: var(--card-bg-dark); }
245
  .item:hover { transform: translateY(-5px); }
246
- .item-preview { max-width: 100%; height: 130px; object-fit: cover; border-radius: 10px; margin-bottom: 10px; cursor: pointer; display: block; }
247
  .item.folder .item-preview { object-fit: contain; font-size: 60px; color: var(--folder-color); line-height: 130px; }
248
  .item p { font-size: 0.9em; margin: 5px 0; word-break: break-all; }
249
  .item a { color: var(--primary); text-decoration: none; }
250
  .item a:hover { color: var(--accent); }
251
- .item-actions { margin-top: 10px; display: flex; flex-wrap: wrap; justify-content: center; gap: 5px;}
252
- .item-actions .btn { font-size: 0.9em !important; padding: 5px 10px !important; }
253
  .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; }
254
- .modal-content { max-width: 95%; max-height: 95%; background: #fff; padding: 10px; border-radius: 15px; overflow: auto; }
255
  body.dark .modal-content { background: var(--card-bg-dark); }
256
  .modal img, .modal video, .modal iframe, .modal pre { max-width: 100%; max-height: 85vh; display: block; margin: auto; border-radius: 10px; }
257
  .modal iframe { width: 80vw; height: 85vh; border: none; }
258
  .modal pre { background: #eee; color: #333; padding: 15px; border-radius: 8px; white-space: pre-wrap; word-wrap: break-word; text-align: left; max-height: 85vh; overflow-y: auto;}
259
  body.dark .modal pre { background: #2b2a33; color: var(--text-dark); }
 
 
260
  #progress-container { width: 100%; background: var(--glass-bg); border-radius: 10px; margin: 15px 0; display: none; }
261
  #progress-bar { width: 0%; height: 20px; background: var(--primary); border-radius: 10px; transition: width 0.3s ease; }
262
  .breadcrumbs { margin-bottom: 20px; font-size: 1.1em; }
@@ -264,47 +268,30 @@ body.dark .modal pre { background: #2b2a33; color: var(--text-dark); }
264
  .breadcrumbs a:hover { text-decoration: underline; }
265
  .breadcrumbs span { margin: 0 5px; color: #aaa; }
266
  .folder-actions { margin-top: 20px; margin-bottom: 10px; display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
267
- .folder-actions input[type=text] { width: auto; flex-grow: 1; margin: 0; }
268
- .folder-actions .btn { margin: 0; }
269
- .instruction-box { background: var(--glass-bg); padding: 15px; border-radius: 15px; margin-top: 20px; border: 1px solid var(--secondary); }
270
- body.dark .instruction-box { border-color: var(--primary); }
271
- .instruction-box ul { list-style: inside; padding-left: 10px; margin-top: 10px; }
272
- .instruction-box li { margin-bottom: 8px; }
273
- .admin-file-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 20px; margin-top: 20px;}
274
- .file-item { background: var(--card-bg); padding: 15px; border-radius: 16px; box-shadow: var(--shadow); transition: var(--transition); display: flex; flex-direction: column; justify-content: space-between; }
275
- body.dark .file-item { background: var(--card-bg-dark); }
276
- .file-item:hover { transform: translateY(-5px); }
277
- .file-preview { max-width: 100%; max-height: 100px; object-fit: contain; border-radius: 10px; margin-bottom: 10px; display: block; margin-left: auto; margin-right: auto; }
278
- .file-item-actions { margin-top: 10px; display: flex; flex-wrap: wrap; justify-content: center; gap: 5px;}
279
- .file-item-actions .btn { font-size: 0.8em !important; padding: 4px 8px !important; }
280
  @media (max-width: 768px) {
281
  .file-grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
282
- .admin-file-grid { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); }
283
- h1 { font-size: 1.8em; }
284
- .btn { padding: 12px 24px; font-size: 1em; }
285
  .folder-actions { flex-direction: column; align-items: stretch; }
286
  .folder-actions input[type=text] { width: 100%; }
287
- .item-actions { flex-direction: column; align-items: stretch; }
288
- .item-actions .btn { width: 100%; margin-bottom: 5px; }
289
- .file-item-actions { flex-direction: column; align-items: stretch; }
290
- .file-item-actions .btn { width: 100%; margin-bottom: 5px; }
291
- .user-item form { display: block !important; margin-left: 0 !important; margin-top: 10px; }
292
- .user-item .btn { width: 100%; }
293
  }
294
  @media (max-width: 480px) {
295
  .container { padding: 15px; }
296
- .file-grid { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 10px; }
297
- .admin-file-grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 10px; }
298
- .item-preview { height: 100px; }
299
- .item.folder .item-preview { font-size: 40px; line-height: 100px; }
300
- .file-preview { max-height: 80px; }
301
- .btn { padding: 10px 20px; font-size: 0.9em; }
302
- h1 { font-size: 1.5em; }
303
- h2 { font-size: 1.3em; }
304
- input, textarea { padding: 12px; font-size: 1em; }
305
  }
306
  '''
307
 
 
308
  @app.route('/register', methods=['GET', 'POST'])
309
  def register():
310
  if request.method == 'POST':
@@ -337,7 +324,7 @@ def register():
337
  flash('Регистрация прошла успешно!')
338
  return redirect(url_for('dashboard'))
339
  except Exception as e:
340
- flash('Ошибка сохранения данных при регистрации.')
341
  logging.error(f"Registration save error: {e}")
342
  return redirect(url_for('register'))
343
 
@@ -345,7 +332,7 @@ def register():
345
  <!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
346
  <title>Регистрация - Zeus Cloud</title><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
347
  <style>''' + BASE_STYLE + '''</style></head><body><div class="container"><h1>Регистрация в Zeus Cloud</h1>
348
- {% with messages = get_flashed_messages() %}{% if messages %}{% for message in messages %}<div class="flash">{{ message }}</div>{% endfor %}{% endif %}{% endwith %}
349
  <form method="POST"><input type="text" name="username" placeholder="Имя (только буквы/цифры, мин 3)" required pattern="[a-zA-Z0-9]{3,}">
350
  <input type="password" name="password" placeholder="Пароль" required><button type="submit" class="btn">Зарегистрироваться</button></form>
351
  <p style="margin-top: 20px;">Уже есть аккаунт? <a href="{{ url_for('login') }}">Войти</a></p></div></body></html>'''
@@ -375,7 +362,7 @@ def login():
375
  <!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
376
  <title>Zeus Cloud</title><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
377
  <style>''' + BASE_STYLE + '''</style></head><body><div class="container"><h1>Zeus Cloud</h1><div id="flash-messages">
378
- {% with messages = get_flashed_messages() %}{% if messages %}{% for message in messages %}<div class="flash">{{ message }}</div>{% endfor %}{% endif %}{% endwith %}</div>
379
  <form method="POST" id="login-form"><input type="text" name="username" placeholder="Имя пользователя" required>
380
  <input type="password" name="password" placeholder="Пароль" required><button type="submit" class="btn">Войти</button></form>
381
  <p style="margin-top: 20px;">Нет аккаунта? <a href="{{ url_for('register') }}">Зарегистрируйтесь</a></p></div>
@@ -393,8 +380,8 @@ def login():
393
  if (data.status === 'success') {
394
  localStorage.setItem('zeusCredentials', JSON.stringify({ username: formData.get('username'), password: formData.get('password') }));
395
  window.location.href = data.redirect;
396
- } else { document.getElementById('flash-messages').innerHTML = `<div class="flash">${data.message}</div>`; }
397
- }).catch(error => { document.getElementById('flash-messages').innerHTML = `<div class="flash">Ошибка соединения!</div>`; });
398
  });
399
  </script></body></html>'''
400
  return render_template_string(html)
@@ -420,36 +407,38 @@ def dashboard():
420
  current_folder, parent_folder = find_node_by_id(user_data['filesystem'], current_folder_id)
421
 
422
  if not current_folder or current_folder.get('type') != 'folder':
423
- flash('Папка не найдена!')
424
  current_folder_id = 'root'
425
  current_folder, parent_folder = find_node_by_id(user_data['filesystem'], current_folder_id)
426
  if not current_folder:
427
  logging.error(f"CRITICAL: Root folder not found for user {username}")
428
- flash('Критическая ошибка: корневая папка не найдена.')
429
  session.pop('username', None)
430
  return redirect(url_for('login'))
431
 
 
432
  items_in_folder = sorted(current_folder.get('children', []), key=lambda x: (x['type'] != 'folder', x.get('name', x.get('original_filename', '')).lower()))
433
 
 
434
  if request.method == 'POST':
435
  if not HF_TOKEN_WRITE:
436
- flash('Загрузка невозможна: токен для записи не настроен.')
437
  return redirect(url_for('dashboard', folder_id=current_folder_id))
438
 
439
  files = request.files.getlist('files')
440
  if not files or all(not f.filename for f in files):
441
- flash('Файлы для загрузки не выбраны.')
442
  return redirect(url_for('dashboard', folder_id=current_folder_id))
443
 
444
  if len(files) > 20:
445
- flash('Максимум 20 файлов за раз!')
446
  return redirect(url_for('dashboard', folder_id=current_folder_id))
447
 
448
  target_folder_id = request.form.get('current_folder_id', 'root')
449
  target_folder_node, _ = find_node_by_id(user_data['filesystem'], target_folder_id)
450
 
451
  if not target_folder_node or target_folder_node.get('type') != 'folder':
452
- flash('Целевая папка для загрузки не найдена!')
453
  return redirect(url_for('dashboard'))
454
 
455
  api = HfApi()
@@ -499,6 +488,7 @@ def dashboard():
499
  except Exception as del_err:
500
  logging.error(f"Failed to delete orphaned file {hf_path} from HF Hub: {del_err}")
501
 
 
502
  except Exception as e:
503
  logging.error(f"Error uploading file {original_filename} for {username}: {e}")
504
  errors.append(f"Ошибка загрузки файла {original_filename}: {e}")
@@ -511,7 +501,7 @@ def dashboard():
511
  save_data(data)
512
  flash(f'{uploaded_count} файл(ов) успешно загружено!')
513
  except Exception as e:
514
- flash('Файлы загружены на сервер, но произошла ошибка сохранения метаданных.')
515
  logging.error(f"Error saving data after upload for {username}: {e}")
516
 
517
  if errors:
@@ -520,6 +510,7 @@ def dashboard():
520
 
521
  return redirect(url_for('dashboard', folder_id=target_folder_id))
522
 
 
523
  breadcrumbs = []
524
  temp_id = current_folder_id
525
  while temp_id:
@@ -531,6 +522,7 @@ def dashboard():
531
  temp_id = parent.get('id')
532
  breadcrumbs.reverse()
533
 
 
534
  html = '''
535
  <!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
536
  <title>Панель управления - Zeus Cloud</title><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
@@ -621,31 +613,29 @@ def dashboard():
621
  {% if not items %} <p>Эта папка пуста.</p> {% endif %}
622
  </div>
623
 
624
- <div class="instruction-box">
625
- <h2>Добавить на главный экран</h2>
626
- <p>Вы можете добавить это приложение на главный экран вашего телефона для быстрого доступа, как обычное приложение.</p>
627
- <h3>Android (Chrome):</h3>
628
- <ul>
629
- <li>Откройте это приложение ({{ request.host_url.rstrip('/') }}) в браузере Chrome.</li>
630
- <li>Нажмите на кнопку меню (⋮) в правом верхнем углу.</li>
631
- <li>Выберите пункт <b>"Установить приложение"</b> или <b>"Добавить на главный экран"</b>.</li>
632
- <li>Следуйте инструкциям на экране для подтверждения.</li>
633
- </ul>
634
- <h3>iOS (Safari):</h3>
635
- <ul>
636
- <li>Откройте это приложение ({{ request.host_url.rstrip('/') }}) в браузере Safari.</li>
637
- <li>Нажмите на кнопку "Поделиться" (квадрат со стрелкой вверх) в нижней части экрана.</li>
638
- <li>Прокрутите вниз и выберите пункт <b>"На экран 'Домой'"</b>.</li>
639
- <li>При необходимости отредактируйте название и нажмите "Добавить".</li>
640
- </ul>
641
- </div>
642
 
643
  <a href="{{ url_for('logout') }}" class="btn" style="margin-top: 20px;" id="logout-btn">Выйти</a>
644
  </div>
645
 
646
  <div class="modal" id="mediaModal" onclick="closeModal(event)">
647
  <div class="modal-content" id="modalContentContainer">
648
- <span onclick="closeModalManual()" style="position: absolute; top: 15px; right: 25px; font-size: 30px; color: #fff; cursor: pointer;">×</span>
649
  <div id="modalContent"></div>
650
  </div>
651
  </div>
@@ -727,6 +717,7 @@ def dashboard():
727
  progressBar.style.width = '50%';
728
  uploadBtn.disabled = true;
729
  uploadBtn.textContent = 'Загрузка...';
 
730
  });
731
 
732
  document.getElementById('logout-btn').addEventListener('click', function(e) {
@@ -737,7 +728,6 @@ def dashboard():
737
 
738
  </script>
739
  </body></html>'''
740
-
741
  template_context = {
742
  'username': username,
743
  'items': items_in_folder,
@@ -747,8 +737,7 @@ def dashboard():
747
  'repo_id': REPO_ID,
748
  'HF_TOKEN_READ': HF_TOKEN_READ,
749
  'hf_file_url': lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{path}{'?download=true' if download else ''}",
750
- 'os': os,
751
- 'request': request
752
  }
753
  return render_template_string(html, **template_context)
754
 
@@ -768,10 +757,10 @@ def create_folder():
768
  folder_name = request.form.get('folder_name', '').strip()
769
 
770
  if not folder_name:
771
- flash('Имя папки не может быть пустым!')
772
  return redirect(url_for('dashboard', folder_id=parent_folder_id))
773
  if not folder_name.isalnum() and '_' not in folder_name and ' ' not in folder_name:
774
- flash('Имя папки может содержать буквы, цифры, пробелы и подчеркивания.')
775
  return redirect(url_for('dashboard', folder_id=parent_folder_id))
776
 
777
  folder_id = uuid.uuid4().hex
@@ -787,10 +776,10 @@ def create_folder():
787
  save_data(data)
788
  flash(f'Папка "{folder_name}" успешно создана.')
789
  except Exception as e:
790
- flash('Ошибка сохранения данных при создании папки.')
791
  logging.error(f"Create folder save error: {e}")
792
  else:
793
- flash('Не удалось найти родительскую папку.')
794
 
795
  return redirect(url_for('dashboard', folder_id=parent_folder_id))
796
 
@@ -798,59 +787,46 @@ def create_folder():
798
  @app.route('/download/<file_id>')
799
  def download_file(file_id):
800
  if 'username' not in session:
801
- flash('Пожалуйста, войдите в систему!')
802
- return redirect(url_for('login'))
 
 
 
 
 
 
803
 
804
- username = session['username']
805
  data = load_data()
806
- user_data = data['users'].get(username)
807
-
808
- is_admin_route = request.referrer and 'admhosto' in request.referrer
809
  file_node = None
810
- target_username = username # Default to current user
811
-
812
- if is_admin() and is_admin_route:
813
- # Admin trying to download, need to find file across all users
814
- # But the URL only contains file_id. We need the user context.
815
- # Let's assume the referrer URL contains the username for admin actions
816
- try:
817
- # Extract username from referrer (e.g., /admhosto/user/someuser)
818
- parts = request.referrer.split('/')
819
- if len(parts) > 2 and parts[-2] == 'user':
820
- target_username = parts[-1]
821
- admin_user_data = data.get('users', {}).get(target_username)
822
- if admin_user_data:
823
- file_node, _ = find_node_by_id(admin_user_data['filesystem'], file_id)
824
- else:
825
- flash(f'(Admin) Пользователь {target_username} не найден.')
826
- return redirect(request.referrer or url_for('admin_panel'))
827
- else: # Fallback if referrer doesn't match expected pattern
828
- flash('(Admin) Не удалось определить пользователя для скачивания файла.')
829
- return redirect(request.referrer or url_for('admin_panel'))
830
-
831
- except Exception as e:
832
- logging.error(f"Admin download error extracting username from referrer {request.referrer}: {e}")
833
- flash('(Admin) Ошибка при обработке запроса на скачивание.')
834
- return redirect(request.referrer or url_for('admin_panel'))
835
- else: # Regular user download
836
- if not user_data:
837
- flash('Пользователь не найден!')
838
- session.pop('username', None)
839
- return redirect(url_for('login'))
840
- file_node, _ = find_node_by_id(user_data['filesystem'], file_id)
841
-
842
 
843
  if not file_node or file_node.get('type') != 'file':
844
- flash('Файл не найден!')
845
- return redirect(request.referrer or url_for('dashboard'))
846
-
847
 
848
  hf_path = file_node.get('path')
849
  original_filename = file_node.get('original_filename', 'downloaded_file')
850
 
851
  if not hf_path:
852
- flash('Ошибка: Путь к файлу не найден в метаданных.')
853
- return redirect(request.referrer or url_for('dashboard'))
854
 
855
  file_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{hf_path}?download=true"
856
 
@@ -871,12 +847,12 @@ def download_file(file_id):
871
  )
872
  except requests.exceptions.RequestException as e:
873
  logging.error(f"Error downloading file from HF ({hf_path}): {e}")
874
- flash(f'Ошибка скачивания файла {original_filename}! ({e})')
875
- return redirect(request.referrer or url_for('dashboard'))
876
  except Exception as e:
877
  logging.error(f"Unexpected error during download ({hf_path}): {e}")
878
- flash('Произошла непредвиденная ошибка при скачивании файла.')
879
- return redirect(request.referrer or url_for('dashboard'))
880
 
881
 
882
  @app.route('/delete_file/<file_id>', methods=['POST'])
@@ -889,7 +865,7 @@ def delete_file(file_id):
889
  data = load_data()
890
  user_data = data['users'].get(username)
891
  if not user_data:
892
- flash('Пользователь не найден!')
893
  session.pop('username', None)
894
  return redirect(url_for('login'))
895
 
@@ -897,26 +873,26 @@ def delete_file(file_id):
897
  current_view_folder_id = request.form.get('current_view_folder_id', 'root')
898
 
899
  if not file_node or file_node.get('type') != 'file' or not parent_node:
900
- flash('Файл не найден или не может быть удален.')
901
  return redirect(url_for('dashboard', folder_id=current_view_folder_id))
902
 
903
  hf_path = file_node.get('path')
904
  original_filename = file_node.get('original_filename', 'файл')
905
 
906
  if not hf_path:
907
- flash(f'Ошибка: Путь к файлу {original_filename} не найден. Удаление только из базы.')
908
  if remove_node(user_data['filesystem'], file_id):
909
  try:
910
  save_data(data)
911
  flash(f'Метаданные файла {original_filename} удалены.')
912
  except Exception as e:
913
- flash('Ошибка сохранения данных после удаления метаданных.')
914
  logging.error(f"Delete file metadata save error: {e}")
915
  return redirect(url_for('dashboard', folder_id=current_view_folder_id))
916
 
917
 
918
  if not HF_TOKEN_WRITE:
919
- flash('Удаление невозможно: токен для записи не настроен.')
920
  return redirect(url_for('dashboard', folder_id=current_view_folder_id))
921
 
922
  try:
@@ -935,10 +911,10 @@ def delete_file(file_id):
935
  save_data(data)
936
  flash(f'Файл {original_filename} успешно удален!')
937
  except Exception as e:
938
- flash('Файл удален с сервера, но произошла ошибка обновления базы данных.')
939
  logging.error(f"Delete file DB update error: {e}")
940
  else:
941
- flash('Файл удален с сервера, но не найден в локальной базе данных для удаления.')
942
 
943
 
944
  except hf_utils.EntryNotFoundError:
@@ -948,13 +924,13 @@ def delete_file(file_id):
948
  save_data(data)
949
  flash(f'Файл {original_filename} не найден на сервере, удален из базы.')
950
  except Exception as e:
951
- flash('Ошибка сохранения данных после удаления метаданных (файл не найден на сервере).')
952
  logging.error(f"Delete file metadata save error (HF not found): {e}")
953
  else:
954
- flash('Файл не найден ни на сервере, ни в базе данных.')
955
  except Exception as e:
956
  logging.error(f"Error deleting file {hf_path} for {username}: {e}")
957
- flash(f'Ошибка удаления файла {original_filename}: {e}')
958
 
959
  return redirect(url_for('dashboard', folder_id=current_view_folder_id))
960
 
@@ -965,14 +941,14 @@ def delete_folder(folder_id):
965
  return redirect(url_for('login'))
966
 
967
  if folder_id == 'root':
968
- flash('Нельзя удалить корневую папку!')
969
  return redirect(url_for('dashboard'))
970
 
971
  username = session['username']
972
  data = load_data()
973
  user_data = data['users'].get(username)
974
  if not user_data:
975
- flash('Пользователь не найден!')
976
  session.pop('username', None)
977
  return redirect(url_for('login'))
978
 
@@ -980,13 +956,13 @@ def delete_folder(folder_id):
980
  current_view_folder_id = request.form.get('current_view_folder_id', 'root')
981
 
982
  if not folder_node or folder_node.get('type') != 'folder' or not parent_node:
983
- flash('Папка не найдена или не может быть удалена.')
984
  return redirect(url_for('dashboard', folder_id=current_view_folder_id))
985
 
986
  folder_name = folder_node.get('name', 'папка')
987
 
988
  if folder_node.get('children'):
989
- flash(f'Папку "{folder_name}" можно удалить только если она пуста.')
990
  return redirect(url_for('dashboard', folder_id=current_view_folder_id))
991
 
992
  if remove_node(user_data['filesystem'], folder_id):
@@ -994,10 +970,10 @@ def delete_folder(folder_id):
994
  save_data(data)
995
  flash(f'Пустая папка "{folder_name}" успешно удалена.')
996
  except Exception as e:
997
- flash('Ошибка сохранения данных после удаления папки.')
998
  logging.error(f"Delete empty folder save error: {e}")
999
  else:
1000
- flash('Не удалось удалить папку из базы данных.')
1001
 
1002
  redirect_to_folder_id = parent_node.get('id', 'root')
1003
  return redirect(url_for('dashboard', folder_id=redirect_to_folder_id))
@@ -1006,15 +982,34 @@ def delete_folder(folder_id):
1006
  @app.route('/get_text_content/<file_id>')
1007
  def get_text_content(file_id):
1008
  if 'username' not in session:
1009
- return Response("Не авторизован", status=401)
 
 
 
 
 
 
1010
 
1011
- username = session['username']
1012
  data = load_data()
1013
- user_data = data['users'].get(username)
1014
- if not user_data:
1015
- return Response("Пользователь не найден", status=404)
1016
-
1017
- file_node, _ = find_node_by_id(user_data['filesystem'], file_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1018
 
1019
  if not file_node or file_node.get('type') != 'file' or file_node.get('file_type') != 'text':
1020
  return Response("Текстовый файл не найден", status=404)
@@ -1062,11 +1057,14 @@ def logout():
1062
 
1063
 
1064
  def is_admin():
1065
- return 'username' in session
 
 
1066
 
1067
  @app.route('/admhosto')
1068
  def admin_panel():
1069
  if not is_admin():
 
1070
  return redirect(url_for('login'))
1071
 
1072
  data = load_data()
@@ -1082,7 +1080,7 @@ def admin_panel():
1082
  if item.get('type') == 'file':
1083
  file_count += 1
1084
  elif item.get('type') == 'folder' and 'children' in item:
1085
- q.append(item['children'])
1086
  user_details.append({
1087
  'username': uname,
1088
  'created_at': udata.get('created_at', 'N/A'),
@@ -1090,18 +1088,19 @@ def admin_panel():
1090
  })
1091
 
1092
  html = '''
1093
- <!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Админ-панель</title>
 
1094
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
1095
  <style>''' + BASE_STYLE + '''</style></head><body><div class="container"><h1>Админ-панель</h1>
1096
- {% with messages = get_flashed_messages() %}{% if messages %}{% for message in messages %}<div class="flash">{{ message }}</div>{% endfor %}{% endif %}{% endwith %}
1097
  <h2>Пользователи</h2><div class="user-list">
1098
  {% for user in user_details %}
1099
  <div class="user-item">
1100
  <a href="{{ url_for('admin_user_files', username=user.username) }}">{{ user.username }}</a>
1101
  <p>Зарегистрирован: {{ user.created_at }}</p>
1102
  <p>Файлов: {{ user.file_count }}</p>
1103
- <form method="POST" action="{{ url_for('admin_delete_user', username=user.username) }}" style="display: inline-block;" onsubmit="return confirm('УДАЛИТЬ пользователя {{ user.username }} и ВСЕ его файлы? НЕОБРАТИМО!');">
1104
- <button type="submit" class="btn delete-btn">Удалить</button>
1105
  </form>
1106
  </div>
1107
  {% else %}<p>Пользователей нет.</p>{% endfor %}</div></div></body></html>'''
@@ -1110,49 +1109,66 @@ def admin_panel():
1110
  @app.route('/admhosto/user/<username>')
1111
  def admin_user_files(username):
1112
  if not is_admin():
 
1113
  return redirect(url_for('login'))
1114
 
1115
  data = load_data()
1116
  user_data = data.get('users', {}).get(username)
1117
  if not user_data:
1118
- flash(f'Пользователь {username} не найден.')
1119
  return redirect(url_for('admin_panel'))
1120
 
1121
  all_files = []
1122
- def collect_files(folder, current_fs):
 
 
 
1123
  for item in folder.get('children', []):
1124
  if item.get('type') == 'file':
1125
- item['parent_path_str'] = get_node_path_string(current_fs, folder.get('id', 'root'))
1126
  all_files.append(item)
1127
  elif item.get('type') == 'folder':
1128
- collect_files(item, current_fs)
1129
 
1130
- user_filesystem = user_data.get('filesystem', {})
1131
- if user_filesystem:
1132
- collect_files(user_filesystem, user_filesystem)
1133
  all_files.sort(key=lambda x: x.get('upload_date', ''), reverse=True)
1134
 
 
1135
  html = '''
1136
  <!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Файлы {{ username }}</title>
1137
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
1138
- <style>''' + BASE_STYLE + '''</style></head><body><div class="container"><h1>Файлы пользователя: {{ username }}</h1>
 
 
 
 
 
 
 
1139
  <a href="{{ url_for('admin_panel') }}" class="btn" style="margin-bottom: 20px;">Назад к пользователям</a>
1140
- {% with messages = get_flashed_messages() %}{% if messages %}{% for message in messages %}<div class="flash">{{ message }}</div>{% endfor %}{% endif %}{% endwith %}
1141
- <div class="admin-file-grid">
1142
  {% for file in files %}
1143
  <div class="file-item">
1144
- {% if file.file_type == 'image' %} <img class="file-preview" src="{{ hf_file_url(file.path) }}" loading="lazy">
1145
- {% elif file.file_type == 'video' %} <video class="file-preview" preload="metadata" muted><source src="{{ hf_file_url(file.path, True) }}#t=0.5"></video>
1146
- {% elif file.file_type == 'pdf' %} <div class="file-preview" style="font-size: 40px; line-height: 100px; text-align: center;">📄</div>
1147
- {% elif file.file_type == 'text' %} <div class="file-preview" style="font-size: 40px; line-height: 100px; text-align: center;">📝</div>
1148
- {% else %} <div class="file-preview" style="font-size: 40px; line-height: 100px; text-align: center;">❓</div> {% endif %}
 
1149
  <p title="{{ file.original_filename }}"><b>{{ file.original_filename | truncate(30) }}</b></p>
1150
  <p style="font-size: 0.8em; color: #888;">В папке: {{ file.parent_path_str }}</p>
1151
  <p style="font-size: 0.8em; color: #888;">Загружен: {{ file.upload_date }}</p>
1152
  <p style="font-size: 0.7em; color: #ccc;">ID: {{ file.id }}</p>
1153
  <p style="font-size: 0.7em; color: #ccc; word-break: break-all;">Path: {{ file.path }}</p>
1154
- <div class="file-item-actions">
 
1155
  <a href="{{ url_for('download_file', file_id=file.id) }}" class="btn download-btn">Скачать</a>
 
 
 
 
 
1156
  <form method="POST" action="{{ url_for('admin_delete_file', username=username, file_id=file.id) }}" style="display: inline-block;" onsubmit="return confirm('Удалить файл {{ file.original_filename }}?');">
1157
  <button type="submit" class="btn delete-btn">Удалить</button>
1158
  </form>
@@ -1160,6 +1176,14 @@ def admin_user_files(username):
1160
  </div>
1161
  {% else %} <p>У пользователя нет файлов.</p> {% endfor %}
1162
  </div></div>
 
 
 
 
 
 
 
 
1163
  <script>
1164
  const repoId = "{{ repo_id }}";
1165
  function hfFileUrl(path, download = false) {
@@ -1167,6 +1191,50 @@ def admin_user_files(username):
1167
  if (download) url += '?download=true';
1168
  return url;
1169
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1170
  </script>
1171
  </body></html>'''
1172
  return render_template_string(html, username=username, files=all_files, repo_id=REPO_ID, hf_file_url=lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{path}{'?download=true' if download else ''}")
@@ -1175,14 +1243,15 @@ def admin_user_files(username):
1175
  @app.route('/admhosto/delete_user/<username>', methods=['POST'])
1176
  def admin_delete_user(username):
1177
  if not is_admin():
 
1178
  return redirect(url_for('login'))
1179
  if not HF_TOKEN_WRITE:
1180
- flash('Удаление невозможно: токен для записи не настроен.')
1181
  return redirect(url_for('admin_panel'))
1182
 
1183
  data = load_data()
1184
  if username not in data['users']:
1185
- flash('Пользователь не найден!')
1186
  return redirect(url_for('admin_panel'))
1187
 
1188
  user_data = data['users'][username]
@@ -1207,11 +1276,11 @@ def admin_delete_user(username):
1207
  logging.warning(f"User folder {user_folder_path_on_hf} not found on HF Hub for user {username}. Skipping HF deletion.")
1208
  else:
1209
  logging.error(f"Error deleting user folder {user_folder_path_on_hf} from HF Hub for {username}: {e}")
1210
- flash(f'Ошибка при удалении файлов пользователя {username} с сервера: {e}. Пользователь НЕ удален из базы.')
1211
  return redirect(url_for('admin_panel'))
1212
  except Exception as e:
1213
  logging.error(f"Unexpected error during HF Hub folder deletion for {username}: {e}")
1214
- flash(f'Неожиданная ошибка при удалении файлов {username} с сервера: {e}. Пользователь НЕ удален из базы.')
1215
  return redirect(url_for('admin_panel'))
1216
 
1217
  try:
@@ -1221,7 +1290,7 @@ def admin_delete_user(username):
1221
  logging.info(f"ADMIN ACTION: Successfully deleted user {username} from database.")
1222
  except Exception as e:
1223
  logging.error(f"Error saving data after deleting user {username}: {e}")
1224
- flash(f'Файлы пользователя {username} удалены с сервера, но произошла ошибка при удалении пользователя из базы данных: {e}')
1225
 
1226
  return redirect(url_for('admin_panel'))
1227
 
@@ -1229,34 +1298,35 @@ def admin_delete_user(username):
1229
  @app.route('/admhosto/delete_file/<username>/<file_id>', methods=['POST'])
1230
  def admin_delete_file(username, file_id):
1231
  if not is_admin():
 
1232
  return redirect(url_for('login'))
1233
  if not HF_TOKEN_WRITE:
1234
- flash('Удаление невозможно: токен для записи не настроен.')
1235
  return redirect(url_for('admin_user_files', username=username))
1236
 
1237
  data = load_data()
1238
  user_data = data.get('users', {}).get(username)
1239
  if not user_data:
1240
- flash(f'Пользователь {username} не найден.')
1241
  return redirect(url_for('admin_panel'))
1242
 
1243
  file_node, parent_node = find_node_by_id(user_data['filesystem'], file_id)
1244
 
1245
  if not file_node or file_node.get('type') != 'file' or not parent_node:
1246
- flash('Файл не найден в структуре пользователя.')
1247
  return redirect(url_for('admin_user_files', username=username))
1248
 
1249
  hf_path = file_node.get('path')
1250
  original_filename = file_node.get('original_filename', 'файл')
1251
 
1252
  if not hf_path:
1253
- flash(f'Ошибка: Путь к файлу {original_filename} не найден в метаданных. Удаление только из базы.')
1254
  if remove_node(user_data['filesystem'], file_id):
1255
  try:
1256
  save_data(data)
1257
  flash(f'Метаданные файла {original_filename} удалены (путь отсутствовал).')
1258
  except Exception as e:
1259
- flash('Ошибка сохранения данных после удаления метаданных (путь отсутствовал).')
1260
  logging.error(f"Admin delete file metadata save error (no path): {e}")
1261
  return redirect(url_for('admin_user_files', username=username))
1262
 
@@ -1276,10 +1346,10 @@ def admin_delete_file(username, file_id):
1276
  save_data(data)
1277
  flash(f'Файл {original_filename} успешно удален!')
1278
  except Exception as e:
1279
- flash('Файл удален с сервера, но произошла ошибка обновления базы данных.')
1280
  logging.error(f"Admin delete file DB update error: {e}")
1281
  else:
1282
- flash('Файл удален с сервера, но не найден в базе данных для удаления метаданных.')
1283
 
1284
  except hf_utils.EntryNotFoundError:
1285
  logging.warning(f"ADMIN ACTION: File {hf_path} not found on HF Hub during delete for user {username}. Removing from DB.")
@@ -1288,14 +1358,14 @@ def admin_delete_file(username, file_id):
1288
  save_data(data)
1289
  flash(f'Файл {original_filename} не найден на сервере, удален из базы.')
1290
  except Exception as e:
1291
- flash('Ошибка сохранения данных после удаления метаданных (файл не найден на сервере).')
1292
  logging.error(f"Admin delete file metadata save error (HF not found): {e}")
1293
  else:
1294
- flash('Файл не найден ни на сервере, ни в базе данных.')
1295
 
1296
  except Exception as e:
1297
  logging.error(f"ADMIN ACTION: Error deleting file {hf_path} for {username}: {e}")
1298
- flash(f'Ошибка удаления файла {original_filename}: {e}')
1299
 
1300
  return redirect(url_for('admin_user_files', username=username))
1301
 
@@ -1323,6 +1393,7 @@ if __name__ == '__main__':
1323
  json.dump({'users': {}}, f)
1324
  logging.info(f"Created empty local database file: {DATA_FILE}")
1325
 
 
1326
  app.run(debug=False, host='0.0.0.0', port=7860)
1327
 
1328
  # --- END OF FILE app (9).py ---
 
1
  # --- START OF FILE app (9).py ---
 
2
  from flask import Flask, render_template_string, request, redirect, url_for, session, flash, send_file, jsonify, Response
3
  from flask_caching import Cache
4
  import json
 
25
  cache = Cache(app, config={'CACHE_TYPE': 'simple'})
26
  logging.basicConfig(level=logging.INFO)
27
 
28
+
29
  def find_node_by_id(filesystem, node_id):
30
  if filesystem.get('id') == node_id:
31
  return filesystem, None
 
214
  * { margin: 0; padding: 0; box-sizing: border-box; }
215
  body { font-family: 'Inter', sans-serif; background: var(--background-light); color: var(--text-light); line-height: 1.6; }
216
  body.dark { background: var(--background-dark); color: var(--text-dark); }
217
+ .container { margin: 20px auto; max-width: 1200px; padding: 25px; background: var(--card-bg); border-radius: 20px; box-shadow: var(--shadow); overflow-x: hidden; }
218
  body.dark .container { background: var(--card-bg-dark); }
219
  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; }
220
  h2 { font-size: 1.5em; margin-top: 30px; color: var(--text-light); }
221
+ body.dark h2 { color: var(--text-dark); }
222
+ h4 { font-size: 1.1em; margin-top: 15px; margin-bottom: 5px; color: var(--accent); }
223
+ ol, ul { margin-left: 20px; margin-bottom: 15px; }
224
+ li { margin-bottom: 5px; }
225
  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); }
226
  body.dark input, body.dark textarea { color: var(--text-dark); }
227
  input:focus, textarea:focus { outline: none; box-shadow: 0 0 0 4px var(--primary); }
 
233
  .delete-btn:hover { background: #cc3333; }
234
  .folder-btn { background: var(--folder-color); }
235
  .folder-btn:hover { background: #e6a000; }
236
+ .flash { color: var(--secondary); text-align: center; margin-bottom: 15px; padding: 10px; background: rgba(0, 221, 235, 0.1); border-radius: 10px; }
237
+ .flash.error { color: var(--delete-color); background: rgba(255, 68, 68, 0.1); }
238
  .file-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 20px; margin-top: 20px; }
239
  .user-list { margin-top: 20px; }
240
  .user-item { padding: 15px; background: var(--card-bg); border-radius: 16px; margin-bottom: 10px; box-shadow: var(--shadow); transition: var(--transition); }
 
245
  .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; }
246
  body.dark .item { background: var(--card-bg-dark); }
247
  .item:hover { transform: translateY(-5px); }
248
+ .item-preview { max-width: 100%; height: 130px; object-fit: cover; border-radius: 10px; margin-bottom: 10px; cursor: pointer; display: block; margin-left: auto; margin-right: auto;}
249
  .item.folder .item-preview { object-fit: contain; font-size: 60px; color: var(--folder-color); line-height: 130px; }
250
  .item p { font-size: 0.9em; margin: 5px 0; word-break: break-all; }
251
  .item a { color: var(--primary); text-decoration: none; }
252
  .item a:hover { color: var(--accent); }
253
+ .item-actions { margin-top: 10px; display: flex; flex-wrap: wrap; gap: 5px; justify-content: center; }
254
+ .item-actions .btn { font-size: 0.9em; padding: 5px 10px; }
255
  .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; }
256
+ .modal-content { max-width: 95%; max-height: 95%; background: #fff; padding: 10px; border-radius: 15px; overflow: auto; position: relative; }
257
  body.dark .modal-content { background: var(--card-bg-dark); }
258
  .modal img, .modal video, .modal iframe, .modal pre { max-width: 100%; max-height: 85vh; display: block; margin: auto; border-radius: 10px; }
259
  .modal iframe { width: 80vw; height: 85vh; border: none; }
260
  .modal pre { background: #eee; color: #333; padding: 15px; border-radius: 8px; white-space: pre-wrap; word-wrap: break-word; text-align: left; max-height: 85vh; overflow-y: auto;}
261
  body.dark .modal pre { background: #2b2a33; color: var(--text-dark); }
262
+ .modal-close-btn { position: absolute; top: 15px; right: 25px; font-size: 30px; color: #aaa; cursor: pointer; background: rgba(0,0,0,0.5); border-radius: 50%; width: 30px; height: 30px; line-height: 30px; text-align: center; }
263
+ body.dark .modal-close-btn { color: #555; background: rgba(255,255,255,0.2); }
264
  #progress-container { width: 100%; background: var(--glass-bg); border-radius: 10px; margin: 15px 0; display: none; }
265
  #progress-bar { width: 0%; height: 20px; background: var(--primary); border-radius: 10px; transition: width 0.3s ease; }
266
  .breadcrumbs { margin-bottom: 20px; font-size: 1.1em; }
 
268
  .breadcrumbs a:hover { text-decoration: underline; }
269
  .breadcrumbs span { margin: 0 5px; color: #aaa; }
270
  .folder-actions { margin-top: 20px; margin-bottom: 10px; display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
271
+ .folder-actions input[type=text] { width: auto; flex-grow: 1; margin: 0; min-width: 150px; }
272
+ .folder-actions .btn { margin: 0; flex-shrink: 0;}
 
 
 
 
 
 
 
 
 
 
 
273
  @media (max-width: 768px) {
274
  .file-grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
 
 
 
275
  .folder-actions { flex-direction: column; align-items: stretch; }
276
  .folder-actions input[type=text] { width: 100%; }
277
+ .item-preview { height: 100px; }
278
+ .item.folder .item-preview { font-size: 50px; line-height: 100px; }
279
+ h1 { font-size: 1.8em; }
280
+ .btn { padding: 12px 24px; font-size: 1em; }
281
+ .item-actions .btn { padding: 4px 8px; font-size: 0.8em;}
 
282
  }
283
  @media (max-width: 480px) {
284
  .container { padding: 15px; }
285
+ .file-grid { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 15px; }
286
+ .item-preview { height: 80px; }
287
+ .item.folder .item-preview { font-size: 40px; line-height: 80px; }
288
+ .item p { font-size: 0.8em;}
289
+ .breadcrumbs { font-size: 1em; }
290
+ .btn { padding: 10px 20px; }
 
 
 
291
  }
292
  '''
293
 
294
+
295
  @app.route('/register', methods=['GET', 'POST'])
296
  def register():
297
  if request.method == 'POST':
 
324
  flash('Регистрация прошла успешно!')
325
  return redirect(url_for('dashboard'))
326
  except Exception as e:
327
+ flash('Ошибка сохранения данных при регистрации.', 'error')
328
  logging.error(f"Registration save error: {e}")
329
  return redirect(url_for('register'))
330
 
 
332
  <!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
333
  <title>Регистрация - Zeus Cloud</title><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
334
  <style>''' + BASE_STYLE + '''</style></head><body><div class="container"><h1>Регистрация в Zeus Cloud</h1>
335
+ {% with messages = get_flashed_messages(with_categories=true) %}{% if messages %}{% for category, message in messages %}<div class="flash {{ category }}">{{ message }}</div>{% endfor %}{% endif %}{% endwith %}
336
  <form method="POST"><input type="text" name="username" placeholder="Имя (только буквы/цифры, мин 3)" required pattern="[a-zA-Z0-9]{3,}">
337
  <input type="password" name="password" placeholder="Пароль" required><button type="submit" class="btn">Зарегистрироваться</button></form>
338
  <p style="margin-top: 20px;">Уже есть аккаунт? <a href="{{ url_for('login') }}">Войти</a></p></div></body></html>'''
 
362
  <!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
363
  <title>Zeus Cloud</title><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
364
  <style>''' + BASE_STYLE + '''</style></head><body><div class="container"><h1>Zeus Cloud</h1><div id="flash-messages">
365
+ {% with messages = get_flashed_messages(with_categories=true) %}{% if messages %}{% for category, message in messages %}<div class="flash {{ category }}">{{ message }}</div>{% endfor %}{% endif %}{% endwith %}</div>
366
  <form method="POST" id="login-form"><input type="text" name="username" placeholder="Имя пользователя" required>
367
  <input type="password" name="password" placeholder="Пароль" required><button type="submit" class="btn">Войти</button></form>
368
  <p style="margin-top: 20px;">Нет аккаунта? <a href="{{ url_for('register') }}">Зарегистрируйтесь</a></p></div>
 
380
  if (data.status === 'success') {
381
  localStorage.setItem('zeusCredentials', JSON.stringify({ username: formData.get('username'), password: formData.get('password') }));
382
  window.location.href = data.redirect;
383
+ } else { document.getElementById('flash-messages').innerHTML = `<div class="flash error">${data.message}</div>`; }
384
+ }).catch(error => { document.getElementById('flash-messages').innerHTML = `<div class="flash error">Ошибка соединения!</div>`; });
385
  });
386
  </script></body></html>'''
387
  return render_template_string(html)
 
407
  current_folder, parent_folder = find_node_by_id(user_data['filesystem'], current_folder_id)
408
 
409
  if not current_folder or current_folder.get('type') != 'folder':
410
+ flash('Папка не найдена!', 'error')
411
  current_folder_id = 'root'
412
  current_folder, parent_folder = find_node_by_id(user_data['filesystem'], current_folder_id)
413
  if not current_folder:
414
  logging.error(f"CRITICAL: Root folder not found for user {username}")
415
+ flash('Критическая ошибка: корневая папка не найдена.', 'error')
416
  session.pop('username', None)
417
  return redirect(url_for('login'))
418
 
419
+
420
  items_in_folder = sorted(current_folder.get('children', []), key=lambda x: (x['type'] != 'folder', x.get('name', x.get('original_filename', '')).lower()))
421
 
422
+
423
  if request.method == 'POST':
424
  if not HF_TOKEN_WRITE:
425
+ flash('Загрузка невозможна: токен для записи не настроен.', 'error')
426
  return redirect(url_for('dashboard', folder_id=current_folder_id))
427
 
428
  files = request.files.getlist('files')
429
  if not files or all(not f.filename for f in files):
430
+ flash('Файлы для загрузки не выбраны.', 'error')
431
  return redirect(url_for('dashboard', folder_id=current_folder_id))
432
 
433
  if len(files) > 20:
434
+ flash('Максимум 20 файлов за раз!', 'error')
435
  return redirect(url_for('dashboard', folder_id=current_folder_id))
436
 
437
  target_folder_id = request.form.get('current_folder_id', 'root')
438
  target_folder_node, _ = find_node_by_id(user_data['filesystem'], target_folder_id)
439
 
440
  if not target_folder_node or target_folder_node.get('type') != 'folder':
441
+ flash('Целевая папка для загрузки не найдена!', 'error')
442
  return redirect(url_for('dashboard'))
443
 
444
  api = HfApi()
 
488
  except Exception as del_err:
489
  logging.error(f"Failed to delete orphaned file {hf_path} from HF Hub: {del_err}")
490
 
491
+
492
  except Exception as e:
493
  logging.error(f"Error uploading file {original_filename} for {username}: {e}")
494
  errors.append(f"Ошибка загрузки файла {original_filename}: {e}")
 
501
  save_data(data)
502
  flash(f'{uploaded_count} файл(ов) успешно загружено!')
503
  except Exception as e:
504
+ flash('Файлы загружены на сервер, но произошла ошибка сохранения метаданных.', 'error')
505
  logging.error(f"Error saving data after upload for {username}: {e}")
506
 
507
  if errors:
 
510
 
511
  return redirect(url_for('dashboard', folder_id=target_folder_id))
512
 
513
+
514
  breadcrumbs = []
515
  temp_id = current_folder_id
516
  while temp_id:
 
522
  temp_id = parent.get('id')
523
  breadcrumbs.reverse()
524
 
525
+
526
  html = '''
527
  <!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
528
  <title>Панель управления - Zeus Cloud</title><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
 
613
  {% if not items %} <p>Эта папка пуста.</p> {% endif %}
614
  </div>
615
 
616
+ <h2 style="margin-top: 30px;">Добавить на главный экран (PWA)</h2>
617
+ <p>Вы можете добавить это приложение на главный экран вашего устройства для быстрого доступа:</p>
618
+ <h4>Для Android (Используя Chrome):</h4>
619
+ <ol>
620
+ <li>Откройте это приложение в браузере Chrome.</li>
621
+ <li>Нажмите на кнопку меню (обычно три точки ⋮ в правом верхнем углу).</li>
622
+ <li>Выберите пункт "Установить приложение" или "Добавить на главный экран".</li>
623
+ <li>Следуйте инструкциям на экране для подтверждения.</li>
624
+ </ol>
625
+ <h4>Для iOS (Используя Safari):</h4>
626
+ <ol>
627
+ <li>Откройте это приложение в браузере Safari.</li>
628
+ <li>Нажмите на кнопку "Поделиться" (квадрат со стрелкой вверх ↑, обычно внизу экрана).</li>
629
+ <li>Пролистайте вниз список действий и выберите "На экран 'Домой'".</li>
630
+ <li>При необходимости измените имя ярлыка и нажмите "Добавить".</li>
631
+ </ol>
 
 
632
 
633
  <a href="{{ url_for('logout') }}" class="btn" style="margin-top: 20px;" id="logout-btn">Выйти</a>
634
  </div>
635
 
636
  <div class="modal" id="mediaModal" onclick="closeModal(event)">
637
  <div class="modal-content" id="modalContentContainer">
638
+ <span onclick="closeModalManual()" class="modal-close-btn">×</span>
639
  <div id="modalContent"></div>
640
  </div>
641
  </div>
 
717
  progressBar.style.width = '50%';
718
  uploadBtn.disabled = true;
719
  uploadBtn.textContent = 'Загрузка...';
720
+
721
  });
722
 
723
  document.getElementById('logout-btn').addEventListener('click', function(e) {
 
728
 
729
  </script>
730
  </body></html>'''
 
731
  template_context = {
732
  'username': username,
733
  'items': items_in_folder,
 
737
  'repo_id': REPO_ID,
738
  'HF_TOKEN_READ': HF_TOKEN_READ,
739
  'hf_file_url': lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{path}{'?download=true' if download else ''}",
740
+ 'os': os
 
741
  }
742
  return render_template_string(html, **template_context)
743
 
 
757
  folder_name = request.form.get('folder_name', '').strip()
758
 
759
  if not folder_name:
760
+ flash('Имя папки не может быть пустым!', 'error')
761
  return redirect(url_for('dashboard', folder_id=parent_folder_id))
762
  if not folder_name.isalnum() and '_' not in folder_name and ' ' not in folder_name:
763
+ flash('Имя папки может содержать буквы, цифры, пробелы и подчеркивания.', 'error')
764
  return redirect(url_for('dashboard', folder_id=parent_folder_id))
765
 
766
  folder_id = uuid.uuid4().hex
 
776
  save_data(data)
777
  flash(f'Папка "{folder_name}" успешно создана.')
778
  except Exception as e:
779
+ flash('Ошибка сохранения данных при создании папки.', 'error')
780
  logging.error(f"Create folder save error: {e}")
781
  else:
782
+ flash('Не удалось найти родительскую папку.', 'error')
783
 
784
  return redirect(url_for('dashboard', folder_id=parent_folder_id))
785
 
 
787
  @app.route('/download/<file_id>')
788
  def download_file(file_id):
789
  if 'username' not in session:
790
+ is_admin_route = request.referrer and 'admhosto' in request.referrer
791
+ if not is_admin_route:
792
+ flash('Пожалуйста, войдите в систему!')
793
+ return redirect(url_for('login'))
794
+ elif not is_admin():
795
+ flash('Доступ запрещен (Admin).')
796
+ return redirect(url_for('login'))
797
+ # Admin access allowed to proceed
798
 
 
799
  data = load_data()
 
 
 
800
  file_node = None
801
+ username_context = None # Track which user the file belongs to
802
+
803
+ if 'username' in session:
804
+ username_context = session['username']
805
+ user_data = data['users'].get(username_context)
806
+ if user_data:
807
+ file_node, _ = find_node_by_id(user_data['filesystem'], file_id)
808
+
809
+ # If not found in current user's context OR if admin is accessing, search all users
810
+ if not file_node and is_admin():
811
+ logging.info(f"Admin searching for file ID {file_id} across all users.")
812
+ for uname, udata in data.get('users', {}).items():
813
+ node, _ = find_node_by_id(udata.get('filesystem', {}), file_id)
814
+ if node and node.get('type') == 'file':
815
+ file_node = node
816
+ username_context = uname # Found the file under this user
817
+ logging.info(f"Admin found file ID {file_id} belonging to user {username_context}")
818
+ break
 
 
 
 
 
 
 
 
 
 
 
 
 
 
819
 
820
  if not file_node or file_node.get('type') != 'file':
821
+ flash('Файл не найден!', 'error')
822
+ return redirect(request.referrer or url_for('dashboard' if 'username' in session else 'login'))
 
823
 
824
  hf_path = file_node.get('path')
825
  original_filename = file_node.get('original_filename', 'downloaded_file')
826
 
827
  if not hf_path:
828
+ flash('Ошибка: Путь к файлу не найден в метаданных.', 'error')
829
+ return redirect(request.referrer or url_for('dashboard' if 'username' in session else 'login'))
830
 
831
  file_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{hf_path}?download=true"
832
 
 
847
  )
848
  except requests.exceptions.RequestException as e:
849
  logging.error(f"Error downloading file from HF ({hf_path}): {e}")
850
+ flash(f'Ошибка скачивания файла {original_filename}! ({e})', 'error')
851
+ return redirect(request.referrer or url_for('dashboard' if 'username' in session else 'login'))
852
  except Exception as e:
853
  logging.error(f"Unexpected error during download ({hf_path}): {e}")
854
+ flash('Произошла непредвиденная ошибка при скачивании файла.', 'error')
855
+ return redirect(request.referrer or url_for('dashboard' if 'username' in session else 'login'))
856
 
857
 
858
  @app.route('/delete_file/<file_id>', methods=['POST'])
 
865
  data = load_data()
866
  user_data = data['users'].get(username)
867
  if not user_data:
868
+ flash('Пользователь не найден!', 'error')
869
  session.pop('username', None)
870
  return redirect(url_for('login'))
871
 
 
873
  current_view_folder_id = request.form.get('current_view_folder_id', 'root')
874
 
875
  if not file_node or file_node.get('type') != 'file' or not parent_node:
876
+ flash('Файл не найден или не может быть удален.', 'error')
877
  return redirect(url_for('dashboard', folder_id=current_view_folder_id))
878
 
879
  hf_path = file_node.get('path')
880
  original_filename = file_node.get('original_filename', 'файл')
881
 
882
  if not hf_path:
883
+ flash(f'Ошибка: Путь к файлу {original_filename} не найден. Удаление только из базы.', 'error')
884
  if remove_node(user_data['filesystem'], file_id):
885
  try:
886
  save_data(data)
887
  flash(f'Метаданные файла {original_filename} удалены.')
888
  except Exception as e:
889
+ flash('Ошибка сохранения данных после удаления метаданных.', 'error')
890
  logging.error(f"Delete file metadata save error: {e}")
891
  return redirect(url_for('dashboard', folder_id=current_view_folder_id))
892
 
893
 
894
  if not HF_TOKEN_WRITE:
895
+ flash('Удаление невозможно: токен для записи не настроен.', 'error')
896
  return redirect(url_for('dashboard', folder_id=current_view_folder_id))
897
 
898
  try:
 
911
  save_data(data)
912
  flash(f'Файл {original_filename} успешно удален!')
913
  except Exception as e:
914
+ flash('Файл удален с сервера, но произошла ошибка обновления базы данных.', 'error')
915
  logging.error(f"Delete file DB update error: {e}")
916
  else:
917
+ flash('Файл удален с сервера, но не найден в локальной базе данных для удаления.', 'error')
918
 
919
 
920
  except hf_utils.EntryNotFoundError:
 
924
  save_data(data)
925
  flash(f'Файл {original_filename} не найден на сервере, удален из базы.')
926
  except Exception as e:
927
+ flash('Ошибка сохранения данных после удаления метаданных (файл не найден на сервере).', 'error')
928
  logging.error(f"Delete file metadata save error (HF not found): {e}")
929
  else:
930
+ flash('Файл не найден ни на сервере, ни в базе данных.', 'error')
931
  except Exception as e:
932
  logging.error(f"Error deleting file {hf_path} for {username}: {e}")
933
+ flash(f'Ошибка удаления файла {original_filename}: {e}', 'error')
934
 
935
  return redirect(url_for('dashboard', folder_id=current_view_folder_id))
936
 
 
941
  return redirect(url_for('login'))
942
 
943
  if folder_id == 'root':
944
+ flash('Нельзя удалить корневую папку!', 'error')
945
  return redirect(url_for('dashboard'))
946
 
947
  username = session['username']
948
  data = load_data()
949
  user_data = data['users'].get(username)
950
  if not user_data:
951
+ flash('Пользователь не найден!', 'error')
952
  session.pop('username', None)
953
  return redirect(url_for('login'))
954
 
 
956
  current_view_folder_id = request.form.get('current_view_folder_id', 'root')
957
 
958
  if not folder_node or folder_node.get('type') != 'folder' or not parent_node:
959
+ flash('Папка не найдена или не может быть удалена.', 'error')
960
  return redirect(url_for('dashboard', folder_id=current_view_folder_id))
961
 
962
  folder_name = folder_node.get('name', 'папка')
963
 
964
  if folder_node.get('children'):
965
+ flash(f'Папку "{folder_name}" можно удалить только если она пуста.', 'error')
966
  return redirect(url_for('dashboard', folder_id=current_view_folder_id))
967
 
968
  if remove_node(user_data['filesystem'], folder_id):
 
970
  save_data(data)
971
  flash(f'Пустая папка "{folder_name}" успешно удалена.')
972
  except Exception as e:
973
+ flash('Ошибка сохранения данных после удаления папки.', 'error')
974
  logging.error(f"Delete empty folder save error: {e}")
975
  else:
976
+ flash('Не удалось удалить папку из базы данных.', 'error')
977
 
978
  redirect_to_folder_id = parent_node.get('id', 'root')
979
  return redirect(url_for('dashboard', folder_id=redirect_to_folder_id))
 
982
  @app.route('/get_text_content/<file_id>')
983
  def get_text_content(file_id):
984
  if 'username' not in session:
985
+ # Allow admin access if authenticated as admin
986
+ if not is_admin():
987
+ return Response("Не авторизован", status=401)
988
+ # Admin is accessing, proceed to find file globally
989
+ else:
990
+ # Regular user access
991
+ pass
992
 
 
993
  data = load_data()
994
+ file_node = None
995
+ username_context = None
996
+
997
+ if 'username' in session:
998
+ username_context = session['username']
999
+ user_data = data['users'].get(username_context)
1000
+ if user_data:
1001
+ file_node, _ = find_node_by_id(user_data['filesystem'], file_id)
1002
+
1003
+ # If not found in user context or if admin is accessing, search all
1004
+ if not file_node and is_admin():
1005
+ logging.info(f"Admin searching for text file ID {file_id} across all users.")
1006
+ for uname, udata in data.get('users', {}).items():
1007
+ node, _ = find_node_by_id(udata.get('filesystem', {}), file_id)
1008
+ if node and node.get('type') == 'file' and node.get('file_type') == 'text':
1009
+ file_node = node
1010
+ username_context = uname
1011
+ logging.info(f"Admin found text file ID {file_id} belonging to user {username_context}")
1012
+ break
1013
 
1014
  if not file_node or file_node.get('type') != 'file' or file_node.get('file_type') != 'text':
1015
  return Response("Текстовый файл не найден", status=404)
 
1057
 
1058
 
1059
  def is_admin():
1060
+ # !! INSECURE PLACEHOLDER - REPLACE WITH ACTUAL ADMIN LOGIC !!
1061
+ # Example: return session.get('username') == 'admin_user'
1062
+ return 'username' in session # Allows any logged-in user, change this!
1063
 
1064
  @app.route('/admhosto')
1065
  def admin_panel():
1066
  if not is_admin():
1067
+ flash('Доступ запрещен.', 'error')
1068
  return redirect(url_for('login'))
1069
 
1070
  data = load_data()
 
1080
  if item.get('type') == 'file':
1081
  file_count += 1
1082
  elif item.get('type') == 'folder' and 'children' in item:
1083
+ q.append(item.get('children', [])) # Ensure list is appended
1084
  user_details.append({
1085
  'username': uname,
1086
  'created_at': udata.get('created_at', 'N/A'),
 
1088
  })
1089
 
1090
  html = '''
1091
+ <!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
1092
+ <title>Админ-панель</title>
1093
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
1094
  <style>''' + BASE_STYLE + '''</style></head><body><div class="container"><h1>Админ-панель</h1>
1095
+ {% with messages = get_flashed_messages(with_categories=true) %}{% if messages %}{% for category, message in messages %}<div class="flash {{ category }}">{{ message }}</div>{% endfor %}{% endif %}{% endwith %}
1096
  <h2>Пользователи</h2><div class="user-list">
1097
  {% for user in user_details %}
1098
  <div class="user-item">
1099
  <a href="{{ url_for('admin_user_files', username=user.username) }}">{{ user.username }}</a>
1100
  <p>Зарегистрирован: {{ user.created_at }}</p>
1101
  <p>Файлов: {{ user.file_count }}</p>
1102
+ <form method="POST" action="{{ url_for('admin_delete_user', username=user.username) }}" style="display: inline; margin-left: 10px;" onsubmit="return confirm('УДАЛИТЬ пользователя {{ user.username }} и ВСЕ его файлы? НЕОБРАТИМО!');">
1103
+ <button type="submit" class="btn delete-btn" style="padding: 5px 10px; font-size: 0.9em;">Удалить</button>
1104
  </form>
1105
  </div>
1106
  {% else %}<p>Пользователей нет.</p>{% endfor %}</div></div></body></html>'''
 
1109
  @app.route('/admhosto/user/<username>')
1110
  def admin_user_files(username):
1111
  if not is_admin():
1112
+ flash('Доступ запрещен.', 'error')
1113
  return redirect(url_for('login'))
1114
 
1115
  data = load_data()
1116
  user_data = data.get('users', {}).get(username)
1117
  if not user_data:
1118
+ flash(f'Пользователь {username} не найден.', 'error')
1119
  return redirect(url_for('admin_panel'))
1120
 
1121
  all_files = []
1122
+ def collect_files(folder, current_path_id='root'):
1123
+ parent_node, _ = find_node_by_id(user_data['filesystem'], current_path_id)
1124
+ parent_path_str = get_node_path_string(user_data['filesystem'], current_path_id)
1125
+
1126
  for item in folder.get('children', []):
1127
  if item.get('type') == 'file':
1128
+ item['parent_path_str'] = parent_path_str
1129
  all_files.append(item)
1130
  elif item.get('type') == 'folder':
1131
+ collect_files(item, item.get('id'))
1132
 
1133
+ collect_files(user_data.get('filesystem', {}))
 
 
1134
  all_files.sort(key=lambda x: x.get('upload_date', ''), reverse=True)
1135
 
1136
+
1137
  html = '''
1138
  <!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Файлы {{ username }}</title>
1139
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
1140
+ <style>''' + BASE_STYLE + '''
1141
+ .file-item { background: var(--card-bg); padding: 15px; border-radius: 16px; box-shadow: var(--shadow); transition: var(--transition); display: flex; flex-direction: column; justify-content: space-between; }
1142
+ body.dark .file-item { background: var(--card-bg-dark); }
1143
+ .file-item:hover { transform: translateY(-5px); }
1144
+ .file-preview { max-width: 100%; height: 100px; object-fit: contain; border-radius: 10px; margin-bottom: 10px; display: block; margin-left: auto; margin-right: auto; }
1145
+ .admin-file-actions { margin-top: 10px; display: flex; flex-wrap: wrap; gap: 5px; justify-content: center; }
1146
+ .admin-file-actions .btn { font-size: 0.8em; padding: 4px 8px; margin: 0; }
1147
+ </style></head><body><div class="container"><h1>Файлы пользователя: {{ username }}</h1>
1148
  <a href="{{ url_for('admin_panel') }}" class="btn" style="margin-bottom: 20px;">Назад к пользователям</a>
1149
+ {% with messages = get_flashed_messages(with_categories=true) %}{% if messages %}{% for category, message in messages %}<div class="flash {{ category }}">{{ message }}</div>{% endfor %}{% endif %}{% endwith %}
1150
+ <div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 20px;">
1151
  {% for file in files %}
1152
  <div class="file-item">
1153
+ <div>
1154
+ {% if file.file_type == 'image' %} <img class="file-preview" src="{{ hf_file_url(file.path) }}" loading="lazy" onclick="openModal('{{ hf_file_url(file.path) }}', '{{ file.file_type }}', '{{ file.id }}')" style="cursor: pointer;">
1155
+ {% elif file.file_type == 'video' %} <video class="file-preview" preload="metadata" muted onclick="openModal('{{ hf_file_url(file.path) }}', '{{ file.file_type }}', '{{ file.id }}')" style="cursor: pointer;"><source src="{{ hf_file_url(file.path, True) }}#t=0.5"></video>
1156
+ {% elif file.file_type == 'pdf' %} <div class="file-preview" style="font-size: 40px; line-height: 100px; text-align: center; color: var(--accent); cursor: pointer;" onclick="openModal('{{ hf_file_url(file.path, True) }}', '{{ file.file_type }}', '{{ file.id }}')">📄</div>
1157
+ {% elif file.file_type == 'text' %} <div class="file-preview" style="font-size: 40px; line-height: 100px; text-align: center; color: var(--secondary); cursor: pointer;" onclick="openModal('{{ url_for('get_text_content', file_id=file.id) }}', '{{ file.file_type }}', '{{ file.id }}')">📝</div>
1158
+ {% else %} <div class="file-preview" style="font-size: 40px; line-height: 100px; text-align: center; color: #aaa;">❓</div> {% endif %}
1159
  <p title="{{ file.original_filename }}"><b>{{ file.original_filename | truncate(30) }}</b></p>
1160
  <p style="font-size: 0.8em; color: #888;">В папке: {{ file.parent_path_str }}</p>
1161
  <p style="font-size: 0.8em; color: #888;">Загружен: {{ file.upload_date }}</p>
1162
  <p style="font-size: 0.7em; color: #ccc;">ID: {{ file.id }}</p>
1163
  <p style="font-size: 0.7em; color: #ccc; word-break: break-all;">Path: {{ file.path }}</p>
1164
+ </div>
1165
+ <div class="admin-file-actions">
1166
  <a href="{{ url_for('download_file', file_id=file.id) }}" class="btn download-btn">Скачать</a>
1167
+ {% set previewable = file.file_type in ['image', 'video', 'pdf', 'text'] %}
1168
+ {% if previewable %}
1169
+ <button class="btn" style="background: var(--accent);"
1170
+ onclick="openModal('{{ hf_file_url(file.path) if file.file_type != 'text' else url_for('get_text_content', file_id=file.id) }}', '{{ file.file_type }}', '{{ file.id }}')">Просмотр</button>
1171
+ {% endif %}
1172
  <form method="POST" action="{{ url_for('admin_delete_file', username=username, file_id=file.id) }}" style="display: inline-block;" onsubmit="return confirm('Удалить файл {{ file.original_filename }}?');">
1173
  <button type="submit" class="btn delete-btn">Удалить</button>
1174
  </form>
 
1176
  </div>
1177
  {% else %} <p>У пользователя нет файлов.</p> {% endfor %}
1178
  </div></div>
1179
+
1180
+ <div class="modal" id="mediaModal" onclick="closeModal(event)">
1181
+ <div class="modal-content" id="modalContentContainer">
1182
+ <span onclick="closeModalManual()" class="modal-close-btn">×</span>
1183
+ <div id="modalContent"></div>
1184
+ </div>
1185
+ </div>
1186
+
1187
  <script>
1188
  const repoId = "{{ repo_id }}";
1189
  function hfFileUrl(path, download = false) {
 
1191
  if (download) url += '?download=true';
1192
  return url;
1193
  }
1194
+ async function openModal(srcOrUrl, type, itemId) {
1195
+ const modal = document.getElementById('mediaModal');
1196
+ const modalContent = document.getElementById('modalContent');
1197
+ modalContent.innerHTML = '<p>Загрузка...</p>';
1198
+ modal.style.display = 'flex';
1199
+
1200
+ try {
1201
+ if (type === 'image') {
1202
+ modalContent.innerHTML = `<img src="${srcOrUrl}" alt="Просмотр изображения">`;
1203
+ } else if (type === 'video') {
1204
+ modalContent.innerHTML = `<video controls autoplay style='max-width: 95%; max-height: 85vh;'><source src="${srcOrUrl}" type="video/mp4">Ваш браузер не поддерживает видео.</video>`;
1205
+ } else if (type === 'pdf') {
1206
+ modalContent.innerHTML = `<iframe src="${srcOrUrl}" title="Просмотр PDF"></iframe>`;
1207
+ } else if (type === 'text') {
1208
+ const response = await fetch(srcOrUrl);
1209
+ if (!response.ok) throw new Error(`Ошибка загрузки текста: ${response.statusText}`);
1210
+ const text = await response.text();
1211
+ const escapedText = text.replace(/</g, "<").replace(/>/g, ">");
1212
+ modalContent.innerHTML = `<pre>${escapedText}</pre>`;
1213
+ } else {
1214
+ modalContent.innerHTML = '<p>Предпросмотр для этого типа файла не поддерживается.</p>';
1215
+ }
1216
+ } catch (error) {
1217
+ console.error("Error loading modal content:", error);
1218
+ modalContent.innerHTML = `<p>Не удалось загрузить содержимое для предпросмотра. ${error.message}</p>`;
1219
+ }
1220
+ }
1221
+
1222
+ function closeModal(event) {
1223
+ const modal = document.getElementById('mediaModal');
1224
+ if (event.target === modal) {
1225
+ closeModalManual();
1226
+ }
1227
+ }
1228
+
1229
+ function closeModalManual() {
1230
+ const modal = document.getElementById('mediaModal');
1231
+ modal.style.display = 'none';
1232
+ const video = modal.querySelector('video');
1233
+ if (video) video.pause();
1234
+ const iframe = modal.querySelector('iframe');
1235
+ if (iframe) iframe.src = 'about:blank';
1236
+ document.getElementById('modalContent').innerHTML = '';
1237
+ }
1238
  </script>
1239
  </body></html>'''
1240
  return render_template_string(html, username=username, files=all_files, repo_id=REPO_ID, hf_file_url=lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{path}{'?download=true' if download else ''}")
 
1243
  @app.route('/admhosto/delete_user/<username>', methods=['POST'])
1244
  def admin_delete_user(username):
1245
  if not is_admin():
1246
+ flash('Доступ запрещен.', 'error')
1247
  return redirect(url_for('login'))
1248
  if not HF_TOKEN_WRITE:
1249
+ flash('Удаление невозможно: токен для записи не настроен.', 'error')
1250
  return redirect(url_for('admin_panel'))
1251
 
1252
  data = load_data()
1253
  if username not in data['users']:
1254
+ flash('Пользователь не найден!', 'error')
1255
  return redirect(url_for('admin_panel'))
1256
 
1257
  user_data = data['users'][username]
 
1276
  logging.warning(f"User folder {user_folder_path_on_hf} not found on HF Hub for user {username}. Skipping HF deletion.")
1277
  else:
1278
  logging.error(f"Error deleting user folder {user_folder_path_on_hf} from HF Hub for {username}: {e}")
1279
+ flash(f'Ошибка при удалении файлов пользователя {username} с сервера: {e}. Пользователь НЕ удален из базы.', 'error')
1280
  return redirect(url_for('admin_panel'))
1281
  except Exception as e:
1282
  logging.error(f"Unexpected error during HF Hub folder deletion for {username}: {e}")
1283
+ flash(f'Неожиданная ошибка при удалении файлов {username} с сервера: {e}. Пользователь НЕ удален из базы.', 'error')
1284
  return redirect(url_for('admin_panel'))
1285
 
1286
  try:
 
1290
  logging.info(f"ADMIN ACTION: Successfully deleted user {username} from database.")
1291
  except Exception as e:
1292
  logging.error(f"Error saving data after deleting user {username}: {e}")
1293
+ flash(f'Файлы пользователя {username} удалены с сервера, но произошла ошибка при удалении пользователя из базы данных: {e}', 'error')
1294
 
1295
  return redirect(url_for('admin_panel'))
1296
 
 
1298
  @app.route('/admhosto/delete_file/<username>/<file_id>', methods=['POST'])
1299
  def admin_delete_file(username, file_id):
1300
  if not is_admin():
1301
+ flash('Доступ запрещен.', 'error')
1302
  return redirect(url_for('login'))
1303
  if not HF_TOKEN_WRITE:
1304
+ flash('Удаление невозможно: токен для записи не настроен.', 'error')
1305
  return redirect(url_for('admin_user_files', username=username))
1306
 
1307
  data = load_data()
1308
  user_data = data.get('users', {}).get(username)
1309
  if not user_data:
1310
+ flash(f'Пользователь {username} не найден.', 'error')
1311
  return redirect(url_for('admin_panel'))
1312
 
1313
  file_node, parent_node = find_node_by_id(user_data['filesystem'], file_id)
1314
 
1315
  if not file_node or file_node.get('type') != 'file' or not parent_node:
1316
+ flash('Файл не найден в структуре пользователя.', 'error')
1317
  return redirect(url_for('admin_user_files', username=username))
1318
 
1319
  hf_path = file_node.get('path')
1320
  original_filename = file_node.get('original_filename', 'файл')
1321
 
1322
  if not hf_path:
1323
+ flash(f'Ошибка: Путь к файлу {original_filename} не найден в метаданных. Удаление только из базы.', 'error')
1324
  if remove_node(user_data['filesystem'], file_id):
1325
  try:
1326
  save_data(data)
1327
  flash(f'Метаданные файла {original_filename} удалены (путь отсутствовал).')
1328
  except Exception as e:
1329
+ flash('Ошибка сохранения данных после удаления метаданных (путь отсутствовал).', 'error')
1330
  logging.error(f"Admin delete file metadata save error (no path): {e}")
1331
  return redirect(url_for('admin_user_files', username=username))
1332
 
 
1346
  save_data(data)
1347
  flash(f'Файл {original_filename} успешно удален!')
1348
  except Exception as e:
1349
+ flash('Файл удален с сервера, но произошла ошибка обновления базы данных.', 'error')
1350
  logging.error(f"Admin delete file DB update error: {e}")
1351
  else:
1352
+ flash('Файл удален с сервера, но не найден в базе данных для удаления метаданных.', 'error')
1353
 
1354
  except hf_utils.EntryNotFoundError:
1355
  logging.warning(f"ADMIN ACTION: File {hf_path} not found on HF Hub during delete for user {username}. Removing from DB.")
 
1358
  save_data(data)
1359
  flash(f'Файл {original_filename} не найден на сервере, удален из базы.')
1360
  except Exception as e:
1361
+ flash('Ошибка сохранения данных после удаления метаданных (файл не найден на сервере).', 'error')
1362
  logging.error(f"Admin delete file metadata save error (HF not found): {e}")
1363
  else:
1364
+ flash('Файл не найден ни на сервере, ни в базе данных.', 'error')
1365
 
1366
  except Exception as e:
1367
  logging.error(f"ADMIN ACTION: Error deleting file {hf_path} for {username}: {e}")
1368
+ flash(f'Ошибка удаления файла {original_filename}: {e}', 'error')
1369
 
1370
  return redirect(url_for('admin_user_files', username=username))
1371
 
 
1393
  json.dump({'users': {}}, f)
1394
  logging.info(f"Created empty local database file: {DATA_FILE}")
1395
 
1396
+
1397
  app.run(debug=False, host='0.0.0.0', port=7860)
1398
 
1399
  # --- END OF FILE app (9).py ---