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,23 +8,16 @@ import threading import time from datetime import datetime from huggingface_hub import HfApi, hf_hub_download -from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError # Import specific HF errors +from huggingface_hub.utils import RepositoryNotFoundError 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() app = Flask(__name__) -# ВАЖНО: Замените '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 переменную или дефолт - +app.secret_key = 'your_unique_secret_key_soola_cosmetics_67890' # Новый уникальный секретный ключ DATA_FILE = 'data_soola.json' USERS_FILE = 'users_soola.json' @@ -45,14 +38,8 @@ CURRENCY_CODE = 'KGS' CURRENCY_NAME = 'Кыргызский сом (с)' # Настройка логирования -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(levelname)s - %(threadName)s - %(message)s', - handlers=[ - logging.FileHandler("app_soola.log", encoding='utf-8'), # Лог в файл - logging.StreamHandler() # Лог в консоль - ] -) +# Уровни: DEBUG, INFO, WARNING, ERROR, CRITICAL +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') # --- Функции работы с данными и пользователями --- @@ -60,31 +47,50 @@ def load_data(): """Загрузка данных о товарах и категориях.""" try: # Попытка скачать актуальные данные перед чтением локальных - download_db_from_hf(specific_file=DATA_FILE) # Пытаемся скачать свежий файл + download_db_from_hf() with open(DATA_FILE, 'r', encoding='utf-8') as file: data = json.load(file) - logging.info(f"Данные успешно загружены из локального {DATA_FILE}") + logging.info(f"Данные успешно загружены из {DATA_FILE}") # Проверка базовой структуры if not isinstance(data, dict): 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} не найден после попытки скачивания. Создание пустой структуры.") - # Создаем пустой файл, если он все еще не существует - 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': []} + 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) @@ -94,35 +100,45 @@ def load_data(): def save_data(data): """Сохранение данных о товарах и категориях.""" try: - # Сортировка перед сохранением для консистентности - 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_thread = threading.Thread(target=upload_db_to_hf, args=(DATA_FILE,), name="HFUploadDataThread") - upload_thread.start() + upload_db_to_hf(specific_file=DATA_FILE) except Exception as e: logging.error(f"Ошибка при сохранении данных в {DATA_FILE}: {e}", exc_info=True) - flash("Ошибка при сохранении данных. См. лог сервера.", "error") - + # В реальном приложении можно добавить механизм повторной попытки или уведомления + # raise # Перевыброс исключения может остановить приложение, если не обработан выше def load_users(): """Загрузка данных пользователей.""" try: - download_db_from_hf(specific_file=USERS_FILE) # Пытаемся скачать свежий файл + # Опционально: скачать файл пользователей перед чтением + # 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}") + logging.info(f"Данные пользователей успешно загружены из {USERS_FILE}") return users if isinstance(users, dict) else {} except FileNotFoundError: - 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 {} + 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 {} @@ -133,37 +149,26 @@ def load_users(): def save_users(users): """Сохранение данных пользователей.""" try: - # Сортировка пользователей по логину для консистентности - sorted_users = dict(sorted(users.items())) with open(USERS_FILE, 'w', encoding='utf-8') as file: - json.dump(sorted_users, file, ensure_ascii=False, indent=4) + json.dump(users, file, ensure_ascii=False, indent=4) logging.info(f"Данные пользователей успешно сохранены в {USERS_FILE}") # Загрузка на HF после сохранения - upload_thread = threading.Thread(target=upload_db_to_hf, args=(USERS_FILE,), name="HFUploadUsersThread") - upload_thread.start() + upload_db_to_hf(specific_file=USERS_FILE) except Exception as e: logging.error(f"Ошибка при сохранении данных пользователей в {USERS_FILE}: {e}", exc_info=True) - flash("Ошибка при сохранении данных пользователей. См. лог сервера.", "error") # --- Функции синхронизации с Hugging Face --- -# Используем Lock для предотвращения одновременной записи/чтения во время синхронизации, если нужно -# hf_lock = threading.Lock() # Раскомментировать, если возникают проблемы с гонкой состояний - def upload_db_to_hf(specific_file=None): """Загрузка файлов данных на Hugging Face. Если specific_file указан, загружает только его. """ if not HF_TOKEN_WRITE: logging.warning("Переменная окружения HF_TOKEN (для записи) не установлена. Загрузка на Hugging Face пропущена.") - return False # Возвращаем статус неудачи - - files_to_upload = [specific_file] if specific_file else SYNC_FILES - success = True # Флаг общего успеха - - # with hf_lock: # Раскомментировать, если используется блокировка + 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: @@ -180,27 +185,30 @@ def upload_db_to_hf(specific_file=None): logging.info(f"Файл {file_name} успешно загружен на Hugging Face.") except Exception as e: 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"Общая ошибка при инициализации или загрузке на Hugging Face: {e}", exc_info=True) - return False def download_db_from_hf(specific_file=None): """Скачивание файлов данных с Hugging Face. Если specific_file указан, скачивает только его. - Возвращает True, если хотя бы один файл был успешно скачан, иначе False. """ + 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 - # with hf_lock: # Раскомментировать, если используется блокировка 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: # Скачиваем в текущую директорию, перезаписывая существующие файлы @@ -208,48 +216,37 @@ def download_db_from_hf(specific_file=None): 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 # Отключаем возобновление для простоты + token=HF_TOKEN_READ, # Передаем токен, если он есть + local_dir=".", + local_dir_use_symlinks=False, + force_download=True # Принудительно скачивать свежую версию ) - # Переименовываем скачанный файл из кеша в нужное место - # 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}, но файл не найден.") - - 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) + 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: - # Логируем другие ошибки - 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 # Успех, если хоть что-то скачали - + 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. Скачивание не выполнено.") - return False except Exception as e: - logging.error(f"Критическая ошибка при попытке скачивания с Hugging Face: {e}", exc_info=True) - return False + # Общая ошибка, если не удалось даже инициализировать Api() или что-то глобальное + logging.error(f"Общая ошибка при попытке скачивания с Hugging Face: {e}", exc_info=True) + # Не прерываем работу приложения, будем использовать локальные файлы, если они есть def periodic_backup(): """Периодическая загрузка данных на HF.""" - backup_interval = 1800 # 30 минут = 1800 секунд + backup_interval = 1800 # 30 минут logging.info(f"Настройка периодического резервного копирования каждые {backup_interval} секунд.") while True: time.sleep(backup_interval) @@ -257,177 +254,151 @@ def periodic_backup(): upload_db_to_hf() # Загружает все SYNC_FILES logging.info("Периодическое резервное копирование завершено.") -# --- HTML Шаблоны --- -CATALOG_TEMPLATE = ''' - - - - - - Soola Cosmetics - Каталог - - - - - - -
-
-

