Eluza133 commited on
Commit
acc6bae
·
verified ·
1 Parent(s): 6bf2429

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +440 -238
app.py CHANGED
@@ -1,3 +1,5 @@
 
 
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
@@ -11,7 +13,7 @@ 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")
@@ -52,6 +54,9 @@ def save_data(data):
52
  raise
53
 
54
  def upload_db_to_hf():
 
 
 
55
  try:
56
  api = HfApi()
57
  api.upload_file(
@@ -67,6 +72,12 @@ def upload_db_to_hf():
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,7 +85,8 @@ def download_db_from_hf():
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:
@@ -85,20 +97,20 @@ def download_db_from_hf():
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 = '''
@@ -187,6 +199,8 @@ input:focus, textarea:focus {
187
  box-shadow: var(--shadow);
188
  display: inline-block;
189
  text-decoration: none;
 
 
190
  }
191
  .btn:hover {
192
  transform: scale(1.05);
@@ -194,14 +208,12 @@ input:focus, textarea:focus {
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;
@@ -244,7 +256,7 @@ body.dark .user-item {
244
  }
245
  @media (max-width: 768px) {
246
  .file-grid {
247
- grid-template-columns: repeat(2, 1fr);
248
  }
249
  }
250
  @media (max-width: 480px) {
@@ -259,6 +271,8 @@ body.dark .user-item {
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);
@@ -267,16 +281,37 @@ body.dark .file-item {
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);
@@ -296,19 +331,29 @@ body.dark .file-item {
296
  z-index: 2000;
297
  justify-content: center;
298
  align-items: center;
 
299
  }
300
- .modal img, .modal video {
301
  max-width: 95%;
302
  max-height: 95%;
 
 
 
 
 
 
 
 
303
  object-fit: contain;
304
- border-radius: 20px;
305
  box-shadow: var(--shadow);
306
  }
307
  .modal iframe {
 
 
308
  border: none;
309
- width: 95%;
310
- max-width: 100%;
311
- height: 95%;
312
  }
313
  #progress-container {
314
  width: 100%;
@@ -440,6 +485,9 @@ def login():
440
  .then(data => {
441
  if (data.status === 'success') {
442
  window.location.href = data.redirect;
 
 
 
443
  }
444
  })
445
  .catch(error => console.error('Ошибка автовхода:', error));
@@ -486,54 +534,85 @@ def dashboard():
486
  flash('Пользователь не найден!')
487
  return redirect(url_for('login'))
488
 
489
- user_files = sorted(data['users'][username]['files'], key=lambda x: x['upload_date'], reverse=True)
490
-
491
  if request.method == 'POST':
492
  files = request.files.getlist('files')
493
- if files and len(files) > 20:
 
 
 
 
494
  flash('Максимум 20 файлов за раз!')
495
  return redirect(url_for('dashboard'))
496
 
497
- if files:
498
- os.makedirs('uploads', exist_ok=True)
499
- api = HfApi()
500
- temp_files = []
501
 
502
- for file in files:
503
- if file and file.filename:
504
- original_filename = secure_filename(file.filename)
505
- unique_filename = f"{uuid.uuid4().hex}_{original_filename}"
506
- temp_path = os.path.join('uploads', unique_filename)
 
 
 
 
 
 
 
 
 
507
  file.save(temp_path)
508
- temp_files.append((temp_path, unique_filename, original_filename))
509
-
510
- for temp_path, unique_filename, original_filename in temp_files:
511
- file_path = f"cloud_files/{username}/{unique_filename}"
512
- api.upload_file(
513
- path_or_fileobj=temp_path,
514
- path_in_repo=file_path,
515
- repo_id=REPO_ID,
516
- repo_type="dataset",
517
- token=HF_TOKEN_WRITE,
518
- commit_message=f"Uploaded file for {username}"
519
- )
520
-
521
- file_info = {
522
- 'filename': original_filename,
523
- 'unique_filename': unique_filename,
524
- 'path': file_path,
525
- 'type': get_file_type(original_filename),
526
- 'upload_date': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
527
- }
528
- data['users'][username]['files'].append(file_info)
529
-
530
- if os.path.exists(temp_path):
531
- os.remove(temp_path)
532
-
533
- save_data(data)
534
- flash('Файлы успешно загружены!')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
535
 
536
- return redirect(url_for('dashboard'))
 
537
 
538
  html = '''
539
  <!DOCTYPE html>
@@ -543,12 +622,13 @@ def dashboard():
543
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
544
  <title>Панель управления - Zeus Cloud</title>
545
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
 
546
  <style>''' + BASE_STYLE + '''</style>
547
  </head>
548
  <body>
549
  <div class="container">
550
  <h1>Панель управления Zeus Cloud</h1>
551
- <p>Пользователь: {{ username }}</p>
552
  {% with messages = get_flashed_messages() %}
553
  {% if messages %}
554
  {% for message in messages %}
@@ -560,7 +640,7 @@ def dashboard():
560
  <input type="file" name="files" multiple required>
561
  <button type="submit" class="btn" id="upload-btn">Загрузить файлы</button>
562
  </form>
563
- <div id="progress-container">
564
  <div id="progress-bar"></div>
565
  </div>
566
  <h2 style="margin-top: 30px;">Ваши файлы</h2>
@@ -568,23 +648,32 @@ def dashboard():
568
  <div class="file-grid">
569
  {% for file in user_files %}
570
  <div class="file-item">
571
- <p style="font-size: 1.1em; font-weight: bold;">{{ file['filename'] }}</p>
572
- {% if file['type'] == 'video' %}
573
- <video class="file-preview" preload="metadata" muted loading="lazy" onclick="openModal('https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ file['path'] }}', true)">
574
- <source src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ file['path'] }}" type="video/mp4">
 
 
575
  </video>
576
- {% elif file['type'] == 'image' %}
577
- <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)">
578
- {% elif file['type'] == 'pdf' %}
579
- <iframe class="file-preview" src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ file['path'] }}" loading="lazy"></iframe>
580
- {% elif file['type'] == 'text' %}
581
- <textarea class="file-preview" readonly style="font-family: monospace; padding: 10px; border: 1px solid #ddd; border-radius: 5px; overflow: auto; width: 100%; height: 200px;">Предпросмотр текстовых файлов пока не поддерживается. Скачайте файл для просмотра.</textarea>
 
 
 
 
582
  {% else %}
583
- <p>Тип файла не поддерживается для предпросмотра</p>
 
 
584
  {% endif %}
585
- <p>{{ file['upload_date'] }}</p>
586
- <a href="{{ url_for('download_file', file_path=file['path'], filename=file['unique_filename'], original_filename=file['filename']) }}" class="btn download-btn">Скачать</a>
587
- <a href="{{ url_for('delete_file', file_path=file['path']) }}" class="btn delete-btn" onclick="return confirm('Вы уверены, что хотите удалить этот файл?');">Удалить</a>
 
588
  </div>
589
  {% endfor %}
590
  {% if not user_files %}
@@ -593,49 +682,74 @@ def dashboard():
593
  </div>
594
  <h2 style="margin-top: 30px;">Добавить на главный экран</h2>
595
  <p>Для быстрого доступа к Zeus Cloud, вы можете добавить это приложение на главный экран вашего телефона:</p>
596
- <div style="margin-top: 10px;">
597
- <h3>Для пользователей Android:</h3>
598
- <ol>
599
- <li>Откройте Zeus Cloud в браузере Chrome.</li>
600
- <li>Нажмите на меню браузера (обычно три точки вверху справа).</li>
601
- <li>Выберите <strong>"Добавить на главный экран"</strong>.</li>
602
- <li>Подтвердите добавление, и иконка приложения появится на вашем главном экране.</li>
603
- </ol>
604
- </div>
605
- <div style="margin-top: 10px;">
606
- <h3>Для пользователей iOS (iPhone/iPad):</h3>
607
- <ol>
608
- <li>Откройте Zeus Cloud в браузере Safari.</li>
609
- <li>Нажмите кнопку <strong>"Поделиться"</strong> (квадрат со стрелкой вверх) в нижней части экрана.</li>
610
- <li>Прокрутите список опций вниз и выберите <strong>"Добавить на экран «Домой»"</strong>.</li>
611
- <li>В правом верхнем углу нажмите <strong>"Добавить"</strong>. Иконка приложения появится на вашем главном экране.</li>
612
- </ol>
 
 
613
  </div>
