diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,84 +1,140 @@ -# --- 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 os import logging +import mimetypes +import os import threading import time +import uuid from datetime import datetime -from huggingface_hub import HfApi, hf_hub_download, HfFileSystem -from werkzeug.utils import secure_filename -import requests from io import BytesIO -import uuid 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 werkzeug.utils import secure_filename + app = Flask(__name__) -app.secret_key = os.getenv("FLASK_SECRET_KEY", "supersecretkey_change_me_please") # Changed default key -DATA_FILE = 'cloudeng_data.json' -REPO_ID = "Eluza133/Z1e1u" # Make sure this repo exists and you have access +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 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) # Filesystem object for easier interaction - -# Basic check for necessary tokens -if not HF_TOKEN_WRITE: - logging.warning("HF_TOKEN (write access) is not set. File/folder uploads and deletions will fail.") -if not HF_TOKEN_READ: - logging.warning("HF_TOKEN_READ is not set. Falling back to HF_TOKEN. File/folder access might fail for private repos if HF_TOKEN is not set.") - cache = Cache(app, config={'CACHE_TYPE': 'simple'}) logging.basicConfig(level=logging.INFO) -@cache.memoize(timeout=60) # Shorter cache timeout +def get_hf_api(): + return HfApi() + +@cache.memoize(timeout=60) def load_data(): try: - if HF_TOKEN_READ: # Only download if token exists - download_db_from_hf() + 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 available, skipping DB download from HF.") - if not os.path.exists(DATA_FILE): - with open(DATA_FILE, 'w', encoding='utf-8') as f: - json.dump({'users': {}, 'files': {}}, f) # Initialize if missing - - 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': {}} # Simplified structure: only users, files are inferred from paths - data.setdefault('users', {}) - logging.info("Data successfully loaded") - return data - except FileNotFoundError: - logging.warning(f"{DATA_FILE} not found. Initializing empty database.") - return {'users': {}} + 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': {}} + except Exception as e: - logging.error(f"Error loading data: {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': {}} + def save_data(data): try: - with open(DATA_FILE, 'w', encoding='utf-8') as file: + # 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: json.dump(data, file, ensure_ascii=False, indent=4) - if HF_TOKEN_WRITE: # Only upload if token exists + os.replace(temp_file, DATA_FILE) # Atomic replace + + if HF_TOKEN_WRITE: upload_db_to_hf() else: - logging.warning("HF_TOKEN_WRITE not available, skipping DB upload to HF.") - cache.clear() - logging.info("Data saved locally") + 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.") except Exception as e: - logging.error(f"Error saving data: {e}") - # Do not raise here to avoid crashing on save errors, just log it. + 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 def upload_db_to_hf(): if not HF_TOKEN_WRITE: - logging.warning("Skipping HF DB upload: Write token not configured.") + logging.warning("Skipping DB upload: HF_TOKEN_WRITE not set.") return try: - api = HfApi() + api = get_hf_api() api.upload_file( path_or_fileobj=DATA_FILE, path_in_repo=DATA_FILE, @@ -87,85 +143,122 @@ 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 to HF: {e}") -def download_db_from_hf(): - if not HF_TOKEN_READ: - logging.warning("Skipping HF DB download: Read token not configured.") - 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 # Force download to get the latest version - ) - logging.info("Database downloaded from Hugging Face") - except Exception as e: # More specific exceptions could be caught (e.g., hf_hub.utils.RepositoryNotFoundError) - logging.error(f"Error downloading database from HF: {e}. Checking if local file exists.") - if not os.path.exists(DATA_FILE): - logging.warning(f"{DATA_FILE} not found locally after download error. Initializing empty database.") - with open(DATA_FILE, 'w', encoding='utf-8') as f: - json.dump({'users': {}}, f) - def periodic_backup(): - if not HF_TOKEN_WRITE: - logging.warning("Periodic backup disabled: Write token not configured.") - return while True: time.sleep(1800) # Backup every 30 minutes logging.info("Starting periodic backup...") - # It might be better to save data only if there were changes, - # but for simplicity, we just upload the current state. - if os.path.exists(DATA_FILE): - upload_db_to_hf() - else: - logging.warning("Skipping periodic backup: data file does not exist.") + 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.") + except Exception as e: + logging.error(f"Error during periodic backup: {e}") def get_file_type(filename): - filename_lower = filename.lower() - if filename_lower.endswith(('.mp4', '.mov', '.avi', '.webm', '.mkv')): + ext = os.path.splitext(filename)[1].lower() + if ext in ('.mp4', '.mov', '.avi', '.mkv', '.webm'): return 'video' - elif filename_lower.endswith(('.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg')): + elif ext in ('.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg'): return 'image' - elif filename_lower.endswith('.pdf'): + elif ext == '.pdf': return 'pdf' - elif filename_lower.endswith('.txt'): + elif ext == '.txt': return 'text' # Add more types as needed - # elif filename_lower.endswith(('.doc', '.docx')): + # 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 get_user_base_path(username): - return f"cloud_files/{username}" - -def get_hf_fs_path(username, current_path, filename=""): - # Ensure current_path is relative and clean - rel_path = Path(current_path.strip('/')) - base_repo_path = f"{REPO_ID}/datasets/{get_user_base_path(username)}" - full_fs_path = f"{base_repo_path}/{rel_path}/{filename}" if filename else f"{base_repo_path}/{rel_path}" - # Clean up potential double slashes, except for the protocol part if present (though HF paths don't use http://) - full_fs_path = full_fs_path.replace("//", "/") - return full_fs_path - -def get_hf_api_path(username, current_path, unique_filename): - # Path used for API operations like upload/delete - rel_path = Path(current_path.strip('/')) - api_path = Path(get_user_base_path(username)) / rel_path / unique_filename - return str(api_path).replace('\\', '/') # Ensure forward slashes - -def get_hf_resolve_url(api_path): - # Construct the URL for direct access/preview - # Ensure the path doesn't start with a slash if REPO_ID already has one - return f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{api_path}" +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 BASE_STYLE = ''' @@ -173,9 +266,9 @@ BASE_STYLE = ''' --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; --folder-color: #ffc107; } * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Inter', sans-serif; background: var(--background-light); color: var(--text-light); line-height: 1.6; } @@ -183,49 +276,64 @@ 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; margin-bottom: 15px; color: var(--text-light); } +h2 { font-size: 1.5em; margin-top: 30px; color: var(--text-light); } body.dark h2 { color: var(--text-dark); } -input, textarea, select { 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, body.dark select { color: var(--text-dark); background: rgba(0,0,0,0.2); } -input:focus, textarea:focus, select: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; text-align: center; } +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:hover { transform: scale(1.05); background: #e6415f; } -.btn-small { padding: 8px 16px; font-size: 0.9em; border-radius: 10px; } -.download-btn { background: var(--secondary); margin-top: 10px; } +.download-btn { background: var(--secondary); } .download-btn:hover { background: #00b8c5; } -.delete-btn { background: var(--delete-color); margin-top: 10px; } +.delete-btn { background: var(--delete-color); } .delete-btn:hover { background: #cc3333; } -.flash { padding: 15px; margin-bottom: 15px; border-radius: 10px; text-align: center; } +.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; } -.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 20px; margin-top: 20px; } -.item { background: var(--card-bg); padding: 15px; border-radius: 16px; box-shadow: var(--shadow); text-align: center; transition: var(--transition); position: relative; } +.item-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) { .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 { 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 img, .item-preview video { max-width: 100%; max-height: 100%; border-radius: 10px; } -.item-preview .file-icon { font-size: 4em; color: #aaa; } /* Placeholder for generic icons */ -.item p { font-size: 0.9em; margin: 5px 0; word-break: break-all; } -.item a { color: var(--primary); text-decoration: none; } -.item a:hover { color: var(--accent); } -.folder-icon { font-size: 4em; color: var(--folder-color); line-height: 150px; } /* Specific style for folder icon */ -.item.folder { background: #fffacd; } /* Light yellow background for folders */ -body.dark .item.folder { background: #5f5b3a; } -.item.folder a { color: #8b4513; font-weight: bold; text-decoration: none; display: block; } -.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.85); z-index: 2000; justify-content: center; align-items: center; padding: 20px; } -.modal-content { max-width: 95%; max-height: 95%; background: white; padding: 10px; border-radius: 15px; overflow: hidden; } -.modal-content iframe { width: 80vw; height: 80vh; border: none; } -.modal img, .modal video { max-width: 100%; max-height: 90vh; object-fit: contain; border-radius: 10px; display: block; margin: auto; } -.modal-close { position: absolute; top: 15px; right: 30px; font-size: 2em; color: white; cursor: pointer; z-index: 2010; } +.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: 1.1em; } -.breadcrumbs a { color: var(--accent); text-decoration: none; } +.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; } -#create-folder-section { margin-top: 20px; padding: 15px; background: var(--glass-bg); border-radius: 15px; } -body.dark #create-folder-section { background: rgba(0,0,0,0.2); } +#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; } ''' @app.route('/register', methods=['GET', 'POST']) @@ -235,77 +343,51 @@ def register(): password = request.form.get('password') if not username or not password: - flash('Имя пользователя и пароль обязательны!', 'error') + flash('Username and password are required!', 'error') return redirect(url_for('register')) - - # Basic validation (add more robust checks as needed) - if not username.isalnum() or len(username) < 3: - flash('Имя пользователя должно быть не менее 3 символов и содержать только буквы и цифры.', 'error') - 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')) data = load_data() if username in data['users']: - flash('Пользователь с таким именем уже существует!', 'error') + flash('Username already exists!', 'error') return redirect(url_for('register')) data['users'][username] = { - 'password': password, # Consider hashing passwords in a real application - 'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S') - # 'files' list is removed, file info inferred from HF repo + 'password': password, # TODO: Hash passwords in a real app! + 'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + 'items': [] } try: save_data(data) session['username'] = username - flash('Регистрация прошла успешно!', 'success') - # Optionally create user's base folder on HF here, although it gets created on first upload too - # try: - # user_base_fs_path = get_hf_fs_path(username, '') # Path to user's root dir - # if HF_TOKEN_WRITE and not HF_FS.exists(user_base_fs_path): - # HF_FS.mkdir(user_base_fs_path, create_parents=True) - # logging.info(f"Created base directory for user {username} on HF.") - # except Exception as e: - # logging.error(f"Could not create base directory for {username} on HF: {e}") + flash('Registration successful!', 'success') return redirect(url_for('dashboard')) except Exception as e: - flash(f'Ошибка сохранения данных: {e}', 'error') - # Rollback user creation if save failed? Complex, maybe just log. - if username in data['users']: - del data['users'][username] # Attempt rollback in memory - return redirect(url_for('register')) - - - html = ''' - - - - - - Регистрация - Zeus Cloud - - - - -
-

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

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

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

