diff --git "a/app.py" "b/app.py"
new file mode 100644--- /dev/null
+++ "b/app.py"
@@ -0,0 +1,1830 @@
+
+
+from flask import Flask, render_template_string, request, redirect, url_for, session, send_file
+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
+from werkzeug.utils import secure_filename
+# Импортируем dotenv для загрузки переменных окружения из .env файла
+from dotenv import load_dotenv
+
+# Загружаем переменные окружения из файла .env (если он есть)
+load_dotenv()
+
+app = Flask(__name__)
+app.secret_key = 'your_unique_secret_key_soola_cosmetics_67890' # Новый уникальный секретный ключ
+DATA_FILE = 'data_soola.json'
+USERS_FILE = 'users_soola.json'
+
+# Список файлов для синхронизации
+SYNC_FILES = [DATA_FILE, USERS_FILE]
+
+# Настройки Hugging Face
+# Убедитесь, что REPO_ID соответствует вашему репозиторию на Hugging Face
+REPO_ID = "Kgshop/Soola" # Замените на ваш ID, если он другой
+HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
+HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") # Может быть тем же, что и HF_TOKEN
+
+# Адрес магазина
+STORE_ADDRESS = "Рынок Дордой, Джунхай, терминал, 38" # Единый адрес
+
+# Валюта (только KGS)
+CURRENCY_CODE = 'KGS'
+CURRENCY_NAME = 'Кыргызский сом (с)'
+
+# Настройка логирования
+# Уровни: DEBUG, INFO, WARNING, ERROR, CRITICAL
+logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
+
+# --- Функции работы с данными и пользователями ---
+
+def load_data():
+ """Загрузка данных о товарах и категориях."""
+ try:
+ # Попытка скачать актуальные данные перед чтением локальных
+ download_db_from_hf()
+ with open(DATA_FILE, 'r', encoding='utf-8') as file:
+ data = json.load(file)
+ logging.info(f"Данные успешно загружены из {DATA_FILE}")
+ # Проверка базовой структуры
+ if not isinstance(data, dict):
+ logging.warning(f"{DATA_FILE} не является словарем. Инициализация пустой структурой.")
+ return {'products': [], 'categories': []}
+ if 'products' not in data:
+ data['products'] = []
+ if 'categories' not in data:
+ data['categories'] = []
+ return data
+ except FileNotFoundError:
+ logging.warning(f"Локальный файл {DATA_FILE} не найден. Попытка скачать с HF.")
+ try:
+ # download_db_from_hf() # Уже вызывали выше, избегаем повторного вызова при первой ошибке
+ # Если скачивание не удалось выше, пытаемся просто создать пустые файлы
+ if not os.path.exists(DATA_FILE):
+ with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'products': [], 'categories': []}, f)
+ logging.info(f"Создан пустой файл {DATA_FILE}")
+ return {'products': [], 'categories': []}
+ else: # Если файл появился после download_db_from_hf
+ with open(DATA_FILE, 'r', encoding='utf-8') as file:
+ data = json.load(file)
+ logging.info(f"Данные успешно загружены из {DATA_FILE} после попытки скачивания.")
+ if not isinstance(data, dict): return {'products': [], 'categories': []}
+ if 'products' not in data: data['products'] = []
+ if 'categories' not in data: data['categories'] = []
+ return data
+ except (FileNotFoundError, RepositoryNotFoundError) as e: # Ловим ошибку репозитория при скачивании
+ logging.warning(f"Файл {DATA_FILE} не найден локально и ошибка при доступе к репозиторию HF ({e}). Создание пустой структуры.")
+ if not os.path.exists(DATA_FILE):
+ with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'products': [], 'categories': []}, f)
+ return {'products': [], 'categories': []}
+ except json.JSONDecodeError:
+ logging.error(f"Ошибка декодирования JSON в {DATA_FILE} после попытки скачивания.")
+ return {'products': [], 'categories': []}
+ except Exception as e:
+ logging.error(f"Неизвестная ошибка при загрузке данных после попытки скачивания: {e}")
+ return {'products': [], 'categories': []}
+ except json.JSONDecodeError:
+ logging.error(f"Ошибка декодирования JSON в локальном {DATA_FILE}. Файл может быть поврежден. Возврат пустой структуры.")
+ # Можно добавить логику восстановления из бэкапа или HF, если нужно
+ return {'products': [], 'categories': []}
+ except Exception as e:
+ logging.error(f"Неизвестная ошибка при загрузке данных ({DATA_FILE}): {e}", exc_info=True)
+ return {'products': [], 'categories': []}
+
+
+def save_data(data):
+ """Сохранение данных о товарах и категориях."""
+ try:
+ with open(DATA_FILE, 'w', encoding='utf-8') as file:
+ json.dump(data, file, ensure_ascii=False, indent=4)
+ logging.info(f"Данные успешно сохранены в {DATA_FILE}")
+ # Загрузка на HF после сохранения (можно сделать опциональной)
+ upload_db_to_hf(specific_file=DATA_FILE)
+ except Exception as e:
+ logging.error(f"Ошибка при сохранении данных в {DATA_FILE}: {e}", exc_info=True)
+ # В реальном приложении можно добавить механизм повторной попытки или уведомления
+ # raise # Перевыброс исключения может остановить приложение, если не обработан выше
+
+def load_users():
+ """Загрузка данных пользователей."""
+ try:
+ # Опционально: скачать файл пользователей перед чтением
+ # download_db_from_hf(specific_file=USERS_FILE) # Раскомментировать, если нужно всегда свежие пользователи
+ with open(USERS_FILE, 'r', encoding='utf-8') as file:
+ users = json.load(file)
+ logging.info(f"Данные пользователей успешно загружены из {USERS_FILE}")
+ return users if isinstance(users, dict) else {}
+ except FileNotFoundError:
+ logging.warning(f"Локальный файл {USERS_FILE} не найден. Попытка скачать с HF.")
+ try:
+ download_db_from_hf(specific_file=USERS_FILE) # Явный вызов для файла пользователей
+ # Повторная попытка чтения после скачивания
+ with open(USERS_FILE, 'r', encoding='utf-8') as file:
+ users = json.load(file)
+ logging.info(f"Данные пользователей загружены из {USERS_FILE} после скачивания.")
+ return users if isinstance(users, dict) else {}
+ except (FileNotFoundError, RepositoryNotFoundError):
+ logging.warning(f"Файл {USERS_FILE} не найден локально и в репозитории HF. Создание пустого файла.")
+ # Создаем пустой файл, если его нет
+ with open(USERS_FILE, 'w', encoding='utf-8') as f: json.dump({}, f)
+ return {}
+ except json.JSONDecodeError:
+ logging.error(f"Ошибка декодирования JSON в {USERS_FILE} после скачивания.")
+ return {}
+ except Exception as e:
+ logging.error(f"Неизвестная ошибка при загрузке пользователей после скачивания: {e}", exc_info=True)
+ return {}
+ except json.JSONDecodeError:
+ logging.error(f"Ошибка декодирования JSON в локальном {USERS_FILE}. Файл может быть поврежден. Возврат пустого словаря.")
+ return {}
+ except Exception as e:
+ logging.error(f"Неизвестная ошибка при загрузке пользователей ({USERS_FILE}): {e}", exc_info=True)
+ return {}
+
+def save_users(users):
+ """Сохранение данных пользователей."""
+ try:
+ with open(USERS_FILE, 'w', encoding='utf-8') as file:
+ json.dump(users, file, ensure_ascii=False, indent=4)
+ logging.info(f"Данные пользователей успешно сохранены в {USERS_FILE}")
+ # Загрузка на HF после сохранения
+ upload_db_to_hf(specific_file=USERS_FILE)
+ except Exception as e:
+ logging.error(f"Ошибка при сохранении данных пользователей в {USERS_FILE}: {e}", exc_info=True)
+
+# --- Функции синхронизации с Hugging Face ---
+
+def upload_db_to_hf(specific_file=None):
+ """Загрузка файлов данных на Hugging Face.
+ Если specific_file указан, загружает только его.
+ """
+ if not HF_TOKEN_WRITE:
+ logging.warning("Переменная окружения HF_TOKEN (для записи) не установлена. Загрузка на Hugging Face пропущена.")
+ return
+ try:
+ api = HfApi()
+ files_to_upload = [specific_file] if specific_file else SYNC_FILES
+ logging.info(f"Начало загрузки файлов {files_to_upload} на HF репозиторий {REPO_ID}...")
+
+ 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_name} успешно загружен на Hugging Face.")
+ except Exception as e:
+ logging.error(f"Ошибка при загрузке файла {file_name} на Hugging Face: {e}")
+ # Продолжаем пытаться загрузить другие файлы
+ else:
+ logging.warning(f"Файл {file_name} не найден локально, пропуск загрузки.")
+ logging.info("Загрузка файлов на HF завершена.")
+ except Exception as e:
+ logging.error(f"Общая ошибка при инициализации или загрузке на Hugging Face: {e}", exc_info=True)
+
+def download_db_from_hf(specific_file=None):
+ """Скачивание файлов данных с Hugging Face.
+ Если specific_file указан, скачивает только его.
+ """
+ if not HF_TOKEN_READ:
+ # Можно использовать и без токена для публичных репозиториев, но лучше предупредить
+ logging.warning("Переменная окружения HF_TOKEN_READ не установлена. Попытка скачивания с Hugging Face без токена (может не сработать для приватных репо).")
+ # Не выходим, пытаемся скачать анонимно
+
+ files_to_download = [specific_file] if specific_file else SYNC_FILES
+ logging.info(f"Начало скачивания файлов {files_to_download} с HF репозитория {REPO_ID}...")
+ downloaded_files_count = 0
+ try:
+ # HfApi() не нужен для hf_hub_download, но можно использовать для проверки существования репо
+ # api = HfApi()
+ # api.dataset_info(repo_id=REPO_ID, token=HF_TOKEN_READ) # Проверка доступности репо
+
+ for file_name in files_to_download:
+ try:
+ # Скачиваем в текущую директорию, перезаписывая существующие файлы
+ local_path = hf_hub_download(
+ repo_id=REPO_ID,
+ filename=file_name,
+ repo_type="dataset",
+ token=HF_TOKEN_READ, # Передаем токен, если он есть
+ local_dir=".",
+ local_dir_use_symlinks=False,
+ force_download=True # Принудительно скачивать свежую версию
+ )
+ logging.info(f"Файл {file_name} успешно скачан из Hugging Face в {local_path}.")
+ downloaded_files_count += 1
+ except RepositoryNotFoundError:
+ logging.error(f"Репозиторий {REPO_ID} не найден на Hugging Face. Скачивание прервано.")
+ break # Прерываем цикл, если репозиторий не найден
+ except Exception as e: # Ловим исключения для каждого файла отдельно
+ # Проверяем, является ли ошибка 'Not Found' для конкретного файла
+ # hf_hub_download часто возвращает HTTPError или FileNotFoundError внутри
+ if "404" in str(e) or isinstance(e, FileNotFoundError):
+ logging.warning(f"Файл {file_name} не найден в репозитории {REPO_ID} или ошибка доступа. Пропуск скачивания этого файла.")
+ else:
+ # Логируем другие, возможно, более серьезные ошибки
+ logging.error(f"Ошибка при скачивании файла {file_name} с Hugging Face: {e}", exc_info=True)
+ logging.info(f"Скачивание файлов с HF завершено. Скачано файлов: {downloaded_files_count}/{len(files_to_download)}.")
+ except RepositoryNotFoundError:
+ # Эта ошибка ловится и выше, но может возникнуть при первой проверке репо, если раскомментировать
+ logging.error(f"Репозиторий {REPO_ID} не найден на Hugging Face. Скачивание не выполнено.")
+ except Exception as e:
+ # Общая ошибка, если не удалось даже инициализировать Api() или что-то глобальное
+ logging.error(f"Общая ошибка при попытке скачивания с Hugging Face: {e}", exc_info=True)
+ # Не прерываем работу приложения, будем использовать локальные файлы, если они есть
+
+
+def periodic_backup():
+ """Периодическая загрузка данных на HF."""
+ backup_interval = 1800 # 30 минут
+ logging.info(f"Настройка периодического резервного копирования каждые {backup_interval} секунд.")
+ while True:
+ time.sleep(backup_interval)
+ logging.info("Запуск периодического резервного копирования...")
+ upload_db_to_hf() # Загружает все SYNC_FILES
+ logging.info("Периодическое резервное копирование завершено.")
+
+
+# --- Маршруты Flask ---
+
+@app.route('/')
+def catalog():
+ """Главная страница каталога товаров."""
+ data = load_data()
+ products = data.get('products', [])
+ categories = data.get('categories', [])
+ is_authenticated = 'user' in session
+
+ # Убираем артефакты {/**/} из HTML шаблона
+ catalog_html = '''
+
+
+
+ {% endfor %}
+ {# Сообщение, если нет товаров ПОСЛЕ фильтрации, будет добавлено через JS #}
+ {% if not products %}
+
Товары пока не добавлены.
+ {% endif %}
+
+
+
+
+
+
+ ×
+
Загрузка...
+
+
+
+
+
+
+ ×
+
Укажите количество и цвет
+
+
+
+
+
+
+
+
+
+
+
+ ×
+
Ваша корзина
+
Ваша корзина пуста.
+
+ Итого: 0.00 {{ currency_code }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ '''
+ return render_template_string(
+ catalog_html,
+ products=products,
+ categories=categories,
+ repo_id=REPO_ID,
+ is_authenticated=is_authenticated,
+ store_address=STORE_ADDRESS,
+ session=session, # session доступен в Jinja2 по умолчанию, но передать явно не помешает
+ currency_code=CURRENCY_CODE
+ )
+
+# --- Остальные маршруты (product_detail, login, auto_login, logout, admin, force_upload, force_download) ---
+# --- Код для этих маршрутов остается таким же, как в предыдущем ответе ---
+# --- ... (включая LOGIN_TEMPLATE и admin_html) ... ---
+
+@app.route('/product/')
+def product_detail(index):
+ """Отдает HTML с деталями одного продукта для модального окна."""
+ data = load_data()
+ products = data.get('products', [])
+ is_authenticated = 'user' in session
+ try:
+ product = products[index]
+ except IndexError:
+ logging.warning(f"Попытка доступа к несуществующему продукту с индексом {index}")
+ return "Товар не найден", 404
+
+ detail_html = '''
+ {# Используем Jinja комментарий #}
+
+
{{ product['name'] }}
+ {# Swiper Slider for Photos #}
+
+
+ {% if product.get('photos') and product['photos']|length > 0 %}
+ {% for photo in product['photos'] %}
+
+
{# Контейнер для зума #}
+
+
+
+ {% endfor %}
+ {% else %}
+
+
+
+ {% endif %}
+
+ {# Элементы управления Swiper (показываем только если фото больше 1) #}
+ {% if product.get('photos') and product['photos']|length > 1 %}
+
+
+
+ {% endif %}
+
+ {% set colors = product.get('colors', []) %}
+ {% if colors and colors|select('ne', '')|list|length > 0 %} {# Проверяем, что список не пуст и не содержит только пустые строки #}
+
+
+ {# Сообщения об успехе/ошибке #}
+ {% with messages = get_flashed_messages(with_categories=true) %}
+ {% if messages %}
+ {% for category, message in messages %}
+
{{ message }}
+ {% endfor %}
+ {% endif %}
+ {% endwith %}
+
+
+
Синхронизация с Hugging Face
+
+
+
+
+
Резервное копирование на Hugging Face происходит автоматически каждые 30 минут, а также после каждого сохранения данных. Используйте эти кнопки для немедленной синхронизации.