diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,5 +1,5 @@ # --- START OF FILE app.py --- -from flask import Flask, render_template_string, request, redirect, url_for, jsonify +from flask import Flask, render_template_string, request, redirect, url_for import json import os import logging @@ -7,12 +7,11 @@ import threading import time from datetime import datetime from huggingface_hub import HfApi, hf_hub_download -from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError +from huggingface_hub.utils import RepositoryNotFoundError from werkzeug.utils import secure_filename app = Flask(__name__) DATA_FILE = 'data.json' -UPLOADS_DIR = 'uploads' REPO_ID = "Kgshop/teenager" HF_TOKEN_WRITE = os.getenv("HF_TOKEN") @@ -20,195 +19,136 @@ HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") LOGO_URL = "https://huggingface.co/spaces/Teenagerkg/optom/resolve/main/PXL_20250419_062456154~2.jpg" -logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +logging.basicConfig(level=logging.INFO) # Use INFO level for production -# Ensure necessary directories exist -os.makedirs(UPLOADS_DIR, exist_ok=True) - -# --- Data Handling --- data_lock = threading.Lock() -def download_db_from_hf(retries=3, delay=5): - for attempt in range(retries): - try: - logging.info(f"Attempting to download {DATA_FILE} from {REPO_ID} (Attempt {attempt + 1}/{retries})") - 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 - etag_timeout=60 # Increase timeout - ) - logging.info("JSON database successfully downloaded from Hugging Face.") - return True - except RepositoryNotFoundError as e: - logging.error(f"Repository {REPO_ID} not found: {e}") - # Don't retry if repo not found - return False - except HfHubHTTPError as e: - # Specific handling for HTTP errors which might be transient - logging.error(f"HTTP error during download: {e}. Retrying in {delay} seconds...") - time.sleep(delay) - except Exception as e: - logging.error(f"Error downloading JSON database (Attempt {attempt + 1}): {e}") - if attempt < retries - 1: - logging.info(f"Retrying download in {delay} seconds...") - time.sleep(delay) - else: - logging.error("Download failed after multiple retries.") - return False - return False +def download_db_from_hf(): + try: + logging.info(f"Попытка скачивания {DATA_FILE} из {REPO_ID}") + 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 latest version is fetched + cache_dir=None # Avoid caching issues if tokens change or file updates + ) + logging.info("JSON база успешно скачана из Hugging Face.") + except RepositoryNotFoundError as e: + logging.error(f"Репозиторий не найден: {e}") + raise + except Exception as e: + logging.error(f"Ошибка при скачивании JSON базы: {e}") + raise +def upload_db_to_hf_internal(): + # Assumes data_lock is already held + 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 Exception as e: + logging.error(f"Ошибка при загрузке резервной копии: {e}") + # Do not raise here, just log, might be called from different contexts def load_data(): - with data_lock: - if not os.path.exists(DATA_FILE): - logging.warning(f"Local file {DATA_FILE} not found. Attempting download.") - if not download_db_from_hf(): - logging.warning("Download failed. Initializing with empty data.") - return {'products': [], 'categories': []} - elif (time.time() - os.path.getmtime(DATA_FILE)) > 600: # Refresh if older than 10 mins - logging.info("Local file is older than 10 minutes. Attempting refresh download.") - download_db_from_hf() # Try to refresh, ignore failure, proceed with local cache + initial_load_error = None + try: + with data_lock: + download_db_from_hf() + except RepositoryNotFoundError: + logging.info("Репозиторий не найден. Будет использоваться локальный файл, если он существует.") + initial_load_error = RepositoryNotFoundError("Repo not found") + except Exception as e: + logging.error(f"Ошибка скачивания базы данных при запуске: {e}. Попытка использовать локальный файл.") + initial_load_error = e + # Always try to read the local file after attempting download or if download failed + if os.path.exists(DATA_FILE): try: with open(DATA_FILE, 'r', encoding='utf-8') as file: data = json.load(file) - logging.info("Data successfully loaded from JSON") + logging.info("Данные успешно загружены из локального JSON") + if not isinstance(data, dict): - logging.warning("Data is not a dictionary, re-initializing structure.") - return {'products': [], 'categories': [] if not isinstance(data, list) else data} - if 'products' not in data: + logging.warning("Структура данных не является словарем, инициализация...") + return {'products': [], 'categories': []} + + # Ensure keys exist and are lists + if 'products' not in data or not isinstance(data['products'], list): data['products'] = [] - logging.warning("Missing 'products' key, initialized.") - if 'categories' not in data: + if 'categories' not in data or not isinstance(data['categories'], list): data['categories'] = [] - logging.warning("Missing 'categories' key, initialized.") - # Ensure products is a list - if not isinstance(data['products'], list): - logging.warning("'products' is not a list, re-initializing.") - data['products'] = [] - # Ensure categories is a list - if not isinstance(data['categories'], list): - logging.warning("'categories' is not a list, re-initializing.") - data['categories'] = [] return data - except FileNotFoundError: - logging.error("Local database file not found even after download attempt.") - return {'products': [], 'categories': []} except json.JSONDecodeError: - logging.error("Error: Cannot decode JSON file. Returning empty data.") - # Optionally try downloading again or backup the corrupted file + logging.error("Ошибка: Невозможно декодировать локальный JSON файл. Создается пустая структура.") return {'products': [], 'categories': []} except Exception as e: - logging.error(f"An unexpected error occurred during data loading: {e}") + logging.error(f"Произошла ошибка при загрузке локальных данных: {e}") return {'products': [], 'categories': []} + else: + # If file doesn't exist (either download failed or repo not found) + logging.warning(f"Локальный файл {DATA_FILE} не найден. Создается пустая структура данных.") + if initial_load_error and isinstance(initial_load_error, RepositoryNotFoundError): + # Create an empty file if the repo wasn't found and no local file exists + try: + with open(DATA_FILE, 'w', encoding='utf-8') as f: + json.dump({'products': [], 'categories': []}, f) + logging.info(f"Создан пустой локальный файл {DATA_FILE}.") + except Exception as e_create: + logging.error(f"Не удалось создать пустой локальный файл: {e_create}") + return {'products': [], 'categories': []} def save_data(data): with data_lock: try: - # Create a backup before overwriting - if os.path.exists(DATA_FILE): - backup_file = f"{DATA_FILE}.bak_{int(time.time())}" - os.rename(DATA_FILE, backup_file) - logging.info(f"Created backup: {backup_file}") - - with open(DATA_FILE, 'w', encoding='utf-8') as file: + temp_file = DATA_FILE + '.tmp' + with open(temp_file, 'w', encoding='utf-8') as file: json.dump(data, file, ensure_ascii=False, indent=4) - logging.info("Data successfully saved to JSON") - # Trigger immediate upload after saving - upload_db_to_hf() + # Atomic replace + os.replace(temp_file, DATA_FILE) + logging.info("Данные успешно сохранены в JSON") + upload_db_to_hf_internal() except Exception as e: - logging.error(f"Error saving data: {e}") - # Attempt to restore backup if save failed - if 'backup_file' in locals() and os.path.exists(backup_file): - try: - os.rename(backup_file, DATA_FILE) - logging.info("Restored data from backup due to save error.") - except Exception as restore_e: - logging.error(f"Failed to restore backup: {restore_e}") - raise # Re-raise the original exception - - -def upload_db_to_hf(): - if not HF_TOKEN_WRITE: - logging.warning("HF_TOKEN_WRITE not set. Skipping upload to Hugging Face.") - return False - if not os.path.exists(DATA_FILE): - logging.warning(f"{DATA_FILE} not found locally. Skipping upload.") - return False - - 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"Automatic database backup {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", - commit_description="Regular automatic backup of the application data." - ) - logging.info("JSON database backup successfully uploaded to Hugging Face.") - return True - except Exception as e: - logging.error(f"Error uploading backup to Hugging Face: {e}") - return False - -def upload_photo_to_hf(local_path, repo_filename, product_name): - if not HF_TOKEN_WRITE: - logging.warning("HF_TOKEN_WRITE not set. Skipping photo upload.") - return False - try: - api = HfApi() - api.upload_file( - path_or_fileobj=local_path, - path_in_repo=f"photos/{repo_filename}", - repo_id=REPO_ID, - repo_type="dataset", - token=HF_TOKEN_WRITE, - commit_message=f"Upload photo {repo_filename} for product {product_name}" - ) - logging.info(f"Photo {repo_filename} uploaded successfully to HF.") - return True - except Exception as e: - logging.error(f"Error uploading photo {repo_filename} to HF: {e}") - return False - finally: - # Clean up local temporary file - if os.path.exists(local_path): - try: - os.remove(local_path) - logging.info(f"Removed temporary local file: {local_path}") - except OSError as e: - logging.error(f"Error removing temporary file {local_path}: {e}") - + logging.error(f"Ошибка при сохранении данных: {e}") + # Clean up temp file if it exists + if os.path.exists(temp_file): + try: + os.remove(temp_file) + except OSError as e_rem: + logging.error(f"Ошибка при удалении временного файла {temp_file}: {e_rem}") + raise # Re-raise after logging and cleanup attempt def periodic_backup(): while True: - time.sleep(900) # Backup every 15 minutes - logging.info("Starting periodic background backup...") - upload_db_to_hf() - - -# --- Flask Routes --- + time.sleep(800) + logging.info("Запуск периодического резервного копирования...") + with data_lock: + try: + upload_db_to_hf_internal() + except Exception as e: + logging.error(f"Ошибка в потоке периодического бэкапа: {e}") @app.route('/') def catalog(): data = load_data() - # Sort products by added_at date, newest first. Handle missing dates gracefully. - products = sorted( - [p for p in data.get('products', []) if isinstance(p, dict)], # Ensure items are dicts - key=lambda x: x.get('added_at', '1970-01-01T00:00:00'), # Default old date if missing - reverse=True - ) + products = sorted(data.get('products', []), key=lambda x: x.get('added_at', ''), reverse=True) categories = data.get('categories', []) - if not isinstance(categories, list): categories = [] # Ensure categories is a list catalog_html = ''' @@ -216,22 +156,21 @@ def catalog():
-{{ product.get('description', 'Нет описания') }}
- + {% endif %} +{{ product['description'][:50]|e }}{% if product['description']|length > 50 %}...{% endif %}
+ +Товары не найдены.
{% endfor %}