614
- <a href="{{ url_for('logout') }}" class="btn" style="margin-top: 20px;" id="logout-btn">Выйти</a>
615
  </div>
616
  <div class="modal" id="mediaModal" onclick="closeModal(event)">
617
- <div id="modalContent"></div>
618
  </div>
619
  <script>
620
- function openModal(src, isVideo) {
 
 
621
  const modal = document.getElementById('mediaModal');
622
  const modalContent = document.getElementById('modalContent');
623
- if (isVideo) {
624
- modalContent.innerHTML = `<video controls autoplay><source src="${src}" type="video/mp4"></video>`;
 
 
 
 
 
 
 
 
 
 
 
 
 
625
  } else {
626
- modalContent.innerHTML = `<img src="${src}">`;
 
627
  }
628
  modal.style.display = 'flex';
629
  }
 
630
  function closeModal(event) {
631
  const modal = document.getElementById('mediaModal');
 
632
  if (event.target === modal) {
633
  modal.style.display = 'none';
634
  const video = modal.querySelector('video');
635
  if (video) {
636
  video.pause();
 
 
 
 
 
637
  }
638
- modalContent.innerHTML = '';
639
  }
640
  }
641
 
@@ -643,50 +757,71 @@ def dashboard():
643
  const progressBar = document.getElementById('progress-bar');
644
  const progressContainer = document.getElementById('progress-container');
645
  const uploadBtn = document.getElementById('upload-btn');
 
646
 
647
  form.addEventListener('submit', function(e) {
648
- e.preventDefault();
649
- const files = form.querySelector('input[type="file"]').files;
650
- if (files.length > 20) {
651
- alert('Максимум 20 файлов за раз!');
652
- return;
653
- }
654
  if (files.length === 0) {
655
  alert('Пожалуйста, выберите файлы для загрузки.');
656
  return;
657
  }
 
 
 
 
658
 
659
  const formData = new FormData(form);
660
  progressContainer.style.display = 'block';
661
  progressBar.style.width = '0%';
662
  uploadBtn.disabled = true;
 
663
 
664
  const xhr = new XMLHttpRequest();
665
- xhr.open('POST', '/dashboard', true);
 
 
666
 
667
  xhr.upload.onprogress = function(event) {
668
  if (event.lengthComputable) {
669
- const percent = (event.loaded / event.total) * 100;
670
  progressBar.style.width = percent + '%';
 
671
  }
672
  };
673
 
674
- xhr.onload = function() {
675
- uploadBtn.disabled = false;
 
676
  progressContainer.style.display = 'none';
677
  progressBar.style.width = '0%';
678
- if (xhr.status === 200) {
679
- location.reload();
680
- } else {
681
- alert('Ошибка загрузки!');
 
 
 
 
 
 
 
 
 
 
 
 
682
  }
683
- };
684
 
685
  xhr.onerror = function() {
686
- alert('Ошибка соединения!');
687
  progressContainer.style.display = 'none';
688
  progressBar.style.width = '0%';
 
689
  uploadBtn.disabled = false;
 
690
  };
691
 
692
  xhr.send(formData);
@@ -695,37 +830,53 @@ def dashboard():
695
  document.getElementById('logout-btn').addEventListener('click', function(e) {
696
  e.preventDefault();
697
  localStorage.removeItem('zeusCredentials');
698
- window.location.href = '/logout';
699
  });
700
  </script>
701
  </body>
702
  </html>
703
  '''
704
- return render_template_string(html, username=username, user_files=user_files, repo_id=REPO_ID)
705
 
706
- @app.route('/download/<path:file_path>/<filename>/<original_filename>')
707
- def download_file(file_path, filename, original_filename):
708
- if 'username' not in session:
709
- flash('Пожалуйста, войдите в систему!')
710
- return redirect(url_for('login'))
711
 
712
- username = session['username']
 
 
 
 
 
 
 
 
 
 
 
 
 
713
  data = load_data()
714
- if username not in data['users']:
715
- session.pop('username', None)
716
- flash('Пользователь не найден!')
717
- return redirect(url_for('login'))
718
 
719
- user_files = data['users'][username]['files']
720
- if not any(file['path'] == file_path for file in user_files):
721
- is_admin_route = request.referrer and 'admhosto' in request.referrer
722
- if not is_admin_route:
 
 
 
 
 
 
 
723
  flash('У вас нет доступа к этому файлу!')
724
  return redirect(url_for('dashboard'))
 
 
 
 
 
725
 
 
726
  file_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{file_path}?download=true"
727
  try:
728
- api = HfApi()
729
  headers = {}
730
  if HF_TOKEN_READ:
731
  headers["authorization"] = f"Bearer {HF_TOKEN_READ}"
@@ -733,27 +884,29 @@ def download_file(file_path, filename, original_filename):
733
  response = requests.get(file_url, headers=headers, stream=True)
734
  response.raise_for_status()
735
 
 
736
  file_content = BytesIO(response.content)
 
 
737
  return send_file(
738
  file_content,
739
  as_attachment=True,
740
- download_name=original_filename,
741
  mimetype='application/octet-stream'
742
  )
743
  except requests.exceptions.RequestException as e:
744
- logging.error(f"Error downloading file from HF: {e}")
745
- flash('Ошибка скачивания файла!')
746
- if request.referrer and 'admhosto' in request.referrer:
747
- return redirect(url_for('admin_user_files', username=file_path.split('/')[1]))
748
  else:
749
- return redirect(url_for('dashboard'))
 
750
  except Exception as e:
751
- logging.error(f"Unexpected error during download: {e}")
752
- flash('Произошла непредвиденная ошибка при скачивании файла.')
753
- if request.referrer and 'admhosto' in request.referrer:
754
- return redirect(url_for('admin_user_files', username=file_path.split('/')[1]))
755
- else:
756
- return redirect(url_for('dashboard'))
757
 
758
  @app.route('/delete/<path:file_path>')
759
  def delete_file(file_path):
@@ -768,13 +921,19 @@ def delete_file(file_path):
768
  flash('Пользователь не найден!')
769
  return redirect(url_for('login'))
770
 
771
- user_files = data['users'][username]['files']
772
  file_to_delete = next((file for file in user_files if file['path'] == file_path), None)
773
 
774
  if not file_to_delete:
775
  flash('Файл не найден!')
776
  return redirect(url_for('dashboard'))
777
 
 
 
 
 
 
 
778
  try:
779
  api = HfApi()
780
  api.delete_file(
@@ -782,26 +941,31 @@ def delete_file(file_path):
782
  repo_id=REPO_ID,
783
  repo_type="dataset",
784
  token=HF_TOKEN_WRITE,
785
- commit_message=f"Deleted file {file_path} for {username}"
786
  )
 
787
  data['users'][username]['files'] = [f for f in user_files if f['path'] != file_path]
788
  save_data(data)
789
- flash('Файл успешно удален!')
790
  except Exception as e:
791
- logging.error(f"Error deleting file: {e}")
792
- flash('Ошибка удаления файла!')
793
 
794
  return redirect(url_for('dashboard'))
795
 
796
  @app.route('/logout')
797
  def logout():
798
  session.pop('username', None)
 
 
799
  return redirect(url_for('login'))
800
 
 
801
  @app.route('/admhosto')
802
  def admin_panel():
 
803
  data = load_data()
804
- users = data['users']
805
 
806
  html = '''
807
  <!DOCTYPE html>
@@ -811,6 +975,7 @@ def admin_panel():
811
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
812
  <title>Админ-панель - Zeus Cloud</title>
813
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
 
814
  <style>''' + BASE_STYLE + '''</style>
815
  </head>
816
  <body>
@@ -824,7 +989,7 @@ def admin_panel():
824
  <p>Дата регистрации: {{ user_data.get('created_at', 'N/A') }}</p>
825
  <p>Количество файлов: {{ user_data.get('files', []) | length }}</p>
826
  <form method="POST" action="{{ url_for('admin_delete_user', username=username) }}" style="display: inline; margin-left: 10px;" onsubmit="return confirm('Вы уверены, что хотите удалить пользователя {{ username }} и все его файлы? Это действие необратимо!');">
827
- <button type="submit" class="btn delete-btn" style="padding: 5px 10px; font-size: 0.9em;">Удалить</button>
828
  </form>
829
  </div>
830
  {% endfor %}
@@ -849,8 +1014,9 @@ def admin_panel():
849
 
850
  @app.route('/admhosto/user/<username>')
851
  def admin_user_files(username):
 
852
  data = load_data()
853
- if username not in data['users']:
854
  flash('Пользователь не найден!')
855
  return redirect(url_for('admin_panel'))
856
 
@@ -864,11 +1030,13 @@ def admin_user_files(username):
864
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
865
  <title>Файлы пользователя {{ username }} - Zeus Cloud</title>
866
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
 
867
  <style>''' + BASE_STYLE + '''</style>
868
  </head>
869
  <body>
870
  <div class="container">
871
  <h1>Файлы пользователя: {{ username }}</h1>
 
872
  {% with messages = get_flashed_messages() %}
873
  {% if messages %}
874
  {% for message in messages %}
@@ -879,24 +1047,25 @@ def admin_user_files(username):
879
  <div class="file-grid">
880
  {% for file in user_files %}
881
  <div class="file-item">
882
- <p style="font-size: 1.1em; font-weight: bold;">{{ file.get('filename', 'N/A') }}</p>
883
- {% if file.get('type') == 'video' %}
884
- <video class="file-preview" preload="metadata" muted loading="lazy" onclick="openModal('https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ file['path'] }}?download=true', true)">
885
- <source src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ file['path'] }}?download=true" type="video/mp4">
886
- </video>
887
- {% elif file.get('type') == 'image' %}
888
- <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)">
889
- {% elif file.get('type') == 'pdf' %}
890
- <iframe class="file-preview" src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ file['path'] }}" loading="lazy"></iframe>
891
- {% elif file.get('type') == 'text' %}
892
- <textarea class="file-preview" readonly style="font-family: monospace; padding: 10px; border: 1px solid #ddd; border-radius: 5px; overflow: auto; width: 100%; height: 200px;">Предпросмотр текстовых файлов пока не поддерживается. Скачайте файл для просмотра.</textarea>
893
- {% else %}
894
- <p>Тип файла не поддерживается для предпросмотра</p>
895
- {% endif %}
896
- <p>{{ file.get('upload_date', 'N/A') }}</p>
897
- <a href="{{ url_for('download_file', file_path=file['path'], filename=file['unique_filename'], original_filename=file['filename']) }}" class="btn download-btn">Скачать</a>
898
- <form method="POST" action="{{ url_for('admin_delete_file', username=username, file_path=file['path']) }}" style="display: inline; margin-left: 5px;" onsubmit="return confirm('Вы уверены, что хотите удалить этот файл?');">
899
- <button type="submit" class="btn delete-btn" style="padding: 5px 10px; font-size: 0.9em;"далить</button>
 
