diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,140 +1,72 @@ +# --- 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 -import logging -import mimetypes import os +import logging import threading import time import uuid from datetime import datetime -from io import BytesIO -from pathlib import Path - -import requests -from flask import (Flask, flash, jsonify, redirect, render_template_string, - request, send_file, session, url_for) -from flask_caching import Cache -from huggingface_hub import HfApi, hf_hub_download +from huggingface_hub import HfApi, hf_hub_download, hf_hub_url from werkzeug.utils import secure_filename +import requests +from io import BytesIO app = Flask(__name__) -app.secret_key = os.getenv("FLASK_SECRET_KEY", "supersecretkey_dev_12345") -DATA_FILE = 'cloudeng_data_v2.json' -REPO_ID = os.getenv("HF_REPO_ID", "Eluza133/Z1e1u") # Make sure this matches your HF repo +app.secret_key = os.getenv("FLASK_SECRET_KEY", "supersecretkey") +DATA_FILE = 'cloudeng_data.json' +REPO_ID = "Eluza133/Z1e1u" HF_TOKEN_WRITE = os.getenv("HF_TOKEN") HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") or HF_TOKEN_WRITE cache = Cache(app, config={'CACHE_TYPE': 'simple'}) logging.basicConfig(level=logging.INFO) -def get_hf_api(): - return HfApi() - -@cache.memoize(timeout=60) +@cache.memoize(timeout=300) def load_data(): try: - if HF_TOKEN_READ: - logging.info(f"Attempting to download {DATA_FILE} from {REPO_ID}") - hf_hub_download( - repo_id=REPO_ID, - filename=DATA_FILE, - repo_type="dataset", - token=HF_TOKEN_READ, - local_dir=".", - local_dir_use_symlinks=False, - force_download=True, # Force download to get latest - etag_timeout=10 # Short timeout to check for updates - ) - logging.info("Database downloaded from Hugging Face.") - else: - logging.warning("HF_TOKEN_READ not set. Cannot download latest database. Using local version if exists.") - - if os.path.exists(DATA_FILE): - with open(DATA_FILE, 'r', encoding='utf-8') as file: - data = json.load(file) - if not isinstance(data, dict): - logging.warning("Data is not a dictionary, initializing.") - data = {'users': {}} - data.setdefault('users', {}) - # Ensure all users have the 'items' structure - for user_data in data['users'].values(): - user_data.setdefault('items', []) - # Migrate old 'files' if necessary (optional, depends on existing data) - if 'files' in user_data and isinstance(user_data['files'], list): - logging.info(f"Migrating old 'files' structure for user.") - for old_file in user_data['files']: - if not any(item['path'] == f"/{old_file['filename']}" for item in user_data['items']): - unique_filename = old_file.get('unique_filename', old_file['filename']) # Assume old didn't have unique - hf_path = old_file.get('path', f"cloud_files/{user_data.get('username', 'unknown')}/{unique_filename}") - user_data['items'].append({ - "type": "file", - "path": f"/{unique_filename}", - "name": unique_filename, - "original_filename": old_file['filename'], - "hf_path": hf_path, - "file_type": get_file_type(old_file['filename']), - "upload_date": old_file.get('upload_date', datetime.now().strftime('%Y-%m-%d %H:%M:%S')) - }) - del user_data['files'] # Remove old key after migration - - logging.info("Data successfully loaded and validated.") - return data - else: - logging.warning(f"{DATA_FILE} not found locally. Initializing empty database.") - return {'users': {}} - + download_db_from_hf() + with open(DATA_FILE, 'r', encoding='utf-8') as file: + data = json.load(file) + if not isinstance(data, dict): + logging.warning("Data is not in dict format, initializing empty database") + return {'users': {}, 'files': {}} + data.setdefault('users', {}) + data.setdefault('files', {}) + # Ensure each user has a files list + for user in data['users']: + data['users'][user].setdefault('files', []) + logging.info("Data successfully loaded") + return data + except FileNotFoundError: + logging.warning(f"{DATA_FILE} not found locally after download attempt. Initializing empty database.") + return {'users': {}, 'files': {}} + except json.JSONDecodeError: + logging.error(f"Error decoding JSON from {DATA_FILE}. Initializing empty database.") + return {'users': {}, 'files': {}} except Exception as e: - logging.error(f"Error loading data: {e}", exc_info=True) - # Fallback to ensure app doesn't crash - if os.path.exists(DATA_FILE): - try: - with open(DATA_FILE, 'r', encoding='utf-8') as file: - data = json.load(file) - if isinstance(data, dict): - data.setdefault('users', {}) - for user_data in data['users'].values(): - user_data.setdefault('items', []) - logging.warning("Loaded potentially stale local data due to error.") - return data - except Exception as e_inner: - logging.error(f"Failed to load even local stale data: {e_inner}") - logging.error("Returning empty database due to loading errors.") - return {'users': {}} - + logging.error(f"Error loading data: {e}") + return {'users': {}, 'files': {}} def save_data(data): try: - # Ensure consistency - for user_data in data.get('users', {}).values(): - user_data.setdefault('items', []) - - temp_file = DATA_FILE + ".tmp" - with open(temp_file, 'w', encoding='utf-8') as file: + with open(DATA_FILE, 'w', encoding='utf-8') as file: json.dump(data, file, ensure_ascii=False, indent=4) - os.replace(temp_file, DATA_FILE) # Atomic replace - - if HF_TOKEN_WRITE: - upload_db_to_hf() - else: - logging.warning("HF_TOKEN_WRITE not set. Cannot upload database to HF.") - - cache.delete_memoized(load_data) # Clear cache after saving - logging.info("Data saved locally.") + 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}", exc_info=True) - # Attempt to remove temp file if it exists - if os.path.exists(temp_file): - try: - os.remove(temp_file) - except OSError as e_rem: - logging.error(f"Could not remove temporary save file {temp_file}: {e_rem}") - raise # Re-raise the original exception + logging.error(f"Error saving data: {e}") + raise def upload_db_to_hf(): if not HF_TOKEN_WRITE: - logging.warning("Skipping DB upload: HF_TOKEN_WRITE not set.") + logging.warning("HF_TOKEN_WRITE not set. Skipping database upload to Hugging Face.") return try: - api = get_hf_api() + api = HfApi() api.upload_file( path_or_fileobj=DATA_FILE, path_in_repo=DATA_FILE, @@ -143,132 +75,74 @@ 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.") + 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 from Hugging Face.") + if not os.path.exists(DATA_FILE): + with open(DATA_FILE, 'w', encoding='utf-8') as f: + json.dump({'users': {}, 'files': {}}, f) + return + try: + hf_hub_download( + repo_id=REPO_ID, + filename=DATA_FILE, + repo_type="dataset", + token=HF_TOKEN_READ, + local_dir=".", + local_dir_use_symlinks=False, + force_download=True # Ensure we get the latest version + ) + logging.info("Database downloaded from Hugging Face") except Exception as e: - logging.error(f"Error uploading database to HF: {e}") + logging.error(f"Error downloading database: {e}") + if not os.path.exists(DATA_FILE): + logging.info("Creating empty database file as download failed and file doesn't exist.") + with open(DATA_FILE, 'w', encoding='utf-8') as f: + json.dump({'users': {}, 'files': {}}, f) def periodic_backup(): while True: - time.sleep(1800) # Backup every 30 minutes - logging.info("Starting periodic backup...") + time.sleep(1800) # Sleep for 30 minutes + logging.info("Attempting periodic backup...") try: - # No need to load/save, just upload the current file - if os.path.exists(DATA_FILE): - upload_db_to_hf() - else: - logging.warning("Periodic backup skipped: Data file does not exist.") + # No need to load data just for backup, save_data handles upload + # Ensure data is loaded once before first potential save + load_data() # Load to ensure cache is populated if needed elsewhere + # Directly trigger upload which uses the current file + upload_db_to_hf() except Exception as e: logging.error(f"Error during periodic backup: {e}") def get_file_type(filename): - ext = os.path.splitext(filename)[1].lower() - if ext in ('.mp4', '.mov', '.avi', '.mkv', '.webm'): + filename_lower = filename.lower() + video_extensions = ('.mp4', '.mov', '.avi', '.webm', '.mkv', '.flv', '.wmv') + image_extensions = ('.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.webp', '.svg') + pdf_extensions = ('.pdf',) + text_extensions = ('.txt', '.md', '.log', '.csv', '.json', '.xml', '.html', '.css', '.js', '.py', '.java', '.c', '.cpp', '.h', '.hpp', '.sh', '.bat') + + if filename_lower.endswith(video_extensions): return 'video' - elif ext in ('.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg'): + elif filename_lower.endswith(image_extensions): return 'image' - elif ext == '.pdf': + elif filename_lower.endswith(pdf_extensions): return 'pdf' - elif ext == '.txt': + elif filename_lower.endswith(text_extensions): return 'text' - # Add more types as needed - # elif ext in ('.doc', '.docx'): - # return 'document' - else: - # Guess based on MIME type for more robustness - mime_type, _ = mimetypes.guess_type(filename) - if mime_type: - if mime_type.startswith('video/'): return 'video' - if mime_type.startswith('image/'): return 'image' - if mime_type == 'application/pdf': return 'pdf' - if mime_type.startswith('text/'): return 'text' - return 'other' - -def generate_unique_filename(original_filename): - name, ext = os.path.splitext(original_filename) - # Limit length of name part to avoid excessively long filenames - name = name[:50] - unique_id = uuid.uuid4().hex[:8] - # Ensure the final filename is secure and valid - secure_name = secure_filename(f"{name}_{unique_id}{ext}") - if not secure_name: # Handle cases where secure_filename returns empty (e.g., "_.txt") - secure_name = f"{unique_id}{secure_filename(ext)}" if ext else unique_id - return secure_name - -def normalize_path(path_str): - if not path_str or not path_str.startswith('/'): - path_str = '/' + (path_str or '') - # Resolve '.' and '..' components and remove trailing slashes (except for root) - norm_path = Path(path_str).resolve() - # Since resolve makes it absolute based on CWD, we need the relative parts - # A simpler approach: normalize slashes and remove redundant ones - parts = [part for part in path_str.split('/') if part] - clean_path = '/' + '/'.join(parts) - return clean_path - -def get_items_in_path(items, path): - path = normalize_path(path) - items_in_current = [] - # Path depth: '/' is 0, '/a' is 1, '/a/b' is 2 - current_depth = path.count('/') if path != '/' else 0 - parent_path = '/'.join(path.split('/')[:-1]) if current_depth > 0 else None - - for item in items: - item_path_norm = normalize_path(item['path']) - item_depth = item_path_norm.count('/') if item_path_norm != '/' else 0 - item_parent_path = '/'.join(item_path_norm.split('/')[:-1]) if item_depth > 0 else '/' - if item_parent_path == path and item_depth == current_depth + 1: - items_in_current.append(item) - # Special case for root path - elif path == '/' and item_depth == 1: - items_in_current.append(item) - - - # Sort: folders first, then by name - items_in_current.sort(key=lambda x: (x['type'] != 'folder', x.get('original_filename') or x['name'])) - return items_in_current, parent_path - - -def get_breadcrumbs(path): - path = normalize_path(path) - breadcrumbs = [] - if path == '/': - return [{'name': 'Home', 'path': '/', 'is_last': True}] - - parts = path.strip('/').split('/') - current_crumb_path = '' - for i, part in enumerate(parts): - current_crumb_path += '/' + part - is_last = (i == len(parts) - 1) - breadcrumbs.append({'name': part, 'path': current_crumb_path, 'is_last': is_last}) - - # Add Home at the beginning - breadcrumbs.insert(0, {'name': 'Home', 'path': '/', 'is_last': False}) - return breadcrumbs - - -def get_hf_item_url(hf_path, is_download=False): - base_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{hf_path}" - params = [] - if is_download: - params.append("download=true") - # No need to add token here; requests headers will handle it if needed - # if HF_TOKEN_READ: - # params.append(f"token={HF_TOKEN_READ}") # This is usually not needed/used - - if params: - return f"{base_url}?{'&'.join(params)}" - return base_url - + return 'other' 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; + --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; } * { 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; } @@ -281,59 +155,39 @@ 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: 12px 24px; background: var(--primary); color: white !important; border: none; border-radius: 14px; cursor: pointer; font-size: 1em; font-weight: 600; transition: var(--transition); box-shadow: var(--shadow); display: inline-block; text-decoration: none; margin-top: 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; text-align: center;} .btn:hover { transform: scale(1.05); background: #e6415f; } -.download-btn { background: var(--secondary); } +.download-btn { background: var(--secondary); margin-top: 10px; } .download-btn:hover { background: #00b8c5; } -.delete-btn { background: var(--delete-color); } +.delete-btn { background: var(--delete-color); margin-top: 10px; } .delete-btn:hover { background: #cc3333; } -.folder-btn { background: var(--folder-color); } -.folder-btn:hover { background: #e6a700; } -.flash { padding: 15px; margin-bottom: 15px; border-radius: 10px; text-align: center; font-weight: 600; } -.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; } -.item-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 20px; margin-top: 20px; } +.flash { padding: 10px; margin-bottom: 15px; border-radius: 8px; background-color: var(--glass-bg); color: var(--accent); text-align: center; } +.file-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 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); } -@media (max-width: 768px) { .item-grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); } } -@media (max-width: 480px) { .item-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-preview { width: 100%; height: 130px; object-fit: contain; border-radius: 10px; margin-bottom: 10px; cursor: pointer; display: flex; align-items: center; justify-content: center; background: #eee; } -body.dark .item-preview { background: #333; } -.item-preview img, .item-preview video { max-width: 100%; max-height: 100%; object-fit: contain; border-radius: 10px;} -.item-preview .icon { font-size: 4em; color: var(--primary); } /* Style for icons */ -.item-info p { font-size: 0.9em; margin: 3px 0; word-break: break-all; } -.item-info .name { font-weight: 600; } -.item-info a { color: var(--primary); text-decoration: none; } -.item-info a:hover { color: var(--accent); } -.item-actions { margin-top: 10px; display: flex; justify-content: center; gap: 5px; flex-wrap: wrap; } -.item-actions .btn { padding: 6px 10px; font-size: 0.8em; } -.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: 10px;} -.modal-content { background: var(--card-bg-dark); padding: 20px; border-radius: 15px; max-width: 95%; max-height: 95%; overflow: auto; display: flex; justify-content: center; align-items: center; position: relative; } -.modal img, .modal video, .modal iframe { max-width: 100%; max-height: 85vh; object-fit: contain; border-radius: 10px; box-shadow: var(--shadow); } -.modal pre { background: #fff; color: #333; padding: 15px; border-radius: 5px; max-height: 85vh; overflow: auto; white-space: pre-wrap; word-wrap: break-word; width: 90vw; max-width: 100%; } -#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: 0.9em; color: var(--accent); } -.breadcrumbs a { color: var(--primary); text-decoration: none; } -.breadcrumbs a:hover { text-decoration: underline; } -.breadcrumbs span { margin: 0 5px; } -#folder-form { margin-top: 20px; display: flex; gap: 10px; align-items: center; } -#folder-form input { margin: 0; flex-grow: 1; } -#folder-form button { margin: 0; white-space: nowrap; } -.icon-folder:before { content: "📁"; font-size: 4em; color: var(--folder-color); } -.icon-file:before { content: "📄"; font-size: 4em; color: var(--secondary); } -.icon-image:before { content: "🖼️"; font-size: 4em; color: var(--primary); } -.icon-video:before { content: "🎬"; font-size: 4em; color: var(--accent); } -.icon-pdf:before { content: "📕"; font-size: 4em; color: #dc3545; } -.icon-text:before { content: "📝"; font-size: 4em; color: #6c757d; } +.file-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 .file-item { background: var(--card-bg-dark); } +.file-item:hover { transform: translateY(-5px); } +.file-preview-container { width: 100%; height: 200px; margin-bottom: 10px; display: flex; justify-content: center; align-items: center; overflow: hidden; background: rgba(0,0,0,0.05); border-radius: 10px; } +.file-preview, .file-preview-embed { max-width: 100%; max-height: 100%; object-fit: cover; border-radius: 10px; cursor: pointer; } +.file-preview-embed { width: 100%; height: 100%; border: none; } +.file-item-info { margin-top: auto; } /* Pushes buttons down */ +.file-item p { font-size: 0.9em; margin: 5px 0; word-break: break-all; } +.file-item .filename { font-weight: 600; } +.file-item a { color: var(--primary); text-decoration: none; } +.file-item a:hover { color: var(--accent); } +.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: 10px; } +.modal-content { position: relative; max-width: 95%; max-height: 95%; } +.modal img, .modal video { display: block; width: 100%; height: 100%; max-width: 90vw; max-height: 90vh; object-fit: contain; border-radius: 10px; box-shadow: var(--shadow); } +.modal-close { position: absolute; top: -30px; right: -20px; color: white; font-size: 30px; cursor: pointer; background: rgba(0,0,0,0.5); border-radius: 50%; width: 30px; height: 30px; display: flex; justify-content: center; align-items: center; line-height: 1; } +#progress-container { width: 100%; background: var(--glass-bg); border-radius: 10px; margin: 15px 0; display: none; overflow: hidden; } +#progress-bar { width: 0%; height: 20px; background: var(--primary); border-radius: 10px 0 0 10px; transition: width 0.3s ease; text-align: center; color: white; line-height: 20px; font-size: 0.8em; } +@media (max-width: 768px) { .file-grid { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); } } +@media (max-width: 480px) { .file-grid { grid-template-columns: 1fr; } } ''' @app.route('/register', methods=['GET', 'POST']) @@ -343,51 +197,69 @@ def register(): password = request.form.get('password') if not username or not password: - flash('Username and password are required!', 'error') + flash('Имя пользователя и пароль обязательны!') return redirect(url_for('register')) - # Basic validation (add more checks as needed) - if len(username) < 3: - flash('Username must be at least 3 characters long.', 'error') - return redirect(url_for('register')) - if not username.isalnum(): - flash('Username must be alphanumeric.', 'error') - return redirect(url_for('register')) + # Basic validation + if not username.isalnum() or len(username) < 3: + flash('Имя пользователя должно быть не менее 3 символов и содержать только буквы и цифры.') + return redirect(url_for('register')) + if len(password) < 6: + flash('Пароль должен быть не менее 6 символов.') + return redirect(url_for('register')) + data = load_data() if username in data['users']: - flash('Username already exists!', 'error') + flash('Пользователь с таким именем уже существует!') return redirect(url_for('register')) data['users'][username] = { - 'password': password, # TODO: Hash passwords in a real app! + 'password': password, # Note: In production, hash passwords! 'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), - 'items': [] + 'files': [] } try: save_data(data) session['username'] = username - flash('Registration successful!', 'success') + flash('Регистрация прошла успешно!') return redirect(url_for('dashboard')) except Exception as e: - flash('Registration failed. Please try again.', 'error') - logging.error(f"Registration failed for {username}: {e}") - return redirect(url_for('register')) - - html = f''' - -Register - Zeus Cloud -

