diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,5 +1,5 @@ -from flask import Flask, render_template_string, request, redirect, url_for, session, send_from_directory +from flask import Flask, render_template_string, request, redirect, url_for, session, send_file import json import os import logging @@ -11,211 +11,185 @@ from huggingface_hub.utils import RepositoryNotFoundError from werkzeug.utils import secure_filename app = Flask(__name__) -app.secret_key = 'your_unique_secret_key_soola_cosmetics_7890' # Уникальный секретный ключ (изменен) -DATA_FILE = 'data_soola.json' -USERS_FILE = 'users_soola.json' +app.secret_key = 'your_unique_secret_key_12345_cosmetics' # Уникальный секретный ключ +DATA_FILE = 'data_soola_cosmetics.json' +USERS_FILE = 'users_soola_cosmetics.json' -# Список файлов для синхронизации (CONFIG_FILE удален) +# Список файлов для синхронизации (config.json убран) SYNC_FILES = [DATA_FILE, USERS_FILE] # Настройки Hugging Face -REPO_ID = "Kgshop/Soola" # Убедитесь, что этот репозиторий существует или создайте его +REPO_ID = "Kgshop/Soola" # Оставляем старый или меняем на новый? Пока оставил старый. HF_TOKEN_WRITE = os.getenv("HF_TOKEN") HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") -# Адрес магазина (только один) +# Адрес магазина (теперь один) STORE_ADDRESS = "Рынок Дордой , Джунхай , терминал , 38" -# Валюта (только KGS) +# Единственная валюта CURRENCY_CODE = 'KGS' CURRENCY_NAME = 'Кыргызский сом (с)' # Настройка логирования -logging.basicConfig(level=logging.INFO) # Изменено на INFO для меньшей детализации в продакшене +logging.basicConfig(level=logging.DEBUG) -# --- Функции работы с данными и пользователями --- +# --- Функции для работы с данными и пользователями --- def load_data(): """Загрузка данных о товарах и категориях.""" try: # Попытка скачать актуальные файлы перед загрузкой - download_files_from_hf() + 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("Данные успешно загружены из JSON") # Проверка структуры данных - if not isinstance(data, dict): - logging.warning(f"Файл {DATA_FILE} имеет неверный формат (не словарь). Создание структуры по умолчанию.") - return {'products': [], 'categories': []} - if 'products' not in data or not isinstance(data['products'], list): - logging.warning(f"Ключ 'products' отсутствует или не является списком в {DATA_FILE}. Инициализация пустым списком.") - data['products'] = [] - if 'categories' not in data or not isinstance(data['categories'], list): - logging.warning(f"Ключ 'categories' отсутствует или не является списком в {DATA_FILE}. Инициализация пустым списком.") - data['categories'] = [] + if not isinstance(data, dict) or 'products' not in data or 'categories' not in data: + logging.warning("Структура данных некорректна, инициализация пустыми списками.") + # Если data это список (старый формат), пытаемся сохранить его как категории + return {'products': [], 'categories': [] if not isinstance(data, list) else data} return data except FileNotFoundError: - logging.warning(f"Локальный файл {DATA_FILE} не найден. Возвращена пустая структура.") - return {'products': [], 'categories': []} + logging.warning(f"Локальный файл {DATA_FILE} не найден. Попытка скачивания с HF.") + # Повторная попытка скачать, если первая была до создания файла + try: + download_db_from_hf() + # Если скачивание успешно, пробуем загрузить снова + with open(DATA_FILE, 'r', encoding='utf-8') as file: + data = json.load(file) + logging.info("Данные успешно загружены из JSON после скачивания.") + if not isinstance(data, dict) or 'products' not in data or 'categories' not in data: + return {'products': [], 'categories': [] if not isinstance(data, list) else data} + return data + except (RepositoryNotFoundError, FileNotFoundError, Exception) as e: + logging.error(f"Не удалось скачать или найти файл {DATA_FILE} после ошибки. Создание локальной базы данных. Ошибка: {e}") + return {'products': [], 'categories': []} # Инициализация пустой структурой except json.JSONDecodeError: - logging.error(f"Ошибка: Невозможно декодировать JSON файл {DATA_FILE}. Возвращена пустая структура.") + logging.error(f"Ошибка: Невозможно декодировать JSON файл {DATA_FILE}.") return {'products': [], 'categories': []} except RepositoryNotFoundError: - logging.error("Репозиторий Hugging Face не найден. Используется локальная база данных (если есть) или создается пустая.") - # Попробуем загрузить локальный файл еще раз, если он вдруг есть - try: - with open(DATA_FILE, 'r', encoding='utf-8') as file: - data = json.load(file) - logging.info(f"Загружен локальный файл {DATA_FILE} после ошибки с репозиторием.") - # Повторная проверка структуры - if not isinstance(data, dict): data = {'products': [], 'categories': []} - if 'products' not in data or not isinstance(data['products'], list): data['products'] = [] - if 'categories' not in data or not isinstance(data['categories'], list): data['categories'] = [] - return data - except (FileNotFoundError, json.JSONDecodeError): - logging.warning(f"Локальный файл {DATA_FILE} также не найден или поврежден. Создание пустой структуры.") - return {'products': [], 'categories': []} + logging.error("Репозиторий Hugging Face не найден. Создание локальной базы данных.") + return {'products': [], 'categories': []} except Exception as e: - logging.error(f"Непредвиденная ошибка при загрузке данных: {e}") + logging.error(f"Произошла непредвиденная ошибка при загрузке данных: {e}") 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}") - upload_files_to_hf([DATA_FILE]) # Загружаем только измененный файл + upload_db_to_hf() # Синхронизация после сохранения except Exception as e: - logging.error(f"Ошибка при сохранении данных: {e}") - # Не прерываем работу приложения, но логируем ошибку - # raise # Можно раскомментировать, если сохранение критично + logging.error(f"Ошибка при сохранении данных в {DATA_FILE}: {e}") + # Не пробрасываем исключение дальше, чтобы приложение не падало, + # но логируем ошибку. В идеале, нужна стратегия обработки таких ошибок. + def load_users(): """Загрузка данных пользователей.""" try: - # Попытка скачать актуальный файл перед загрузкой - download_files_from_hf([USERS_FILE]) + # Попытка скачать актуальный файл пользователей + # download_db_from_hf() # Не будем скачивать каждый раз при загрузке юзеров, только при старте и по запросу with open(USERS_FILE, 'r', encoding='utf-8') as file: users = json.load(file) logging.info(f"Данные пользователей успешно загружены из {USERS_FILE}") - if not isinstance(users, dict): - logging.warning(f"Файл {USERS_FILE} имеет неверный формат (не словарь). Возвращен пустой словарь.") - return {} - return users + return users if isinstance(users, dict) else {} except FileNotFoundError: - logging.warning(f"Локальный файл {USERS_FILE} не найден. Возвращен пустой словарь.") + logging.warning(f"Локальный файл пользователей {USERS_FILE} не найден. Возвращаем пустой словарь.") return {} except json.JSONDecodeError: - logging.error(f"Ошибка: Невозможно декодировать JSON файл {USERS_FILE}. Возвращен пустой словарь.") + logging.error(f"Ошибка: Невозможно декодировать JSON файл пользователей {USERS_FILE}.") return {} - except RepositoryNotFoundError: - logging.error("Репозиторий Hugging Face не найден при загрузке пользователей. Используется локальный файл (если есть).") - try: - with open(USERS_FILE, 'r', encoding='utf-8') as file: - users = json.load(file) - logging.info(f"Загружен локальный файл {USERS_FILE} после ошибки с репозиторием.") - if not isinstance(users, dict): return {} - return users - except (FileNotFoundError, json.JSONDecodeError): - logging.warning(f"Локальный файл {USERS_FILE} также не найден или поврежден. Возвращен пустой словарь.") - return {} except Exception as e: - logging.error(f"Непредвиденная ошибка при загрузке пользователей: {e}") + logging.error(f"Произошла ошибка при загрузке пользователей: {e}") 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}") - upload_files_to_hf([USERS_FILE]) # Загружаем только измененный файл + upload_db_to_hf() # Синхронизация после сохранения except Exception as e: - logging.error(f"Ошибка при сохранении пользователей: {e}") - # Не прерываем работу приложения, но логируем ошибку + logging.error(f"Ошибка при сохранении пользователей в {USERS_FILE}: {e}") + # --- Функции синхронизации с Hugging Face --- -def upload_files_to_hf(filenames=SYNC_FILES): - """Загрузка указанных файлов на Hugging Face.""" +def upload_db_to_hf(): + """Загрузка файлов данных на Hugging Face.""" if not HF_TOKEN_WRITE: - logging.warning("HF_TOKEN (write) не установлен. Загрузка на Hugging Face отключена.") + logging.warning("Переменная окружения HF_TOKEN (WRITE) не установлена. Загрузка на Hugging Face отключена.") return try: api = HfApi() - for file_name in filenames: + for file_name in SYNC_FILES: if os.path.exists(file_name): + logging.info(f"Начинается загрузка файла {file_name} на Hugging Face...") 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"Автоматическое обновление файла {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + commit_message=f"Автоматическое резервное копирование {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" ) - logging.info(f"Файл {file_name} успешно загружен на Hugging Face.") + logging.info(f"Резервная копия {file_name} успешно загружена на Hugging Face.") else: - logging.warning(f"Файл {file_name} не найден локально для загрузки.") + logging.warning(f"Файл {file_name} не найден для загрузки на Hugging Face.") except Exception as e: - logging.error(f"Ошибка при загрузке файлов на Hugging Face: {e}") + logging.error(f"Ошибка при загрузке резервной копии на Hugging Face: {e}") -def download_files_from_hf(filenames=SYNC_FILES): - """Скачивание указанных файлов с Hugging Face.""" +def download_db_from_hf(): + """Скачивание файлов данных с Hugging Face.""" if not HF_TOKEN_READ: - logging.warning("HF_TOKEN_READ не установлен. Скачивание с Hugging Face отключено.") - # Важно: Если нет токена на чтение, нельзя просто продолжать, - # т.к. мы можем перезаписать свежие локальные данные старыми. - # Поэтому просто выходим, полагаясь на локальные файлы. - return + logging.warning("Переменная окружения HF_TOKEN_READ не установлена. Скачивание с Hugging Face может быть недоступно для приватных репозиториев.") + # Можно продолжать без токена для публичных репо + # return # Раскомментировать, если чтение без токена не нужно + + logging.info("Попытка скачивания файлов с Hugging Face...") + api = HfApi() # Создаем объект API здесь + for file_name in SYNC_FILES: + try: + hf_hub_download( + repo_id=REPO_ID, + filename=file_name, + repo_type="dataset", + token=HF_TOKEN_READ, # Может быть None, если HF_TOKEN_READ не установлен + local_dir=".", + local_dir_use_symlinks=False, + force_download=True, # Принудительно скачиваем для обновления + resume_download=False # Не возобновляем, скачиваем заново + ) + logging.info(f"Файл {file_name} успешно скачан/обновлен из Hugging Face.") + except RepositoryNotFoundError: + logging.error(f"Репозиторий {REPO_ID} не найден на Hugging Face.") + # Не прерываем цикл, пытаемся скачать другие файлы + continue # Переходим к следующему файлу + except Exception as e: + # Логируем ошибку для конкретного файла, но не падаем + logging.error(f"Ошибка при скачивании файла {file_name} с Hugging Face: {e}. Пропускаем.") + # Если файл не скачался, будем использовать локальную версию (если есть) - try: - api = HfApi() # Api для скачивания не нужна, но оставим для консистентности - logging.info(f"Попытка скачивания файлов: {filenames}") - for file_name in filenames: - try: - 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.") - except RepositoryNotFoundError as e: - logging.error(f"Репозиторий '{REPO_ID}' не найден при попытке скачать {file_name}: {e}") - raise # Передаем ошибку выше, чтобы load_data/load_users обработали ее - except Exception as e: # Ловим другие возможные ошибки скачивания файла - if "404 Client Error" in str(e) and "Entry not found" in str(e): - logging.warning(f"Файл {file_name} не найден в репозитории {REPO_ID}. Пропускаем скачивание.") - else: - logging.error(f"Ошибка при скачивании файла {file_name}: {e}") - # Не прерываем скачивание других файлов, но логируем ошибку - except RepositoryNotFoundError: - # Эта ошибка уже обработана в load_data/load_users, просто логируем здесь для полноты - logging.error(f"Репозиторий '{REPO_ID}' не найден. Скачивание файлов невозможно.") - raise # Передаем ошибку выше - except Exception as e: - logging.error(f"Общая ошибка при скачи��ании файлов: {e}") - # Не прерываем работу, но логируем def periodic_backup(): - """Периодическая загрузка всех файлов на HF.""" - logging.info("Запуск потока периодического резервного копирования.") + """Периодическая загрузка данных на Hugging Face.""" while True: - time.sleep(1800) # Увеличено до 30 минут (1800 секунд) - logging.info("Выполнение периодического резервного копирования...") - upload_files_to_hf(SYNC_FILES) + logging.info("Запуск периодического резервного копирования...") + upload_db_to_hf() logging.info("Периодическое резервное копирование завершено.") - + time.sleep(1800) # Увеличил интервал до 30 минут (1800 секунд) # --- Маршруты Flask --- @app.route('/') def catalog(): - """Главная страница каталога.""" + """Главная страница каталога товаров.""" data = load_data() products = data.get('products', []) categories = data.get('categories', []) @@ -227,137 +201,237 @@ def catalog(): - Soola Cosmetics - Каталог {/* Название изменено */} + Soola Cosmetics - Каталог
-

