Eluza133 commited on
Commit
f3a5554
·
verified ·
1 Parent(s): 0f13439

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +154 -224
app.py CHANGED
@@ -1,4 +1,4 @@
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, Response
4
  from flask_caching import Cache
@@ -17,7 +17,7 @@ import uuid
17
  app = Flask(__name__)
18
  app.secret_key = os.getenv("FLASK_SECRET_KEY", "supersecretkey_folders_unique")
19
  DATA_FILE = 'cloudeng_data_v2.json'
20
- REPO_ID = "Eluza133/Z1e1u" # Replace with your actual repo ID if different
21
  HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
22
  HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") or HF_TOKEN_WRITE
23
  UPLOAD_FOLDER = 'uploads'
@@ -26,22 +26,20 @@ os.makedirs(UPLOAD_FOLDER, exist_ok=True)
26
  cache = Cache(app, config={'CACHE_TYPE': 'simple'})
27
  logging.basicConfig(level=logging.INFO)
28
 
29
- # --- Filesystem Helper Functions ---
30
-
31
  def find_node_by_id(filesystem, node_id):
32
  if filesystem.get('id') == node_id:
33
- return filesystem, None # Node found, no parent (it's the root)
34
 
35
- queue = [(filesystem, None)] # (node, parent)
36
  while queue:
37
  current_node, parent = queue.pop(0)
38
  if current_node.get('type') == 'folder' and 'children' in current_node:
39
  for i, child in enumerate(current_node['children']):
40
  if child.get('id') == node_id:
41
- return child, current_node # Node found, return node and its parent
42
  if child.get('type') == 'folder':
43
  queue.append((child, current_node))
44
- return None, None # Node not found
45
 
46
  def add_node(filesystem, parent_id, node_data):
47
  parent_node, _ = find_node_by_id(filesystem, parent_id)
@@ -57,7 +55,6 @@ def remove_node(filesystem, node_id):
57
  if node_to_remove and parent_node and 'children' in parent_node:
58
  parent_node['children'] = [child for child in parent_node['children'] if child.get('id') != node_id]
59
  return True
60
- # Cannot remove root or if node/parent not found correctly
61
  return False
62
 
63
  def get_node_path_string(filesystem, node_id):
@@ -67,10 +64,10 @@ def get_node_path_string(filesystem, node_id):
67
  while current_id:
68
  node, parent = find_node_by_id(filesystem, current_id)
69
  if not node:
70
- break # Should not happen in a consistent tree
71
- if node.get('id') != 'root': # Don't add root name to path string
72
  path_list.append(node.get('name', node.get('original_filename', '')))
73
- if not parent: # Reached root's parent (None)
74
  break
75
  current_id = parent.get('id') if parent else None
76
 
@@ -85,37 +82,27 @@ def initialize_user_filesystem(user_data):
85
  "name": "root",
86
  "children": []
87
  }
88
- # Migrate old files if they exist
89
  if 'files' in user_data and isinstance(user_data['files'], list):
90
  for old_file in user_data['files']:
91
- # Attempt to create a somewhat unique ID based on old data, or generate new one
92
  file_id = old_file.get('id', uuid.uuid4().hex)
93
  original_filename = old_file.get('filename', 'unknown_file')
94
- # Assume old paths didn't have unique names or folder structure in the path itself
95
- # Create a unique name now for consistency
96
  name_part, ext_part = os.path.splitext(original_filename)
97
  unique_suffix = uuid.uuid4().hex[:8]
98
  unique_filename = f"{name_part}_{unique_suffix}{ext_part}"
99
- # Assume old files were at the 'root' level equivalent
100
- hf_path = f"cloud_files/{session['username']}/root/{unique_filename}" # New standard path
101
 
