# -*- coding: utf-8 -*-
from flask import Flask, render_template_string, request, redirect, url_for, send_file
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
from urllib.parse import quote
app = Flask(__name__)
DATA_FILE = 'data.json'
REPO_ID = "Kgshop/Mebelhause"
HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
LOGO_URL = "https://huggingface.co/spaces/Mebelhause/Kg/resolve/main/Screenshot_20250411-112027.png"
WHATSAPP_NUMBER = "+996700253966"
logging.basicConfig(level=logging.INFO)
def load_data():
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:
logging.warning("Структура данных некорректна, используется структура по умолчанию.")
return {'products': [], 'categories': []}
# Ensure products and categories are lists
if not isinstance(data.get('products'), list):
data['products'] = []
if not isinstance(data.get('categories'), list):
data['categories'] = []
return data
except FileNotFoundError:
logging.warning("Локальный файл базы данных не найден. Создание пустой базы.")
return {'products': [], 'categories': []}
except json.JSONDecodeError:
logging.error("Ошибка: Невозможно декодировать JSON файл. Возврат пустой базы.")
return {'products': [], 'categories': []}
except RepositoryNotFoundError:
logging.error("Репозиторий Hugging Face не найден. Создание локальной базы данных.")
# Create an empty file if it doesn't exist locally after repo not found
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 Exception as e:
logging.error(f"Произошла ошибка при загрузке данных: {e}. Возврат пустой базы.")
return {'products': [], 'categories': []}
def save_data(data):
try:
# Ensure structure is correct before saving
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'] = []
with open(DATA_FILE, 'w', encoding='utf-8') as file:
json.dump(data, file, ensure_ascii=False, indent=4)
logging.info("Данные успешно сохранены в JSON")
upload_db_to_hf()
except Exception as e:
logging.error(f"Ошибка при сохранении данных: {e}")
# Optionally re-raise or handle differently
# raise
def upload_db_to_hf():
if not HF_TOKEN_WRITE:
logging.warning("HF_TOKEN (write) не установлен. Загрузка на Hugging Face пропущена.")
return
if not os.path.exists(DATA_FILE):
logging.warning(f"Файл {DATA_FILE} не найден для загрузки.")
return
try:
api = HfApi()
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 RepositoryNotFoundError:
logging.error(f"Репозиторий {REPO_ID} не найден на Hugging Face. Не удалось загрузить.")
except Exception as e:
logging.error(f"Ошибка при загрузке резервной копии на Hugging Face: {e}")
def download_db_from_hf():
if not HF_TOKEN_READ:
logging.warning("HF_TOKEN_READ не установлен. Скачивание с Hugging Face пропущено.")
# Attempt to create an empty file if none exists
if not os.path.exists(DATA_FILE):
save_data({'products': [], 'categories': []}) # Save an empty structure
return
try:
hf_hub_download(
repo_id=REPO_ID,
filename=DATA_FILE,
repo_type="dataset",
token=HF_TOKEN_READ,
local_dir=".",
local_dir_use_symlinks=False,
force_download=True, # Ensure we get the latest version
resume_download=False # Avoid issues with partial downloads
)
logging.info("JSON база успешно скачана из Hugging Face.")
# Verify file integrity after download
try:
with open(DATA_FILE, 'r', encoding='utf-8') as f:
json.load(f)
logging.info("Проверка целостности JSON после скачивания: OK")
except (json.JSONDecodeError, FileNotFoundError) as e:
logging.error(f"Ошибка проверки JSON после скачивания: {e}. Файл может быть поврежден или пуст.")
# Attempt to create a default empty file if it's broken
save_data({'products': [], 'categories': []})
except RepositoryNotFoundError as e:
logging.error(f"Репозиторий {REPO_ID} не найден на Hugging Face: {e}")
if not os.path.exists(DATA_FILE):
save_data({'products': [], 'categories': []}) # Save an empty structure
# Do not raise here, allow fallback to local/empty data
except Exception as e:
# Catch specific hf_hub exceptions if needed, e.g., HTTPError
logging.error(f"Ошибка при скачивании JSON базы из Hugging Face: {e}")
if not os.path.exists(DATA_FILE):
save_data({'products': [], 'categories': []}) # Save an empty structure
# Do not raise here, allow fallback to local/empty data
def periodic_backup():
while True:
time.sleep(800) # Sleep first to avoid immediate backup on start
logging.info("Запуск периодического резервного копирования...")
# Ensure data is loaded before backup attempt
try:
load_data() # Reload potentially updated data
except Exception as e:
logging.error(f"Ошибка загрузки данных перед бэкапом: {e}")
# Now attempt upload
upload_db_to_hf()
# --- Global Styles ---
GLOBAL_STYLES = """
"""
# --- Landing Page ---
@app.route('/')
def landing():
landing_html = '''
Mebel Hause KG - Изготовление мебели на заказ
''' + GLOBAL_STYLES + '''
Mebel Hause KG
Создаем мебель вашей мечты с любовью и мастерством
'''
return render_template_string(detail_html, product=product, repo_id=REPO_ID)
# --- Admin Panel ---
@app.route('/admin', methods=['GET', 'POST'])
def admin():
data = load_data()
products = data.get('products', [])
categories = data.get('categories', [])
message = None # To display success/error messages
if request.method == 'POST':
action = request.form.get('action')
logging.debug(f"Admin action received: {action}")
logging.debug(f"Form data: {request.form}")
logging.debug(f"Files data: {request.files}")
try:
if action == 'add_category':
category_name = request.form.get('category_name', '').strip()
if category_name and category_name not in categories:
categories.append(category_name)
save_data(data)
message = ("Категория добавлена", "success")
elif not category_name:
message = ("Название категории не может быть пустым", "error")
else:
message = ("Категория уже существует", "error")
return redirect(url_for('admin', msg=message[0], type=message[1]))
elif action == 'delete_category':
category_index_str = request.form.get('category_index')
if category_index_str is not None:
category_index = int(category_index_str)
if 0 <= category_index < len(categories):
deleted_category = categories.pop(category_index)
# Update products using this category
for product in products:
if product.get('category') == deleted_category:
product['category'] = 'Без категории' # Assign to default
save_data(data)
message = ("Категория удалена", "success")
else:
message = ("Неверный индекс категории", "error")
else:
message = ("Индекс категории не указан", "error")
return redirect(url_for('admin', msg=message[0], type=message[1]))
elif action == 'add' or action == 'edit':
name = request.form.get('name', '').strip()
price_str = request.form.get('price', '0').replace(',', '.')
description = request.form.get('description', '').strip()
category = request.form.get('category')
photos_files = request.files.getlist('photos')
# Get colors - handle empty strings and potential duplicates
colors = [c.strip() for c in request.form.getlist('colors') if c.strip()]
colors = sorted(list(set(colors))) # Remove duplicates and sort
logging.debug(f"Received colors: {request.form.getlist('colors')}")
logging.debug(f"Processed colors: {colors}")
if not name or not description:
message = ("Название и описание товара обязательны", "error")
# Need to pass current state back if redirecting
return redirect(url_for('admin', msg=message[0], type=message[1]))
try:
price = float(price_str)
if price < 0: raise ValueError("Price cannot be negative")
except ValueError:
message = ("Некорректное значение цены", "error")
return redirect(url_for('admin', msg=message[0], type=message[1]))
photos_list = []
# Upload new photos if provided
if photos_files and any(f.filename for f in photos_files):
if not HF_TOKEN_WRITE:
message = ("HF_TOKEN (write) не установлен. Невозможно загрузить фото.", "warning")
# Decide if you want to proceed without photos or stop
# return redirect(url_for('admin', msg=message[0], type=message[1]))
else:
api = HfApi()
uploads_dir = 'uploads_temp' # Temporary local storage
os.makedirs(uploads_dir, exist_ok=True)
for photo in photos_files[:10]: # Limit photos
if photo and photo.filename:
try:
photo_filename = secure_filename(f"{datetime.now().strftime('%Y%m%d%H%M%S')}_{photo.filename}")
temp_path = os.path.join(uploads_dir, photo_filename)
photo.save(temp_path)
logging.info(f"Uploading photo {photo_filename} to HF...")
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"Фото для товара {name}"
)
photos_list.append(photo_filename)
logging.info(f"Photo {photo_filename} uploaded successfully.")
# Clean up local temp file
if os.path.exists(temp_path):
os.remove(temp_path)
except Exception as e:
logging.error(f"Ошибка загрузки фото {photo.filename}: {e}")
message = (f"Ошибка загрузки фото {photo.filename}", "error")
# Decide whether to stop or continue
# Clean up temp directory if empty
if os.path.exists(uploads_dir) and not os.listdir(uploads_dir):
os.rmdir(uploads_dir)
if action == 'add':
new_product = {
'name': name,
'price': price,
'description': description,
'category': category if category in categories else 'Без категории',
'photos': photos_list, # Only new photos for 'add'
'colors': colors
}
products.append(new_product)
message = ("Товар успешно добавлен", "success")
elif action == 'edit':
index_str = request.form.get('index')
if index_str is not None:
index = int(index_str)
if 0 <= index < len(products):
# Update fields
products[index]['name'] = name
products[index]['price'] = price
products[index]['description'] = description
products[index]['category'] = category if category in categories else 'Без категории'
products[index]['colors'] = colors
# If new photos were uploaded, replace the old list. Otherwise, keep existing photos.
if photos_list:
# Optionally delete old photos from HF here if needed (more complex)
products[index]['photos'] = photos_list
# If no new photos uploaded, 'photos_list' is empty,
# and we don't touch products[index]['photos']
message = ("Товар успешно обновлен", "success")
else:
message = ("Неверный индекс товара для редактирования", "error")
else:
message = ("Индекс товара для редактирования не указан", "error")
elif action == 'delete':
index_str = request.form.get('index')
if index_str is not None:
index = int(index_str)
if 0 <= index < len(products):
# Optionally delete photos from HF here (more complex)
del products[index]
message = ("Товар удален", "success")
else:
message = ("Неверный индекс товара для удаления", "error")
else:
message = ("Индекс товара для удаления не указан", "error")
# Save changes if any action potentially modified data (except delete errors)
if message and message[1] != "error": # Save on success or warning
save_data(data)
# Redirect only after processing and potential save
return redirect(url_for('admin', msg=message[0] if message else None, type=message[1] if message else None))
except Exception as e:
logging.error(f"Ошибка в админ панели ({action}): {e}", exc_info=True)
message = (f"Произошла внутренняя ошибка: {e}", "error")
return redirect(url_for('admin', msg=message[0], type=message[1]))
# --- Admin HTML ---
flash_message = request.args.get('msg')
flash_type = request.args.get('type', 'info') # Default type
admin_html = '''
Админ-панель - Mebel Hause KG
''' + GLOBAL_STYLES + '''
{% if product.get('photos') and product['photos']|length > 0 %}
Фотографии:
{% for photo in product['photos'] %}
{% endfor %}
{% else %}
Фотографии: Нет
{% endif %}
Редактировать товар
{% endfor %}
{% else %}
В базе данных пока нет товаров.
{% endif %}
'''
return render_template_string(
admin_html,
products=products,
categories=categories,
repo_id=REPO_ID,
logo_url=LOGO_URL,
flash_message=flash_message,
flash_type=flash_type,
current_year=datetime.now().year
)
@app.route('/backup', methods=['POST'])
def backup():
try:
upload_db_to_hf()
# Redirect back with success message
return redirect(url_for('admin', msg="Резервное копирование на Hugging Face запущено.", type="success"))
except Exception as e:
logging.error(f"Ошибка при ручном резервном копировании: {e}")
return redirect(url_for('admin', msg=f"Ошибка резервного копирования: {e}", type="error"))
@app.route('/download', methods=['GET'])
def download():
try:
# Optionally trigger a download from HF first to ensure local is up-to-date
# download_db_from_hf() # Uncomment if you want to force sync before download
if os.path.exists(DATA_FILE):
return send_file(DATA_FILE, as_attachment=True, download_name='mebelhause_database.json')
else:
return redirect(url_for('admin', msg="Локальный файл базы данных не найден.", type="error"))
except Exception as e:
logging.error(f"Ошибка при скачивании файла базы данных: {e}")
return redirect(url_for('admin', msg=f"Ошибка скачивания файла: {e}", type="error"))
if __name__ == '__main__':
# Ensure data file exists on start, try loading/downloading
try:
initial_data = load_data()
# If load_data created an empty structure due to errors, save it.
if not os.path.exists(DATA_FILE) or os.path.getsize(DATA_FILE) == 0:
save_data(initial_data)
except Exception as e:
logging.error(f"Критическая ошибка при инициализации базы данных: {e}")
# Consider exiting if the DB is crucial and cannot be loaded/created
# exit(1)
# Start periodic backup in a separate thread
if HF_TOKEN_WRITE and HF_TOKEN_READ: # Only run backup if tokens are set
backup_thread = threading.Thread(target=periodic_backup, daemon=True)
backup_thread.start()
else:
logging.warning("Токены Hugging Face (WRITE или READ) не установлены. Периодическое резервное копирование отключено.")
# Run Flask App
# Use 'waitress' for production instead of Flask's built-in server
# from waitress import serve
# serve(app, host='0.0.0.0', port=7860)
# For development:
app.run(debug=False, host='0.0.0.0', port=7860) # Set debug=False for production/waitress