diff --git "a/app.py" "b/app.py"
deleted file mode 100644--- "a/app.py"
+++ /dev/null
@@ -1,3061 +0,0 @@
-
-# -*- coding: utf-8 -*-
-from flask import Flask, render_template_string, request, redirect, url_for, send_from_directory, flash
-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, HFValidationError # Import specific errors
-from werkzeug.utils import secure_filename
-import urllib.parse
-
-app = Flask(__name__)
-# It's highly recommended to set a secret key for flash messages and sessions
-app.secret_key = os.environ.get('FLASK_SECRET_KEY', 'a_default_secret_key_change_me') # CHANGE THIS in production!
-
-DATA_FILE = 'data.json'
-UPLOADS_DIR = 'uploads' # Directory for temporary uploads
-
-# Ensure uploads directory exists
-os.makedirs(UPLOADS_DIR, exist_ok=True)
-
-# Настройки Hugging Face
-REPO_ID = "Kgshop/Mebelhause" # Используем ваш репозиторий
-HF_TOKEN_WRITE = os.getenv("HF_TOKEN") # Use HF_TOKEN for write access
-HF_TOKEN_READ = os.getenv("HF_TOKEN_READ", HF_TOKEN_WRITE) # Use separate READ token if provided, otherwise fallback to WRITE token
-
-# Ссылка на логотип (обновлено)
-LOGO_URL = "https://huggingface.co/spaces/Mebelhause/Kg/resolve/main/Screenshot_20250411-112027.png"
-WHATSAPP_NUMBER = "+996700253966" # Номер для WhatsApp
-
-# Настройка логирования
-logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
-
-# --- Функции работы с данными и Hugging Face ---
-
-# Global lock for file access
-data_lock = threading.Lock()
-
-def download_db_from_hf():
- """Скачивает файл базы данных с Hugging Face."""
- if not HF_TOKEN_READ:
- logging.warning("HF_TOKEN_READ не установлен. Пропуск скачивания с Hugging Face.")
- return False
- try:
- logging.info(f"Попытка скачивания {DATA_FILE} из {REPO_ID} (dataset)")
- hf_hub_download(
- repo_id=REPO_ID,
- filename=DATA_FILE,
- repo_type="dataset", # Explicitly dataset
- token=HF_TOKEN_READ,
- local_dir=".",
- local_dir_use_symlinks=False, # Recommended for Spaces/Docker
- force_download=True, # Принудительно скачивать свежую версию
- etag_timeout=10 # Уменьшить таймаут для проверки etag
- )
- logging.info(f"Файл {DATA_FILE} успешно скачан из Hugging Face.")
- return True
- except RepositoryNotFoundError:
- logging.error(f"Репозиторий {REPO_ID} (dataset) не найден на Hugging Face.")
- return False
- except HFValidationError as e:
- logging.error(f"Ошибка валидации Hugging Face (возможно, неверный токен?): {e}")
- return False
- except Exception as e:
- # Check for common HTTP errors within the exception string
- if "401 Client Error" in str(e):
- logging.error(f"Ошибка аутентификации (401) при скачивании с Hugging Face. Проверьте HF_TOKEN_READ. Детали: {e}")
- elif "404 Client Error" in str(e):
- logging.error(f"Файл {DATA_FILE} не найден в репозитории {REPO_ID} (dataset) на Hugging Face. Детали: {e}")
- else:
- logging.error(f"Неизвестная ошибка при скачивании JSON базы ({type(e).__name__}): {e}")
- return False
-
-def load_data():
- """Загружает данные из локального JSON файла, предварительно пытаясь скачать с HF."""
- with data_lock: # Ensure only one thread accesses the file load logic at a time
- data_loaded_from_hf = False
- if download_db_from_hf():
- try:
- with open(DATA_FILE, 'r', encoding='utf-8') as f:
- data = json.load(f)
- logging.info("Данные успешно загружены из локального JSON после скачивания с HF.")
- if isinstance(data, dict) and 'products' in data and 'categories' in data:
- data_loaded_from_hf = True
- return data
- else:
- logging.warning("Структура JSON файла с HF некорректна. Попытка загрузить существующий локальный файл.")
- except FileNotFoundError:
- logging.warning("Локальный файл базы данных не найден после попытки скачивания с HF.")
- except json.JSONDecodeError as e:
- logging.error(f"Ошибка декодирования JSON файла, скачанного с HF: {e}")
- except Exception as e:
- logging.error(f"Произошла ошибка при чтении локального файла JSON после скачивания с HF: {e}")
-
- # If download failed or HF file was invalid, try loading existing local file
- if not data_loaded_from_hf:
- logging.info("Попытка загрузить существующий локальный файл data.json (если есть).")
- try:
- with open(DATA_FILE, 'r', encoding='utf-8') as f:
- data = json.load(f)
- logging.info("Данные успешно загружены из существующего локального JSON.")
- if isinstance(data, dict) and 'products' in data and 'categories' in data:
- return data
- else:
- logging.warning("Структура существующего локального JSON файла некорректна. Используется пустая структура.")
- except FileNotFoundError:
- logging.info("Локальный файл data.json не найден. Создание пустой структуры данных.")
- except json.JSONDecodeError as e:
- logging.error(f"Ошибка декодирования существующего локального JSON: {e}. Создание пустой структуры данных.")
- except Exception as e:
- logging.error(f"Произошла ошибка при загрузке существующего локального JSON: {e}")
-
- # Return default empty structure if all loading attempts fail
- logging.warning("Не удалось загрузить данные ни с HF, ни локально. Возвращается пустая структура.")
- return {'products': [], 'categories': []}
-
-
-def save_data(data):
- """Сохраняет данные в локальный JSON файл и загружает на Hugging Face."""
- with data_lock: # Ensure thread safety for saving
- try:
- # Backup current data file before writing, just in case
- if os.path.exists(DATA_FILE):
- backup_file = f"{DATA_FILE}.bak_{datetime.now().strftime('%Y%m%d%H%M%S')}"
- try:
- os.rename(DATA_FILE, backup_file)
- logging.info(f"Создана резервная копия старого файла: {backup_file}")
- except OSError as e:
- logging.error(f"Не удалось создать резервную копию {DATA_FILE}: {e}")
-
- with open(DATA_FILE, 'w', encoding='utf-8') as f:
- json.dump(data, f, ensure_ascii=False, indent=4)
- logging.info("Данные успешно сохранены в локальный JSON.")
- # Schedule upload to HF in a separate thread to avoid blocking the request
- upload_thread = threading.Thread(target=upload_db_to_hf)
- upload_thread.start()
- except Exception as e:
- logging.error(f"Ошибка при сохранении данных в локальный JSON: {e}")
- # Optional: Restore from backup if write failed? More complex logic needed.
-
-def upload_db_to_hf():
- """Загружает файл базы данных на Hugging Face."""
- if not HF_TOKEN_WRITE:
- logging.warning("HF_TOKEN_WRITE не установлен. Пропуск загрузки data.json на Hugging Face.")
- return
- if not os.path.exists(DATA_FILE):
- logging.warning(f"Файл {DATA_FILE} для загрузки на HF не найден.")
- return
-
- # Adding a small delay before upload, allows file system to settle sometimes
- time.sleep(1)
-
- with data_lock: # Acquire lock briefly to ensure file isn't being written to during upload prep
- file_path = DATA_FILE
- if not os.path.exists(file_path):
- logging.warning(f"Файл {file_path} все еще не найден перед загрузкой.")
- return
- try:
- logging.info(f"Попытка загрузки {file_path} в {REPO_ID} (dataset)")
- api = HfApi()
- api.upload_file(
- path_or_fileobj=file_path,
- path_in_repo=DATA_FILE, # Upload to root of the dataset repo
- repo_id=REPO_ID,
- repo_type="dataset",
- token=HF_TOKEN_WRITE,
- commit_message=f"Update database {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
- )
- logging.info(f"{DATA_FILE} успешно загружен на Hugging Face.")
- except HFValidationError as e:
- logging.error(f"Ошибка валидации Hugging Face при загрузке data.json (возможно, неверный токен?): {e}")
- except Exception as e:
- logging.error(f"Ошибка при загрузке {DATA_FILE} на Hugging Face ({type(e).__name__}): {e}")
-
-
-def upload_photo_to_hf(local_path, filename_in_repo, product_name):
- """Загружает файл фотографии на Hugging Face в папку 'photos/'."""
- if not HF_TOKEN_WRITE:
- logging.warning("HF_TOKEN_WRITE не установлен. Пропуск загрузки фото на Hugging Face.")
- return False
- if not os.path.exists(local_path):
- logging.error(f"Локальный файл фото {local_path} не найден для загрузки на HF.")
- return False
-
- path_in_repo = f"photos/{filename_in_repo}" # Ensure it's saved in the photos directory
-
- try:
- logging.info(f"Попытка загрузки фото {local_path} как {path_in_repo} в {REPO_ID} (dataset)")
- api = HfApi()
- api.upload_file(
- path_or_fileobj=local_path,
- path_in_repo=path_in_repo,
- repo_id=REPO_ID,
- repo_type="dataset",
- token=HF_TOKEN_WRITE,
- commit_message=f"Upload photo {filename_in_repo} for product {product_name}"
- )
- logging.info(f"Фото {filename_in_repo} успешно загружено на Hugging Face.")
- return True
- except HFValidationError as e:
- logging.error(f"Ошибка валидации Hugging Face при загрузке фото {filename_in_repo} (возможно, неверный токен?): {e}")
- return False
- except Exception as e:
- logging.error(f"Ошибка при загрузке фото {filename_in_repo} на Hugging Face ({type(e).__name__}): {e}")
- return False
- finally:
- # Clean up the temporary local file after attempting upload
- if os.path.exists(local_path):
- try:
- os.remove(local_path)
- logging.info(f"Удален временный файл фото: {local_path}")
- except OSError as e:
- logging.error(f"Ошибка при удалении временного файла фото {local_path}: {e}")
-
-def periodic_backup():
- """Периодически сохраняет резервную копию на Hugging Face."""
- interval_seconds = 900 # 15 minutes
- logging.info(f"Периодическое резервное копирование настроено с интервалом {interval_seconds} секунд.")
- while True:
- time.sleep(interval_seconds)
- logging.info("Запуск периодического резервного копирования data.json на HF...")
- # Run upload in a separate thread to avoid blocking the backup loop if upload takes time
- backup_upload_thread = threading.Thread(target=upload_db_to_hf)
- backup_upload_thread.start()
-
-
-# --- HTML Шаблоны ---
-
-# Общие стили для обеих страниц + Мобильные стили
-COMMON_STYLES = '''
-
-
-
-'''
-
-LANDING_PAGE_HTML = '''
-
-
-
-
-
- MebelHause KG - Изготовление мебели на заказ в Бишкеке
-
-
-
- {{ common_styles | safe }}
-
-
-
-
Цвета: {{ (product.get('colors') or ['нет']) | join(', ') }}
{# Display 'нет' if empty #}
-
- {% set current_photos = product.get('photos', []) %}
- {% if current_photos %}
-
- {% for photo in current_photos %}
- {% set photo_url = 'https://huggingface.co/datasets/' + repo_id + '/resolve/main/photos/' + photo %}
-
- {% endfor %}
-
- {% else %}
-
Фото нет
- {% endif %}
-
-
-
- Редактировать
-
-
-
-
-
-
-
-
- {% endfor %}
-
- {% else %}
-
Товаров пока нет. Добавьте первый товар с помощью формы выше.
- {% endif %}
-
-
-
-
-
-
Управление Базой Данных (Hugging Face)
-
- Данные сохраняются на Hugging Face при каждом изменении и автоматически каждые 15 минут.
- Вы можете принудительно синхронизировать данные кнопками ниже.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-'''
-
-
-# --- Маршруты Flask ---
-
-@app.route('/')
-def landing_page():
- """Отображает главную страницу (лендинг)."""
- current_year = datetime.now().year
- return render_template_string(
- LANDING_PAGE_HTML,
- common_styles=COMMON_STYLES,
- logo_url=LOGO_URL,
- whatsapp_number=WHATSAPP_NUMBER.replace("+", ""), # Убираем + для ссылки wa.me
- current_year=current_year
- )
-
-@app.route('/catalog')
-def catalog():
- """Отображает страницу каталога."""
- data = load_data()
- # Ensure products and categories are lists, even if missing/null in data.json
- products = data.get('products', []) if isinstance(data.get('products'), list) else []
- categories = data.get('categories', []) if isinstance(data.get('categories'), list) else []
- current_year = datetime.now().year
- return render_template_string(
- CATALOG_PAGE_HTML,
- common_styles=COMMON_STYLES,
- logo_url=LOGO_URL,
- products=products,
- categories=categories,
- repo_id=REPO_ID,
- whatsapp_number=WHATSAPP_NUMBER.replace("+", ""),
- current_year=current_year
- )
-
-# Note: The /product/ route for modal content is no longer needed
-# as the JavaScript now generates the modal content directly from the 'products' array.
-
-@app.route('/admin', methods=['GET', 'POST'])
-def admin():
- """Отображает админ-панель и обрабатывает действия администратора."""
- data = load_data()
- # Ensure products and categories are lists
- products = data.get('products', []) if isinstance(data.get('products'), list) else []
- categories = data.get('categories', []) if isinstance(data.get('categories'), list) else []
-
- if request.method == 'POST':
- action = request.form.get('action')
- logging.info(f"Admin action received: {action}")
-
- try:
- # --- Category Actions ---
- 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) # Save triggers async HF upload
- flash(f"Категория '{category_name}' успешно добавлена.", 'success')
- logging.info(f"Category added: {category_name}")
- elif category_name in categories:
- flash(f"Ошибка: Категория '{category_name}' уже существует.", 'error')
- logging.warning(f"Attempted to add duplicate category: {category_name}")
- else:
- flash("Ошибка: Название категории не может быть пустым.", 'error')
- logging.warning("Attempted to add empty category name.")
- return redirect(url_for('admin')) # Redirect even on error to show flash
-
- elif action == 'delete_category':
- category_index_str = request.form.get('category_index')
- if category_index_str is not None:
- try:
- category_index = int(category_index_str)
- if 0 <= category_index < len(categories):
- deleted_category = categories.pop(category_index)
- # Update products using this category
- updated_count = 0
- for product in products:
- if product.get('category') == deleted_category:
- product['category'] = 'Без категории'
- updated_count += 1
- save_data(data)
- flash(f"Категория '{deleted_category}' удалена. {updated_count} товаров обновлено.", 'success')
- logging.info(f"Category deleted: {deleted_category}, updated {updated_count} products.")
- else:
- flash("Ошибка: Неверный индекс категории для удаления.", 'error')
- logging.warning(f"Invalid category index for delete: {category_index}")
- except ValueError:
- flash("Ошибка: Некорректный индекс категории.", 'error')
- logging.warning(f"Non-integer category index received: {category_index_str}")
- else:
- flash("Ошибка: Индекс категории не указан.", 'error')
- logging.warning("Category index missing for delete action.")
- return redirect(url_for('admin'))
-
- # --- Product Actions ---
- elif action == 'add' or action == 'edit':
- index = -1 # Default for 'add'
- if action == 'edit':
- index_str = request.form.get('index')
- if index_str is None:
- flash("Ошибка: Индекс товара не указан для редактирования.", 'error')
- return redirect(url_for('admin'))
- try:
- index = int(index_str)
- if not (0 <= index < len(products)):
- flash("Ошибка: Неверный индекс товара для редактирования.", 'error')
- logging.warning(f"Invalid product index for edit: {index}")
- return redirect(url_for('admin'))
- except ValueError:
- flash("Ошибка: Некорректный индекс товара для редактирования.", 'error')
- logging.warning(f"Non-integer product index for edit: {index_str}")
- return redirect(url_for('admin'))
-
- # --- Field Validation ---
- 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', 'Без категории')
- colors = sorted(list(set(c.strip() for c in request.form.getlist('colors') if c.strip()))) # Unique, sorted, non-empty
- photos_files = request.files.getlist('photos')
- delete_current_photos = request.form.get('delete_current_photos') == 'true'
-
- error = False
- if not name:
- flash("Ошибка: Название товара не может быть пустым.", 'error')
- error = True
- if not description:
- flash("Ошибка: Описание товара не может быть пустым.", 'error')
- error = True
- try:
- price = int(float(price_str)) # Allow float input, convert to int
- if price < 0:
- flash("Ошибка: Цена не может быть отрицательной.", 'error')
- error = True
- except ValueError:
- flash("Ошибка: Некорректный формат цены.", 'error')
- error = True
- price = 0 # Set default on error
-
- if error:
- # Need to re-render form with errors, ideally preserving input
- # This is complex without a form library. Redirecting for now.
- logging.warning(f"Validation error during product {'edit' if action=='edit' else 'add'}.")
- return redirect(url_for('admin'))
-
- # --- Photo Handling ---
- MAX_FILE_SIZE = 5 * 1024 * 1024 # 5 MB
- MAX_PHOTOS = 10
- allowed_extensions = {'png', 'jpg', 'jpeg', 'webp'}
-
- def allowed_file(filename):
- return '.' in filename and \
- filename.rsplit('.', 1)[1].lower() in allowed_extensions
-
- newly_uploaded_filenames = []
- current_photos = products[index].get('photos', []) if action == 'edit' else []
-
- # Filter out empty file inputs
- valid_photos_files = [pf for pf in photos_files if pf and pf.filename]
-
- if len(current_photos) + len(valid_photos_files) - (len(current_photos) if delete_current_photos else 0) > MAX_PHOTOS:
- flash(f"Ошибка: Превышен лимит в {MAX_PHOTOS} фотографий на товар.", 'error')
- return redirect(url_for('admin'))
-
-
- for photo in valid_photos_files:
- if allowed_file(photo.filename):
- # Check file size
- file_size = len(photo.read())
- photo.seek(0) # Reset stream position after reading size
- if file_size > MAX_FILE_SIZE:
- flash(f"Ошибка: Файл '{photo.filename}' слишком большой (макс 5MB).", 'warning')
- logging.warning(f"File too large: {photo.filename} ({file_size} bytes)")
- continue # Skip this file
-
- original_filename = secure_filename(photo.filename)
- timestamp = datetime.now().strftime("%Y%m%d%H%M%S%f")
- unique_filename = f"{timestamp}_{original_filename}"
- temp_path = os.path.join(UPLOADS_DIR, unique_filename)
-
- try:
- photo.save(temp_path)
- logging.info(f"Photo saved temporarily to {temp_path}")
- # Schedule photo upload in a separate thread
- photo_upload_thread = threading.Thread(
- target=upload_photo_to_hf,
- args=(temp_path, unique_filename, name)
- # temp_path will be deleted inside upload_photo_to_hf
- )
- photo_upload_thread.start()
- newly_uploaded_filenames.append(unique_filename)
- except Exception as e:
- flash(f"Ошибка при сохранении фото '{original_filename}': {e}", 'error')
- logging.error(f"Error saving photo {original_filename} locally: {e}")
- # Clean up if temp file exists but HF upload wasn't started
- if os.path.exists(temp_path):
- try: os.remove(temp_path)
- except OSError as rm_err: logging.error(f"Error removing temp file {temp_path} after save error: {rm_err}")
- else:
- flash(f"Ошибка: Недопустимый тип файла для '{photo.filename}'. Разрешены: {', '.join(allowed_extensions)}.", 'warning')
- logging.warning(f"Invalid file type uploaded: {photo.filename}")
-
- # --- Update Data Structure ---
- final_photos = current_photos
- if action == 'edit' and delete_current_photos:
- # TODO: Optionally delete old photos from HF here (requires careful implementation)
- # Need to store old filenames before clearing
- logging.info(f"Deleting current photos for product index {index}")
- final_photos = []
-
- # Add newly uploaded photos
- final_photos.extend(newly_uploaded_filenames)
- final_photos = list(dict.fromkeys(final_photos)) # Keep unique and order
-
- product_data = {
- 'name': name,
- 'price': price,
- 'description': description,
- 'category': category if category in categories else 'Без категории',
- 'photos': final_photos,
- 'colors': colors
- }
-
- if action == 'add':
- products.append(product_data)
- flash(f"Товар '{name}' успешно добавлен.", 'success')
- logging.info(f"Product added: {name}")
- elif action == 'edit':
- products[index].update(product_data)
- flash(f"Товар '{name}' успешно обновлен.", 'success')
- logging.info(f"Product updated: {name} (index {index})")
-
- save_data(data)
- return redirect(url_for('admin'))
-
- elif action == 'delete':
- index_str = request.form.get('index')
- if index_str is not None:
- try:
- index = int(index_str)
- if 0 <= index < len(products):
- deleted_product = products.pop(index)
- # TODO: Optionally delete associated photos from HF
- # photos_to_delete = deleted_product.get('photos', [])
- # Implement deletion logic here if needed, likely async
- save_data(data)
- flash(f"Товар '{deleted_product.get('name', 'N/A')}' удален.", 'success')
- logging.info(f"Product deleted: {deleted_product.get('name', 'N/A')} (index {index})")
- else:
- flash("Ошибка: Неверный индекс товара для удаления.", 'error')
- logging.warning(f"Invalid product index for delete: {index}")
- except ValueError:
- flash("Ошибка: Некорректный индекс товара.", 'error')
- logging.warning(f"Non-integer product index for delete: {index_str}")
- else:
- flash("Ошибка: Индекс товара не указан для удаления.", 'error')
- logging.warning("Product index missing for delete action.")
- return redirect(url_for('admin'))
-
- except Exception as e:
- logging.exception("Произошла ошибка при обработке POST запроса в /admin")
- flash(f"Внутренняя ошибка сервера: {e}. См. логи.", 'error')
- # Redirect to avoid resubmission on refresh
- return redirect(url_for('admin'))
-
-
- # --- GET Request ---
- current_year = datetime.now().year
- # Pass flash messages implicitly via get_flashed_messages in template
- return render_template_string(
- ADMIN_PAGE_HTML,
- common_styles=COMMON_STYLES,
- logo_url=LOGO_URL,
- products=products,
- categories=categories,
- repo_id=REPO_ID,
- current_year=current_year
- )
-
-
-@app.route('/backup', methods=['POST'])
-def backup():
- """Запускает принудительное резервное копирование data.json на HF."""
- status_js = "window.parent.updateStatus('Не удалось запустить резервное копирование.', 'backup-loading');" # Default error
- if not HF_TOKEN_WRITE:
- status_js = "window.parent.updateStatus('Ошибка: Токен для записи (HF_TOKEN) не установлен.', 'backup-loading');"
- logging.warning("Backup requested but HF_TOKEN_WRITE is not set.")
- else:
- try:
- # Use a thread to avoid blocking the response
- upload_thread = threading.Thread(target=upload_db_to_hf)
- upload_thread.start()
- # Give immediate feedback, actual status comes from logs
- status_js = "window.parent.updateStatus('Запущено резервное копирование на Hugging Face...', 'backup-loading');"
- logging.info("Manual backup to HF triggered.")
- except Exception as e:
- status_js = f"window.parent.updateStatus('Ошибка запуска копирования: {str(e)}.', 'backup-loading');"
- logging.error(f"Error starting manual backup thread: {e}")
-
- # Return JS to update the parent window's status indicator
- # Use text/html content type for script tag execution in iframe/target
- response = app.make_response(f"")
- response.headers['Content-Type'] = 'text/html'
- return response
-
-
-@app.route('/download', methods=['POST'])
-def download():
- """Запускает принудительное скачивание базы данных с HF."""
- status_js = "window.parent.updateStatus('Не удалось запустить скачивание.', 'download-loading');"
- if not HF_TOKEN_READ:
- status_js = "window.parent.updateStatus('Ошибка: Токен для чтения (HF_TOKEN_READ) не установлен.', 'download-loading');"
- logging.warning("Download requested but HF_TOKEN_READ is not set.")
- else:
- try:
- # Perform download synchronously as it affects the data used immediately after
- if download_db_from_hf():
- status_js = "window.parent.updateStatus('База данных успешно скачана с HF. Перезагрузите страницу.', 'download-loading');"
- logging.info("Manual download from HF successful.")
- else:
- status_js = "window.parent.updateStatus('Ошибка скачивания с HF (см. логи).', 'download-loading');"
- logging.warning("Manual download from HF failed (check logs for details).")
- except Exception as e:
- status_js = f"window.parent.updateStatus('Ошибка скачивания: {str(e)}.', 'download-loading');"
- logging.error(f"Error during manual download: {e}")
-
- response = app.make_response(f"")
- response.headers['Content-Type'] = 'text/html'
- return response
-
-# Optional: Route to serve uploaded images directly if needed, but HF link is better
-# @app.route('/uploads/')
-# def uploaded_file(filename):
-# return send_from_directory(app.config['UPLOADS_DIR'], filename)
-
-# --- Запуск приложения ---
-
-if __name__ == '__main__':
- print("Starting MebelHause KG Flask Application...") # Use print before logging is fully configured if needed
-
- # --- Token Checks ---
- if not HF_TOKEN_WRITE:
- logging.warning("(!) Переменная окружения HF_TOKEN (для записи) не установлена. Загрузка данных и фото на Hugging Face НЕ БУДЕТ РАБОТАТЬ.")
- if not HF_TOKEN_READ:
- logging.warning("(!) Переменная окружения HF_TOKEN_READ (для чтения) не установлена. Скачивание данных с Hugging Face может не работать (если отличается от HF_TOKEN).")
- if not app.secret_key or app.secret_key == 'a_default_secret_key_change_me':
- logging.warning("(!) Секретный ключ Flask (FLASK_SECRET_KEY) не установлен или используется значение по умолчанию. Установите его для безопасности!")
-
- # --- Initial Data Load ---
- try:
- logging.info("Загрузка начальных данных...")
- load_data() # Attempt to load/download data on startup
- logging.info("Начальная загрузка данных завершена.")
- except Exception as e:
- logging.exception("(!) КРИТИЧЕСКАЯ ОШИБКА: Не удалось инициализировать базу данных при запуске!")
- # Depending on severity, you might want to exit here
- # sys.exit(1)
-
- # --- Start Periodic Backup Thread ---
- if HF_TOKEN_WRITE:
- backup_thread = threading.Thread(target=periodic_backup, daemon=True)
- backup_thread.start()
- logging.info("Запущен поток для периодического резервного копирования на Hugging Face.")
- else:
- logging.info("Поток периодического резервного копирования не запущен (HF_TOKEN не установлен).")
-
- # --- Run Flask App ---
- # Use environment variable for port, default to 7860 for HF Spaces compatibility
- port = int(os.environ.get("PORT", 7860))
- host = '0.0.0.0' # Listen on all available network interfaces
-
- logging.info(f"Запуск Flask приложения на http://{host}:{port}")
-
- # Use Waitress for production deployment
- try:
- from waitress import serve
- serve(app, host=host, port=port, threads=8) # Adjust threads as needed
- except ImportError:
- logging.warning("Waitress не найден. Запуск с использованием встроенного сервера Flask (НЕ РЕКОМЕНДУЕТСЯ для продакшена).")
- # Fallback to Flask's built-in server (for development/testing only)
- # DO NOT use debug=True in production
- app.run(host=host, port=port, debug=False)