900
  </form>
901
  </div>
902
  {% endfor %}
@@ -904,103 +1073,120 @@ def admin_user_files(username):
904
  <p>У этого пользователя пока нет файлов.</p>
905
  {% endif %}
906
  </div>
907
- <a href="{{ url_for('admin_panel') }}" class="btn" style="margin-top: 20px;">Назад к списку пользователей</a>
908
  </div>
909
  <div class="modal" id="mediaModal" onclick="closeModal(event)">
910
- <div id="modalContent"></div>
911
  </div>
912
  <script>
913
- function openModal(src, isVideo) {
 
 
914
  const modal = document.getElementById('mediaModal');
915
  const modalContent = document.getElementById('modalContent');
916
- const tokenParam = HF_TOKEN_READ ? `?token=${HF_TOKEN_READ}` : "";
917
- const finalSrc = src + tokenParam;
918
-
919
- if (isVideo) {
920
- const videoSrc = finalSrc.includes('?download=true') ? finalSrc : finalSrc.replace('?', '?download=true&');
921
- modalContent.innerHTML = `<video controls autoplay style='max-width: 95%; max-height: 95vh;'><source src="${videoSrc}" type="video/mp4"></video>`;
 
 
 
 
 
 
 
922
  } else {
923
- const img_src = finalSrc.includes('?download=true') ? finalSrc : finalSrc.replace('?', '?download=true&');
924
- modalContent.innerHTML = `<img src="${img_src}" style='max-width: 95%; max-height: 95vh;'>`;
925
- }
926
- modal.style.display = 'flex';
927
  }
 
928
  function closeModal(event) {
929
  const modal = document.getElementById('mediaModal');
930
  if (event.target === modal) {
931
  modal.style.display = 'none';
932
  const video = modal.querySelector('video');
933
- if (video) {
934
- video.pause();
935
- }
936
  document.getElementById('modalContent').innerHTML = '';
937
  }
938
  }
939
- const HF_TOKEN_READ = "{{ os.getenv('HF_TOKEN_READ') or os.getenv('HF_TOKEN') }}";
940
  </script>
941
  </body>
942
  </html>
