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 }}
+
+
+ {% for crumb in breadcrumbs %}
+ {% if not crumb.is_last %}
+
{{ crumb.name }} /
+ {% else %}
+
{{ crumb.name }}
+ {% endif %}
+ {% endfor %}
+
+
+{% 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' %}
+
+ {% 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 %}
-
-
- {% for crumb in breadcrumbs %}
- {% if loop.last %}
-
{{ crumb.name }}
- {% else %}
-
{{ crumb.name }}
-
/
- {% endif %}
- {% endfor %}
-
+ # 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' %}
-
- {% else %}
-
-
- {% if item.type == 'image' %}
-
- {% 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 %}
-
-
- {% for crumb in breadcrumbs %}
- {% if loop.last %}
-
{{ crumb.name }}
+ user_data = data['users'][username]
+ user_items = user_data.get('items', [])
+ current_path = normalize_path(request.args.get('path', '/'))
+
+ items_in_current_path, parent_path = get_items_in_path(user_items, current_path)
+ breadcrumbs = get_breadcrumbs(current_path)
+
+ html = f'''
+
+
Files for {{ username }} - Admin
+
+
+
+
Files for User: {{ username }}
+
Back to User List
+
+ {% for crumb in breadcrumbs %}
+ {% if not crumb.is_last %}
+
{{ crumb.name }} /
+ {% else %}
+
{{ crumb.name }}
+ {% endif %}
+ {% endfor %}
+
+{% with messages = get_flashed_messages(with_categories=true) %}
+ {% if messages %}{% for category, message in messages %}
{{ message }}
{% endfor %}{% endif %}
+{% endwith %}
+
+{% if parent_path is not none %}
+
⬆️ Go Up
+{% endif %}
+
+
+ {% for item in items_in_current_path %}
+
+
+ {% if item.type == 'folder' %}
+
+
+
+ {% elif item.file_type == 'image' %}
+
+ {% elif item.file_type == 'video' %}
+
+ {% elif item.file_type == 'pdf' %}
+
+ {% elif item.file_type == 'text' %}
+
{% else %}
-
{{ crumb.name }}
-
/
+
{% endif %}
- {% endfor %}
-
-
-
Содержимое папки: {{ current_path or 'Корень' }}
-
- {% for item in items %}
- {% if item.type == 'directory' %}
-
- {% else %}
-
-
- {% if item.type == 'image' %}
- {% elif item.type == 'video' %}
- {% elif item.type == 'pdf' %}
- {% elif item.type == 'text' %}
- {% else %}
{% endif %}
-
-
{{ item.original_name }}
-
{{ (item.size / 1024 / 1024) | round(2) }} MB
-
Скачать
-
-
+
+
+
+ {% if item.type == 'folder' %}
+ {{ item.name }}
+ {% else %}
+ {{ item.original_filename }}({{ item.name }})
+ {% endif %}
+
+
Path: {{ item.path }}
+
Created/Uploaded: {{ item.upload_date or item.created_at }}
+
+
+ {% if item.type == 'file' %}
+
Download
{% endif %}
- {% else %}
-
Эта папка пуста.
- {% endfor %}
+
+
+ {% endfor %}
+ {% if not items_in_current_path %}
+
This folder is empty for user {{ username }}.
+ {% endif %}
+
+
+
+
+
+'''
+ return render_template_string(html, username=username, items_in_current_path=items_in_current_path,
+ current_path=current_path, parent_path=parent_path, breadcrumbs=breadcrumbs,
+ get_hf_item_url=get_hf_item_url, HF_TOKEN_READ=HF_TOKEN_READ)
@app.route('/admhosto/delete_user/
', methods=['POST'])
def admin_delete_user(username):
- if not check_admin():
- flash('Доступ запрещен.', 'error')
- return redirect(url_for('login'))
-
- if not HF_TOKEN_WRITE:
- flash('Ошибка: Удаление пользователя невозможно, токен записи HF не настроен.', 'error')
- return redirect(url_for('admin_panel'))
+ if not is_admin():
+ flash('Admin privileges required.', 'error')
+ 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'))
- # Path to the user's entire directory on HF Hub
- user_folder_api_path = get_user_base_path(username) # e.g., "cloud_files/testuser"
+ logging.warning(f"ADMIN ACTION: Attempting to delete user {username} and all their files.")
+ api = get_hf_api()
+ hf_user_folder = f"cloud_files/{username}"
+ delete_folder_error = False
- try:
- api = HfApi()
- # Attempt to delete the user's entire folder on HF Hub
+ # Step 1: Delete user's folder from Hugging Face Hub
+ if HF_TOKEN_WRITE:
try:
- logging.info(f"Admin attempting to delete HF folder: {user_folder_api_path}")
- api.delete_folder(
- folder_path=user_folder_api_path,
- repo_id=REPO_ID,
- repo_type="dataset",
- token=HF_TOKEN_WRITE,
- commit_message=f"Admin deleted all files for user {username}"
- )
- logging.info(f"Successfully deleted HF folder for user {username}")
- except Exception as folder_delete_error:
- # Log error but proceed to delete user from DB anyway
- # It might be the folder was already empty/deleted, or another issue.
- logging.error(f"Error deleting HF folder {user_folder_api_path} for user {username}: {folder_delete_error}. User DB entry will still be removed.")
- # Optionally flash a warning about potential orphaned files if deletion failed badly.
- # flash(f'Предупреждение: Не удалось полностью очистить файлы пользователя {username} на HF. {folder_delete_error}', 'warning')
-
-
- # Delete the user from the local database
- del data['users'][username]
- save_data(data) # Save changes to local JSON and potentially upload to HF
-
- flash(f'Пользователь {username} и его файлы (попытка удаления) успешно удалены из системы.', 'success')
- logging.info(f"Admin deleted user {username} from database.")
- cache.clear() # Clear cache
+ logging.info(f"Admin deleting HF folder: {hf_user_folder}")
+ api.delete_folder(
+ folder_path=hf_user_folder,
+ repo_id=REPO_ID,
+ repo_type="dataset",
+ token=HF_TOKEN_WRITE,
+ commit_message=f"Admin deleted user {username} and all files"
+ )
+ logging.info(f"Successfully deleted HF folder for user {username}.")
+ except Exception as e:
+ # Log error but continue to delete user from DB
+ logging.error(f"Error deleting HF folder {hf_user_folder} for user {username}: {e}")
+ # Check if it was "not found" error, which is okay if user had no uploads
+ if "404" not in str(e) and "not found" not in str(e).lower():
+ delete_folder_error = True
+ flash(f"Warning: Could not completely delete storage folder for {username}. Check HF repo.", 'warning')
+ else:
+ logging.info(f"HF folder {hf_user_folder} likely did not exist or was already empty.")
+
+ else:
+ logging.warning("HF_TOKEN_WRITE not set. Cannot delete user's folder from HF.")
+ flash("Warning: Cannot delete user files from storage (admin token missing).", 'warning')
+ # Set flag to indicate potential orphaned files
+ if data['users'][username].get('items'):
+ delete_folder_error = True
+ # Step 2: Delete user from the database
+ del data['users'][username]
+
+ try:
+ save_data(data)
+ log_msg = f"Admin successfully deleted user {username}."
+ if delete_folder_error:
+ log_msg += " (Potential errors deleting storage folder)"
+ logging.warning(log_msg) # Log as warning because it's a destructive admin action
+ flash(f'User "{username}" has been deleted.', 'success' if not delete_folder_error else 'warning')
except Exception as e:
- logging.error(f"Error during admin deletion of user {username}: {e}")
- flash(f'Произошла ошибка при удалении пользователя {username}: {e}', 'error')
+ flash('Error saving changes after deleting user.', 'error')
+ logging.error(f"Failed to save data after deleting user {username}: {e}")
+ # If save fails, user is deleted in memory but not persisted - critical error
return redirect(url_for('admin_panel'))
@app.route('/admhosto/delete_item//', methods=['POST'])
def admin_delete_item(username, item_path):
- if not check_admin():
- flash('Доступ запрещен.', 'error')
- return redirect(url_for('login'))
-
- if not HF_TOKEN_WRITE:
- flash('Ошибка: Удаление невозможно, токен записи HF не настроен.', 'error')
- parent_path = str(Path(item_path).parent)
- if parent_path == '.': parent_path = ''
- return redirect(url_for('admin_user_files', username=username, 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('\\','/')
- item_name = Path(item_path).name
- is_folder = False
- full_fs_path = f"datasets/{REPO_ID}/{full_hf_api_path}"
-
- try:
- if HF_FS.exists(full_fs_path):
- is_folder = HF_FS.isdir(full_fs_path)
- else:
- flash(f'Элемент "{item_name}" не найден для удаления.', 'warning')
- parent_path = str(Path(item_path).parent); parent_path = '' if parent_path == '.' else parent_path
- return redirect(url_for('admin_user_files', username=username, current_path=parent_path))
-
- api = HfApi()
- item_type_str = "папка" if is_folder else "файл"
- if is_folder:
- api.delete_folder(
- folder_path=full_hf_api_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE,
- commit_message=f"Admin deleted folder {item_name} for user {username}"
- )
- else:
- api.delete_file(
- path_in_repo=full_hf_api_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE,
- commit_message=f"Admin deleted file {item_name} for user {username}"
- )
-
- flash(f'{item_type_str.capitalize()} "{item_name}" успешно удален(а).', 'success')
- logging.info(f"Admin deleted {item_type_str} {full_hf_api_path} for user {username}")
- cache.clear()
-
- except Exception as e:
- item_type_str = "папку" if is_folder else "файл"
- logging.error(f"[Admin] Error deleting {item_type_str} {full_hf_api_path} for user {username}: {e}")
- flash(f'Ошибка при удалении {item_type_str} "{item_name}": {e}', 'error')
-
- parent_path = str(Path(item_path).parent); parent_path = '' if parent_path == '.' else parent_path
- return redirect(url_for('admin_user_files', username=username, current_path=parent_path))
+ if not is_admin():
+ flash('Admin privileges required.', 'error')
+ return redirect(url_for('admin_login'))
+
+ data = load_data()
+ if username not in data.get('users', {}):
+ flash(f'User "{username}" not found.', 'error')
+ return redirect(url_for('admin_panel'))
+
+ item_path_normalized = normalize_path(item_path)
+ user_items = data['users'][username].get('items', [])
+ item_to_delete = next((item for item in user_items if item['path'] == item_path_normalized), None)
+
+ if not item_to_delete:
+ flash('Item not found for this user.', 'error')
+ referer = request.referrer or url_for('admin_user_files', username=username)
+ return redirect(referer)
+
+ current_view_path = '/'.join(item_path_normalized.split('/')[:-1]) or '/'
+ logging.warning(f"ADMIN ACTION: Attempting deletion of item '{item_path_normalized}' for user {username}.")
+
+ api = get_hf_api()
+ errors = []
+ deleted_hf_paths = []
+ items_to_remove_from_db = []
+
+ if item_to_delete['type'] == 'file':
+ items_to_remove_from_db.append(item_to_delete)
+ hf_path = item_to_delete.get('hf_path')
+ if hf_path:
+ deleted_hf_paths.append(hf_path)
+ else:
+ logging.warning(f"Admin Delete: File item {item_path_normalized} missing hf_path.")
+
+ elif item_to_delete['type'] == 'folder':
+ items_to_remove_from_db.append(item_to_delete)
+ folder_prefix = item_path_normalized + ('/' if item_path_normalized != '/' else '')
+ children_to_delete = [item for item in user_items if item['path'].startswith(folder_prefix) and item['path'] != item_path_normalized]
+
+ for child in children_to_delete:
+ items_to_remove_from_db.append(child)
+ if child['type'] == 'file' and child.get('hf_path'):
+ deleted_hf_paths.append(child['hf_path'])
+
+ folder_hf_base = f"cloud_files/{username}"
+ relative_folder_path = item_path_normalized.lstrip('/')
+ hf_folder_path_to_delete = f"{folder_hf_base}/{relative_folder_path}" if relative_folder_path else folder_hf_base
+
+ if HF_TOKEN_WRITE:
+ try:
+ logging.info(f"Admin deleting HF folder: {hf_folder_path_to_delete}")
+ api.delete_folder(folder_path=hf_folder_path_to_delete, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, commit_message=f"Admin deleted folder {item_path_normalized} for {username}")
+ logging.info(f"Admin successfully deleted HF folder: {hf_folder_path_to_delete}")
+ except Exception as e:
+ logging.error(f"Admin error deleting folder {hf_folder_path_to_delete} from HF: {e}.")
+ if "404" not in str(e) and "not found" not in str(e).lower():
+ errors.append(f"Could not fully remove storage folder '{item_to_delete['name']}'.")
+ else:
+ logging.warning("HF_TOKEN_WRITE not set. Cannot delete folder from HF.")
+ if any(item['type'] == 'file' for item in children_to_delete):
+ errors.append("Cannot delete storage folder (token missing).")
+
+ # Delete individual files (if needed)
+ if HF_TOKEN_WRITE:
+ for hf_path_to_delete in deleted_hf_paths:
+ try:
+ logging.info(f"Admin deleting HF file: {hf_path_to_delete}")
+ api.delete_file(path_in_repo=hf_path_to_delete, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, commit_message=f"Admin deleted item {hf_path_to_delete} for {username}")
+ except Exception as e:
+ logging.error(f"Admin error deleting file {hf_path_to_delete} from HF: {e}")
+ if "404" not in str(e) and "not found" not in str(e).lower():
+ errors.append(f"Failed to delete file '{hf_path_to_delete.split('/')[-1]}' from storage.")
+ elif deleted_hf_paths:
+ logging.warning(f"HF_TOKEN_WRITE not set. Cannot delete {len(deleted_hf_paths)} associated files from HF.")
+ errors.append("Cannot delete files from storage (token missing).")
+
+ # Update DB
+ paths_to_remove = {item['path'] for item in items_to_remove_from_db}
+ data['users'][username]['items'] = [item for item in user_items if item['path'] not in paths_to_remove]
+
+ try:
+ save_data(data)
+ item_display_name = item_to_delete.get('original_filename') or item_to_delete['name']
+ if not errors:
+ flash(f"Admin successfully deleted '{item_display_name}' for user {username}.", 'success')
+ else:
+ flash(f"Admin deleted '{item_display_name}' from listing, but errors occurred during storage cleanup.", 'warning')
+ for error in errors:
+ flash(error, 'error')
+ logging.warning(f"Admin deleted item '{item_path_normalized}' for user {username}. Errors: {len(errors)}")
+ except Exception as e:
+ flash('Error saving changes after admin deletion.', 'error')
+ logging.error(f"Failed to save data after admin deleting item for {username}: {e}")
+
+ return redirect(url_for('admin_user_files', username=username, path=current_view_path))
+
if __name__ == '__main__':
- # Initial data load/check on startup
- load_data()
+ if not HF_TOKEN_WRITE:
+ logging.warning("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
+ logging.warning("! HF_TOKEN (write access) is NOT SET.")
+ logging.warning("! File uploads, deletions, folder creations, and DB backups to HF Hub WILL FAIL.")
+ logging.warning("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
+ if not HF_TOKEN_READ:
+ logging.warning("HF_TOKEN_READ is not set. Using HF_TOKEN (write) for reads.")
+ logging.warning("Downloads/previews MIGHT fail if HF_TOKEN is not set or repo is private.")
+
+ # Initial data load/creation on startup
+ if not os.path.exists(DATA_FILE):
+ logging.info(f"{DATA_FILE} not found, attempting initial download or creating empty.")
+ try:
+ load_data() # Try to download/initialize
+ except Exception as e:
+ logging.error(f"Critical error during initial data load: {e}. Starting with empty.", exc_info=True)
+ with open(DATA_FILE, 'w', encoding='utf-8') as f:
+ json.dump({'users': {}}, f)
+
- # Start periodic backup thread only if write token is present
if HF_TOKEN_WRITE:
backup_thread = threading.Thread(target=periodic_backup, daemon=True)
backup_thread.start()
+ logging.info("Periodic backup thread started.")
else:
- logging.warning("Periodic backup thread not started: Write token missing.")
-
- # Run Flask app
- # Use waitress or gunicorn for production instead of Flask's built-in server
- port = int(os.environ.get("PORT", 7860))
- app.run(debug=False, host='0.0.0.0', port=port)
+ logging.warning("Periodic backup thread NOT started (HF_TOKEN_WRITE missing).")
-# --- END OF FILE app (8).py ---
\ No newline at end of file
+ # Use Gunicorn or Waitress in production instead of app.run(debug=True)
+ # Example: gunicorn --bind 0.0.0.0:7860 app:app
+ app.run(debug=False, host='0.0.0.0', port=7860) # Debug=False is important for production
\ No newline at end of file