diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,79 +1,81 @@ +# --- START OF FILE app (8).py --- + +from flask import Flask, render_template_string, request, redirect, url_for, session, flash, send_file, jsonify +from flask_caching import Cache import json -import logging -import mimetypes import os +import logging import threading import time -import uuid from datetime import datetime -from io import BytesIO - -import requests -from flask import (Flask, flash, jsonify, redirect, render_template_string, - request, send_file, session, url_for) -from flask_caching import Cache -from huggingface_hub import HfApi, hf_hub_download +from huggingface_hub import HfApi, hf_hub_download, HfFileSystem from werkzeug.utils import secure_filename +import requests +from io import BytesIO +import uuid +from pathlib import Path app = Flask(__name__) -app.secret_key = os.getenv("FLASK_SECRET_KEY", "a-very-secret-and-complex-key") -DATA_FILE = 'cloudeng_data_v2.json' -REPO_ID = "Eluza133/Z1e1u" +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 HF_TOKEN_WRITE = os.getenv("HF_TOKEN") HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") or HF_TOKEN_WRITE -UPLOAD_FOLDER = 'temp_uploads' -os.makedirs(UPLOAD_FOLDER, exist_ok=True) +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=300) +@cache.memoize(timeout=60) # Shorter cache timeout def load_data(): try: - download_db_from_hf() + if HF_TOKEN_READ: # Only download if token exists + download_db_from_hf() + 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': {}, 'files': {}} + return {'users': {}} # Simplified structure: only users, files are inferred from paths data.setdefault('users', {}) - data.setdefault('files', {}) - for user_data in data['users'].values(): - user_data.setdefault('files', []) - user_data.setdefault('folders', []) logging.info("Data successfully loaded") return data except FileNotFoundError: logging.warning(f"{DATA_FILE} not found. Initializing empty database.") - return {'users': {}, 'files': {}} - except json.JSONDecodeError: - logging.error(f"Error decoding JSON from {DATA_FILE}. Initializing empty database.") - return {'users': {}, 'files': {}} + return {'users': {}} except Exception as e: logging.error(f"Error loading data: {e}") - return {'users': {}, 'files': {}} + return {'users': {}} def save_data(data): try: - # Ensure default structures exist before saving - data.setdefault('users', {}) - data.setdefault('files', {}) # Keep top-level files if needed, or remove if unused - for user_data in data['users'].values(): - user_data.setdefault('files', []) - user_data.setdefault('folders', []) - with open(DATA_FILE, 'w', encoding='utf-8') as file: json.dump(data, file, ensure_ascii=False, indent=4) - upload_db_to_hf() + if HF_TOKEN_WRITE: # Only upload if token exists + upload_db_to_hf() + else: + logging.warning("HF_TOKEN_WRITE not available, skipping DB upload to HF.") cache.clear() - logging.info("Data saved and uploaded to HF") + logging.info("Data saved locally") except Exception as e: logging.error(f"Error saving data: {e}") - raise + # Do not raise here to avoid crashing on save errors, just log it. def upload_db_to_hf(): if not HF_TOKEN_WRITE: - logging.warning("HF Write Token not set. Skipping database upload.") + logging.warning("Skipping HF DB upload: Write token not configured.") return try: api = HfApi() @@ -87,15 +89,11 @@ def upload_db_to_hf(): ) logging.info("Database uploaded to Hugging Face") except Exception as e: - logging.error(f"Error uploading database: {e}") + logging.error(f"Error uploading database to HF: {e}") def download_db_from_hf(): if not HF_TOKEN_READ: - logging.warning("HF Read Token not set. Skipping database download attempt.") - # If file doesn't exist locally, create an empty one - if not os.path.exists(DATA_FILE): - with open(DATA_FILE, 'w', encoding='utf-8') as f: - json.dump({'users': {}, 'files': {}}, f) + logging.warning("Skipping HF DB download: Read token not configured.") return try: hf_hub_download( @@ -105,382 +103,129 @@ def download_db_from_hf(): token=HF_TOKEN_READ, local_dir=".", local_dir_use_symlinks=False, - force_download=True, # Force download to get latest version - resume_download=False # Start fresh + force_download=True # Force download to get the latest version ) logging.info("Database downloaded from Hugging Face") - except Exception as e: - logging.error(f"Error downloading database: {e}") + 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"Creating empty {DATA_FILE} as download failed and file doesn't exist.") + 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': {}, 'files': {}}, 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...") - upload_db_to_hf() + # 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.") + def get_file_type(filename): - ext = os.path.splitext(filename)[1].lower() - if ext in ('.mp4', '.mov', '.avi', '.webm', '.mkv', '.flv'): + filename_lower = filename.lower() + if filename_lower.endswith(('.mp4', '.mov', '.avi', '.webm', '.mkv')): return 'video' - elif ext in ('.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg'): + elif filename_lower.endswith(('.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg')): return 'image' - elif ext == '.pdf': + elif filename_lower.endswith('.pdf'): return 'pdf' - elif ext == '.txt': + elif filename_lower.endswith('.txt'): return 'text' - return 'other' - -def generate_unique_filename(original_filename): - _, ext = os.path.splitext(original_filename) - safe_base = secure_filename(os.path.splitext(original_filename)[0]) - if not safe_base: # Handle cases like ".bashrc" - safe_base = "file" - unique_id = uuid.uuid4().hex[:8] # Shorter UUID part - return f"{safe_base}_{unique_id}{ext}" - -def get_folder_icon(): - return ''' - - - ''' - -def get_file_icon(file_type): - # Basic file icon, could be expanded with specific icons per type - return ''' - - - ''' + # Add more types as needed + # elif filename_lower.endswith(('.doc', '.docx')): + # return 'document' + else: + 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}" + BASE_STYLE = ''' :root { - --primary: #ff4d6d; /* Hot Pink */ - --secondary: #00ddeb; /* Cyan */ - --accent: #8b5cf6; /* Violet */ - --success: #10b981; /* Emerald */ - --warning: #f59e0b; /* Amber */ - --danger: #ef4444; /* Red */ - --info: #3b82f6; /* Blue */ - --background-light: #f8fafc; /* Slate 50 */ - --background-dark: #0f172a; /* Slate 900 */ - --card-bg-light: #ffffff; - --card-bg-dark: #1e293b; /* Slate 800 */ - --text-light: #334155; /* Slate 700 */ - --text-dark: #e2e8f0; /* Slate 200 */ - --border-light: #e2e8f0; /* Slate 200 */ - --border-dark: #334155; /* Slate 700 */ - --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); - --shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); - --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); - --radius-sm: 0.25rem; - --radius: 0.5rem; - --radius-lg: 0.75rem; - --transition: all 0.2s ease-in-out; -} -*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap'); -body { - font-family: 'Inter', sans-serif; - background-color: var(--background-light); - color: var(--text-light); - line-height: 1.6; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} -body.dark { - background-color: var(--background-dark); - color: var(--text-dark); -} -.container { - max-width: 1300px; - margin: 2rem auto; - padding: 1.5rem 2rem; -} -.card { - background-color: var(--card-bg-light); - border-radius: var(--radius-lg); - box-shadow: var(--shadow); - padding: 2rem; - margin-bottom: 1.5rem; -} -body.dark .card { background-color: var(--card-bg-dark); } -h1 { - font-size: 2.25rem; /* 36px */ - font-weight: 800; - text-align: center; - margin-bottom: 2rem; - background: linear-gradient(135deg, var(--primary), var(--accent)); - -webkit-background-clip: text; - color: transparent; -} -h2 { - font-size: 1.5rem; /* 24px */ - font-weight: 700; - margin-top: 2rem; - margin-bottom: 1rem; - border-bottom: 1px solid var(--border-light); - padding-bottom: 0.5rem; -} -body.dark h2 { color: var(--text-dark); border-bottom-color: var(--border-dark); } -input[type="text"], input[type="password"], input[type="file"], textarea { - width: 100%; - padding: 0.75rem 1rem; - margin: 0.5rem 0 1rem 0; - border: 1px solid var(--border-light); - border-radius: var(--radius); - background-color: var(--background-light); - color: var(--text-light); - font-size: 1rem; - transition: var(--transition); -} -body.dark input[type="text"], body.dark input[type="password"], body.dark input[type="file"], body.dark textarea { - background-color: var(--background-dark); - color: var(--text-dark); - border-color: var(--border-dark); -} -input:focus, textarea:focus { - outline: none; - border-color: var(--primary); - box-shadow: 0 0 0 3px rgba(255, 77, 109, 0.3); /* primary with opacity */ -} -input[type="file"] { padding: 0.5rem; } -input::file-selector-button { - padding: 0.5rem 1rem; - border: none; - background-color: var(--accent); - color: white; - border-radius: var(--radius-sm); - cursor: pointer; - margin-right: 1rem; - transition: var(--transition); -} -input::file-selector-button:hover { background-color: #7c3aed; } /* Darker accent */ -.btn { - padding: 0.75rem 1.5rem; - border: none; - border-radius: var(--radius); - cursor: pointer; - font-size: 1rem; - font-weight: 600; - transition: var(--transition); - text-decoration: none; - display: inline-block; - text-align: center; - box-shadow: var(--shadow-sm); -} -.btn-primary { background-color: var(--primary); color: white; } -.btn-primary:hover { background-color: #e6415f; transform: translateY(-2px); box-shadow: var(--shadow); } -.btn-secondary { background-color: var(--secondary); color: var(--text-light); } -.btn-secondary:hover { background-color: #00b8c5; transform: translateY(-2px); box-shadow: var(--shadow); } -.btn-accent { background-color: var(--accent); color: white; } -.btn-accent:hover { background-color: #7c3aed; transform: translateY(-2px); box-shadow: var(--shadow); } -.btn-danger { background-color: var(--danger); color: white; } -.btn-danger:hover { background-color: #dc2626; transform: translateY(-2px); box-shadow: var(--shadow); } -.btn-sm { padding: 0.5rem 1rem; font-size: 0.875rem; } -.flash { - padding: 1rem; - margin-bottom: 1rem; - border-radius: var(--radius); - text-align: center; - font-weight: 500; -} -.flash-success { background-color: #d1fae5; color: #065f46; } /* Green */ -.flash-error { background-color: #fee2e2; color: #991b1b; } /* Red */ -.flash-info { background-color: #dbeafe; color: #1e40af; } /* Blue */ -body.dark .flash-success { background-color: #059669; color: #d1fae5; } -body.dark .flash-error { background-color: #b91c1c; color: #fee2e2; } -body.dark .flash-info { background-color: #2563eb; color: #dbeafe; } - -.breadcrumb { - display: flex; - align-items: center; - margin-bottom: 1.5rem; - font-size: 0.9rem; - color: #64748b; /* Slate 500 */ - background-color: var(--card-bg-light); - padding: 0.75rem 1.5rem; - border-radius: var(--radius); - box-shadow: var(--shadow-sm); -} -body.dark .breadcrumb { background-color: var(--card-bg-dark); color: #94a3b8; } /* Slate 400 */ -.breadcrumb a { color: var(--primary); text-decoration: none; font-weight: 500; } -.breadcrumb a:hover { text-decoration: underline; } -.breadcrumb span { margin: 0 0.5rem; } - -.file-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); - gap: 1.5rem; - margin-top: 1.5rem; -} -.grid-item { - background-color: var(--card-bg-light); - border-radius: var(--radius-lg); - padding: 1rem; - box-shadow: var(--shadow); - text-align: center; - transition: var(--transition); - position: relative; - overflow: hidden; - cursor: pointer; -} -body.dark .grid-item { background-color: var(--card-bg-dark); } -.grid-item:hover { transform: translateY(-5px); box-shadow: var(--shadow-lg); } -.grid-item-icon { margin-bottom: 0.5rem; } -.grid-item-icon img, .grid-item-icon video { - width: 100%; - height: 120px; - object-fit: cover; - border-radius: var(--radius-sm); - background-color: #e2e8f0; /* Placeholder bg */ -} -.grid-item-icon .file-placeholder-icon svg { - width: 64px; height: 64px; margin: 28px auto; /* Center vertically */ - color: #94a3b8; /* Slate 400 */ -} -body.dark .grid-item-icon .file-placeholder-icon svg { color: #64748b; } /* Slate 500 */ -.grid-item-name { - font-weight: 500; - font-size: 0.9rem; - margin-bottom: 0.5rem; - word-break: break-all; /* Prevent long names overflowing */ - line-height: 1.3; - height: 2.6em; /* Limit to 2 lines */ - overflow: hidden; - text-overflow: ellipsis; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; -} -.grid-item-actions { - margin-top: 0.75rem; - display: flex; - justify-content: center; - gap: 0.5rem; -} -.grid-item-actions .btn { padding: 0.3rem 0.6rem; font-size: 0.75rem; } - -.modal { - display: none; - position: fixed; - z-index: 1000; - left: 0; top: 0; - width: 100%; height: 100%; - overflow: auto; - background-color: rgba(0,0,0,0.85); - align-items: center; - justify-content: center; - padding: 20px; -} -.modal-content { - position: relative; - margin: auto; - padding: 0; - background-color: var(--card-bg-dark); /* Dark background for better contrast */ - border-radius: var(--radius-lg); - max-width: 90vw; - max-height: 90vh; - display: flex; - justify-content: center; - align-items: center; - overflow: hidden; -} -.modal img, .modal video, .modal iframe, .modal pre { - max-width: 100%; - max-height: calc(90vh - 40px); /* Account for padding */ - display: block; - object-fit: contain; - border-radius: var(--radius-sm); -} -.modal iframe { width: 80vw; height: 85vh; background-color: #fff; } -.modal pre { - background-color: #f1f5f9; /* Light bg for text */ - color: #1e293b; - padding: 1.5rem; - border-radius: var(--radius-sm); - white-space: pre-wrap; - word-wrap: break-word; - overflow: auto; - font-family: 'Courier New', Courier, monospace; - font-size: 0.9rem; - max-height: calc(90vh - 60px); -} -.modal-close { - position: absolute; - top: 15px; - right: 25px; - color: #f1f1f1; - font-size: 40px; - font-weight: bold; - transition: 0.3s; - cursor: pointer; - z-index: 1010; -} -.modal-close:hover, .modal-close:focus { color: #bbb; text-decoration: none; } - -#progress-container { - width: 100%; - background-color: var(--border-light); - border-radius: var(--radius); - margin: 1rem 0; - overflow: hidden; - display: none; -} -body.dark #progress-container { background-color: var(--border-dark); } -#progress-bar { - width: 0%; - height: 15px; - background-color: var(--primary); - border-radius: var(--radius); - transition: width 0.4s ease; - text-align: center; - line-height: 15px; - color: white; - font-size: 0.75rem; -} - -.user-list { margin-top: 1.5rem; } -.user-item { - display: flex; - justify-content: space-between; - align-items: center; - padding: 1rem 1.5rem; - background-color: var(--card-bg-light); - border-radius: var(--radius); - margin-bottom: 1rem; - box-shadow: var(--shadow-sm); - transition: var(--transition); -} -body.dark .user-item { background-color: var(--card-bg-dark); } -.user-item:hover { box-shadow: var(--shadow); } -.user-item a { color: var(--primary); text-decoration: none; font-weight: 600; font-size: 1.1rem;} -.user-item a:hover { color: var(--accent); } -.user-item p { font-size: 0.9rem; color: #64748b; } -body.dark .user-item p { color: #94a3b8; } -.user-item .actions { display: flex; gap: 0.5rem; } - -@media (max-width: 768px) { - .file-grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 1rem; } - .grid-item { padding: 0.75rem; } - .grid-item-icon img, .grid-item-icon video { height: 100px; } - .grid-item-icon .file-placeholder-icon svg { width: 48px; height: 48px; margin: 26px auto; } - h1 { font-size: 1.75rem; } - h2 { font-size: 1.25rem; } - .container { padding: 1rem; margin: 1rem auto; } - .card { padding: 1.5rem; } - .user-item { flex-direction: column; align-items: flex-start; gap: 0.5rem;} - .user-item .actions { margin-top: 0.5rem; } -} -@media (max-width: 480px) { - .file-grid { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 0.75rem; } - .grid-item-name { font-size: 0.8rem; height: 2.4em; } - .btn { padding: 0.6rem 1.2rem; font-size: 0.9rem; } - .breadcrumb { font-size: 0.8rem; padding: 0.5rem 1rem; } - .modal-content { max-width: 95vw; } + --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; } +* { 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; margin-bottom: 15px; 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; } +.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:hover { background: #00b8c5; } +.delete-btn { background: var(--delete-color); margin-top: 10px; } +.delete-btn:hover { background: #cc3333; } +.flash { padding: 15px; margin-bottom: 15px; border-radius: 10px; text-align: center; } +.flash.success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; } +.flash.error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; } +.flash.info { background-color: #d1ecf1; color: #0c5460; border: 1px solid #bee5eb; } +.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; } +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; } +#progress-container { width: 100%; background: var(--glass-bg); border-radius: 10px; margin: 15px 0; display: none; } +#progress-bar { width: 0%; height: 20px; background: var(--primary); border-radius: 10px; transition: width 0.3s ease; } +.breadcrumbs { margin-bottom: 20px; font-size: 1.1em; } +.breadcrumbs a { color: var(--accent); text-decoration: none; } +.breadcrumbs span { margin: 0 5px; } +#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); } ''' @app.route('/register', methods=['GET', 'POST']) @@ -492,12 +237,13 @@ def register(): if not username or not password: flash('Имя пользователя и пароль обязательны!', 'error') return redirect(url_for('register')) - + # Basic validation (add more robust checks as needed) - if len(username) < 3 or len(password) < 6: - flash('Имя пользователя должно быть не менее 3 символов, пароль - не менее 6.', 'error') + if not username.isalnum() or len(username) < 3: + flash('Имя пользователя должно быть не менее 3 символов и содержать только буквы и цифры.', 'error') return redirect(url_for('register')) + data = load_data() if username in data['users']: @@ -505,21 +251,31 @@ def register(): return redirect(url_for('register')) data['users'][username] = { - 'password': password, # Store password directly (INSECURE! Use hashing in production) - 'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), - 'files': [], - 'folders': [] # Initialize folders list + '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 } 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}") return redirect(url_for('dashboard')) except Exception as e: - flash('Ошибка сохранения данных при регистрации.', 'error') - logging.error(f"Registration save error: {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 = ''' @@ -527,72 +283,52 @@ def register(): Регистрация - Zeus Cloud - + +
-
-

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

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

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