943
  '''
944
- return render_template_string(html, username=username, user_files=user_files, repo_id=REPO_ID, os=os)
 
945
 
946
  @app.route('/admhosto/delete_user/<username>', methods=['POST'])
947
  def admin_delete_user(username):
 
 
 
 
 
948
  data = load_data()
949
- if username not in data['users']:
950
  flash('Пользователь не найден!')
951
  return redirect(url_for('admin_panel'))
952
 
953
- user_files_to_delete = data['users'][username].get('files', [])
 
954
 
955
  try:
956
  api = HfApi()
957
- paths_to_delete = [file['path'] for file in user_files_to_delete]
958
- if paths_to_delete:
959
- try:
960
- api.delete_folder(
961
- folder_path=f"cloud_files/{username}",
962
- repo_id=REPO_ID,
963
- repo_type="dataset",
964
- token=HF_TOKEN_WRITE,
965
- commit_message=f"Deleted all files for user {username} by admin"
966
- )
967
- logging.info(f"Successfully deleted folder for user {username}")
968
- except Exception as folder_delete_error:
969
- logging.warning(f"Could not delete folder for {username}, attempting individual file deletion: {folder_delete_error}")
970
- for file_path in paths_to_delete:
971
- try:
972
- api.delete_file(
973
- path_in_repo=file_path,
974
- repo_id=REPO_ID,
975
- repo_type="dataset",
976
- token=HF_TOKEN_WRITE
977
- )
978
- except Exception as file_delete_error:
979
- logging.error(f"Error deleting file {file_path} for user {username}: {file_delete_error}")
980
 
 
 
981
  del data['users'][username]
982
  save_data(data)
983
- flash(f'Пользователь {username} и его файлы успешно удалены!')
984
- logging.info(f"Admin deleted user {username} and their files.")
985
 
986
  except Exception as e:
987
- logging.error(f"Error deleting user {username}: {e}")
988
- flash(f'Ошибка при удалении пользователя {username}!')
989
 
990
  return redirect(url_for('admin_panel'))
991
 
 
992
  @app.route('/admhosto/delete_file/<username>/<path:file_path>', methods=['POST'])
993
  def admin_delete_file(username, file_path):
 
 
 
 
 
994
  data = load_data()
995
- if username not in data['users']:
996
  flash('Пользователь не найден!')
997
  return redirect(url_for('admin_panel'))
998
 
999
  user_files = data['users'][username].get('files', [])
1000
- file_exists_in_db = any(f['path'] == file_path for f in user_files)
 
1001
 
1002
- if not file_exists_in_db:
1003
- flash('Файл не найден в базе данных пользователя!')
1004
 
1005
  try:
1006
  api = HfApi()
@@ -1011,27 +1197,43 @@ def admin_delete_file(username, file_path):
1011
  token=HF_TOKEN_WRITE,
1012
  commit_message=f"Admin deleted file {file_path} for user {username}"
1013
  )
 
 
 
 
 
 
 
 
 
1014
 
1015
- data['users'][username]['files'] = [f for f in user_files if f['path'] != file_path]
1016
- save_data(data)
1017
- flash('Файл успешно удален!')
1018
- logging.info(f"Admin deleted file {file_path} for user {username}")
1019
 
1020
  except Exception as e:
1021
- logging.error(f"Error deleting file {file_path} by admin: {e}")
1022
- flash('Ошибка удаления файла!')
 
1023
 
1024
  return redirect(url_for('admin_user_files', username=username))
1025
 
 
1026
  if __name__ == '__main__':
1027
  if not HF_TOKEN_WRITE:
1028
- logging.warning("HF_TOKEN (write access) is not set. File uploads and deletions will fail.")
1029
  if not HF_TOKEN_READ:
1030
- 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.")
 
 
 
 
1031
 
1032
  if HF_TOKEN_WRITE:
1033
- threading.Thread(target=periodic_backup, daemon=True).start()
 
 
1034
  else:
1035
- logging.warning("Periodic backup disabled because HF_TOKEN (write access) is not set.")
 
 
1036
 
1037
- app.run(debug=False, host='0.0.0.0', port=7860)
 
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
 
13
  import requests
14
  from io import BytesIO
15
  import uuid
16
+ import os.path
17
 
18
  app = Flask(__name__)
19
  app.secret_key = os.getenv("FLASK_SECRET_KEY", "supersecretkey")
 
54
  raise
55
 
56
  def upload_db_to_hf():
57
+ if not HF_TOKEN_WRITE:
58
+ logging.warning("HF_TOKEN_WRITE not set. Skipping database upload.")
59
+ return
60
  try:
61
  api = HfApi()
62
  api.upload_file(
 
72
  logging.error(f"Error uploading database: {e}")
73
 
74
  def download_db_from_hf():
75
+ if not HF_TOKEN_READ:
76
+ logging.warning("HF_TOKEN_READ not set. Cannot download database.")
77
+ if not os.path.exists(DATA_FILE):
78
+ with open(DATA_FILE, 'w', encoding='utf-8') as f:
79
+ json.dump({'users': {}, 'files': {}}, f)
80
+ return
81
  try:
82
  hf_hub_download(
83
  repo_id=REPO_ID,
 
85
  repo_type="dataset",
86
  token=HF_TOKEN_READ,
87
  local_dir=".",
88
+ local_dir_use_symlinks=False,
89
+ force_download=True # Ensure we get the latest version
90
  )
91
  logging.info("Database downloaded from Hugging Face")
92
  except Exception as e:
 
97
 
98
  def periodic_backup():
99
  while True:
 
100
  time.sleep(1800)
101
+ logging.info("Attempting periodic backup...")
102
+ upload_db_to_hf()
103
 
104
  def get_file_type(filename):
105
+ filename_lower = filename.lower()
106
+ if filename_lower.endswith(('.mp4', '.mov', '.avi', '.webm', '.mkv', '.flv')):
107
+ return 'video'
108
+ elif filename_lower.endswith(('.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg')):
109
+ return 'image'
110
+ elif filename_lower.endswith('.pdf'):
111
+ return 'pdf'
112
+ elif filename_lower.endswith('.txt'):
113
+ return 'text'
 
114
  return 'other'
115
 
116
  BASE_STYLE = '''
 
199
  box-shadow: var(--shadow);
200
  display: inline-block;
201
  text-decoration: none;
202
+ margin-right: 5px; /* Added margin */
203
+ margin-bottom: 5px; /* Added margin */
204
  }
205
  .btn:hover {
206
  transform: scale(1.05);
 
208
  }
209
  .download-btn {
210
  background: var(--secondary);
 
211
  }
212
  .download-btn:hover {
213
  background: #00b8c5;
214
  }
215
  .delete-btn {
216
  background: var(--delete-color);
 
217
  }
218
  .delete-btn:hover {
219
  background: #cc3333;
 
256
  }
257
  @media (max-width: 768px) {
258
  .file-grid {
259
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
260
  }
261
  }
262
  @media (max-width: 480px) {
 
271
  box-shadow: var(--shadow);
272
  text-align: center;
273
  transition: var(--transition);
274
+ word-wrap: break-word; /* Ensure long names wrap */
275
+ overflow: hidden; /* Hide overflow */
276
  }
277
  body.dark .file-item {
278
  background: var(--card-bg-dark);
 
281
  transform: translateY(-5px);
282
  }
283
  .file-preview {
284
+ width: 100%; /* Ensure preview takes full width */
285
+ height: 180px; /* Fixed height for consistency */
286
+ object-fit: cover; /* Cover the area */
287
+ border-radius: 10px;
288
+ margin-bottom: 10px;
289
+ cursor: pointer;
290
+ background-color: #eee; /* Placeholder color */
291
+ }
292
+ .file-preview-icon { /* Style for icons */
293
+ width: 100%;
294
+ height: 180px;
295
+ display: flex;
296
+ justify-content: center;
297
+ align-items: center;
298
+ font-size: 5em; /* Large icon */
299
+ color: var(--secondary);
300
  border-radius: 10px;
301
  margin-bottom: 10px;
302
+ background-color: rgba(0, 221, 235, 0.1); /* Light background */
303
+ cursor: pointer;
304
  }
305
  .file-item p {
306
  font-size: 0.9em;
307
  margin: 5px 0;
308
+ white-space: nowrap;
309
+ overflow: hidden;
310
+ text-overflow: ellipsis; /* Add ellipsis for long names */
311
+ }
312
+ .file-item .filename { /* Style for filename */
313
+ font-weight: 600;
314
+ margin-bottom: 8px;
315
  }
316
  .file-item a {
317
  color: var(--primary);
 
331
  z-index: 2000;
332
  justify-content: center;
333
  align-items: center;
334
+ padding: 10px;
335
  }
336
+ .modal-content {
337
  max-width: 95%;
338
  max-height: 95%;
339
+ display: flex; /* Use flex for centering */
340
+ justify-content: center;
341
+ align-items: center;
342
+ }
343
+
344
+ .modal img, .modal video {
345
+ max-width: 100%;
346
+ max-height: 100%;
347
  object-fit: contain;
348
+ border-radius: 10px;
349
  box-shadow: var(--shadow);
350
  }
351
  .modal iframe {
352
+ width: 90vw;
353
+ height: 90vh;
354
  border: none;
355
+ border-radius: 10px;
356
+ background: white; /* Background for iframe content */
 
357
  }
358
  #progress-container {
359
  width: 100%;
 
485
  .then(data => {
486
  if (data.status === 'success') {
487
  window.location.href = data.redirect;
488
+ } else {
489
+ // Clear invalid stored credentials
490
+ localStorage.removeItem('zeusCredentials');
491
  }
492
  })
493
  .catch(error => console.error('Ошибка автовхода:', error));
 
534
  flash('Пользователь не найден!')
535
  return redirect(url_for('login'))
536
 
 
 
537
  if request.method == 'POST':
538
  files = request.files.getlist('files')
