from flask import Flask, render_template_string, request, redirect, url_for, session, send_file, flash, jsonify, g import json import os import logging import threading import time from datetime import datetime from huggingface_hub import HfApi, hf_hub_download from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError from werkzeug.utils import secure_filename from dotenv import load_dotenv import requests import uuid import copy import secrets load_dotenv() app = Flask(__name__) app.secret_key = os.getenv("FLASK_SECRET_KEY", 'your_unique_secret_key_korea_global_12345') app.config.update( SESSION_COOKIE_SAMESITE="None", SESSION_COOKIE_SECURE=True, SESSION_COOKIE_HTTPONLY=True, PREFERRED_URL_SCHEME="https" ) DATA_FILE = 'data_kglobal.json' USERS_FILE = 'users_kglobal.json' SYNC_FILES = [DATA_FILE, USERS_FILE] REPO_ID = "Kgshop/cosmeticshop" HF_TOKEN_WRITE = os.getenv("HF_TOKEN") HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") STORE_ADDRESS = "Рынок Дордой, Джунхай, 4 проход , 390 контейнер " CURRENCY_CODE = 'USD' CURRENCY_NAME = 'Доллар США ($)' DOWNLOAD_RETRIES = 3 DOWNLOAD_DELAY = 5 UPLOAD_DELAY = 2 logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') data_lock = threading.Lock() users_lock = threading.Lock() app_data = {'products': [], 'categories': [], 'orders': {}} app_users = {} def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY): if not HF_TOKEN_READ and not HF_TOKEN_WRITE: logging.warning("HF_TOKEN_READ/HF_TOKEN_WRITE not set. Download might fail for private repos.") token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE files_to_download = [specific_file] if specific_file else SYNC_FILES logging.info(f"Attempting download for {files_to_download} from {REPO_ID}...") all_successful = True for file_name in files_to_download: success = False local_file_path = os.path.join(".", file_name) for attempt in range(retries + 1): try: logging.info(f"Downloading {file_name} (Attempt {attempt + 1}/{retries + 1})...") hf_hub_download( repo_id=REPO_ID, filename=file_name, repo_type="dataset", token=token_to_use, local_dir=".", local_dir_use_symlinks=False, force_download=True, resume_download=False, cache_dir=None ) logging.info(f"Successfully downloaded and overwrote {file_name}.") success = True break except RepositoryNotFoundError: logging.error(f"Repository {REPO_ID} not found. Download cancelled for all files.") return False except HfHubHTTPError as e: if e.response.status_code == 404: logging.warning(f"File {file_name} not found in repo {REPO_ID} (404). Checking local file.") if not os.path.exists(local_file_path): logging.warning(f"Local file {file_name} also not found. Creating an empty default.") try: default_content = {} if file_name == DATA_FILE: default_content = {'products': [], 'categories': [], 'orders': {}} elif file_name == USERS_FILE: default_content = {} if default_content is not None: with open(local_file_path, 'w', encoding='utf-8') as f: json.dump(default_content, f, ensure_ascii=False, indent=4) logging.info(f"Created empty local file {file_name}.") except Exception as create_e: logging.error(f"Failed to create empty local file {file_name}: {create_e}") else: logging.info(f"File {file_name} not found on HF, but exists locally. Using local version.") success = True break else: logging.error(f"HTTP error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...") except requests.exceptions.RequestException as e: logging.error(f"Network error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...") except Exception as e: logging.error(f"Unexpected error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...", exc_info=True) if attempt < retries: time.sleep(delay) if not success: logging.error(f"Failed to download {file_name} after {retries + 1} attempts.") all_successful = False logging.info(f"Download process finished. Overall success: {all_successful}") return all_successful def _load_from_file(file_path, default_value, lock): try: with lock: with open(file_path, 'r', encoding='utf-8') as file: content = json.load(file) logging.info(f"Data loaded successfully from {file_path}") if file_path == DATA_FILE: if not isinstance(content, dict): raise ValueError("Data file is not a dictionary") if 'products' not in content: content['products'] = [] if 'categories' not in content: content['categories'] = [] if 'orders' not in content: content['orders'] = {} elif file_path == USERS_FILE: if not isinstance(content, dict): raise ValueError("Users file is not a dictionary") return content except (FileNotFoundError, json.JSONDecodeError, ValueError) as e: logging.error(f"Error loading local file {file_path}: {e}. Returning default.") if not os.path.exists(file_path): try: with lock: with open(file_path, 'w', encoding='utf-8') as f: json.dump(default_value, f, ensure_ascii=False, indent=4) logging.info(f"Created default local file {file_path}.") except Exception as create_e: logging.error(f"Failed to create default local file {file_path}: {create_e}") return copy.deepcopy(default_value) def load_initial_data(): global app_data, app_users logging.info("Attempting initial data load...") download_db_from_hf() app_data = _load_from_file(DATA_FILE, {'products': [], 'categories': [], 'orders': {}}, data_lock) app_users = _load_from_file(USERS_FILE, {}, users_lock) logging.info(f"Initial load complete. Products: {len(app_data.get('products',[]))}, Categories: {len(app_data.get('categories',[]))}, Orders: {len(app_data.get('orders',{}))}, Users: {len(app_users)}") def get_data(): with data_lock: return copy.deepcopy(app_data) def save_data(new_data): global app_data try: if not isinstance(new_data, dict): logging.error("Attempted to save invalid data structure (not a dict). Aborting save.") return False if 'products' not in new_data: new_data['products'] = [] if 'categories' not in new_data: new_data['categories'] = [] if 'orders' not in new_data: new_data['orders'] = {} with data_lock: app_data = copy.deepcopy(new_data) with open(DATA_FILE, 'w', encoding='utf-8') as file: json.dump(app_data, file, ensure_ascii=False, indent=4) logging.info(f"Data successfully saved to {DATA_FILE} and memory cache updated.") upload_db_to_hf(DATA_FILE) return True except Exception as e: logging.error(f"Error saving data to {DATA_FILE}: {e}", exc_info=True) return False def get_users(): with users_lock: return copy.deepcopy(app_users) def save_users(new_users): global app_users try: if not isinstance(new_users, dict): logging.error("Attempted to save invalid users structure (not a dict). Aborting save.") return False with users_lock: app_users = copy.deepcopy(new_users) with open(USERS_FILE, 'w', encoding='utf-8') as file: json.dump(app_users, file, ensure_ascii=False, indent=4) logging.info(f"User data successfully saved to {USERS_FILE} and memory cache updated.") upload_db_to_hf(USERS_FILE) return True except Exception as e: logging.error(f"Error saving user data to {USERS_FILE}: {e}", exc_info=True) return False def upload_db_to_hf(specific_file=None): if not HF_TOKEN_WRITE: logging.warning("HF_TOKEN (for writing) not set. Skipping upload to Hugging Face.") return False try: api = HfApi() files_to_upload = [specific_file] if specific_file else SYNC_FILES logging.info(f"Starting upload of {files_to_upload} to HF repo {REPO_ID}...") all_successful = True for file_name in files_to_upload: if os.path.exists(file_name): try: api.upload_file( path_or_fileobj=file_name, path_in_repo=file_name, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, commit_message=f"Sync {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" ) logging.info(f"File {file_name} successfully uploaded to Hugging Face.") time.sleep(UPLOAD_DELAY) except Exception as e: logging.error(f"Error uploading file {file_name} to Hugging Face: {e}") all_successful = False else: logging.warning(f"File {file_name} not found locally, skipping upload.") all_successful = False logging.info(f"Finished uploading files to HF. Overall success: {all_successful}") return all_successful except Exception as e: logging.error(f"General error during Hugging Face upload initialization or process: {e}", exc_info=True) return False CATALOG_TEMPLATE = '''
{% endif %}
{{ product.get('description', '')[:50] }}{% if product.get('description', '')|length > 50 %}...{% endif %}
Категория: {{ product.get('category', 'Без категории') }}
{% if is_authenticated %}Цена: {{ "%.2f"|format(product['price']) }} {{ currency_code }}
{% else %}Цена: Доступна после входа
{% endif %}Описание:
{{ product.get('description', 'Описание отсутствует.')|replace('\\n', '
')|safe }}
Доступные цвета/варианты: {{ valid_colors|join(', ') }}
{% endif %}Резервное копирование происходит автоматически после каждого сохранения изменений. Используйте эти кнопки для немедленной синхронизации.
Сохранение данных (товары, пользователи, категории) происходит локально, синхронизация с датацентром - автоматически после сохранения или принудительно.
Категорий пока нет.
{% endif %}Логин: {{ user_data.get('login', 'N/A') }}
Токен: {{ token }}
Имя: {{ user_data.get('first_name', 'N/A') }} {{ user_data.get('last_name', '') }}
Телефон: {{ user_data.get('phone', 'N/A') }}
Локация: {{ user_data.get('city', 'N/A') }}, {{ user_data.get('country', 'N/A') }}
Пользователей пока нет.
{% endif %}Категория: {{ product.get('category', 'Без категории') }}
Цена: {{ "%.2f"|format(product['price']) }} {{ currency_code }}
Описание: {{ product.get('description', 'N/A')[:150] }}{% if product.get('description', '')|length > 150 %}...{% endif %}
{% set colors = product.get('colors', []) %} {% set valid_colors = colors|select('ne', '')|list %}Цвета/Вар-ты: {{ valid_colors|join(', ') if valid_colors else 'Нет' }}
{% if product.get('photos') and product['photos']|length > 1 %}(Всего фото: {{ product['photos']|length }})
{% endif %}Товаров пока нет.
{% endif %}Общая сумма товаров: {{ "%.2f"|format(order.total_price) }} {{ currency_code }}
ИТОГО К ОПЛАТЕ: {{ "%.2f"|format(order.total_price) }} {{ currency_code }}
Логин (для админ-панели): {{ order.user_info.get('login', 'Не указан') }}
Имя: {{ order.user_info.get('first_name', 'Не указан') }} {{ order.user_info.get('last_name', '') }}
Телефон: {{ order.user_info.get('phone', 'Не указан') }}
Страна: {{ order.user_info.get('country', 'Не указана') }}
Город: {{ order.user_info.get('city', 'Не указан') }}
Пожалуйста, свяжитесь с нами по WhatsApp для подтверждения и уточнения деталей.
Этот заказ был оформлен без входа в систему или данные пользователя не сохранились.
Пожалуйста, свяжитесь с нами по WhatsApp для подтверждения и уточнения деталей.
Заказ с таким ID не найден.
← Вернуться в каталог {% endif %}Вход выполнен успешно. Перенаправление в каталог...
''' return login_response_html else: logging.warning(f"Failed login attempt with token.") error_message = "Неверный токен." return render_template_string(LOGIN_TEMPLATE, error=error_message), 401 return render_template_string(LOGIN_TEMPLATE, error=None) @app.route('/auto_login', methods=['POST']) def auto_login(): data = request.get_json() if not data or 'token' not in data: logging.warning("Auto_login request missing data or token.") return jsonify({"error": "Invalid request"}), 400 token_attempt = data.get('token', '').strip() if not token_attempt: logging.warning("Attempted auto_login with empty token.") return jsonify({"error": "Token not provided"}), 400 current_users = get_users() user_data = current_users.get(token_attempt) if user_data: session['user_token'] = token_attempt session['user'] = user_data.get('login', token_attempt) session['user_info'] = user_data session.modified = True logging.info(f"Auto-login successful with token.") return jsonify({"message": "OK"}), 200 else: logging.warning(f"Failed auto-login attempt with invalid or non-existent token.") return jsonify({"error": "Auto-login failed"}), 401 @app.route('/logout') def logout(): logged_out_user = session.get('user', 'Anonymous') session.pop('user_token', None) session.pop('user', None) session.pop('user_info', None) session.modified = True logging.info(f"User '{logged_out_user}' logged out.") logout_response_html = f'''Выход выполнен. Перенаправление на главную страницу...
''' return logout_response_html @app.route('/create_order', methods=['POST']) def create_order(): if 'user_token' not in session: return jsonify({"error": "Пожалуйста, войдите в систему по токену для создания заказа."}), 401 order_data = request.get_json() if not order_data or 'cart' not in order_data or not isinstance(order_data['cart'], list) or not order_data['cart']: logging.warning("Create order request missing cart data or cart is empty/invalid.") return jsonify({"error": "Корзина пуста или не передана в верном формате."}), 400 cart_items = order_data['cart'] total_price = 0 processed_cart = [] data_cache = get_data() products_cache = {p['name']: p for p in data_cache.get('products', [])} for item in cart_items: if not isinstance(item, dict) or not all(k in item for k in ('id', 'name', 'quantity', 'color')): logging.error(f"Invalid cart item structure received: {item}") return jsonify({"error": "Неверный формат товара в корзине."}), 400 try: quantity = int(item['quantity']) product_name = item['name'] if product_name not in products_cache: logging.error(f"Product '{product_name}' from cart not found in server data.") return jsonify({"error": f"Товар '{product_name}' не найден."}), 400 price = float(products_cache[product_name]['price']) photo = products_cache[product_name].get('photos', [None])[0] if price < 0 or quantity <= 0: raise ValueError("Invalid price or quantity") total_price += price * quantity processed_cart.append({ "name": product_name, "price": price, "quantity": quantity, "color": item.get('color', 'N/A'), "photo": photo, "photo_url": f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/photos/{photo}" if photo else "https://via.placeholder.com/60x60.png?text=N/A" }) except (ValueError, TypeError, KeyError) as e: logging.error(f"Invalid data in cart item: {item}. Error: {e}") return jsonify({"error": "Неверные данные (цена, количество или товар) в корзине."}), 400 order_id = f"{datetime.now().strftime('%y%m%d%H%M%S')}-{uuid.uuid4().hex[:4]}" order_timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') user_info_for_order = session.get('user_info', {}) user_info_for_order_copy = { k: v for k, v in user_info_for_order.items() if v } new_order = { "id": order_id, "created_at": order_timestamp, "cart": processed_cart, "total_price": round(total_price, 2), "user_info": user_info_for_order_copy, "status": "new" } current_data = get_data() if 'orders' not in current_data or not isinstance(current_data.get('orders'), dict): current_data['orders'] = {} current_data['orders'][order_id] = new_order if save_data(current_data): logging.info(f"Order {order_id} created successfully. User token: {session.get('user_token', 'Unknown')}") return jsonify({"order_id": order_id}), 201 else: logging.error(f"Failed to save order {order_id} to file/cache.") return jsonify({"error": "Ошибка сервера при сохранении заказа."}), 500 @app.route('/order/{new_token}. Сохраните его! (будет сохранен после синхронизации)", 'success')
elif action == 'delete_user':
token_to_delete = request.form.get('token')
if token_to_delete and token_to_delete in users_copy:
deleted_login = users_copy[token_to_delete].get('login', token_to_delete)
del users_copy[token_to_delete]
save_needed_users = True
logging.info(f"User with token '{token_to_delete}' (login '{deleted_login}') staged for deletion.")
flash(f"Пользователь '{deleted_login}' будет удален после сохранения.", 'success')
elif token_to_delete:
logging.warning(f"Attempted to delete non-existent user with token: {token_to_delete}")
flash(f"Пользователь с токеном '{token_to_delete}' не найден.", 'error')
else:
flash("Не указан токен пользователя для удаления.", 'error')
else:
logging.warning(f"Received unknown admin action: {action}")
flash(f"Неизвестное действие: {action}", 'warning')
final_save_success = True
if save_needed_data:
data_copy['products'].sort(key=lambda p: p.get('name', '').lower())
if not save_data(data_copy):
flash("Ошибка при сохранении основных данных (товары/категории).", 'error')
final_save_success = False
if save_needed_users:
if not save_users(users_copy):
flash("Ошибка при сохранении данных пользователей.", 'error')
final_save_success = False
if final_save_success and (save_needed_data or save_needed_users):
flash("Все изменения успешно сохранены локально.", 'success')
except Exception as e:
logging.error(f"Error processing admin action '{action}': {e}", exc_info=True)
flash(f"Произошла внутренняя ошибка при обработке действия '{action}'. Подробности в логе сервера.", 'error')
return redirect(url_for('admin'))
display_data = get_data()
display_users = get_users()
display_products = sorted(display_data.get('products', []), key=lambda p: (not p.get('is_top', False), p.get('name', '').lower()))
display_categories = sorted(display_data.get('categories', []))
display_users_sorted_list = sorted(display_users.items(), key=lambda item: item[1].get('login', item[0]).lower())
display_users_sorted_dict = dict(display_users_sorted_list)
return render_template_string(
ADMIN_TEMPLATE,
products=display_products,
categories=display_categories,
users=display_users_sorted_dict,
repo_id=REPO_ID,
currency_code=CURRENCY_CODE
)
@app.route('/force_upload', methods=['POST'])
def force_upload():
logging.info("Forcing upload to Hugging Face via admin request...")
try:
success = upload_db_to_hf()
if success:
flash("Данные успешно загружены на Hugging Face.", 'success')
else:
flash("Во время загрузки на Hugging Face произошли ошибки (не все файлы могли быть загружены). Проверьте логи.", 'warning')
except Exception as e:
logging.error(f"Critical error during forced upload: {e}", exc_info=True)
flash(f"Critical error during forced upload to Hugging Face: {e}", 'error')
return redirect(url_for('admin'))
@app.route('/force_download', methods=['POST'])
def force_download():
logging.info("Forcing download from Hugging Face via admin request...")
try:
if download_db_from_hf():
load_initial_data()
flash("Данные успешно скачаны с Hugging Face и загружены в память. Локальные файлы обновлены.", 'success')
else:
flash("Не удалось скачать данные с Hugging Face после нескольких попыток. Используются текущие локальные данные. Проверьте логи.", 'error')
except Exception as e:
logging.error(f"Critical error during forced download: {e}", exc_info=True)
flash(f"Critical error during forced download from Hugging Face: {e}", 'error')
return redirect(url_for('admin'))
if __name__ == '__main__':
logging.info("Application starting up...")
logging.info("Performing initial data load from local files or HF...")
load_initial_data()
logging.info("Initial data load complete.")
port = int(os.environ.get('PORT', 7860))
logging.info(f"Starting Flask app server on host 0.0.0.0 and port {port}. Ensure HTTPS is configured for session cookies with SameSite=None to work correctly in iframes.")
try:
from waitress import serve
serve(app, host='0.0.0.0', port=port, threads=8)
except ImportError:
logging.warning("Waitress not found. Falling back to Flask development server. NOT FOR PRODUCTION USE.")
logging.warning("Install waitress for a production-ready server: pip install waitress")
app.run(debug=False, host='0.0.0.0', port=port)