from flask import Flask, render_template_string, request, redirect, url_for, send_file, Response 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 import zipfile import io import tempfile app = Flask(__name__) DATA_FILE = 'dataasdem.json' # Убедитесь, что REPO_ID указан в формате owner/repo_name REPO_ID = "flpolprojects/Clients" HF_TOKEN_WRITE = os.getenv("HF_TOKEN") # Токен для записи (нужен для загрузки DB и фото) HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") # Токен для чтения (нужен для скачивания DB и фото) # Если HF_TOKEN_READ не установлен, используем HF_TOKEN_WRITE для чтения if not HF_TOKEN_READ and HF_TOKEN_WRITE: HF_TOKEN_READ = HF_TOKEN_WRITE elif not HF_TOKEN_READ and not HF_TOKEN_WRITE: logging.error("Hugging Face токены HF_TOKEN или HF_TOKEN_READ не установлены.") # Приложение может работать локально, но загрузка/скачивание с HF не будет работать. LOGO_URL = "https://cdn-avatars.huggingface.co/v1/production/uploads/67b22aaeae9b6a59f1cfb849/NQvBksXzJItYt6hfFjyaB.jpeg" logging.basicConfig(level=logging.DEBUG) def load_data(): """Загружает данные из локального JSON файла, предварительно скачав его с HF.""" logging.info("Попытка загрузки данных.") try: download_db_from_hf() with open(DATA_FILE, 'r', encoding='utf-8') as file: data = json.load(file) logging.info("Данные успешно загружены из JSON.") # Проверка структуры данных, чтобы избежать ошибок NoneType if not isinstance(data, dict): logging.warning("Файл JSON имеет некорректную структуру (не словарь верхнего уровня).") return {'products': [], 'categories': []} if 'products' not in data or not isinstance(data['products'], list): logging.warning("В JSON отсутствует ключ 'products' или он не является списком.") data['products'] = [] if 'categories' not in data or not isinstance(data['categories'], list): logging.warning("В JSON отсутствует ключ 'categories' или он не является списком.") data['categories'] = [] return data except FileNotFoundError: logging.warning(f"Локальный файл базы данных '{DATA_FILE}' не найден после попытки скачивания.") return {'products': [], 'categories': []} except json.JSONDecodeError: logging.error(f"Ошибка: Невозможно декодировать JSON файл '{DATA_FILE}'. Файл может быть поврежден.") return {'products': [], 'categories': []} except RepositoryNotFoundError: logging.error(f"Репозиторий '{REPO_ID}' не найден на Hugging Face. Создание локальной пустой базы данных.") # Создаем пустую локальную базу данных, если репозиторий не существует initial_data = {'products': [], 'categories': []} try: with open(DATA_FILE, 'w', encoding='utf-8') as file: json.dump(initial_data, file, ensure_ascii=False, indent=4) logging.info(f"Создан пустой локальный файл базы данных '{DATA_FILE}'.") except Exception as save_err: logging.error(f"Ошибка при создании пустого локального файла базы данных: {save_err}") return initial_data except Exception as e: logging.error(f"Произошла ошибка при загрузке данных: {e}") return {'products': [], 'categories': []} def save_data(data): """Сохраняет данные в локальный JSON файл и загружает на HF.""" logging.info("Попытка сохранения данных.") try: # Сортируем продукты по времени добавления перед сохранением # Это поможет сохранить порядок добавления, хотя список в памяти сортируется отдельно для отображения if 'products' in data: data['products'].sort(key=lambda x: x.get('added_at', ''), reverse=True) with open(DATA_FILE, 'w', encoding='utf-8') as file: json.dump(data, file, ensure_ascii=False, indent=4) logging.info(f"Данные успешно сохранены в локальный JSON файл '{DATA_FILE}'.") upload_db_to_hf() except Exception as e: logging.error(f"Ошибка при сохранении данных: {e}") # В случае ошибки сохранения, лучше пробросить исключение, чтобы админ получил обратную связь raise def upload_db_to_hf(): """Загружает локальный JSON файл на Hugging Face Hub.""" if not HF_TOKEN_WRITE: logging.warning("HF_TOKEN_WRITE не установлен. Пропуск загрузки базы данных на Hugging Face.") return try: api = HfApi() logging.info(f"Загрузка файла '{DATA_FILE}' в репозиторий '{REPO_ID}' на Hugging Face.") api.upload_file( path_or_fileobj=DATA_FILE, path_in_repo=DATA_FILE, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, commit_message=f"Автоматическое резервное копирование базы данных {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" ) logging.info("Резервная копия JSON базы успешно загружена на Hugging Face.") except Exception as e: logging.error(f"Ошибка при загрузке резервной копии '{DATA_FILE}' на Hugging Face: {e}") def download_db_from_hf(): """Скачивает JSON файл базы данных с Hugging Face Hub.""" if not HF_TOKEN_READ: logging.warning("HF_TOKEN_READ не установлен. Пропуск скачивания базы данных с Hugging Face.") # Если токена нет, FileNotFoundError в load_data обработает отсутствие локального файла raise FileNotFoundError(f"Не удалось скачать базу данных '{DATA_FILE}' из-за отсутствия HF_TOKEN_READ.") try: logging.info(f"Скачивание файла '{DATA_FILE}' из репозитория '{REPO_ID}' на Hugging Face.") hf_hub_download( repo_id=REPO_ID, filename=DATA_FILE, repo_type="dataset", token=HF_TOKEN_READ, local_dir=".", # Скачиваем прямо в корневую директорию приложения local_dir_use_symlinks=False # Важно для надежности в разных окружениях ) logging.info(f"JSON база '{DATA_FILE}' успешно скачана из Hugging Face.") except RepositoryNotFoundError as e: logging.error(f"Репозиторий '{REPO_ID}' не найден. Невозможно скачать базу данных.") raise # Пробрасываем ошибку, чтобы load_data мог ее обработать except Exception as e: logging.error(f"Ошибка при скачивании JSON базы '{DATA_FILE}' из Hugging Face: {e}") # Пробрасываем ошибку, чтобы load_data мог ее обработать raise def periodic_backup(): """Выполняет периодическое резервное копирование.""" # Добавляем небольшую задержку при старте, чтобы приложение успело инициализироваться time.sleep(60) # Задержка в 60 секунд перед первым бэкапом while True: try: logging.info("Запуск периодического резервного копирования.") # Нет необходимости загружать данные, backup загружает локальный файл upload_db_to_hf() except Exception as e: logging.error(f"Ошибка во время периодического бэкапа: {e}") # Ждем 800 секунд (приблизительно 13.3 минуты) time.sleep(800) # --- Новая функция для загрузки фотографий и создания ZIP --- @app.route('/download_photos') def download_photos(): """ Скачивает все фотографии, указанные в базе данных, из репозитория HF Hub и упаковывает их в ZIP-архив для скачивания. """ logging.info("Получен запрос на скачивание всех фотографий.") data = load_data() all_photo_filenames = set() # Используем set для уникальных имен файлов # Собираем все уникальные имена файлов фотографий из продуктов if 'products' in data and isinstance(data['products'], list): for product in data['products']: if 'photos' in product and isinstance(product['photos'], list): for photo_filename in product['photos']: if photo_filename and isinstance(photo_filename, str): # Удаляем возможные пробелы или другие нежелательные символы cleaned_filename = photo_filename.strip() if cleaned_filename: all_photo_filenames.add(cleaned_filename) logging.info(f"Найдено {len(all_photo_filenames)} уникальных файлов фотографий в базе данных.") if not all_photo_filenames: logging.warning("В базе данных нет фотографий для скачивания. Создаю пустой ZIP.") # Если нет фотографий, создаем пустой ZIP с информационным файлом buffer = io.BytesIO() with zipfile.ZipFile(buffer, 'w') as zipf: zipf.writestr('info.txt', 'В базе данных не найдено фотографий для скачивания.') buffer.seek(0) return send_file(buffer, mimetype='application/zip', as_attachment=True, download_name='product_photos_empty.zip') # Используем BytesIO для создания ZIP архива в памяти buffer = io.BytesIO() # Используем TemporaryDirectory для безопасного скачивания файлов во временное место # Это автоматически удалит директорию и ее содержимое после выхода из блока with try: with tempfile.TemporaryDirectory() as temp_dir: logging.info(f"Создана временная директория для скачивания: {temp_dir}") downloaded_files_paths = [] # Скачиваем каждый файл фотографии for filename in all_photo_filenames: remote_path = f"photos/{filename}" local_path = os.path.join(temp_dir, filename) # Сохраняем во временной директории # Проверяем, существует ли файл уже во временной директории (на всякий случай) if os.path.exists(local_path): logging.debug(f"Файл уже существует во временной директории, пропуск скачивания: {local_path}") downloaded_files_paths.append(local_path) continue # Пропускаем скачивание, если файл уже есть logging.info(f"Попытка скачивания файла: {remote_path}") try: # Скачиваем файл из HF Hub в нашу временную директорию # cache_dir=None предотвращает кэширование во временной директории downloaded_file_path = hf_hub_download( repo_id=REPO_ID, filename=remote_path, repo_type="dataset", token=HF_TOKEN_READ, local_dir=temp_dir, local_dir_use_symlinks=False, cache_dir=None # Не используем кэш HF Hub для временных файлов ) # Проверяем, что hf_hub_download вернул путь и файл существует if downloaded_file_path and os.path.exists(downloaded_file_path): downloaded_files_paths.append(downloaded_file_path) logging.info(f"Файл успешно скачан и добавлен в список для архивации: {downloaded_file_path}") else: logging.warning(f"Скачивание файла {remote_path} не удалось или файл не найден по пути: {downloaded_file_path}") except Exception as e: # Логируем ошибку для этого файла, но продолжаем скачивать остальные logging.error(f"Ошибка скачивания файла {remote_path} из Hugging Face: {e}") # Файл не будет добавлен в downloaded_files_paths, если произошла ошибка if not downloaded_files_paths: logging.error("После попыток скачивания ни одного файла не было успешно загружено.") return "Ошибка: Не удалось скачать ни одной фотографии.", 500 # Возвращаем ошибку, если ни один файл не скачался # Создаем ZIP архив из скачанных файлов logging.info(f"Начинаю создание ZIP архива из {len(downloaded_files_paths)} скачанных файлов.") with zipfile.ZipFile(buffer, 'w', zipfile.ZIP_DEFLATED) as zipf: for local_path in downloaded_files_paths: # Добавляем файл в архив, используя только его базовое имя arcname = os.path.basename(local_path) try: zipf.write(local_path, arcname=arcname) logging.debug(f"Добавлен файл в ZIP: {arcname}") except Exception as e: logging.error(f"Ошибка при добавлении файла {local_path} в ZIP архив: {e}") # Можно продолжить, если ошибка только с одним файлом # Перемещаем указатель буфера в начало buffer.seek(0) logging.info("ZIP архив успешно создан в памяти. Отправляю файл пользователю.") # Отправляем ZIP файл пользователю return send_file( buffer, mimetype='application/zip', as_attachment=True, download_name='product_photos.zip' # Имя файла, которое увидит пользователь при скачивании ) except Exception as e: # Общая ошибка при работе с временной директорией или zip logging.error(f"Произошла общая ошибка при создании или скачивании ZIP архива: {e}") return f"Произошла ошибка при обработке фотографий: {e}", 500 # --- Конец новой функции --- @app.route('/') def catalog(): data = load_data() # Сортируем продукты по дате добавления для отображения новых сверху products = sorted(data.get('products', []), key=lambda x: x.get('added_at', ''), reverse=True) categories = data.get('categories', []) # HTML шаблон для каталога - остался прежним, кроме добавления LOGO_URL в header-logo catalog_html = ''' Asdem - нижнее белье оптом
# Используем переменную logo_url