-
+

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

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

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

''' - return render_template_string(html, style=BASE_STYLE) + return render_template_string(html) @app.route('/', methods=['GET', 'POST']) def login(): if request.method == 'POST': - content_type = request.headers.get('Content-Type') - if content_type == 'application/json': - req_data = request.get_json() - username = req_data.get('username') - password = req_data.get('password') - is_ajax = True - elif content_type == 'application/x-www-form-urlencoded': - username = request.form.get('username') - password = request.form.get('password') - is_ajax = True # Assume form submission via JS is AJAX - else: # Handle direct form submission without JS - username = request.form.get('username') - password = request.form.get('password') - is_ajax = False - - if not username or not password: - if is_ajax: - return jsonify({'status': 'error', 'message': 'Имя пользователя и пароль обязательны!'}) - else: - flash('Имя пользователя и пароль обязательны!', 'error') - return redirect(url_for('login')) - + username = request.form.get('username') + password = request.form.get('password') data = load_data() - if username in data['users'] and data['users'][username]['password'] == password: # Direct password check (INSECURE!) + # 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 session['username'] = username - if is_ajax: - return jsonify({'status': 'success', 'redirect': url_for('dashboard')}) - else: - flash('Вход выполнен успешно!', 'success') - return redirect(url_for('dashboard')) + # Check if localStorage save is requested (e.g., via a checkbox) + # For simplicity, always save on successful login for now + return jsonify({'status': 'success', 'redirect': url_for('dashboard')}) else: - if is_ajax: - return jsonify({'status': 'error', 'message': 'Неверное имя пользователя или пароль!'}) - else: - flash('Неверное имя пользоват��ля или пароль!', 'error') - return redirect(url_for('login')) + return jsonify({'status': 'error', 'message': 'Неверное имя пользователя или пароль!'}) + + # If already logged in (session exists), redirect to dashboard + if 'username' in session: + return redirect(url_for('dashboard')) html = ''' @@ -601,190 +337,271 @@ def login(): Вход - Zeus Cloud - + +
-
-