Soola Cosmetics

{/* Название изменено */} +

Soola Cosmetics

+ +
+ +
+ Наш адрес: {{ store_address }}
@@ -366,58 +440,60 @@ def catalog(): {% endfor %}
+
- +
+
- {% if products %} - {% for product in products %} -
+ {% for product in products %} +
+ +
{% if product.get('photos') and product['photos']|length > 0 %} -
- {{ product['name'] }} {# Обработка ошибки загрузки фото #} -
+ {{ product['name'] }} {% else %} -
- Нет фото -
+ {# ��конка-заглушка #} {% endif %} -
-
-

{{ product['name'] }}

- {% if is_authenticated %} -
{{ "%.2f"|format(product['price']) }} {{ currency_code }}
{/* Цена в KGS */} - {% else %} -
Цена по запросу
{/* Изменено сообщение */} - {% endif %} -

{{ product.get('description', 'Нет описания') }}

{# Добавлено .get с default #} -
-
- - {% if is_authenticated %} - - {% endif %} -
-
- {% endfor %} - {% else %} -

Товары не найдены.

+ +

{{ product['name'] }}

+ + {% if is_authenticated %} +
{{ product['price'] }} {{ currency_code }}
+ {% else %} +
Цена по запросу
+ {% endif %} + +

{{ product['description'] }}

+ +
+ + {% if is_authenticated %} + + {% endif %} +
+
+ {% endfor %} + {% if not products %} +

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

{% endif %}
-
Наш адрес: {{ store_address }}
{/* Один адрес */}
@@ -425,82 +501,108 @@ def catalog(): -
''' - return render_template_string( - admin_html, - products=products, - categories=categories, - repo_id=REPO_ID, - users=users, - # kgs_to_usd=kgs_to_usd, # Удалено - # convert_price=convert_price # Удалено - currency_code=CURRENCY_CODE # Передаем код валюты - ) + return render_template_string(admin_html, + products=products, + categories=categories, + repo_id=REPO_ID, + users=users, + currency_code=CURRENCY_CODE, # Передаем код валюты + admin_message=admin_message) # Передаем сообщение + + +# --- Вспомогательные функции --- + +def upload_photos(photos_files, product_name): + """Загружает фотографии на Hugging Face и возвращает список имен файлов.""" + photos_list = [] + if not HF_TOKEN_WRITE: + logging.warning("HF_TOKEN (WRITE) не установлен. Загрузка фото отключена.") + return [] + + if photos_files and any(f.filename for f in photos_files): + uploads_dir = 'uploads_temp' # Временная папка + os.makedirs(uploads_dir, exist_ok=True) + api = HfApi() + for photo in photos_files[:10]: # Ограничение на 10 фото + if photo and photo.filename: + try: + filename = secure_filename(f"{product_name.replace(' ', '_')}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}_{photo.filename}") + temp_path = os.path.join(uploads_dir, filename) + photo.save(temp_path) # Сохраняем локально временно + + # Загружаем на HF + path_in_repo = f"photos/{filename}" + logging.info(f"Загрузка фото: {temp_path} -> {path_in_repo}") + api.upload_file( + path_or_fileobj=temp_path, + path_in_repo=path_in_repo, + repo_id=REPO_ID, + repo_type="dataset", + token=HF_TOKEN_WRITE, + commit_message=f"Добавлено/обновлено фото для товара {product_name}" + ) + photos_list.append(filename) + logging.info(f"Фото {filename} успешно загружено.") + + # Удаляем временный файл + if os.path.exists(temp_path): + os.remove(temp_path) + + except Exception as e: + logging.error(f"Ошибка при загрузке фото {photo.filename}: {e}") + # Пытаемся удалить временный файл, если он остался + if os.path.exists(temp_path): + try: os.remove(temp_path) + except OSError: pass + continue # Пропускаем это фото + + # Удаляем временную директорию, если она пуста + try: + if not os.listdir(uploads_dir): + os.rmdir(uploads_dir) + except OSError: + logging.warning(f"Не удалось удалить временную директорию {uploads_dir}") + + return photos_list + +# Маршруты для ручной синхронизации из админки @app.route('/backup', methods=['POST']) def backup(): - """Принудительная загрузка всех файлов на HF.""" - # Проверка прав админа - if session.get('user') != 'admin': # Или другая проверка прав - return "Доступ запрещен.", 403 - logging.info(f"Админ '{session.get('user')}' инициировал принудительную загрузку на HF.") - upload_files_to_hf(SYNC_FILES) - # Добавить сообщение об успехе/ошибке через flash или параметр - return redirect(url_for('admin')) # Возвращаемся в админку - - -@app.route('/download', methods=['GET']) # Изменено на GET для простоты вызова кнопкой -def download(): - """Принудительное скачивание всех файлов с HF.""" - # Проверка прав админа - if session.get('user') != 'admin': # Или другая проверка прав - return "Доступ запрещен.", 403 - logging.info(f"Админ '{session.get('user')}' инициировал принудительное скачивание с HF.") + """Принудительная загрузка данных на HF (из админки).""" + if session.get('user') != 'admin': + return "Доступ запрещен", 403 + logging.info("Запущена принудительная загрузка на HF из админ-панели.") + upload_db_to_hf() + session['admin_message'] = "Резервная копия успешно загружена на Hugging Face." + return redirect(url_for('admin')) + +@app.route('/download_admin', methods=['POST']) +def download_admin(): + """Принудительное скачивание данных с HF (из админки).""" + if session.get('user') != 'admin': + return "Доступ запрещен", 403 + logging.info("Запущено принудительное скачивание с HF из админ-панели.") try: - download_files_from_hf(SYNC_FILES) - # Добавить сообщение об успехе + download_db_from_hf() + session['admin_message'] = "Данные успешно скачаны/обновлены с Hugging Face. Страница перезагружена." except Exception as e: logging.error(f"Ошибка при принудительном скачивании с HF: {e}") - # Добавить сообщение об ошибке - return redirect(url_for('admin')) # Возвращаемся в админку + session['admin_message'] = f"Ошибка при скачивании данных: {e}" + return redirect(url_for('admin')) # Перезагружаем админку, чтобы отобразить скачанные данные # --- Запуск приложения --- - if __name__ == '__main__': - # Проверяем наличие необходимых токенов HF при запуске - if not HF_TOKEN_WRITE: - logging.warning("Переменная окружения HF_TOKEN (для записи) не установлена. Загрузка на Hugging Face будет невозможна.") - if not HF_TOKEN_READ: - logging.warning("Переменная окружения HF_TOKEN_READ (для чтения) не установлена. Скачивание с Hugging Face будет невозможно.") - - # Запускаем поток для периодического резервного копирования - # Убедимся, что он запускается только один раз, не при перезагрузках Flask debug - if os.environ.get("WERKZEUG_RUN_MAIN") == "true" or not app.debug: - backup_thread = threading.Thread(target=periodic_backup, daemon=True) - backup_thread.start() - - # Первоначальная загрузка данных при старте + # Попытка загрузить данные при старте (включая скачивание с HF) + print("Инициализация приложения Soola Cosmetics...") try: - logging.info("Первоначальная загрузка данных при старте приложения...") - load_data() + print("Попытка загрузить данные при старте...") + load_data() # Выполняем начальную загрузку/синхронизацию load_users() - logging.info("Первоначальная загрузка данных завершена.") + print("Начальная загрузка данных завершена.") except Exception as e: - logging.error(f"Критическая ошибка при первоначальной загрузке данных: {e}", exc_info=True) - # Решить, стоит ли прерывать запуск или продолжать с пустыми данными + logging.error(f"Не удалось инициализировать базу данных при старте: {e}", exc_info=True) + print(f"!!! ВНИМАНИЕ: Не удалось загрузить данные при старте: {e}") + print("Приложение запустится с пустыми данными или последней локальной копией.") + + # Запуск потока для периодического резервного копирования + print("Запуск потока для периодического резервного копирования...") + backup_thread = threading.Thread(target=periodic_backup, daemon=True) + backup_thread.start() - # Запуск Flask приложения - # Используйте host='0.0.0.0' для доступности в локальной сети - # debug=True удобно для разработки, но должно быть False в продакшене - app.run(debug=True, host='0.0.0.0', port=7860) + print("Запуск Flask приложения...") + # Используйте Gunicorn или Waitress для продакшена вместо встроенного сервера Flask + app.run(debug=False, host='0.0.0.0', port=7860) # debug=False для продакшена - \ No newline at end of file