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,16 +8,23 @@ import threading import time from datetime import datetime from huggingface_hub import HfApi, hf_hub_download -from huggingface_hub.utils import RepositoryNotFoundError +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() app = Flask(__name__) -app.secret_key = 'your_unique_secret_key_soola_cosmetics_67890' # Новый уникальный секретный ключ +# ВАЖНО: Замените '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' @@ -38,8 +45,14 @@ CURRENCY_CODE = 'KGS' CURRENCY_NAME = 'Кыргызский сом (с)' # Настройка логирования -# Уровни: DEBUG, INFO, WARNING, ERROR, CRITICAL -logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +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() # Лог в консоль + ] +) # --- Функции работы с данными и пользователями --- @@ -47,50 +60,31 @@ def load_data(): """Загрузка данных о товарах и категориях.""" try: # Попытка скачать актуальные данные перед чтением локальных - download_db_from_hf() + 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}") + 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} не найден. Попытка скачать с 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': []} + 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}. Файл может быть поврежден. Возврат пустой структуры.") - # Можно добавить логику восстановления из бэкапа или HF, если нужно + # Попытка восстановить из бэкапа или создать пустой, если нужно return {'products': [], 'categories': []} except Exception as e: logging.error(f"Неизвестная ошибка при загрузке данных ({DATA_FILE}): {e}", exc_info=True) @@ -100,45 +94,35 @@ 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_db_to_hf(specific_file=DATA_FILE) + # Вызываем в отдельном потоке, чтобы не блокировать основной запрос + 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) - # В реальном приложении можно добавить механизм повторной попытки или уведомления - # raise # Перевыброс исключения может остановить приложение, если не обработан выше + flash("Ошибка при сохранении данных. См. лог сервера.", "error") + 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} не найден. Попытка скачать с 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 {} + 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 {} @@ -149,26 +133,37 @@ 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(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") # --- Функции синхронизации с 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 + return False # Возвращаем статус неудачи + + files_to_upload = [specific_file] if specific_file else SYNC_FILES + success = True # Флаг общего успеха + + # with hf_lock: # Раскомментировать, если используется блокировка 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: @@ -185,30 +180,27 @@ 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: # Скачиваем в текущую директорию, перезаписывая существующие файлы @@ -216,37 +208,48 @@ def download_db_from_hf(specific_file=None): repo_id=REPO_ID, filename=file_name, repo_type="dataset", - token=HF_TOKEN_READ, # Передаем токен, если он есть - local_dir=".", - local_dir_use_symlinks=False, - force_download=True # Принудительно скачивать свежую версию + token=HF_TOKEN_READ, # Передаем токен, если он есть (может быть None) + local_dir=".", # Скачиваем в текущую директорию + local_dir_use_symlinks=False, # Избегаем символических ссылок + force_download=True, # Принудительно скачивать свежую версию + resume_download=False # Отключаем возобновление для простоты ) - logging.info(f"Файл {file_name} успешно скачан из Hugging Face в {local_path}.") - downloaded_files_count += 1 + # Переименовываем скачанный файл из кеша в нужное место + # 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) 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 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 RepositoryNotFoundError: - # Эта ошибка ловится и выше, но может возникнуть при первой проверке репо, если раскомментировать logging.error(f"Репозиторий {REPO_ID} не найден на Hugging Face. Скачивание не выполнено.") + return False except Exception as e: - # Общая ошибка, если не удалось даже инициализировать Api() или что-то глобальное - logging.error(f"Общая ошибка при попытке скачивания с Hugging Face: {e}", exc_info=True) - # Не прерываем работу приложения, будем использовать локальные файлы, если они есть + logging.error(f"Критическая ошибка при попытке скачивания с Hugging Face: {e}", exc_info=True) + return False def periodic_backup(): """Периодическая загрузка данных на HF.""" - backup_interval = 1800 # 30 минут + backup_interval = 1800 # 30 минут = 1800 секунд logging.info(f"Настройка периодического резервного копирования каждые {backup_interval} секунд.") while True: time.sleep(backup_interval) @@ -254,151 +257,177 @@ def periodic_backup(): upload_db_to_hf() # Загружает все SYNC_FILES logging.info("Периодическое резервное копирование завершено.") +# --- HTML Шаблоны --- -# --- Маршруты Flask --- - -@app.route('/') -def catalog(): - """Главная страница каталога товаров.""" - data = load_data() - products = data.get('products', []) - categories = data.get('categories', []) - is_authenticated = 'user' in session - - # Убираем артефакты {/**/} из HTML шаблона - catalog_html = ''' - - -
- - -
- {% endif %}
- {{ product.get('description', '')[:50] }}{% if product.get('description', '')|length > 50 %}...{% endif %}
-
+ {% endif %}
+ {{ product.get('description', '') }}
+