diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,5 +1,5 @@ -# Импортируем необходимые библиотеки +# -*- coding: utf-8 -*- from flask import Flask, render_template_string, request, redirect, url_for, session, send_file, flash import json import os @@ -8,43 +8,49 @@ import threading import time from datetime import datetime from huggingface_hub import HfApi, hf_hub_download -from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError # Добавляем HfHubHTTPError +from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError # Import specific HF errors from werkzeug.utils import secure_filename # Импортируем dotenv для загрузки переменных окружения из .env файла from dotenv import load_dotenv # Загружаем переменные окружения из файла .env (если он есть) +# Убедитесь, что у вас есть файл .env в той же директории, что и этот скрипт +# Пример .env файла: +# HF_TOKEN="hf_YOUR_WRITE_TOKEN_HERE" +# HF_TOKEN_READ="hf_YOUR_READ_TOKEN_HERE" # Может быть таким же или другим load_dotenv() -# Инициализация Flask приложения app = Flask(__name__) -# Установите СВОЙ уникальный и надежный секретный ключ -app.secret_key = os.getenv('FLASK_SECRET_KEY', 'fallback_very_secret_key_soola_12345') # Загружаем из .env или используем запасной +# ВАЖНО: Замените 'your_unique_secret_key_soola_cosmetics_67890' на действительно случайную и секретную строку! +# Можно сгенерировать, например, с помощью: import secrets; secrets.token_hex(16) +app.secret_key = os.getenv('FLASK_SECRET_KEY', 'your_unique_secret_key_soola_cosmetics_67890') # Используем env переменную или дефолт -# Константы для файлов данных DATA_FILE = 'data_soola.json' USERS_FILE = 'users_soola.json' -# Список файлов для синхронизации с Hugging Face +# Список файлов для синхронизации SYNC_FILES = [DATA_FILE, USERS_FILE] -# Настройки Hugging Face (Замените REPO_ID на ваш) -REPO_ID = os.getenv("HF_REPO_ID", "Kgshop/Soola") # Загружаем из .env или используем значение по умолчанию -HF_TOKEN_WRITE = os.getenv("HF_TOKEN") # Токен для записи (должен быть установлен в .env) -HF_TOKEN_READ = os.getenv("HF_TOKEN_READ", HF_TOKEN_WRITE) # Токен для чтения (может быть тем же или отдельным, или отсутствовать для публичных репо) +# Настройки 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" +# Адрес магазина +STORE_ADDRESS = "Рынок Дордой, Джунхай, терминал, 38" # Единый адрес + +# Валюта (только KGS) CURRENCY_CODE = 'KGS' CURRENCY_NAME = 'Кыргызский сом (с)' # Настройка логирования logging.basicConfig( level=logging.INFO, - format='%(asctime)s - %(levelname)s - [%(threadName)s] - %(message)s', # Добавляем имя потока + format='%(asctime)s - %(levelname)s - %(threadName)s - %(message)s', handlers=[ - logging.FileHandler("app_soola.log", encoding='utf-8'), # Логирование в файл - logging.StreamHandler() # Логирование в консоль + logging.FileHandler("app_soola.log", encoding='utf-8'), # Лог в файл + logging.StreamHandler() # Лог в консоль ] ) @@ -52,115 +58,110 @@ logging.basicConfig( def load_data(): """Загрузка данных о товарах и категориях.""" - # Сначала пытаемся скачать с HF - try: - download_db_from_hf(specific_file=DATA_FILE) - logging.info(f"Успешно проверена/скачана свежая версия {DATA_FILE} с HF.") - except Exception as e: - logging.warning(f"Не удалось скачать {DATA_FILE} с HF перед загрузкой: {e}. Используется локальная версия.") - - # Затем читаем локальный файл try: + # Попытка скачать актуальные данные перед чтением локальных + download_db_from_hf(specific_file=DATA_FILE) # Пы��аемся скачать свежий файл 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'] = [] + logging.warning(f"{DATA_FILE} не является словарем. Инициализация пустой структурой.") + return {'products': [], 'categories': []} + if 'products' not in data: + logging.warning(f"Ключ 'products' отсутствует в {DATA_FILE}. Добавлен пустой список.") + data['products'] = [] + if 'categories' not in data: + logging.warning(f"Ключ 'categories' отсутствует в {DATA_FILE}. Добавлен пустой список.") + data['categories'] = [] return data except FileNotFoundError: - logging.warning(f"Локальный файл {DATA_FILE} не найден. Создание пустой структуры.") - return {'products': [], 'categories': []} # Возвращаем пустую структуру, не создаем файл здесь + logging.warning(f"Локальный файл {DATA_FILE} не найден после попытки скачивания. Создание пустой структуры.") + # Создаем пустой файл, если он все еще не существует + 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': []} except json.JSONDecodeError: logging.error(f"Ошибка декодирования JSON в локальном {DATA_FILE}. Файл может быть поврежден. Возврат пустой структуры.") - # TODO: Возможно, стоит попытаться восстановить из бэкапа или принудительно скачать с HF + # Попытка восстановить из бэкапа или создать пустой, если нужно return {'products': [], 'categories': []} except Exception as e: - logging.error(f"Неизвестная ошибка при загрузке данных из {DATA_FILE}: {e}", exc_info=True) + logging.error(f"Неизвестная ошибка при загрузке данных ({DATA_FILE}): {e}", exc_info=True) return {'products': [], 'categories': []} def save_data(data): """Сохранение данных о товарах и категориях.""" try: - # Создаем резервную копию перед перезаписью - if os.path.exists(DATA_FILE): - backup_file = f"{DATA_FILE}.bak_{datetime.now().strftime('%Y%m%d%H%M%S')}" - try: - os.rename(DATA_FILE, backup_file) - logging.info(f"Создана резервная копия: {backup_file}") - except OSError as e: - logging.warning(f"Не удалось создать резервную копию {DATA_FILE}: {e}") - + # Сортировка перед сохранением для консистентности + data.get('products', []).sort(key=lambda x: x.get('name', '').lower()) + data.get('categories', []).sort() 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) + # Загрузка на HF после сохранения (можно сделать опциональной) + # Вызываем в отдельном потоке, чтобы не блокировать основной запрос + upload_thread = threading.Thread(target=upload_db_to_hf, args=(DATA_FILE,), name="HFUploadDataThread") + upload_thread.start() except Exception as e: logging.error(f"Ошибка при сохранении данных в {DATA_FILE}: {e}", exc_info=True) - flash("Произошла ошибка при сохранении данных товаров/категорий.", 'error') + flash("Ошибка при сохранении данных. См. лог сервера.", "error") + def load_users(): """Загрузка данных пользователей.""" - # Сначала пытаемся скачать с HF - try: - download_db_from_hf(specific_file=USERS_FILE) - logging.info(f"Успешно проверена/скачана свежая версия {USERS_FILE} с HF.") - except Exception as e: - logging.warning(f"Не удалось скачать {USERS_FILE} с HF перед загрузкой: {e}. Используется локальная версия.") - - # Затем читаем локальный файл 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} не найден. Создание пустого словаря пользователей.") + logging.warning(f"Локальный файл {USERS_FILE} не найден после попытки скачивания. Создание пустого файла.") + if not os.path.exists(USERS_FILE): + with open(USERS_FILE, 'w', encoding='utf-8') as f: json.dump({}, f) + logging.info(f"Создан пустой файл {USERS_FILE}") return {} except json.JSONDecodeError: logging.error(f"Ошибка декодирования JSON в локальном {USERS_FILE}. Файл может быть поврежден. Возврат пустого словаря.") return {} except Exception as e: - logging.error(f"Неизвестная ошибка при загрузке пользователей из {USERS_FILE}: {e}", exc_info=True) + logging.error(f"Неизвестная ошибка при загрузке пользователей ({USERS_FILE}): {e}", exc_info=True) return {} def save_users(users): """Сохранение данных пользователей.""" try: - # Создаем резервную копию - if os.path.exists(USERS_FILE): - backup_file = f"{USERS_FILE}.bak_{datetime.now().strftime('%Y%m%d%H%M%S')}" - try: - os.rename(USERS_FILE, backup_file) - logging.info(f"Создана резервная копия: {backup_file}") - except OSError as e: - logging.warning(f"Не удалось создать резервную копию {USERS_FILE}: {e}") - + # Сортировка пользователей по логину для консистентности + sorted_users = dict(sorted(users.items())) with open(USERS_FILE, 'w', encoding='utf-8') as file: - json.dump(users, file, ensure_ascii=False, indent=4) + json.dump(sorted_users, file, ensure_ascii=False, indent=4) logging.info(f"Данные пользователей успешно сохранены в {USERS_FILE}") # Загрузка на HF после сохранения - upload_db_to_hf(specific_file=USERS_FILE) + upload_thread = threading.Thread(target=upload_db_to_hf, args=(USERS_FILE,), name="HFUploadUsersThread") + upload_thread.start() except Exception as e: logging.error(f"Ошибка при сохранении данных пользователей в {USERS_FILE}: {e}", exc_info=True) - flash("Произошла ошибка при сохранении данных пользователей.", 'error') + flash("Ошибка при сохранении данных пользователей. См. лог сервера.", "error") # --- Функции синхронизации с Hugging Face --- +# Используем Lock для предотвращения одновременной записи/чтения во время синхронизации, если нужно +# hf_lock = threading.Lock() # Раскомментировать, если возникают проблемы с гонкой состояний + def upload_db_to_hf(specific_file=None): - """Загрузка файлов данных на Hugging Face.""" + """Загрузка файлов данных на Hugging Face. + Если specific_file указан, загружает только его. + """ if not HF_TOKEN_WRITE: - logging.warning("HF_TOKEN (для записи) не установлен. Загрузка на Hugging Face пропущена.") + logging.warning("Переменная окру��ения HF_TOKEN (для записи) не установлена. Загрузка на Hugging Face пропущена.") return False # Возвращаем статус неудачи files_to_upload = [specific_file] if specific_file else SYNC_FILES - success = True # Флаг успешности для всех файлов - api = None # Инициализируем None + success = True # Флаг общего успеха + + # with hf_lock: # Раскомментировать, если используется блокировка try: api = HfApi() logging.info(f"Начало загрузки файлов {files_to_upload} на HF репозиторий {REPO_ID}...") @@ -168,7 +169,6 @@ def upload_db_to_hf(specific_file=None): for file_name in files_to_upload: if os.path.exists(file_name): try: - logging.info(f"Загрузка файла {file_name}...") api.upload_file( path_or_fileobj=file_name, path_in_repo=file_name, @@ -178,249 +178,256 @@ def upload_db_to_hf(specific_file=None): commit_message=f"Sync {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" ) logging.info(f"Файл {file_name} успешно загружен на Hugging Face.") - except HfHubHTTPError as http_err: - logging.error(f"HTTP ошибка при загрузке файла {file_name} на Hugging Face: {http_err}") - # Проверяем код ответа, если это 401/403, возможно, проблема с токеном - if http_err.response.status_code in [401, 403]: - logging.error("Ошибка авторизации (401/403). Проверьте HF_TOKEN.") - success = False except Exception as e: - logging.error(f"Общая ошибка при загрузке файла {file_name} на Hugging Face: {e}", exc_info=True) - success = False + logging.error(f"Ошибка при загрузке файла {file_name} на Hugging Face: {e}") + success = False # Отмечаем неудачу, если хоть один файл не загрузился + # Продолжаем пытаться загрузить другие файлы else: logging.warning(f"Файл {file_name} не найден локально, пропуск загрузки.") - # Не считаем это полной неудачей, если другие файлы загрузятся + success = False # Считаем это тоже неполным успехом logging.info("Загрузка файлов на HF завершена.") return success except Exception as e: - logging.error(f"Критическая ошибка при инициализации HfApi или процессе загрузки: {e}", exc_info=True) + logging.error(f"Общая ошибка при инициализации или загрузке на Hugging Face: {e}", exc_info=True) return False def download_db_from_hf(specific_file=None): - """Скачивание файлов данных с Hugging Face.""" - if not REPO_ID: - logging.warning("HF_REPO_ID не установлен. Скачивание с Hugging Face невозможно.") - return False - - token_to_use = HF_TOKEN_READ or None # Используем None если HF_TOKEN_READ пустой - if not token_to_use: - logging.info("HF_TOKEN_READ не установлен. Попытка скачивания без токена (для публичных репозиториев).") - + """Скачивание файлов данных с Hugging Face. + Если specific_file указан, скачивает только его. + Возвращает True, если хотя бы один файл был успешно скачан, иначе False. + """ files_to_download = [specific_file] if specific_file else SYNC_FILES logging.info(f"Начало скачивания файлов {files_to_download} с HF репозитория {REPO_ID}...") downloaded_files_count = 0 - success = True # Флаг общего успеха + # with hf_lock: # Раскомментировать, если используется блокировка + try: + 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, # Передаем токен, если он есть (может быть None) + local_dir=".", # Скачиваем в текущую директорию + local_dir_use_symlinks=False, # Избегаем символических ссылок + force_download=True, # Принудительно скачивать свежую версию + resume_download=False # Отключаем возобновление для простоты + ) + # Переименовываем скачанный файл из кеша в нужное место + # hf_hub_download возвращает путь в кеше, нужно скопировать/переместить + if os.path.exists(local_path): + os.replace(local_path, file_name) # Перемещаем с заменой + logging.info(f"Файл {file_name} успешно скачан из Hugging Face и сохранен как {file_name}.") + downloaded_files_count += 1 + else: + logging.error(f"hf_hub_download вернул путь {local_path}, но файл не найден.") - for file_name in files_to_download: - try: - # Скачиваем в текущую директорию, перезаписывая существующие файлы - local_path = hf_hub_download( - repo_id=REPO_ID, - filename=file_name, - repo_type="dataset", - token=token_to_use, - local_dir=".", # Скачать в текущую директорию - local_dir_use_symlinks=False, # Важно для Windows и Docker - force_download=True, # Принудительно скачать свежую версию - resume_download=False # Не возобновлять, а скачивать заново - ) - logging.info(f"Файл {file_name} успешно скачан из Hugging Face в {local_path}.") - downloaded_files_count += 1 - except RepositoryNotFoundError: - logging.error(f"Репозиторий {REPO_ID} не найден на Hugging Face. Скачивание прервано.") - success = False - break # Прерываем цикл, если репозиторий не найден - except HfHubHTTPError as http_err: - # Проверяем, является ли ошибка 'Not Found' для конкретного файла (404) - if http_err.response.status_code == 404: - logging.warning(f"Файл {file_name} не найден в репозитории {REPO_ID}. Пропуск скачивания этого файла.") - # Не считаем это полной неудачей, если другие файлы скачаются - elif http_err.response.status_code in [401, 403]: - logging.error(f"Ошибка авторизации (401/403) при скачивании файла {file_name}. Проверьте HF_TOKEN_READ (если репозиторий приватный).") - success = False - else: - # Логируем другие HTTP ошибки - logging.error(f"HTTP ошибка при скачивании файла {file_name} с Hugging Face: {http_err}", exc_info=True) - success = False - except Exception as e: # Ловим другие возможные исключения (например, проблемы с сетью) - logging.error(f"Общая ошибка при скачивании файла {file_name} с Hugging Face: {e}", exc_info=True) - success = False + except HfHubHTTPError as e: + # Проверяем, является ли ошибка 'Not Found' для конкретного файла + if e.response.status_code == 404: + logging.warning(f"Файл {file_name} не найден в репозитории {REPO_ID}. Пропуск скачивания этого файла.") + else: + logging.error(f"HTTP ошибка при скачивании файла {file_name} с Hugging Face: {e}", exc_info=True) + except RepositoryNotFoundError: + logging.error(f"Репозиторий {REPO_ID} не найден на Hugging Face. Скачивание прервано.") + break # Прерываем цикл, если репозиторий не найден + except Exception as e: + # Логируем другие ошибки + logging.error(f"Общая ошибка при скачивании файла {file_name} с Hugging Face: {e}", exc_info=True) + + logging.info(f"Скачивание файлов с HF завершено. Успешно скачано: {downloaded_files_count}/{len(files_to_download)}.") + return downloaded_files_count > 0 # Успех, если хоть что-то скачали - logging.info(f"Скачивание файлов с HF завершено. Скачано файлов: {downloaded_files_count}/{len(files_to_download)}.") - return success + except RepositoryNotFoundError: + logging.error(f"Репозиторий {REPO_ID} не найден на Hugging Face. Скачивание не выполнено.") + return False + except Exception as e: + logging.error(f"Критическая ошибка при попытке скачивания с Hugging Face: {e}", exc_info=True) + return False def periodic_backup(): """Периодическая загрузка данных на HF.""" - backup_interval = 1800 # 30 минут (в секундах) - logging.info(f"Запуск потока периодического резервного копирования каждые {backup_interval} секунд.") + backup_interval = 1800 # 30 минут = 1800 секунд + logging.info(f"Настройка периодического резервного копирования каждые {backup_interval} секунд.") while True: time.sleep(backup_interval) logging.info("Запуск периодического резервного копирования...") - try: - success = upload_db_to_hf() # Загружает все SYNC_FILES - if success: - logging.info("Периодическое резервное копирование успешно завершено.") - else: - logging.warning("Периодическое резервное копирование завершено с ошибками.") - except Exception as e: - # Ловим непредвиденные ошибки в самом потоке - logging.error(f"Критическая ошибка в потоке периодического бэкапа: {e}", exc_info=True) - # Продолжаем работу потока, но логируем ошибку - # Добавим небольшую паузу на всякий случай, чтобы не зациклиться при мгновенной ошибке - time.sleep(5) + upload_db_to_hf() # Загружает все SYNC_FILES + logging.info("Периодическое резервное копирование завершено.") -# --- Маршруты Flask --- +# --- HTML Шаблоны --- -@app.route('/') -def catalog(): - """Главная страница каталога товаров.""" - data = load_data() - products = data.get('products', []) - categories = data.get('categories', []) - # Сортировка для отображения - products.sort(key=lambda x: x.get('name', '').lower()) - categories.sort() - is_authenticated = 'user' in session - - # Шаблон каталога (HTML+CSS+JS) - catalog_html = ''' - - - - - - Soola Cosmetics - Каталог - - - - - - -
-
-

Soola Cosmetics

-