diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,6 +1,6 @@ # --- START OF FILE app (8).py --- -from flask import Flask, render_template_string, request, redirect, url_for, session, flash, send_file, jsonify +from flask import Flask, render_template_string, request, redirect, url_for, session, flash, send_file, jsonify, Response from flask_caching import Cache import json import os @@ -8,62 +8,133 @@ import logging import threading import time from datetime import datetime -from huggingface_hub import HfApi, hf_hub_download, HfHubHTTPError +from huggingface_hub import HfApi, hf_hub_download, utils as hf_utils from werkzeug.utils import secure_filename import requests from io import BytesIO import uuid -from pathlib import Path app = Flask(__name__) -app.secret_key = os.getenv("FLASK_SECRET_KEY", "supersecretkey_for_zeus_cloud_v2") +app.secret_key = os.getenv("FLASK_SECRET_KEY", "supersecretkey_folders_unique") DATA_FILE = 'cloudeng_data_v2.json' -REPO_ID = "Eluza133/Z1e1u" +REPO_ID = "Eluza133/Z1e1u" # Replace with your actual repo ID if different HF_TOKEN_WRITE = os.getenv("HF_TOKEN") HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") or HF_TOKEN_WRITE -UPLOADS_DIR = 'uploads' -CLOUD_BASE_FOLDER = 'cloud_files' +UPLOAD_FOLDER = 'uploads' +os.makedirs(UPLOAD_FOLDER, exist_ok=True) cache = Cache(app, config={'CACHE_TYPE': 'simple'}) logging.basicConfig(level=logging.INFO) +# --- Filesystem Helper Functions --- + +def find_node_by_id(filesystem, node_id): + if filesystem.get('id') == node_id: + return filesystem, None # Node found, no parent (it's the root) + + queue = [(filesystem, None)] # (node, parent) + while queue: + current_node, parent = queue.pop(0) + if current_node.get('type') == 'folder' and 'children' in current_node: + for i, child in enumerate(current_node['children']): + if child.get('id') == node_id: + return child, current_node # Node found, return node and its parent + if child.get('type') == 'folder': + queue.append((child, current_node)) + return None, None # Node not found + +def add_node(filesystem, parent_id, node_data): + parent_node, _ = find_node_by_id(filesystem, parent_id) + if parent_node and parent_node.get('type') == 'folder': + if 'children' not in parent_node: + parent_node['children'] = [] + parent_node['children'].append(node_data) + return True + return False + +def remove_node(filesystem, node_id): + node_to_remove, parent_node = find_node_by_id(filesystem, node_id) + if node_to_remove and parent_node and 'children' in parent_node: + parent_node['children'] = [child for child in parent_node['children'] if child.get('id') != node_id] + return True + # Cannot remove root or if node/parent not found correctly + return False + +def get_node_path_string(filesystem, node_id): + path_list = [] + current_id = node_id + + while current_id: + node, parent = find_node_by_id(filesystem, current_id) + if not node: + break # Should not happen in a consistent tree + if node.get('id') != 'root': # Don't add root name to path string + path_list.append(node.get('name', node.get('original_filename', ''))) + if not parent: # Reached root's parent (None) + break + current_id = parent.get('id') if parent else None + + return " / ".join(reversed(path_list)) or "Root" + + +def initialize_user_filesystem(user_data): + if 'filesystem' not in user_data: + user_data['filesystem'] = { + "type": "folder", + "id": "root", + "name": "root", + "children": [] + } + # Migrate old files if they exist + if 'files' in user_data and isinstance(user_data['files'], list): + for old_file in user_data['files']: + # Attempt to create a somewhat unique ID based on old data, or generate new one + file_id = old_file.get('id', uuid.uuid4().hex) + original_filename = old_file.get('filename', 'unknown_file') + # Assume old paths didn't have unique names or folder structure in the path itself + # Create a unique name now for consistency + name_part, ext_part = os.path.splitext(original_filename) + unique_suffix = uuid.uuid4().hex[:8] + unique_filename = f"{name_part}_{unique_suffix}{ext_part}" + # Assume old files were at the 'root' level equivalent + hf_path = f"cloud_files/{session['username']}/root/{unique_filename}" # New standard path + + file_node = { + 'type': 'file', + 'id': file_id, + 'original_filename': original_filename, + 'unique_filename': unique_filename, + 'path': hf_path, # Store the intended *new* path structure + 'file_type': get_file_type(original_filename), + 'upload_date': old_file.get('upload_date', datetime.now().strftime('%Y-%m-%d %H:%M:%S')) + } + add_node(user_data['filesystem'], 'root', file_node) + # Note: This doesn't automatically rename/move files on HF Hub. + # It just structures the metadata according to the new system. + # Manual migration on HF Hub might be needed for old files to match `hf_path`. + del user_data['files'] # Remove the old flat list + + # --- Data Handling --- -@cache.memoize(timeout=60) +@cache.memoize(timeout=300) def load_data(): try: - if not os.path.exists(DATA_FILE): - logging.info(f"{DATA_FILE} not found locally, attempting download from HF.") - download_db_from_hf() # This will create an empty one if download fails - + download_db_from_hf() with open(DATA_FILE, 'r', encoding='utf-8') as file: - try: - data = json.load(file) - except json.JSONDecodeError: - logging.warning("JSONDecodeError: Data file is corrupted or empty. Initializing empty database.") - return {'users': {}} # Return only users initially - + data = json.load(file) if not isinstance(data, dict): logging.warning("Data is not in dict format, initializing empty database") return {'users': {}} - - # Ensure basic structure and initialize root folder for users if missing data.setdefault('users', {}) + # Initialize filesystem for users who don't have one (new structure) for username, user_data in data['users'].items(): - if 'root' not in user_data: - user_data['root'] = { - "type": "folder", - "name": "/", - "children": {} - } - logging.info("Data successfully loaded and validated") + initialize_user_filesystem(user_data) # Use temporary session context if needed or pass username + logging.info("Data successfully loaded and initialized") return data - except FileNotFoundError: - logging.warning(f"Data file {DATA_FILE} not found after attempting download. Initializing empty database.") - return {'users': {}} except Exception as e: logging.error(f"Error loading data: {e}") - return {'users': {}} # Safer default + return {'users': {}} def save_data(data): try: @@ -78,7 +149,7 @@ def save_data(data): def upload_db_to_hf(): if not HF_TOKEN_WRITE: - logging.warning("HF_TOKEN_WRITE not set. Skipping database upload.") + logging.warning("HF_TOKEN_WRITE not set, skipping database upload.") return try: api = HfApi() @@ -96,7 +167,7 @@ def upload_db_to_hf(): def download_db_from_hf(): if not HF_TOKEN_READ: - logging.warning("HF_TOKEN_READ not set. Skipping database download.") + logging.warning("HF_TOKEN_READ not set, skipping database download.") if not os.path.exists(DATA_FILE): with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}}, f) @@ -108,172 +179,55 @@ def download_db_from_hf(): repo_type="dataset", token=HF_TOKEN_READ, local_dir=".", - local_dir_use_symlinks=False, - force_download=True # Ensure we get the latest version + local_dir_use_symlinks=False ) logging.info("Database downloaded from Hugging Face") - except HfHubHTTPError as e: - logging.error(f"Error downloading database (HTTP Error): {e}") - if e.response.status_code == 404: - logging.info("Database file not found on Hugging Face. Creating a new local file.") - if not os.path.exists(DATA_FILE): - with open(DATA_FILE, 'w', encoding='utf-8') as f: - json.dump({'users': {}}, f) - elif not os.path.exists(DATA_FILE): - logging.warning("Download failed, and local file doesn't exist. Creating empty file.") + except hf_utils.RepositoryNotFoundError: + logging.error(f"Repository {REPO_ID} not found.") + if not os.path.exists(DATA_FILE): with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}}, f) + except hf_utils.EntryNotFoundError: + logging.warning(f"{DATA_FILE} not found in repository {REPO_ID}. Initializing empty database.") + if not os.path.exists(DATA_FILE): + with open(DATA_FILE, 'w', encoding='utf-8') as f: + json.dump({'users': {}}, f) except Exception as e: - logging.error(f"Generic error downloading database: {e}") + logging.error(f"Error downloading database: {e}") if not os.path.exists(DATA_FILE): - logging.warning("Generic download error, and local file doesn't exist. Creating empty file.") with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}}, f) def periodic_backup(): while True: - time.sleep(1800) # Backup every 30 minutes - logging.info("Initiating periodic backup...") upload_db_to_hf() - -# --- Helper Functions --- - -def get_node_by_path(user_data, path_str): - """Navigates the user's file tree and returns the node at the given path.""" - if path_str == '/' or not path_str: - return user_data.get('root') - - parts = [part for part in path_str.strip('/').split('/') if part] - current_node = user_data.get('root') - if not current_node: - return None - - for part in parts: - if current_node.get('type') == 'folder' and part in current_node.get('children', {}): - current_node = current_node['children'][part] - else: - return None # Path not found - return current_node - -def add_node(user_data, parent_path_str, node_data): - """Adds a new node (file or folder) to the parent path.""" - parent_node = get_node_by_path(user_data, parent_path_str) - if not parent_node or parent_node.get('type') != 'folder': - logging.error(f"Cannot add node: Parent path '{parent_path_str}' not found or is not a folder.") - return False - - node_name = node_data.get('name') - if not node_name: - logging.error("Cannot add node: Node data must include a 'name'.") - return False - - if node_name in parent_node.get('children', {}): - logging.warning(f"Node '{node_name}' already exists in '{parent_path_str}'. Overwriting not implemented here.") - # Or decide how to handle conflicts, maybe return False - return False # Prevent overwriting for now - - if 'children' not in parent_node: - parent_node['children'] = {} - - parent_node['children'][node_name] = node_data - logging.info(f"Node '{node_name}' added to '{parent_path_str}'.") - return True - -def delete_node_recursive(user_data, path_str, api, username): - """Deletes a node and its children (if a folder) from JSON and HF Hub.""" - node_to_delete = get_node_by_path(user_data, path_str) - if not node_to_delete: - logging.warning(f"Node not found for deletion: {path_str}") - return False, "Node not found" - - parent_path_str = '/'.join(path_str.strip('/').split('/')[:-1]) - if not parent_path_str: - parent_path_str = '/' - node_name = path_str.strip('/').split('/')[-1] - - parent_node = get_node_by_path(user_data, parent_path_str) - - paths_to_delete_hf = [] - def collect_hf_paths(node): - if node.get('type') == 'file': - paths_to_delete_hf.append(node.get('storage_path')) - elif node.get('type') == 'folder': - for child in node.get('children', {}).values(): - collect_hf_paths(child) - - collect_hf_paths(node_to_delete) - - # Delete from HF Hub first - deleted_count_hf = 0 - errors_hf = [] - if HF_TOKEN_WRITE: - for hf_path in paths_to_delete_hf: - if hf_path: # Ensure path exists - try: - logging.info(f"Attempting to delete from HF: {hf_path}") - api.delete_file( - path_in_repo=hf_path, - repo_id=REPO_ID, - repo_type="dataset", - token=HF_TOKEN_WRITE, - ) - deleted_count_hf += 1 - except HfHubHTTPError as e: - if e.response.status_code == 404: - logging.warning(f"File not found on HF Hub (already deleted?): {hf_path}") - # Count as success if not found, maybe it was already gone - deleted_count_hf += 1 - else: - logging.error(f"Error deleting file {hf_path} from HF: {e}") - errors_hf.append(f"Failed to delete {hf_path.split('/')[-1]}") - except Exception as e: - logging.error(f"Unexpected error deleting file {hf_path} from HF: {e}") - errors_hf.append(f"Error deleting {hf_path.split('/')[-1]}") - else: - logging.warning("HF_TOKEN_WRITE not set. Skipping deletion from HF Hub.") - errors_hf.append("Cannot delete from cloud: Write token missing.") - - - # Delete from JSON structure if HF deletion was at least partially successful or skipped - if parent_node and node_name in parent_node.get('children', {}): - del parent_node['children'][node_name] - logging.info(f"Node '{node_name}' removed from JSON structure.") - if not errors_hf: - return True, "Deleted successfully." - else: - # Report partial success with errors - return True, f"Deleted from storage structure. HF deletion issues: {'; '.join(errors_hf)}" - else: - logging.error(f"Could not find node '{node_name}' in parent '{parent_path_str}' for JSON deletion.") - # Even if JSON delete fails, report HF errors if any - if errors_hf: - return False, f"JSON deletion failed. HF deletion issues: {'; '.join(errors_hf)}" - else: - return False, "Failed to delete from storage structure." - + time.sleep(1800) # 30 minutes def get_file_type(filename): - ext = Path(filename).suffix.lower() - if ext in ('.mp4', '.mov', '.avi', '.webm', '.mkv'): + filename_lower = filename.lower() + if filename_lower.endswith(('.mp4', '.mov', '.avi', '.webm', '.mkv')): return 'video' - elif ext in ('.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg'): + elif filename_lower.endswith(('.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg')): return 'image' - elif ext == '.pdf': + elif filename_lower.endswith('.pdf'): return 'pdf' - elif ext in ('.txt', '.md', '.log', '.py', '.js', '.css', '.html', '.json', '.xml', '.csv'): + elif filename_lower.endswith('.txt'): return 'text' - else: - return 'other' + # Add more types as needed + # elif filename_lower.endswith(('.doc', '.docx')): + # return 'document' + return 'other' + -# --- Base Style --- +# --- Base Style (unchanged from original) --- BASE_STYLE = ''' :root { --primary: #ff4d6d; --secondary: #00ddeb; --accent: #8b5cf6; --background-light: #f5f6fa; --background-dark: #1a1625; --card-bg: rgba(255, 255, 255, 0.95); --card-bg-dark: rgba(40, 35, 60, 0.95); - --text-light: #2a1e5a; --text-dark: #e8e1ff; - --shadow: 0 10px 30px rgba(0, 0, 0, 0.2); --glass-bg: rgba(255, 255, 255, 0.15); - --transition: all 0.3s ease; --delete-color: #ff4444; --folder-color: #ffcc00; + --text-light: #2a1e5a; --text-dark: #e8e1ff; --shadow: 0 10px 30px rgba(0, 0, 0, 0.2); + --glass-bg: rgba(255, 255, 255, 0.15); --transition: all 0.3s ease; --delete-color: #ff4444; + --folder-color: #ffc107; } * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Inter', sans-serif; background: var(--background-light); color: var(--text-light); line-height: 1.6; } @@ -286,18 +240,15 @@ body.dark h2 { color: var(--text-dark); } 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); } body.dark input, body.dark textarea { color: var(--text-dark); } input:focus, textarea:focus { outline: none; box-shadow: 0 0 0 4px var(--primary); } -.btn { padding: 10px 20px; background: var(--primary); color: white; border: none; border-radius: 12px; cursor: pointer; font-size: 1em; font-weight: 600; transition: var(--transition); box-shadow: var(--shadow); display: inline-block; text-decoration: none; margin: 5px; } +.btn { padding: 14px 28px; background: var(--primary); color: white; border: none; border-radius: 14px; cursor: pointer; font-size: 1.1em; font-weight: 600; transition: var(--transition); box-shadow: var(--shadow); display: inline-block; text-decoration: none; margin-top: 5px; margin-right: 5px; } .btn:hover { transform: scale(1.05); background: #e6415f; } .download-btn { background: var(--secondary); } .download-btn:hover { background: #00b8c5; } .delete-btn { background: var(--delete-color); } .delete-btn:hover { background: #cc3333; } -.folder-btn { background: var(--folder-color); color: #333; } -.folder-btn:hover { background: #e6b800; } -.flash { padding: 10px; margin-bottom: 15px; border-radius: 8px; text-align: center; } -.flash.success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; } -.flash.error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; } -.flash.info { background-color: #d1ecf1; color: #0c5460; border: 1px solid #bee5eb; } +.folder-btn { background: var(--folder-color); } +.folder-btn:hover { background: #e6a000; } +.flash { color: var(--secondary); text-align: center; margin-bottom: 15px; } .file-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 20px; margin-top: 20px; } .user-list { margin-top: 20px; } .user-item { padding: 15px; background: var(--card-bg); border-radius: 16px; margin-bottom: 10px; box-shadow: var(--shadow); transition: var(--transition); } @@ -305,34 +256,36 @@ body.dark .user-item { background: var(--card-bg-dark); } .user-item:hover { transform: translateY(-5px); } .user-item a { color: var(--primary); text-decoration: none; font-weight: 600; } .user-item a:hover { color: var(--accent); } +@media (max-width: 768px) { .file-grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); } } +@media (max-width: 480px) { .file-grid { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); } } .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; } body.dark .item { background: var(--card-bg-dark); } .item:hover { transform: translateY(-5px); } -.item-icon { font-size: 3em; margin-bottom: 10px; } -.folder-icon { color: var(--folder-color); } -.file-icon { color: var(--secondary); } -.item-preview { max-width: 100%; height: 150px; object-fit: cover; border-radius: 10px; margin-bottom: 10px; cursor: pointer; background-color: #eee; display: flex; align-items: center; justify-content: center; font-size: 2em; } -body.dark .item-preview { background-color: #333; } +.item-preview { max-width: 100%; height: 130px; object-fit: cover; border-radius: 10px; margin-bottom: 10px; cursor: pointer; display: block; } +.item.folder .item-preview { object-fit: contain; font-size: 60px; color: var(--folder-color); line-height: 130px; } /* Folder icon styling */ .item p { font-size: 0.9em; margin: 5px 0; word-break: break-all; } .item a { color: var(--primary); text-decoration: none; } .item a:hover { color: var(--accent); } .item-actions { margin-top: 10px; } -.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.85); z-index: 2000; justify-content: center; align-items: center; padding: 20px; } -.modal-content { background: white; padding: 20px; border-radius: 15px; max-width: 90%; max-height: 90%; overflow: auto; position: relative; } +.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; } +.modal-content { max-width: 95%; max-height: 95%; background: #fff; padding: 10px; border-radius: 15px; overflow: auto; } body.dark .modal-content { background: var(--card-bg-dark); } -.modal-close { position: absolute; top: 10px; right: 15px; font-size: 2em; color: #aaa; cursor: pointer; line-height: 1; } -body.dark .modal-close { color: #ccc; } -.modal img, .modal video, .modal iframe, .modal embed { display: block; max-width: 100%; max-height: 80vh; margin: 0 auto; border-radius: 10px; } -.modal pre { white-space: pre-wrap; word-wrap: break-word; max-height: 80vh; overflow-y: auto; background: #f8f8f8; padding: 15px; border-radius: 8px; color: #333; } -body.dark .modal pre { background: #222; color: #eee; } +.modal img, .modal video, .modal iframe, .modal pre { max-width: 100%; max-height: 85vh; display: block; margin: auto; border-radius: 10px; } +.modal iframe { width: 80vw; height: 85vh; border: none; } +.modal pre { background: #eee; color: #333; padding: 15px; border-radius: 8px; white-space: pre-wrap; word-wrap: break-word; text-align: left; max-height: 85vh; overflow-y: auto;} +body.dark .modal pre { background: #2b2a33; color: var(--text-dark); } #progress-container { width: 100%; background: var(--glass-bg); border-radius: 10px; margin: 15px 0; display: none; } #progress-bar { width: 0%; height: 20px; background: var(--primary); border-radius: 10px; transition: width 0.3s ease; } .breadcrumbs { margin-bottom: 20px; font-size: 1.1em; } .breadcrumbs a { color: var(--accent); text-decoration: none; } +.breadcrumbs a:hover { text-decoration: underline; } .breadcrumbs span { margin: 0 5px; color: #aaa; } +.folder-actions { margin-top: 20px; margin-bottom: 10px; display: flex; gap: 10px; align-items: center; flex-wrap: wrap; } +.folder-actions input[type=text] { width: auto; flex-grow: 1; margin: 0; } +.folder-actions .btn { margin: 0; } ''' -# --- Flask Routes --- +# --- Routes --- @app.route('/register', methods=['GET', 'POST']) def register(): @@ -341,674 +294,503 @@ def register(): password = request.form.get('password') if not username or not password: - flash('Имя пользователя и пароль обязательны!', 'error') + flash('Имя пользователя и пароль обязательны!') return redirect(url_for('register')) - - # Basic validation if not username.isalnum() or len(username) < 3: - flash('Имя пользователя должно быть не менее 3 символов и содержать только буквы и цифры.', 'error') - return redirect(url_for('register')) - if len(password) < 6: - flash('Пароль должен быть не менее 6 символов.', 'error') + flash('Имя пользователя должно содержать только буквы и цифры (минимум 3 символа)!') return redirect(url_for('register')) data = load_data() if username in data['users']: - flash('Пользователь с таким именем уже существует!', 'error') + flash('Пользователь с таким именем уже существует!') return redirect(url_for('register')) data['users'][username] = { - 'password': password, # TODO: Hash passwords in a real app! + 'password': password, # Plain text - consider hashing in production! 'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), - 'root': { # Initialize root folder - "type": "folder", - "name": "/", - "children": {} + 'filesystem': { + "type": "folder", "id": "root", "name": "root", "children": [] } } try: save_data(data) session['username'] = username - flash('Регистрация прошла успешно!', 'success') + flash('Регистрация прошла успешно!') return redirect(url_for('dashboard')) except Exception as e: - flash('Ошибка при сохранении данных пользователя.', 'error') + flash('Ошибка сохранения данных при регистрации.') logging.error(f"Registration save error: {e}") return redirect(url_for('register')) html = ''' - - - - - Регистрация - Zeus Cloud V2 - - - - -
-