-
- - -''' + 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

+
''' return render_template_string(html) @app.route('/', methods=['GET', 'POST']) @@ -315,1088 +397,1108 @@ def login(): password = request.form.get('password') data = load_data() - # Use .get() for safer dictionary access - user_data = data.get('users', {}).get(username) - - if user_data and user_data.get('password') == password: # Again, use hashed passwords ideally + # TODO: Use hashed passwords and verification + if username in data.get('users', {}) and data['users'][username].get('password') == password: session['username'] = username - # Check if localStorage save is requested (e.g., via a checkbox) - # For simplicity, always save on successful login for now + logging.info(f"User {username} logged in successfully.") return jsonify({'status': 'success', 'redirect': url_for('dashboard')}) else: - return jsonify({'status': 'error', 'message': 'Неверное имя пользователя или пароль!'}) + logging.warning(f"Failed login attempt for username: {username}") + return jsonify({'status': 'error', 'message': 'Invalid username or password!'}) - # If already logged in (session exists), redirect to dashboard + # If user is already logged in, redirect to dashboard if 'username' in session: return redirect(url_for('dashboard')) - html = ''' - - - - - - Вход - Zeus Cloud - - - - -
-

Zeus Cloud

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

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

-
- - - -''' + 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

+''' return render_template_string(html) -@app.route('/dashboard/', defaults={'current_path': ''}) -@app.route('/dashboard/', methods=['GET', 'POST']) -def dashboard(current_path): +@app.route('/dashboard', methods=['GET', 'POST']) +def dashboard(): if 'username' not in session: - flash('Пожалуйста, войдите в систему!', 'info') + flash('Please log in to access the dashboard.', 'info') return redirect(url_for('login')) username = session['username'] - data = load_data() # Load user data, primarily for auth check + data = load_data() if username not in data.get('users', {}): session.pop('username', None) - flash('Пользователь не найден!', 'error') + flash('User not found. Please log in again.', 'error') return redirect(url_for('login')) - # Normalize path: remove leading/trailing slashes for consistency internally - current_path = current_path.strip('/') + user_data = data['users'][username] + user_items = user_data.get('items', []) + current_path = normalize_path(request.args.get('path', '/')) if request.method == 'POST': - action = request.form.get('action') - - if action == 'upload': - if not HF_TOKEN_WRITE: - flash('Ошибка: Загрузка невозможна, токен записи HF не настроен.', 'error') - return redirect(url_for('dashboard', current_path=current_path)) - - files = request.files.getlist('files') - if not files or all(f.filename == '' for f in files): - flash('Файлы для загрузки не выбраны.', 'info') - return redirect(url_for('dashboard', current_path=current_path)) - - # Limit simultaneous uploads if needed (example: max 20) - if len(files) > 20: - flash('Максимум 20 файлов за раз!', 'error') - return redirect(url_for('dashboard', current_path=current_path)) - - os.makedirs('uploads', exist_ok=True) - api = HfApi() - uploaded_count = 0 - errors = [] - - for file in files: - if file and file.filename: - original_filename = secure_filename(file.filename) - unique_suffix = uuid.uuid4().hex[:8] # Shorter UUID part - unique_filename = f"{original_filename}_{unique_suffix}" # Add UUID part, keep original extension - - # Ensure extension is preserved if original had one - base, ext = os.path.splitext(original_filename) - unique_filename = f"{base}_{unique_suffix}{ext}" - - - temp_path = os.path.join('uploads', unique_filename) # Save temporarily with unique name + 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 len(files) > 20: + flash('Maximum 20 files per upload allowed.', 'error') + return redirect(url_for('dashboard', path=current_path)) + + os.makedirs('uploads', exist_ok=True) + api = get_hf_api() + uploaded_count = 0 + 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 + + try: file.save(temp_path) - # Construct the path within the HF dataset repository - hf_api_path = get_hf_api_path(username, current_path, unique_filename) - - try: - api.upload_file( - path_or_fileobj=temp_path, - path_in_repo=hf_api_path, - repo_id=REPO_ID, - repo_type="dataset", - token=HF_TOKEN_WRITE, - commit_message=f"User {username} uploaded {original_filename} to {current_path}" - ) - uploaded_count += 1 - logging.info(f"Uploaded '{original_filename}' to {hf_api_path}") - except Exception as e: - errors.append(f"Не удалось загрузить {original_filename}: {e}") - logging.error(f"Error uploading {original_filename} to {hf_api_path}: {e}") - finally: - if os.path.exists(temp_path): - os.remove(temp_path) # Clean up temp file + # 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, + repo_id=REPO_ID, + repo_type="dataset", + token=HF_TOKEN_WRITE, + commit_message=f"User {username} uploaded {original_filename}" + ) + + 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), + 'upload_date': datetime.now().strftime('%Y-%m-%d %H:%M:%S') + } + user_items.append(file_info) + uploaded_count += 1 + + 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}") + 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} файл(ов) успешно загружено!', 'success') + flash(f'{uploaded_count} file(s) uploaded successfully!', 'success') if errors: - flash('Некоторые файлы не удалось загрузить:', 'error') for error in errors: flash(error, 'error') - # No need to save_data() here as file metadata isn't stored in JSON anymore - - elif action == 'create_folder': - if not HF_TOKEN_WRITE: - flash('Ошибка: Создание папки невозможно, токен записи HF не настроен.', 'error') - return redirect(url_for('dashboard', current_path=current_path)) - - folder_name = request.form.get('folder_name') - if not folder_name: - flash('Имя папки не может быть пустым.', 'error') - return redirect(url_for('dashboard', current_path=current_path)) - - safe_folder_name = secure_filename(folder_name) - if not safe_folder_name: - flash('Недопустимое имя папки.', 'error') - return redirect(url_for('dashboard', current_path=current_path)) - - # Create a placeholder file to make the directory visible in HF UI / listings - placeholder_filename = ".keep" - hf_api_path = get_hf_api_path(username, os.path.join(current_path, safe_folder_name).replace('\\','/'), placeholder_filename) - - try: - api = HfApi() - # Upload an empty file (or fileobj) - from io import BytesIO - empty_file = BytesIO(b"") - api.upload_file( - path_or_fileobj=empty_file, - path_in_repo=hf_api_path, - repo_id=REPO_ID, - repo_type="dataset", - token=HF_TOKEN_WRITE, - commit_message=f"User {username} created folder {safe_folder_name} in {current_path}" - ) - flash(f'Папка "{safe_folder_name}" создана.', 'success') - logging.info(f"Created folder '{safe_folder_name}' at {hf_api_path} for user {username}") - except Exception as e: - flash(f'Не удалось создать папку "{safe_folder_name}": {e}', 'error') - logging.error(f"Error creating folder {safe_folder_name} for {username}: {e}") + 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}") - return redirect(url_for('dashboard', current_path=current_path)) + return redirect(url_for('dashboard', path=current_path)) # --- GET Request Logic --- - items = [] - folders = [] - files_list = [] + items_in_current_path, parent_path = get_items_in_path(user_items, current_path) + breadcrumbs = get_breadcrumbs(current_path) + + html = f''' + +Dashboard - Zeus Cloud + + +
+

