diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,6 +1,4 @@ -# --- START OF FILE app (8).py --- - -from flask import Flask, render_template_string, request, redirect, url_for, session, flash, send_file, jsonify +from flask import Flask, render_template_string, request, redirect, url_for, session, flash, send_file, jsonify, send_from_directory from flask_caching import Cache import json import os @@ -13,19 +11,18 @@ from werkzeug.utils import secure_filename import requests from io import BytesIO import uuid -import os.path +import mimetypes app = Flask(__name__) app.secret_key = os.getenv("FLASK_SECRET_KEY", "supersecretkey") DATA_FILE = 'cloudeng_data.json' -REPO_ID = "Eluza133/Z1e1u" +REPO_ID = os.getenv("HF_REPO_ID", "Eluza133/Z1e1u") HF_TOKEN_WRITE = os.getenv("HF_TOKEN") HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") or HF_TOKEN_WRITE cache = Cache(app, config={'CACHE_TYPE': 'simple'}) logging.basicConfig(level=logging.INFO) -@cache.memoize(timeout=300) def load_data(): try: download_db_from_hf() @@ -33,14 +30,23 @@ def load_data(): data = json.load(file) if not isinstance(data, dict): logging.warning("Data is not in dict format, initializing empty database") - return {'users': {}, 'files': {}} + return {'users': {}} data.setdefault('users', {}) - data.setdefault('files', {}) + # Ensure each user has a root folder structure + for user_data in data['users'].values(): + if 'root' not in user_data or not isinstance(user_data['root'], dict) or user_data['root'].get('_type') != 'folder': + user_data['root'] = {'_type': 'folder', 'items': {}} logging.info("Data successfully loaded") return data + except FileNotFoundError: + logging.info(f"{DATA_FILE} not found, creating new database.") + return {'users': {}} + except json.JSONDecodeError: + logging.error(f"Error decoding JSON from {DATA_FILE}. Initializing empty database.") + return {'users': {}} except Exception as e: logging.error(f"Error loading data: {e}") - return {'users': {}, 'files': {}} + return {'users': {}} def save_data(data): try: @@ -55,7 +61,7 @@ def save_data(data): def upload_db_to_hf(): if not HF_TOKEN_WRITE: - logging.warning("HF_TOKEN_WRITE not set. Skipping database upload.") + logging.warning("HF_TOKEN_WRITE is not set. Skipping database upload.") return try: api = HfApi() @@ -72,12 +78,6 @@ def upload_db_to_hf(): logging.error(f"Error uploading database: {e}") def download_db_from_hf(): - if not HF_TOKEN_READ: - logging.warning("HF_TOKEN_READ not set. Cannot download database.") - if not os.path.exists(DATA_FILE): - with open(DATA_FILE, 'w', encoding='utf-8') as f: - json.dump({'users': {}, 'files': {}}, f) - return try: hf_hub_download( repo_id=REPO_ID, @@ -85,34 +85,84 @@ def download_db_from_hf(): repo_type="dataset", token=HF_TOKEN_READ, local_dir=".", - local_dir_use_symlinks=False, - force_download=True # Ensure we get the latest version + local_dir_use_symlinks=False ) logging.info("Database downloaded from Hugging Face") except Exception as e: logging.error(f"Error downloading database: {e}") if not os.path.exists(DATA_FILE): + logging.warning(f"{DATA_FILE} not found locally after failed download. Creating empty.") with open(DATA_FILE, 'w', encoding='utf-8') as f: - json.dump({'users': {}, 'files': {}}, f) + json.dump({'users': {}}, f) def periodic_backup(): while True: - time.sleep(1800) - logging.info("Attempting periodic backup...") upload_db_to_hf() + time.sleep(1800) -def get_file_type(filename): - filename_lower = filename.lower() - if filename_lower.endswith(('.mp4', '.mov', '.avi', '.webm', '.mkv', '.flv')): - return 'video' - elif filename_lower.endswith(('.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg')): +def get_mime_type(filename): + mime_type, _ = mimetypes.guess_type(filename) + if mime_type: + return mime_type + return 'application/octet-stream' + +def get_preview_type(mime_type): + if mime_type.startswith('image/'): return 'image' - elif filename_lower.endswith('.pdf'): + elif mime_type.startswith('video/'): + return 'video' + elif mime_type == 'application/pdf': return 'pdf' - elif filename_lower.endswith('.txt'): + elif mime_type == 'text/plain': return 'text' return 'other' +def get_item_at_path(data, username, path_list): + user_data = data['users'].get(username) + if not user_data: + return None + current_folder = user_data.get('root') + if not current_folder or current_folder.get('_type') != 'folder': + user_data['root'] = {'_type': 'folder', 'items': {}} # Attempt to fix broken structure + current_folder = user_data['root'] + + for part in path_list: + if current_folder.get('_type') != 'folder' or part not in current_folder.get('items', {}): + return None # Path segment is not a folder or doesn't exist + current_folder = current_folder['items'][part] + return current_folder + +def add_item_at_path(data, username, path_list, item_name, item_data): + parent_folder = get_item_at_path(data, username, path_list) + if parent_folder and parent_folder.get('_type') == 'folder': + parent_folder['items'][item_name] = item_data + return True + return False + +def delete_item_at_path(data, username, path_list, item_name): + parent_folder = get_item_at_path(data, username, path_list) + if parent_folder and parent_folder.get('_type') == 'folder' and item_name in parent_folder.get('items', {}): + deleted_item = parent_folder['items'].pop(item_name) + return deleted_item + return None + +def list_items_at_path(data, username, path_list): + folder = get_item_at_path(data, username, path_list) + if folder and folder.get('_type') == 'folder': + # Sort folders first, then files, alphabetically by name + items = sorted(folder.get('items', {}).items(), key=lambda item: (item[1].get('_type') != 'folder', item[0])) + return items + return [] + +def get_hf_storage_path(username, path_list, unique_name): + # Construct the full path on HF Hub including user folder and cloud path + user_base_path = f"cloud_files/{username}" + cloud_path = '/'.join(path_list) + if cloud_path: + return f"{user_base_path}/{cloud_path}/{unique_name}" + else: + return f"{user_base_path}/{unique_name}" + BASE_STYLE = ''' :root { --primary: #ff4d6d; @@ -128,6 +178,7 @@ BASE_STYLE = ''' --glass-bg: rgba(255, 255, 255, 0.15); --transition: all 0.3s ease; --delete-color: #ff4444; + --folder-color: #facc15; /* Yellow */ } * { margin: 0; padding: 0; box-sizing: border-box; } body { @@ -135,13 +186,14 @@ body { background: var(--background-light); color: var(--text-light); line-height: 1.6; + padding: 20px; } body.dark { background: var(--background-dark); color: var(--text-dark); } .container { - margin: 20px auto; + margin: 0 auto; max-width: 1200px; padding: 25px; background: var(--card-bg); @@ -199,8 +251,7 @@ input:focus, textarea:focus { box-shadow: var(--shadow); display: inline-block; text-decoration: none; - margin-right: 5px; /* Added margin */ - margin-bottom: 5px; /* Added margin */ + text-align: center; } .btn:hover { transform: scale(1.05); @@ -208,20 +259,28 @@ input:focus, textarea:focus { } .download-btn { background: var(--secondary); + margin-top: 10px; } .download-btn:hover { background: #00b8c5; } .delete-btn { background: var(--delete-color); + margin-top: 10px; } .delete-btn:hover { background: #cc3333; } .flash { - color: var(--secondary); - text-align: center; + padding: 10px; margin-bottom: 15px; + border-radius: 8px; + text-align: center; + background-color: var(--secondary); /* Using secondary for flash messages */ + color: var(--text-light); /* Text color matching background */ +} +body.dark .flash { + color: var(--text-dark); } .file-grid { display: grid; @@ -256,7 +315,7 @@ body.dark .user-item { } @media (max-width: 768px) { .file-grid { - grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + grid-template-columns: repeat(2, 1fr); } } @media (max-width: 480px) { @@ -264,54 +323,68 @@ body.dark .user-item { grid-template-columns: 1fr; } } -.file-item { +.file-item, .folder-item { background: var(--card-bg); padding: 15px; border-radius: 16px; box-shadow: var(--shadow); text-align: center; transition: var(--transition); - word-wrap: break-word; /* Ensure long names wrap */ - overflow: hidden; /* Hide overflow */ + display: flex; + flex-direction: column; + justify-content: space-between; } -body.dark .file-item { +body.dark .file-item, body.dark .folder-item { background: var(--card-bg-dark); } -.file-item:hover { +.file-item:hover, .folder-item:hover { transform: translateY(-5px); } -.file-preview { - width: 100%; /* Ensure preview takes full width */ - height: 180px; /* Fixed height for consistency */ - object-fit: cover; /* Cover the area */ - border-radius: 10px; - margin-bottom: 10px; +.folder-item { cursor: pointer; - background-color: #eee; /* Placeholder color */ + background-color: var(--glass-bg); + border: 2px dashed var(--folder-color); } -.file-preview-icon { /* Style for icons */ - width: 100%; - height: 180px; +body.dark .folder-item { + background-color: rgba(40, 35, 60, 0.5); +} +.folder-item a { + display: block; + text-decoration: none; + color: var(--text-light); + font-weight: 600; + flex-grow: 1; display: flex; + flex-direction: column; justify-content: center; align-items: center; - font-size: 5em; /* Large icon */ - color: var(--secondary); +} +body.dark .folder-item a { + color: var(--text-dark); +} +.folder-icon::before { + content: "📁"; /* Folder emoji */ + font-size: 3em; + margin-bottom: 10px; + color: var(--folder-color); +} +.file-preview { + max-width: 100%; + max-height: 200px; + object-fit: cover; border-radius: 10px; margin-bottom: 10px; - background-color: rgba(0, 221, 235, 0.1); /* Light background */ + loading: lazy; cursor: pointer; } .file-item p { font-size: 0.9em; margin: 5px 0; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; /* Add ellipsis for long names */ + word-break: break-all; /* Prevent long names breaking layout */ } -.file-item .filename { /* Style for filename */ - font-weight: 600; - margin-bottom: 8px; +.file-item .file-name { + font-weight: 600; + margin-bottom: 10px; } .file-item a { color: var(--primary); @@ -320,6 +393,17 @@ body.dark .file-item { .file-item a:hover { color: var(--accent); } +.file-actions { + display: flex; + justify-content: center; + gap: 10px; + flex-wrap: wrap; +} +.file-actions .btn { + padding: 8px 15px; + font-size: 0.9em; + margin-top: 0; +} .modal { display: none; position: fixed; @@ -327,33 +411,23 @@ body.dark .file-item { left: 0; width: 100%; height: 100%; - background: rgba(0, 0, 0, 0.85); + background: rgba(0, 0, 0, 0.9); z-index: 2000; justify-content: center; align-items: center; - padding: 10px; } -.modal-content { +.modal img, .modal video, .modal iframe { max-width: 95%; - max-height: 95%; - display: flex; /* Use flex for centering */ - justify-content: center; - align-items: center; -} - -.modal img, .modal video { - max-width: 100%; - max-height: 100%; + max-height: 95vh; object-fit: contain; - border-radius: 10px; + border-radius: 20px; box-shadow: var(--shadow); + background: white; /* Ensure iframe/text background is visible */ + border: none; /* Remove default iframe border */ } .modal iframe { - width: 90vw; - height: 90vh; - border: none; - border-radius: 10px; - background: white; /* Background for iframe content */ + width: 95%; /* Give iframes a width */ + height: 95vh; /* And height */ } #progress-container { width: 100%; @@ -368,6 +442,42 @@ body.dark .file-item { background: var(--primary); border-radius: 10px; transition: width 0.3s ease; + text-align: center; + line-height: 20px; + color: white; + font-size: 0.8em; +} +.current-path { + margin-bottom: 20px; + font-size: 1.2em; + font-weight: 600; +} +.path-segment { + color: var(--primary); + text-decoration: none; +} +.path-segment:hover { + text-decoration: underline; +} +.path-segment:not(:last-child)::after { + content: " / "; + color: var(--text-light); +} +body.dark .path-segment:not(:last-child)::after { + color: var(--text-dark); +} +.folder-form { + margin-top: 20px; + display: flex; + gap: 10px; +} +.folder-form input[type="text"] { + flex-grow: 1; + margin: 0; +} +.folder-form button { + flex-shrink: 0; + margin: 0; } ''' @@ -386,14 +496,19 @@ def register(): if not username or not password: flash('Имя пользователя и пароль обязательны!') return redirect(url_for('register')) + + if '/' in username or '.' in username: + flash('Имя пользователя не может содержать символы "/" или "."') + return redirect(url_for('register')) data['users'][username] = { 'password': password, 'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), - 'files': [] + 'root': {'_type': 'folder', 'items': {}} # Initialize root folder } save_data(data) session['username'] = username + session['current_path'] = [] # Initialize current path in session flash('Регистрация прошла успешно!') return redirect(url_for('dashboard')) @@ -438,6 +553,7 @@ def login(): if username in data['users'] and data['users'][username]['password'] == password: session['username'] = username + session['current_path'] = [] # Reset path on login return jsonify({'status': 'success', 'redirect': url_for('dashboard')}) else: return jsonify({'status': 'error', 'message': 'Неверное имя пользователя или пароль!'}) @@ -485,9 +601,6 @@ def login(): .then(data => { if (data.status === 'success') { window.location.href = data.redirect; - } else { - // Clear invalid stored credentials - localStorage.removeItem('zeusCredentials'); } }) .catch(error => console.error('Ошибка автовхода:', error)); @@ -505,7 +618,7 @@ def login(): if (data.status === 'success') { const username = formData.get('username'); const password = formData.get('password'); - localStorage.setItem('zeusCredentials', JSON.stringify({ username, password })); + // localStorage.setItem('zeusCredentials', JSON.stringify({ username, password })); // Uncomment to enable persistent login window.location.href = data.redirect; } else { document.getElementById('flash-messages').innerHTML = `
${data.message}
`; @@ -521,8 +634,9 @@ def login(): ''' return render_template_string(html) -@app.route('/dashboard', methods=['GET', 'POST']) -def dashboard(): +@app.route('/dashboard', defaults={'path': ''}, methods=['GET', 'POST']) +@app.route('/dashboard/', methods=['GET', 'POST']) +def dashboard(path): if 'username' not in session: flash('Пожалуйста, войдите в систему!') return redirect(url_for('login')) @@ -531,88 +645,114 @@ def dashboard(): data = load_data() if username not in data['users']: session.pop('username', None) + session.pop('current_path', None) flash('Пользователь не найден!') return redirect(url_for('login')) - if request.method == 'POST': - files = request.files.getlist('files') - if not files or all(not f.filename for f in files): - flash('Файлы не выбраны!') - return redirect(url_for('dashboard')) + # Normalize path: empty string or single / becomes root [] + path_list = [p for p in path.split('/') if p] + session['current_path'] = path_list + + current_folder = get_item_at_path(data, username, path_list) - if len(files) > 20: - flash('Максимум 20 файлов за раз!') - return redirect(url_for('dashboard')) + if not current_folder or current_folder.get('_type') != 'folder': + flash('Папка не найдена!') + session['current_path'] = [] + return redirect(url_for('dashboard')) - if not HF_TOKEN_WRITE: - flash('Загрузка невозможна: отсутствует токен Hugging Face для записи.') - return redirect(url_for('dashboard')) + items_in_folder = list_items_at_path(data, username, path_list) - os.makedirs('uploads', exist_ok=True) - api = HfApi() - uploaded_files_info = [] - upload_errors = [] - - for file in files: - if file and file.filename: - original_filename = file.filename - sanitized_base, extension = os.path.splitext(secure_filename(original_filename)) - unique_id = str(uuid.uuid4())[:8] # Shorter UUID for filename - unique_filename = f"{sanitized_base}_{unique_id}{extension}" - temp_path = os.path.join('uploads', unique_filename) # Save temp with unique name - - try: - file.save(temp_path) - file_path_in_repo = f"cloud_files/{username}/{unique_filename}" - - api.upload_file( - path_or_fileobj=temp_path, - path_in_repo=file_path_in_repo, - repo_id=REPO_ID, - repo_type="dataset", - token=HF_TOKEN_WRITE, - commit_message=f"Uploaded {unique_filename} for {username}" - ) - - file_info = { - 'filename': unique_filename, # Unique name used in storage - 'original_name': original_filename, # Original name for display - 'path': file_path_in_repo, - 'type': get_file_type(original_filename), - 'upload_date': datetime.now().strftime('%Y-%m-%d %H:%M:%S') - } - uploaded_files_info.append(file_info) - - except Exception as e: - logging.error(f"Error uploading file {original_filename} ({unique_filename}): {e}") - upload_errors.append(original_filename) - finally: - if os.path.exists(temp_path): - os.remove(temp_path) - - if uploaded_files_info: - current_data = load_data() # Reload data before modifying - if username in current_data['users']: - if 'files' not in current_data['users'][username]: - current_data['users'][username]['files'] = [] - current_data['users'][username]['files'].extend(uploaded_files_info) - save_data(current_data) - flash(f'{len(uploaded_files_info)} файл(ов) успешно загружено!') - else: - flash('Ошибка: пользователь не найден после загрузки.') # Should not happen if session check passed + if request.method == 'POST': + if 'files' in request.files: + files = request.files.getlist('files') + if files and len(files) > 20: + flash('Максимум 20 файлов за раз!') + return redirect(url_for('dashboard', path='/'.join(path_list))) + + if files: + os.makedirs('uploads', exist_ok=True) + api = HfApi() + uploaded_count = 0 + + for file in files: + if file and file.filename: + original_filename = file.filename + secured_filename = secure_filename(original_filename) + # Generate unique name + unique_filename = f"{uuid.uuid4().hex}_{secured_filename}" + temp_path = os.path.join('uploads', unique_filename) + file.save(temp_path) + + # Construct HF storage path relative to user's cloud_files + hf_file_path = get_hf_storage_path(username, path_list, unique_filename) + + try: + api.upload_file( + path_or_fileobj=temp_path, + path_in_repo=hf_file_path, + repo_id=REPO_ID, + repo_type="dataset", + token=HF_TOKEN_WRITE, + commit_message=f"Uploaded file {secured_filename} for {username} in /{'/'.join(path_list)}" + ) + + mime_type = get_mime_type(secured_filename) + + file_info = { + '_type': 'file', + 'original_name': original_filename, + 'unique_name': unique_filename, + 'path_on_hf': hf_file_path, + 'upload_date': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + 'mime_type': mime_type + } + + # Add file to the database structure + add_item_at_path(data, username, path_list, original_filename, file_info) + uploaded_count += 1 + + except Exception as e: + logging.error(f"Error uploading file {original_filename} to HF: {e}") + flash(f'Ошибка загрузки файла {original_filename}!') + + finally: + if os.path.exists(temp_path): + os.remove(temp_path) + + if uploaded_count > 0: + save_data(data) + flash(f'Успешно загружено {uploaded_count} файл(ов)!') + + elif 'folder_name' in request.form: + folder_name = request.form.get('folder_name') + if not folder_name: + flash('Имя папки не может быть пустым!') + elif '/' in folder_name or '.' in folder_name: + flash('Имя папки не может содержать символы "/" или "."') + else: + # Check if item with same name already exists + if folder_name in current_folder.get('items', {}): + flash(f'Элемент с именем "{folder_name}" уже существует в этой папке!') + else: + folder_data = { + '_type': 'folder', + 'items': {} + } + if add_item_at_path(data, username, path_list, folder_name, folder_data): + save_data(data) + flash(f'Папка "{folder_name}" создана!') + else: + flash('Не удалось создать папку.') - if upload_errors: - flash(f'Ошибка при загрузке файлов: {", ".join(upload_errors)}') - # Use AJAX response if request expects JSON (like from the JS progress handler) - if request.headers.get('X-Requested-With') == 'XMLHttpRequest': - # Even though JS reloads, sending a success helps confirm operation - return jsonify({'status': 'success', 'uploaded': len(uploaded_files_info), 'errors': len(upload_errors)}) - else: - return redirect(url_for('dashboard')) # Standard redirect for non-JS form submit + return redirect(url_for('dashboard', path='/'.join(path_list))) - # GET request part - user_files = sorted(data['users'][username].get('files', []), key=lambda x: x.get('upload_date', ''), reverse=True) + # Build breadcrumbs + breadcrumbs = [{'name': 'Главная', 'path': url_for('dashboard')}] + current_breadcrumb_path = [] + for segment in path_list: + current_breadcrumb_path.append(segment) + breadcrumbs.append({'name': segment, 'path': url_for('dashboard', path='/'.join(current_breadcrumb_path))}) html = ''' @@ -622,13 +762,23 @@ def dashboard(): Панель управления - Zeus Cloud -

Панель управления Zeus Cloud

-

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

+

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

+ +
+ {% for breadcrumb in breadcrumbs %} + {% if not loop.last %} + {{ breadcrumb.name }} + {% else %} + {{ breadcrumb.name }} + {% endif %} + {% endfor %} +
+ {% with messages = get_flashed_messages() %} {% if messages %} {% for message in messages %} @@ -636,120 +786,157 @@ def dashboard(): {% endfor %} {% endif %} {% endwith %} +
- + ''' - return render_template_string(html, users=users) + def admin_get_total_files_recursive(item): + if not item or item.get('_type') != 'folder': return 0 + count = 0 + for key in item.get('items', {}): + sub_item = item['items'][key] + if sub_item.get('_type') == 'file': + count += 1 + elif sub_item.get('_type') == 'folder': + count += admin_get_total_files_recursive(sub_item) + return count + + return render_template_string(html, users=users, admin_get_total_files=admin_get_total_files_recursive) + +@app.route('/admhosto/user/', defaults={'path': ''}) +@app.route('/admhosto/user//') +def admin_user_files(username, path): + if 'is_admin' not in session: + return redirect(url_for('admin_panel_login')) -@app.route('/admhosto/user/') -def admin_user_files(username): - # Add proper admin authentication here if needed data = load_data() - if username not in data.get('users', {}): + if username not in data['users']: flash('Пользователь не найден!') - return redirect(url_for('admin_panel')) + return redirect(url_for('admin_panel_dashboard')) + + path_list = [p for p in path.split('/') if p] + + current_folder = get_item_at_path(data, username, path_list) + + if not current_folder or current_folder.get('_type') != 'folder': + flash('Папка не найдена!') + return redirect(url_for('admin_user_files', username=username)) # Redirect to user's root + + items_in_folder = list_items_at_path(data, username, path_list) + + # Build breadcrumbs for admin view + breadcrumbs = [{'name': 'Админ-панель', 'path': url_for('admin_panel_dashboard')}, + {'name': username, 'path': url_for('admin_user_files', username=username)}] + current_breadcrumb_path = [] + for segment in path_list: + current_breadcrumb_path.append(segment) + breadcrumbs.append({'name': segment, 'path': url_for('admin_user_files', username=username, path='/'.join(current_breadcrumb_path))}) - user_files = sorted(data['users'][username].get('files', []), key=lambda x: x.get('upload_date', ''), reverse=True) html = ''' @@ -1028,15 +1426,24 @@ def admin_user_files(username): - Файлы пользователя {{ username }} - Zeus Cloud + Файлы пользователя {{ username }} (Админ) - Zeus Cloud -
-

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

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

Файлы пользователя: {{ username }} (Админ)

+ +
+ {% for breadcrumb in breadcrumbs %} + {% if not loop.last %} + {{ breadcrumb.name }} + {% else %} + {{ breadcrumb.name }} + {% endif %} + {% endfor %} +
+ {% with messages = get_flashed_messages() %} {% if messages %} {% for message in messages %} @@ -1044,196 +1451,308 @@ def admin_user_files(username): {% endfor %} {% endif %} {% endwith %} +
- {% for file in user_files %} -
-

{{ file.original_name }}

- {% set file_url = "https://huggingface.co/datasets/" + repo_id + "/resolve/main/" + file.path + "?token=" + hf_token_read if hf_token_read else "https://huggingface.co/datasets/" + repo_id + "/resolve/main/" + file.path %} - {% if file.type == 'video' %} - - {% elif file.type == 'image' %} - {{ file.original_name }} - {% elif file.type == 'pdf' %} -
- {% elif file.type == 'text' %} -
- {% else %} -
- {% endif %} -

{{ file.get('upload_date', 'N/A') }}

- Скачать -
- -
-
+ {% for name, item in items_in_folder %} + {% if item._type == 'folder' %} +
+
+

{{ name }}

+
+
+ +
+
+
+ {% elif item._type == 'file' %} +
+

{{ item.original_name }}

+ {% if item.mime_type and item.mime_type.startswith('image/') %} + {{ item.original_name }} + {% elif item.mime_type and item.mime_type.startswith('video/') %} + + {% elif item.mime_type == 'application/pdf' %} +

📄

+ + {% elif item.mime_type == 'text/plain' %} +

📃

+ + {% else %} +

📦

+ {% endif %} + +

Загружен: {{ item.get('upload_date', 'N/A') }}

+
+ + Скачать +
+ +
+
+
+ {% endif %} {% endfor %} - {% if not user_files %} -

У этого пользователя пока нет файлов.

+ {% if not items_in_folder %} +

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

{% endif %}
- + Назад к списку пользователей
-