Register for Zeus Cloud

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

Already have an account? Login

-
''' + flash('Ошибка при сохранении данных. Попробуйте еще раз.') + logging.error(f"Error saving data during registration: {e}") + return redirect(url_for('register')) + + html = ''' + + + + + + Регистрация - 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']) @@ -397,112 +269,157 @@ def login(): password = request.form.get('password') data = load_data() - # TODO: Use hashed passwords and verification - if username in data.get('users', {}) and data['users'][username].get('password') == password: + # Note: Add password hashing and verification here in production + if username in data['users'] and data['users'][username].get('password') == password: session['username'] = username - logging.info(f"User {username} logged in successfully.") return jsonify({'status': 'success', 'redirect': url_for('dashboard')}) else: - logging.warning(f"Failed login attempt for username: {username}") - return jsonify({'status': 'error', 'message': 'Invalid username or password!'}) + return jsonify({'status': 'error', 'message': 'Неверное имя пользователя или пароль!'}) - # If user is already logged in, redirect to dashboard + # If already logged in, redirect to dashboard if 'username' in session: return redirect(url_for('dashboard')) - html = f''' - -Zeus Cloud Login -

Zeus Cloud

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

No account? Register here

-''' + html = ''' + + + + + + Zeus Cloud + + + + +
+

