diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,3 +1,5 @@ +# --- START OF FILE app (8).py --- + from flask import Flask, render_template_string, request, redirect, url_for, session, flash, send_file, jsonify from flask_caching import Cache import json @@ -6,66 +8,62 @@ import logging import threading import time from datetime import datetime -from huggingface_hub import HfApi, hf_hub_download +from huggingface_hub import HfApi, hf_hub_download, HfHubHTTPError 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") -DATA_FILE = 'cloudeng_data.json' +app.secret_key = os.getenv("FLASK_SECRET_KEY", "supersecretkey_for_zeus_cloud_v2") +DATA_FILE = 'cloudeng_data_v2.json' REPO_ID = "Eluza133/Z1e1u" 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' cache = Cache(app, config={'CACHE_TYPE': 'simple'}) logging.basicConfig(level=logging.INFO) -@cache.memoize(timeout=300) +# --- Data Handling --- + +@cache.memoize(timeout=60) def load_data(): try: - download_db_from_hf() + 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 + with open(DATA_FILE, 'r', encoding='utf-8') as file: - data = json.load(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 + if not isinstance(data, dict): + logging.warning("Data is not in dict format, initializing empty database") return {'users': {}} - data.setdefault('users', {}) - # Initialize 'items' list for users if migrating from old structure - for user_data in data['users'].values(): - if 'files' in user_data: - if 'items' not in user_data or not user_data['items']: - # Convert old 'files' list to new 'items' format - user_data['items'] = [] - for file_info in user_data['files']: - # Assuming old files were in the root - original_filename = file_info.get('filename', 'unknown_file') - unique_path = file_info.get('path') # Old path might be unique enough or need migration - # If unique_path wasn't stored before, generate one (simple migration assumption) - if not unique_path or not os.path.basename(unique_path).startswith(file_info.get('filename', '')): - unique_id = uuid.uuid4().hex - sanitized_name = secure_filename(original_filename) - unique_filename = f"{unique_id}_{sanitized_name}" - unique_path = f"cloud_files/{file_info.get('username')}/{unique_filename}" # Need username here, but old structure doesn't store it per file - - # This migration is imperfect without knowing the old path structure exactly - # A safer migration would iterate the HF repo structure - # For simplicity here, we assume files were at cloud_files/user/filename and might need new unique ID if filename wasn't sufficient - # Let's stick to the assumption that old 'path' was unique enough for HF but not necessarily for display filename duplicates - user_data['items'].append({ - 'type': 'file', - 'name': original_filename, - 'original_filename': original_filename, - 'unique_path': file_info['path'], # Use existing path from old structure - 'parent_path': os.path.dirname(file_info['path']).rstrip('/') + '/', # Infer parent path (should be cloud_files/username/) - 'upload_date': file_info.get('upload_date', datetime.now().strftime('%Y-%m-%d %H:%M:%S')), - 'preview_type': get_preview_type(original_filename) - }) - del user_data['files'] # Remove old key after migration + # Ensure basic structure and initialize root folder for users if missing + data.setdefault('users', {}) + 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") 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: - return {'users': {}} + logging.error(f"Error loading data: {e}") + return {'users': {}} # Safer default def save_data(data): try: @@ -73,11 +71,14 @@ def save_data(data): json.dump(data, file, ensure_ascii=False, indent=4) upload_db_to_hf() cache.clear() + logging.info("Data saved and uploaded to HF") except Exception as e: + logging.error(f"Error saving data: {e}") raise def upload_db_to_hf(): if not HF_TOKEN_WRITE: + logging.warning("HF_TOKEN_WRITE not set. Skipping database upload.") return try: api = HfApi() @@ -89,10 +90,17 @@ def upload_db_to_hf(): token=HF_TOKEN_WRITE, commit_message=f"Backup {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" ) + logging.info("Database uploaded to Hugging Face") except Exception as e: logging.error(f"Error uploading database: {e}") def download_db_from_hf(): + if not HF_TOKEN_READ: + 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) + return try: hf_hub_download( repo_id=REPO_ID, @@ -100,401 +108,297 @@ def download_db_from_hf(): repo_type="dataset", token=HF_TOKEN_READ, local_dir=".", - local_dir_use_symlinks=False + local_dir_use_symlinks=False, + force_download=True # Ensure we get the latest version ) + 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.") + with open(DATA_FILE, 'w', encoding='utf-8') as f: + json.dump({'users': {}}, f) except Exception as e: - logging.error(f"Error downloading database: {e}") + logging.error(f"Generic 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(): - if not HF_TOKEN_WRITE: - return while True: - time.sleep(1800) + time.sleep(1800) # Backup every 30 minutes + logging.info("Initiating periodic backup...") upload_db_to_hf() -def get_preview_type(filename): - video_extensions = ('.mp4', '.mov', '.avi', '.webm') - image_extensions = ('.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp') - pdf_extensions = ('.pdf',) - text_extensions = ('.txt', '.log', '.md', '.csv') # Common text types +# --- 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.") - name, ext = os.path.splitext(filename.lower()) - if ext in video_extensions: + + # 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." + + +def get_file_type(filename): + ext = Path(filename).suffix.lower() + if ext in ('.mp4', '.mov', '.avi', '.webm', '.mkv'): return 'video' - elif ext in image_extensions: + elif ext in ('.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg'): return 'image' - elif ext in pdf_extensions: + elif ext == '.pdf': return 'pdf' - elif ext in text_extensions: - return 'txt' - return 'other' - -def get_item_by_path(user_data, item_path): - for item in user_data.get('items', []): - # Files use 'unique_path', Folders use 'path' for lookup - if item.get('type') == 'file' and item.get('unique_path') == item_path: - return item - if item.get('type') == 'folder' and item.get('path') == item_path: - return item - return None - -def get_items_in_folder(user_data, folder_path): - # Ensure folder_path ends with a slash for accurate parent matching - if not folder_path.endswith('/'): - folder_path += '/' - - items = [item for item in user_data.get('items', []) if item.get('parent_path') == folder_path] - # Sort: folders first, then files, then alphabetically by name - items.sort(key=lambda x: (x.get('type') != 'folder', x.get('name', '').lower())) - return items - -def get_all_descendant_files(user_data, folder_path): - files_to_delete = [] - folder_path = folder_path.rstrip('/') + '/' # Ensure trailing slash - - for item in user_data.get('items', []): - # Check if item is a file and its parent path starts with the folder path - if item.get('type') == 'file' and item.get('parent_path', '').startswith(folder_path): - files_to_delete.append(item) - # Also need to handle files directly inside the folder - # This logic is covered by startswith if the folder path is correct - return files_to_delete + elif ext in ('.txt', '.md', '.log', '.py', '.js', '.css', '.html', '.json', '.xml', '.csv'): + return 'text' + else: + return 'other' +# --- Base Style --- 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: #ffc107; + --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; } * { 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; -} -body.dark { - background: var(--background-dark); - color: var(--text-dark); -} -.container { - margin: 20px auto; - max-width: 1200px; - padding: 25px; - background: var(--card-bg); - border-radius: 20px; - box-shadow: var(--shadow); -} -body.dark .container { - background: var(--card-bg-dark); -} -h1 { - font-size: 2em; - font-weight: 800; - text-align: center; - margin-bottom: 25px; - background: linear-gradient(135deg, var(--primary), var(--accent)); - -webkit-background-clip: text; - color: transparent; -} -h2 { - font-size: 1.5em; - margin-top: 30px; - color: var(--text-light); -} -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: 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; -} -.btn:hover { - transform: scale(1.05); - background: #e6415f; -} -.download-btn { - background: var(--secondary); - margin-top: 10px; -} -.download-btn:hover { - background: #00b8c5; -} -.delete-btn { - background: var(--delete-color); - margin-top: 10px; -} -.delete-btn:hover { - background: #cc3333; -} -.flash { - color: var(--secondary); - text-align: center; - margin-bottom: 15px; -} -.item-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); /* Smaller grid for folders/files */ - 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); -} -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); -} -.item-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; - align-items: center; - justify-content: space-between; - min-height: 150px; /* Ensure consistent size */ -} -body.dark .item-item { - background: var(--card-bg-dark); -} -.item-item:hover { - transform: translateY(-5px); -} -.item-icon { - width: 60px; - height: 60px; - margin-bottom: 10px; -} -.item-icon.folder { - fill: var(--folder-color); /* SVG color */ -} -.item-icon.file { - fill: var(--secondary); /* SVG color */ -} -.item-preview { - max-width: 100%; - max-height: 120px; /* Smaller preview */ - object-fit: contain; - border-radius: 10px; - margin-bottom: 10px; - loading: lazy; - cursor: pointer; /* Indicate clickable */ -} -.item-name { - font-size: 1em; - margin: 5px 0; - word-break: break-all; - flex-grow: 1; /* Allow name to take space */ -} -.item-item .item-actions { - margin-top: auto; /* Push actions to bottom */ - display: flex; - flex-direction: column; /* Stack buttons */ - width: 100%; - align-items: center; -} -.item-item .item-actions .btn { - width: 80%; /* Make buttons narrower */ - margin-top: 5px; /* Space between buttons */ - padding: 8px 15px; /* Smaller padding */ - font-size: 0.9em; /* Smaller font */ -} -.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 { - max-width: 95%; - max-height: 95%; - background: white; /* Background for non-media */ - padding: 20px; - border-radius: 10px; - box-shadow: var(--shadow); - overflow: auto; /* Scroll for text/pdf */ -} -.modal img, .modal video, .modal iframe { - display: block; /* Remove extra space below inline elements */ - margin: auto; /* Center media */ - max-width: 100%; - max-height: 80vh; /* Limit height to avoid overflow */ - object-fit: contain; - border-radius: 10px; -} -.modal pre { - white-space: pre-wrap; /* Wrap text */ - word-wrap: break-word; - color: var(--text-light); /* Text color */ -} -body.dark .modal-content { - background: var(--card-bg-dark); - color: var(--text-dark); -} -body.dark .modal pre { - 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; -} -.path-breadcrumb { - margin-bottom: 20px; - font-size: 1.1em; -} -.path-breadcrumb a { - color: var(--accent); - text-decoration: none; -} -.path-breadcrumb a:hover { - text-decoration: underline; -} -.path-breadcrumb span { - margin: 0 5px; -} -.action-bar { - display: flex; - gap: 10px; - margin-bottom: 20px; - flex-wrap: wrap; -} -.action-bar .btn { - padding: 10px 20px; - font-size: 1em; - margin-top: 0; -} +body { font-family: 'Inter', sans-serif; background: var(--background-light); color: var(--text-light); line-height: 1.6; } +body.dark { background: var(--background-dark); color: var(--text-dark); } +.container { margin: 20px auto; max-width: 1200px; padding: 25px; background: var(--card-bg); border-radius: 20px; box-shadow: var(--shadow); } +body.dark .container { background: var(--card-bg-dark); } +h1 { font-size: 2em; font-weight: 800; text-align: center; margin-bottom: 25px; background: linear-gradient(135deg, var(--primary), var(--accent)); -webkit-background-clip: text; color: transparent; } +h2 { font-size: 1.5em; margin-top: 30px; color: var(--text-light); } +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: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; } +.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); } +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); } +.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 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; } +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; } +#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 span { margin: 0 5px; color: #aaa; } ''' +# --- Flask Routes --- + @app.route('/register', methods=['GET', 'POST']) def register(): if request.method == 'POST': username = request.form.get('username') password = request.form.get('password') - data = load_data() - if not username or not password: - flash('Имя пользователя и пароль обязательны!') + flash('Имя пользователя и пароль обязательны!', 'error') 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') + return redirect(url_for('register')) + + data = load_data() + if username in data['users']: - flash('Пользователь с таким именем уже существует!') + flash('Пользователь с таким именем уже существует!', 'error') return redirect(url_for('register')) data['users'][username] = { - 'password': password, + 'password': password, # TODO: Hash passwords in a real app! 'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), - 'items': [] # Use 'items' for files and folders + 'root': { # Initialize root folder + "type": "folder", + "name": "/", + "children": {} + } } - save_data(data) - session['username'] = username - flash('Регистрация прошла успешно!') - return redirect(url_for('dashboard')) + try: + save_data(data) + session['username'] = username + flash('Регистрация прошла успешно!', 'success') + return redirect(url_for('dashboard')) + except Exception as e: + flash('Ошибка при сохранении данных пользователя.', 'error') + logging.error(f"Registration save error: {e}") + return redirect(url_for('register')) html = '''
- - -Уже есть аккаунт? Войти
@@ -506,446 +410,414 @@ def register(): @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['users'] and data['users'][username]['password'] == password: + if username in data.get('users', {}) and data['users'][username].get('password') == password: session['username'] = username - return jsonify({'status': 'success', 'redirect': url_for('dashboard')}) + # 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 else: - return jsonify({'status': 'error', 'message': 'Неверное имя пользователя или пароль!'}) + if request.headers.get('X-Requested-With') == 'XMLHttpRequest': + return jsonify({'status': 'error', 'message': 'Неверное имя пользователя или пароль!'}) + else: + flash('Неверное имя пользователя или пароль!', 'error') + return redirect(url_for('login')) + html = ''' - - -Нет аккаунта? Зарегистри��уйтесь
Пользователь: {{ username }}
- {% with messages = get_flashed_messages() %} +Пользователь: {{ username }} | Выйти
+ + {% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} - {% for message in messages %} -