Zeus Cloud

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

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

+

Zeus Cloud

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

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

''' - return render_template_string(html, style=BASE_STYLE) + return render_template_string(html) - -@app.route('/dashboard', methods=['GET', 'POST']) -def dashboard(): +@app.route('/dashboard/', defaults={'current_path': ''}) +@app.route('/dashboard/', methods=['GET', 'POST']) +def dashboard(current_path): if 'username' not in session: flash('Пожалуйста, войдите в систему!', 'info') return redirect(url_for('login')) username = session['username'] - data = load_data() - if username not in data['users']: + data = load_data() # Load user data, primarily for auth check + if username not in data.get('users', {}): session.pop('username', None) flash('Пользователь не найден!', 'error') return redirect(url_for('login')) - user_data = data['users'][username] - user_data.setdefault('files', []) - user_data.setdefault('folders', []) - - current_path = request.args.get('path', '/') - if not current_path.startswith('/'): current_path = '/' + current_path - if not current_path.endswith('/'): current_path += '/' - + # Normalize path: remove leading/trailing slashes for consistency internally + current_path = current_path.strip('/') if request.method == 'POST': - files = request.files.getlist('files') - target_path = request.form.get('current_path', '/') - if not target_path.startswith('/'): target_path = '/' + target_path - if not target_path.endswith('/'): target_path += '/' + action = request.form.get('action') - if not files or files[0].filename == '': - flash('Файлы для загрузки не выбраны.', 'warning') - return redirect(url_for('dashboard', path=target_path)) + if action == 'upload': + if not HF_TOKEN_WRITE: + flash('Ошибка: Загрузка невозможна, токен записи HF не настроен.', 'error') + return redirect(url_for('dashboard', current_path=current_path)) - if len(files) > 20: - flash('Максимум 20 файлов за раз!', 'warning') - return redirect(url_for('dashboard', path=target_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)) - api = HfApi(token=HF_TOKEN_WRITE) - uploaded_file_infos = [] + # 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 = [] - try: for file in files: - if file: - original_filename = file.filename - unique_filename = generate_unique_filename(original_filename) - temp_path = os.path.join(UPLOAD_FOLDER, f"{uuid.uuid4()}_{unique_filename}") # Unique temp name + 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 file.save(temp_path) - hf_file_path = f"cloud_files/{username}{target_path.lstrip('/')}{unique_filename}" - - api.upload_file( - path_or_fileobj=temp_path, - path_in_repo=hf_file_path, - repo_id=REPO_ID, - repo_type="dataset", - commit_message=f"Upload: {original_filename} for {username} to {target_path}" - ) - - file_info = { - 'original_filename': original_filename, - 'unique_filename': unique_filename, # Stored on HF - 'hf_path': hf_file_path, # Full path in HF repo - 'path_prefix': target_path, # Directory path within user's space - 'type': get_file_type(original_filename), - 'upload_date': datetime.now().strftime('%Y-%m-%d %H:%M:%S') - } - uploaded_file_infos.append(file_info) - - if os.path.exists(temp_path): - os.remove(temp_path) - - user_data['files'].extend(uploaded_file_infos) - save_data(data) - flash(f'{len(uploaded_file_infos)} Файл(ов) успешно загружено!', 'success') + # 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 + + if uploaded_count > 0: + flash(f'{uploaded_count} файл(ов) успешно загружено!', '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}") + + return redirect(url_for('dashboard', current_path=current_path)) + + + # --- GET Request Logic --- + items = [] + folders = [] + files_list = [] + + if HF_TOKEN_READ: # Only list files if read token is available + 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('\\', '/') + + logging.info(f"Listing files for user {username} in HF path: {target_path_in_repo}") + + # Check if the target path exists before listing + if not HF_FS.exists(target_path_in_repo): + logging.warning(f"Path {target_path_in_repo} does not exist in HF repo.") + # It might be the user's root, which is fine if empty + if current_path != "": # If not root, maybe flash a message? + flash(f"Папка '{current_path}' не найдена или пуста.", 'info') + + else: + # Use HfFileSystem.ls() which returns detailed info + repo_items = HF_FS.ls(target_path_in_repo, detail=True) + + 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 + + relative_item_path = Path(item_path).relative_to(user_base_fs_path).as_posix() # 'folder/file.txt' or 'folder' + + item_type = item_info['type'] # 'file' or 'directory' + + 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) except Exception as e: - logging.error(f"File upload error for {username}: {e}") - flash(f'Ошибка при загрузке файлов: {e}', 'error') - # Clean up any partially saved temp files if error occurs mid-loop - for temp_path, _ in [(os.path.join(UPLOAD_FOLDER, f"{uuid.uuid4()}_{generate_unique_filename(f.filename)}"), f.filename) for f in files if f]: - if os.path.exists(temp_path): - try: os.remove(temp_path) - except OSError: pass - - return redirect(url_for('dashboard', path=target_path)) - - - # Filter files and folders for the current path - current_files = sorted( - [f for f in user_data['files'] if f.get('path_prefix') == current_path], - key=lambda x: x.get('upload_date', ''), reverse=True - ) - # Folders are represented simply by their paths ending in '/' - current_folders = sorted( - [f for f in user_data['folders'] if f.startswith(current_path) and f != current_path and f.count('/') == current_path.count('/')] - ) - - # Build breadcrumbs - breadcrumbs = [] - path_parts = list(filter(None, current_path.split('/'))) - temp_path = '/' - breadcrumbs.append({'name': 'Корень', 'path': '/'}) - for i, part in enumerate(path_parts): - temp_path += part + '/' - breadcrumbs.append({'name': part, 'path': temp_path}) + 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 = ''' @@ -794,208 +611,181 @@ def dashboard(): Панель управления - Zeus Cloud - - + + +
-