102
  file_node = {
103
  'type': 'file',
104
  'id': file_id,
105
  'original_filename': original_filename,
106
  'unique_filename': unique_filename,
107
- 'path': hf_path, # Store the intended *new* path structure
108
  'file_type': get_file_type(original_filename),
109
  'upload_date': old_file.get('upload_date', datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
110
  }
111
  add_node(user_data['filesystem'], 'root', file_node)
112
- # Note: This doesn't automatically rename/move files on HF Hub.
113
- # It just structures the metadata according to the new system.
114
- # Manual migration on HF Hub might be needed for old files to match `hf_path`.
115
- del user_data['files'] # Remove the old flat list
116
-
117
 
118
- # --- Data Handling ---
119
 
120
  @cache.memoize(timeout=300)
121
  def load_data():
@@ -127,9 +114,8 @@ def load_data():
127
  logging.warning("Data is not in dict format, initializing empty database")
128
  return {'users': {}}
129
  data.setdefault('users', {})
130
- # Initialize filesystem for users who don't have one (new structure)
131
  for username, user_data in data['users'].items():
132
- initialize_user_filesystem(user_data) # Use temporary session context if needed or pass username
133
  logging.info("Data successfully loaded and initialized")
134
  return data
135
  except Exception as e:
@@ -201,7 +187,7 @@ def download_db_from_hf():
201
  def periodic_backup():
202
  while True:
203
  upload_db_to_hf()
204
- time.sleep(1800) # 30 minutes
205
 
206
  def get_file_type(filename):
207
  filename_lower = filename.lower()
@@ -213,13 +199,9 @@ def get_file_type(filename):
213
  return 'pdf'
214
  elif filename_lower.endswith('.txt'):
215
  return 'text'
216
- # Add more types as needed
217
- # elif filename_lower.endswith(('.doc', '.docx')):
218
- # return 'document'
219
  return 'other'
220
 
221
 
222
- # --- Base Style (unchanged from original) ---
223
  BASE_STYLE = '''
224
  :root {
225
  --primary: #ff4d6d; --secondary: #00ddeb; --accent: #8b5cf6;
@@ -236,7 +218,9 @@ body.dark { background: var(--background-dark); color: var(--text-dark); }
236
  body.dark .container { background: var(--card-bg-dark); }
237
  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; }
238
  h2 { font-size: 1.5em; margin-top: 30px; color: var(--text-light); }
239
- body.dark h2 { color: var(--text-dark); }
 
 
240
  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); }
241
  body.dark input, body.dark textarea { color: var(--text-dark); }
242
  input:focus, textarea:focus { outline: none; box-shadow: 0 0 0 4px var(--primary); }
@@ -256,17 +240,16 @@ body.dark .user-item { background: var(--card-bg-dark); }
256
  .user-item:hover { transform: translateY(-5px); }
257
  .user-item a { color: var(--primary); text-decoration: none; font-weight: 600; }
258
  .user-item a:hover { color: var(--accent); }
259
- @media (max-width: 768px) { .file-grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); } }
260
- @media (max-width: 480px) { .file-grid { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); } }
261
  .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; }
262
  body.dark .item { background: var(--card-bg-dark); }
263
  .item:hover { transform: translateY(-5px); }
264
  .item-preview { max-width: 100%; height: 130px; object-fit: cover; border-radius: 10px; margin-bottom: 10px; cursor: pointer; display: block; }
265
- .item.folder .item-preview { object-fit: contain; font-size: 60px; color: var(--folder-color); line-height: 130px; } /* Folder icon styling */
266
  .item p { font-size: 0.9em; margin: 5px 0; word-break: break-all; }
267
  .item a { color: var(--primary); text-decoration: none; }
268
  .item a:hover { color: var(--accent); }
269
- .item-actions { margin-top: 10px; }
 
270
  .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; }
271
  .modal-content { max-width: 95%; max-height: 95%; background: #fff; padding: 10px; border-radius: 15px; overflow: auto; }
272
  body.dark .modal-content { background: var(--card-bg-dark); }
@@ -283,10 +266,45 @@ body.dark .modal pre { background: #2b2a33; color: var(--text-dark); }
283
  .folder-actions { margin-top: 20px; margin-bottom: 10px; display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
284
  .folder-actions input[type=text] { width: auto; flex-grow: 1; margin: 0; }
285
  .folder-actions .btn { margin: 0; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
286
  '''
287
 
288
- # --- Routes ---
289
-
290
  @app.route('/register', methods=['GET', 'POST'])
291
  def register():
292
  if request.method == 'POST':
@@ -307,7 +325,7 @@ def register():
307
  return redirect(url_for('register'))
308
 
309
  data['users'][username] = {
310
- 'password': password, # Plain text - consider hashing in production!
311
  'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
312
  'filesystem': {
313
  "type": "folder", "id": "root", "name": "root", "children": []
@@ -341,21 +359,18 @@ def login():
341
  data = load_data()
342
 
343
  user = data['users'].get(username)
344
- if user and user['password'] == password: # Plain text comparison
345
  session['username'] = username
346
- # Initialize filesystem if missing (for very old users before structure change)
347
  if 'filesystem' not in user:
348
  initialize_user_filesystem(user)
349
  try:
350
  save_data(data)
351
  except Exception as e:
352
  logging.error(f"Error saving data after filesystem init for {username}: {e}")
353
- # Proceed with login anyway, but log the error
354
  return jsonify({'status': 'success', 'redirect': url_for('dashboard')})
355
  else:
356
  return jsonify({'status': 'error', 'message': 'Неверное имя пользователя или пароль!'})
357
 
358
- # Login page HTML with JS for AJAX login and auto-login check
359
  html = '''
360
  <!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
361
  <title>Zeus Cloud</title><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
@@ -398,29 +413,24 @@ def dashboard():
398
  return redirect(url_for('login'))
399
 
400
  user_data = data['users'][username]
401
- # Ensure filesystem exists (should be handled by load_data/login now)
402
  if 'filesystem' not in user_data:
403
  initialize_user_filesystem(user_data)
404
- # No need to save here usually, but good practice if modified
405
 
406
  current_folder_id = request.args.get('folder_id', 'root')
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('Папка не найдена!')
411
- current_folder_id = 'root' # Reset to root
412
  current_folder, parent_folder = find_node_by_id(user_data['filesystem'], current_folder_id)
413
- if not current_folder: # Should always find root
414
- # This indicates a serious data structure issue
415
  logging.error(f"CRITICAL: Root folder not found for user {username}")
416
  flash('Критическая ошибка: корневая папка не найдена.')
417
  session.pop('username', None)
418
  return redirect(url_for('login'))
419
 
420
-
421
  items_in_folder = sorted(current_folder.get('children', []), key=lambda x: (x['type'] != 'folder', x.get('name', x.get('original_filename', '')).lower()))
422
 
423
-
424
  if request.method == 'POST':
425
  if not HF_TOKEN_WRITE:
426
  flash('Загрузка невозможна: токен для записи не настроен.')
@@ -440,7 +450,7 @@ def dashboard():
440
 
441
  if not target_folder_node or target_folder_node.get('type') != 'folder':
442
  flash('Целевая папка для загрузки не найдена!')
443
- return redirect(url_for('dashboard')) # Redirect to root dashboard
444
 
445
  api = HfApi()
446
  uploaded_count = 0
@@ -455,7 +465,7 @@ def dashboard():
455
  file_id = uuid.uuid4().hex
456
 
457
  hf_path = f"cloud_files/{username}/{target_folder_id}/{unique_filename}"
458
- temp_path = os.path.join(UPLOAD_FOLDER, f"{file_id}_{unique_filename}") # Temporary local path with unique ID
459
 
460
  try:
461
  file.save(temp_path)
@@ -482,22 +492,19 @@ def dashboard():
482
  if add_node(user_data['filesystem'], target_folder_id, file_info):
483
  uploaded_count += 1
484
  else:
485
- # This means add_node failed, which shouldn't happen if target_folder_node was found
486
  errors.append(f"Ошибка добавления метаданных для {original_filename}.")
487
  logging.error(f"Failed to add node metadata for file {file_id} to folder {target_folder_id} for user {username}")
488
- # Attempt to delete the orphaned file from HF Hub
489
  try:
490
  api.delete_file(path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE)
491
  except Exception as del_err:
492
  logging.error(f"Failed to delete orphaned file {hf_path} from HF Hub: {del_err}")
493
 
494
-
495
  except Exception as e:
496
  logging.error(f"Error uploading file {original_filename} for {username}: {e}")
497
  errors.append(f"Ошибка загрузки файла {original_filename}: {e}")
498
  finally:
499
  if os.path.exists(temp_path):
500
- os.remove(temp_path) # Clean up temp file
501
 
502
  if uploaded_count > 0:
503
  try:
@@ -509,24 +516,21 @@ def dashboard():
509
 
510
  if errors:
511
  for error_msg in errors:
512
- flash(error_msg, 'error') # Use 'error' category if your CSS handles it
513
 
514
  return redirect(url_for('dashboard', folder_id=target_folder_id))
515
 
516
-
517
- # Build breadcrumbs
518
  breadcrumbs = []
519
  temp_id = current_folder_id
520
  while temp_id:
521
  node, parent = find_node_by_id(user_data['filesystem'], temp_id)
522
  if not node: break
523
- is_link = (node['id'] != current_folder_id) # Don't link the current folder
524
  breadcrumbs.append({'id': node['id'], 'name': node.get('name', 'Root'), 'is_link': is_link})
525
  if not parent: break
526
  temp_id = parent.get('id')
527
  breadcrumbs.reverse()
528
 
529
-
530
  html = '''
531
  <!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
532
  <title>Панель управления - Zeus Cloud</title><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
@@ -574,10 +578,10 @@ def dashboard():
574
  <a href="{{ url_for('dashboard', folder_id=item.id) }}" class="item-preview" title="Перейти в папку {{ item.name }}">📁</a>
575
  <p><b>{{ item.name }}</b></p>
576
  <div class="item-actions">
577
- <a href="{{ url_for('dashboard', folder_id=item.id) }}" class="btn folder-btn" style="font-size: 0.9em; padding: 5px 10px;">Открыть</a>
578
  <form method="POST" action="{{ url_for('delete_folder', folder_id=item.id) }}" style="display: inline;" onsubmit="return confirm('Удалить папку {{ item.name }}? Папку можно удалить только если она пуста.');">
579
  <input type="hidden" name="current_view_folder_id" value="{{ current_folder_id }}">
580
- <button type="submit" class="btn delete-btn" style="font-size: 0.9em; padding: 5px 10px;">Удалить</button>
581
  </form>
582
  </div>
583
  {% elif item.type == 'file' %}
@@ -601,14 +605,14 @@ def dashboard():
601
  <p title="{{ item.original_filename }}">{{ item.original_filename | truncate(25, True) }}</p>
602
  <p style="font-size: 0.8em; color: #888;">{{ item.upload_date }}</p>
603
  <div class="item-actions">
604
- <a href="{{ url_for('download_file', file_id=item.id) }}" class="btn download-btn" style="font-size: 0.9em; padding: 5px 10px;">Скачать</a>
605
  {% if previewable %}
606
- <button class="btn" style="font-size: 0.9em; padding: 5px 10px; background: var(--accent);"
607
  onclick="openModal('{{ hf_file_url(item.path) if item.file_type != 'text' else url_for('get_text_content', file_id=item.id) }}', '{{ item.file_type }}', '{{ item.id }}')">Просмотр</button>
608
  {% endif %}
609
  <form method="POST" action="{{ url_for('delete_file', file_id=item.id) }}" style="display: inline;" onsubmit="return confirm('Вы уверены, что хотите удалить файл {{ item.original_filename }}?');">
610
  <input type="hidden" name="current_view_folder_id" value="{{ current_folder_id }}">
611
- <button type="submit" class="btn delete-btn" style="font-size: 0.9em; padding: 5px 10px;">Удалить</button>
612
  </form>
613
  </div>
614
  {% endif %}
@@ -617,8 +621,25 @@ def dashboard():
617
  {% if not items %} <p>Эта папка пуста.</p> {% endif %}
618
  </div>
619
 
620
- <h2 style="margin-top: 30px;">Добавить на главный экран</h2>
621
- <p>Инструкции по добавлению на главный экран...</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
622
  <a href="{{ url_for('logout') }}" class="btn" style="margin-top: 20px;" id="logout-btn">Выйти</a>
623
  </div>
624
 
@@ -636,16 +657,13 @@ def dashboard():
636
  function hfFileUrl(path, download = false) {
637
  let url = `https://huggingface.co/datasets/${repoId}/resolve/main/${path}`;
638
  if (download) url += '?download=true';
639
- // Note: Adding token directly in URL is generally discouraged for security.
640
- // Pre-signed URLs or backend proxying is better if needed.
641
- // For simplicity here, we rely on browser session / token auth if repo is private.
642
  return url;
643
  }
644
 
645
  async function openModal(srcOrUrl, type, itemId) {
646
  const modal = document.getElementById('mediaModal');
647
  const modalContent = document.getElementById('modalContent');
648
- modalContent.innerHTML = '<p>Загрузка...</p>'; // Loading indicator
649
  modal.style.display = 'flex';
650
 
651
  try {
@@ -656,11 +674,9 @@ def dashboard():
656
  } else if (type === 'pdf') {
657
  modalContent.innerHTML = `<iframe src="${srcOrUrl}" title="Просмотр PDF"></iframe>`;
658
  } else if (type === 'text') {
659
- // Fetch text content from backend endpoint
660
- const response = await fetch(srcOrUrl); // srcOrUrl is the Flask endpoint URL here
661
  if (!response.ok) throw new Error(`Ошибка загрузки текста: ${response.statusText}`);
662
  const text = await response.text();
663
- // Escape HTML to prevent XSS
664
  const escapedText = text.replace(/</g, "<").replace(/>/g, ">");
665
  modalContent.innerHTML = `<pre>${escapedText}</pre>`;
666
  } else {
@@ -674,7 +690,6 @@ def dashboard():
674
 
675
  function closeModal(event) {
676
  const modal = document.getElementById('mediaModal');
677
- // Close if clicked on the background (modal itself)
678
  if (event.target === modal) {
679
  closeModalManual();
680
  }
@@ -686,7 +701,7 @@ def dashboard():
686
  const video = modal.querySelector('video');
687
  if (video) video.pause();
688
  const iframe = modal.querySelector('iframe');
689
- if (iframe) iframe.src = 'about:blank'; // Stop PDF loading
690
  document.getElementById('modalContent').innerHTML = '';
691
  }
692
 
@@ -696,7 +711,6 @@ def dashboard():
696
  const uploadBtn = document.getElementById('upload-btn');
697
 
698
  form.addEventListener('submit', function(e) {
699
- // Basic client-side validation (optional, backend validation is key)
700
  const files = form.querySelector('input[type="file"]').files;
701
  if (files.length === 0) {
702
  alert('Пожалуйста, выберите файлы для загрузки.');
@@ -708,66 +722,11 @@ def dashboard():
708
  e.preventDefault();
709
  return;
710
  }
711
- // Prevent default only if using AJAX, otherwise let the form submit normally
712
- // Using normal form submission for simplicity now, progress bar won't work accurately without AJAX
713
- // e.preventDefault(); // Uncomment this and implement AJAX below for progress bar
714
-
715
- // Show progress indication (even if not accurate for standard POST)
716
- progressContainer.style.display = 'block';
717
- progressBar.style.width = '50%'; // Indicate activity
718
- uploadBtn.disabled = true;
719
- uploadBtn.textContent = 'Загрузка...';
720
 
721
- // --- AJAX Upload Implementation (Optional, replaces standard form submit) ---
722
- /*
723
- e.preventDefault(); // Keep this line if using AJAX
724
- const formData = new FormData(form);
725
  progressContainer.style.display = 'block';
726
- progressBar.style.width = '0%';
727
  uploadBtn.disabled = true;
728
  uploadBtn.textContent = 'Загрузка...';
729
-
730
- const xhr = new XMLHttpRequest();
731
- const targetFolderId = form.querySelector('input[name="current_folder_id"]').value;
732
- xhr.open('POST', `/dashboard?folder_id=${targetFolderId}`, true); // Post to the dashboard URL
733
-
734
- xhr.upload.onprogress = function(event) {
735
- if (event.lengthComputable) {
736
- const percent = (event.loaded / event.total) * 100;
737
- progressBar.style.width = percent + '%';
738
- }
739
- };
740
-
741
- xhr.onload = function() {
742
- uploadBtn.disabled = false;
743
- uploadBtn.textContent = 'Загрузить файлы сюда';
744
- progressContainer.style.display = 'none';
745
- progressBar.style.width = '0%';
746
- if (xhr.status >= 200 && xhr.status < 300) {
747
- // Success - Reload the page to see changes
748
- // More advanced: parse response and update DOM dynamically
749
- window.location.reload();
750
- } else {
751
- // Error
752
- alert(`Ошибка загрузки: ${xhr.statusText || 'Неизвестная ошибка'}`);
753
- // Try to parse error from response if available
754
- try {
755
- const response = JSON.parse(xhr.responseText);
756
- if(response.message) alert(response.message);
757
- } catch(e) {}
758
- }
759
- };
760
-
761
- xhr.onerror = function() {
762
- alert('Ошибка сети во время загрузки!');
763
- progressContainer.style.display = 'none';
764
- progressBar.style.width = '0%';
765
- uploadBtn.disabled = false;
766
- uploadBtn.textContent = 'Загрузить файлы сюда';
767
- };
768
-
769
- xhr.send(formData);
770
- */
771
  });
772
 
773
  document.getElementById('logout-btn').addEventListener('click', function(e) {
@@ -778,7 +737,7 @@ def dashboard():
778
 
779
  </script>
780
  </body></html>'''
781
- # Pass helper function to template context
782
  template_context = {
783
  'username': username,
784
  'items': items_in_folder,
@@ -788,7 +747,8 @@ def dashboard():
788
  'repo_id': REPO_ID,
789
  'HF_TOKEN_READ': HF_TOKEN_READ,
790
  'hf_file_url': lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{path}{'?download=true' if download else ''}",
791
- 'os': os # For admin template usage if needed, be cautious
 
792
  }
793
  return render_template_string(html, **template_context)
794
 
@@ -810,7 +770,6 @@ def create_folder():
810
  if not folder_name:
811
  flash('Имя п��пки не может быть пустым!')
812
  return redirect(url_for('dashboard', folder_id=parent_folder_id))
813
- # Basic validation for folder names (avoiding problematic chars)
814
  if not folder_name.isalnum() and '_' not in folder_name and ' ' not in folder_name:
815
  flash('Имя папки может содержать буквы, цифры, пробелы и подчеркивания.')
816
  return redirect(url_for('dashboard', folder_id=parent_folder_id))
@@ -830,7 +789,6 @@ def create_folder():
830
  except Exception as e:
831
  flash('Ошибка сохранения данных при создании папки.')
832
  logging.error(f"Create folder save error: {e}")
833
- # Attempt to rollback? Difficult without transactions.
834
  else:
835
  flash('Не удалось найти родительскую папку.')
836
 
@@ -846,28 +804,46 @@ def download_file(file_id):
846
  username = session['username']
847
  data = load_data()
848
  user_data = data['users'].get(username)
849
- if not user_data:
850
- flash('Пользователь не найден!')
851
- session.pop('username', None)
852
- return redirect(url_for('login'))
853
-
854
- file_node, _ = find_node_by_id(user_data['filesystem'], file_id)
855
 
856
- # Also check admin access if needed
857
  is_admin_route = request.referrer and 'admhosto' in request.referrer
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
858
  if not file_node or file_node.get('type') != 'file':
859
- # Check if admin is trying to download from admin panel
860
- if is_admin_route:
861
- # Admin might be trying to download a file listed in the panel
862
- # Need to search across all users if the file_id is globally unique
863
- # Or, the admin route should pass username context
864
- # For now, assume admin routes provide enough context elsewhere or this check fails
865
- flash('(Admin) Файл не найден в структуре пользователя.')
866
- # Redirect back to admin panel or specific user view if possible
867
- return redirect(request.referrer or url_for('admin_panel'))
868
- else:
869
- flash('Файл не найден!')
870
- return redirect(url_for('dashboard')) # Redirect user to their root
871
 
872
  hf_path = file_node.get('path')
873
  original_filename = file_node.get('original_filename', 'downloaded_file')
@@ -896,7 +872,6 @@ def download_file(file_id):
896
  except requests.exceptions.RequestException as e:
897
  logging.error(f"Error downloading file from HF ({hf_path}): {e}")
898
  flash(f'Ошибка скачивания файла {original_filename}! ({e})')
899
- # Determine redirect target
900
  return redirect(request.referrer or url_for('dashboard'))
901
  except Exception as e:
902
  logging.error(f"Unexpected error during download ({hf_path}): {e}")
@@ -919,7 +894,7 @@ def delete_file(file_id):
919
  return redirect(url_for('login'))
920
 
921
  file_node, parent_node = find_node_by_id(user_data['filesystem'], file_id)
922
- current_view_folder_id = request.form.get('current_view_folder_id', 'root') # Where to redirect back
923
 
924
  if not file_node or file_node.get('type') != 'file' or not parent_node:
925
  flash('Файл не найден или не может быть удален.')
@@ -930,7 +905,6 @@ def delete_file(file_id):
930
 
931
  if not hf_path:
932
  flash(f'Ошибка: Путь к файлу {original_filename} не найден. Удаление только из базы.')
933
- # Proceed to remove from DB only
934
  if remove_node(user_data['filesystem'], file_id):
935
  try:
936
  save_data(data)
@@ -956,7 +930,6 @@ def delete_file(file_id):
956
  )
957
  logging.info(f"Deleted file {hf_path} from HF Hub for user {username}")
958
 
959
- # Now remove from DB
960
  if remove_node(user_data['filesystem'], file_id):
961
  try:
962
  save_data(data)
@@ -1004,7 +977,7 @@ def delete_folder(folder_id):
1004
  return redirect(url_for('login'))
1005
 
1006
  folder_node, parent_node = find_node_by_id(user_data['filesystem'], folder_id)
1007
- current_view_folder_id = request.form.get('current_view_folder_id', 'root') # Where to redirect back
1008
 
1009
  if not folder_node or folder_node.get('type') != 'folder' or not parent_node:
1010
  flash('Папка не найдена или не может быть удалена.')
@@ -1012,26 +985,20 @@ def delete_folder(folder_id):
1012
 
1013
  folder_name = folder_node.get('name', 'папка')
1014
 
1015
- # Check if folder is empty
1016
  if folder_node.get('children'):
1017
  flash(f'Папку "{folder_name}" можно удалить только если она пуста.')
1018
  return redirect(url_for('dashboard', folder_id=current_view_folder_id))
1019
 
1020
- # Folder is empty, proceed with deletion from DB
1021
  if remove_node(user_data['filesystem'], folder_id):
1022
  try:
1023
  save_data(data)
1024
  flash(f'Пустая папка "{folder_name}" успешно удалена.')
1025
- # Note: We don't delete anything from HF Hub here, as empty folders don't really exist there.
1026
- # Files inside folders are deleted individually via delete_file.
1027
  except Exception as e:
1028
  flash('Ошибка сохранения данных после удаления папки.')
1029
  logging.error(f"Delete empty folder save error: {e}")
1030
  else:
1031
  flash('Не удалось удалить папку из базы данных.')
1032
 
1033
-
1034
- # Redirect to the parent folder after deletion
1035
  redirect_to_folder_id = parent_node.get('id', 'root')
1036
  return redirect(url_for('dashboard', folder_id=redirect_to_folder_id))
1037
 
@@ -1066,11 +1033,9 @@ def get_text_content(file_id):
1066
  response = requests.get(file_url, headers=headers)
1067
  response.raise_for_status()
1068
 
1069
- # Limit file size to prevent server overload (e.g., 1MB)
1070
  if len(response.content) > 1 * 1024 * 1024:
1071
  return Response("Файл слишком большой для предпросмотра.", status=413)
1072
 
1073
- # Try decoding with UTF-8, fallback to latin-1 or others if needed
1074
  try:
1075
  text_content = response.content.decode('utf-8')
1076
  except UnicodeDecodeError:
@@ -1083,7 +1048,7 @@ def get_text_content(file_id):
1083
 
1084
  except requests.exceptions.RequestException as e:
1085
  logging.error(f"Error fetching text content from HF ({hf_path}): {e}")
1086
- return Response(f"Ошибка загрузки содержимого: {e}", status=502) # Bad Gateway or appropriate error
1087
  except Exception as e:
1088
  logging.error(f"Unexpected error fetching text content ({hf_path}): {e}")
1089
  return Response("Внутренняя ошибка сервера", status=500)
@@ -1092,17 +1057,12 @@ def get_text_content(file_id):
1092
  @app.route('/logout')
1093
  def logout():
1094
  session.pop('username', None)
1095
- # JS on login page handles localStorage removal upon redirection
1096
  flash('Вы успешно вышли из системы.')
1097
  return redirect(url_for('login'))
1098
 
1099
 
1100
- # --- Admin Routes (Simplified - Add Auth Check) ---
1101
-
1102
  def is_admin():
1103
- # Implement proper admin check (e.g., check session for specific admin user/role)
1104
- # For now, allow access if logged in (INSECURE - REPLACE THIS)
1105
- return 'username' in session # Placeholder - VERY INSECURE
1106
 
1107
  @app.route('/admhosto')
1108
  def admin_panel():
@@ -1112,7 +1072,6 @@ def admin_panel():
1112
  data = load_data()
1113
  users = data.get('users', {})
1114
 
1115
- # Calculate total files per user
1116
  user_details = []
1117
  for uname, udata in users.items():
1118
  file_count = 0
@@ -1131,7 +1090,7 @@ def admin_panel():
1131
  })
1132
 
1133
  html = '''
1134
- <!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><title>Админ-панель</title>
1135
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
1136
  <style>''' + BASE_STYLE + '''</style></head><body><div class="container"><h1>Админ-панель</h1>
1137
  {% with messages = get_flashed_messages() %}{% if messages %}{% for message in messages %}<div class="flash">{{ message }}</div>{% endfor %}{% endif %}{% endwith %}
@@ -1141,8 +1100,8 @@ def admin_panel():
1141
  <a href="{{ url_for('admin_user_files', username=user.username) }}">{{ user.username }}</a>
1142
  <p>Зарегистрирован: {{ user.created_at }}</p>
1143
  <p>Файлов: {{ user.file_count }}</p>
1144
- <form method="POST" action="{{ url_for('admin_delete_user', username=user.username) }}" style="display: inline; margin-left: 10px;" onsubmit="return confirm('УДАЛИТЬ пользователя {{ user.username }} и ВСЕ его файлы? НЕОБРАТИМО!');">
1145
- <button type="submit" class="btn delete-btn" style="padding: 5px 10px; font-size: 0.9em;">Удалить</button>
1146
  </form>
1147
  </div>
1148
  {% else %}<p>Пользователей нет.</p>{% endfor %}</div></div></body></html>'''
@@ -1159,33 +1118,27 @@ def admin_user_files(username):
1159
  flash(f'Пользователь {username} не найден.')
1160
  return redirect(url_for('admin_panel'))
1161
 
1162
- # Flatten the file structure for simple display
1163
  all_files = []
1164
- def collect_files(folder):
1165
  for item in folder.get('children', []):
1166
  if item.get('type') == 'file':
1167
- # Add parent folder info for context
1168
- item['parent_path_str'] = get_node_path_string(user_data['filesystem'], folder.get('id', 'root'))
1169
  all_files.append(item)
1170
  elif item.get('type') == 'folder':
1171
- collect_files(item)
1172
 
1173
- collect_files(user_data.get('filesystem', {}))
 
 
1174
  all_files.sort(key=lambda x: x.get('upload_date', ''), reverse=True)
1175
 
1176
-
1177
  html = '''
1178
- <!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><title>Файлы {{ username }}</title>
1179
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
1180
- <style>''' + BASE_STYLE + '''
1181
- .file-item { background: var(--card-bg); padding: 15px; border-radius: 16px; box-shadow: var(--shadow); transition: var(--transition); }
1182
- body.dark .file-item { background: var(--card-bg-dark); }
1183
- .file-item:hover { transform: translateY(-5px); }
1184
- .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; }
1185
- </style></head><body><div class="container"><h1>Файлы пользователя: {{ username }}</h1>
1186
  <a href="{{ url_for('admin_panel') }}" class="btn" style="margin-bottom: 20px;">Назад к пользователям</a>
1187
  {% with messages = get_flashed_messages() %}{% if messages %}{% for message in messages %}<div class="flash">{{ message }}</div>{% endfor %}{% endif %}{% endwith %}
1188
- <div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 20px;">
1189
  {% for file in files %}
1190
  <div class="file-item">
1191
  {% if file.file_type == 'image' %} <img class="file-preview" src="{{ hf_file_url(file.path) }}" loading="lazy">
@@ -1198,17 +1151,16 @@ body.dark .file-item { background: var(--card-bg-dark); }
1198
  <p style="font-size: 0.8em; color: #888;">Загружен: {{ file.upload_date }}</p>
1199
  <p style="font-size: 0.7em; color: #ccc;">ID: {{ file.id }}</p>
1200
  <p style="font-size: 0.7em; color: #ccc; word-break: break-all;">Path: {{ file.path }}</p>
1201
- <div style="margin-top: 10px;">
1202
- <a href="{{ url_for('download_file', file_id=file.id) }}" class="btn download-btn" style="font-size: 0.8em; padding: 4px 8px;">Скачать</a>
1203
  <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 }}?');">
1204
- <button type="submit" class="btn delete-btn" style="font-size: 0.8em; padding: 4px 8px;">Удалить</button>
1205
  </form>
1206
  </div>
1207
  </div>
1208
  {% else %} <p>У пользователя нет файлов.</p> {% endfor %}
1209
  </div></div>
1210
  <script>
1211
- // Basic helper function for URL generation if needed by JS
1212
  const repoId = "{{ repo_id }}";
1213
  function hfFileUrl(path, download = false) {
1214
  let url = `https://huggingface.co/datasets/${repoId}/resolve/main/${path}`;
@@ -1236,11 +1188,8 @@ def admin_delete_user(username):
1236
  user_data = data['users'][username]
1237
  logging.warning(f"ADMIN ACTION: Attempting to delete user {username} and all their data.")
1238
 
1239
- # 1. Attempt to delete files from Hugging Face Hub
1240
  try:
1241
  api = HfApi()
1242
- # Construct the base folder path for the user on HF Hub
1243
- # This assumes the top-level folder structure is consistent
1244
  user_folder_path_on_hf = f"cloud_files/{username}"
1245
 
1246
  logging.info(f"Attempting to delete HF Hub folder: {user_folder_path_on_hf} for user {username}")
@@ -1250,28 +1199,21 @@ def admin_delete_user(username):
1250
  repo_type="dataset",
1251
  token=HF_TOKEN_WRITE,
1252
  commit_message=f"ADMIN ACTION: Deleted all files/folders for user {username}"
1253
- # ignore_patterns=None, # Be careful with ignore patterns if used
1254
  )
1255
  logging.info(f"Successfully initiated deletion of folder {user_folder_path_on_hf} on HF Hub.")
1256
- # Note: Deletion might be async on HF side.
1257
 
1258
  except hf_utils.HfHubHTTPError as e:
1259
- # It's possible the folder doesn't exist (e.g., user never uploaded)
1260
  if e.response.status_code == 404:
1261
  logging.warning(f"User folder {user_folder_path_on_hf} not found on HF Hub for user {username}. Skipping HF deletion.")
1262
  else:
1263
  logging.error(f"Error deleting user folder {user_folder_path_on_hf} from HF Hub for {username}: {e}")
1264
  flash(f'Ошибка при удалении файлов пользователя {username} с сервера: {e}. Пользователь НЕ удален из базы.')
1265
- # Stop the process if HF deletion fails critically? Or proceed to delete from DB anyway?
1266
- # Decision: Stop here to allow manual check/cleanup.
1267
  return redirect(url_for('admin_panel'))
1268
  except Exception as e:
1269
  logging.error(f"Unexpected error during HF Hub folder deletion for {username}: {e}")
1270
  flash(f'Неожиданная ошибка при удалении файлов {username} с сервера: {e}. Пользователь НЕ удален из базы.')
1271
  return redirect(url_for('admin_panel'))
1272
 
1273
-
1274
- # 2. Delete user from the database
1275
  try:
1276
  del data['users'][username]
1277
  save_data(data)
@@ -1280,7 +1222,6 @@ def admin_delete_user(username):
1280
  except Exception as e:
1281
  logging.error(f"Error saving data after deleting user {username}: {e}")
1282
  flash(f'Файлы пользователя {username} удалены с сервера, но произошла ошибка при удалении пользователя из базы данных: {e}')
1283
- # Data inconsistency state - requires manual check
1284
 
1285
  return redirect(url_for('admin_panel'))
1286
 
@@ -1303,7 +1244,6 @@ def admin_delete_file(username, file_id):
1303
 
1304
  if not file_node or file_node.get('type') != 'file' or not parent_node:
1305
  flash('Файл не найден в структуре пользователя.')
1306
- # Optionally attempt deletion from HF anyway if path known? Risky.
1307
  return redirect(url_for('admin_user_files', username=username))
1308
 
1309
  hf_path = file_node.get('path')
@@ -1311,7 +1251,6 @@ def admin_delete_file(username, file_id):
1311
 
1312
  if not hf_path:
1313
  flash(f'Ошибка: Путь к файлу {original_filename} не найден в метаданных. Удаление только из базы.')
1314
- # Remove from DB only
1315
  if remove_node(user_data['filesystem'], file_id):
1316
  try:
1317
  save_data(data)
@@ -1321,7 +1260,6 @@ def admin_delete_file(username, file_id):
1321
  logging.error(f"Admin delete file metadata save error (no path): {e}")
1322
  return redirect(url_for('admin_user_files', username=username))
1323
 
1324
- # Proceed with HF deletion and DB update
1325
  try:
1326
  api = HfApi()
1327
  api.delete_file(
@@ -1333,7 +1271,6 @@ def admin_delete_file(username, file_id):
1333
  )
1334
  logging.info(f"ADMIN ACTION: Deleted file {hf_path} from HF Hub for user {username}")
1335
 
1336
- # Now remove from DB
1337
  if remove_node(user_data['filesystem'], file_id):
1338
  try:
1339
  save_data(data)
@@ -1342,7 +1279,6 @@ def admin_delete_file(username, file_id):
1342
  flash('Файл удален с сервера, но произошла ошибка обновления базы данных.')
1343
  logging.error(f"Admin delete file DB update error: {e}")
1344
  else:
1345
- # Should not happen if found initially
1346
  flash('Файл удален с сервера, но не найден в базе данных для удаления метаданных.')
1347
 
1348
  except hf_utils.EntryNotFoundError:
@@ -1364,8 +1300,6 @@ def admin_delete_file(username, file_id):
1364
  return redirect(url_for('admin_user_files', username=username))
1365
 
1366
 
1367
- # --- App Initialization ---
1368
-
1369
  if __name__ == '__main__':
1370
  if not HF_TOKEN_WRITE:
1371
  logging.warning("HF_TOKEN (write access) is not set. File uploads, deletions, and backups will fail.")
@@ -1373,26 +1307,22 @@ if __name__ == '__main__':
1373
  logging.warning("HF_TOKEN_READ is not set. Falling back to HF_TOKEN. File downloads/previews might fail for private repos if HF_TOKEN is also not set.")
1374
 
1375
  if HF_TOKEN_WRITE:
1376
- # Initial download before starting backup thread
1377
  logging.info("Performing initial database download before starting background backup.")
1378
  download_db_from_hf()
1379
  threading.Thread(target=periodic_backup, daemon=True).start()
1380
  logging.info("Periodic backup thread started.")
1381
  else:
1382
  logging.warning("Periodic backup disabled because HF_TOKEN (write access) is not set.")
1383
- # Still attempt initial download if read token exists
1384
  if HF_TOKEN_READ:
1385
  logging.info("Performing initial database download (read-only mode).")
1386
  download_db_from_hf()
1387
  else:
1388
  logging.warning("No read or write token. Database operations with Hugging Face Hub are disabled.")
1389
- # Check if local DB exists, if not create empty one
1390
  if not os.path.exists(DATA_FILE):
1391
  with open(DATA_FILE, 'w', encoding='utf-8') as f:
1392
  json.dump({'users': {}}, f)
1393
  logging.info(f"Created empty local database file: {DATA_FILE}")
1394
 
1395
-
1396
  app.run(debug=False, host='0.0.0.0', port=7860)
1397
 
1398
- # --- END OF FILE app (8).py ---
 
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
 
17
  app = Flask(__name__)
18
  app.secret_key = os.getenv("FLASK_SECRET_KEY", "supersecretkey_folders_unique")
19
  DATA_FILE = 'cloudeng_data_v2.json'
20
+ REPO_ID = "Eluza133/Z1e1u"
21
  HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
22
  HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") or HF_TOKEN_WRITE
23
  UPLOAD_FOLDER = 'uploads'
 
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
32
 
33
+ queue = [(filesystem, None)]
34
  while queue:
35
  current_node, parent = queue.pop(0)
36
  if current_node.get('type') == 'folder' and 'children' in current_node:
37
  for i, child in enumerate(current_node['children']):
38
  if child.get('id') == node_id:
39
+ return child, current_node
40
  if child.get('type') == 'folder':
41
  queue.append((child, current_node))
42
+ return None, None
43
 
44
  def add_node(filesystem, parent_id, node_data):
45
  parent_node, _ = find_node_by_id(filesystem, parent_id)
 
55
  if node_to_remove and parent_node and 'children' in parent_node:
56
  parent_node['children'] = [child for child in parent_node['children'] if child.get('id') != node_id]
57
  return True
 
58
  return False
59
 
60
  def get_node_path_string(filesystem, node_id):
 
64
  while current_id:
65
  node, parent = find_node_by_id(filesystem, current_id)
66
  if not node:
67
+ break
68
+ if node.get('id') != 'root':
69
  path_list.append(node.get('name', node.get('original_filename', '')))
70
+ if not parent:
71
  break
72
  current_id = parent.get('id') if parent else None
73
 
 
82
  "name": "root",
83
  "children": []
84
  }
 
85
  if 'files' in user_data and isinstance(user_data['files'], list):
86
  for old_file in user_data['files']:
 
87
  file_id = old_file.get('id', uuid.uuid4().hex)
88
  original_filename = old_file.get('filename', 'unknown_file')
 
 
89
  name_part, ext_part = os.path.splitext(original_filename)
90
  unique_suffix = uuid.uuid4().hex[:8]
91
  unique_filename = f"{name_part}_{unique_suffix}{ext_part}"
92
+ hf_path = f"cloud_files/{session['username']}/root/{unique_filename}"
 
93
 
94
  file_node = {
95
  'type': 'file',
96
  'id': file_id,
97
  'original_filename': original_filename,
98
  'unique_filename': unique_filename,
99
+ 'path': hf_path,
100
  'file_type': get_file_type(original_filename),
101
  'upload_date': old_file.get('upload_date', datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
102
  }
103
  add_node(user_data['filesystem'], 'root', file_node)
104
+ del user_data['files']
 
 
 
 
105
 
 
106
 
107
  @cache.memoize(timeout=300)
108
  def load_data():
 
114
  logging.warning("Data is not in dict format, initializing empty database")
115
  return {'users': {}}
116
  data.setdefault('users', {})
 
117
  for username, user_data in data['users'].items():
118
+ initialize_user_filesystem(user_data)
119
  logging.info("Data successfully loaded and initialized")
120
  return data
121
  except Exception as e:
 
187
  def periodic_backup():
188
  while True:
189
  upload_db_to_hf()
190
+ time.sleep(1800)
191
 
192
  def get_file_type(filename):
193
  filename_lower = filename.lower()
 
199
  return 'pdf'
200
  elif filename_lower.endswith('.txt'):
201
  return 'text'
 
 
 
202
  return 'other'
203
 
204
 
 
205
  BASE_STYLE = '''
206
  :root {
207
  --primary: #ff4d6d; --secondary: #00ddeb; --accent: #8b5cf6;
 
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); }
 
240
  .user-item:hover { transform: translateY(-5px); }
241
  .user-item a { color: var(--primary); text-decoration: none; font-weight: 600; }
242
  .user-item a:hover { color: var(--accent); }
 
 
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); }
 
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':
 
325
  return redirect(url_for('register'))
326
 
327
  data['users'][username] = {
328
+ 'password': password,
329
  'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
330
  'filesystem': {
331
  "type": "folder", "id": "root", "name": "root", "children": []
 
359
  data = load_data()
360
 
361
  user = data['users'].get(username)
362
+ if user and user['password'] == password:
363
  session['username'] = username
 
364
  if 'filesystem' not in user:
365
  initialize_user_filesystem(user)
366
  try:
367
  save_data(data)
368
  except Exception as e:
369
  logging.error(f"Error saving data after filesystem init for {username}: {e}")
 
370
  return jsonify({'status': 'success', 'redirect': url_for('dashboard')})
371
  else:
372
  return jsonify({'status': 'error', 'message': 'Неверное имя пользователя или пароль!'})
373
 
 
374
  html = '''
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">
 
413
  return redirect(url_for('login'))
414
 
415
  user_data = data['users'][username]
 
416
  if 'filesystem' not in user_data:
417
  initialize_user_filesystem(user_data)
 
418
 
419
  current_folder_id = request.args.get('folder_id', 'root')
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('Загрузка невозможна: токен для записи не настроен.')
 
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()
456
  uploaded_count = 0
 
465
  file_id = uuid.uuid4().hex
466
 
467
  hf_path = f"cloud_files/{username}/{target_folder_id}/{unique_filename}"
468
+ temp_path = os.path.join(UPLOAD_FOLDER, f"{file_id}_{unique_filename}")
469
 
470
  try:
471
  file.save(temp_path)
 
492
  if add_node(user_data['filesystem'], target_folder_id, file_info):
493
  uploaded_count += 1
494
  else:
 
495
  errors.append(f"Ошибка добавления метаданных для {original_filename}.")
496
  logging.error(f"Failed to add node metadata for file {file_id} to folder {target_folder_id} for user {username}")
 
497
  try:
498
  api.delete_file(path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE)
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}")
505
  finally:
506
  if os.path.exists(temp_path):
507
+ os.remove(temp_path)
508
 
509
  if uploaded_count > 0:
510
  try:
 
516
 
517
  if errors:
518
  for error_msg in errors:
519
+ flash(error_msg, 'error')
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:
526
  node, parent = find_node_by_id(user_data['filesystem'], temp_id)
527
  if not node: break
528
+ is_link = (node['id'] != current_folder_id)
529
  breadcrumbs.append({'id': node['id'], 'name': node.get('name', 'Root'), 'is_link': is_link})
530
  if not parent: break
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">
 
578
  <a href="{{ url_for('dashboard', folder_id=item.id) }}" class="item-preview" title="Перейти в папку {{ item.name }}">📁</a>
579
  <p><b>{{ item.name }}</b></p>
580
  <div class="item-actions">
581
+ <a href="{{ url_for('dashboard', folder_id=item.id) }}" class="btn folder-btn">Открыть</a>
582
  <form method="POST" action="{{ url_for('delete_folder', folder_id=item.id) }}" style="display: inline;" onsubmit="return confirm('Удалить папку {{ item.name }}? Папку можно удалить только если она пуста.');">
583
  <input type="hidden" name="current_view_folder_id" value="{{ current_folder_id }}">
584
+ <button type="submit" class="btn delete-btn">Удалить</button>
585
  </form>
586
  </div>
587
  {% elif item.type == 'file' %}
 
605
  <p title="{{ item.original_filename }}">{{ item.original_filename | truncate(25, True) }}</p>
606
  <p style="font-size: 0.8em; color: #888;">{{ item.upload_date }}</p>
607
  <div class="item-actions">
608
+ <a href="{{ url_for('download_file', file_id=item.id) }}" class="btn download-btn">Скачать</a>
609
  {% if previewable %}
610
+ <button class="btn" style="background: var(--accent);"
611
  onclick="openModal('{{ hf_file_url(item.path) if item.file_type != 'text' else url_for('get_text_content', file_id=item.id) }}', '{{ item.file_type }}', '{{ item.id }}')">Просмотр</button>
612
  {% endif %}
613
  <form method="POST" action="{{ url_for('delete_file', file_id=item.id) }}" style="display: inline;" onsubmit="return confirm('Вы уверены, что хотите удалить файл {{ item.original_filename }}?');">
614
  <input type="hidden" name="current_view_folder_id" value="{{ current_folder_id }}">
615
+ <button type="submit" class="btn delete-btn">Удалить</button>
616
  </form>
617
  </div>
618
  {% endif %}
 
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
 
 
657
  function hfFileUrl(path, download = false) {
658
  let url = `https://huggingface.co/datasets/${repoId}/resolve/main/${path}`;
659
  if (download) url += '?download=true';
 
 
 
660
  return url;
661
  }
662
 
663
  async function openModal(srcOrUrl, type, itemId) {
664
  const modal = document.getElementById('mediaModal');
665
  const modalContent = document.getElementById('modalContent');
666
+ modalContent.innerHTML = '<p>Загрузка...</p>';
667
  modal.style.display = 'flex';
668
 
669
  try {
 
674
  } else if (type === 'pdf') {
675
  modalContent.innerHTML = `<iframe src="${srcOrUrl}" title="Просмотр PDF"></iframe>`;
676
  } else if (type === 'text') {
677
+ const response = await fetch(srcOrUrl);
 
678
  if (!response.ok) throw new Error(`Ошибка загрузки текста: ${response.statusText}`);
679
  const text = await response.text();
 
680
  const escapedText = text.replace(/</g, "<").replace(/>/g, ">");
681
  modalContent.innerHTML = `<pre>${escapedText}</pre>`;
682
  } else {
 
690
 
691
  function closeModal(event) {
692
  const modal = document.getElementById('mediaModal');
 
693
  if (event.target === modal) {
694
  closeModalManual();
695
  }
 
701
  const video = modal.querySelector('video');
702
  if (video) video.pause();
703
  const iframe = modal.querySelector('iframe');
704
+ if (iframe) iframe.src = 'about:blank';
705
  document.getElementById('modalContent').innerHTML = '';
706
  }
707
 
 
711
  const uploadBtn = document.getElementById('upload-btn');
712
 
713
  form.addEventListener('submit', function(e) {
 
714
  const files = form.querySelector('input[type="file"]').files;
715
  if (files.length === 0) {
716
  alert('Пожалуйста, выберите файлы для загрузки.');
 
722
  e.preventDefault();
723
  return;
724
  }
 
 
 
 
 
 
 
 
 
725
 
 
 
 
 
726
  progressContainer.style.display = 'block';
727
+ progressBar.style.width = '50%';
728
  uploadBtn.disabled = true;
729
  uploadBtn.textContent = 'Загрузка...';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
730
  });
731
 
732
  document.getElementById('logout-btn').addEventListener('click', function(e) {
 
737
 
738
  </script>
739
  </body></html>'''
740
+
741
  template_context = {
742
  'username': username,
743
  'items': items_in_folder,
 
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
 
 
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))
 
789
  except Exception as e:
790
  flash('Ошибка сохранения данных при создании папки.')
791
  logging.error(f"Create folder save error: {e}")
 
792
  else:
793
  flash('Не удалось найти родительскую папку.')
794
 
 
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')
 
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}")
 
894
  return redirect(url_for('login'))
895
 
896
  file_node, parent_node = find_node_by_id(user_data['filesystem'], 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('Файл не найден или не может быть удален.')
 
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)
 
930
  )
931
  logging.info(f"Deleted file {hf_path} from HF Hub for user {username}")
932
 
 
933
  if remove_node(user_data['filesystem'], file_id):
934
  try:
935
  save_data(data)
 
977
  return redirect(url_for('login'))
978
 
979
  folder_node, parent_node = find_node_by_id(user_data['filesystem'], 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('Папка не найдена или не может быть удалена.')
 
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):
993
  try:
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))
1004
 
 
1033
  response = requests.get(file_url, headers=headers)
1034
  response.raise_for_status()
1035
 
 
1036
  if len(response.content) > 1 * 1024 * 1024:
1037
  return Response("Файл слишком большой для предпросмотра.", status=413)
1038
 
 
1039
  try:
1040
  text_content = response.content.decode('utf-8')
1041
  except UnicodeDecodeError:
 
1048
 
1049
  except requests.exceptions.RequestException as e:
1050
  logging.error(f"Error fetching text content from HF ({hf_path}): {e}")
1051
+ return Response(f"Ошибка загрузки содержимого: {e}", status=502)
1052
  except Exception as e:
1053
  logging.error(f"Unexpected error fetching text content ({hf_path}): {e}")
1054
  return Response("Внутренняя ошибка сервера", status=500)
 
1057
  @app.route('/logout')
1058
  def logout():
1059
  session.pop('username', None)
 
1060
  flash('Вы успешно вышли из системы.')
1061
  return redirect(url_for('login'))
1062
 
1063
 
 
 
1064
  def is_admin():
1065
+ return 'username' in session
 
 
1066
 
1067
  @app.route('/admhosto')
1068
  def admin_panel():
 
1072
  data = load_data()
1073
  users = data.get('users', {})
1074
 
 
1075
  user_details = []
1076
  for uname, udata in users.items():
1077
  file_count = 0
 
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 %}
 
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>'''
 
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">
 
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>
1159
  </div>
1160
  </div>
1161
  {% else %} <p>У пользователя нет файлов.</p> {% endfor %}
1162
  </div></div>
1163
  <script>
 
1164
  const repoId = "{{ repo_id }}";
1165
  function hfFileUrl(path, download = false) {
1166
  let url = `https://huggingface.co/datasets/${repoId}/resolve/main/${path}`;
 
1188
  user_data = data['users'][username]
1189
  logging.warning(f"ADMIN ACTION: Attempting to delete user {username} and all their data.")
1190
 
 
1191
  try:
1192
  api = HfApi()
 
 
1193
  user_folder_path_on_hf = f"cloud_files/{username}"
1194
 
1195
  logging.info(f"Attempting to delete HF Hub folder: {user_folder_path_on_hf} for user {username}")
 
1199
  repo_type="dataset",
1200
  token=HF_TOKEN_WRITE,
1201
  commit_message=f"ADMIN ACTION: Deleted all files/folders for user {username}"
 
1202
  )
1203
  logging.info(f"Successfully initiated deletion of folder {user_folder_path_on_hf} on HF Hub.")
 
1204
 
1205
  except hf_utils.HfHubHTTPError as e:
 
1206
  if e.response.status_code == 404:
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:
1218
  del data['users'][username]
1219
  save_data(data)
 
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
 
 
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')
 
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)
 
1260
  logging.error(f"Admin delete file metadata save error (no path): {e}")
1261
  return redirect(url_for('admin_user_files', username=username))
1262
 
 
1263
  try:
1264
  api = HfApi()
1265
  api.delete_file(
 
1271
  )
1272
  logging.info(f"ADMIN ACTION: Deleted file {hf_path} from HF Hub for user {username}")
1273
 
 
1274
  if remove_node(user_data['filesystem'], file_id):
1275
  try:
1276
  save_data(data)
 
1279
  flash('Файл удален с сервера, но произошла ошибка обновления базы данных.')
1280
  logging.error(f"Admin delete file DB update error: {e}")
1281
  else:
 
1282
  flash('Файл удален с сервера, но не найден в базе данных для удаления метаданных.')
1283
 
1284
  except hf_utils.EntryNotFoundError:
 
1300
  return redirect(url_for('admin_user_files', username=username))
1301
 
1302
 
 
 
1303
  if __name__ == '__main__':
1304
  if not HF_TOKEN_WRITE:
1305
  logging.warning("HF_TOKEN (write access) is not set. File uploads, deletions, and backups will fail.")
 
1307
  logging.warning("HF_TOKEN_READ is not set. Falling back to HF_TOKEN. File downloads/previews might fail for private repos if HF_TOKEN is also not set.")
1308
 
1309
  if HF_TOKEN_WRITE:
 
1310
  logging.info("Performing initial database download before starting background backup.")
1311
  download_db_from_hf()
1312
  threading.Thread(target=periodic_backup, daemon=True).start()
1313
  logging.info("Periodic backup thread started.")
1314
  else:
1315
  logging.warning("Periodic backup disabled because HF_TOKEN (write access) is not set.")
 
1316
  if HF_TOKEN_READ:
1317
  logging.info("Performing initial database download (read-only mode).")
1318
  download_db_from_hf()
1319
  else:
1320
  logging.warning("No read or write token. Database operations with Hugging Face Hub are disabled.")
 
1321
  if not os.path.exists(DATA_FILE):
1322
  with open(DATA_FILE, 'w', encoding='utf-8') as f:
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 ---