Каталог

{% for category in categories %} # Экранируем категории {% endfor %}
{% for product in products %}
{% if product.get('photos') and product['photos']|length > 0 %}
{{ product['name']|e }}
{% else %}
No Image Available
{% endif %}

{{ product['name']|e }}

# Экранируем имя
{{ product['price']|e }} с
# Экранируем цену

{{ product['description'][:50]|e }}{% if product['description']|length > 50 %}...{% endif %}

# Экранируем описание
{% endfor %}
{% if not products %}

Нет товаров в каталоге.

{% endif %}
{# jQuery не используется, можно удалить #} {# Popper не используется, можно удалить #} {# Скрипты jQuery и Popper не используются в вашем текущем коде, их можно безопасно удалить #} {# #} {# #} ''' # Передаем logo_url как отдельную переменную, т.к. она используется напрямую в href return render_template_string(catalog_html, products=products, categories=categories, repo_id=REPO_ID, logo_url=LOGO_URL) @app.route('/product/') def product_detail(index): data = load_data() products = data.get('products', []) # Используем .get() с дефолтом [] try: product = products[index] except (IndexError, TypeError): # Добавляем TypeError на случай, если products не список logging.error(f"Попытка доступа к несуществующему индексу продукта: {index}") return "Продукт не найден", 404 # HTML шаблон для деталей продукта detail_html = '''

{{ product['name']|e }}

{# Экранируем имя #}
{% if product.get('photos') and product['photos']|length > 0 %} {% for photo in product['photos'] %}
{{ product['name']|e }}
{% endfor %} {% else %}
No Image Available
{% endif %}

Категория: {{ product.get('category', 'Без категории')|e }}

{# Экранируем категорию #}

Цена: {{ product.get('price', 'N/A')|e }} с

{# Экранируем цену, добавляем дефолт #}

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

{# Экранируем описание, добавляем дефолт #}

Доступные цвета: {{ product.get('colors', ['Не указан'])|join(', ')|e }}

{# Экранируем цвета, добавляем дефолт #}
''' return render_template_string(detail_html, product=product, repo_id=REPO_ID) @app.route('/admin', methods=['GET', 'POST']) def admin(): data = load_data() products = data.get('products', []) categories = data.get('categories', []) # Убедимся, что категории уникальны и сортированы (необязательно, но полезно) categories = sorted(list(set(c for c in categories if isinstance(c, str) and c.strip()))) # Обновляем данные после возможной очистки категорий data['categories'] = categories if request.method == 'POST': action = request.form.get('action') logging.debug(f"Админ: Получено действие POST - {action}") if action == 'add_category': category_name = request.form.get('category_name') if category_name and isinstance(category_name, str) and category_name.strip(): cleaned_name = category_name.strip() if cleaned_name not in categories: categories.append(cleaned_name) categories.sort() # Сортируем после добавления data['categories'] = categories # Обновляем в словаре data save_data(data) logging.info(f"Админ: Добавлена категория '{cleaned_name}'.") else: logging.warning(f"Админ: Попытка добавить существующую категорию '{cleaned_name}'.") # Всегда перенаправляем после POST запроса return redirect(url_for('admin')) else: logging.warning("Админ: Попытка добавить пустую или некорректную категорию.") return "Ошибка: Не указано название категории или оно некорректно.", 400 elif action == 'delete_category': try: category_index = int(request.form.get('category_index')) if 0 <= category_index < len(categories): deleted_category = categories.pop(category_index) # Обновляем в словаре data перед сохранением data['categories'] = categories # Проходим по продуктам и меняем удаленную категорию на "Без категории" for product in products: if product.get('category') == deleted_category: product['category'] = 'Без категории' save_data(data) logging.info(f"Админ: Удалена категория '{deleted_category}'.") return redirect(url_for('admin')) else: logging.warning(f"Админ: Попытка удалить категорию по некорректному индексу {category_index}.") return "Ошибка: Некорректный индекс категории.", 400 except (ValueError, TypeError): logging.error("Админ: Получен некорректный индекс категории для удаления.") return "Ошибка: Некорректный индекс категории.", 400 elif action == 'add': name = request.form.get('name', '').strip() price_str = request.form.get('price', '').strip() description = request.form.get('description', '').strip() category = request.form.get('category', 'Без категории').strip() photos_files = request.files.getlist('photos') colors_list = request.form.getlist('colors') # Получаем список цветов if not name or not price_str or not description: logging.warning("Админ: Попытка добавить товар с незаполненными обязательными полями.") return "Ошибка: Заполните все обязательные поля (Название, Цена, Описание).", 400 try: # Заменяем запятую на точку для корректного преобразования в float price = float(price_str.replace(',', '.')) if price < 0: raise ValueError("Цена не может быть отрицательной") except (ValueError, TypeError): logging.warning(f"Админ: Попытка добавить товар с некорректной ценой: '{price_str}'.") return "Ошибка: Некорректное значение цены.", 400 photos_list = [] if photos_files: logging.info(f"Админ: Получено {len(photos_files)} файлов для загрузки.") api = HfApi() uploads_dir = 'uploads' # Временная директория для сохранения перед загрузкой на HF os.makedirs(uploads_dir, exist_ok=True) # Убедимся, что директория существует for photo in photos_files[:10]: # Ограничиваем до 10 файлов if photo and photo.filename: original_filename = secure_filename(photo.filename) # Добавляем timestamp к имени файла, чтобы избежать коллизий timestamp = datetime.now().strftime("%Y%m%d%H%M%S%f") photo_filename = f"{timestamp}_{original_filename}" temp_path = os.path.join(uploads_dir, photo_filename) try: photo.save(temp_path) logging.debug(f"Админ: Файл сохранен локально: {temp_path}") # Загружаем файл на Hugging Face logging.info(f"Админ: Загрузка фото '{photo_filename}' на Hugging Face...") api.upload_file( path_or_fileobj=temp_path, path_in_repo=f"photos/{photo_filename}", # Указываем путь внутри репозитория repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, commit_message=f"Добавлено фото {photo_filename} для товара '{name}'" ) logging.info(f"Админ: Фото '{photo_filename}' успешно загружено на Hugging Face.") photos_list.append(photo_filename) # Добавляем имя файла в список для JSON except Exception as e: logging.error(f"Админ: Ошибка загрузки фото '{original_filename}' на Hugging Face: {e}") # Продолжаем, даже если один файл не загрузился finally: # Удаляем временный файл после попытки загрузки if os.path.exists(temp_path): os.remove(temp_path) logging.debug(f"Админ: Временный файл удален: {temp_path}") # Очищаем и фильтруем список цветов cleaned_colors = [color.strip() for color in colors_list if color and color.strip()] new_product = { 'name': name, 'price': price, 'description': description, # Проверяем, существует ли выбранная категория, иначе "Без категории" 'category': category if category in categories else 'Без категории', 'photos': photos_list, 'colors': cleaned_colors, 'added_at': datetime.now().isoformat() # Добавляем метку времени } products.append(new_product) save_data(data) # Сохраняем обновленные данные logging.info(f"Админ: Товар '{name}' успешно добавлен.") return redirect(url_for('admin')) # Перенаправляем на ту же страницу после добавления elif action == 'edit': try: index = int(request.form.get('index')) if not (0 <= index < len(products)): logging.warning(f"Админ: Попытка редактирования продукта по некорректному индексу {index}.") return "Ошибка: Некорректный индекс продукта.", 400 product = products[index] # Получаем продукт по индексу name = request.form.get('name', '').strip() price_str = request.form.get('price', '').strip() description = request.form.get('description', '').strip() category = request.form.get('category', 'Без категории').strip() photos_files = request.files.getlist('photos') colors_list = request.form.getlist('colors') if not name or not price_str or not description: logging.warning(f"Админ: Попытка редактирования товара {index} с незаполненными обязательными полями.") return "Ошибка: Заполните все обязательные поля (Название, Цена, Описание).", 400 try: price = float(price_str.replace(',', '.')) if price < 0: raise ValueError("Цена не может быть отрицательной") except (ValueError, TypeError): logging.warning(f"Админ: Попытка редактирования товара {index} с некорректной ценой: '{price_str}'.") return "Ошибка: Некорректное значение цены.", 400 # Обновляем текстовые поля и категорию product['name'] = name product['price'] = price product['description'] = description product['category'] = category if category in categories else 'Без категории' # Обновляем цвета product['colors'] = [color.strip() for color in colors_list if color and color.strip()] # Обработка новых фотографий (заменяем существующие, если новые загружены) if photos_files and any(photo.filename for photo in photos_files): logging.info(f"Админ: Получено {len(photos_files)} новых файлов для товара {index} для загрузки.") new_photos_list = [] api = HfApi() uploads_dir = 'uploads' os.makedirs(uploads_dir, exist_ok=True) for photo in photos_files[:10]: if photo and photo.filename: original_filename = secure_filename(photo.filename) timestamp = datetime.now().strftime("%Y%m%d%H%M%S%f") photo_filename = f"{timestamp}_{original_filename}" temp_path = os.path.join(uploads_dir, photo_filename) try: photo.save(temp_path) logging.debug(f"Админ: Файл сохранен локально: {temp_path}") logging.info(f"Админ: Загрузка нового фото '{photo_filename}' на Hugging Face...") api.upload_file( path_or_fileobj=temp_path, path_in_repo=f"photos/{photo_filename}", repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, commit_message=f"Обновлено фото {photo_filename} для товара '{name}' (индекс {index})" ) logging.info(f"Админ: Новое фото '{photo_filename}' успешно загружено.") new_photos_list.append(photo_filename) # Добавляем новое имя в список except Exception as e: logging.error(f"Админ: Ошибка загрузки нового фото '{original_filename}' для товара {index}: {e}") finally: if os.path.exists(temp_path): os.remove(temp_path) logging.debug(f"Админ: Временный файл удален: {temp_path}") # Заменяем старый список фото на новый product['photos'] = new_photos_list logging.info(f"Админ: Список фото для товара {index} обновлен.") save_data(data) # Сохраняем обновленные данные logging.info(f"Админ: Товар '{name}' (индекс {index}) успешно обновлен.") return redirect(url_for('admin')) # Перенаправляем после редактирования except (ValueError, TypeError): logging.error("Админ: Получен некорректный индекс продукта для редактирования.") return "Ошибка: Некорректный индекс продукта.", 400 elif action == 'delete': try: index = int(request.form.get('index')) if 0 <= index < len(products): deleted_product_name = products[index]['name'] del products[index] save_data(data) # Сохраняем обновленные данные logging.info(f"Админ: Товар '{deleted_product_name}' (индекс {index}) успешно удален.") return redirect(url_for('admin')) # Перенаправляем после удаления else: logging.warning(f"Админ: Попытка удаления продукта по некорректному индексу {index}.") return "Ошибка: Некорректный индекс продукта.", 400 except (ValueError, TypeError): logging.error("Админ: Получен некорректный индекс продукта для удаления.") return "Ошибка: Некорректный индекс продукта.", 400 # Если action не распознан logging.warning(f"Админ: Получен нераспознанный action POST: {action}") return "Неизвестное действие.", 400 # GET запрос - отображение админ-панели admin_html = ''' Админ-панель
{# Используем переменную logo_url #}

Админ-панель

Добавление товара

Управление категориями

Список категорий (всего: {{ categories|length }})

{# Показываем количество #} {% if categories %}
{% for category in categories %}

{{ category|e }}

{# Экранируем #}
{# Используем inline-block #} {# Добавляем подтверждение #}
{% endfor %}
{% else %}

Нет добавленных категорий.

{% endif %}

Управление базой данных и файлами

{# Обновленный заголовок #}
{# Группируем кнопки #}
{# Ссылка на новый эндпоинт #}

Список товаров (всего: {{ products|length }})

{# Показываем количество #} {% if products %}
{% for product in products %}

{{ product['name']|e }}

{# Экранируем #}

Категория: {{ product.get('category', 'Без категории')|e }}

{# Экранируем #}

Цена: {{ product.get('price', 'N/A')|e }} с

{# Экранируем #}

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

{# Экранируем #}

Цвета: {{ product.get('colors', ['Не указан'])|join(', ')|e }}

{# Экранируем #} {% if product.get('photos') and product['photos']|length > 0 %}
{# Применяем класс для галереи фото #} {% for photo in product['photos'] %} {{ product['name']|e }} {% endfor %}
{% endif %}
Редактировать
{# Экранируем #} {# Экранируем #} {# Экранируем #} {# Уточняем #}
{% for color in product.get('colors', []) %}
{# Экранируем #}
{% endfor %} {# Если нет цветов, добавляем один пустой инпут при открытии для удобства #} {% if not product.get('colors') %}
{% endif %}
{# Выравниваем кнопку удаления #}
{# Используем inline-block #} {# Добавляем подтверждение #}
{% endfor %}
{% else %}

Нет добавленных товаров.

{% endif %}
''' # Передаем logo_url как отдельную переменную return render_template_string(admin_html, products=products, categories=categories, repo_id=REPO_ID, logo_url=LOGO_URL) @app.route('/backup', methods=['POST']) def backup(): """Вручную создает резервную копию базы данных на Hugging Face.""" logging.info("Получен запрос на ручное резервное копирование.") try: # Нет необходимости загружать данные здесь, просто загружаем локальный файл upload_db_to_hf() return "Резервная копия базы данных успешно создана на Hugging Face.", 200 except Exception as e: logging.error(f"Ошибка ручного резервного копирования: {e}") return f"Произошла ошибка при создании резервной копии: {e}", 500 @app.route('/download', methods=['GET']) def download(): """Вручную скачивает базу данных с Hugging Face.""" logging.info("Получен запрос на ручное скачивание базы данных.") try: download_db_from_hf() # Вместо простого сообщения, отправляем сам файл для скачивания return send_file(DATA_FILE, as_attachment=True, download_name=DATA_FILE) except FileNotFoundError: return f"Файл '{DATA_FILE}' не найден локально после попытки скачивания. Репозиторий может быть пуст или не существует.", 404 except RepositoryNotFoundError: return f"Репозиторий '{REPO_ID}' не найден на Hugging Face.", 404 except Exception as e: logging.error(f"Ошибка ручного скачивания базы данных: {e}") return f"Произошла ошибка при скачивании базы данных: {e}", 500 if __name__ == '__main__': # Запускаем периодический бэкап в отдельном потоке logging.info("Запуск потока периодического резервного копирования.") backup_thread = threading.Thread(target=periodic_backup, daemon=True) backup_thread.start() # При старте приложения пытаемся загрузить данные logging.info("Попытка загрузки данных при старте приложения.") try: load_data() logging.info("Данные успешно загружены или инициализированы при старте.") except Exception as e: logging.error(f"Критическая ошибка при загрузке/инициализации базы данных при старте: {e}") # Приложение может продолжить работу с пустой базой, но нужно уведомить # Запускаем Flask приложение logging.info("Запуск Flask приложения.") # debug=True следует использовать только при разработке. # Для production используйте продакшн-сервер (Gunicorn, uWSGI и т.д.) app.run(debug=True, host='0.0.0.0', port=7860)