Zeus Cloud

-
- Пользователь: {{ username }} - Выйти -
+

Zeus Cloud

+

Пользователь: {{ username }}

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

Загрузка файлов

-
- - - -
-
+ -
-

Создать папку

-
- - - +

Действия

+ + + + +
+
+ +
+
+ + +
-
- +
-
-

Содержимое: {{ current_path }}

- - -
- {# Display Folders #} - {% for folder_path in current_folders %} - {% set folder_name = folder_path.strip('/').split('/')[-1] %} - -
{folder_icon | safe}
-
{{ folder_name }}
-
-
- -
-
-
- {% endfor %} - {# Display Files #} - {% for file in current_files %} -
-
- {% if file.type == 'image' %} - {{ file.original_filename }} - {% elif file.type == 'video' %} -
-
-

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

-

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

-
-
-

Android (Chrome):

-
    -
  1. Нажмите меню (три точки).
  2. -
  3. Выберите "Установить приложение" или "Добавить на главный экран".
  4. -
  5. Подтвердите добавление.
  6. -
-
-
-

iOS (Safari):

-
    -
  1. Нажмите кнопку "Поделиться" .
  2. -
  3. Прокрутите вниз и выберите "На экран «Домой»".
  4. -
  5. Нажмите "Добавить".
  6. -
-
-
-
+

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

+

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

+ + Выйти
-