Zeus Cloud

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

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

+
+ + + +''' return render_template_string(html) @app.route('/dashboard', methods=['GET', 'POST']) def dashboard(): if 'username' not in session: - flash('Please log in to access the dashboard.', 'info') + flash('Пожалуйста, войдите в систему!', 'error') 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('User not found. Please log in again.', 'error') + flash('Пользователь не найден!', 'error') return redirect(url_for('login')) - user_data = data['users'][username] - user_items = user_data.get('items', []) - current_path = normalize_path(request.args.get('path', '/')) - if request.method == 'POST': + if not HF_TOKEN_WRITE: + flash('Загрузка невозможна: токен для записи на Hugging Face не настроен.', 'error') + return redirect(url_for('dashboard')) + files = request.files.getlist('files') - if not files or not files[0].filename: - flash('No files selected for upload.', 'info') - return redirect(url_for('dashboard', path=current_path)) + if not files or all(not f.filename for f in files): + flash('Файлы для загрузки не выбраны.', 'warning') + return redirect(url_for('dashboard')) if len(files) > 20: - flash('Maximum 20 files per upload allowed.', 'error') - return redirect(url_for('dashboard', path=current_path)) + flash('Максимум 20 файлов за раз!', 'warning') + # Consider returning a JSON response if the request came from JS/XHR + if request.headers.get('X-Requested-With') == 'XMLHttpRequest': + return jsonify({'status': 'error', 'message': 'Максимум 20 файлов за раз!'}), 400 + return redirect(url_for('dashboard')) os.makedirs('uploads', exist_ok=True) - api = get_hf_api() - uploaded_count = 0 + api = HfApi() + uploaded_file_infos = [] errors = [] for file in files: if file and file.filename: original_filename = file.filename - unique_filename = generate_unique_filename(original_filename) - temp_path = os.path.join('uploads', unique_filename) # Save with unique name locally too + secure_original_filename = secure_filename(original_filename) + if not secure_original_filename: # Handle cases like '...' + secure_original_filename = 'file' + + unique_filename = f"{uuid.uuid4().hex}_{secure_original_filename}" + temp_path = os.path.join('uploads', unique_filename) # Use unique name for temp file too + hf_path = f"cloud_files/{username}/{unique_filename}" try: file.save(temp_path) - # Construct HF path including folders - hf_relative_path = Path(current_path.lstrip('/')) / unique_filename - hf_full_path = f"cloud_files/{username}/{hf_relative_path}" - - # Construct item path within user's virtual FS - item_path = normalize_path(f"{current_path}/{unique_filename}") - - # Check for name collision in current directory - if any(item['path'] == item_path for item in user_items): - errors.append(f"File '{original_filename}' (renamed to {unique_filename}) already exists in this folder.") - os.remove(temp_path) - continue - - logging.info(f"Uploading {original_filename} as {unique_filename} to {hf_full_path}") api.upload_file( path_or_fileobj=temp_path, - path_in_repo=hf_full_path, + path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, @@ -510,995 +427,827 @@ def dashboard(): ) file_info = { - 'type': 'file', - 'path': item_path, - 'name': unique_filename, - 'original_filename': original_filename, - 'hf_path': hf_full_path, - 'file_type': get_file_type(original_filename), + 'original_filename': original_filename, # Store original name for display + 'hf_path': hf_path, # Store the unique path on HF + 'type': get_file_type(original_filename), 'upload_date': datetime.now().strftime('%Y-%m-%d %H:%M:%S') + # Add size later if needed: 'size': os.path.getsize(temp_path) } - user_items.append(file_info) - uploaded_count += 1 + # Ensure user exists and has 'files' list + if username in data['users'] and isinstance(data['users'][username].get('files'), list): + data['users'][username]['files'].append(file_info) + uploaded_file_infos.append(original_filename) + else: + logging.error(f"User {username} or their file list not found/invalid during upload.") + errors.append(f"Ошибка структуры данных для пользователя {username}") except Exception as e: - logging.error(f"Error uploading file {original_filename}: {e}", exc_info=True) - errors.append(f"Failed to upload {original_filename}: {e}") + logging.error(f"Error uploading file {original_filename} for user {username}: {e}") + errors.append(f"Ошибка загрузки файла: {original_filename}") finally: if os.path.exists(temp_path): try: os.remove(temp_path) - except OSError as e_rem: - logging.error(f"Could not remove temp upload file {temp_path}: {e_rem}") - - user_data['items'] = user_items # Update user data with new items list - try: - save_data(data) - if uploaded_count > 0: - flash(f'{uploaded_count} file(s) uploaded successfully!', 'success') - if errors: - for error in errors: - flash(error, 'error') - except Exception as e: - flash('Error saving upload information. Please try again.', 'error') - logging.error(f"Failed to save data after upload for {username}: {e}") + except Exception as e_rem: + logging.error(f"Error removing temp file {temp_path}: {e_rem}") - return redirect(url_for('dashboard', path=current_path)) + if uploaded_file_infos or errors: + try: + save_data(data) + if uploaded_file_infos: + flash(f"Успешно загружено: {', '.join(uploaded_file_infos)}", 'success') + if errors: + flash(f"Произошли ошибки: {'; '.join(errors)}", 'error') + except Exception as e: + flash('Критическая ошибка при сохранении данных после загрузки!', 'error') + logging.error(f"Error saving data after upload: {e}") + + # For AJAX requests, return JSON status + if request.headers.get('X-Requested-With') == 'XMLHttpRequest': + if not errors: + return jsonify({'status': 'success'}) + else: + # Return error even if some succeeded, let client reload to see changes/errors + return jsonify({'status': 'error', 'message': '; '.join(errors)}), 500 + return redirect(url_for('dashboard')) - # --- GET Request Logic --- - items_in_current_path, parent_path = get_items_in_path(user_items, current_path) - breadcrumbs = get_breadcrumbs(current_path) + # GET request part + user_data = data['users'].get(username, {}) + user_files = sorted(user_data.get('files', []), key=lambda x: x.get('upload_date', ''), reverse=True) - html = f''' - -Dashboard - Zeus Cloud - - -
-