Soola Cosmetics

-
-
Наш адрес: {{ store_address }}
+
Наш адрес: {{ store_address }}
-
{% for category in categories %} @@ -436,712 +407,594 @@ CATALOG_TEMPLATE = '''
- +
-
-
- {% for product in products %} -
-
- {% if product.get('photos') and product['photos']|length > 0 %} - {# Используем правильный URL для Hugging Face Datasets #} - {{ product['name'] }} - {% else %} - No Image - {% endif %} -
-
-

{{ product['name'] }}

- {% if is_authenticated %} -
{{ "%.2f"|format(product['price']) }} {{ currency_code }}
- {% else %} -
Цена доступна после входа
- {% endif %} -

{{ product.get('description', '') }}

-
-
- - {% if is_authenticated %} - - {% endif %} +
+ {% for product in products %} +
+
+ {% if product.get('photos') and product['photos']|length > 0 %} + {{ product['name'] }} + {% else %} + No Image + {% endif %} +
+
+

{{ product['name'] }}

+ {% if is_authenticated %} +
{{ "%.2f"|format(product['price']) }} {{ currency_code }}
+ {% else %} +
Цена доступна после входа
+ {% endif %} +

{{ product.get('description', '')[:50] }}{% if product.get('description', '')|length > 50 %}...{% endif %}

+
+
+ + {% if is_authenticated %} + + {% endif %} +
+ {% endfor %} + {# Сообщение, если нет товаров ПОСЛЕ фильтрации, будет добавлено через JS #} + {% if not products %} +

Товары пока не добавлены.

+ {% endif %}
- {% endfor %} - {# Сообщение, если нет товаров ПОСЛЕ фильтрации, будет добавлено через JS #} - {% if not products %} -

Товары пока не добавлены.

- {% endif %}
-
- -