'''
return render_template_string(html)
@app.route('/', methods=['GET', 'POST'])
def login():
- if 'username' in session:
- return redirect(url_for('dashboard'))
-
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
data = load_data()
- if username in data.get('users', {}) and data['users'][username].get('password') == password:
+ user = data['users'].get(username)
+ if user and user['password'] == password: # Plain text comparison
session['username'] = username
- # Use JSON response for JS fetch
- if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
- return jsonify({'status': 'success', 'redirect': url_for('dashboard')})
- else:
- return redirect(url_for('dashboard')) # Fallback for non-JS
+ # Initialize filesystem if missing (for very old users before structure change)
+ if 'filesystem' not in user:
+ initialize_user_filesystem(user)
+ try:
+ save_data(data)
+ except Exception as e:
+ logging.error(f"Error saving data after filesystem init for {username}: {e}")
+ # Proceed with login anyway, but log the error
+ return jsonify({'status': 'success', 'redirect': url_for('dashboard')})
else:
- if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
- return jsonify({'status': 'error', 'message': 'Неверное имя пользователя или пароль!'})
- else:
- flash('Неверное имя пользователя или пароль!', 'error')
- return redirect(url_for('login'))
-
+ return jsonify({'status': 'error', 'message': 'Неверное имя пользователя или пароль!'})
+ # Login page HTML with JS for AJAX login and auto-login check
html = '''
-
-
-
-
- Zeus Cloud V2
-
-
-
-
-
-
Zeus Cloud V2
-
- {% with messages = get_flashed_messages(with_categories=true) %}
- {% if messages %}
- {% for category, message in messages %}
-
-
- {% with messages = get_flashed_messages(with_categories=true) %}
- {% if messages %}
- {% for category, message in messages %}
-
{{ message }}
- {% endfor %}
- {% endif %}
- {% endwith %}
-
-
- {% for crumb in breadcrumbs %}
- {% if not loop.last %}
- {{ crumb.name }}/
- {% else %}
- {{ crumb.name }}
- {% endif %}
- {% endfor %}
-
+ if not current_folder or current_folder.get('type') != 'folder':
+ flash('Папка не найдена!')
+ current_folder_id = 'root' # Reset to root
+ current_folder, parent_folder = find_node_by_id(user_data['filesystem'], current_folder_id)
+ if not current_folder: # Should always find root
+ # This indicates a serious data structure issue
+ logging.error(f"CRITICAL: Root folder not found for user {username}")
+ flash('Критическая ошибка: корневая папка не найдена.')
+ session.pop('username', None)
+ return redirect(url_for('login'))
-
- parent_node = get_node_by_path(user_data, current_path_str)
- if not parent_node or parent_node.get('type') != 'folder':
- return jsonify({'status': 'error', 'message': 'Целевая папка не найдена'}), 404
+
+
- files = request.files.getlist('files')
- if not files or all(not f.filename for f in files):
- return jsonify({'status': 'error', 'message': 'Файлы не выбраны'}), 400
+
- if not HF_TOKEN_WRITE:
- return jsonify({'status': 'error', 'message': 'Ошибка конфигурации сервера: отсутствует токен для записи'}), 500
+
+'''
+ # Pass helper function to template context
+ template_context = {
+ 'username': username,
+ 'items': items_in_folder,
+ 'current_folder_id': current_folder_id,
+ 'current_folder': current_folder,
+ 'breadcrumbs': breadcrumbs,
+ 'repo_id': REPO_ID,
+ 'HF_TOKEN_READ': HF_TOKEN_READ,
+ 'hf_file_url': lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{path}{'?download=true' if download else ''}",
+ 'os': os # For admin template usage if needed, be cautious
+ }
+ return render_template_string(html, **template_context)
@app.route('/create_folder', methods=['POST'])
@@ -1018,555 +800,599 @@ def create_folder():
username = session['username']
data = load_data()
- if username not in data.get('users', {}):
- return jsonify({'status': 'error', 'message': 'Пользователь не найден'}), 403
+ user_data = data['users'].get(username)
+ if not user_data:
+ return jsonify({'status': 'error', 'message': 'Пользователь не найден'}), 404
- user_data = data['users'][username]
- current_path_str = request.form.get('current_path', '/')
+ parent_folder_id = request.form.get('parent_folder_id', 'root')
folder_name = request.form.get('folder_name', '').strip()
- # Basic validation
if not folder_name:
- flash('Имя папки не может быть пустым!', 'error')
- return redirect(url_for('dashboard', folder_path=current_path_str))
- if not folder_name.replace('.', '').replace('_', '').replace('-', '').isalnum():
- flash('Имя папки может содержать только буквы, цифры, точки, дефисы и подчеркивания!', 'error')
- return redirect(url_for('dashboard', folder_path=current_path_str))
- if folder_name in ['.', '..']:
- flash('Недопустимое имя папки.', 'error')
- return redirect(url_for('dashboard', folder_path=current_path_str))
-
-
- folder_name = secure_filename(folder_name) # Sanitize further
-
- new_folder_data = {
- "type": "folder",
- "name": folder_name,
- "children": {}
+ flash('Имя папки не может быть пустым!')
+ return redirect(url_for('dashboard', folder_id=parent_folder_id))
+ # Basic validation for folder names (avoiding problematic chars)
+ if not folder_name.isalnum() and '_' not in folder_name and ' ' not in folder_name:
+ flash('Имя папки может содержать буквы, цифры, пробелы и подчеркивания.')
+ return redirect(url_for('dashboard', folder_id=parent_folder_id))
+
+ folder_id = uuid.uuid4().hex
+ folder_data = {
+ 'type': 'folder',
+ 'id': folder_id,
+ 'name': folder_name,
+ 'children': []
}
- if add_node(user_data, current_path_str, new_folder_data):
+ if add_node(user_data['filesystem'], parent_folder_id, folder_data):
try:
save_data(data)
- flash(f'Папка "{folder_name}" успешно создана.', 'success')
+ flash(f'Папка "{folder_name}" успешно создана.')
except Exception as e:
- flash('Ошибка при сохранении данных.', 'error')
- # Attempt to revert the change in memory? Difficult.
+ flash('Ошибка сохранения данных п��и создании папки.')
+ logging.error(f"Create folder save error: {e}")
+ # Attempt to rollback? Difficult without transactions.
else:
- flash(f'Папка "{folder_name}" уже существует или произошла ошибка.', 'error')
+ flash('Не удалось найти родительскую папку.')
- return redirect(url_for('dashboard', folder_path=current_path_str))
+ return redirect(url_for('dashboard', folder_id=parent_folder_id))
-@app.route('/delete_item', methods=['POST'])
-def delete_item():
+@app.route('/download/')
+def download_file(file_id):
if 'username' not in session:
- flash('Пожалуйста, войдите в систему!', 'info')
+ flash('Пожалуйста, войдите в систему!')
return redirect(url_for('login'))
username = session['username']
data = load_data()
- if username not in data.get('users', {}):
+ user_data = data['users'].get(username)
+ if not user_data:
+ flash('Пользователь не найден!')
session.pop('username', None)
- flash('Пользователь не найден!', 'error')
return redirect(url_for('login'))
- item_path_str = request.form.get('item_path', '').strip()
- if not item_path_str:
- flash('Не указан путь к элементу для удаления.', 'error')
- return redirect(url_for('dashboard'))
+ file_node, _ = find_node_by_id(user_data['filesystem'], file_id)
+
+ # Also check admin access if needed
+ is_admin_route = request.referrer and 'admhosto' in request.referrer
+ if not file_node or file_node.get('type') != 'file':
+ # Check if admin is trying to download from admin panel
+ if is_admin_route:
+ # Admin might be trying to download a file listed in the panel
+ # Need to search across all users if the file_id is globally unique
+ # Or, the admin route should pass username context
+ # For now, assume admin routes provide enough context elsewhere or this check fails
+ flash('(Admin) Файл не найден в структуре пользователя.')
+ # Redirect back to admin panel or specific user view if possible
+ return redirect(request.referrer or url_for('admin_panel'))
+ else:
+ flash('Файл не найден!')
+ return redirect(url_for('dashboard')) # Redirect user to their root
+
+ hf_path = file_node.get('path')
+ original_filename = file_node.get('original_filename', 'downloaded_file')
+
+ if not hf_path:
+ flash('Ошибка: Путь к файлу не найден в метаданных.')
+ return redirect(request.referrer or url_for('dashboard'))
+
+ file_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{hf_path}?download=true"
- user_data = data['users'][username]
- api = HfApi() if HF_TOKEN_WRITE else None
-
- if not api and get_node_by_path(user_data, item_path_str): # Need token to delete from HF
- node_to_delete = get_node_by_path(user_data, item_path_str)
- is_file = node_to_delete.get('type') == 'file' if node_to_delete else False
- if is_file and node_to_delete.get('storage_path'):
- flash('Ошибка конфигурации: невозможно удалить файл из облака без токена записи.', 'error')
- # Redirect back to the containing folder
- parent_path = '/'.join(item_path_str.strip('/').split('/')[:-1])
- return redirect(url_for('dashboard', folder_path=parent_path))
- elif node_to_delete and node_to_delete.get('type') == 'folder':
- # Allow deleting empty folder structure from JSON even without token? Risky.
- # For now, require token for all deletes involving potential cloud objects.
- flash('Ошибка конфигурации: невозможно удалить папку из облака без токена записи.', 'error')
- parent_path = '/'.join(item_path_str.strip('/').split('/')[:-1])
- return redirect(url_for('dashboard', folder_path=parent_path))
-
-
- # Call recursive delete helper
- deleted, message = delete_node_recursive(user_data, item_path_str, api, username)
-
- if deleted:
- try:
- save_data(data)
- flash(f'Элемент "{item_path_str.split("/")[-1]}" удален. {message}', 'success')
- except Exception as e:
- flash(f'Элемент удален из структуры, но произошла ошибка сохранения: {e}', 'error')
- # State might be inconsistent
- else:
- flash(f'Ошибка удаления элемента "{item_path_str.split("/")[-1]}": {message}', 'error')
+ try:
+ headers = {}
+ if HF_TOKEN_READ:
+ headers["authorization"] = f"Bearer {HF_TOKEN_READ}"
- # Redirect back to the containing folder
- parent_path = '/'.join(item_path_str.strip('/').split('/')[:-1])
- return redirect(url_for('dashboard', folder_path=parent_path))
+ response = requests.get(file_url, headers=headers, stream=True)
+ response.raise_for_status()
+
+ file_content = BytesIO(response.content)
+ return send_file(
+ file_content,
+ as_attachment=True,
+ download_name=original_filename,
+ mimetype='application/octet-stream'
+ )
+ except requests.exceptions.RequestException as e:
+ logging.error(f"Error downloading file from HF ({hf_path}): {e}")
+ flash(f'Ошибка скачивания файла {original_filename}! ({e})')
+ # Determine redirect target
+ return redirect(request.referrer or url_for('dashboard'))
+ except Exception as e:
+ logging.error(f"Unexpected error during download ({hf_path}): {e}")
+ flash('Произошла непредвиденная ошибка при скачивании файла.')
+ return redirect(request.referrer or url_for('dashboard'))
-@app.route('/download//')
-def download_file(storage_path, filename):
+@app.route('/delete_file/', methods=['POST'])
+def delete_file(file_id):
if 'username' not in session:
- flash('Пожалуйста, войдите в систему!', 'info')
+ flash('Пожалуйста, войдите в систему!')
return redirect(url_for('login'))
username = session['username']
data = load_data()
- user_data = data.get('users', {}).get(username)
-
+ user_data = data['users'].get(username)
if not user_data:
+ flash('Пользователь не найден!')
session.pop('username', None)
- flash('Пользователь не найден!', 'error')
return redirect(url_for('login'))
- # Verify the user actually owns this file via storage_path
- # This requires searching the user's data structure.
- # For simplicity here, we assume the path is correct if the user is logged in.
- # A more secure check would traverse the user_data['root'] to find the storage_path.
- # Example check (can be slow for large structures):
- # file_found = False
- # def find_file(node):
- # nonlocal file_found
- # if file_found: return
- # if node.get('type') == 'file' and node.get('storage_path') == storage_path:
- # file_found = True
- # return
- # if node.get('type') == 'folder':
- # for child in node.get('children', {}).values():
- # find_file(child)
- # find_file(user_data['root'])
- # if not file_found:
- # # Check if admin is trying to download (referer check is weak security)
- # is_admin_route = request.referrer and '/admhosto/' in request.referrer
- # if not is_admin_route:
- # flash('Доступ к файлу запрещен.', 'error')
- # # Redirect to user's root or previous page if possible
- # return redirect(url_for('dashboard'))
- # # If admin, allow proceed (admin authorization should be robust)
-
- # If checks pass or are skipped:
- file_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{storage_path}?download=true"
- try:
- headers = {}
- if HF_TOKEN_READ:
- headers["authorization"] = f"Bearer {HF_TOKEN_READ}"
+ file_node, parent_node = find_node_by_id(user_data['filesystem'], file_id)
+ current_view_folder_id = request.form.get('current_view_folder_id', 'root') # Where to redirect back
- response = requests.get(file_url, headers=headers, stream=True, timeout=60) # Add timeout
- response.raise_for_status()
+ if not file_node or file_node.get('type') != 'file' or not parent_node:
+ flash('Файл не найден или не может быть удален.')
+ return redirect(url_for('dashboard', folder_id=current_view_folder_id))
- # Stream the download
- return send_file(
- BytesIO(response.content), # Consider streaming response.raw if files are large
- as_attachment=True,
- download_name=filename, # Use the original filename for the user
- mimetype='application/octet-stream'
+ hf_path = file_node.get('path')
+ original_filename = file_node.get('original_filename', 'файл')
+
+ if not hf_path:
+ flash(f'Ошибка: Путь к файлу {original_filename} не найден. Удаление только из базы.')
+ # Proceed to remove from DB only
+ if remove_node(user_data['filesystem'], file_id):
+ try:
+ save_data(data)
+ flash(f'Метаданные файла {original_filename} удалены.')
+ except Exception as e:
+ flash('Ошибка сохранения данных после удаления метаданных.')
+ logging.error(f"Delete file metadata save error: {e}")
+ return redirect(url_for('dashboard', folder_id=current_view_folder_id))
+
+
+ if not HF_TOKEN_WRITE:
+ flash('Удаление невозможно: токен для записи не настроен.')
+ return redirect(url_for('dashboard', folder_id=current_view_folder_id))
+
+ try:
+ api = HfApi()
+ api.delete_file(
+ path_in_repo=hf_path,
+ repo_id=REPO_ID,
+ repo_type="dataset",
+ token=HF_TOKEN_WRITE,
+ commit_message=f"User {username} deleted file {original_filename} (ID: {file_id})"
)
- except requests.exceptions.RequestException as e:
- logging.error(f"Error downloading file from HF {storage_path}: {e}")
- flash(f'Ошибка скачивания файла "{filename}".', 'error')
+ logging.info(f"Deleted file {hf_path} from HF Hub for user {username}")
+
+ # Now remove from DB
+ if remove_node(user_data['filesystem'], file_id):
+ try:
+ save_data(data)
+ flash(f'Файл {original_filename} успешно удален!')
+ except Exception as e:
+ flash('Файл удален с сервера, но произошла ошибка обновления базы данных.')
+ logging.error(f"Delete file DB update error: {e}")
+ else:
+ flash('Файл удален с сервера, но не найден в локальной базе данных для удаления.')
+
+
+ except hf_utils.EntryNotFoundError:
+ logging.warning(f"File {hf_path} not found on HF Hub during delete attempt for user {username}. Removing from DB.")
+ if remove_node(user_data['filesystem'], file_id):
+ try:
+ save_data(data)
+ flash(f'Файл {original_filename} не найден на сервере, удален из базы.')
+ except Exception as e:
+ flash('Ошибка сохранения данных после удаления метаданных (файл не найден на сервере).')
+ logging.error(f"Delete file metadata save error (HF not found): {e}")
+ else:
+ flash('Файл не найден ни на сервере, ни в базе данных.')
except Exception as e:
- logging.error(f"Unexpected error during download {storage_path}: {e}")
- flash(f'Непредвиденная ошибка при скачивании файла "{filename}".', 'error')
-
- # Redirect back to the likely folder view if download fails
- folder_parts = storage_path.split('/')
- # Assuming structure cloud_files/username/folder/../unique_file
- if len(folder_parts) > 3:
- referer_folder = '/'.join(folder_parts[2:-1]) # Extract folder path part
- return redirect(url_for('dashboard', folder_path=referer_folder))
- else:
- return redirect(url_for('dashboard')) # Fallback to root
+ logging.error(f"Error deleting file {hf_path} for {username}: {e}")
+ flash(f'Ошибка удаления файла {original_filename}: {e}')
+ return redirect(url_for('dashboard', folder_id=current_view_folder_id))
-@app.route('/hf_url/')
-def get_hf_url(storage_path):
- """Provides a potentially authenticated URL for previews (images, videos, pdf)."""
+@app.route('/delete_folder/', methods=['POST'])
+def delete_folder(folder_id):
if 'username' not in session:
- return "Unauthorized", 401
+ flash('Пожалуйста, войдите в систему!')
+ return redirect(url_for('login'))
- # Basic check: ensure the user is logged in.
- # More robust check: verify user owns storage_path (as in download_file).
+ if folder_id == 'root':
+ flash('Нельзя удалить корневую папку!')
+ return redirect(url_for('dashboard'))
- base_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{storage_path}"
+ username = session['username']
+ data = load_data()
+ user_data = data['users'].get(username)
+ if not user_data:
+ flash('Пользователь не найден!')
+ session.pop('username', None)
+ return redirect(url_for('login'))
+
+ folder_node, parent_node = find_node_by_id(user_data['filesystem'], folder_id)
+ current_view_folder_id = request.form.get('current_view_folder_id', 'root') # Where to redirect back
+
+ if not folder_node or folder_node.get('type') != 'folder' or not parent_node:
+ flash('Папка не найдена или не может быть удалена.')
+ return redirect(url_for('dashboard', folder_id=current_view_folder_id))
- # For private repos, direct linking might not work without extra steps
- # or redirecting through the app which handles authentication.
- # If repo is public or using token for access via JS isn't feasible/secure,
- # we might need to proxy the content.
+ folder_name = folder_node.get('name', 'папка')
- # Simple approach: Redirect to HF URL, relying on browser auth or public access.
- # If using a read token, it needs to be handled client-side (potentially insecure)
- # or server-side via proxying.
+ # Check if folder is empty
+ if folder_node.get('children'):
+ flash(f'Папку "{folder_name}" можно удалить только если она пуста.')
+ return redirect(url_for('dashboard', folder_id=current_view_folder_id))
- # Let's proxy for simplicity and security if a token is needed.
- if HF_TOKEN_READ:
+ # Folder is empty, proceed with deletion from DB
+ if remove_node(user_data['filesystem'], folder_id):
try:
- headers = {"authorization": f"Bearer {HF_TOKEN_READ}"}
- response = requests.get(base_url, headers=headers, stream=True, timeout=15)
- response.raise_for_status()
-
- # Stream the content back
- return send_file(
- BytesIO(response.content), # Again, consider response.raw for large files
- mimetype=response.headers.get('Content-Type', 'application/octet-stream'),
- as_attachment=False # Display inline
- )
- except requests.exceptions.RequestException as e:
- logging.error(f"Proxy error for {storage_path}: {e}")
- return "Error fetching file", 500
+ save_data(data)
+ flash(f'Пустая папка "{folder_name}" успешно удалена.')
+ # Note: We don't delete anything from HF Hub here, as empty folders don't really exist there.
+ # Files inside folders are deleted individually via delete_file.
except Exception as e:
- logging.error(f"Unexpected proxy error for {storage_path}: {e}")
- return "Server error", 500
+ flash('Ошибка сохранения данных после удаления папки.')
+ logging.error(f"Delete empty folder save error: {e}")
else:
- # If no read token, assume public repo and redirect
- return redirect(base_url)
+ flash('Не удалось удалить папку из базы данных.')
+
+
+ # Redirect to the parent folder after deletion
+ redirect_to_folder_id = parent_node.get('id', 'root')
+ return redirect(url_for('dashboard', folder_id=redirect_to_folder_id))
+
+
+@app.route('/get_text_content/')
+def get_text_content(file_id):
+ if 'username' not in session:
+ return Response("Не авторизован", status=401)
+
+ username = session['username']
+ data = load_data()
+ user_data = data['users'].get(username)
+ if not user_data:
+ return Response("Пользователь не найден", status=404)
+
+ file_node, _ = find_node_by_id(user_data['filesystem'], file_id)
+
+ if not file_node or file_node.get('type') != 'file' or file_node.get('file_type') != 'text':
+ return Response("Текстовый файл не найден", status=404)
+
+ hf_path = file_node.get('path')
+ if not hf_path:
+ return Response("Ошибка: путь к файлу отсутствует", status=500)
+
+ file_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{hf_path}?download=true"
+
+ try:
+ headers = {}
+ if HF_TOKEN_READ:
+ headers["authorization"] = f"Bearer {HF_TOKEN_READ}"
+
+ response = requests.get(file_url, headers=headers)
+ response.raise_for_status()
+
+ # Limit file size to prevent server overload (e.g., 1MB)
+ if len(response.content) > 1 * 1024 * 1024:
+ return Response("Файл слишком большой для предпросмотра.", status=413)
+
+ # Try decoding with UTF-8, fallback to latin-1 or others if needed
+ try:
+ text_content = response.content.decode('utf-8')
+ except UnicodeDecodeError:
+ try:
+ text_content = response.content.decode('latin-1')
+ except Exception:
+ return Response("Не удалось определить кодировку файла.", status=500)
+
+ return Response(text_content, mimetype='text/plain')
+
+ except requests.exceptions.RequestException as e:
+ logging.error(f"Error fetching text content from HF ({hf_path}): {e}")
+ return Response(f"Ошибка загрузки содержимого: {e}", status=502) # Bad Gateway or appropriate error
+ except Exception as e:
+ logging.error(f"Unexpected error fetching text content ({hf_path}): {e}")
+ return Response("Внутренняя ошибка сервера", status=500)
@app.route('/logout')
def logout():
session.pop('username', None)
- flash('Вы успешно вышли из системы.', 'success')
- # Client-side JS on login page should handle clearing localStorage if used
+ # JS on login page handles localStorage removal upon redirection
+ flash('Вы успешно вышли из системы.')
return redirect(url_for('login'))
-# --- Admin Routes (Placeholder - Add Proper Admin Auth!) ---
-# WARNING: These routes currently lack proper authentication!
-# Implement robust admin checks (e.g., specific admin user, role, session flag).
+# --- Admin Routes (Simplified - Add Auth Check) ---
def is_admin():
- # Placeholder: Implement real admin check here!
- # return session.get('is_admin') == True
- # For now, allow access if logged in (INSECURE!)
- return 'username' in session
+ # Implement proper admin check (e.g., check session for specific admin user/role)
+ # For now, allow access if logged in (INSECURE - REPLACE THIS)
+ return 'username' in session # Placeholder - VERY INSECURE
@app.route('/admhosto')
def admin_panel():
- if not is_admin(): return redirect(url_for('login'))
+ if not is_admin():
+ return redirect(url_for('login'))
+
data = load_data()
users = data.get('users', {})
- user_list = []
+
+ # Calculate total files per user
+ user_details = []
for uname, udata in users.items():
- file_count = 0
- def count_files(node):
- nonlocal file_count
- if node.get('type') == 'file':
- file_count += 1
- elif node.get('type') == 'folder':
- for child in node.get('children', {}).values():
- count_files(child)
- count_files(udata.get('root', {}))
- user_list.append({
- 'username': uname,
- 'created_at': udata.get('created_at', 'N/A'),
- 'file_count': file_count
- })
+ file_count = 0
+ q = [udata.get('filesystem', {}).get('children', [])]
+ while q:
+ current_level = q.pop(0)
+ for item in current_level:
+ if item.get('type') == 'file':
+ file_count += 1
+ elif item.get('type') == 'folder' and 'children' in item:
+ q.append(item['children'])
+ user_details.append({
+ 'username': uname,
+ 'created_at': udata.get('created_at', 'N/A'),
+ 'file_count': file_count
+ })
html = '''
-
-Админ-панель - Zeus Cloud V2
+Админ-панель
-
Админ-панель Zeus Cloud V2
-
Список пользователей
-{% for user in user_list %}
+
Админ-панель
+{% with messages = get_flashed_messages() %}{% if messages %}{% for message in messages %}
-
-'''
- return render_template_string(html, username=username, items=items, current_node=current_node,
- current_path=folder_path, breadcrumbs=breadcrumbs, repo_id=REPO_ID, hf_token_read=HF_TOKEN_READ)
+'''
+ return render_template_string(html, username=username, files=all_files, repo_id=REPO_ID, hf_file_url=lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{path}{'?download=true' if download else ''}")
-@app.route('/admhosto/delete_item/', methods=['POST'])
-def admin_delete_item(username):
- if not is_admin(): return redirect(url_for('login'))
+@app.route('/admhosto/delete_user/', methods=['POST'])
+def admin_delete_user(username):
+ if not is_admin():
+ return redirect(url_for('login'))
+ if not HF_TOKEN_WRITE:
+ flash('Удаление невозможно: токен для записи не настроен.')
+ return redirect(url_for('admin_panel'))
data = load_data()
- user_data = data.get('users', {}).get(username)
- if not user_data:
- flash(f'Пользователь "{username}" не найден!', 'error')
+ if username not in data['users']:
+ flash('Пользователь не найден!')
return redirect(url_for('admin_panel'))
- item_path_str = request.form.get('item_path', '').strip()
- if not item_path_str:
- flash('Не указан путь к элементу для удаления.', 'error')
- return redirect(url_for('admin_user_files', username=username))
+ user_data = data['users'][username]
+ logging.warning(f"ADMIN ACTION: Attempting to delete user {username} and all their data.")
- api = HfApi() if HF_TOKEN_WRITE else None
- if not api:
- # Check if trying to delete something that exists in HF
- node_to_delete = get_node_by_path(user_data, item_path_str)
- if node_to_delete and (node_to_delete.get('storage_path') or node_to_delete.get('type') == 'folder'):
- flash('Ошибка конфигурации: Админ не может удалить элементы из облака без токена записи.', 'error')
- parent_path = '/'.join(item_path_str.strip('/').split('/')[:-1])
- return redirect(url_for('admin_user_files', username=username, folder_path=parent_path))
+ # 1. Attempt to delete files from Hugging Face Hub
+ try:
+ api = HfApi()
+ # Construct the base folder path for the user on HF Hub
+ # This assumes the top-level folder structure is consistent
+ user_folder_path_on_hf = f"cloud_files/{username}"
- # Call recursive delete
- deleted, message = delete_node_recursive(user_data, item_path_str, api, username)
+ logging.info(f"Attempting to delete HF Hub folder: {user_folder_path_on_hf} for user {username}")
+ api.delete_folder(
+ folder_path=user_folder_path_on_hf,
+ repo_id=REPO_ID,
+ repo_type="dataset",
+ token=HF_TOKEN_WRITE,
+ commit_message=f"ADMIN ACTION: Deleted all files/folders for user {username}"
+ # ignore_patterns=None, # Be careful with ignore patterns if used
+ )
+ logging.info(f"Successfully initiated deletion of folder {user_folder_path_on_hf} on HF Hub.")
+ # Note: Deletion might be async on HF side.
- if deleted:
- try:
- save_data(data)
- flash(f'Админ удалил "{item_path_str.split("/")[-1]}" пользователя {username}. {message}', 'success')
- except Exception as e:
- flash(f'Элемент удален из структуры, но ошибка сохранения: {e}', 'error')
- else:
- flash(f'Админ: Ошибка удаления "{item_path_str.split("/")[-1]}": {message}', 'error')
+ except hf_utils.HfHubHTTPError as e:
+ # It's possible the folder doesn't exist (e.g., user never uploaded)
+ if e.response.status_code == 404:
+ logging.warning(f"User folder {user_folder_path_on_hf} not found on HF Hub for user {username}. Skipping HF deletion.")
+ else:
+ logging.error(f"Error deleting user folder {user_folder_path_on_hf} from HF Hub for {username}: {e}")
+ flash(f'Ошибка при удалении файлов пользователя {username} с сервера: {e}. Пользователь НЕ удален из базы.')
+ # Stop the process if HF deletion fails critically? Or proceed to delete from DB anyway?
+ # Decision: Stop here to allow manual check/cleanup.
+ return redirect(url_for('admin_panel'))
+ except Exception as e:
+ logging.error(f"Unexpected error during HF Hub folder deletion for {username}: {e}")
+ flash(f'Неожиданная ошибка при удалении файлов {username} с сервера: {e}. Пользователь НЕ удален из базы.')
+ return redirect(url_for('admin_panel'))
- parent_path = '/'.join(item_path_str.strip('/').split('/')[:-1])
- return redirect(url_for('admin_user_files', username=username, folder_path=parent_path))
-@app.route('/admhosto/delete_user/', methods=['POST'])
-def admin_delete_user(username):
- if not is_admin(): return redirect(url_for('login'))
+ # 2. Delete user from the database
+ try:
+ del data['users'][username]
+ save_data(data)
+ flash(f'Пользователь {username} и его файлы (запрос на удаление отправлен) успешно удалены из базы данных!')
+ logging.info(f"ADMIN ACTION: Successfully deleted user {username} from database.")
+ except Exception as e:
+ logging.error(f"Error saving data after deleting user {username}: {e}")
+ flash(f'Файлы пользователя {username} удалены с сервера, но произошла ошибка при удалении пользователя из базы данных: {e}')
+ # Data inconsistency state - requires manual check
+
+ return redirect(url_for('admin_panel'))
+
+
+@app.route('/admhosto/delete_file//', methods=['POST'])
+def admin_delete_file(username, file_id):
+ if not is_admin():
+ return redirect(url_for('login'))
+ if not HF_TOKEN_WRITE:
+ flash('Удаление невозможно: токен для записи не настроен.')
+ return redirect(url_for('admin_user_files', username=username))
data = load_data()
- if username not in data.get('users', {}):
- flash(f'Пользователь "{username}" не найден!', 'error')
+ user_data = data.get('users', {}).get(username)
+ if not user_data:
+ flash(f'Пользователь {username} не найден.')
return redirect(url_for('admin_panel'))
- user_data = data['users'][username]
- api = HfApi() if HF_TOKEN_WRITE else None
+ file_node, parent_node = find_node_by_id(user_data['filesystem'], file_id)
- if not api:
- flash('Ошибка конфигурации: Админ не может удалить пользователя и его файлы из облака без токена записи.', 'error')
- return redirect(url_for('admin_panel'))
+ if not file_node or file_node.get('type') != 'file' or not parent_node:
+ flash('Файл не найден в структуре пользователя.')
+ # Optionally attempt deletion from HF anyway if path known? Risky.
+ return redirect(url_for('admin_user_files', username=username))
+
+ hf_path = file_node.get('path')
+ original_filename = file_node.get('original_filename', 'файл')
+
+ if not hf_path:
+ flash(f'Ошибка: Путь к файлу {original_filename} не найден в метаданных. Удаление только из базы.')
+ # Remove from DB only
+ if remove_node(user_data['filesystem'], file_id):
+ try:
+ save_data(data)
+ flash(f'Метаданные файла {original_filename} удалены (путь отсутствовал).')
+ except Exception as e:
+ flash('Ошибка сохранения данных после удаления метаданных (путь отсутствовал).')
+ logging.error(f"Admin delete file metadata save error (no path): {e}")
+ return redirect(url_for('admin_user_files', username=username))
- # Collect all file paths to delete from HF
- hf_paths_to_delete = []
- def collect_paths(node):
- if node.get('type') == 'file' and node.get('storage_path'):
- hf_paths_to_delete.append(node.get('storage_path'))
- elif node.get('type') == 'folder':
- for child in node.get('children', {}).values():
- collect_paths(child)
- collect_paths(user_data.get('root', {}))
-
- # Delete files from HF Hub
- errors_hf = []
- if hf_paths_to_delete:
- logging.info(f"Admin deleting {len(hf_paths_to_delete)} files for user {username} from HF...")
- # Attempting folder deletion first might be faster if API supports it reliably
- user_cloud_folder = f"{CLOUD_BASE_FOLDER}/{username}"
- try:
- # This might fail if folder isn't empty or API limitations
- api.delete_folder(
- folder_path=user_cloud_folder,
- repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE,
- commit_message=f"Admin deleted all data for user {username}"
- )
- logging.info(f"Successfully deleted HF folder {user_cloud_folder}")
- except Exception as folder_del_err:
- logging.warning(f"Could not delete folder {user_cloud_folder} directly ({folder_del_err}), attempting individual file deletion...")
- for hf_path in hf_paths_to_delete:
- try:
- api.delete_file(path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE)
- except HfHubHTTPError as e:
- if e.response.status_code != 404: # Ignore if already gone
- logging.error(f"Admin: Error deleting file {hf_path} for user {username}: {e}")
- errors_hf.append(hf_path.split('/')[-1])
- except Exception as e:
- logging.error(f"Admin: Unexpected error deleting file {hf_path} for {username}: {e}")
- errors_hf.append(hf_path.split('/')[-1])
-
- # Delete user from JSON data
- del data['users'][username]
+ # Proceed with HF deletion and DB update
try:
- save_data(data)
- msg = f'Пользователь {username} и его данные удалены.'
- if errors_hf:
- msg += f' Ошибки при удалении некоторых файлов из облака: {", ".join(errors_hf)}'
- flash(msg, 'success' if not errors_hf else 'warning')
- logging.info(f"Admin successfully deleted user {username}.")
+ api = HfApi()
+ api.delete_file(
+ path_in_repo=hf_path,
+ repo_id=REPO_ID,
+ repo_type="dataset",
+ token=HF_TOKEN_WRITE,
+ commit_message=f"ADMIN ACTION: Deleted file {original_filename} (ID: {file_id}) for user {username}"
+ )
+ logging.info(f"ADMIN ACTION: Deleted file {hf_path} from HF Hub for user {username}")
+
+ # Now remove from DB
+ if remove_node(user_data['filesystem'], file_id):
+ try:
+ save_data(data)
+ flash(f'Файл {original_filename} успешно удален!')
+ except Exception as e:
+ flash('Файл удален с сервера, но произошла ошибка обновления базы данных.')
+ logging.error(f"Admin delete file DB update error: {e}")
+ else:
+ # Should not happen if found initially
+ flash('Файл удален с сервера, но не найден в базе данных для удаления метаданных.')
+
+ except hf_utils.EntryNotFoundError:
+ logging.warning(f"ADMIN ACTION: File {hf_path} not found on HF Hub during delete for user {username}. Removing from DB.")
+ if remove_node(user_data['filesystem'], file_id):
+ try:
+ save_data(data)
+ flash(f'Файл {original_filename} не найден на сервере, удален из базы.')
+ except Exception as e:
+ flash('Ошибка сохранения данных после удаления метаданных (файл не найден на сервере).')
+ logging.error(f"Admin delete file metadata save error (HF not found): {e}")
+ else:
+ flash('Файл не найден ни на сервере, ни в базе данных.')
+
except Exception as e:
- flash(f'Пользователь удален из облака (с возможными ошибками), но произошла ошибка сохранения локальных данных: {e}', 'error')
- # Critical error, data file might be out of sync
- logging.error(f"CRITICAL: Failed to save data after admin deleted user {username}: {e}")
+ logging.error(f"ADMIN ACTION: Error deleting file {hf_path} for {username}: {e}")
+ flash(f'Ошибка удаления файла {original_filename}: {e}')
- return redirect(url_for('admin_panel'))
+ return redirect(url_for('admin_user_files', username=username))
-if __name__ == '__main__':
- os.makedirs(UPLOADS_DIR, exist_ok=True)
+# --- App Initialization ---
+if __name__ == '__main__':
if not HF_TOKEN_WRITE:
- logging.warning("HF_TOKEN (write access) is not set. File/folder uploads & deletions will fail.")
+ logging.warning("HF_TOKEN (write access) is not set. File uploads, deletions, and backups will fail.")
if not HF_TOKEN_READ:
- 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 not set or lacks read access.")
-
- # Initial data load attempt
- load_data()
+ 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.")
- # Start periodic backup thread only if write token exists
if HF_TOKEN_WRITE:
- backup_thread = threading.Thread(target=periodic_backup, daemon=True)
- backup_thread.start()
+ # Initial download before starting backup thread
+ logging.info("Performing initial database download before starting background backup.")
+ download_db_from_hf()
+ threading.Thread(target=periodic_backup, daemon=True).start()
logging.info("Periodic backup thread started.")
else:
- logging.warning("Periodic backup disabled (HF_TOKEN_WRITE not set).")
+ logging.warning("Periodic backup disabled because HF_TOKEN (write access) is not set.")
+ # Still attempt initial download if read token exists
+ if HF_TOKEN_READ:
+ logging.info("Performing initial database download (read-only mode).")
+ download_db_from_hf()
+ else:
+ logging.warning("No read or write token. Database operations with Hugging Face Hub are disabled.")
+ # Check if local DB exists, if not create empty one
+ if not os.path.exists(DATA_FILE):
+ with open(DATA_FILE, 'w', encoding='utf-8') as f:
+ json.dump({'users': {}}, f)
+ logging.info(f"Created empty local database file: {DATA_FILE}")
+
- # Run Flask app
app.run(debug=False, host='0.0.0.0', port=7860)
# --- END OF FILE app (8).py ---
\ No newline at end of file