Zeus Cloud Dashboard

Welcome, {{ username }}!

-

Current Path: {{ current_path }}

- - + # Generate HF URLs for previews + for file_info in user_files: + try: + # Use hf_hub_url for generating the correct URL (handles private repos if token is set) + # We need the non-download URL for previews like images/iframes + file_info['preview_url'] = hf_hub_url( + repo_id=REPO_ID, + filename=file_info['hf_path'], + repo_type='dataset', + revision='main' # or specific commit hash + ) + # URL for modal (image/video) might need '?download=true' or token if private, handled by JS/direct link + file_info['modal_url'] = file_info['preview_url'] # Start with base url + # Add token parameter IF necessary and repo is private (complex logic, maybe simplify) + # Simplification: Assume public or token handled by browser session/cookies if needed + # OR pass token to JS if needed for fetch/modal + file_info['download_url'] = url_for('download_file', hf_path=file_info['hf_path'], original_filename=file_info['original_filename']) + file_info['delete_url'] = url_for('delete_file', hf_path=file_info['hf_path']) + except Exception as e: + logging.error(f"Error generating HF URL for {file_info.get('hf_path')}: {e}") + file_info['preview_url'] = '#' + file_info['modal_url'] = '#' + file_info['download_url'] = '#' + file_info['delete_url'] = '#' + + + 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 %} +
+
+ + +
+
+
0%
+
+

Ваши файлы

+

Ваши файлы под надежной защитой квантовой криптографии*
(*маркетинговое заявление)

+
+ {% for file in user_files %} +
+
+ {% if file['type'] == 'video' %} + + {% elif file['type'] == 'image' %} + {{ file.original_filename }} + {% elif file['type'] == 'pdf' %} + + {% elif file['type'] == 'text' %} + + {% else %} +

Нет предпросмотра для {{ file.type }}

+ {% endif %} +
+
+

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

+

{{ file.upload_date }}

+ Скачать + Удалить +
+
+ {% endfor %} + {% if not user_files %} +

Вы еще не загрузили ни одного файла.

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

Files and Folders

-{% if parent_path is not none %} -⬆️ Go Up -{% endif %} - -
- {% for item in items_in_current_path %} -
-
- {% if item.type == 'folder' %} - - - - {% elif item.file_type == 'image' %} - {{ item.original_filename }} - {% elif item.file_type == 'video' %} - - {% elif item.file_type == 'pdf' %} - - {% elif item.file_type == 'text' %} - - {% else %} - - {% endif %} -
-
-

- {% if item.type == 'folder' %} - {{ item.name }} - {% else %} - {{ item.original_filename }} - {% endif %} -

-

{% if item.type == 'file' %} {{ item.upload_date }} {% else %} Folder {% endif %}

-
-
- {% if item.type == 'file' %} - Download - {% endif %} -
- -
+
+

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

+

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

+
+
+

Android (Chrome):

+
    +
  1. Откройте Zeus Cloud в браузере Chrome.
  2. +
  3. Нажмите на меню (три точки).
  4. +
  5. Выберите "Установить приложение" или "Добавить на главный экран".
  6. +
  7. Подтвердите добавление.
  8. +
+
+
+

iOS (Safari):

+
    +
  1. Откройте Zeus Cloud в браузере Safari.
  2. +
  3. Нажмите кнопку "Поделиться" (квадрат со стрелкой).
  4. +
  5. Прокрутите вниз и выберите "На экран «Домой»".
  6. +
  7. Нажмите "Добавить".
  8. +
+
- {% endfor %} - {% if not items_in_current_path %} -

This folder is empty.