Zeus Cloud Dashboard

Welcome, {{ username }}!

+

Current Path: {{ current_path }}

+ + + +{% 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 %} +
+ +
+
+
+ {% 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 - for item_info in repo_items: - item_path = item_info['name'] # Full path like 'datasets/REPO_ID/cloud_files/user/folder/file.txt' - item_name = Path(item_path).name # Just 'file.txt' or 'folder' - # Skip the placeholder file used for folder creation - if item_name == ".keep": - continue +@app.route('/create_folder', methods=['POST']) +def create_folder(): + if 'username' not in session: + return redirect(url_for('login')) - relative_item_path = Path(item_path).relative_to(user_base_fs_path).as_posix() # 'folder/file.txt' or 'folder' + username = session['username'] + data = load_data() + if username not in data.get('users', {}): + return redirect(url_for('login')) - item_type = item_info['type'] # 'file' or 'directory' + current_path = normalize_path(request.args.get('path', '/')) + folder_name = request.form.get('folder_name', '').strip() - if item_type == 'directory': - folders.append({ - 'name': item_name, - 'path': relative_item_path # Path relative to user's root - }) - elif item_type == 'file': - # Try to infer original filename if it follows the pattern name_uuid.ext - original_name = item_name - try: - base, ext = os.path.splitext(item_name) - if len(base) > 9 and base[-9] == '_': # check for _uuidpart - uuid_part = base[-8:] - if all(c in '0123456789abcdefABCDEF' for c in uuid_part): - original_name = base[:-9] + ext - except Exception: - pass # Keep item_name if parsing fails - - - file_info = { - 'name': item_name, # The actual unique name on HF - 'original_name': original_name, # Best guess at original name - 'path': relative_item_path, # Path relative to user's root - 'hf_api_path': Path(get_user_base_path(username)) / relative_item_path, # Path for API calls from 'cloud_files/...' - 'type': get_file_type(original_name), # Guess type from original name - 'size': item_info.get('size', 0), # Size in bytes - 'url': get_hf_resolve_url(Path(get_user_base_path(username)) / relative_item_path) # Direct URL - # 'upload_date': # HF FS ls doesn't easily provide modification time, skip for now - } - files_list.append(file_info) + if not folder_name: + flash('Folder name cannot be empty.', 'error') + return redirect(url_for('dashboard', path=current_path)) - except Exception as e: - logging.error(f"Error listing files from HF for user {username} at path '{current_path}': {e}") - flash(f"Ошибка при получении списка файлов: {e}", 'error') - - # Sort folders and files alphabetically - folders.sort(key=lambda x: x['name'].lower()) - files_list.sort(key=lambda x: x['original_name'].lower()) - items = folders + files_list - - # Breadcrumbs - breadcrumbs = [{'name': 'Корень', 'path': ''}] - if current_path: - path_parts = Path(current_path).parts - cumulative_path = '' - for part in path_parts: - cumulative_path = os.path.join(cumulative_path, part).replace('\\', '/') - breadcrumbs.append({'name': part, 'path': cumulative_path}) - - - 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 %} - - + # 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)) -

Действия

-
- - - -
-
- -
-
- - - -
-
- - -

Содержимое папки: {{ current_path or 'Корень' }}

-

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

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

{{ item.name }}

-
-
- -
-
- {% else %} -
-
- {% if item.type == 'image' %} - {{ item.original_name }} - {% elif item.type == 'video' %} - - {% elif item.type == 'pdf' %} - - {% elif item.type == 'text' %} - - {% else %} - - {% endif %} -
-

{{ item.original_name }}

-

{{ (item.size / 1024 / 1024) | round(2) }} MB

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

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

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

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

-

Для быстрого доступа к Zeus Cloud, вы можете добавить это приложение на главный экран вашего телефона (инструкции для Chrome/Safari).

- - Выйти -
+ 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)) - - - -''' - # Pass necessary variables to the template - return render_template_string(html, - username=username, - items=items, # Combined list of folders and files - current_path=current_path, - breadcrumbs=breadcrumbs, - HF_TOKEN_READ=HF_TOKEN_READ) # Pass token for JS checks if needed + try: + save_data(data) + flash(f"Folder '{folder_name}' created successfully.", 'success') + except Exception as e: + flash('Error creating folder.', 'error') + logging.error(f"Failed to save data after creating folder for {username}: {e}") + + return redirect(url_for('dashboard', path=current_path)) -@app.route('/download//') -def download_file(hf_api_path, download_name): +@app.route('/download/') +def download_item(item_path): if 'username' not in session: - flash('Пожалуйста, войдите в систему!', 'info') + flash('Please log in.', 'info') return redirect(url_for('login')) username = session['username'] - # Basic check: does the path start with the user's expected base directory? - # This is a weak security check, proper ACLs would be better. - expected_base = get_user_base_path(username) - if not str(hf_api_path).startswith(expected_base): - # Check if it's an admin trying to download (e.g., from admin panel) - # A more robust way would be checking an admin session flag. - is_admin_referrer = request.referrer and 'admhosto' in request.referrer - if not is_admin_referrer: - flash('Ошибка: Несанкционированный доступ к файлу.', 'error') - logging.warning(f"User {username} attempted unauthorized download of {hf_api_path}") - return redirect(url_for('dashboard')) - # If admin referrer, proceed (assuming admin has rights) - else: - logging.info(f"Admin initiated download of {hf_api_path} (original user path)") + 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') + return redirect(url_for('dashboard')) - if not HF_TOKEN_READ: - flash('Ошибка: Скачивание невозможно, токен чтения HF не настроен.', 'error') - return redirect(request.referrer or url_for('dashboard')) + 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) - file_url = get_hf_resolve_url(hf_api_path) + "?download=true" + + 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}") try: headers = {} if HF_TOKEN_READ: - headers["authorization"] = f"Bearer {HF_TOKEN_READ}" + headers["Authorization"] = f"Bearer {HF_TOKEN_READ}" - response = requests.get(file_url, headers=headers, stream=True) + 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 + return send_file( - BytesIO(response.content), # Read content into memory (consider streaming for large files) + file_content, as_attachment=True, - download_name=download_name, # Use the original filename for the user - mimetype='application/octet-stream' # Generic mimetype + download_name=original_filename, + mimetype=mimetypes.guess_type(original_filename)[0] or 'application/octet-stream' ) - except requests.exceptions.RequestException as e: - logging.error(f"Error downloading file {hf_api_path} from HF: {e}") - status_code = e.response.status_code if e.response is not None else 'N/A' - if status_code == 404: - flash(f'Ошибка: Файл "{download_name}" не найден на сервере.', 'error') - else: - flash(f'Ошибка скачивания файла "{download_name}" (код: {status_code}).', 'error') - # Redirect back to where they came from (dashboard or admin page) - return redirect(request.referrer or url_for('dashboard')) + logging.error(f"Error downloading file from HF ({hf_path}): {e}") + flash(f'Error downloading file: {e}', 'error') except Exception as e: - logging.error(f"Unexpected error during download of {hf_api_path}: {e}") - flash('Произошла непредвиденная ошибка при скачивании файла.', 'error') - return redirect(request.referrer or url_for('dashboard')) + logging.error(f"Unexpected error during download ({hf_path}): {e}", exc_info=True) + flash('An unexpected error occurred during download.', 'error') + referer = request.referrer or url_for('dashboard') + return redirect(referer) @app.route('/delete/', methods=['POST']) def delete_item(item_path): if 'username' not in session: - flash('Пожалуйста, войдите в систему!', 'info') + flash('Please log in.', 'info') return redirect(url_for('login')) username = session['username'] - # Path received is relative to user's root, e.g., "folder/file.txt" or "folder" + data = load_data() + if username not in data.get('users', {}): + return redirect(url_for('login')) - if not HF_TOKEN_WRITE: - flash('Ошибка: Удаление невозможно, токен записи HF не настроен.', 'error') - # Determine the parent path to redirect back correctly - parent_path = str(Path(item_path).parent) - if parent_path == '.': parent_path = '' - return redirect(url_for('dashboard', current_path=parent_path)) - - # Construct the full path for HF API/FS operations - full_hf_api_path = str(Path(get_user_base_path(username)) / item_path).replace('\\','/') # e.g., cloud_files/user/folder/file.txt - item_name = Path(item_path).name - is_folder = False - full_fs_path = f"datasets/{REPO_ID}/{full_hf_api_path}" # Path for HF_FS + 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) - try: - if HF_FS.exists(full_fs_path): - is_folder = HF_FS.isdir(full_fs_path) - else: - # Item might have been deleted already, or path is wrong - flash(f'Элемент "{item_name}" не найден для удаления.', 'warning') - parent_path = str(Path(item_path).parent) - if parent_path == '.': parent_path = '' - return redirect(url_for('dashboard', current_path=parent_path)) - - api = HfApi() - if is_folder: - # Delete folder recursively - api.delete_folder( - folder_path=full_hf_api_path, - repo_id=REPO_ID, - repo_type="dataset", - token=HF_TOKEN_WRITE, - commit_message=f"User {username} deleted folder {item_name}" - ) - flash(f'Папка "{item_name}" и ее содержимое удалены.', 'success') - logging.info(f"User {username} deleted folder {full_hf_api_path}") + 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: - # Delete a single file - api.delete_file( - path_in_repo=full_hf_api_path, - repo_id=REPO_ID, - repo_type="dataset", - token=HF_TOKEN_WRITE, - commit_message=f"User {username} deleted file {item_name}" - ) - flash(f'Файл "{item_name}" удален.', 'success') - logging.info(f"User {username} deleted file {full_hf_api_path}") + 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']) - # Clear cache to reflect changes immediately - cache.clear() + # 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] + + try: + 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') except Exception as e: - item_type_str = "папку" if is_folder else "файл" - logging.error(f"Error deleting {item_type_str} {full_hf_api_path} for user {username}: {e}") - flash(f'Ошибка при удалении {item_type_str} "{item_name}": {e}', 'error') + 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. - # Redirect back to the parent folder - parent_path = str(Path(item_path).parent) - if parent_path == '.': parent_path = '' # Handle root case - return redirect(url_for('dashboard', current_path=parent_path)) + return redirect(url_for('dashboard', path=current_view_path)) @app.route('/logout') def logout(): - session.pop('username', None) - flash('Вы вышли из системы.', 'info') - # JavaScript on login page handles localStorage cleanup + 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 return redirect(url_for('login')) # --- Admin Routes --- -# WARNING: These routes lack proper admin authentication. -# Add a check (e.g., session variable, password) before allowing access. -def check_admin(): - # Replace with actual admin check logic - # For now, assumes anyone accessing /admhosto is admin - # return session.get('is_admin') == True - return True + +ADMIN_USERNAME = os.getenv("ADMIN_USER", "admin") +ADMIN_PASSWORD = os.getenv("ADMIN_PASS", "password") # Use env vars for real passwords! + +def is_admin(): + # Simple session check for admin status + return session.get('is_admin') + +@app.route('/admin/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: + session['is_admin'] = True + flash('Admin login successful.', 'success') + return redirect(url_for('admin_panel')) + else: + flash('Invalid admin credentials.', 'error') + return redirect(url_for('admin_login')) + + # Simple login form for admin + 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 %} +
+
+
+ +
''' + return render_template_string(html) + +@app.route('/admin/logout') +def admin_logout(): + session.pop('is_admin', None) + flash('Admin logged out.', 'info') + return redirect(url_for('login')) + @app.route('/admhosto') def admin_panel(): - if not check_admin(): - flash('Доступ запрещен.', 'error') - return redirect(url_for('login')) + if not is_admin(): + return redirect(url_for('admin_login')) data = load_data() users = data.get('users', {}) - html = ''' - - - - - - Админ-панель - Zeus Cloud - - - - - -
-

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

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

Список пользователей

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

{{ username }}

-

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

- Файлы -
- -
-
- {% else %} -

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

- {% endfor %} -
-
- - -''' + 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 %}
''' return render_template_string(html, users=users) -@app.route('/admhosto/user//', defaults={'current_path': ''}) -@app.route('/admhosto/user//') -def admin_user_files(username, current_path): - if not check_admin(): - flash('Доступ запрещен.', 'error') - return redirect(url_for('login')) +@app.route('/admhosto/user/') +def admin_user_files(username): + if not is_admin(): + return redirect(url_for('admin_login')) data = load_data() if username not in data.get('users', {}): - flash(f'Пользователь {username} не найден!', 'error') + flash(f'User "{username}" not found.', 'error') return redirect(url_for('admin_panel')) - current_path = current_path.strip('/') - items = [] - folders = [] - files_list = [] - - if HF_TOKEN_READ: - try: - user_base_fs_path = f"datasets/{REPO_ID}/{get_user_base_path(username)}" - target_path_in_repo = os.path.join(user_base_fs_path, current_path).replace('\\', '/') - - if HF_FS.exists(target_path_in_repo): - repo_items = HF_FS.ls(target_path_in_repo, detail=True) - for item_info in repo_items: - item_path = item_info['name'] - item_name = Path(item_path).name - if item_name == ".keep": continue - - relative_item_path = Path(item_path).relative_to(user_base_fs_path).as_posix() - item_type = item_info['type'] - - if item_type == 'directory': - folders.append({ - 'name': item_name, - 'path': relative_item_path, - 'type': 'directory' # Explicitly add type for template - }) - elif item_type == 'file': - original_name = item_name - try: - base, ext = os.path.splitext(item_name) - if len(base) > 9 and base[-9] == '_': - uuid_part = base[-8:] - if all(c in '0123456789abcdefABCDEF' for c in uuid_part): - original_name = base[:-9] + ext - except Exception: pass - - hf_api_path_for_file = Path(get_user_base_path(username)) / relative_item_path # Correct path for download/delete links - - file_info = { - 'name': item_name, - 'original_name': original_name, - 'path': relative_item_path, - 'hf_api_path': hf_api_path_for_file, # Path for download/delete links - 'type': get_file_type(original_name), - 'size': item_info.get('size', 0), - 'url': get_hf_resolve_url(hf_api_path_for_file) - } - files_list.append(file_info) - else: - flash(f"Папка '{current_path}' для пользователя {username} не найдена или пуста.", 'info') - - except Exception as e: - logging.error(f"[Admin] Error listing files for {username} at '{current_path}': {e}") - flash(f"Ошибка при получении списка файлов для {username}: {e}", 'error') - - folders.sort(key=lambda x: x['name'].lower()) - files_list.sort(key=lambda x: x['original_name'].lower()) - items = folders + files_list - - breadcrumbs = [{'name': 'Корень', 'path': ''}] - if current_path: - path_parts = Path(current_path).parts - cumulative_path = '' - for part in path_parts: - cumulative_path = os.path.join(cumulative_path, part).replace('\\', '/') - breadcrumbs.append({'name': part, 'path': cumulative_path}) - - html = ''' - - - - - - Файлы пользователя {{ username }} - Zeus Cloud - - - - - -
-

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

-

« Назад к списку пользователей

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