Регистрация в Zeus Cloud V2

- {% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} - {% for category, message in messages %} -
{{ message }}
- {% endfor %} - {% endif %} - {% endwith %} -
- - - -
-

Уже есть аккаунт? Войти

-
- - -''' + +Регистрация - Zeus Cloud +

Регистрация в Zeus Cloud

+{% with messages = get_flashed_messages() %}{% if messages %}{% for message in messages %}
{{ message }}
{% endfor %}{% endif %}{% endwith %} +
+
+

Уже есть аккаунт? Войти

''' 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 %} -
{{ message }}
- {% endfor %} - {% endif %} - {% endwith %} -
-
- - - -
-

Нет аккаунта? Зарегистрируйтесь

-
- - - -''' + +Zeus Cloud +

Zeus Cloud

+{% with messages = get_flashed_messages() %}{% if messages %}{% for message in messages %}
{{ message }}
{% endfor %}{% endif %}{% endwith %}
+
+
+

Нет аккаунта? Зарегистрируйтесь

+''' return render_template_string(html) -@app.route('/dashboard/') -@app.route('/dashboard/') -def dashboard(folder_path='/'): +@app.route('/dashboard', methods=['GET', 'POST']) +def dashboard(): 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', {}): + if username not in data['users']: session.pop('username', None) - flash('Пользователь не найден!', 'error') + flash('Пользователь не найден!') return redirect(url_for('login')) user_data = data['users'][username] + # Ensure filesystem exists (should be handled by load_data/login now) + if 'filesystem' not in user_data: + initialize_user_filesystem(user_data) + # No need to save here usually, but good practice if modified - # Normalize folder_path - folder_path = '/' + folder_path.strip('/') - if folder_path != '/' and not folder_path.endswith('/'): - folder_path += '/' - - current_node = get_node_by_path(user_data, folder_path) - - if not current_node or current_node.get('type') != 'folder': - flash('Папка не найдена!', 'error') - return redirect(url_for('dashboard', folder_path='/')) # Redirect to root - - items = [] - # Sort folders first, then files, both alphabetically - children = current_node.get('children', {}) - sorted_names = sorted(children.keys()) - - for name in sorted_names: - child = children[name] - if child.get('type') == 'folder': - items.append(child) - - for name in sorted_names: - child = children[name] - if child.get('type') == 'file': - # Ensure necessary keys exist - child.setdefault('original_filename', name) - child.setdefault('storage_path', 'Unknown Path') - child.setdefault('upload_date', 'N/A') - child['file_type'] = get_file_type(child['original_filename']) - items.append(child) - - - # Breadcrumbs - breadcrumbs = [{'name': 'Корень', 'path': '/'}] - cumulative_path = '/' - for part in [p for p in folder_path.strip('/').split('/') if p]: - cumulative_path += part + '/' - breadcrumbs.append({'name': part, 'path': cumulative_path}) + current_folder_id = request.args.get('folder_id', 'root') + current_folder, parent_folder = find_node_by_id(user_data['filesystem'], current_folder_id) - html = ''' - - - - - Панель управления - Zeus Cloud V2 - - - - - -
-