- {% endif %} -
- -Logout -
- - - -''' - return render_template_string(html, username=username, items_in_current_path=items_in_current_path, - current_path=current_path, parent_path=parent_path, breadcrumbs=breadcrumbs, - get_hf_item_url=get_hf_item_url, HF_TOKEN_READ=HF_TOKEN_READ) # Pass function and token to template - -@app.route('/create_folder', methods=['POST']) -def create_folder(): - if 'username' not in session: - return redirect(url_for('login')) - - username = session['username'] - data = load_data() - if username not in data.get('users', {}): - return redirect(url_for('login')) - - current_path = normalize_path(request.args.get('path', '/')) - folder_name = request.form.get('folder_name', '').strip() - - if not folder_name: - flash('Folder name cannot be empty.', 'error') - return redirect(url_for('dashboard', path=current_path)) - - # Basic validation for folder name - if not folder_name.replace(' ','').isalnum() and '_' not in folder_name and '-' not in folder_name: - flash('Folder name can only contain letters, numbers, spaces, underscores, and hyphens.', 'error') - return redirect(url_for('dashboard', path=current_path)) - - folder_name = secure_filename(folder_name) # Clean it up - if not folder_name: - flash('Invalid folder name.', 'error') - return redirect(url_for('dashboard', path=current_path)) - - - new_folder_path = normalize_path(f"{current_path}/{folder_name}") - user_items = data['users'][username].get('items', []) - - # Check if folder or file with the same path already exists - if any(item['path'] == new_folder_path for item in user_items): - flash(f"A folder or file named '{folder_name}' already exists here.", 'error') - return redirect(url_for('dashboard', path=current_path)) + - folder_info = { - 'type': 'folder', - 'path': new_folder_path, - 'name': folder_name, - 'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S') - # 'hf_path' is not strictly needed for folders managed in JSON only - } - user_items.append(folder_info) - data['users'][username]['items'] = user_items + + + +''' + # Pass username, files, repo_id and token (if needed) to template + return render_template_string(html, username=username, user_files=user_files, repo_id=REPO_ID, HF_TOKEN_READ=HF_TOKEN_READ) -@app.route('/download/') -def download_item(item_path): +@app.route('/download//') +def download_file(hf_path, original_filename): if 'username' not in session: - flash('Please log in.', 'info') + flash('Пожалуйста, войдите в систему!', 'error') return redirect(url_for('login')) username = session['username'] data = load_data() - if username not in data.get('users', {}): - return redirect(url_for('login')) - - # Normalize the path passed in the URL - item_path_normalized = normalize_path(item_path) - - user_items = data['users'][username].get('items', []) - item_to_download = next((item for item in user_items if item['path'] == item_path_normalized and item['type'] == 'file'), None) - - is_admin_request = request.args.get('admin_context') == 'true' # Check if admin initiated - - if not item_to_download: - # If admin is trying to download, check across all users (less efficient) - if is_admin_request: - found = False - for uname, udata in data.get('users', {}).items(): - item_to_download = next((item for item in udata.get('items', []) if item['path'] == item_path_normalized and item['type'] == 'file'), None) - if item_to_download: - # Check admin privileges here if needed - found = True - break - if not found: - flash('File not found.', 'error') - referer = request.referrer or url_for('admin_panel') - return redirect(referer) - else: - flash('File not found or you do not have permission.', 'error') + is_admin_request = request.referrer and 'admhosto' in request.referrer + + # Permission check: User must own the file OR it's an admin request + if not is_admin_request: + if username not in data['users']: + session.pop('username', None) + flash('Пользователь не найден!', 'error') + return redirect(url_for('login')) + user_files = data['users'][username].get('files', []) + if not any(file.get('hf_path') == hf_path for file in user_files): + flash('У вас нет доступа к этому файлу или файл не найден!', 'error') return redirect(url_for('dashboard')) + else: # Admin request check if user exists + owner_username = hf_path.split('/')[1] if '/' in hf_path else None + if owner_username not in data['users']: + flash(f'Владелец файла ({owner_username}) не найден!', 'error') + return redirect(url_for('admin_panel')) - if not item_to_download.get('hf_path'): - flash('File metadata is incomplete (missing HF path). Cannot download.', 'error') - referer = request.referrer or url_for('dashboard') - return redirect(referer) - - - hf_path = item_to_download['hf_path'] - original_filename = item_to_download.get('original_filename', item_to_download['name']) - download_url = get_hf_item_url(hf_path, is_download=True) - - logging.info(f"Attempting download for {username if not is_admin_request else 'admin'} - Item: {item_path_normalized}, HF Path: {hf_path}, URL: {download_url}") - + # Generate the download URL (usually includes ?download=true) + # Using hf_hub_url might be safer if direct URL structure changes try: + # Get a URL that forces download + file_url = hf_hub_url(repo_id=REPO_ID, filename=hf_path, repo_type="dataset", revision="main") + # Append download=true if not already present (hf_hub_url might not add it) + file_url += "?download=true" + + api = HfApi() headers = {} if HF_TOKEN_READ: - headers["Authorization"] = f"Bearer {HF_TOKEN_READ}" + headers["authorization"] = f"Bearer {HF_TOKEN_READ}" - response = requests.get(download_url, headers=headers, stream=True, timeout=60) # Added timeout - response.raise_for_status() - - # Stream the download - file_content = BytesIO(response.content) # Read into memory (for moderate files) - # For very large files, stream response directly: - # return Response(stream_with_context(response.iter_content(chunk_size=8192)), content_type=response.headers['Content-Type']) - # But send_file is simpler for most cases + # Use requests to stream the download + response = requests.get(file_url, headers=headers, stream=True) + response.raise_for_status() # Check for HTTP errors (like 404 Not Found) + # Stream the content using send_file return send_file( - file_content, + BytesIO(response.content), # Read content into memory (consider streaming for large files) as_attachment=True, - download_name=original_filename, - mimetype=mimetypes.guess_type(original_filename)[0] or 'application/octet-stream' + download_name=original_filename, # Use the original filename for the user + mimetype='application/octet-stream' # Generic mimetype ) except requests.exceptions.RequestException as e: logging.error(f"Error downloading file from HF ({hf_path}): {e}") - flash(f'Error downloading file: {e}', 'error') + flash('Ошибка скачивания файла с сервера хранилища!', 'error') except Exception as e: - logging.error(f"Unexpected error during download ({hf_path}): {e}", exc_info=True) - flash('An unexpected error occurred during download.', 'error') + logging.error(f"Unexpected error during download ({hf_path}): {e}") + flash('Произошла непредвиденная ошибка при скачивании файла.', 'error') + + # Redirect back based on where the request came from + if is_admin_request: + owner_username = hf_path.split('/')[1] if '/' in hf_path else None + if owner_username: + return redirect(url_for('admin_user_files', username=owner_username)) + else: + return redirect(url_for('admin_panel')) # Fallback + else: + return redirect(url_for('dashboard')) - referer = request.referrer or url_for('dashboard') - return redirect(referer) -@app.route('/delete/', methods=['POST']) -def delete_item(item_path): +@app.route('/delete/') +def delete_file(hf_path): if 'username' not in session: - flash('Please log in.', 'info') + flash('Пожалуйста, войдите в систему!', 'error') return redirect(url_for('login')) + if not HF_TOKEN_WRITE: + flash('Удаление невозможна: токен для записи на Hugging Face не настроен.', 'error') + return redirect(url_for('dashboard')) + username = session['username'] data = load_data() - if username not in data.get('users', {}): + if username not in data['users'] or 'files' not in data['users'][username]: + session.pop('username', None) + flash('Пользователь или его файлы не найдены!', 'error') return redirect(url_for('login')) - item_path_normalized = normalize_path(item_path) - user_items = data['users'][username].get('items', []) - item_to_delete = next((item for item in user_items if item['path'] == item_path_normalized), None) - - if not item_to_delete: - flash('Item not found.', 'error') - # Try to guess the previous path if possible - parent = '/'.join(item_path_normalized.split('/')[:-1]) or '/' - return redirect(url_for('dashboard', path=parent)) - - current_view_path = '/'.join(item_path_normalized.split('/')[:-1]) or '/' # Path user was viewing - - api = get_hf_api() - errors = [] - deleted_hf_paths = [] - items_to_remove_from_db = [] - - if item_to_delete['type'] == 'file': - items_to_remove_from_db.append(item_to_delete) - hf_path = item_to_delete.get('hf_path') - if hf_path: - deleted_hf_paths.append(hf_path) - else: - logging.warning(f"File item {item_path_normalized} missing hf_path, only removing from DB.") - - elif item_to_delete['type'] == 'folder': - items_to_remove_from_db.append(item_to_delete) - # Find all children (files and subfolders) recursively - folder_prefix = item_path_normalized + ('/' if item_path_normalized != '/' else '') - children_to_delete = [item for item in user_items if item['path'].startswith(folder_prefix) and item['path'] != item_path_normalized] - - for child in children_to_delete: - items_to_remove_from_db.append(child) - if child['type'] == 'file' and child.get('hf_path'): - deleted_hf_paths.append(child['hf_path']) - - # Try deleting the folder on HF Hub (might contain files not tracked or delete empty structure) - # Construct the HF folder path correctly - folder_hf_base = f"cloud_files/{username}" - relative_folder_path = item_path_normalized.lstrip('/') - hf_folder_path_to_delete = f"{folder_hf_base}/{relative_folder_path}" if relative_folder_path else folder_hf_base - - try: - if HF_TOKEN_WRITE: - logging.info(f"Attempting to delete HF folder: {hf_folder_path_to_delete}") - api.delete_folder( - folder_path=hf_folder_path_to_delete, - repo_id=REPO_ID, - repo_type="dataset", - token=HF_TOKEN_WRITE, - commit_message=f"User {username} deleted folder {item_path_normalized} and contents" - ) - logging.info(f"Successfully deleted HF folder: {hf_folder_path_to_delete}") - # If folder deletion worked, we don't need to delete individual files listed in deleted_hf_paths - # But let's keep the individual deletion logic just in case delete_folder fails partially or has different semantics. - # For safety, we'll still attempt individual deletes if delete_folder seems to fail. - # Clear the list if delete_folder succeeds reliably? For now, keep both attempts. - else: - logging.warning(f"HF_TOKEN_WRITE not set. Cannot delete folder {hf_folder_path_to_delete} from HF.") - - - except Exception as e: - logging.error(f"Error deleting folder {hf_folder_path_to_delete} from HF Hub: {e}. Proceeding with individual file deletions if any.") - errors.append(f"Could not fully remove folder '{item_to_delete['name']}' from storage. Some files might remain.") - - - # Delete individual files from HF if listed (covers single file delete and folder contents) - if HF_TOKEN_WRITE: - for hf_path_to_delete in deleted_hf_paths: - try: - logging.info(f"Deleting HF file: {hf_path_to_delete}") - api.delete_file( - path_in_repo=hf_path_to_delete, - repo_id=REPO_ID, - repo_type="dataset", - token=HF_TOKEN_WRITE, - commit_message=f"User {username} deleted item associated with {hf_path_to_delete}" - ) - except Exception as e: - # Log error but continue trying to remove from DB - logging.error(f"Error deleting file {hf_path_to_delete} from HF Hub: {e}") - errors.append(f"Failed to delete file '{hf_path_to_delete.split('/')[-1]}' from storage.") - elif deleted_hf_paths: - logging.warning(f"HF_TOKEN_WRITE not set. Cannot delete {len(deleted_hf_paths)} associated files from HF.") - errors.append("Could not delete files from storage (token missing). Removed from listing.") - - - # Update the database: remove all marked items - paths_to_remove = {item['path'] for item in items_to_remove_from_db} - data['users'][username]['items'] = [item for item in user_items if item['path'] not in paths_to_remove] + user_files = data['users'][username]['files'] + original_filename = "Неизвестный файл" + file_found_in_db = False + for file_info in user_files: + if file_info.get('hf_path') == hf_path: + original_filename = file_info.get('original_filename', original_filename) + file_found_in_db = True + break + + if not file_found_in_db: + flash(f'Файл ({original_filename}) не найден в вашей базе данных!', 'warning') + # Optionally try deleting from HF anyway if out of sync? For now, just redirect. + return redirect(url_for('dashboard')) 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}" + ) + # Remove from database only after successful HF deletion + data['users'][username]['files'] = [f for f in user_files if f.get('hf_path') != hf_path] save_data(data) - if not errors: - flash(f"'{item_to_delete.get('original_filename') or item_to_delete['name']}' deleted successfully.", 'success') - else: - flash(f"'{item_to_delete.get('original_filename') or item_to_delete['name']}' removed from listing, but some errors occurred.", 'warning') - for error in errors: - flash(error, 'error') + flash(f'Файл "{original_filename}" успешно удален!', 'success') + logging.info(f"User {username} deleted file {hf_path}") + except Exception as e: - flash('Error saving changes after deletion.', 'error') - logging.error(f"Failed to save data after deleting item for {username}: {e}") - # Potentially revert DB changes in memory if save fails? Complex. + # Check if file not found error means it was already deleted + if "404" in str(e) or "not found" in str(e).lower(): + logging.warning(f"File {hf_path} not found on HF during delete attempt by {username}, possibly already deleted. Removing from DB.") + data['users'][username]['files'] = [f for f in user_files if f.get('hf_path') != hf_path] + try: + save_data(data) + flash(f'Файл "{original_filename}" не найден в хранилище, удален из списка.', 'warning') + except Exception as save_e: + flash('Ошибка при обновлении базы данных после обнаружения отсутствующего файла.', 'error') + logging.error(f"Error saving data after failed delete (file not found): {save_e}") - return redirect(url_for('dashboard', path=current_view_path)) + else: + logging.error(f"Error deleting file {hf_path} for user {username}: {e}") + flash(f'Ошибка при удалении файла "{original_filename}"!', 'error') + return redirect(url_for('dashboard')) @app.route('/logout') def logout(): - username = session.pop('username', None) - if username: - logging.info(f"User {username} logged out.") - flash('You have been logged out.', 'info') - # Optional: Clear client-side storage via JS if needed, but session pop is key + session.pop('username', None) + flash('Вы успешно вышли из системы.', 'success') + # JS on login page handles localStorage clearing return redirect(url_for('login')) # --- Admin Routes --- -ADMIN_USERNAME = os.getenv("ADMIN_USER", "admin") -ADMIN_PASSWORD = os.getenv("ADMIN_PASS", "password") # Use env vars for real passwords! +# Simple password protection for admin routes (replace with proper auth) +ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "adminpass123") -def is_admin(): - # Simple session check for admin status +def check_admin(): + """Checks if admin is logged in via session.""" return session.get('is_admin') -@app.route('/admin/login', methods=['GET', 'POST']) +@app.route('/admhosto/login', methods=['GET', 'POST']) def admin_login(): if request.method == 'POST': - username = request.form.get('username') password = request.form.get('password') - if username == ADMIN_USERNAME and password == ADMIN_PASSWORD: + if password == ADMIN_PASSWORD: session['is_admin'] = True - flash('Admin login successful.', 'success') + flash('Успешный вход в админ-панель.', 'success') return redirect(url_for('admin_panel')) else: - flash('Invalid admin credentials.', 'error') + flash('Неверный пароль администратора.', 'error') return redirect(url_for('admin_login')) - # Simple login form for admin + if check_admin(): + return redirect(url_for('admin_panel')) + html = f''' -Admin Login -

