diff --git "a/app.py" "b/app.py"
--- "a/app.py"
+++ "b/app.py"
@@ -1,12 +1,13 @@
+# --- START OF FILE app.py ---
import os
import hmac
import hashlib
import json
-from urllib.parse import unquote, parse_qsl, quote
-from flask import Flask, request, jsonify, Response, send_file
+from urllib.parse import unquote, parse_qsl
+from flask import Flask, request, jsonify, Response, session, send_file
+import time
import logging
import threading
-import time
from datetime import datetime
from huggingface_hub import HfApi, hf_hub_download, utils as hf_utils
from werkzeug.utils import secure_filename
@@ -15,84 +16,91 @@ from io import BytesIO
import uuid
# --- Configuration ---
-# VITAL: Set these environment variables or replace placeholders
-BOT_TOKEN = os.environ.get('TELEGRAM_BOT_TOKEN', 'YOUR_BOT_TOKEN') # Your Telegram Bot Token for validation
-HF_TOKEN_WRITE = os.environ.get('HF_TOKEN') # Your Hugging Face WRITE token
-HF_TOKEN_READ = os.environ.get('HF_TOKEN_READ') or HF_TOKEN_WRITE # Your Hugging Face READ token (falls back to WRITE)
-REPO_ID = os.environ.get('HF_REP' , 'Eluza133/Z1e1u') # e.g., "Eluza133/Z1e1u"
-FLASK_SECRET_KEY = os.getenv("FLASK_SECRET_KEY", "a_very_secret_key_for_flask") # For potential future session use, though limited here
-
-HOST = '0.0.0.0'
-PORT = 7860
-DATA_FILE = 'cloudeng_mini_app_data.json' # Changed filename to avoid conflicts
-UPLOAD_FOLDER = 'mini_app_uploads'
+BOT_TOKEN = os.environ.get('TELEGRAM_BOT_TOKEN', 'YOUR_BOT_TOKEN') # ВАЖНО: Замените или используйте env!
+HF_TOKEN_WRITE = os.environ.get("HF_TOKEN")
+HF_TOKEN_READ = os.environ.get("HF_TOKEN_READ") or HF_TOKEN_WRITE
+FLASK_SECRET_KEY = os.environ.get("FLASK_SECRET_KEY", "supersecretkey_mini_app_unique")
+REPO_ID = "Eluza133/Z1e1u" # Ваш репозиторий HF
+DATA_FILE = 'cloudeng_telegram_data.json' # Файл данных для TG версии
+UPLOAD_FOLDER = 'uploads_tg'
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
+AUTH_DATA_LIFETIME = 3600 # Время жизни initData (1 час)
# --- Flask App Initialization ---
app = Flask(__name__)
app.secret_key = FLASK_SECRET_KEY
-logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
-
-# --- Telegram Validation Logic ---
-AUTH_DATA_LIFETIME = 3600 # 1 hour validity for initData
-
-def check_telegram_authorization(auth_data: str, bot_token: str) -> dict | None:
- if not auth_data: return None
- try:
- parsed_data = dict(parse_qsl(unquote(auth_data)))
- if "hash" not in parsed_data: return None
- telegram_hash = parsed_data.pop('hash')
- auth_date_ts = int(parsed_data.get('auth_date', 0))
- if time.time() - auth_date_ts > AUTH_DATA_LIFETIME: return None # Expired
- data_check_string = "\n".join(sorted([f"{k}={v}" for k, v in parsed_data.items()]))
- 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 == telegram_hash:
- user_data = parsed_data.get('user')
- if user_data: return json.loads(user_data)
- return {} # Valid but no user data? Return empty dict
- return None # Hash mismatch
- except Exception as e:
- logging.error(f"Telegram validation error: {e}")
- return None
+logging.basicConfig(level=logging.INFO)
-# --- Filesystem Logic ---
+# --- Helper Functions ---
def find_node_by_id(filesystem, node_id):
- if not filesystem: return None, None
- if filesystem.get('id') == node_id: return filesystem, None
+ if not filesystem or not isinstance(filesystem, dict):
+ 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:
+ if current_node.get('type') == 'folder' and 'children' in current_node and isinstance(current_node['children'], list):
for child in current_node['children']:
- if child.get('id') == node_id: return child, current_node
- if child.get('type') == 'folder': queue.append((child, current_node))
+ if isinstance(child, dict): # Добавлена проверка типа
+ 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'] = []
+ if 'children' not in parent_node or not isinstance(parent_node['children'], list):
+ 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]
+ if node_to_remove and parent_node and 'children' in parent_node and isinstance(parent_node['children'], list):
+ parent_node['children'] = [child for child in parent_node['children'] if not isinstance(child, dict) or child.get('id') != node_id]
return True
- elif node_to_remove and not parent_node: # Trying to remove root? Disallow.
- return False
- return False # Node not found or parent invalid
-
-def initialize_user_filesystem(user_id_str):
- return {
- "type": "folder",
- "id": "root",
- "name": "root",
- "children": []
- }
+ return False
+
+def get_node_path_string(filesystem, node_id):
+ path_list = []
+ current_id = node_id
+ visited = set() # Защита от циклов
+
+ while current_id and current_id not in visited:
+ visited.add(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):
+ if 'filesystem' not in user_data or not isinstance(user_data.get('filesystem'), dict):
+ user_data['filesystem'] = {
+ "type": "folder",
+ "id": "root",
+ "name": "root",
+ "children": []
+ }
+
+def get_file_type(filename):
+ filename_lower = filename.lower()
+ if filename_lower.endswith(('.mp4', '.mov', '.avi', '.webm', '.mkv')): return 'video'
+ if filename_lower.endswith(('.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg')): return 'image'
+ if filename_lower.endswith('.pdf'): return 'pdf'
+ if filename_lower.endswith('.txt'): return 'text'
+ if filename_lower.endswith(('.doc', '.docx')): return 'doc'
+ if filename_lower.endswith(('.xls', '.xlsx')): return 'xls'
+ if filename_lower.endswith(('.ppt', '.pptx')): return 'ppt'
+ if filename_lower.endswith(('.zip', '.rar', '.7z', '.tar', '.gz')): return 'archive'
+ if filename_lower.endswith(('.mp3', '.wav', '.ogg', '.aac', '.flac')): return 'audio'
+ return 'other'
# --- Data Persistence ---
data_lock = threading.Lock()
@@ -101,229 +109,366 @@ def load_data():
with data_lock:
try:
download_db_from_hf()
- if not os.path.exists(DATA_FILE):
- logging.warning(f"{DATA_FILE} not found locally after potential download attempt. Initializing empty.")
- return {'users': {}}
- with open(DATA_FILE, 'r', encoding='utf-8') as file:
- data = json.load(file)
- if not isinstance(data, dict): return {'users': {}}
- data.setdefault('users', {})
- # No filesystem initialization here, do it on first access if needed
- return data
+ if os.path.exists(DATA_FILE) and os.path.getsize(DATA_FILE) > 0:
+ with open(DATA_FILE, 'r', encoding='utf-8') as file:
+ data = json.load(file)
+ if not isinstance(data, dict):
+ logging.warning(f"{DATA_FILE} is not a dict, initializing.")
+ data = {'users': {}}
+ else:
+ data = {'users': {}}
+
+ data.setdefault('users', {})
+ # Ensure all users have initialized filesystem
+ for user_id, user_data in data['users'].items():
+ if isinstance(user_data, dict): # Check if user_data is a dict
+ initialize_user_filesystem(user_data)
+ else:
+ logging.warning(f"Invalid data format for user {user_id}, re-initializing.")
+ data['users'][user_id] = {} # Initialize as empty dict or default structure
+ initialize_user_filesystem(data['users'][user_id])
+
+
+ logging.info("Data loaded/initialized")
+ return data
+ except json.JSONDecodeError:
+ logging.error(f"Error decoding JSON from {DATA_FILE}. Returning empty data.")
+ return {'users': {}}
except Exception as e:
logging.error(f"Error loading data: {e}")
- return {'users': {}} # Return default structure on error
+ return {'users': {}}
def save_data(data):
- with data_lock:
+ with data_lock:
try:
- # Ensure user data integrity before saving
+ # Ensure filesystem structure is valid before saving
for user_id, user_data in data.get('users', {}).items():
- if 'filesystem' not in user_data or not isinstance(user_data['filesystem'], dict):
- logging.warning(f"Filesystem missing or invalid for user {user_id}. Reinitializing.")
- user_data['filesystem'] = initialize_user_filesystem(user_id)
- if 'user_info' not in user_data or not isinstance(user_data['user_info'], dict):
- logging.warning(f"User info missing for user {user_id}.")
- # Optionally add placeholder if needed: user_data['user_info'] = {'id': user_id}
+ if isinstance(user_data, dict):
+ initialize_user_filesystem(user_data) # Ensures filesystem exists and is a dict
with open(DATA_FILE, 'w', encoding='utf-8') as file:
json.dump(data, file, ensure_ascii=False, indent=4)
upload_db_to_hf()
- logging.info("Data saved and upload initiated.")
+ logging.info("Data saved and uploaded to HF")
except Exception as e:
logging.error(f"Error saving data: {e}")
- # Optionally raise e to signal failure upstream
+ # Optionally re-raise or handle appropriately
+ # raise
-# --- Hugging Face Integration ---
def upload_db_to_hf():
- if not HF_TOKEN_WRITE or not REPO_ID:
- logging.warning("HF_TOKEN_WRITE or REPO_ID not set, skipping database upload.")
+ if not HF_TOKEN_WRITE:
+ logging.warning("HF_TOKEN_WRITE not set, skipping database upload.")
+ return
+ if not os.path.exists(DATA_FILE):
+ logging.warning(f"{DATA_FILE} not found, skipping 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')}"
+ 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 TG App {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
)
- logging.info(f"Database {DATA_FILE} uploaded to HF dataset {REPO_ID}")
+ logging.info("Database uploaded to Hugging Face")
except Exception as e:
- logging.error(f"Error uploading database to HF: {e}")
+ logging.error(f"Error uploading database: {e}")
def download_db_from_hf():
- if not HF_TOKEN_READ or not REPO_ID:
- logging.warning("HF_TOKEN_READ or REPO_ID not set, skipping database download.")
+ 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, resume_download=True
+ local_dir=".", local_dir_use_symlinks=False, etag_timeout=60 # Increased timeout
)
- logging.info(f"Database {DATA_FILE} downloaded from HF dataset {REPO_ID}")
- return True
+ logging.info("Database downloaded from Hugging Face")
except hf_utils.EntryNotFoundError:
- logging.warning(f"{DATA_FILE} not found in HF repo {REPO_ID}. Will use/create local.")
- return False
+ logging.warning(f"{DATA_FILE} not found in repo. Initializing empty DB locally.")
+ 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 from HF: {e}")
- return False
+ 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("Initiating periodic data backup.")
- all_data = load_data() # Load current state
- save_data(all_data) # Save and upload
+ logging.info("Starting periodic backup...")
+ data = load_data() # Load current data before saving
+ save_data(data)
-def get_file_type(filename):
- filename_lower = filename.lower()
- if filename_lower.endswith(('.mp4', '.mov', '.avi', '.webm', '.mkv')): return 'video'
- if filename_lower.endswith(('.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg')): return 'image'
- if filename_lower.endswith('.pdf'): return 'pdf'
- if filename_lower.endswith(('.txt', '.log', '.md', '.py', '.js', '.css', '.html', '.json', '.xml')): return 'text'
- if filename_lower.endswith(('.mp3', '.wav', '.ogg', '.aac', '.flac')): return 'audio'
- return 'other'
+# --- Telegram Validation ---
+def check_telegram_authorization(auth_data: str, bot_token: str) -> dict | None:
+ if not auth_data: return None
+ try:
+ parsed_data = dict(parse_qsl(unquote(auth_data)))
+ if "hash" not in parsed_data: return None
+
+ telegram_hash = parsed_data.pop('hash')
+ auth_date_ts = int(parsed_data.get('auth_date', 0))
+ current_ts = int(time.time())
+ if current_ts - auth_date_ts > AUTH_DATA_LIFETIME:
+ logging.warning(f"Auth data expired: {current_ts - auth_date_ts} seconds old.")
+ return None
-# --- HTML, CSS, JS Template ---
+ data_check_string = "\n".join(sorted([f"{k}={v}" for k, v in parsed_data.items()]))
+ 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 == telegram_hash:
+ user_data_str = parsed_data.get('user')
+ if user_data_str:
+ try:
+ return json.loads(user_data_str)
+ except json.JSONDecodeError:
+ logging.error("Failed to decode user JSON from initData")
+ return None
+ return {} # Valid hash, but no user field? Return empty dict
+ else:
+ logging.warning("Hash mismatch during validation.")
+ return None
+ except Exception as e:
+ logging.error(f"Error during Telegram validation: {e}")
+ return None
+
+# --- HTML Template ---
HTML_TEMPLATE = """
- Zeus Cloud Mini
+ Zeus Cloud
+
-
-
Загрузка...
-
-
-
Папка пуста
+
-
-
-
-
Загрузка...
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -335,530 +480,520 @@ HTML_TEMPLATE = """
@@ -870,440 +1005,446 @@ HTML_TEMPLATE = """
@app.route('/')
def index():
- if BOT_TOKEN == 'YOUR_BOT_TOKEN' or REPO_ID.startswith("YOUR_"):
- # Render a basic error page if essential config is missing
- error_html = """
Config Error
-
-
Ошибка Конфигурации Сервера
-
Необходимо установить переменные окружения TELEGRAM_BOT_TOKEN и HF_REPO_ID для работы приложения.
-
Обратитесь к администратору.
"""
- return Response(error_html, status=500, mimetype='text/html')
- return Response(HTML_TEMPLATE.replace("{{ REPO_ID }}", REPO_ID), mimetype='text/html')
-
-@app.route('/validate', methods=['POST'])
+ """Serves the main Mini App HTML."""
+ # Replace placeholder in template - avoids exposing token directly in JS if inspected early
+ # Although, initData mechanism is the primary security layer.
+ html_content = HTML_TEMPLATE.replace("{{ bot_token_placeholder }}", "YOUR_BOT_TOKEN" if BOT_TOKEN == 'YOUR_BOT_TOKEN' else "HIDDEN")
+ return Response(html_content, mimetype='text/html')
+
+@app.route('/validate_telegram', methods=['POST'])
def validate_telegram_data():
+ """Validates initData, creates user if new, and establishes session."""
+ if BOT_TOKEN == 'YOUR_BOT_TOKEN':
+ logging.warning("Attempting validation with placeholder BOT_TOKEN!")
+ # Return error immediately if token isn't set for production
+ # return jsonify({"status": "error", "message": "Server configuration error: Bot token not set."}), 500
+
data = request.get_json()
if not data or 'initData' not in data:
return jsonify({"status": "error", "message": "Missing initData"}), 400
init_data_string = data['initData']
- validated_user_data = check_telegram_authorization(init_data_string, BOT_TOKEN)
+ validated_user_info = check_telegram_authorization(init_data_string, BOT_TOKEN)
+
+ if validated_user_info is not None and 'id' in validated_user_info:
+ user_id_str = str(validated_user_info['id'])
+ session['user_id'] = user_id_str
+ session['user_info'] = validated_user_info # Store full info if needed
- if validated_user_data is not None and 'id' in validated_user_data:
- user_id_str = str(validated_user_data['id'])
+ # Check if user exists, create if not
all_data = load_data()
if user_id_str not in all_data['users']:
- logging.info(f"New user detected via validation: {user_id_str}. Initializing data structure.")
- all_data['users'][user_id_str] = {
- 'user_info': validated_user_data,
- 'filesystem': initialize_user_filesystem(user_id_str),
- 'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
- }
- # Optionally trigger save immediately or rely on next operation/backup
- # save_data(all_data) # Uncomment to save on first validation
+ logging.info(f"New user detected: {user_id_str}, Name: {validated_user_info.get('first_name')}")
+ all_data['users'][user_id_str] = {
+ 'tg_info': validated_user_info,
+ 'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
+ 'filesystem': { "type": "folder", "id": "root", "name": "root", "children": [] }
+ }
+ try:
+ save_data(all_data)
+ logging.info(f"New user {user_id_str} saved.")
+ except Exception as e:
+ logging.error(f"Failed to save new user {user_id_str}: {e}")
+ # Should we proceed or return error? Let's proceed but log.
else:
- # Optionally update user_info if it changed (e.g., name, username)
- all_data['users'][user_id_str]['user_info'] = validated_user_data
+ # Ensure filesystem exists for existing user (data integrity check)
+ if 'filesystem' not in all_data['users'][user_id_str] or not isinstance(all_data['users'][user_id_str]['filesystem'], dict):
+ logging.warning(f"Filesystem missing or invalid for user {user_id_str}. Reinitializing.")
+ initialize_user_filesystem(all_data['users'][user_id_str])
+ # Optionally save immediately if structure was corrected
+ # try: save_data(all_data)
+ # except: logging.error(...)
+
+ return jsonify({"status": "ok", "user": validated_user_info})
+ else:
+ logging.warning("Telegram validation failed or user ID missing.")
+ session.clear() # Clear session on failed validation
+ return jsonify({"status": "error", "message": "Invalid Telegram data or expired session."}), 403
- # Consider saving updated user_info if changed
- # save_data(all_data)
+# --- Filesystem API Routes (Require Session) ---
- return jsonify({"status": "ok", "user": validated_user_data})
- else:
- logging.warning(f"Validation failed for initData: {init_data_string[:100]}...")
- return jsonify({"status": "error", "message": "Недействительные данные авторизации"}), 403
-
-# --- API Routes ---
-def get_validated_user(request_data):
- if not request_data or 'initData' not in request_data: return None, "Missing initData"
- init_data_string = request_data['initData']
- validated_user = check_telegram_authorization(init_data_string, BOT_TOKEN)
- if not validated_user or 'id' not in validated_user: return None, "Invalid or expired authorization"
- return validated_user, None
-
-@app.route('/api/list_items', methods=['POST'])
-def list_items():
- request_data = request.get_json()
- validated_user, error_msg = get_validated_user(request_data)
- if error_msg: return jsonify({"status": "error", "message": error_msg}), 403
-
- user_id_str = str(validated_user['id'])
- folder_id = request_data.get('folder_id', 'root')
- all_data = load_data()
- user_data = all_data['users'].get(user_id_str)
-
- if not user_data: return jsonify({"status": "error", "message": "User data not found"}), 404
- if 'filesystem' not in user_data: user_data['filesystem'] = initialize_user_filesystem(user_id_str)
-
- current_folder, _ = find_node_by_id(user_data['filesystem'], folder_id)
- if not current_folder or current_folder.get('type') != 'folder':
- return jsonify({"status": "error", "message": "Folder not found"}), 404
+@app.before_request
+def check_session():
+ # Allow access to '/' and '/validate_telegram' without session
+ if request.endpoint in ['index', 'validate_telegram_data', 'static']:
+ return
+ # Allow download and text_content if accessed via specific means (e.g., direct link after initial auth)
+ # This is less secure than validating initData on each request, but simpler for downloads.
+ # Consider adding more robust checks if needed.
+ if request.endpoint in ['download_file_route', 'get_text_content_route']:
+ # Maybe check a temporary token here in the future?
+ # For now, just rely on the existing session from the main app load.
+ pass
+
+ if 'user_id' not in session:
+ logging.warning(f"Unauthorized access attempt to {request.endpoint}. No user_id in session.")
+ return jsonify({"status": "error", "message": "Unauthorized. Please relaunch the app."}), 401
+
+@app.route('/filesystem/
', methods=['GET'])
+def get_folder_content(folder_id):
+ user_id = session.get('user_id')
+ if not user_id: return jsonify({"status": "error", "message": "Unauthorized"}), 401
+
+ data = load_data()
+ user_data = data['users'].get(user_id)
+ if not user_data or 'filesystem' not in user_data:
+ return jsonify({"status": "error", "message": "User data not found"}), 404
+
+ folder_node, _ = find_node_by_id(user_data['filesystem'], folder_id)
+ if not folder_node or folder_node.get('type') != 'folder':
+ # Try finding root if requested folder doesn't exist
+ folder_node, _ = find_node_by_id(user_data['filesystem'], 'root')
+ if not folder_node:
+ return jsonify({"status": "error", "message": "Root folder not found"}), 404
+ folder_id = 'root' # Reset to root
+
+
+ items_in_folder = sorted(folder_node.get('children', []), key=lambda x: (x.get('type', 'file') != 'folder', x.get('name', x.get('original_filename', '')).lower()))
# Build breadcrumbs
breadcrumbs = []
temp_id = folder_id
- safety_count = 0
- while temp_id and safety_count < 20: # Prevent infinite loop
+ visited_bc = set()
+ while temp_id and temp_id not in visited_bc:
+ visited_bc.add(temp_id)
node, parent = find_node_by_id(user_data['filesystem'], temp_id)
if not node: break
- breadcrumbs.append({'id': node['id'], 'name': node.get('name', 'Root' if node['id'] == 'root' else 'Unknown')})
+ breadcrumbs.append({'id': node['id'], 'name': node.get('name', 'Root')})
if not parent: break
temp_id = parent.get('id')
- safety_count += 1
breadcrumbs.reverse()
+
return jsonify({
"status": "ok",
- "items": current_folder.get('children', []),
- "breadcrumbs": breadcrumbs
+ "items": items_in_folder,
+ "breadcrumbs": breadcrumbs,
+ "current_folder_name": folder_node.get('name', 'Root'),
+ "current_folder_id": folder_id
})
-@app.route('/api/create_folder', methods=['POST'])
-def api_create_folder():
- request_data = request.get_json()
- validated_user, error_msg = get_validated_user(request_data)
- if error_msg: return jsonify({"status": "error", "message": error_msg}), 403
- user_id_str = str(validated_user['id'])
- parent_folder_id = request_data.get('parent_folder_id', 'root')
- folder_name = request_data.get('folder_name', '').strip()
+@app.route('/folder', methods=['POST'])
+def create_folder_route():
+ user_id = session.get('user_id')
+ if not user_id: return jsonify({"status": "error", "message": "Unauthorized"}), 401
- if not folder_name: return jsonify({"status": "error", "message": "Имя папки не может быть пустым"}), 400
- # Basic validation for folder name
- if not all(c.isalnum() or c in ' _-' for c in folder_name):
- return jsonify({"status": "error", "message": "Имя папки содержит недопустимые символы"}), 400
+ req_data = request.get_json()
+ parent_folder_id = req_data.get('parent_folder_id', 'root')
+ folder_name = req_data.get('folder_name', '').strip()
- all_data = load_data()
- user_data = all_data['users'].get(user_id_str)
- if not user_data: return jsonify({"status": "error", "message": "User data not found"}), 404
- if 'filesystem' not in user_data: user_data['filesystem'] = initialize_user_filesystem(user_id_str)
+ if not folder_name:
+ return jsonify({'status': 'error', 'message': 'Имя папки не может быть пустым!'}), 400
+ if not folder_name.replace(' ', '').replace('_', '').replace('-', '').isalnum():
+ return jsonify({'status': 'error', 'message': 'Имя папки содержит недопустимые символы.'}), 400
+ if len(folder_name) > 50: # Limit length
+ return jsonify({'status': 'error', 'message': 'Имя папки слишком длинное.'}), 400
-
- # Check if folder with same name already exists in parent
- parent_node, _ = find_node_by_id(user_data['filesystem'], parent_folder_id)
- if parent_node and parent_node.get('type') == 'folder':
- existing_names = {child.get('name', '').lower() for child in parent_node.get('children', []) if child.get('type') == 'folder'}
- if folder_name.lower() in existing_names:
- return jsonify({"status": "error", "message": f"Папка с именем '{folder_name}' уже существует здесь"}), 409 # Conflict
+ data = load_data()
+ user_data = data['users'].get(user_id)
+ if not user_data: return jsonify({'status': 'error', 'message': 'User data not found'}), 404
folder_id = uuid.uuid4().hex
- folder_data = {'type': 'folder', 'id': folder_id, 'name': folder_name, 'children': []}
+ folder_data = { 'type': 'folder', 'id': folder_id, 'name': folder_name, 'children': [] }
if add_node(user_data['filesystem'], parent_folder_id, folder_data):
try:
- save_data(all_data)
- return jsonify({"status": "ok", "new_folder": folder_data})
+ save_data(data)
+ return jsonify({'status': 'success', 'message': f'Папка "{folder_name}" создана.'})
except Exception as e:
- logging.error(f"Create folder save error for user {user_id_str}: {e}")
- # Attempt to rollback add_node? Difficult without deep copy.
- return jsonify({"status": "error", "message": "Ошибка сохранения данных"}), 500
+ logging.error(f"Create folder save error for user {user_id}: {e}")
+ # Attempt to remove added node if save failed? Complex.
+ return jsonify({'status': 'error', 'message': 'Ошибка сохранения данных при создании папки.'}), 500
else:
- return jsonify({"status": "error", "message": "Не удалось найти родительскую папку"}), 404
+ return jsonify({'status': 'error', 'message': 'Не удалось найти родительскую папку.'}), 404
-@app.route('/api/upload', methods=['POST'])
-def api_upload():
- init_data_string = request.form.get('initData')
- validated_user = check_telegram_authorization(init_data_string, BOT_TOKEN)
- if not validated_user or 'id' not in validated_user:
- return jsonify({"status": "error", "message": "Invalid or expired authorization"}), 403
+@app.route('/upload/', methods=['POST'])
+def upload_file_route(folder_id):
+ user_id = session.get('user_id')
+ user_info = session.get('user_info', {})
+ user_identifier = user_info.get('username', user_id) # Use username in path if available, else ID
- user_id_str = str(validated_user['id'])
- parent_folder_id = request.form.get('parent_folder_id', 'root')
- files = request.files.getlist('files')
+ if not user_id: return jsonify({"status": "error", "message": "Unauthorized"}), 401
+ if not HF_TOKEN_WRITE:
+ return jsonify({'status': 'error', 'message': 'Загрузка невозможна: токен HF для записи не настроен.'}), 503
- if not files: return jsonify({"status": "error", "message": "Нет файлов для загрузки"}), 400
- if not HF_TOKEN_WRITE or not REPO_ID:
- return jsonify({"status": "error", "message": "Загрузка не настроена на сервере (нет токена/репозитория)"}), 503
+ files = request.files.getlist('files')
+ if not files or all(not f.filename for f in files):
+ return jsonify({'status': 'error', 'message': 'Файлы для загрузки не выбраны.'}), 400
+ if len(files) > 20:
+ return jsonify({'status': 'error', 'message': 'Максимум 20 файлов за раз!'}), 400
- all_data = load_data()
- user_data = all_data['users'].get(user_id_str)
- if not user_data: return jsonify({"status": "error", "message": "User data not found"}), 404
- if 'filesystem' not in user_data: user_data['filesystem'] = initialize_user_filesystem(user_id_str)
+ data = load_data()
+ user_data = data['users'].get(user_id)
+ if not user_data: return jsonify({'status': 'error', 'message': 'User data not found'}), 404
- target_folder_node, _ = find_node_by_id(user_data['filesystem'], parent_folder_id)
+ target_folder_node, _ = find_node_by_id(user_data['filesystem'], folder_id)
if not target_folder_node or target_folder_node.get('type') != 'folder':
- return jsonify({"status": "error", "message": "Целевая папка не найдена"}), 404
+ return jsonify({'status': 'error', 'message': 'Целевая папка для загрузки не найдена!'}), 404
api = HfApi()
uploaded_count = 0
errors = []
- uploaded_files_metadata = []
+ save_needed = False
for file in files:
if file and file.filename:
original_filename = secure_filename(file.filename)
+ if len(original_filename) > 100: # Limit filename length
+ original_filename = original_filename[:97] + '...'
+
name_part, ext_part = os.path.splitext(original_filename)
- unique_suffix = uuid.uuid4().hex[:8]
+ unique_suffix = uuid.uuid4().hex[:6]
unique_filename = f"{name_part}_{unique_suffix}{ext_part}"
file_id = uuid.uuid4().hex
- hf_path = f"cloud_files/{user_id_str}/{parent_folder_id}/{unique_filename}" # Include user_id and folder_id
+
+ # Use user_id in the HF path for uniqueness
+ hf_path = f"cloud_files/{user_id}/{folder_id}/{unique_filename}"
temp_path = os.path.join(UPLOAD_FOLDER, f"{file_id}_{unique_filename}")
try:
file.save(temp_path)
- logging.info(f"Uploading {original_filename} (as {unique_filename}) to {hf_path} for user {user_id_str}")
api.upload_file(
- path_or_fileobj=temp_path, path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE,
- commit_message=f"User {user_id_str} uploaded {original_filename} to folder {parent_folder_id}"
+ path_or_fileobj=temp_path, path_in_repo=hf_path, repo_id=REPO_ID,
+ repo_type="dataset", token=HF_TOKEN_WRITE,
+ commit_message=f"TG User {user_identifier} ({user_id}) uploaded {original_filename} to folder {folder_id}"
)
- logging.info(f"Successfully uploaded {hf_path}")
-
file_info = {
'type': 'file', 'id': file_id, 'original_filename': original_filename,
'unique_filename': unique_filename, 'path': hf_path,
'file_type': get_file_type(original_filename),
- 'upload_date': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
- # 'size': os.path.getsize(temp_path) # Optionally add size
+ 'upload_date': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
}
- uploaded_files_metadata.append(file_info) # Store metadata even if adding node fails later
-
+ if add_node(user_data['filesystem'], folder_id, file_info):
+ uploaded_count += 1
+ save_needed = True
+ else:
+ errors.append(f"Ошибка добавления метаданных для {original_filename}.")
+ logging.error(f"Failed to add node metadata for file {file_id} to folder {folder_id} for user {user_id}")
+ # Attempt to delete orphaned file from HF
+ try: api.delete_file(path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE)
+ except Exception as del_err: logging.error(f"Failed to delete orphaned file {hf_path}: {del_err}")
except Exception as e:
- logging.error(f"Error uploading file {original_filename} for user {user_id_str}: {e}")
- errors.append(f"Ошибка '{original_filename}': {e}")
+ logging.error(f"Error uploading file {original_filename} for {user_id}: {e}")
+ errors.append(f"Ошибка загрузки {original_filename}: {str(e)[:100]}") # Limit error msg length
finally:
if os.path.exists(temp_path):
- os.remove(temp_path)
-
- # Add metadata to filesystem and save ONCE after all uploads attempt
- added_to_db_count = 0
- save_error = None
- if uploaded_files_metadata:
- for file_info in uploaded_files_metadata:
- if add_node(user_data['filesystem'], parent_folder_id, file_info):
- added_to_db_count += 1
- else:
- errors.append(f"Ошибка добавления метаданных для {file_info['original_filename']}")
- logging.error(f"Failed to add node metadata for file {file_info['id']} for user {user_id_str}")
- # Consider attempting to delete the orphaned file from HF here? Complex.
+ try: os.remove(temp_path)
+ except Exception as rm_err: logging.error(f"Error removing temp file {temp_path}: {rm_err}")
- if added_to_db_count > 0:
+ response_message = ""
+ if uploaded_count > 0:
+ response_message += f'{uploaded_count} файл(ов) успешно загружено. '
+ if save_needed:
try:
- save_data(all_data)
+ save_data(data)
except Exception as e:
- save_error = f"Ошибка сохранения данных: {e}"
- logging.error(f"Error saving data after upload for user {user_id_str}: {e}")
- errors.append(save_error)
+ response_message += 'Ошибка сохранения метаданных. '
+ logging.error(f"Error saving data after upload for {user_id}: {e}")
+ if errors:
+ response_message += "Ошибки: " + "; ".join(errors)
- uploaded_count = len(uploaded_files_metadata)
+ status = 'success' if uploaded_count > 0 else 'error'
+ if uploaded_count > 0 and errors: status = 'partial_success' # Indicate partial success
- return jsonify({
- "status": "ok",
- "uploaded_count": uploaded_count,
- "db_added_count": added_to_db_count,
- "errors": errors
- })
+ return jsonify({'status': status, 'message': response_message.strip()})
-@app.route('/api/delete_item', methods=['POST'])
-def api_delete_item():
- request_data = request.get_json()
- validated_user, error_msg = get_validated_user(request_data)
- if error_msg: return jsonify({"status": "error", "message": error_msg}), 403
-
- user_id_str = str(validated_user['id'])
- item_id = request_data.get('item_id')
- if not item_id: return jsonify({"status": "error", "message": "Missing item_id"}), 400
-
- all_data = load_data()
- user_data = all_data['users'].get(user_id_str)
- if not user_data: return jsonify({"status": "error", "message": "User data not found"}), 404
- if 'filesystem' not in user_data: return jsonify({"status": "error", "message": "Filesystem not found"}), 500
-
- item_node, parent_node = find_node_by_id(user_data['filesystem'], item_id)
-
- if not item_node: return jsonify({"status": "error", "message": "Элемент не найден"}), 404
- if item_id == 'root': return jsonify({"status": "error", "message": "Нельзя удалить корневой элемент"}), 400
-
- item_type = item_node.get('type')
- item_name = item_node.get('name', item_node.get('original_filename', 'элемент'))
-
- # Handle folder deletion
- if item_type == 'folder':
- if item_node.get('children'):
- return jsonify({"status": "error", "message": f"Папку '{item_name}' можно удалить только если она пуста"}), 400
- # Just remove from DB
- if remove_node(user_data['filesystem'], item_id):
- try:
- save_data(all_data)
- logging.info(f"User {user_id_str} deleted empty folder {item_name} (ID: {item_id})")
- return jsonify({"status": "ok", "message": f"Папка '{item_name}' удалена"})
- except Exception as e:
- logging.error(f"Delete empty folder save error for user {user_id_str}: {e}")
- # Rollback? Difficult.
- return jsonify({"status": "error", "message": "Ошибка сохранения после удаления папки"}), 500
- else:
- return jsonify({"status": "error", "message": "Не удалось удалить папку из структуры"}), 500
+@app.route('/download/')
+def download_file_route(file_id):
+ user_id = session.get('user_id')
+ if not user_id: return Response("Unauthorized", status=401)
+ if not HF_TOKEN_READ: return Response("Server configuration error: Read token missing", status=503)
- # Handle file deletion
- elif item_type == 'file':
- hf_path = item_node.get('path')
- if not hf_path:
- logging.warning(f"File {item_name} (ID: {item_id}) for user {user_id_str} has no path. Removing only metadata.")
- if remove_node(user_data['filesystem'], item_id):
- try: save_data(all_data); return jsonify({"status": "ok", "message": f"Метаданные файла '{item_name}' удалены (путь отсутствовал)"})
- except Exception as e: return jsonify({"status": "error", "message": "Ошибка сохранения после удаления метаданных"}), 500
- else: return jsonify({"status": "error", "message": "Не удалось удалить метаданные файла"}), 500
+ data = load_data()
+ user_data = data['users'].get(user_id)
+ if not user_data: return Response("User data not found", status=404)
- if not HF_TOKEN_WRITE or not REPO_ID:
- return jsonify({"status": "error", "message": "Удаление файлов не настроено на сервере"}), 503
+ file_node, _ = find_node_by_id(user_data['filesystem'], file_id)
+ if not file_node or file_node.get('type') != 'file':
+ return Response("File not found", status=404)
+ hf_path = file_node.get('path')
+ original_filename = file_node.get('original_filename', 'downloaded_file')
+ if not hf_path: return Response("Error: File path missing", status=500)
+
+ file_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{hf_path}?download=true"
+ try:
+ headers = {"authorization": f"Bearer {HF_TOKEN_READ}"}
+ response = requests.get(file_url, headers=headers, stream=True, timeout=60) # Added timeout
+ response.raise_for_status()
+
+ # Stream response for potentially large files
+ def generate():
+ for chunk in response.iter_content(chunk_size=8192):
+ yield chunk
+
+ return Response(generate(), mimetype='application/octet-stream', headers={
+ "Content-Disposition": f"attachment; filename*=UTF-8''{secure_filename(original_filename)}"
+ })
+
+ except requests.exceptions.RequestException as e:
+ logging.error(f"Error downloading file from HF ({hf_path}) for user {user_id}: {e}")
+ return Response(f'Download error: {e}', status=502)
+ except Exception as e:
+ logging.error(f"Unexpected error during download ({hf_path}) for user {user_id}: {e}")
+ return Response('Internal server error during download', status=500)
+
+
+@app.route('/file/', methods=['DELETE'])
+def delete_file_route(file_id):
+ user_id = session.get('user_id')
+ user_info = session.get('user_info', {})
+ user_identifier = user_info.get('username', user_id)
+ if not user_id: return jsonify({"status": "error", "message": "Unauthorized"}), 401
+ if not HF_TOKEN_WRITE:
+ return jsonify({'status': 'error', 'message': 'Удаление невозможно: токен HF для записи не настроен.'}), 503
+
+ data = load_data()
+ user_data = data['users'].get(user_id)
+ if not user_data: return jsonify({'status': 'error', 'message': 'User data not found'}), 404
+
+ file_node, parent_node = find_node_by_id(user_data['filesystem'], file_id)
+ if not file_node or file_node.get('type') != 'file' or not parent_node:
+ return jsonify({'status': 'error', 'message': 'Файл не найден.'}), 404
+
+ hf_path = file_node.get('path')
+ original_filename = file_node.get('original_filename', 'файл')
+
+ delete_from_hf = True
+ if not hf_path:
+ logging.warning(f"HF path missing for file {file_id} user {user_id}. Deleting only metadata.")
+ delete_from_hf = False
+
+ if delete_from_hf:
try:
api = HfApi()
- logging.info(f"Attempting to delete HF file {hf_path} for user {user_id_str}")
api.delete_file(
path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE,
- commit_message=f"User {user_id_str} deleted file {item_name} (ID: {item_id})"
+ commit_message=f"TG User {user_identifier} ({user_id}) deleted file {original_filename}"
)
- logging.info(f"Successfully deleted HF file {hf_path}")
-
+ logging.info(f"Deleted file {hf_path} from HF Hub for user {user_id}")
except hf_utils.EntryNotFoundError:
- logging.warning(f"File {hf_path} not found on HF Hub during delete attempt for user {user_id_str}. Removing from DB.")
+ logging.warning(f"File {hf_path} not found on HF Hub during delete for user {user_id}.")
except Exception as e:
- logging.error(f"Error deleting file {hf_path} from HF for user {user_id_str}: {e}")
- # Don't delete from DB if HF deletion failed unexpectedly
- return jsonify({"status": "error", "message": f"Ошибка удаления файла с сервера: {e}"}), 500
-
- # If HF deletion successful or file not found on HF, remove from DB
- if remove_node(user_data['filesystem'], item_id):
- try:
- save_data(all_data)
- return jsonify({"status": "ok", "message": f"Файл '{item_name}' удален"})
- except Exception as e:
- logging.error(f"Delete file DB update error for user {user_id_str}: {e}")
- return jsonify({"status": "error", "message": "Ошибка сохранения после удаления файла"}), 500
- else:
- # This case should be rare if find_node_by_id worked initially
- return jsonify({"status": "error", "message": "Не удалось удалить файл из структуры после операции на сервере"}), 500
+ logging.error(f"Error deleting file {hf_path} from HF for {user_id}: {e}")
+ return jsonify({'status': 'error', 'message': f'Ошибка удаления файла с сервера: {str(e)[:100]}'}), 500
+ # Always try to remove from DB
+ if remove_node(user_data['filesystem'], file_id):
+ try:
+ save_data(data)
+ return jsonify({'status': 'success', 'message': f'Файл {original_filename} удален.'})
+ except Exception as e:
+ logging.error(f"Delete file DB save error for user {user_id}: {e}")
+ return jsonify({'status': 'error', 'message': 'Файл удален с сервера (если был), но ошибка сохранения базы.'}), 500
else:
- return jsonify({"status": "error", "message": "Неизвестный тип элемента"}), 400
-
+ # This case should ideally not happen if file_node was found earlier
+ logging.error(f"Failed to remove node {file_id} from filesystem structure for user {user_id} after finding it initially.")
+ return jsonify({'status': 'error', 'message': 'Ошибка удаления файла из структуры данных.'}), 500
-# --- Direct Access Routes (Download/Preview) ---
-# WARNING: These endpoints currently LACK AUTHORIZATION.
-# In a real application, you MUST implement secure access control,
-# e.g., using short-lived signed URLs or token-based authentication.
-def find_file_globally(file_id):
- """Finds a file node by ID across all users. Use with extreme caution."""
- all_data = load_data()
- for user_id, user_data in all_data.get('users', {}).items():
- if 'filesystem' in user_data:
- file_node, _ = find_node_by_id(user_data['filesystem'], file_id)
- if file_node and file_node.get('type') == 'file':
- return file_node, user_id # Return node and owner ID
- return None, None
+@app.route('/folder/', methods=['DELETE'])
+def delete_folder_route(folder_id):
+ user_id = session.get('user_id')
+ if not user_id: return jsonify({"status": "error", "message": "Unauthorized"}), 401
+ if folder_id == 'root': return jsonify({'status': 'error', 'message': 'Нельзя удалить корневую папку!'}), 400
-@app.route('/download/')
-def download_file_unauth(file_id):
- file_node, owner_id = find_file_globally(file_id)
- if not file_node:
- return Response("Файл не найден", status=404)
+ data = load_data()
+ user_data = data['users'].get(user_id)
+ if not user_data: return jsonify({'status': 'error', 'message': 'User data not found'}), 404
- hf_path = file_node.get('path')
- original_filename = file_node.get('original_filename', f'download_{file_id}')
- if not hf_path or not REPO_ID:
- return Response("Ошибка сервера: Путь к файлу или репозиторий не настроен", status=500)
+ folder_node, parent_node = find_node_by_id(user_data['filesystem'], folder_id)
+ if not folder_node or folder_node.get('type') != 'folder' or not parent_node:
+ return jsonify({'status': 'error', 'message': 'Папка не найдена.'}), 404
- file_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{quote(hf_path)}?download=true"
- logging.info(f"Attempting unauthenticated download for file ID {file_id} (Owner: {owner_id}) from URL: {file_url}")
+ folder_name = folder_node.get('name', 'папка')
+ if folder_node.get('children'):
+ return jsonify({'status': 'error', 'message': f'Папку "{folder_name}" можно удалить только если она пуста.'}), 400
- try:
- headers = {}
- if HF_TOKEN_READ: headers["authorization"] = f"Bearer {HF_TOKEN_READ}"
- response = requests.get(file_url, headers=headers, stream=True, timeout=60)
- response.raise_for_status()
+ if remove_node(user_data['filesystem'], folder_id):
+ try:
+ save_data(data)
+ return jsonify({'status': 'success', 'message': f'Папка "{folder_name}" удалена.'})
+ except Exception as e:
+ logging.error(f"Delete empty folder save error for user {user_id}: {e}")
+ # Attempt to add node back? Difficult state to recover.
+ return jsonify({'status': 'error', 'message': 'Ошибка сохранения данных после удаления папки.'}), 500
+ else:
+ logging.error(f"Failed to remove folder node {folder_id} for user {user_id} after finding it.")
+ return jsonify({'status': 'error', 'message': 'Ошибка удаления папки из структуры данных.'}), 500
- # Stream response if large? For simplicity, load into BytesIO first
- file_content = BytesIO(response.content)
- return send_file(
- file_content, as_attachment=True, download_name=original_filename,
- mimetype='application/octet-stream' # Generic mimetype
- )
- except requests.exceptions.RequestException as e:
- logging.error(f"Error downloading file from HF ({hf_path}): {e}")
- return Response(f"Ошибка скачивания файла: {e}", status=502)
- except Exception as e:
- logging.error(f"Unexpected error during download ({hf_path}): {e}")
- return Response("Внутренняя ошибка сервера при скачивании", status=500)
+@app.route('/text_content/')
+def get_text_content_route(file_id):
+ user_id = session.get('user_id')
+ if not user_id: return Response("Unauthorized", status=401)
+ if not HF_TOKEN_READ: return Response("Server configuration error: Read token missing", status=503)
+ data = load_data()
+ user_data = data['users'].get(user_id)
+ if not user_data: return Response("User data not found", status=404)
-@app.route('/get_text_content/')
-def get_text_content_unauth(file_id):
- file_node, owner_id = find_file_globally(file_id)
- if not file_node or file_node.get('file_type') != 'text':
- return Response("Текстовый файл не найден", status=404)
+ file_node, _ = find_node_by_id(user_data['filesystem'], file_id)
+ if not file_node or file_node.get('type') != 'file' or file_node.get('file_type') != 'text':
+ return Response("Text file not found", status=404)
hf_path = file_node.get('path')
- if not hf_path or not REPO_ID:
- return Response("Ошибка сервера: Путь к файлу или репозиторий не настроен", status=500)
-
- file_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{quote(hf_path)}?download=true"
- logging.info(f"Attempting unauthenticated text preview for file ID {file_id} (Owner: {owner_id}) from URL: {file_url}")
+ if not hf_path: return Response("Error: File path missing", status=500)
+ file_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{hf_path}?download=true"
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, timeout=30)
response.raise_for_status()
-
- if len(response.content) > 2 * 1024 * 1024: # Limit preview size (e.g., 2MB)
- return Response("Файл слишком большой для предпросмотра.", status=413)
-
- # Try decoding with common encodings
- text_content = None
+ if len(response.content) > 1 * 1024 * 1024: # 1MB limit for preview
+ return Response("File too large for preview.", status=413)
try: text_content = response.content.decode('utf-8')
- except UnicodeDecodeError:
- try: text_content = response.content.decode('windows-1251')
- except UnicodeDecodeError:
- try: text_content = response.content.decode('latin-1')
- except Exception: return Response("Не удалось определить кодировку файла.", status=500)
-
+ except UnicodeDecodeError: text_content = response.content.decode('latin-1', errors='replace')
return Response(text_content, mimetype='text/plain; charset=utf-8')
-
except requests.exceptions.RequestException as e:
- logging.error(f"Error fetching text content from HF ({hf_path}): {e}")
- return Response(f"Ошибка загрузки содержимого: {e}", status=502)
+ logging.error(f"Error fetching text content from HF ({hf_path}) for user {user_id}: {e}")
+ return Response(f"Error loading content: {e}", status=502)
except Exception as e:
- logging.error(f"Unexpected error fetching text content ({hf_path}): {e}")
- return Response("Внутренняя ошибка сервера при получении текста", status=500)
+ logging.error(f"Unexpected error fetching text content ({hf_path}) for user {user_id}: {e}")
+ return Response("Internal server error", status=500)
-# --- Server Start ---
+# --- Main Execution ---
if __name__ == '__main__':
if BOT_TOKEN == 'YOUR_BOT_TOKEN':
- logging.critical("!!! TELEGRAM_BOT_TOKEN is not set or uses placeholder. Validation WILL FAIL. !!!")
+ logging.warning("*"*60)
+ logging.warning("WARNING: Using placeholder Telegram BOT_TOKEN!")
+ logging.warning("Telegram validation will likely FAIL.")
+ logging.warning("Set the TELEGRAM_BOT_TOKEN environment variable.")
+ logging.warning("*"*60)
if not HF_TOKEN_WRITE:
- logging.warning("!!! HF_TOKEN (write access) is not set. File uploads, deletions, and backups will fail. !!!")
+ logging.warning("HF_TOKEN (write access) is not set. Uploads, deletions, backups will fail.")
if not HF_TOKEN_READ:
- logging.warning("HF_TOKEN_READ is not set. Using HF_TOKEN (write) for reads. Downloads/previews might fail for private repos if HF_TOKEN is also not set.")
- if not REPO_ID or REPO_ID.startswith("YOUR_"):
- logging.critical(f"!!! HF_REPO_ID is not set or uses placeholder ('{REPO_ID}'). Application needs a valid Hugging Face dataset repository ID. !!!")
-
- # Initial DB download and start backup thread only if configured
- if HF_TOKEN_WRITE and HF_TOKEN_READ and REPO_ID and not REPO_ID.startswith("YOUR_"):
- logging.info("Performing initial database download...")
- download_db_from_hf() # Attempt download first
- if not os.path.exists(DATA_FILE): # If download failed or file absent, 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}")
- threading.Thread(target=periodic_backup, daemon=True).start()
- logging.info("Periodic backup thread started (every 30 min).")
- elif HF_TOKEN_READ and REPO_ID and not REPO_ID.startswith("YOUR_"):
- logging.info("Read-only mode: Performing initial database download.")
- download_db_from_hf()
- if not os.path.exists(DATA_FILE):
- logging.warning(f"Could not download DB and file {DATA_FILE} does not exist locally. Starting with empty state (read-only).")
- # Don't create the file in read-only to avoid confusion
+ logging.warning("HF_TOKEN_READ is not set (or HF_TOKEN). Downloads/previews might fail.")
+ if not FLASK_SECRET_KEY or FLASK_SECRET_KEY == "supersecretkey_mini_app_unique":
+ logging.warning("Using default/insecure FLASK_SECRET_KEY. Set a strong secret key via environment variable.")
+
+ # Start periodic backup in a separate thread if write token exists
+ if HF_TOKEN_WRITE:
+ logging.info("Attempting initial database download before starting periodic backup.")
+ download_db_from_hf() # Load the latest DB before starting
+ backup_thread = threading.Thread(target=periodic_backup, daemon=True)
+ backup_thread.start()
+ logging.info("Periodic backup thread started.")
else:
- logging.warning("HF tokens/repo not fully configured. Database operations with Hugging Face Hub are limited or disabled.")
- if not os.path.exists(DATA_FILE):
- with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}}, f)
- logging.info(f"Created empty local database file: {DATA_FILE}")
+ logging.warning("Periodic backup disabled (HF_TOKEN_WRITE not set).")
+ # Still try to download if read token exists
+ if HF_TOKEN_READ:
+ logging.info("Attempting initial database download (read-only access).")
+ download_db_from_hf()
+ else:
+ logging.warning("No read/write HF tokens. HF DB operations disabled.")
+ # Ensure local file exists if no download possible
+ if not os.path.exists(DATA_FILE):
+ with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}}, f)
+
+ logging.info(f"Starting Flask server for Telegram Mini App on 0.0.0.0:7860")
+ app.run(host='0.0.0.0', port=7860, debug=False) # debug=False for production
- logging.info(f"Starting Flask server on {HOST}:{PORT}")
- # Use waitress or gunicorn for production instead of debug=True
- # from waitress import serve
- # serve(app, host=HOST, port=PORT)
- app.run(host=HOST, port=PORT, debug=False) # debug=False is crucial for production
\ No newline at end of file
+# --- END OF FILE app.py ---
\ No newline at end of file