Zeus Cloud V2

-

Пользователь: {{ username }} | Выйти

- - {% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} - {% for category, message in messages %} -
{{ message }}
- {% endfor %} - {% endif %} - {% endwith %} - - + 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')) -
-
- - - - -
-
- - - -
-
-
- - -

Содержимое папки: {{ current_node.name }}

-
- {% for item in items %} -
- {% if item.type == 'folder' %} -
-

{{ item.name }}

-
- Открыть -
- - -
-
- {% elif item.type == 'file' %} -
- {% if item.file_type == 'image' %} - {{ item.original_filename }} - {% elif item.file_type == 'video' %} - - {% elif item.file_type == 'pdf' %} - - {% elif item.file_type == 'text' %} - - {% else %} - - {% endif %} -
-

{{ item.original_filename | truncate(25) }}

-

Тип: {{ item.file_type }} | {{ item.upload_date }}

-
- Скачать -
- - -
-
- {% endif %} -
- {% else %} -

Эта папка пуста.

- {% endfor %} -
-
-

Добавить на главный экран

-

Для быстрого доступа к Zeus Cloud V2, добавьте приложение на главный экран:

-
    -
  • Android (Chrome): Меню (три точки) -> "Установить приложение" или "Добавить на главный экран".
  • -
  • iOS (Safari): Кнопка "Поделиться" (квадрат со стрелкой) -> "На экран «Домой»".
  • -