539
+ if not files or all(not f.filename for f in files):
540
+ flash('Файлы не выбраны!')
541
+ return redirect(url_for('dashboard'))
542
+
543
+ if len(files) > 20:
544
  flash('Максимум 20 файлов за раз!')
545
  return redirect(url_for('dashboard'))
546
 
547
+ if not HF_TOKEN_WRITE:
548
+ flash('Загрузка невозможна: отсутствует токен Hugging Face для записи.')
549
+ return redirect(url_for('dashboard'))
 
550
 
551
+ os.makedirs('uploads', exist_ok=True)
552
+ api = HfApi()
553
+ uploaded_files_info = []
554
+ upload_errors = []
555
+
556
+ for file in files:
557
+ if file and file.filename:
558
+ original_filename = file.filename
559
+ sanitized_base, extension = os.path.splitext(secure_filename(original_filename))
560
+ unique_id = str(uuid.uuid4())[:8] # Shorter UUID for filename
561
+ unique_filename = f"{sanitized_base}_{unique_id}{extension}"
562
+ temp_path = os.path.join('uploads', unique_filename) # Save temp with unique name
563
+
564
+ try:
565
  file.save(temp_path)
566
+ file_path_in_repo = f"cloud_files/{username}/{unique_filename}"
567
+
568
+ api.upload_file(
569
+ path_or_fileobj=temp_path,
570
+ path_in_repo=file_path_in_repo,
571
+ repo_id=REPO_ID,
572
+ repo_type="dataset",
573
+ token=HF_TOKEN_WRITE,
574
+ commit_message=f"Uploaded {unique_filename} for {username}"
575
+ )
576
+
577
+ file_info = {
578
+ 'filename': unique_filename, # Unique name used in storage
579
+ 'original_name': original_filename, # Original name for display
580
+ 'path': file_path_in_repo,
581
+ 'type': get_file_type(original_filename),
582
+ 'upload_date': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
583
+ }
584
+ uploaded_files_info.append(file_info)
585
+
586
+ except Exception as e:
587
+ logging.error(f"Error uploading file {original_filename} ({unique_filename}): {e}")
588
+ upload_errors.append(original_filename)
589
+ finally:
590
+ if os.path.exists(temp_path):
591
+ os.remove(temp_path)
592
+
593
+ if uploaded_files_info:
594
+ current_data = load_data() # Reload data before modifying
595
+ if username in current_data['users']:
596
+ if 'files' not in current_data['users'][username]:
597
+ current_data['users'][username]['files'] = []
598
+ current_data['users'][username]['files'].extend(uploaded_files_info)
599
+ save_data(current_data)
600
+ flash(f'{len(uploaded_files_info)} файл(ов) успешно загружено!')
601
+ else:
602
+ flash('Ошибка: пользователь не найден после загрузки.') # Should not happen if session check passed
603
+
604
+ if upload_errors:
605
+ flash(f'Ошибка при загрузке файлов: {", ".join(upload_errors)}')
606
+
607
+ # Use AJAX response if request expects JSON (like from the JS progress handler)
608
+ if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
609
+ # Even though JS reloads, sending a success helps confirm operation
610
+ return jsonify({'status': 'success', 'uploaded': len(uploaded_files_info), 'errors': len(upload_errors)})
611
+ else:
612
+ return redirect(url_for('dashboard')) # Standard redirect for non-JS form submit
613
 
614
+ # GET request part
615
+ user_files = sorted(data['users'][username].get('files', []), key=lambda x: x.get('upload_date', ''), reverse=True)
616
 
