diff --git "a/app.py" "b/app.py"
--- "a/app.py"
+++ "b/app.py"
@@ -2,7 +2,8 @@
# -*- coding: utf-8 -*-
import os
-from flask import Flask, request, Response, render_template_string, jsonify, redirect, url_for, make_response
+import flask
+from flask import Flask, request, Response, render_template_string, jsonify, redirect, url_for, send_file
import hmac
import hashlib
import json
@@ -12,27 +13,195 @@ from datetime import datetime
import logging
import threading
from huggingface_hub import HfApi, hf_hub_download, list_repo_files
-from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError
-from werkzeug.utils import secure_filename
+from huggingface_hub.utils import RepositoryNotFoundError, EntryNotFoundError
+import mimetypes
+import io
# --- Configuration ---
-BOT_TOKEN = os.getenv("BOT_TOKEN", "6750208873:AAE2hvPlJ99dBdhGa_Brre0IIpUdOvXxHt4")
+BOT_TOKEN = os.getenv("BOT_TOKEN", "6750208873:AAE2hvPlJ99dBdhGa_Brre0IIpUdOvXxHt4") # Use environment variable or default
HOST = '0.0.0.0'
PORT = 7860
+DATA_FILE = 'data.json' # Local file for user and file metadata
# Hugging Face Settings
-REPO_ID = "Eluza133/Z1e1u"
-HF_UPLOAD_FOLDER = "uploads" # Base folder within the HF repo
+REPO_ID = os.getenv("HF_REPO_ID", "Eluza133/Z1e1u") # Target repository
+HF_DATA_FILE_PATH = "data.json" # Path for the metadata file within the HF repo
+HF_UPLOAD_FOLDER = "uploads" # Folder within the HF repo to store user files
HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE") # Token with write access
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") # Token with read access (can be same as write)
-# File Upload Settings
-MAX_FILES_PER_UPLOAD = 20
+# Constants
+MAX_UPLOAD_FILES = 20
+AUTH_TIMEOUT = 86400 # 24 hours validity for initData
app = Flask(__name__)
-logging.basicConfig(level=logging.INFO)
-app.secret_key = os.urandom(24)
-hf_api = HfApi()
+logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
+app.secret_key = os.urandom(24) # For potential future session use
+
+# --- Hugging Face & Data Handling ---
+_data_lock = threading.Lock()
+metadata_cache = {} # In-memory cache for data.json
+
+def get_hf_api(write=False):
+ token = HF_TOKEN_WRITE if write else HF_TOKEN_READ
+ if not token:
+ logging.warning(f"Hugging Face {'write' if write else 'read'} token not set.")
+ return None
+ return HfApi(token=token)
+
+def download_metadata_from_hf():
+ global metadata_cache
+ api = get_hf_api(write=False)
+ if not api:
+ logging.warning("HF Read token missing. Cannot download metadata.")
+ return False
+ try:
+ logging.info(f"Attempting to download {HF_DATA_FILE_PATH} from {REPO_ID}...")
+ download_path = hf_hub_download(
+ repo_id=REPO_ID,
+ filename=HF_DATA_FILE_PATH,
+ repo_type="dataset",
+ token=api.token,
+ local_dir=".",
+ local_dir_use_symlinks=False,
+ force_download=True,
+ etag_timeout=10
+ )
+ logging.info("Metadata file successfully downloaded from Hugging Face.")
+ with _data_lock:
+ try:
+ with open(download_path, 'r', encoding='utf-8') as f:
+ metadata_cache = json.load(f)
+ logging.info("Successfully loaded downloaded metadata into cache.")
+ except (FileNotFoundError, json.JSONDecodeError) as e:
+ logging.error(f"Error reading downloaded metadata file: {e}. Resetting cache.")
+ metadata_cache = {}
+ # Clean up downloaded file? hf_hub_download uses a cache, maybe not needed.
+ # if os.path.exists(DATA_FILE) and download_path != DATA_FILE:
+ # os.remove(download_path) # Remove temp download if different name
+ return True
+ except EntryNotFoundError:
+ logging.warning(f"Metadata file '{HF_DATA_FILE_PATH}' not found in repo '{REPO_ID}'. Starting fresh.")
+ with _data_lock:
+ metadata_cache = {}
+ return True # It's not an error, just no existing data
+ except RepositoryNotFoundError:
+ logging.error(f"Hugging Face repository '{REPO_ID}' not found. Cannot download metadata.")
+ except Exception as e:
+ logging.error(f"Error downloading metadata from Hugging Face: {e}", exc_info=True)
+ return False
+
+def load_local_metadata():
+ global metadata_cache
+ with _data_lock:
+ if not metadata_cache: # Only load from file if cache is empty
+ try:
+ with open(DATA_FILE, 'r', encoding='utf-8') as f:
+ metadata_cache = json.load(f)
+ logging.info("Metadata loaded from local JSON.")
+ except FileNotFoundError:
+ logging.warning(f"{DATA_FILE} not found locally. Starting with empty data.")
+ metadata_cache = {}
+ except json.JSONDecodeError:
+ logging.error(f"Error decoding {DATA_FILE}. Starting with empty data.")
+ metadata_cache = {}
+ except Exception as e:
+ logging.error(f"Unexpected error loading metadata: {e}")
+ metadata_cache = {}
+ return metadata_cache
+
+def save_metadata(data_to_update=None):
+ global metadata_cache
+ with _data_lock:
+ try:
+ if data_to_update:
+ # Deep merge might be needed if updating nested structures
+ # For now, simple update assumes top-level keys (user IDs)
+ metadata_cache.update(data_to_update)
+
+ # Save updated cache to local file
+ with open(DATA_FILE, 'w', encoding='utf-8') as f:
+ json.dump(metadata_cache, f, ensure_ascii=False, indent=4)
+ logging.info(f"Metadata successfully saved locally to {DATA_FILE}.")
+
+ # Trigger async upload to HF after successful local save
+ upload_metadata_to_hf_async()
+ return True
+ except Exception as e:
+ logging.error(f"Error saving metadata: {e}", exc_info=True)
+ return False
+
+def update_user_file_metadata(user_id, file_info_list):
+ user_id_str = str(user_id)
+ with _data_lock:
+ if user_id_str not in metadata_cache:
+ metadata_cache[user_id_str] = {"user_info": {}, "files": []}
+
+ if "files" not in metadata_cache[user_id_str]:
+ metadata_cache[user_id_str]["files"] = []
+
+ # Add new files, potentially checking for duplicates if needed
+ existing_filenames = {f['filename'] for f in metadata_cache[user_id_str]["files"]}
+ new_files_added = 0
+ for file_info in file_info_list:
+ if file_info['filename'] not in existing_filenames:
+ metadata_cache[user_id_str]["files"].append(file_info)
+ existing_filenames.add(file_info['filename'])
+ new_files_added += 1
+ else:
+ # Handle update logic if a file with the same name is re-uploaded
+ # For now, we just log it. Replace or versioning could be added.
+ logging.warning(f"File '{file_info['filename']}' already exists for user {user_id}. Skipping add.")
+ # Example: Update existing entry
+ # for i, existing_file in enumerate(metadata_cache[user_id_str]["files"]):
+ # if existing_file['filename'] == file_info['filename']:
+ # metadata_cache[user_id_str]["files"][i] = file_info # Replace with new metadata
+ # break
+
+ if new_files_added > 0:
+ logging.info(f"Added {new_files_added} file metadata entries for user {user_id}.")
+ # Save metadata (which will trigger HF upload)
+ if not save_metadata():
+ return False # Propagate save error
+ else:
+ logging.info(f"No new file metadata added for user {user_id}.")
+
+ return True # Indicate metadata was processed (even if no new files added)
+
+def _upload_metadata_to_hf_task():
+ api = get_hf_api(write=True)
+ if not api:
+ logging.warning("HF Write token missing. Skipping metadata upload.")
+ return
+ if not os.path.exists(DATA_FILE):
+ logging.warning(f"{DATA_FILE} does not exist locally. Skipping upload.")
+ return
+
+ try:
+ # Acquire lock only for reading the file path and ensuring it has content
+ with _data_lock:
+ if os.path.getsize(DATA_FILE) == 0:
+ logging.warning(f"{DATA_FILE} is empty. Skipping upload.")
+ # Optionally upload an empty file or a default structure like {}
+ # For now, skip.
+ return
+ file_to_upload = DATA_FILE # Use the local file path
+
+ logging.info(f"Attempting to upload {file_to_upload} to {REPO_ID}/{HF_DATA_FILE_PATH}...")
+ api.upload_file(
+ path_or_fileobj=file_to_upload,
+ path_in_repo=HF_DATA_FILE_PATH,
+ repo_id=REPO_ID,
+ repo_type="dataset",
+ commit_message=f"Update metadata {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
+ )
+ logging.info("Metadata successfully uploaded to Hugging Face.")
+ except Exception as e:
+ logging.error(f"Error uploading metadata to Hugging Face: {e}", exc_info=True)
+
+def upload_metadata_to_hf_async():
+ upload_thread = threading.Thread(target=_upload_metadata_to_hf_task, daemon=True)
+ upload_thread.start()
# --- Telegram Verification ---
def verify_telegram_data(init_data_str):
@@ -41,50 +210,97 @@ def verify_telegram_data(init_data_str):
received_hash = parsed_data.pop('hash', [None])[0]
if not received_hash:
- logging.warning("Verification failed: Hash missing")
- return None, False, "Хэш отсутствует"
+ logging.warning("Verification failed: Hash missing from initData.")
+ return None, False, "Hash missing"
data_check_list = []
+ # Sort keys alphabetically for consistent string generation
for key, value in sorted(parsed_data.items()):
+ # Make sure values are handled correctly (they are lists in parse_qs)
data_check_list.append(f"{key}={value[0]}")
data_check_string = "\n".join(data_check_list)
+ # Calculate secret key
secret_key = hmac.new("WebAppData".encode(), BOT_TOKEN.encode(), hashlib.sha256).digest()
+ # Calculate data hash
calculated_hash = hmac.new(secret_key, data_check_string.encode(), hashlib.sha256).hexdigest()
- if calculated_hash == received_hash:
- auth_date = int(parsed_data.get('auth_date', [0])[0])
- current_time = int(time.time())
- # Allow data up to 24 hours old, adjust if needed
- if current_time - auth_date > 86400:
- logging.warning(f"Telegram InitData is older than 24 hours (Auth Date: {auth_date}, Current: {current_time}). Allowing.")
- # return parsed_data, False, "Данные авторизации устарели" # Uncomment to enforce stricter timeout
+ # Compare hashes
+ if calculated_hash != received_hash:
+ logging.warning(f"Verification failed: Hash mismatch. Calculated: {calculated_hash}, Received: {received_hash}")
+ return parsed_data, False, "Invalid hash"
+
+ # Check auth_date (timestamp)
+ auth_date = int(parsed_data.get('auth_date', [0])[0])
+ current_time = int(time.time())
+ if current_time - auth_date > AUTH_TIMEOUT:
+ logging.warning(f"Verification failed: initData expired. Auth time: {auth_date}, Current time: {current_time}")
+ return parsed_data, False, "Data expired"
+
+ # Extract user info if present
+ user_info_dict = None
+ if 'user' in parsed_data:
+ try:
+ user_json_str = unquote(parsed_data['user'][0])
+ user_info_dict = json.loads(user_json_str)
+ except Exception as e:
+ logging.error(f"Could not parse user JSON from initData: {e}")
+ # Continue verification, but user info might be missing
+
+ logging.info(f"Telegram data verified successfully for user ID: {user_info_dict.get('id') if user_info_dict else 'Unknown'}")
+ return user_info_dict, True, "Verified"
- return parsed_data, True, "Верификация успешна"
- else:
- logging.warning(f"Verification failed. Calculated: {calculated_hash}, Received: {received_hash}")
- return parsed_data, False, "Неверный хэш"
except Exception as e:
- logging.error(f"Error verifying Telegram data: {e}")
- return None, False, f"Ошибка верификации: {e}"
+ logging.error(f"Error during Telegram data verification: {e}", exc_info=True)
+ return None, False, "Verification exception"
-def get_user_id_from_init_data(init_data_str):
- parsed_data, is_valid, _ = verify_telegram_data(init_data_str)
+# --- User Authentication & Data Update ---
+def authenticate_and_get_user(init_data_str):
+ user_info, is_valid, message = verify_telegram_data(init_data_str)
if not is_valid:
- return None
+ return None, message
+
+ user_id = user_info.get('id') if user_info else None
+ if not user_id:
+ logging.warning("Verification successful but user ID is missing in user data.")
+ return None, "User ID missing"
+
+ user_id_str = str(user_id)
+
+ # Ensure user exists in metadata cache and update basic info if needed
+ with _data_lock:
+ should_save = False
+ if user_id_str not in metadata_cache:
+ metadata_cache[user_id_str] = {
+ "user_info": user_info,
+ "files": []
+ }
+ logging.info(f"New user registered: {user_id}")
+ should_save = True
+ else:
+ # Optionally update user_info if it has changed (e.g., name, username, photo)
+ # This requires comparing fields and deciding if an update is needed.
+ # Simple approach: Always update if provided.
+ if "user_info" not in metadata_cache[user_id_str] or metadata_cache[user_id_str]["user_info"] != user_info:
+ metadata_cache[user_id_str]["user_info"] = user_info
+ # logging.info(f"User info updated for user: {user_id}") # Can be noisy
+ should_save = True # Save if info changed or was missing
+
+ # Save metadata only if the user was new or info was updated
+ if should_save:
+ # save_metadata is already thread-safe and uploads async
+ if not save_metadata():
+ # Handle save failure, though it's unlikely to fail just for user info update
+ logging.error(f"Failed to save metadata after updating/adding user {user_id}")
+ # Decide if this should prevent the user from proceeding. For now, allow.
+
+ return user_info, "Authenticated"
- user_data_json = parsed_data.get('user', [None])[0]
- if not user_data_json:
- return None
- try:
- user_info = json.loads(unquote(user_data_json))
- return user_info.get('id')
- except (json.JSONDecodeError, TypeError) as e:
- logging.error(f"Could not parse user data from initData: {e}")
- return None
# --- HTML Templates ---
-TEMPLATE = """
+
+# Main User Interface Template
+USER_TEMPLATE = """
@@ -94,296 +310,209 @@ TEMPLATE = """
-
+
-
-
-
+
Загрузка...
+
-
- Мои файлы
- Загрузка списка файлов...
-
- У вас пока нет загруженных файлов.
+
+ Ваши файлы
+
+
+ - У вас пока нет загруженных файлов.
+
+
-
-
+
+
"""
+# Admin Panel Template
+ADMIN_TEMPLATE = """
+
+
+
+
+
+ Admin - Zeus Cloud
+
+
+
+
+
+
+
+
Zeus Cloud - Админ Панель
+
ВНИМАНИЕ: Этот раздел не защищен! Добавьте аутентификацию для реального использования.
+
+
+
Управление метаданными
+
+
+
+
+
+ {% if users %}
+
+ {% for user_id, data in users.items() %}
+
+
+
+
Язык: {{ data.user_info.language_code or 'N/A' }}
+
Premium: {{ 'Да' if data.user_info and data.user_info.is_premium else 'Нет' }}
+
+
+ Файлов загружено: {{ data.files|length if data.files else 0 }}
+
+
Просмотреть файлы
+
+ {% endfor %}
+
+ {% else %}
+
Пользователей не найдено.
+ {% endif %}
+
+
+
+
+"""
+
+ADMIN_USER_FILES_TEMPLATE = """
+
+
+
+
+
+ Файлы пользователя {{ user_info.first_name or user_id }} - Admin
+
+
+
+
+
+
+
+
← Назад к списку пользователей
+
Файлы пользователя
+
{{ user_info.first_name or '' }} {{ user_info.last_name or '' }} (ID: {{ user_id }})
+
+ {% if files %}
+
+
+
+ | Имя файла |
+ Размер |
+ Дата загрузки |
+ Тип |
+ Действия |
+
+
+
+ {% for file in files|sort(attribute='uploaded_at_ts', reverse=true) %}
+
+ | {{ file.filename }} |
+ {{ file.size | filesizeformat if file.size else 'N/A' }} |
+ {{ file.uploaded_at_str or 'N/A' }} |
+ {{ file.content_type or 'N/A' }} |
+
+ Скачать
+ |
+
+ {% endfor %}
+
+
+ {% else %}
+
У этого пользователя нет загруженных файлов.
+ {% endif %}
+
+
+
+
+"""
+
+# --- Jinja Filters ---
+@app.template_filter('filesizeformat')
+def filesizeformat(value):
+ try:
+ bytes_val = int(value)
+ if bytes_val == 0: return '0 Bytes'
+ k = 1024
+ sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
+ i = min(int(math.floor(math.log(bytes_val) / math.log(k))), len(sizes) - 1)
+ return f"{bytes_val / math.pow(k, i):.2f} {sizes[i]}"
+ except (ValueError, TypeError):
+ return value
+ except Exception: # Catch log(0) or other math errors
+ return 'N/A'
+
+import math # Needs import for the filter
+
# --- Flask Routes ---
+
@app.route('/')
def index():
- theme_params = {} # Let JS handle theme application
- return render_template_string(TEMPLATE, theme=theme_params, max_files=MAX_FILES_PER_UPLOAD)
+ # Render the main user interface
+ # Theme params will be applied by JS after tg.ready()
+ return render_template_string(USER_TEMPLATE, theme={}, max_files=MAX_UPLOAD_FILES)
-@app.route('/verify', methods=['POST'])
-def verify_route():
+@app.route('/files', methods=['POST'])
+def get_user_files():
req_data = request.get_json()
init_data_str = req_data.get('initData')
if not init_data_str:
- return jsonify({"status": "error", "verified": False, "message": "Missing initData"}), 400
+ return jsonify({"status": "error", "message": "Missing initData"}), 400
- user_data_parsed, is_valid, message = verify_telegram_data(init_data_str)
+ user_info, message = authenticate_and_get_user(init_data_str)
+ if not user_info:
+ return jsonify({"status": "error", "message": message}), 403
- user_info_dict = {}
- if user_data_parsed and 'user' in user_data_parsed:
- try:
- user_json_str = unquote(user_data_parsed['user'][0])
- user_info_dict = json.loads(user_json_str)
- except Exception as e:
- logging.error(f"Could not parse user JSON in /verify: {e}")
-
- if is_valid:
- return jsonify({"status": "ok", "verified": True, "user": user_info_dict}), 200
- else:
- return jsonify({"status": "error", "verified": False, "message": message, "user": user_info_dict}), 403
+ user_id_str = str(user_info['id'])
+ with _data_lock:
+ user_data = metadata_cache.get(user_id_str, {})
+ files = user_data.get('files', [])
+ return jsonify({"status": "ok", "files": files}), 200
@app.route('/upload', methods=['POST'])
def upload_files():
- if not HF_TOKEN_WRITE:
- return jsonify({"status": "error", "message": "Функция загрузки недоступна (ошибка конфигурации сервера)."}), 503
-
init_data_str = request.form.get('initData')
if not init_data_str:
- return jsonify({"status": "error", "message": "Ошибка авторизации: initData отсутствует."}), 400
+ return jsonify({"status": "error", "message": "Missing initData"}), 400
- user_id = get_user_id_from_init_data(init_data_str)
- if not user_id:
- _, _, verification_message = verify_telegram_data(init_data_str) # Get specific reason
- return jsonify({"status": "error", "message": f"Ошибка авторизации: {verification_message}"}), 403
+ user_info, message = authenticate_and_get_user(init_data_str)
+ if not user_info:
+ return jsonify({"status": "error", "message": message}), 403
+
+ user_id = user_info['id']
+ user_id_str = str(user_id)
- files = request.files.getlist('files')
- if not files:
- return jsonify({"status": "error", "message": "Файлы для загрузки не найдены."}), 400
+ uploaded_files = request.files.getlist('files') # Key matches JS FormData
+ if not uploaded_files or len(uploaded_files) == 0:
+ return jsonify({"status": "error", "message": "No files selected for upload."}), 400
+ if len(uploaded_files) > MAX_UPLOAD_FILES:
+ return jsonify({"status": "error", "message": f"Cannot upload more than {MAX_UPLOAD_FILES} files at once."}), 400
- if len(files) > MAX_FILES_PER_UPLOAD:
- return jsonify({"status": "error", "message": f"Слишком много файлов. Максимум {MAX_FILES_PER_UPLOAD} за раз."}), 400
+ api = get_hf_api(write=True)
+ if not api:
+ return jsonify({"status": "error", "message": "Server error: Cannot connect to storage."}), 500
- successful_uploads = 0
+ successful_uploads_metadata = []
errors = []
- user_folder = f"{HF_UPLOAD_FOLDER}/{user_id}"
- for file in files:
- if file and file.filename:
- filename = secure_filename(file.filename)
- path_in_repo = f"{user_folder}/{filename}"
- try:
- logging.info(f"Attempting to upload '{filename}' for user {user_id} to {REPO_ID}/{path_in_repo}")
- # Pass file object directly
- hf_api.upload_file(
- path_or_fileobj=file.stream, # Use file.stream
- path_in_repo=path_in_repo,
- repo_id=REPO_ID,
- repo_type="dataset",
- token=HF_TOKEN_WRITE,
- commit_message=f"Upload {filename} by user {user_id}"
- )
- successful_uploads += 1
- logging.info(f"Successfully uploaded '{filename}' for user {user_id}")
- except HfHubHTTPError as e:
- logging.error(f"HTTP error uploading {filename} for user {user_id}: {e}")
- # Check for specific errors like 413 Payload Too Large if needed
- errors.append(f"{filename}: Ошибка сервера ({e.response.status_code if e.response else 'N/A'})")
- except Exception as e:
- logging.exception(f"Error uploading {filename} for user {user_id}")
- errors.append(f"{filename}: {e}")
- else:
- errors.append("Получен пустой файл")
+ for file_storage in uploaded_files:
+ filename = file_storage.filename
+ if not filename:
+ errors.append("Received a file without a name.")
+ continue
- if successful_uploads > 0 and not errors:
- return jsonify({"status": "ok", "message": f"Загружено {successful_uploads} файл(ов)."}), 200
- elif successful_uploads > 0 and errors:
- error_details = "; ".join(errors)
- return jsonify({"status": "partial", "message": f"Загружено {successful_uploads} файл(ов). Ошибки: {error_details}"}), 207 # Multi-Status
- else:
- error_details = "; ".join(errors)
- return jsonify({"status": "error", "message": f"Не удалось загрузить файлы. Ошибки: {error_details}"}), 500
+ path_in_repo = f"{HF_UPLOAD_FOLDER}/{user_id_str}/{filename}"
+ file_content = file_storage.read() # Read content into memory
+ file_size = len(file_content)
+ content_type, _ = mimetypes.guess_type(filename)
+ try:
+ logging.info(f"Uploading '{filename}' for user {user_id} to {path_in_repo}...")
+ # Use upload_file with path_or_fileobj=BytesIO(file_content)
+ file_obj = io.BytesIO(file_content)
+ api.upload_file(
+ path_or_fileobj=file_obj,
+ path_in_repo=path_in_repo,
+ repo_id=REPO_ID,
+ repo_type="dataset",
+ commit_message=f"User {user_id} uploaded {filename}"
+ # Consider adding run_as_future=True for concurrency if needed, but handle results
+ )
+ logging.info(f"Successfully uploaded '{filename}' for user {user_id}.")
+ now = time.time()
+ successful_uploads_metadata.append({
+ "filename": filename,
+ "hf_path": path_in_repo,
+ "uploaded_at_ts": now,
+ "uploaded_at_str": datetime.fromtimestamp(now).strftime('%Y-%m-%d %H:%M:%S'),
+ "size": file_size,
+ "content_type": content_type
+ })
+ except Exception as e:
+ logging.error(f"Failed to upload '{filename}' for user {user_id}: {e}", exc_info=True)
+ errors.append(f"Ошибка загрузки {filename}: {e}")
-@app.route('/list_files', methods=['POST'])
-def list_user_files():
- if not HF_TOKEN_READ:
- return jsonify({"status": "error", "message": "Функция просмотра файлов недоступна (ошибка конфигурации сервера)."}), 503
+ # Update metadata only if there were successful uploads
+ if successful_uploads_metadata:
+ if not update_user_file_metadata(user_id, successful_uploads_metadata):
+ # Add metadata update errors to the list shown to the user
+ errors.append("Ошибка обновления списка файлов после загрузки.")
- req_data = request.get_json()
- init_data_str = req_data.get('initData')
+ if not errors:
+ return jsonify({"status": "ok", "message": f"Загружено {len(successful_uploads_metadata)} файл(ов)."}), 200
+ else:
+ # Return partial success/error message
+ return jsonify({
+ "status": "error" if not successful_uploads_metadata else "partial_success",
+ "message": f"Загружено {len(successful_uploads_metadata)} из {len(uploaded_files)}. Ошибки: {'; '.join(errors)}",
+ "uploaded_files": [f['filename'] for f in successful_uploads_metadata],
+ "errors": errors
+ }), 207 # Multi-Status or choose appropriate code
+
+
+@app.route('/download/', methods=['GET'])
+def download_file(filename):
+ init_data_str = request.args.get('initData')
if not init_data_str:
- return jsonify({"status": "error", "message": "Ошибка авторизации: initData отсутствует."}), 400
+ return "Authentication required.", 401
- user_id = get_user_id_from_init_data(init_data_str)
- if not user_id:
- _, _, verification_message = verify_telegram_data(init_data_str)
- return jsonify({"status": "error", "message": f"Ошибка авторизации: {verification_message}"}), 403
+ user_info, message = authenticate_and_get_user(init_data_str)
+ if not user_info:
+ return f"Access denied: {message}", 403
- user_folder = f"{HF_UPLOAD_FOLDER}/{user_id}"
- files_list = []
+ user_id = user_info['id']
+ user_id_str = str(user_id)
- try:
- repo_files = list_repo_files(repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_READ, path_in_repo=user_folder)
-
- for file_path in repo_files:
- # Ensure we only list files directly in the user's folder, not subfolders if any were created manually
- if file_path.startswith(user_folder + "/") and file_path.count('/') == user_folder.count('/') + 1:
- filename = os.path.basename(file_path)
- file_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{quote(file_path)}"
- files_list.append({
- "name": filename,
- "url": file_url,
- "path": file_path # Keep internal path if needed later
- })
- return jsonify({"status": "ok", "files": files_list})
+ # Check if user actually owns this file according to metadata
+ with _data_lock:
+ user_data = metadata_cache.get(user_id_str, {})
+ user_files = user_data.get('files', [])
+ file_metadata = next((f for f in user_files if f['filename'] == filename), None)
+
+ if not file_metadata:
+ logging.warning(f"User {user_id} attempted to download unlisted/unowned file: {filename}")
+ return "File not found or access denied.", 404
+
+ api = get_hf_api(write=False)
+ if not api:
+ return "Server error: Cannot connect to storage.", 500
+ path_in_repo = file_metadata.get('hf_path', f"{HF_UPLOAD_FOLDER}/{user_id_str}/{filename}")
+
+ try:
+ logging.info(f"User {user_id} requesting download of {path_in_repo}")
+ # Download the file from HF Hub to the server's cache
+ local_file_path = hf_hub_download(
+ repo_id=REPO_ID,
+ filename=path_in_repo,
+ repo_type="dataset",
+ token=api.token,
+ # local_dir=".", # Let it use the default HF cache
+ # local_dir_use_symlinks=False,
+ force_download=False, # Use cache if available
+ etag_timeout=10
+ )
+ logging.info(f"File {path_in_repo} downloaded to cache: {local_file_path}")
+
+ # Send the file from the cache path
+ # Guess mimetype again for safety, or use stored one
+ content_type = file_metadata.get('content_type') or mimetypes.guess_type(filename)[0] or 'application/octet-stream'
+
+ return send_file(
+ local_file_path,
+ mimetype=content_type,
+ as_attachment=False, # Try to display inline first for media
+ download_name=filename # Original filename for download prompt
+ )
+
+ except EntryNotFoundError:
+ logging.error(f"File not found on Hugging Face: {path_in_repo}")
+ return "File not found on storage.", 404
except RepositoryNotFoundError:
- logging.warning(f"Repository {REPO_ID} or user folder {user_folder} not found.")
- # This is expected if the user hasn't uploaded anything yet, return empty list
- return jsonify({"status": "ok", "files": []})
+ logging.error(f"Repository not found: {REPO_ID}")
+ return "Storage repository not found.", 500
except Exception as e:
- logging.exception(f"Error listing files for user {user_id}")
- return jsonify({"status": "error", "message": f"Не удалось получить список файлов: {e}"}), 500
+ logging.error(f"Error downloading file {path_in_repo} for user {user_id}: {e}", exc_info=True)
+ return "Server error during download.", 500
+
+
+# --- Admin Routes ---
+# WARNING: These routes are unprotected! Add proper authentication.
+
+@app.route('/admin')
+def admin_panel():
+ current_data = load_local_metadata() # Load latest from cache/local file
+ return render_template_string(ADMIN_TEMPLATE, users=current_data)
+
+@app.route('/admin/user/')
+def admin_user_files(user_id):
+ current_data = load_local_metadata()
+ user_data = current_data.get(str(user_id))
+ if not user_data:
+ return "User not found", 404
+
+ user_info = user_data.get("user_info", {"id": user_id}) # Basic info for display
+ files = user_data.get("files", [])
+
+ return render_template_string(ADMIN_USER_FILES_TEMPLATE,
+ user_id=user_id,
+ user_info=user_info,
+ files=files)
+
+@app.route('/admin/download//', methods=['GET'])
+def admin_download_file(user_id, filename):
+ # WARNING: Add admin authentication check here!
+ user_id_str = str(user_id)
+ logging.info(f"Admin request to download file '{filename}' for user {user_id}")
+
+ api = get_hf_api(write=False)
+ if not api:
+ return "Server error: Cannot connect to storage.", 500
+
+ # Find the file path from metadata if possible for accuracy
+ path_in_repo = f"{HF_UPLOAD_FOLDER}/{user_id_str}/{filename}" # Default assumption
+ with _data_lock:
+ user_data = metadata_cache.get(user_id_str, {})
+ user_files = user_data.get('files', [])
+ file_metadata = next((f for f in user_files if f['filename'] == filename), None)
+ if file_metadata and 'hf_path' in file_metadata:
+ path_in_repo = file_metadata['hf_path']
+ try:
+ local_file_path = hf_hub_download(
+ repo_id=REPO_ID,
+ filename=path_in_repo,
+ repo_type="dataset",
+ token=api.token,
+ force_download=False,
+ etag_timeout=10
+ )
+ logging.info(f"Admin download: File {path_in_repo} cached at {local_file_path}")
+
+ content_type = mimetypes.guess_type(filename)[0] or 'application/octet-stream'
+ if file_metadata and 'content_type' in file_metadata:
+ content_type = file_metadata['content_type'] or content_type
+
+ return send_file(
+ local_file_path,
+ mimetype=content_type,
+ as_attachment=True, # Force download for admin
+ download_name=filename
+ )
+ except EntryNotFoundError:
+ logging.error(f"Admin download: File not found on Hugging Face: {path_in_repo}")
+ return "File not found on storage.", 404
+ except Exception as e:
+ logging.error(f"Admin download: Error for file {path_in_repo}: {e}", exc_info=True)
+ return "Server error during download.", 500
+
+@app.route('/admin/download_metadata', methods=['POST'])
+def admin_trigger_download_metadata():
+ # WARNING: Unprotected endpoint
+ success = download_metadata_from_hf()
+ if success:
+ return jsonify({"status": "ok", "message": "Скачивание data.json с Hugging Face завершено. Обновите страницу."})
+ else:
+ return jsonify({"status": "error", "message": "Ошибка скачивания data.json. Проверьте логи."}), 500
+
+# Removed admin upload metadata trigger - upload happens automatically on save
# --- App Initialization ---
if __name__ == '__main__':
print("---")
print("--- ZEUS CLOUD MINI APP SERVER ---")
print("---")
- print(f"Flask server starting on http://{HOST}:{PORT}")
+ print(f"Starting Flask server on http://{HOST}:{PORT}")
print(f"Using Bot Token ID: {BOT_TOKEN.split(':')[0]}")
+ print(f"Metadata file (local): {DATA_FILE}")
print(f"Hugging Face Repo: {REPO_ID}")
- print(f"HF Upload Folder: {HF_UPLOAD_FOLDER}")
+ print(f"HF Metadata Path: {HF_DATA_FILE_PATH}")
+ print(f"HF Upload Folder: {HF_UPLOAD_FOLDER}//")
+
if not HF_TOKEN_READ or not HF_TOKEN_WRITE:
print("---")
print("--- WARNING: HUGGING FACE TOKEN(S) NOT SET ---")
- print("--- File upload/listing WILL NOT WORK. Set HF_TOKEN_READ and HF_TOKEN_WRITE environment variables.")
+ print("--- Storage functionality requires HF_TOKEN_READ and HF_TOKEN_WRITE env vars.")
print("---")
else:
print("--- Hugging Face tokens found.")
+ print("--- Attempting initial metadata download from Hugging Face...")
+ download_metadata_from_hf() # Attempt to get latest metadata on startup
+
+ # Load initial data from local file (might have been updated by download)
+ load_local_metadata()
+ print(f"--- Initial metadata cache loaded with {len(metadata_cache)} user(s).")
+ print("---")
+ print("--- SECURITY WARNING ---")
+ print("--- The /admin routes are NOT protected by authentication.")
+ print("--- Implement proper auth before any production deployment.")
+ print("---")
print("--- Server Ready ---")
- # Use a production server like Waitress or Gunicorn instead of app.run() for deployment
+
+ # Use Waitress or Gunicorn for production
# from waitress import serve
# serve(app, host=HOST, port=PORT)
- app.run(host=HOST, port=PORT, debug=False)
\ No newline at end of file
+ app.run(host=HOST, port=PORT, debug=False, threaded=True) # threaded=True useful for async tasks like HF uploads
\ No newline at end of file