diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,265 +1,47 @@ -import flask -from flask import Flask, render_template_string, request, redirect, url_for, session, flash, send_file, jsonify, Response -from flask_caching import Cache +import asyncio +import hashlib +import hmac import json -import os import logging +import os import threading import time +import uuid from datetime import datetime -from huggingface_hub import HfApi, hf_hub_download, utils as hf_utils -from werkzeug.utils import secure_filename -import requests from io import BytesIO -import uuid -import hmac -import hashlib -from urllib.parse import unquote +from urllib.parse import parse_qsl, unquote -app = Flask(__name__) -app.secret_key = os.getenv("FLASK_SECRET_KEY", "supersecretkey_folders_unique_telegram") - -# --- Telegram Configuration --- -BOT_API_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "6750208873:AAE2hvPlJ99dBdhGa_Brre0IIpUdOvXxHt4") -ADMIN_TELEGRAM_IDS = [int(admin_id) for admin_id in os.getenv("ADMIN_TELEGRAM_IDS", "123456789,987654321").split(',') if admin_id.strip()] # Example: "123456,789012" -# For local testing, you can mock a Telegram user. -# Example: http://localhost:7860/auth/telegram?mock_user_id=YOUR_TELEGRAM_ID (replace YOUR_TELEGRAM_ID) -MOCK_TELEGRAM_MODE = os.getenv("MOCK_TELEGRAM_MODE", "False").lower() == "true" +import requests +from flask import (Flask, Response, 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, utils as hf_utils +from telegram import (InlineKeyboardButton, InlineKeyboardMarkup, Update, + WebAppInfo) +from telegram.ext import (Application, CommandHandler, ContextTypes, + MessageHandler, filters) +from werkzeug.utils import secure_filename +# --- Configuration --- +BOT_TOKEN = "6750208873:AAE2hvPlJ99dBdhGa_Brre0IIpUdOvXxHt4" # Provided Bot API token +ADMIN_TELEGRAM_ID = os.getenv("ADMIN_TELEGRAM_ID", "YOUR_ADMIN_TELEGRAM_ID_REPLACE_ME") # Replace with actual Admin Telegram ID +WEBAPP_URL = os.getenv("WEBAPP_URL", "https://example.com/launch_webapp") # Replace with your deployed WebApp URL (must be HTTPS for production) -DATA_FILE = 'cloudeng_data_tg.json' -REPO_ID = "Eluza133/Z1e1u" # Replace with your actual Repo ID +app = Flask(__name__) +app.secret_key = os.getenv("FLASK_SECRET_KEY", "supersecretkey_folders_unique_telegram_edition") +DATA_FILE = 'cloudeng_data_tg.json' # Using a new data file for TG version +REPO_ID = "Eluza133/Z1e1u" HF_TOKEN_WRITE = os.getenv("HF_TOKEN") HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") or HF_TOKEN_WRITE -UPLOAD_FOLDER = 'uploads' +UPLOAD_FOLDER = 'uploads_tg' os.makedirs(UPLOAD_FOLDER, exist_ok=True) cache = Cache(app, config={'CACHE_TYPE': 'simple'}) -logging.basicConfig(level=logging.INFO) - - -def check_telegram_authorization(init_data_str: str, bot_token: str) -> dict | None: - """ - Validates the initData string from Telegram Web App. - Returns user data dictionary if valid, None otherwise. - """ - try: - # Telegram.WebApp.initData is URL-encoded, ensure it's decoded if necessary. - # Flask's request.get_json() or request.form should handle decoding if initData is part of JSON/form. - # If initData_str is directly from JS, it might already be decoded. - # For safety, let's try to unquote it if it looks like a query string. - if '%' in init_data_str: # Basic check for URL encoding - init_data_str = unquote(init_data_str) - - params = {} - for item in init_data_str.split('&'): - key, value = item.split('=', 1) - params[key] = value - - hash_to_check = params.pop('hash', None) - if not hash_to_check: - logging.warning("No hash found in initData") - return None - - data_check_arr = [f"{k}={v}" for k, v in sorted(params.items())] - data_check_string = "\n".join(data_check_arr) - - secret_key = hmac.new("WebAppData".encode(), bot_token.encode(), hashlib.sha256).digest() - calculated_hash = hmac.new(secret_key, data_check_string.encode(), hashlib.sha256).hexdigest() - - if calculated_hash == hash_to_check: - user_data = params.get("user") - if user_data: - return json.loads(user_data) # user_data is a JSON string - return {} # Should contain user data if auth is valid - else: - logging.warning(f"Telegram auth hash mismatch. Calculated: {calculated_hash}, Received: {hash_to_check}") - return None - except Exception as e: - logging.error(f"Error in check_telegram_authorization: {e}", exc_info=True) - return None - - -def find_node_by_id(filesystem, node_id): - if not filesystem: return None, None - if filesystem.get('id') == node_id: - return filesystem, None - - queue = [(filesystem, None)] - while queue: - current_node, parent = queue.pop(0) - if current_node.get('type') == 'folder' and 'children' in current_node: - for i, child in enumerate(current_node['children']): - if child.get('id') == node_id: - return child, current_node - if child.get('type') == 'folder': - queue.append((child, current_node)) - return None, None - -def add_node(filesystem, parent_id, node_data): - parent_node, _ = find_node_by_id(filesystem, parent_id) - if parent_node and parent_node.get('type') == 'folder': - if 'children' not in parent_node: - parent_node['children'] = [] - parent_node['children'].append(node_data) - return True - return False - -def remove_node(filesystem, node_id): - node_to_remove, parent_node = find_node_by_id(filesystem, node_id) - if node_to_remove and parent_node and 'children' in parent_node: - parent_node['children'] = [child for child in parent_node['children'] if child.get('id') != node_id] - return True - # Special case: removing root's child directly if filesystem is root and parent_node is None - if node_to_remove and filesystem.get('id') == 'root' and parent_node is None: - if 'children' in filesystem: - filesystem['children'] = [child for child in filesystem['children'] if child.get('id') != node_id] - return True - return False - - -def get_node_path_string(filesystem, node_id): - path_list = [] - current_id = node_id - - while current_id: - node, parent = find_node_by_id(filesystem, current_id) - if not node: - break - if node.get('id') != 'root': - path_list.append(node.get('name', node.get('original_filename', ''))) - if not parent: - break - current_id = parent.get('id') if parent else None - return " / ".join(reversed(path_list)) or "Root" - - -def initialize_user_filesystem(user_data_storage_entry): # Expects the value part of data['users'][telegram_id_str] - if 'filesystem' not in user_data_storage_entry: - user_data_storage_entry['filesystem'] = { - "type": "folder", - "id": "root", - "name": "root", # Could be user's TG name, but "root" is fine - "children": [] - } - # Migration logic for old 'files' list, if any, could go here - # For simplicity, new TG users start with an empty root folder. - - -@cache.memoize(timeout=300) -def load_data(): - try: - if os.path.exists(DATA_FILE): # Only download if it exists on HF or local is missing - download_db_from_hf() - - if not os.path.exists(DATA_FILE): # If still not exists, create empty - with open(DATA_FILE, 'w', encoding='utf-8') as f: - json.dump({'users': {}}, f) - logging.info(f"Created empty local database file: {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 in dict format, initializing empty database") - return {'users': {}} - data.setdefault('users', {}) - # User data is now keyed by str(telegram_user_id) - for tg_user_id_str, user_storage_entry in data['users'].items(): - initialize_user_filesystem(user_storage_entry) # Pass the dict value directly - logging.info("Data successfully loaded and initialized for Telegram users") - return data - except Exception as e: - logging.error(f"Error loading data: {e}") - return {'users': {}} - -def save_data(data): - try: - with open(DATA_FILE, 'w', encoding='utf-8') as file: - json.dump(data, file, ensure_ascii=False, indent=4) - upload_db_to_hf() - cache.clear() - logging.info("Data saved and uploaded to HF") - except Exception as e: - logging.error(f"Error saving data: {e}") - raise - -def upload_db_to_hf(): - if not HF_TOKEN_WRITE: - logging.warning("HF_TOKEN_WRITE not set, skipping database upload.") - return - try: - api = HfApi() - api.upload_file( - path_or_fileobj=DATA_FILE, - path_in_repo=DATA_FILE, - repo_id=REPO_ID, - repo_type="dataset", - token=HF_TOKEN_WRITE, - commit_message=f"Backup {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" - ) - logging.info("Database uploaded to Hugging Face") - except Exception as e: - logging.error(f"Error uploading database: {e}") - -def download_db_from_hf(): - if not HF_TOKEN_READ: - logging.warning("HF_TOKEN_READ not set, skipping database download.") - if not os.path.exists(DATA_FILE): - with open(DATA_FILE, 'w', encoding='utf-8') as f: - json.dump({'users': {}}, f) - 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 # Ensure we get the latest if available - ) - logging.info("Database downloaded from Hugging Face") - except hf_utils.RepositoryNotFoundError: - logging.warning(f"Repository {REPO_ID} not found.") - if not os.path.exists(DATA_FILE): - with open(DATA_FILE, 'w', encoding='utf-8') as f: - json.dump({'users': {}}, f) - except hf_utils.EntryNotFoundError: - logging.warning(f"{DATA_FILE} not found in repository {REPO_ID}. Initializing empty database if local also missing.") - if not os.path.exists(DATA_FILE): - with open(DATA_FILE, 'w', encoding='utf-8') as f: - json.dump({'users': {}}, f) - except Exception as e: - logging.error(f"Error downloading database: {e}") - if not os.path.exists(DATA_FILE): - with open(DATA_FILE, 'w', encoding='utf-8') as f: - json.dump({'users': {}}, f) - -def periodic_backup(): - while True: - time.sleep(1800) # Backup every 30 minutes - logging.info("Attempting periodic backup...") - data = load_data() # Load current data before saving (or just save current state if save_data handles loading) - # save_data implicitly uploads, so this might be redundant if data is saved frequently - # The main purpose of save_data(data) is to ensure data is written to disk then uploaded. - # A direct call to upload_db_to_hf() might be better if we only want to backup the existing disk file. - # However, frequent load/save operations are handled by endpoints. This is more of a fallback. - upload_db_to_hf() - - -def get_file_type(filename): - filename_lower = filename.lower() - if filename_lower.endswith(('.mp4', '.mov', '.avi', '.webm', '.mkv')): - return 'video' - elif filename_lower.endswith(('.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg')): - return 'image' - elif filename_lower.endswith('.pdf'): - return 'pdf' - elif filename_lower.endswith('.txt'): - return 'text' - return 'other' +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +# --- Styles --- BASE_STYLE = ''' :root { --primary: #ff4d6d; --secondary: #00ddeb; --accent: #8b5cf6; @@ -270,400 +52,188 @@ BASE_STYLE = ''' --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; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } -body.dark { background: var(--background-dark); color: var(--text-dark); } -.container { margin: 10px auto; max-width: 100%; padding: 15px; background: var(--card-bg); border-radius: 20px; box-shadow: var(--shadow); overflow-x: hidden; } +body { font-family: 'Inter', sans-serif; background: var(--background-light); color: var(--text-light); line-height: 1.6; } +body.dark { background: var(--background-dark); color: var(--text-dark); } /* Basic dark mode support */ +.container { margin: 20px auto; max-width: 1200px; padding: 25px; background: var(--card-bg); border-radius: 20px; box-shadow: var(--shadow); overflow-x: hidden; } body.dark .container { background: var(--card-bg-dark); } -h1 { font-size: 1.8em; font-weight: 800; text-align: center; margin-bottom: 20px; background: linear-gradient(135deg, var(--primary), var(--accent)); -webkit-background-clip: text; color: transparent; } -h2 { font-size: 1.3em; margin-top: 25px; color: var(--text-light); } +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; color: var(--text-light); } body.dark h2 { color: var(--text-dark); } -h4 { font-size: 1em; margin-top: 12px; margin-bottom: 4px; color: var(--accent); } -ol, ul { margin-left: 18px; margin-bottom: 12px; } -li { margin-bottom: 4px; } -input, textarea { width: 100%; padding: 12px; margin: 10px 0; border: none; border-radius: 12px; background: var(--glass-bg); color: var(--text-light); font-size: 1em; box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.1); } -body.dark input, body.dark textarea { color: var(--text-dark); background: rgba(255,255,255,0.05); } -input:focus, textarea:focus { outline: none; box-shadow: 0 0 0 3px var(--primary); } -.btn { padding: 12px 24px; background: var(--primary); color: white; border: none; border-radius: 12px; cursor: pointer; font-size: 1em; font-weight: 600; transition: var(--transition); box-shadow: var(--shadow); display: inline-block; text-decoration: none; margin-top: 4px; margin-right: 4px; } -.btn:hover { transform: scale(1.03); background: #e6415f; } +h4 { font-size: 1.1em; margin-top: 15px; margin-bottom: 5px; color: var(--accent); } +ol, ul { margin-left: 20px; margin-bottom: 15px; } +li { margin-bottom: 5px; } +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); background: rgba(0,0,0,0.2); } +input:focus, textarea: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; margin-top: 5px; margin-right: 5px; } +.btn:hover { transform: scale(1.05); background: #e6415f; } .download-btn { background: var(--secondary); } .download-btn:hover { background: #00b8c5; } .delete-btn { background: var(--delete-color); } .delete-btn:hover { background: #cc3333; } .folder-btn { background: var(--folder-color); } .folder-btn:hover { background: #e6a000; } -.flash { color: var(--secondary); text-align: center; margin-bottom: 12px; padding: 8px; background: rgba(0, 221, 235, 0.1); border-radius: 8px; } +.flash { color: var(--secondary); text-align: center; margin-bottom: 15px; padding: 10px; background: rgba(0, 221, 235, 0.1); border-radius: 10px; } .flash.error { color: var(--delete-color); background: rgba(255, 68, 68, 0.1); } -.file-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 15px; margin-top: 15px; } -.user-list { margin-top: 15px; } -.user-item { padding: 12px; background: var(--card-bg); border-radius: 14px; margin-bottom: 8px; box-shadow: var(--shadow); transition: var(--transition); } +.file-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(-4px); } +.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); } -.item { background: var(--card-bg); padding: 12px; border-radius: 14px; box-shadow: var(--shadow); text-align: center; transition: var(--transition); display: flex; flex-direction: column; justify-content: space-between; } +.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(-4px); } -.item-preview { max-width: 100%; height: 100px; object-fit: cover; border-radius: 8px; margin-bottom: 8px; cursor: pointer; display: block; margin-left: auto; margin-right: auto;} -.item.folder .item-preview { object-fit: contain; font-size: 50px; color: var(--folder-color); line-height: 100px; } -.item p { font-size: 0.85em; margin: 4px 0; word-break: break-all; } +.item:hover { transform: translateY(-5px); } +.item-preview { max-width: 100%; height: 130px; object-fit: cover; border-radius: 10px; margin-bottom: 10px; cursor: pointer; display: block; margin-left: auto; margin-right: auto;} +.item.folder .item-preview { object-fit: contain; font-size: 60px; color: var(--folder-color); line-height: 130px; } +.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); } -.item-actions { margin-top: 8px; display: flex; flex-wrap: wrap; gap: 4px; justify-content: center; } -.item-actions .btn { font-size: 0.8em; padding: 4px 8px; } +.item-actions { margin-top: 10px; display: flex; flex-wrap: wrap; gap: 5px; justify-content: center; } +.item-actions .btn { font-size: 0.9em; padding: 5px 10px; } .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; } -.modal-content { max-width: 95vw; max-height: 95vh; background: #fff; padding: 8px; border-radius: 12px; overflow: auto; position: relative; } +.modal-content { max-width: 95%; max-height: 95%; background: #fff; padding: 10px; border-radius: 15px; overflow: auto; position: relative; } body.dark .modal-content { background: var(--card-bg-dark); } -.modal img, .modal video, .modal iframe, .modal pre { max-width: 100%; max-height: 80vh; display: block; margin: auto; border-radius: 8px; } -.modal iframe { width: 90vw; height: 80vh; border: none; } -.modal pre { background: #eee; color: #333; padding: 12px; border-radius: 6px; white-space: pre-wrap; word-wrap: break-word; text-align: left; max-height: 80vh; overflow-y: auto;} +.modal img, .modal video, .modal iframe, .modal pre { max-width: 100%; max-height: 85vh; display: block; margin: auto; border-radius: 10px; } +.modal iframe { width: 80vw; height: 85vh; border: none; } +.modal pre { background: #eee; color: #333; padding: 15px; border-radius: 8px; white-space: pre-wrap; word-wrap: break-word; text-align: left; max-height: 85vh; overflow-y: auto;} body.dark .modal pre { background: #2b2a33; color: var(--text-dark); } -.modal-close-btn { position: absolute; top: 10px; right: 15px; font-size: 24px; color: #aaa; cursor: pointer; background: rgba(0,0,0,0.5); border-radius: 50%; width: 24px; height: 24px; line-height: 24px; text-align: center; } +.modal-close-btn { position: absolute; top: 15px; right: 25px; font-size: 30px; color: #aaa; cursor: pointer; background: rgba(0,0,0,0.5); border-radius: 50%; width: 30px; height: 30px; line-height: 30px; text-align: center; } body.dark .modal-close-btn { color: #555; background: rgba(255,255,255,0.2); } -#progress-container { width: 100%; background: var(--glass-bg); border-radius: 8px; margin: 12px 0; display: none; position: relative; height: 18px; } -#progress-bar { width: 0%; height: 100%; background: var(--primary); border-radius: 8px; transition: width 0.3s ease; } -#progress-text { position: absolute; width: 100%; text-align: center; line-height: 18px; color: white; font-size: 0.8em; font-weight: bold; text-shadow: 1px 1px 1px rgba(0,0,0,0.5); } -.breadcrumbs { margin-bottom: 15px; font-size: 1em; } +#progress-container { width: 100%; background: var(--glass-bg); border-radius: 10px; margin: 15px 0; display: none; position: relative; height: 20px; } +#progress-bar { width: 0%; height: 100%; background: var(--primary); border-radius: 10px; transition: width 0.3s ease; } +#progress-text { position: absolute; width: 100%; text-align: center; line-height: 20px; color: white; font-size: 0.9em; font-weight: bold; text-shadow: 1px 1px 1px rgba(0,0,0,0.5); } +.breadcrumbs { margin-bottom: 20px; font-size: 1.1em; } .breadcrumbs a { color: var(--accent); text-decoration: none; } .breadcrumbs a:hover { text-decoration: underline; } -.breadcrumbs span { margin: 0 4px; color: #aaa; } -.folder-actions { margin-top: 15px; margin-bottom: 8px; display: flex; gap: 8px; align-items: center; flex-wrap: wrap; } -.folder-actions input[type=text] { width: auto; flex-grow: 1; margin: 0; min-width: 120px; } +.breadcrumbs span { margin: 0 5px; color: #aaa; } +.folder-actions { margin-top: 20px; margin-bottom: 10px; display: flex; gap: 10px; align-items: center; flex-wrap: wrap; } +.folder-actions input[type=text] { width: auto; flex-grow: 1; margin: 0; min-width: 150px; } .folder-actions .btn { margin: 0; flex-shrink: 0;} -#tg-user-info { margin-bottom: 15px; padding: 10px; background: var(--glass-bg); border-radius: 8px; text-align: center; font-size: 0.9em;} -body.dark #tg-user-info { background: rgba(255,255,255,0.05); } +@media (max-width: 768px) { + .file-grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); } + .folder-actions { flex-direction: column; align-items: stretch; } + .folder-actions input[type=text] { width: 100%; } + .item-preview { height: 100px; } + .item.folder .item-preview { font-size: 50px; line-height: 100px; } + h1 { font-size: 1.8em; } + .btn { padding: 12px 24px; font-size: 1em; } + .item-actions .btn { padding: 4px 8px; font-size: 0.8em;} +} @media (max-width: 480px) { - .container { padding: 10px; margin: 5px auto; } - .file-grid { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 10px; } + .container { padding: 15px; } + .file-grid { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 15px; } .item-preview { height: 80px; } .item.folder .item-preview { font-size: 40px; line-height: 80px; } .item p { font-size: 0.8em;} - .breadcrumbs { font-size: 0.9em; } - .btn { padding: 10px 20px; font-size: 0.9em; } - .item-actions .btn { padding: 3px 6px; font-size: 0.7em;} - h1 { font-size: 1.6em; } + .breadcrumbs { font-size: 1em; } + .btn { padding: 10px 20px; } } +/* Telegram WebApp specific styles */ +body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-rendering: optimizeLegibility; } +/* Adjust for Telegram theme */ +body.telegram-dark-theme { --background-light: #1a1625; --text-light: #e8e1ff; --card-bg: rgba(40, 35, 60, 0.95); /* ... and other dark theme vars */ } +body.telegram-light-theme { /* default vars are mostly light */ } ''' -@app.route('/') -def index_page(): - # This page will initiate Telegram authentication - # It will not require prior Flask session auth. - # After TG auth, it redirects to dashboard. - html = ''' -
-Аутентификация через Telegram...
Загрузка приложения Zeus Cloud...
+Пользователь: {{ display_name }}
{% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} - {% for category, message in messages %} -{{ item.name }}
{{ item.upload_date }}
Эта папка пуста.
{% endif %}Зарегистрирован: {{ user.created_at }}
+Файлов: {{ user.file_count }}
+ +Пользователей нет.
{% endfor %}{{ file.original_filename | truncate(30) }}
+В папке: {{ file.parent_path_str }}
+Загружен: {{ file.upload_date }}
+ID: {{ file.id }}
+Path: {{ file.path }}
+У пользователя нет файлов.
{% endfor %} +