617
  html = '''
618
  <!DOCTYPE html>
 
622
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
623
  <title>Панель управления - Zeus Cloud</title>
624
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
625
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"> <!-- Font Awesome for icons -->
626
  <style>''' + BASE_STYLE + '''</style>
627
  </head>
628
  <body>
629
  <div class="container">
630
  <h1>Панель управления Zeus Cloud</h1>
631
+ <p>Пользователь: <strong>{{ username }}</strong></p>
632
  {% with messages = get_flashed_messages() %}
633
  {% if messages %}
634
  {% for message in messages %}
 
640
  <input type="file" name="files" multiple required>
641
  <button type="submit" class="btn" id="upload-btn">Загрузить файлы</button>
642
  </form>
643
+ <div id="progress-container" style="display: none;">
644
  <div id="progress-bar"></div>
645
  </div>
646
  <h2 style="margin-top: 30px;">Ваши файлы</h2>
 
648
  <div class="file-grid">
649
  {% for file in user_files %}
650
  <div class="file-item">
651
+ <p class="filename" title="{{ file.original_name }}">{{ file.original_name }}</p> {# Display original name #}
652
+ {% set file_url = "https://huggingface.co/datasets/" + repo_id + "/resolve/main/" + file.path + "?token=" + hf_token_read if hf_token_read else "https://huggingface.co/datasets/" + repo_id + "/resolve/main/" + file.path %}
653
+ {% if file.type == 'video' %}
654
+ <video class="file-preview" preload="metadata" muted loading="lazy" onclick="openModal('{{ file_url }}', '{{ file.type }}')">
655
+ <source src="{{ file_url }}" type="video/mp4"> {# Use variable for URL #}
656
+ Ваш браузер не поддерживает предпросмотр видео.
657
  </video>
658
+ {% elif file.type == 'image' %}
659
+ <img class="file-preview" src="{{ file_url }}" alt="{{ file.original_name }}" loading="lazy" onclick="openModal('{{ file_url }}', '{{ file.type }}')">
660
+ {% elif file.type == 'pdf' %}
661
+ <div class="file-preview-icon" onclick="openModal('{{ file_url }}', '{{ file.type }}')">
662
+ <i class="fas fa-file-pdf"></i>
663
+ </div>
664
+ {% elif file.type == 'text' %}
665
+ <div class="file-preview-icon" onclick="openModal('{{ file_url }}', '{{ file.type }}')">
666
+ <i class="fas fa-file-alt"></i>
667
+ </div>
668
  {% else %}
669
+ <div class="file-preview-icon" onclick="alert('Предпросмотр для этого типа файла не поддерживается.')">
670
+ <i class="fas fa-file"></i> {# Generic file icon #}
671
+ </div>
672
  {% endif %}
673
+ <p style="font-size: 0.8em; color: #666;">{{ file.upload_date }}</p> {# Smaller date #}
674
+ {# Use original_name for the download attribute #}
675
+ <a href="{{ url_for('download_file', file_path=file.path, filename=file.original_name) }}" class="btn download-btn">Скачать</a>
676
+ <a href="{{ url_for('delete_file', file_path=file.path) }}" class="btn delete-btn" onclick="return confirm('Вы уверены, что хотите удалить файл \'{{ file.original_name }}\'?');">Удалить</a>
677
  </div>
678
  {% endfor %}
679
  {% if not user_files %}
 
682
  </div>
683
  <h2 style="margin-top: 30px;">Добавить на главный экран</h2>
684
  <p>Для быстрого доступа к Zeus Cloud, вы можете добавить это приложение на главный экран вашего телефона:</p>
685
+ <div style="display: flex; flex-wrap: wrap; gap: 20px; margin-top: 15px;">
686
+ <div style="flex: 1; min-width: 250px;">
687
+ <h3><i class="fab fa-android"></i> Android:</h3>
688
+ <ol>
689
+ <li>Откройте Zeus Cloud в браузере Chrome.</li>
690
+ <li>Нажмите на меню <i class="fas fa-ellipsis-v"></i> (обычно три точки).</li>
691
+ <li>Выберите <strong>"Установить приложение"</strong> или <strong>"Добавить на главный экран"</strong>.</li>
692
+ <li>Подтвердите добавление.</li>
693
+ </ol>
694
+ </div>
695
+ <div style="flex: 1; min-width: 250px;">
696
+ <h3><i class="fab fa-apple"></i> iOS (iPhone/iPad):</h3>
697
+ <ol>
698
+ <li>Откройте Zeus Cloud в браузере Safari.</li>
699
+ <li>Нажмите кнопку <strong>"Поделиться"</strong> <i class="fas fa-external-link-square-alt"></i> (квадрат со стрелкой).</li>
700
+ <li>Прокрутите вниз и выберите <strong>"На экран «Домой»"</strong>.</li>
701
+ <li>Нажмите <strong>"Добавить"</strong>.</li>
702
+ </ol>
703
+ </div>
704
  </div>
705
+ <a href="{{ url_for('logout') }}" class="btn" style="margin-top: 30px; background-color: var(--accent);" id="logout-btn">Выйти</a>
706
  </div>
707
  <div class="modal" id="mediaModal" onclick="closeModal(event)">
708
+ <div class="modal-content" id="modalContent"></div>
709
  </div>
710
  <script>
711
+ const HF_TOKEN_READ = "{{ hf_token_read or '' }}"; // Pass read token to JS if available
712
+
713
+ function openModal(src, fileType) {
714
  const modal = document.getElementById('mediaModal');
715
  const modalContent = document.getElementById('modalContent');
716
+ modalContent.innerHTML = ''; // Clear previous content
717
+
718
+ // Add token to URL if needed and not already present
719
+ let finalSrc = src;
720
+ if (HF_TOKEN_READ && src.indexOf('token=') === -1) {
721
+ finalSrc += (src.indexOf('?') === -1 ? '?' : '&') + 'token=' + HF_TOKEN_READ;
722
+ }
723
+
724
+ if (fileType === 'video') {
725
+ modalContent.innerHTML = `<video controls autoplay><source src="${finalSrc}" type="video/mp4">Ваш браузер не поддерживает это видео.</video>`;
726
+ } else if (fileType === 'image') {
727
+ modalContent.innerHTML = `<img src="${finalSrc}" alt="Предпросмотр изображения">`;
728
+ } else if (fileType === 'pdf' || fileType === 'text') {
729
+ // Use iframe for PDF and Text preview
730
+ modalContent.innerHTML = `<iframe src="${finalSrc}"></iframe>`;
731
  } else {
732
+ alert('Предпросмотр для этого типа файла не поддерживается.');
733
+ return; // Don't open modal for unsupported types
734
  }
735
  modal.style.display = 'flex';
736
  }
737
+
738
  function closeModal(event) {
739
  const modal = document.getElementById('mediaModal');
740
+ // Close only if clicking on the background (the modal itself)
741
  if (event.target === modal) {
742
  modal.style.display = 'none';
743
  const video = modal.querySelector('video');
744
  if (video) {
745
  video.pause();
746
+ video.src = ''; // Stop loading video
747
+ }
748
+ const iframe = modal.querySelector('iframe');
749
+ if (iframe) {
750
+ iframe.src = 'about:blank'; // Clear iframe
751
  }
752
+ document.getElementById('modalContent').innerHTML = ''; // Clear content
753
  }
754
  }
755
 
 
757
  const progressBar = document.getElementById('progress-bar');
758
  const progressContainer = document.getElementById('progress-container');
759
  const uploadBtn = document.getElementById('upload-btn');
760
+ const fileInput = form.querySelector('input[type="file"]');
761
 
762
  form.addEventListener('submit', function(e) {
763
+ e.preventDefault(); // Prevent default form submission
764
+
765
+ const files = fileInput.files;
 
 
 
766
  if (files.length === 0) {
767
  alert('Пожалуйста, выберите файлы для загрузки.');
768
  return;
769
  }
770
+ if (files.length > 20) {
771
+ alert('Ма��симум 20 файлов за раз!');
772
+ return;
773
+ }
774
 
775
  const formData = new FormData(form);
776
  progressContainer.style.display = 'block';
777
  progressBar.style.width = '0%';
778
  uploadBtn.disabled = true;
779
+ uploadBtn.textContent = 'Загрузка...';
780
 
781
  const xhr = new XMLHttpRequest();
782
+ xhr.open('POST', '{{ url_for("dashboard") }}', true);
783
+ // Required for Flask to know it's an AJAX request (optional but good practice)
784
+ xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
785
 
786
  xhr.upload.onprogress = function(event) {
787
  if (event.lengthComputable) {
788
+ const percent = Math.round((event.loaded / event.total) * 100);
789
  progressBar.style.width = percent + '%';
790
+ progressBar.textContent = percent + '%'; // Show percentage text
791
  }
792
  };
793
 
794
+ xhr.onload = function() {
795
+ uploadBtn.disabled = false;
796
+ uploadBtn.textContent = 'Загрузить файлы';
797
  progressContainer.style.display = 'none';
798
  progressBar.style.width = '0%';
799
+ progressBar.textContent = ''; // Clear percentage text
800
+
801
+ if (xhr.status === 200) {
802
+ // Attempt to parse JSON response for more detailed feedback (optional)
803
+ try {
804
+ const response = JSON.parse(xhr.responseText);
805
+ // We could potentially update the file list dynamically here
806
+ // For now, just reload to show flash messages and updated list
807
+ location.reload();
808
+ } catch (err) {
809
+ // If response wasn't JSON or error, still reload
810
+ location.reload();
811
+ }
812
+ } else {
813
+ // Handle server errors (e.g., 500)
814
+ alert('Ошибка сервера при загрузке файлов. Код: ' + xhr.status);
815
  }
816
+ };
817
 
818
  xhr.onerror = function() {
819
+ alert('Ошибка сети при загрузке файлов.');
820
  progressContainer.style.display = 'none';
821
  progressBar.style.width = '0%';
822
+ progressBar.textContent = '';
823
  uploadBtn.disabled = false;
824
+ uploadBtn.textContent = 'Загрузить файлы';
825
  };
826
 
827
  xhr.send(formData);
 
830
  document.getElementById('logout-btn').addEventListener('click', function(e) {
831
  e.preventDefault();
832
  localStorage.removeItem('zeusCredentials');
833
+ window.location.href = '{{ url_for("logout") }}';
834
  });
835
  </script>
836
  </body>
837
  </html>
838
  '''
839
+ return render_template_string(html, username=username, user_files=user_files, repo_id=REPO_ID, hf_token_read=HF_TOKEN_READ)
840
 
 
 
 
 
 
841
 
842
+ @app.route('/download/<path:file_path>/<filename>')
843
+ def download_file(file_path, filename):
844
+ if 'username' not in session and not (request.referrer and 'admhosto' in request.referrer):
845
+ flash('Пожалуйста, войдите в систему!')
846
+ return redirect(url_for('login'))
847
+
848
+ current_username = session.get('username')
849
+ is_admin_request = request.referrer and 'admhosto' in request.referrer
850
+
851
+ # Basic check: if it's not an admin request, the user must be logged in
852
+ if not is_admin_request and not current_username:
853
+ flash('Доступ запрещен.')
854
+ return redirect(url_for('login'))
855
+
856
  data = load_data()
 
 
 
 
857
 
858
+ # Determine the owner of the file from the path
859
+ try:
860
+ file_owner = file_path.split('/')[1]
861
+ except IndexError:
862
+ flash('Неверный путь к файлу.')
863
+ return redirect(request.referrer or url_for('dashboard'))
864
+
865
+ # Permission check
866
+ if not is_admin_request:
867
+ # Regular user request: check if they own the file
868
+ if current_username != file_owner:
869
  flash('У вас нет доступа к этому файлу!')
870
  return redirect(url_for('dashboard'))
871
+ # Verify the file exists in their list (optional, but good practice)
872
+ if not any(f['path'] == file_path for f in data.get('users', {}).get(current_username, {}).get('files', [])):
873
+ flash('Файл не найден в вашем аккаунте.')
874
+ return redirect(url_for('dashboard'))
875
+ # If it's an admin request, we allow the download (assuming admin is verified elsewhere or implicitly via URL)
876
 
877
+ # Construct download URL
878
  file_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{file_path}?download=true"
