diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,6 +1,4 @@ -# --- START OF FILE app (8).py --- - -from flask import Flask, render_template_string, request, redirect, url_for, session, flash, send_file, jsonify, Response +from flask import Flask, render_template_string, request, redirect, url_for, session, flash, send_file, jsonify from flask_caching import Cache import json import os @@ -8,181 +6,56 @@ import logging import threading import time from datetime import datetime -from huggingface_hub import HfApi, hf_hub_download, HfFileSystem -from huggingface_hub.utils import RepositoryNotFoundError, EntryNotFoundError +from huggingface_hub import HfApi, hf_hub_download from werkzeug.utils import secure_filename import requests from io import BytesIO import uuid -from pathlib import Path import mimetypes +import PyPDF2 +from base64 import b64encode app = Flask(__name__) -app.secret_key = os.getenv("FLASK_SECRET_KEY", "supersecretkey_dev_12345") # Use a strong, environment-specific key in production +app.secret_key = os.getenv("FLASK_SECRET_KEY", "supersecretkey") DATA_FILE = 'cloudeng_data.json' -REPO_ID = "Eluza133/Z1e1u" # Replace with your actual repo ID +REPO_ID = "Eluza133/Z1e1u" HF_TOKEN_WRITE = os.getenv("HF_TOKEN") HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") or HF_TOKEN_WRITE -HF_FS = HfFileSystem(token=HF_TOKEN_READ) - - -# Ensure necessary tokens are set -if not HF_TOKEN_WRITE: - print("CRITICAL: HF_TOKEN (write access) environment variable is not set. File uploads and deletions will fail.") -if not HF_TOKEN_READ: - print("WARNING: HF_TOKEN_READ environment variable is not set. Falling back to HF_TOKEN. File downloads might fail for private repos if HF_TOKEN is not set.") - -cache = Cache(app, config={'CACHE_TYPE': 'simple', 'CACHE_DEFAULT_TIMEOUT': 60}) # Shorter cache for faster updates during dev/testing -logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') - -# --- Helper Functions --- - -def get_user_data_root(data, username): - user_data = data.setdefault('users', {}).get(username) - if not user_data: - return None - if 'root' not in user_data: - user_data['root'] = {"type": "folder", "name": "/", "children": {}} - return user_data['root'] - -def get_node_by_path(root_node, path_str): - if not path_str or path_str == '/': - return root_node - parts = [p for p in path_str.strip('/').split('/') if p] - current_node = root_node - for part in parts: - if current_node['type'] != 'folder' or part not in current_node['children']: - return None - current_node = current_node['children'][part] - return current_node - -def get_parent_node(root_node, path_str): - if not path_str or path_str == '/': - return None, None # No parent for root - parts = [p for p in path_str.strip('/').split('/') if p] - if not parts: - return None, None - parent_path = '/' + '/'.join(parts[:-1]) - parent_node = get_node_by_path(root_node, parent_path) - child_name = parts[-1] - return parent_node, child_name +cache = Cache(app, config={'CACHE_TYPE': 'simple'}) +logging.basicConfig(level=logging.INFO) -def generate_unique_filename(original_filename): - name, ext = os.path.splitext(original_filename) - # Limit name length to avoid issues, keep extension - safe_name = secure_filename(name)[:50] # Limit base name length - unique_id = uuid.uuid4().hex[:8] - return f"{safe_name}_{unique_id}{ext}" - -def get_preview_type(filename): - ext = Path(filename).suffix.lower() - if ext in ('.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.svg'): - return 'image' - elif ext in ('.mp4', '.mov', '.avi', '.webm', '.ogv', '.mkv'): - return 'video' - elif ext == '.pdf': - return 'pdf' - elif ext == '.txt': - return 'text' - else: - # Add more types as needed (e.g., 'audio', 'document') - mime_type, _ = mimetypes.guess_type(filename) - if mime_type: - if mime_type.startswith('audio/'): - return 'audio' # Requires frontend player - # Can add more specific document checks here if desired - return 'other' - - -# --- Data Handling --- - -@cache.memoize(timeout=60) # Cache for 1 minute +@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 for checking updates - ) - logging.info("Database downloaded successfully.") - else: - logging.warning("HF_TOKEN_READ not set, skipping download. Using local file if exists.") - - if not os.path.exists(DATA_FILE): - logging.warning(f"{DATA_FILE} not found locally and could not be downloaded. Initializing empty database.") - return {'users': {}} # Only users key needed initially - + 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 a dictionary, initializing empty database") - return {'users': {}} - data.setdefault('users', {}) # Ensure users key exists - # Initialize root for existing users if missing - for user_data in data['users'].values(): - if 'root' not in user_data: - user_data['root'] = {"type": "folder", "name": "/", "children": {}} - logging.info("Data successfully loaded and validated.") + logging.warning("Data is not in dict format, initializing empty database") + return {'users': {}, 'files': {}, 'folders': {}} + data.setdefault('users', {}) + data.setdefault('files', {}) + data.setdefault('folders', {}) + logging.info("Data successfully loaded") return data - - except FileNotFoundError: - logging.warning(f"{DATA_FILE} not found. Initializing empty database.") - return {'users': {}} - except json.JSONDecodeError: - logging.error(f"Error decoding JSON from {DATA_FILE}. Returning empty database.") - # Consider backing up the corrupted file here - return {'users': {}} - except EntryNotFoundError: - logging.warning(f"{DATA_FILE} not found in HF repo {REPO_ID}. Initializing empty database.") - # Create a default empty file locally - with open(DATA_FILE, 'w', encoding='utf-8') as f: - json.dump({'users': {}}, f) - return {'users': {}} - except RepositoryNotFoundError: - logging.error(f"HF repository {REPO_ID} not found or access denied.") - # Create a default empty file locally if it doesn't exist - if not os.path.exists(DATA_FILE): - with open(DATA_FILE, 'w', encoding='utf-8') as f: - json.dump({'users': {}}, f) - # Try loading local file anyway - if os.path.exists(DATA_FILE): - try: - with open(DATA_FILE, 'r', encoding='utf-8') as f: - return json.load(f) - except Exception as e: - logging.error(f"Error loading local fallback file: {e}") - return {'users': {}} - else: - return {'users': {}} - except Exception as e: - logging.error(f"Unexpected error loading data: {e}", exc_info=True) - # Attempt to return a safe default - return {'users': {}} - + logging.error(f"Error loading data: {e}") + return {'users': {}, 'files': {}, 'folders': {}} def save_data(data): - if not HF_TOKEN_WRITE: - logging.error("Cannot save data: HF_TOKEN (write access) is not configured.") - # Optionally save locally anyway? - # with open(DATA_FILE, 'w', encoding='utf-8') as file: - # json.dump(data, file, ensure_ascii=False, indent=4) - # logging.warning("Data saved locally ONLY due to missing write token.") - raise Exception("Hugging Face Write Token not configured.") try: - # Save locally first with open(DATA_FILE, 'w', encoding='utf-8') as file: 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 - # Upload to HF Hub +def upload_db_to_hf(): + try: api = HfApi() api.upload_file( path_or_fileobj=DATA_FILE, @@ -192,166 +65,303 @@ def save_data(data): token=HF_TOKEN_WRITE, commit_message=f"Backup {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" ) - cache.clear() # Clear cache after saving - logging.info("Data saved locally and uploaded to HF Hub.") + logging.info("Database uploaded to Hugging Face") except Exception as e: - logging.error(f"Error saving data: {e}", exc_info=True) - raise # Re-raise to indicate failure - + logging.error(f"Error uploading database: {e}") -def delete_hf_object(path_in_repo, is_folder=False): - if not HF_TOKEN_WRITE: - logging.error(f"Cannot delete HF object {path_in_repo}: Write token not configured.") - raise Exception("Hugging Face Write Token not configured.") +def download_db_from_hf(): try: - api = HfApi() - if is_folder: - logging.info(f"Attempting to delete folder: {path_in_repo} from {REPO_ID}") - # Note: delete_folder might require listing contents first depending on HF implementation details. - # Robust implementation might need to list and delete files individually if delete_folder fails. - api.delete_folder( - folder_path=path_in_repo, - repo_id=REPO_ID, - repo_type="dataset", - token=HF_TOKEN_WRITE, - commit_message=f"Deleted folder {path_in_repo}", - ignore_patterns=None # Ensure all contents are targeted - ) - logging.info(f"Successfully deleted folder: {path_in_repo}") - else: - logging.info(f"Attempting to delete file: {path_in_repo} from {REPO_ID}") - api.delete_file( - path_in_repo=path_in_repo, - repo_id=REPO_ID, - repo_type="dataset", - token=HF_TOKEN_WRITE, - commit_message=f"Deleted file {path_in_repo}" - ) - logging.info(f"Successfully deleted file: {path_in_repo}") - except EntryNotFoundError: - logging.warning(f"Object {path_in_repo} not found on HF Hub during deletion attempt.") - # This is often okay, especially if DB and storage got out of sync. + hf_hub_download( + repo_id=REPO_ID, + filename=DATA_FILE, + repo_type="dataset", + token=HF_TOKEN_READ, + local_dir=".", + local_dir_use_symlinks=False + ) + logging.info("Database downloaded from Hugging Face") except Exception as e: - logging.error(f"Error deleting HF object {path_in_repo}: {e}", exc_info=True) - raise # Re-raise to indicate potential failure - - -# --- Background Backup (Removed as save_data now uploads) --- -# def periodic_backup(): ... (no longer needed if save_data uploads) - + logging.error(f"Error downloading database: {e}") + if not os.path.exists(DATA_FILE): + with open(DATA_FILE, 'w', encoding='utf-8') as f: + json.dump({'users': {}, 'files': {}, 'folders': {}}, f) + +def periodic_backup(): + while True: + upload_db_to_hf() + time.sleep(1800) + +def get_file_type(filename): + mime_type, _ = mimetypes.guess_type(filename) + if mime_type: + if mime_type.startswith('video'): + return 'video' + elif mime_type.startswith('image'): + return 'image' + elif mime_type == 'application/pdf': + return 'pdf' + elif mime_type == 'text/plain': + return 'txt' + return 'other' + +def generate_unique_filename(filename): + unique_id = str(uuid.uuid4()) + _, ext = os.path.splitext(filename) + return f"{unique_id}{ext}" -# --- Base Styling (Keep as is) --- 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: #ffb86c; + --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; } * { 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: 10px 20px; background: var(--primary); color: white; border: none; border-radius: 10px; cursor: pointer; font-size: 1em; font-weight: 600; transition: var(--transition); box-shadow: var(--shadow); display: inline-block; text-decoration: none; margin: 5px; vertical-align: middle; } -.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); } -.folder-btn:hover { background: #e6a25a; } -.flash { color: var(--secondary); text-align: center; margin-bottom: 15px; font-weight: bold; } -.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); } -@media (max-width: 768px) { .file-grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); } } -@media (max-width: 480px) { .file-grid { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); } } -.item-card { 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-card { background: var(--card-bg-dark); } -.item-card:hover { transform: translateY(-5px); } -.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; } -body.dark .item-preview { background-color: #333; } -.item-preview i { font-size: 4em; color: #aaa; } /* For folder/file icons */ -.item-card p { font-size: 0.9em; margin: 5px 0; word-break: break-all; } -.item-card .actions { margin-top: 10px; } -.item-card a, .item-card button { color: var(--primary); text-decoration: none; } -.item-card a:hover, .item-card button: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; } -.modal-content { position: relative; max-width: 95%; max-height: 95%; background: #fff; padding: 10px; border-radius: 5px; overflow: hidden; } -body.dark .modal-content { background: #222; } -.modal-content img, .modal-content video, .modal-content iframe { display: block; max-width: 100%; max-height: calc(95vh - 40px); /* Account for padding/controls */ object-fit: contain; margin: auto; } -.modal-content pre { max-width: 90vw; max-height: calc(95vh - 40px); overflow: auto; background: #eee; padding: 15px; border-radius: 5px; color: #333; font-family: monospace; white-space: pre-wrap; word-wrap: break-word; } -body.dark .modal-content pre { background: #333; color: #eee; } -.close-modal { position: absolute; top: 10px; right: 15px; font-size: 2em; color: #fff; cursor: pointer; text-shadow: 0 0 5px black; z-index: 2010; } -#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; text-align: center; color: white; line-height: 20px; } -.breadcrumbs { margin-bottom: 20px; font-size: 1.1em; } -.breadcrumbs a { color: var(--accent); text-decoration: none; } -.breadcrumbs a:hover { text-decoration: underline; } -.breadcrumbs span { margin: 0 5px; color: #aaa; } -body.dark .breadcrumbs a { color: var(--secondary); } -.form-inline { display: flex; align-items: center; margin-top: 15px; } -.form-inline input[type="text"] { flex-grow: 1; margin: 0 10px 0 0; } -.form-inline button { flex-shrink: 0; } -.folder-icon { font-size: 4em; color: var(--folder-color); } /* Simple folder icon */ +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; +} +.file-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + gap: 20px; + margin-top: 20px; +} +.folder-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) { + .file-grid, .folder-grid { + grid-template-columns: repeat(2, 1fr); + } +} +@media (max-width: 480px) { + .file-grid, .folder-grid { + grid-template-columns: 1fr; + } +} +.file-item, .folder-item { + background: var(--card-bg); + padding: 15px; + border-radius: 16px; + box-shadow: var(--shadow); + text-align: center; + transition: var(--transition); +} +body.dark .file-item, body.dark .folder-item { + background: var(--card-bg-dark); +} +.file-item:hover, .folder-item:hover { + transform: translateY(-5px); +} +.file-preview { + max-width: 100%; + max-height: 200px; + object-fit: cover; + border-radius: 10px; + margin-bottom: 10px; + loading: lazy; +} +.file-item p, .folder-item p { + font-size: 0.9em; + margin: 5px 0; +} +.file-item a, .folder-item a { + color: var(--primary); + text-decoration: none; +} +.file-item a:hover, .folder-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; +} +.modal img, .modal video, .modal iframe, .modal pre { + max-width: 95%; + max-height: 95%; + object-fit: contain; + border-radius: 20px; + box-shadow: var(--shadow); +} +#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; +} ''' -# --- Routes --- - @app.route('/register', methods=['GET', 'POST']) def register(): if request.method == 'POST': username = request.form.get('username') password = request.form.get('password') - - if not username or not password: - flash('Имя пользователя и пароль обязательны!') - return redirect(url_for('register')) - - # Basic validation (add more robust checks as needed) - if not username.isalnum() or len(username) < 3: - flash('Имя пользователя должно быть не менее 3 символов и содержать только буквы и цифры.') - return redirect(url_for('register')) - data = load_data() - if username in data['users']: flash('Пользователь с таким именем уже существует!') return redirect(url_for('register')) - + if not username or not password: + flash('Имя пользователя и пароль обязательны!') + return redirect(url_for('register')) data['users'][username] = { - 'password': password, # Store plaintext - BAD PRACTICE! Use hashing (e.g., werkzeug.security) in production. + 'password': password, 'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), - 'root': {"type": "folder", "name": "/", "children": {}} # Initialize root folder structure + 'files': [], + 'folders': [] } - try: - save_data(data) - session['username'] = username - flash('Регистрация прошла успешно!') - return redirect(url_for('dashboard')) - except Exception as e: - flash(f'Ошибка сохранения данных: {e}') - return redirect(url_for('register')) - - + save_data(data) + session['username'] = username + flash('Регистрация прошла успешно!') + return redirect(url_for('dashboard')) html = '''
- + +Уже есть аккаунт? Войти
Нет аккау��та? Зарегистрируйтесь