Admin Login

-{% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %}{% for category, message in messages %}
{{ message }}
{% endfor %}{% endif %} -{% endwith %} -
-
-
- -
''' + + + + Вход в Админ-панель + + + + +
+

Вход в Админ-панель

+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %}{% for category, message in messages %} +
{{ message }}
+ {% endfor %}{% endif %} + {% endwith %} +
+ + +
+
+''' return render_template_string(html) -@app.route('/admin/logout') +@app.route('/admhosto/logout') def admin_logout(): session.pop('is_admin', None) - flash('Admin logged out.', 'info') - return redirect(url_for('login')) + flash('Вы вышли из админ-панели.', 'info') + return redirect(url_for('admin_login')) @app.route('/admhosto') def admin_panel(): - if not is_admin(): + if not check_admin(): return redirect(url_for('admin_login')) data = load_data() users = data.get('users', {}) - html = f''' - -Admin Panel - Zeus Cloud -

Admin Panel

-Admin Logout -

User List

-{% for username, user_data in users.items() %} -
- {{{{ username }}}} -

Registered: {{ user_data.get('created_at', 'N/A') }}

-

Items: {{ user_data.get('items', []) | length }}

-
- -
-
-{% endfor %} -{% if not users %}

No users registered yet.

{% endif %}
-{% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %}
- {% for category, message in messages %}
{{ message }}
{% endfor %} -
{% endif %} -{% endwith %}
''' + html = ''' + + + + + + Админ-панель - Zeus Cloud + + + + +
+

Админ-панель Zeus Cloud

+ Выйти + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %}
+ {% for category, message in messages %} +
{{ message }}
+ {% endfor %} +
{% endif %} + {% endwith %} +

Список пользователей ({{ users | length }})

+
+ {% for username, user_data in users.items() | sort %} +
+ {{ username }} +

Дата регистрации: {{ user_data.get('created_at', 'N/A') }}

+

Количество файлов: {{ user_data.get('files', []) | length }}

+
+
+ +
+
+
+ {% endfor %} + {% if not users %} +

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

+ {% endif %} +
+
+ + +''' return render_template_string(html, users=users) @app.route('/admhosto/user/') def admin_user_files(username): - if not is_admin(): + if not check_admin(): return redirect(url_for('admin_login')) data = load_data() if username not in data.get('users', {}): - flash(f'User "{username}" not found.', 'error') + flash(f'Пользователь "{username}" не найден!', 'error') return redirect(url_for('admin_panel')) - user_data = data['users'][username] - user_items = user_data.get('items', []) - current_path = normalize_path(request.args.get('path', '/')) - - items_in_current_path, parent_path = get_items_in_path(user_items, current_path) - breadcrumbs = get_breadcrumbs(current_path) - - html = f''' - -Files for {{ username }} - Admin - - -
-

Files for User: {{ username }}

-Back to User List - -{% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %}{% for category, message in messages %}
{{ message }}
{% endfor %}{% endif %} -{% endwith %} - -{% if parent_path is not none %} -⬆️ Go Up -{% endif %} - -
- {% for item in items_in_current_path %} -
-
- {% if item.type == 'folder' %} - - - - {% elif item.file_type == 'image' %} - {{ item.original_filename }} - {% elif item.file_type == 'video' %} - - {% elif item.file_type == 'pdf' %} - - {% elif item.file_type == 'text' %} - - {% else %} - - {% endif %} -
-
-

- {% if item.type == 'folder' %} - {{ item.name }} - {% else %} - {{ item.original_filename }}
({{ item.name }}) - {% endif %} -

-

Path: {{ item.path }}

-

Created/Uploaded: {{ item.upload_date or item.created_at }}

-
-
- {% if item.type == 'file' %} - Download - {% endif %} -
- -
-
+ user_files = sorted(data['users'][username].get('files', []), key=lambda x: x.get('upload_date', ''), reverse=True) + + # Generate HF URLs for previews + for file_info in user_files: + try: + file_info['preview_url'] = hf_hub_url(repo_id=REPO_ID, filename=file_info['hf_path'], repo_type='dataset') + file_info['modal_url'] = file_info['preview_url'] # Use same base URL + file_info['download_url'] = url_for('download_file', hf_path=file_info['hf_path'], original_filename=file_info['original_filename']) + file_info['admin_delete_url'] = url_for('admin_delete_file', username=username, hf_path=file_info['hf_path']) + except Exception as e: + logging.error(f"Admin: Error generating HF URL for {file_info.get('hf_path')}: {e}") + file_info['preview_url'] = '#' + file_info['modal_url'] = '#' + file_info['download_url'] = '#' + file_info['admin_delete_url'] = '#' + + + html = ''' + + + + + + Файлы пользователя {{ username }} - Zeus Cloud + + + + +
+ ← Назад + Выйти +

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