879
  try:
 
880
  headers = {}
881
  if HF_TOKEN_READ:
882
  headers["authorization"] = f"Bearer {HF_TOKEN_READ}"
 
884
  response = requests.get(file_url, headers=headers, stream=True)
885
  response.raise_for_status()
886
 
887
+ # Use BytesIO to handle the content in memory
888
  file_content = BytesIO(response.content)
889
+
890
+ # Send the file using the original filename provided in the URL
891
  return send_file(
892
  file_content,
893
  as_attachment=True,
894
+ download_name=filename, # Use the passed filename for download
895
  mimetype='application/octet-stream'
896
  )
897
  except requests.exceptions.RequestException as e:
898
+ logging.error(f"Error downloading file '{filename}' ({file_path}) from HF: {e}")
899
+ status_code = e.response.status_code if e.response is not None else 'N/A'
900
+ if status_code == 404:
901
+ flash(f'Ошибка скачивания: Файл "{filename}" не найден на сервере.')
902
  else:
903
+ flash(f'Ошибка скачивания файла "{filename}" (код: {status_code}).')
904
+ return redirect(request.referrer or url_for('dashboard'))
905
  except Exception as e:
906
+ logging.error(f"Unexpected error during download of '{filename}' ({file_path}): {e}")
907
+ flash(f'Произошла непредвиденная ошибка при скачивании файла "{filename}".')
908
+ return redirect(request.referrer or url_for('dashboard'))
909
+
 
 
910
 
911
  @app.route('/delete/<path:file_path>')
912
  def delete_file(file_path):
 
921
  flash('Пользователь не найден!')
922
  return redirect(url_for('login'))
923
 
924
+ user_files = data['users'][username].get('files', [])
925
  file_to_delete = next((file for file in user_files if file['path'] == file_path), None)
926
 
927
  if not file_to_delete:
928
  flash('Файл не найден!')
929
  return redirect(url_for('dashboard'))
930
 
931
+ if not HF_TOKEN_WRITE:
932
+ flash('Удаление невозможно: отсутствует токен Hugging Face для записи.')
933
+ return redirect(url_for('dashboard'))
934
+
935
+ original_name = file_to_delete.get('original_name', 'N/A') # Get original name for message
936
+
937
  try:
938
  api = HfApi()
939
  api.delete_file(
 
941
  repo_id=REPO_ID,
942
  repo_type="dataset",
943
  token=HF_TOKEN_WRITE,
944
+ commit_message=f"Deleted {file_path} for {username}"
945
  )
946
+ # Update data only after successful deletion from HF
947
  data['users'][username]['files'] = [f for f in user_files if f['path'] != file_path]
948
  save_data(data)
949
+ flash(f'Файл "{original_name}" успешн�� удален!')
950
  except Exception as e:
951
+ logging.error(f"Error deleting file {file_path} for {username}: {e}")
952
+ flash(f'Ошибка удаления файла "{original_name}"!')
953
 
954
  return redirect(url_for('dashboard'))
955
 
956
  @app.route('/logout')
957
  def logout():
958
  session.pop('username', None)
959
+ flash('Вы вышли из системы.')
960
+ # Redirecting to login page handles clearing localStorage via its JS
961
  return redirect(url_for('login'))
962
 
963
+
964
  @app.route('/admhosto')
965
  def admin_panel():
966
+ # Add proper admin authentication here if needed
967
  data = load_data()
968
+ users = data.get('users', {})
969
 
970
  html = '''
971
  <!DOCTYPE html>
 
975
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
976
  <title>Админ-панель - Zeus Cloud</title>
977
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
978
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
979
  <style>''' + BASE_STYLE + '''</style>
980
  </head>
981
  <body>
 
989
  <p>Дата регистрации: {{ user_data.get('created_at', 'N/A') }}</p>
990
  <p>Количество файлов: {{ user_data.get('files', []) | length }}</p>
991
  <form method="POST" action="{{ url_for('admin_delete_user', username=username) }}" style="display: inline; margin-left: 10px;" onsubmit="return confirm('Вы уверены, что хотите удалить пользователя {{ username }} и все его файлы? Это действие необратимо!');">
992
+ <button type="submit" class="btn delete-btn" style="padding: 5px 10px; font-size: 0.9em;">Удалить пользователя</button>
993
  </form>
994
  </div>
995
  {% endfor %}
 
1014
 
1015
  @app.route('/admhosto/user/<username>')
1016
  def admin_user_files(username):
1017
+ # Add proper admin authentication here if needed
1018
  data = load_data()
1019
+ if username not in data.get('users', {}):
1020
  flash('Пользователь не найден!')
1021
  return redirect(url_for('admin_panel'))
1022
 
 
1030
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
1031
  <title>Файлы пользователя {{ username }} - Zeus Cloud</title>
1032
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
1033
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
1034
  <style>''' + BASE_STYLE + '''</style>
1035
  </head>
1036
  <body>
1037
  <div class="container">
1038
  <h1>Файлы пользователя: {{ username }}</h1>
1039
+ <a href="{{ url_for('admin_panel') }}" class="btn" style="margin-bottom: 20px; background-color: var(--accent);">Назад к списку пользователей</a>
1040
  {% with messages = get_flashed_messages() %}
1041
  {% if messages %}
1042
  {% for message in messages %}
 
1047
  <div class="file-grid">
1048
  {% for file in user_files %}
1049
  <div class="file-item">
1050
+ <p class="filename" title="{{ file.original_name }}">{{ file.original_name }}</p>
1051
+ {% set file_url = "https://huggingface.co/datasets/" + repo_id + "/resolve/main/" + file.path + "?token=" + hf_token_read if hf_token_read else "https://huggingface.co/datasets/" + repo_id + "/resolve/main/" + file.path %}
1052
+ {% if file.type == 'video' %}
1053
+ <video class="file-preview" preload="metadata" muted loading="lazy" onclick="openModal('{{ file_url }}', '{{ file.type }}')">
1054
+ <source src="{{ file_url }}" type="video/mp4">
1055
+ </video>
1056
+ {% elif file.type == 'image' %}
1057
+ <img class="file-preview" src="{{ file_url }}" alt="{{ file.original_name }}" loading="lazy" onclick="openModal('{{ file_url }}', '{{ file.type }}')">
1058
+ {% elif file.type == 'pdf' %}
1059
+ <div class="file-preview-icon" onclick="openModal('{{ file_url }}', '{{ file.type }}')"><i class="fas fa-file-pdf"></i></div>
1060
+ {% elif file.type == 'text' %}
1061
+ <div class="file-preview-icon" onclick="openModal('{{ file_url }}', '{{ file.type }}')"><i class="fas fa-file-alt"></i></div>
1062
+ {% else %}
1063
+ <div class="file-preview-icon" onclick="alert('Предпросмотр для этого типа файла не поддерживается.')"><i class="fas fa-file"></i></div>
1064
+ {% endif %}
1065
+ <p style="font-size: 0.8em; color: #666;">{{ file.get('upload_date', 'N/A') }}</p>
1066
+ <a href="{{ url_for('download_file', file_path=file.path, filename=file.original_name) }}" class="btn download-btn">Скачать</a>
1067
+ <form method="POST" action="{{ url_for('admin_delete_file', username=username, file_path=file.path) }}" style="display: inline;" onsubmit="return confirm('Вы уверены, что хотите удалить файл \'{{ file.original_name }}\'?');">
1068
+ <button type="submit" class="btn delete-btn" style="padding: 10px 15px; font-size: 0.9em;">Удалить</button>
1069
  </form>
1070
  </div>
1071
  {% endfor %}
 
1073
  <p>У этого пользователя пока нет файлов.</p>
1074
  {% endif %}
1075
  </div>
1076
+
1077
  </div>
1078
  <div class="modal" id="mediaModal" onclick="closeModal(event)">
1079
+ <div class="modal-content" id="modalContent"></div>
1080
  </div>
1081
  <script>
1082
+ const HF_TOKEN_READ = "{{ hf_token_read or '' }}";
1083
+
1084
+ function openModal(src, fileType) {
1085
  const modal = document.getElementById('mediaModal');
1086
  const modalContent = document.getElementById('modalContent');
1087
+ modalContent.innerHTML = ''; // Clear previous content
1088
+
1089
+ let finalSrc = src;
1090
+ if (HF_TOKEN_READ && src.indexOf('token=') === -1) {
1091
+ finalSrc += (src.indexOf('?') === -1 ? '?' : '&') + 'token=' + HF_TOKEN_READ;
1092
+ }
1093
+
1094
+ if (fileType === 'video') {
1095
+ modalContent.innerHTML = `<video controls autoplay><source src="${finalSrc}" type="video/mp4"></video>`;
1096
+ } else if (fileType === 'image') {
1097
+ modalContent.innerHTML = `<img src="${finalSrc}">`;
1098
+ } else if (fileType === 'pdf' || fileType === 'text') {
1099
+ modalContent.innerHTML = `<iframe src="${finalSrc}"></iframe>`;
1100
  } else {
1101
+ alert('Предпросмотр для этого типа файла не поддерживается.');
1102
+ return;
1103
+ }
1104
+ modal.style.display = 'flex';
1105
  }
1106
+
1107
  function closeModal(event) {
1108
  const modal = document.getElementById('mediaModal');
1109
  if (event.target === modal) {
1110
  modal.style.display = 'none';
1111
  const video = modal.querySelector('video');
1112
+ if (video) { video.pause(); video.src=''; }
1113
+ const iframe = modal.querySelector('iframe');
1114
+ if (iframe) { iframe.src = 'about:blank'; }
1115
  document.getElementById('modalContent').innerHTML = '';
1116
  }
1117
  }
 
1118
  </script>
1119
  </body>
1120
  </html>
1121
  '''