-
-
+ items_in_folder = sorted(current_folder.get('children', []), key=lambda x: (x['type'] != 'folder', x.get('name', x.get('original_filename', '')).lower())) - - - - -''' - 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) + # Build breadcrumbs + breadcrumbs = [] + temp_id = current_folder_id + while temp_id: + node, parent = find_node_by_id(user_data['filesystem'], temp_id) + if not node: break + is_link = (node['id'] != current_folder_id) # Don't link the current folder + breadcrumbs.append({'id': node['id'], 'name': node.get('name', 'Root'), 'is_link': is_link}) + if not parent: break + temp_id = parent.get('id') + breadcrumbs.reverse() -@app.route('/upload', methods=['POST']) -def upload_files(): - if 'username' not in session: - return jsonify({'status': 'error', 'message': 'Не авторизован'}), 401 + html = ''' + +Панель управления - Zeus Cloud +
+

Zeus Cloud

Пользователь: {{ username }}

+{% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} +{% endwith %} - username = session['username'] - data = load_data() - if username not in data.get('users', {}): - return jsonify({'status': 'error', 'message': 'Пользователь не найден'}), 403 + - user_data = data['users'][username] - current_path_str = request.form.get('current_path', '/') - current_path_str = '/' + current_path_str.strip('/') - if current_path_str != '/': - current_path_str += '/' +
+
+ + + +
+
- 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 +

