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 +from flask import Flask, render_template_string, request, redirect, url_for, session, flash, send_file, jsonify, send_from_directory from flask_caching import Cache import json import os @@ -13,19 +11,18 @@ from werkzeug.utils import secure_filename import requests from io import BytesIO import uuid -import os.path +import mimetypes app = Flask(__name__) app.secret_key = os.getenv("FLASK_SECRET_KEY", "supersecretkey") DATA_FILE = 'cloudeng_data.json' -REPO_ID = "Eluza133/Z1e1u" +REPO_ID = os.getenv("HF_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) -@cache.memoize(timeout=300) def load_data(): try: download_db_from_hf() @@ -33,14 +30,23 @@ def load_data(): data = json.load(file) if not isinstance(data, dict): logging.warning("Data is not in dict format, initializing empty database") - return {'users': {}, 'files': {}} + return {'users': {}} data.setdefault('users', {}) - data.setdefault('files', {}) + # Ensure each user has a root folder structure + for user_data in data['users'].values(): + if 'root' not in user_data or not isinstance(user_data['root'], dict) or user_data['root'].get('_type') != 'folder': + user_data['root'] = {'_type': 'folder', 'items': {}} logging.info("Data successfully loaded") return data + except FileNotFoundError: + logging.info(f"{DATA_FILE} not found, creating new database.") + return {'users': {}} + except json.JSONDecodeError: + logging.error(f"Error decoding JSON from {DATA_FILE}. Initializing empty database.") + return {'users': {}} except Exception as e: logging.error(f"Error loading data: {e}") - return {'users': {}, 'files': {}} + return {'users': {}} def save_data(data): try: @@ -55,7 +61,7 @@ def save_data(data): def upload_db_to_hf(): if not HF_TOKEN_WRITE: - logging.warning("HF_TOKEN_WRITE not set. Skipping database upload.") + logging.warning("HF_TOKEN_WRITE is not set. Skipping database upload.") return try: api = HfApi() @@ -72,12 +78,6 @@ def upload_db_to_hf(): logging.error(f"Error uploading database: {e}") def download_db_from_hf(): - if not HF_TOKEN_READ: - logging.warning("HF_TOKEN_READ not set. Cannot download database.") - 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, @@ -85,34 +85,84 @@ def download_db_from_hf(): repo_type="dataset", token=HF_TOKEN_READ, local_dir=".", - local_dir_use_symlinks=False, - force_download=True # Ensure we get the latest version + local_dir_use_symlinks=False ) logging.info("Database downloaded from Hugging Face") except Exception as e: logging.error(f"Error downloading database: {e}") if not os.path.exists(DATA_FILE): + logging.warning(f"{DATA_FILE} not found locally after failed download. Creating empty.") with open(DATA_FILE, 'w', encoding='utf-8') as f: - json.dump({'users': {}, 'files': {}}, f) + json.dump({'users': {}}, f) def periodic_backup(): while True: - time.sleep(1800) - logging.info("Attempting periodic backup...") upload_db_to_hf() + time.sleep(1800) -def get_file_type(filename): - filename_lower = filename.lower() - if filename_lower.endswith(('.mp4', '.mov', '.avi', '.webm', '.mkv', '.flv')): - return 'video' - elif filename_lower.endswith(('.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg')): +def get_mime_type(filename): + mime_type, _ = mimetypes.guess_type(filename) + if mime_type: + return mime_type + return 'application/octet-stream' + +def get_preview_type(mime_type): + if mime_type.startswith('image/'): return 'image' - elif filename_lower.endswith('.pdf'): + elif mime_type.startswith('video/'): + return 'video' + elif mime_type == 'application/pdf': return 'pdf' - elif filename_lower.endswith('.txt'): + elif mime_type == 'text/plain': return 'text' return 'other' +def get_item_at_path(data, username, path_list): + user_data = data['users'].get(username) + if not user_data: + return None + current_folder = user_data.get('root') + if not current_folder or current_folder.get('_type') != 'folder': + user_data['root'] = {'_type': 'folder', 'items': {}} # Attempt to fix broken structure + current_folder = user_data['root'] + + for part in path_list: + if current_folder.get('_type') != 'folder' or part not in current_folder.get('items', {}): + return None # Path segment is not a folder or doesn't exist + current_folder = current_folder['items'][part] + return current_folder + +def add_item_at_path(data, username, path_list, item_name, item_data): + parent_folder = get_item_at_path(data, username, path_list) + if parent_folder and parent_folder.get('_type') == 'folder': + parent_folder['items'][item_name] = item_data + return True + return False + +def delete_item_at_path(data, username, path_list, item_name): + parent_folder = get_item_at_path(data, username, path_list) + if parent_folder and parent_folder.get('_type') == 'folder' and item_name in parent_folder.get('items', {}): + deleted_item = parent_folder['items'].pop(item_name) + return deleted_item + return None + +def list_items_at_path(data, username, path_list): + folder = get_item_at_path(data, username, path_list) + if folder and folder.get('_type') == 'folder': + # Sort folders first, then files, alphabetically by name + items = sorted(folder.get('items', {}).items(), key=lambda item: (item[1].get('_type') != 'folder', item[0])) + return items + return [] + +def get_hf_storage_path(username, path_list, unique_name): + # Construct the full path on HF Hub including user folder and cloud path + user_base_path = f"cloud_files/{username}" + cloud_path = '/'.join(path_list) + if cloud_path: + return f"{user_base_path}/{cloud_path}/{unique_name}" + else: + return f"{user_base_path}/{unique_name}" + BASE_STYLE = ''' :root { --primary: #ff4d6d; @@ -128,6 +178,7 @@ BASE_STYLE = ''' --glass-bg: rgba(255, 255, 255, 0.15); --transition: all 0.3s ease; --delete-color: #ff4444; + --folder-color: #facc15; /* Yellow */ } * { margin: 0; padding: 0; box-sizing: border-box; } body { @@ -135,13 +186,14 @@ body { background: var(--background-light); color: var(--text-light); line-height: 1.6; + padding: 20px; } body.dark { background: var(--background-dark); color: var(--text-dark); } .container { - margin: 20px auto; + margin: 0 auto; max-width: 1200px; padding: 25px; background: var(--card-bg); @@ -199,8 +251,7 @@ input:focus, textarea:focus { box-shadow: var(--shadow); display: inline-block; text-decoration: none; - margin-right: 5px; /* Added margin */ - margin-bottom: 5px; /* Added margin */ + text-align: center; } .btn:hover { transform: scale(1.05); @@ -208,20 +259,28 @@ input:focus, textarea:focus { } .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; + padding: 10px; margin-bottom: 15px; + border-radius: 8px; + text-align: center; + background-color: var(--secondary); /* Using secondary for flash messages */ + color: var(--text-light); /* Text color matching background */ +} +body.dark .flash { + color: var(--text-dark); } .file-grid { display: grid; @@ -256,7 +315,7 @@ body.dark .user-item { } @media (max-width: 768px) { .file-grid { - grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + grid-template-columns: repeat(2, 1fr); } } @media (max-width: 480px) { @@ -264,54 +323,68 @@ body.dark .user-item { grid-template-columns: 1fr; } } -.file-item { +.file-item, .folder-item { background: var(--card-bg); padding: 15px; border-radius: 16px; box-shadow: var(--shadow); text-align: center; transition: var(--transition); - word-wrap: break-word; /* Ensure long names wrap */ - overflow: hidden; /* Hide overflow */ + display: flex; + flex-direction: column; + justify-content: space-between; } -body.dark .file-item { +body.dark .file-item, body.dark .folder-item { background: var(--card-bg-dark); } -.file-item:hover { +.file-item:hover, .folder-item:hover { transform: translateY(-5px); } -.file-preview { - width: 100%; /* Ensure preview takes full width */ - height: 180px; /* Fixed height for consistency */ - object-fit: cover; /* Cover the area */ - border-radius: 10px; - margin-bottom: 10px; +.folder-item { cursor: pointer; - background-color: #eee; /* Placeholder color */ + background-color: var(--glass-bg); + border: 2px dashed var(--folder-color); } -.file-preview-icon { /* Style for icons */ - width: 100%; - height: 180px; +body.dark .folder-item { + background-color: rgba(40, 35, 60, 0.5); +} +.folder-item a { + display: block; + text-decoration: none; + color: var(--text-light); + font-weight: 600; + flex-grow: 1; display: flex; + flex-direction: column; justify-content: center; align-items: center; - font-size: 5em; /* Large icon */ - color: var(--secondary); +} +body.dark .folder-item a { + color: var(--text-dark); +} +.folder-icon::before { + content: "📁"; /* Folder emoji */ + font-size: 3em; + margin-bottom: 10px; + color: var(--folder-color); +} +.file-preview { + max-width: 100%; + max-height: 200px; + object-fit: cover; border-radius: 10px; margin-bottom: 10px; - background-color: rgba(0, 221, 235, 0.1); /* Light background */ + loading: lazy; cursor: pointer; } .file-item p { font-size: 0.9em; margin: 5px 0; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; /* Add ellipsis for long names */ + word-break: break-all; /* Prevent long names breaking layout */ } -.file-item .filename { /* Style for filename */ - font-weight: 600; - margin-bottom: 8px; +.file-item .file-name { + font-weight: 600; + margin-bottom: 10px; } .file-item a { color: var(--primary); @@ -320,6 +393,17 @@ body.dark .file-item { .file-item a:hover { color: var(--accent); } +.file-actions { + display: flex; + justify-content: center; + gap: 10px; + flex-wrap: wrap; +} +.file-actions .btn { + padding: 8px 15px; + font-size: 0.9em; + margin-top: 0; +} .modal { display: none; position: fixed; @@ -327,33 +411,23 @@ body.dark .file-item { left: 0; width: 100%; height: 100%; - background: rgba(0, 0, 0, 0.85); + background: rgba(0, 0, 0, 0.9); z-index: 2000; justify-content: center; align-items: center; - padding: 10px; } -.modal-content { +.modal img, .modal video, .modal iframe { max-width: 95%; - max-height: 95%; - display: flex; /* Use flex for centering */ - justify-content: center; - align-items: center; -} - -.modal img, .modal video { - max-width: 100%; - max-height: 100%; + max-height: 95vh; object-fit: contain; - border-radius: 10px; + border-radius: 20px; box-shadow: var(--shadow); + background: white; /* Ensure iframe/text background is visible */ + border: none; /* Remove default iframe border */ } .modal iframe { - width: 90vw; - height: 90vh; - border: none; - border-radius: 10px; - background: white; /* Background for iframe content */ + width: 95%; /* Give iframes a width */ + height: 95vh; /* And height */ } #progress-container { width: 100%; @@ -368,6 +442,42 @@ body.dark .file-item { background: var(--primary); border-radius: 10px; transition: width 0.3s ease; + text-align: center; + line-height: 20px; + color: white; + font-size: 0.8em; +} +.current-path { + margin-bottom: 20px; + font-size: 1.2em; + font-weight: 600; +} +.path-segment { + color: var(--primary); + text-decoration: none; +} +.path-segment:hover { + text-decoration: underline; +} +.path-segment:not(:last-child)::after { + content: " / "; + color: var(--text-light); +} +body.dark .path-segment:not(:last-child)::after { + color: var(--text-dark); +} +.folder-form { + margin-top: 20px; + display: flex; + gap: 10px; +} +.folder-form input[type="text"] { + flex-grow: 1; + margin: 0; +} +.folder-form button { + flex-shrink: 0; + margin: 0; } ''' @@ -386,14 +496,19 @@ def register(): if not username or not password: flash('Имя пользователя и пароль обязательны!') return redirect(url_for('register')) + + if '/' in username or '.' in username: + flash('Имя пользователя не может содержать символы "/" или "."') + return redirect(url_for('register')) data['users'][username] = { 'password': password, 'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), - 'files': [] + 'root': {'_type': 'folder', 'items': {}} # Initialize root folder } save_data(data) session['username'] = username + session['current_path'] = [] # Initialize current path in session flash('Регистрация прошла успешно!') return redirect(url_for('dashboard')) @@ -438,6 +553,7 @@ def login(): if username in data['users'] and data['users'][username]['password'] == password: session['username'] = username + session['current_path'] = [] # Reset path on login return jsonify({'status': 'success', 'redirect': url_for('dashboard')}) else: return jsonify({'status': 'error', 'message': 'Неверное имя пользователя или пароль!'}) @@ -485,9 +601,6 @@ def login(): .then(data => { if (data.status === 'success') { window.location.href = data.redirect; - } else { - // Clear invalid stored credentials - localStorage.removeItem('zeusCredentials'); } }) .catch(error => console.error('Ошибка автовхода:', error)); @@ -505,7 +618,7 @@ def login(): if (data.status === 'success') { const username = formData.get('username'); const password = formData.get('password'); - localStorage.setItem('zeusCredentials', JSON.stringify({ username, password })); + // localStorage.setItem('zeusCredentials', JSON.stringify({ username, password })); // Uncomment to enable persistent login window.location.href = data.redirect; } else { document.getElementById('flash-messages').innerHTML = `
Пользователь: {{ username }}
+Пользователь: {{ username }}
+ +