1122
+ return render_template_string(html, username=username, user_files=user_files, repo_id=REPO_ID, hf_token_read=HF_TOKEN_READ)
1123
+
1124
 
1125
  @app.route('/admhosto/delete_user/<username>', methods=['POST'])
1126
  def admin_delete_user(username):
1127
+ # Add proper admin authentication here
1128
+ if not HF_TOKEN_WRITE:
1129
+ flash('Удаление невозможно: отсутствует токен Hugging Face для записи.')
1130
+ return redirect(url_for('admin_panel'))
1131
+
1132
  data = load_data()
1133
+ if username not in data.get('users', {}):
1134
  flash('Пользователь не найден!')
1135
  return redirect(url_for('admin_panel'))
1136
 
1137
+ user_folder_path = f"cloud_files/{username}"
1138
+ logging.info(f"Admin attempting to delete user '{username}' and folder '{user_folder_path}'")
1139
 
1140
  try:
1141
  api = HfApi()
1142
+ # Attempt to delete the entire user folder from Hugging Face Hub
1143
+ api.delete_folder(
1144
+ folder_path=user_folder_path,
1145
+ repo_id=REPO_ID,
1146
+ repo_type="dataset",
1147
+ token=HF_TOKEN_WRITE,
1148
+ commit_message=f"Admin deleted folder {user_folder_path} for user {username}",
1149
+ ignore_patterns=None # Ensure all files are targeted
1150
+ )
1151
+ logging.info(f"Successfully deleted folder '{user_folder_path}' from HF Hub for user {username}.")
1152
+
1153
+ except Exception as e:
1154
+ # Log error if folder deletion fails, but proceed to delete user from DB
1155
+ # This handles cases where the folder might be empty or already partially deleted
1156
+ logging.error(f"Error deleting folder '{user_folder_path}' from HF Hub for user {username}: {e}. Proceeding to delete user from database.")
1157
+ # Optionally, could iterate and delete files individually here as a fallback
 
 
 
 
 
 
 
1158
 
1159
+ try:
1160
+ # Delete the user from the local data structure
1161
  del data['users'][username]
1162
  save_data(data)
1163
+ flash(f'Пользователь {username} и его файлы (попытка удаления с сервера) успешно удалены из базы данных!')
1164
+ logging.info(f"Admin successfully deleted user {username} from the database.")
1165
 
1166
  except Exception as e:
1167
+ logging.error(f"Error deleting user {username} from database after attempting HF deletion: {e}")
1168
+ flash(f'Ошибка при удалении пользователя {username} из базы данных!')
1169
 
1170
  return redirect(url_for('admin_panel'))
1171
 
1172
+
1173
  @app.route('/admhosto/delete_file/<username>/<path:file_path>', methods=['POST'])
1174
  def admin_delete_file(username, file_path):
1175
+ # Add proper admin authentication here
1176
+ if not HF_TOKEN_WRITE:
1177
+ flash('Удаление невозможно: отсутствует токен Hugging Face для записи.')
1178
+ return redirect(url_for('admin_user_files', username=username))
1179
+
1180
  data = load_data()
1181
+ if username not in data.get('users', {}):
1182
  flash('Пользователь не найден!')
1183
  return redirect(url_for('admin_panel'))
1184
 
1185
  user_files = data['users'][username].get('files', [])
1186
+ file_to_delete = next((f for f in user_files if f['path'] == file_path), None)
1187
+ original_name = file_to_delete.get('original_name', file_path) if file_to_delete else file_path
1188
 
1189
+ logging.info(f"Admin attempting to delete file '{file_path}' for user '{username}'")
 
1190
 
1191
  try:
1192
  api = HfApi()
 
1197
  token=HF_TOKEN_WRITE,
1198
  commit_message=f"Admin deleted file {file_path} for user {username}"
1199
  )
1200
+ logging.info(f"Successfully deleted file {file_path} from HF Hub.")
1201
+
1202
+ # Update the database only if the file was found there
1203
+ if file_to_delete:
1204
+ data['users'][username]['files'] = [f for f in user_files if f['path'] != file_path]
1205
+ save_data(data)
1206
+ logging.info(f"Removed file {file_path} from database for user {username}.")
1207
+ else:
1208
+ logging.warning(f"File {file_path} deleted from HF Hub by admin, but was not found in user {username}'s database list.")
1209
 
1210
+ flash(f'Файл "{original_name}" успешно удален!')
 
 
 
1211
 
1212
  except Exception as e:
1213
+ logging.error(f"Error deleting file {file_path} from HF Hub by admin: {e}")
1214
+ # Check if error is 'not found' - maybe already deleted?
1215
+ flash(f'Ошибка удаления файла "{original_name}"!')
1216
 
1217
  return redirect(url_for('admin_user_files', username=username))
1218
 
1219
+
1220
  if __name__ == '__main__':
1221
  if not HF_TOKEN_WRITE:
1222
+ logging.warning("!!! HF_TOKEN (write access) is NOT set. File uploads, deletions, and backups WILL FAIL.")
1223
  if not HF_TOKEN_READ:
1224
+ logging.warning("!!! HF_TOKEN_READ is NOT set. Falling back to HF_TOKEN if available. File downloads/previews might fail for private repos if no read token is present.")
1225
+
1226
+ if not os.path.exists(DATA_FILE):
1227
+ logging.info(f"{DATA_FILE} not found, attempting initial download...")
1228
+ download_db_from_hf() # Try to download on startup if missing
1229
 
1230
  if HF_TOKEN_WRITE:
1231
+ backup_thread = threading.Thread(target=periodic_backup, daemon=True)
1232
+ backup_thread.start()
1233
+ logging.info("Periodic backup thread started.")
1234
  else:
1235
+ logging.warning("Periodic backup thread NOT started because HF_TOKEN_WRITE is not set.")
1236
+
1237
+ app.run(debug=False, host='0.0.0.0', port=7860)
1238
 
1239
+ # --- END OF FILE app (8).py ---