Содержимое папки: {{ current_folder.name if current_folder_id != 'root' else 'Главная' }}

+
+ {% for item in items %} +
+ {% if item.type == 'folder' %} + 📁 +

{{ item.name }}

+
+ Открыть +
+ + +
+
+ {% elif item.type == 'file' %} + {% set previewable = item.file_type in ['image', 'video', 'pdf', 'text'] %} + {% if item.file_type == 'image' %} + {{ item.original_filename }} + {% elif item.file_type == 'video' %} + + {% elif item.file_type == 'pdf' %} +
📄
+ {% elif item.file_type == 'text' %} +
📝
+ {% else %} +
+ {% endif %} +

{{ item.original_filename | truncate(25, True) }}

+

{{ item.upload_date }}

+
+ Скачать + {% if previewable %} + + {% endif %} +
+ + +
+
+ {% endif %} +
+ {% endfor %} + {% if not items %}

Эта папка пуста.

{% endif %} +
- if len(files) > 20: - return jsonify({'status': 'error', 'message': 'Максимум 20 файлов за раз'}), 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 %}
{{ message }}
{% endfor %}{% endif %}{% endwith %} +

Пользователи

+{% for user in user_details %}
{{ user.username }} -

Дата регистрации: {{ user.created_at }} | Файлов: {{ user.file_count }}