+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} + {% endwith %} +
+ {% for file in user_files %} +
+
+ {% if file['type'] == 'video' %} + + {% elif file['type'] == 'image' %} + {{ file.original_filename }} + {% elif file['type'] == 'pdf' %} + + {% elif file['type'] == 'text' %} + + {% else %} +

Нет предпросмотра для {{ file.type }}

+ {% endif %} +
+
+

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

+

{{ file.get('upload_date', 'N/A') }}

+ Скачать +
+ +
+
+
+ {% endfor %} + {% if not user_files %} +

У этого пользователя пока нет файлов.

+ {% endif %}
- {% endfor %} - {% if not items_in_current_path %} -

This folder is empty for user {{ username }}.

- {% endif %} -
-
- - - -''' - return render_template_string(html, username=username, items_in_current_path=items_in_current_path, - current_path=current_path, parent_path=parent_path, breadcrumbs=breadcrumbs, - get_hf_item_url=get_hf_item_url, HF_TOKEN_READ=HF_TOKEN_READ) + + + + +''' + return render_template_string(html, username=username, user_files=user_files, repo_id=REPO_ID) @app.route('/admhosto/delete_user/', methods=['POST']) def admin_delete_user(username): - if not is_admin(): - flash('Admin privileges required.', 'error') - return redirect(url_for('admin_login')) + if not check_admin(): + flash('Требуется авторизация администратора.', 'error') + return redirect(url_for('admin_login')) + if not HF_TOKEN_WRITE: + flash('Удаление невозможно: токен для записи на Hugging Face не настроен.', 'error') + return redirect(url_for('admin_panel')) data = load_data() if username not in data.get('users', {}): - flash(f'User "{username}" not found.', 'error') + flash(f'Пользователь "{username}" не найден!', 'error') return redirect(url_for('admin_panel')) - logging.warning(f"ADMIN ACTION: Attempting to delete user {username} and all their files.") - api = get_hf_api() - hf_user_folder = f"cloud_files/{username}" - delete_folder_error = False + user_files_to_delete = data['users'][username].get('files', []) + hf_paths_to_delete = [file['hf_path'] for file in user_files_to_delete if 'hf_path' in file] + folder_path = f"cloud_files/{username}" - # Step 1: Delete user's folder from Hugging Face Hub - if HF_TOKEN_WRITE: + try: + api = HfApi() + deleted_hf = False + # Try deleting the whole folder first (more efficient) try: - logging.info(f"Admin deleting HF folder: {hf_user_folder}") + logging.info(f"Admin attempting to delete folder: {folder_path}") api.delete_folder( - folder_path=hf_user_folder, - repo_id=REPO_ID, - repo_type="dataset", - token=HF_TOKEN_WRITE, - commit_message=f"Admin deleted user {username} and all files" + folder_path=folder_path, + repo_id=REPO_ID, + repo_type="dataset", + token=HF_TOKEN_WRITE, + commit_message=f"Admin deleted folder for user {username}" ) - logging.info(f"Successfully deleted HF folder for user {username}.") - except Exception as e: - # Log error but continue to delete user from DB - logging.error(f"Error deleting HF folder {hf_user_folder} for user {username}: {e}") - # Check if it was "not found" error, which is okay if user had no uploads - if "404" not in str(e) and "not found" not in str(e).lower(): - delete_folder_error = True - flash(f"Warning: Could not completely delete storage folder for {username}. Check HF repo.", 'warning') - else: - logging.info(f"HF folder {hf_user_folder} likely did not exist or was already empty.") - - else: - logging.warning("HF_TOKEN_WRITE not set. Cannot delete user's folder from HF.") - flash("Warning: Cannot delete user files from storage (admin token missing).", 'warning') - # Set flag to indicate potential orphaned files - if data['users'][username].get('items'): - delete_folder_error = True - - # Step 2: Delete user from the database - del data['users'][username] - - try: + logging.info(f"Admin successfully deleted folder {folder_path}") + deleted_hf = True + except Exception as folder_del_err: + logging.warning(f"Admin failed to delete folder {folder_path} ({folder_del_err}). Attempting individual file deletion.") + # Fallback: delete files individually if folder deletion fails or isn't supported well + if hf_paths_to_delete: + delete_results = api.delete_files( + paths_in_repo=hf_paths_to_delete, + repo_id=REPO_ID, + repo_type="dataset", + token=HF_TOKEN_WRITE, + commit_message=f"Admin deleted files for user {username}", + # Consider adding ignore_patterns=None, ignore_regex=None if needed + ) + # Note: delete_files might not raise errors for non-existent files, just logs them. + # We assume success if no exception is raised here. + logging.info(f"Admin deleted individual files for user {username}. Results (may vary): {delete_results}") + deleted_hf = True # Assume deletion attempted/succeeded for DB removal + + + # Delete user from database AFTER attempting HF deletion + del data['users'][username] save_data(data) - log_msg = f"Admin successfully deleted user {username}." - if delete_folder_error: - log_msg += " (Potential errors deleting storage folder)" - logging.warning(log_msg) # Log as warning because it's a destructive admin action - flash(f'User "{username}" has been deleted.', 'success' if not delete_folder_error else 'warning') + flash(f'Пользователь "{username}" и его файлы (попытка удаления из хранилища) успешно удалены из базы данных!', 'success') + logging.info(f"Admin deleted user {username} entry from database.") + except Exception as e: - flash('Error saving changes after deleting user.', 'error') - logging.error(f"Failed to save data after deleting user {username}: {e}") - # If save fails, user is deleted in memory but not persisted - critical error + logging.error(f"Error during admin deletion of user {username}: {e}") + flash(f'Произошла ошибка при удалении пользователя "{username}"!', 'error') return redirect(url_for('admin_panel')) -@app.route('/admhosto/delete_item//', methods=['POST']) -def admin_delete_item(username, item_path): - if not is_admin(): - flash('Admin privileges required.', 'error') - return redirect(url_for('admin_login')) +@app.route('/admhosto/delete_file//', methods=['POST']) +def admin_delete_file(username, hf_path): + if not check_admin(): + flash('Требуется авторизация администратора.', 'error') + return redirect(url_for('admin_login')) + if not HF_TOKEN_WRITE: + flash('Удаление невозможно: токен для записи на Hugging Face не настроен.', 'error') + return redirect(url_for('admin_user_files', username=username)) data = load_data() if username not in data.get('users', {}): - flash(f'User "{username}" not found.', 'error') + flash(f'Пользователь "{username}" не найден!', 'error') return redirect(url_for('admin_panel')) - item_path_normalized = normalize_path(item_path) - user_items = data['users'][username].get('items', []) - item_to_delete = next((item for item in user_items if item['path'] == item_path_normalized), None) - - if not item_to_delete: - flash('Item not found for this user.', 'error') - referer = request.referrer or url_for('admin_user_files', username=username) - return redirect(referer) - - current_view_path = '/'.join(item_path_normalized.split('/')[:-1]) or '/' - logging.warning(f"ADMIN ACTION: Attempting deletion of item '{item_path_normalized}' for user {username}.") + user_files = data['users'][username].get('files', []) + original_filename = "Неизвестный файл" + file_found_in_db = False + for file_info in user_files: + if file_info.get('hf_path') == hf_path: + original_filename = file_info.get('original_filename', original_filename) + file_found_in_db = True + break - api = get_hf_api() - errors = [] - deleted_hf_paths = [] - items_to_remove_from_db = [] - - if item_to_delete['type'] == 'file': - items_to_remove_from_db.append(item_to_delete) - hf_path = item_to_delete.get('hf_path') - if hf_path: - deleted_hf_paths.append(hf_path) - else: - logging.warning(f"Admin Delete: File item {item_path_normalized} missing hf_path.") - - elif item_to_delete['type'] == 'folder': - items_to_remove_from_db.append(item_to_delete) - folder_prefix = item_path_normalized + ('/' if item_path_normalized != '/' else '') - children_to_delete = [item for item in user_items if item['path'].startswith(folder_prefix) and item['path'] != item_path_normalized] - - for child in children_to_delete: - items_to_remove_from_db.append(child) - if child['type'] == 'file' and child.get('hf_path'): - deleted_hf_paths.append(child['hf_path']) - - folder_hf_base = f"cloud_files/{username}" - relative_folder_path = item_path_normalized.lstrip('/') - hf_folder_path_to_delete = f"{folder_hf_base}/{relative_folder_path}" if relative_folder_path else folder_hf_base - - if HF_TOKEN_WRITE: - try: - logging.info(f"Admin deleting HF folder: {hf_folder_path_to_delete}") - api.delete_folder(folder_path=hf_folder_path_to_delete, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, commit_message=f"Admin deleted folder {item_path_normalized} for {username}") - logging.info(f"Admin successfully deleted HF folder: {hf_folder_path_to_delete}") - except Exception as e: - logging.error(f"Admin error deleting folder {hf_folder_path_to_delete} from HF: {e}.") - if "404" not in str(e) and "not found" not in str(e).lower(): - errors.append(f"Could not fully remove storage folder '{item_to_delete['name']}'.") - else: - logging.warning("HF_TOKEN_WRITE not set. Cannot delete folder from HF.") - if any(item['type'] == 'file' for item in children_to_delete): - errors.append("Cannot delete storage folder (token missing).") - - # Delete individual files (if needed) - if HF_TOKEN_WRITE: - for hf_path_to_delete in deleted_hf_paths: - try: - logging.info(f"Admin deleting HF file: {hf_path_to_delete}") - api.delete_file(path_in_repo=hf_path_to_delete, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, commit_message=f"Admin deleted item {hf_path_to_delete} for {username}") - except Exception as e: - logging.error(f"Admin error deleting file {hf_path_to_delete} from HF: {e}") - if "404" not in str(e) and "not found" not in str(e).lower(): - errors.append(f"Failed to delete file '{hf_path_to_delete.split('/')[-1]}' from storage.") - elif deleted_hf_paths: - logging.warning(f"HF_TOKEN_WRITE not set. Cannot delete {len(deleted_hf_paths)} associated files from HF.") - errors.append("Cannot delete files from storage (token missing).") - - # Update DB - paths_to_remove = {item['path'] for item in items_to_remove_from_db} - data['users'][username]['items'] = [item for item in user_items if item['path'] not in paths_to_remove] + if not file_found_in_db: + flash(f'Файл ({original_filename}) не найден в базе данных пользователя "{username}"!', 'warning') + # Proceed with HF deletion attempt anyway in case DB is out of sync 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"Admin deleted file {original_filename} for user {username}" + ) + + # Remove from DB after successful HF deletion (or if it wasn't in DB initially) + data['users'][username]['files'] = [f for f in user_files if f.get('hf_path') != hf_path] save_data(data) - item_display_name = item_to_delete.get('original_filename') or item_to_delete['name'] - if not errors: - flash(f"Admin successfully deleted '{item_display_name}' for user {username}.", 'success') - else: - flash(f"Admin deleted '{item_display_name}' from listing, but errors occurred during storage cleanup.", 'warning') - for error in errors: - flash(error, 'error') - logging.warning(f"Admin deleted item '{item_path_normalized}' for user {username}. Errors: {len(errors)}") - except Exception as e: - flash('Error saving changes after admin deletion.', 'error') - logging.error(f"Failed to save data after admin deleting item for {username}: {e}") + flash(f'Файл "{original_filename}" пользователя "{username}" успешно удален!', 'success') + logging.info(f"Admin deleted file {hf_path} for user {username}") - return redirect(url_for('admin_user_files', username=username, path=current_view_path)) + except Exception as e: + # Check if file not found error means it was already deleted + if "404" in str(e) or "not found" in str(e).lower(): + logging.warning(f"Admin: File {hf_path} not found on HF during delete attempt for {username}. Removing from DB.") + data['users'][username]['files'] = [f for f in user_files if f.get('hf_path') != hf_path] + try: + save_data(data) + flash(f'Файл "{original_filename}" не найден в хранилище, удален из списка пользователя "{username}".', 'warning') + except Exception as save_e: + flash('Ошибка при обновлении базы данных после обнаружения отсутствующего файла.', 'error') + logging.error(f"Admin: Error saving data after failed delete (file not found): {save_e}") + else: + logging.error(f"Admin error deleting file {hf_path} for user {username}: {e}") + flash(f'Ошибка при удалении файла "{original_filename}" пользователя "{username}"!', 'error') + return redirect(url_for('admin_user_files', username=username)) if __name__ == '__main__': if not HF_TOKEN_WRITE: - logging.warning("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") - logging.warning("! HF_TOKEN (write access) is NOT SET.") - logging.warning("! File uploads, deletions, folder creations, and DB backups to HF Hub WILL FAIL.") - logging.warning("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") + logging.warning("HF_TOKEN (write access) is not set. File uploads and deletions will fail.") if not HF_TOKEN_READ: - logging.warning("HF_TOKEN_READ is not set. Using HF_TOKEN (write) for reads.") - logging.warning("Downloads/previews MIGHT fail if HF_TOKEN is not set or repo is private.") + logging.warning("HF_TOKEN_READ is not set. Falling back to HF_TOKEN (if set). File downloads/previews might fail for private repos if HF_TOKEN is also not set.") + if not os.getenv("ADMIN_PASSWORD"): + logging.warning("ADMIN_PASSWORD environment variable not set. Using default weak password 'adminpass123'.") - # Initial data load/creation on startup - if not os.path.exists(DATA_FILE): - logging.info(f"{DATA_FILE} not found, attempting initial download or creating empty.") - try: - load_data() # Try to download/initialize - except Exception as e: - logging.error(f"Critical error during initial data load: {e}. Starting with empty.", exc_info=True) - with open(DATA_FILE, 'w', encoding='utf-8') as f: - json.dump({'users': {}}, f) + + # Initial data load and potential download from HF + logging.info("Performing initial data load...") + load_data() + logging.info("Initial data load complete.") + # Start periodic backup only if write token is available if HF_TOKEN_WRITE: backup_thread = threading.Thread(target=periodic_backup, daemon=True) backup_thread.start() logging.info("Periodic backup thread started.") else: - logging.warning("Periodic backup thread NOT started (HF_TOKEN_WRITE missing).") + logging.warning("Periodic backup disabled because HF_TOKEN (write access) is not set.") + + # Use waitress or gunicorn in production instead of Flask's built-in server + # Example using Flask's server for development: + app.run(debug=False, host='0.0.0.0', port=int(os.getenv("PORT", 7860))) - # Use Gunicorn or Waitress in production instead of app.run(debug=True) - # Example: gunicorn --bind 0.0.0.0:7860 app:app - app.run(debug=False, host='0.0.0.0', port=7860) # Debug=False is important for production \ No newline at end of file +# --- END OF FILE app (8).py --- \ No newline at end of file