+

Зарегистрирован: {{ user.created_at }}

+

Файлов: {{ user.file_count }}

- +
-{% else %}

Пользователей пока нет.

{% endfor %}
-{% with messages = get_flashed_messages(with_categories=true) %} -{% if messages %}
-{% for category, message in messages %}
{{ message }}
{% endfor %} -
{% endif %}{% endwith %}
-''' - return render_template_string(html, user_list=user_list) +{% else %}

Пользователей нет.

{% endfor %}
''' + return render_template_string(html, user_details=user_details) -@app.route('/admhosto/user//') -@app.route('/admhosto/user//') -def admin_user_files(username, folder_path='/'): - if not is_admin(): return redirect(url_for('login')) +@app.route('/admhosto/user/') +def admin_user_files(username): + if not is_admin(): + return redirect(url_for('login')) data = load_data() user_data = data.get('users', {}).get(username) if not user_data: - flash(f'Пользователь "{username}" не найден!', 'error') + flash(f'Пользователь {username} не найден.') return redirect(url_for('admin_panel')) - folder_path = '/' + folder_path.strip('/') - if folder_path != '/' and not folder_path.endswith('/'): - folder_path += '/' - - current_node = get_node_by_path(user_data, folder_path) - if not current_node or current_node.get('type') != 'folder': - flash('Папка не найдена для этого пользователя!', 'error') - return redirect(url_for('admin_user_files', username=username)) # Redirect to user's root - - items = [] - children = current_node.get('children', {}) - sorted_names = sorted(children.keys()) + # Flatten the file structure for simple display + all_files = [] + def collect_files(folder): + for item in folder.get('children', []): + if item.get('type') == 'file': + # Add parent folder info for context + item['parent_path_str'] = get_node_path_string(user_data['filesystem'], folder.get('id', 'root')) + all_files.append(item) + elif item.get('type') == 'folder': + collect_files(item) - for name in sorted_names: - child = children[name] - if child.get('type') == 'folder': - items.append(child) - - for name in sorted_names: - child = children[name] - if child.get('type') == 'file': - child.setdefault('original_filename', name) - child.setdefault('storage_path', 'Unknown Path') - child.setdefault('upload_date', 'N/A') - child['file_type'] = get_file_type(child['original_filename']) - items.append(child) - - breadcrumbs = [{'name': 'Корень', 'path': '/'}] - cumulative_path = '/' - for part in [p for p in folder_path.strip('/').split('/') if p]: - cumulative_path += part + '/' - breadcrumbs.append({'name': part, 'path': cumulative_path}) + collect_files(user_data.get('filesystem', {})) + all_files.sort(key=lambda x: x.get('upload_date', ''), reverse=True) html = ''' - -Файлы пользователя {{ username }} - Zeus Cloud V2 +Файлы {{ username }} - -
-

Файлы пользователя: {{ username }}

-Назад к списку -{% with messages = get_flashed_messages(with_categories=true) %} -{% if messages %}{% for category, message in messages %}
{{ message }}
{% endfor %}{% endif %}{% endwith %} - - - -

Содержимое: {{ current_node.name }}

-
-{% for item in items %} -
- {% if item.type == 'folder' %} -
-

{{ item.name }}

-
- Открыть -
- - -
-
- {% elif item.type == 'file' %} -
- {% if item.file_type == 'image' %} - {{ item.original_filename }} - {% elif item.file_type == 'video' %} - {% elif item.file_type == 'pdf' %} - {% elif item.file_type == 'text' %} - {% else %}{% endif %} -
-

{{ item.original_filename | truncate(25) }}

-

Тип: {{ item.file_type }} | {{ item.upload_date }}

-

Путь: {{ item.storage_path }}

-
- Скачать -
- - -
-
- {% endif %} +

Файлы пользователя: {{ username }}

+Назад к пользователям +{% with messages = get_flashed_messages() %}{% if messages %}{% for message in messages %}
{{ message }}
{% endfor %}{% endif %}{% endwith %} +
+{% for file in files %} +
+ {% if file.file_type == 'image' %} + {% elif file.file_type == 'video' %} + {% elif file.file_type == 'pdf' %}
📄
+ {% elif file.file_type == 'text' %}
📝
+ {% else %}
{% endif %} +

{{ file.original_filename | truncate(30) }}

+

В папке: {{ file.parent_path_str }}

+

Загружен: {{ file.upload_date }}

+

ID: {{ file.id }}

+

Path: {{ file.path }}

+
+ Скачать +
+ +
-{% else %}

Папка пуста.

{% endfor %} -
- -
- -''' - 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