from flask import Flask, render_template_string, request, redirect, url_for, session, send_file, 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, HfHubHTTPError
from werkzeug.utils import secure_filename
from dotenv import load_dotenv
import filelock # Добавлен импорт
load_dotenv()
app = Flask(__name__)
app.secret_key = 'your_unique_secret_key_soola_cosmetics_67890'
DATA_FILE = 'data_soola.json'
USERS_FILE = 'users_soola.json'
LOCK_FILE_SUFFIX = ".lock"
SYNC_FILES = [DATA_FILE, USERS_FILE]
REPO_ID = "Kgshop/Soola"
HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
STORE_ADDRESS = "Рынок Дордой, Джунхай, терминал, 38"
CURRENCY_CODE = 'KGS'
CURRENCY_NAME = 'Кыргызский сом (с)'
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# --- Helper for file locking ---
def get_lock(filename):
return filelock.FileLock(filename + LOCK_FILE_SUFFIX, timeout=10) # 10 секунд таймаут
# --- Modified Download Function with Retries ---
def download_db_from_hf(specific_file=None, max_retries=3, retry_delay=5):
if not HF_TOKEN_READ:
logging.warning("Переменная окружения HF_TOKEN_READ не установлена. Попытка скачивания с Hugging Face без токена.")
files_to_download = [specific_file] if specific_file else SYNC_FILES
logging.info(f"Начало скачивания файлов {files_to_download} с HF репозитория {REPO_ID}...")
all_successful = True
for file_name in files_to_download:
download_successful = False
for attempt in range(max_retries):
try:
logging.info(f"Попытка {attempt + 1}/{max_retries} скачивания файла {file_name}...")
lock = get_lock(file_name)
with lock:
local_path = 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,
resume_download=False # Добавлено для большей надежности при перезаписи
)
logging.info(f"Файл {file_name} успешно скачан из Hugging Face в {local_path}.")
download_successful = True
break # Выход из цикла ретраев при успехе
except RepositoryNotFoundError:
logging.error(f"Репозиторий {REPO_ID} не найден на Hugging Face. Скачивание прервано.")
all_successful = False
return False # Нет смысла ретраить, если репо нет
except HfHubHTTPError as e:
if "404" in str(e) or "not found" in str(e).lower():
logging.warning(f"Файл {file_name} не найден в репозитории {REPO_ID} (попытка {attempt + 1}/{max_retries}). Пропуск скачивания этого файла.")
# Не считаем это полным провалом, просто файла нет на HF
download_successful = True # Считаем "успешным" в плане отсутствия ошибки скачивания
break
else:
logging.error(f"Ошибка HTTP при скачивании {file_name} (попытка {attempt + 1}/{max_retries}): {e}")
except Exception as e:
logging.error(f"Ошибка при скачивании файла {file_name} с Hugging Face (попытка {attempt + 1}/{max_retries}): {e}", exc_info=True)
if attempt < max_retries - 1:
logging.info(f"Ожидание {retry_delay} секунд перед следующей попыткой...")
time.sleep(retry_delay)
else:
logging.error(f"Не удалось скачать файл {file_name} после {max_retries} попыток.")
all_successful = False
if not download_successful:
all_successful = False # Помечаем общую неуспешность если хотя бы один файл не скачался
logging.info(f"Скачивание файлов с HF завершено. Общий результат: {'Успех' if all_successful else 'Неудача'}.")
return all_successful
# --- Modified Load Functions ---
def load_data():
file_path = DATA_FILE
logging.info(f"Попытка загрузки данных из {file_path}...")
download_success = download_db_from_hf(specific_file=file_path)
if download_success:
logging.info(f"Скачивание {file_path} с HF успешно (или файл отсутствовал на HF). Попытка загрузки локальной версии.")
else:
logging.warning(f"Не удалось скачать {file_path} с HF после всех попыток. Попытка загрузить существующий локальный файл (если есть).")
try:
lock = get_lock(file_path)
with lock:
with open(file_path, 'r', encoding='utf-8') as file:
data = json.load(file)
logging.info(f"Данные успешно загружены из локального файла {file_path}")
if not isinstance(data, dict):
logging.warning(f"{file_path} не является словарем. Инициализация пустой структурой.")
return {'products': [], 'categories': []}
if 'products' not in data: data['products'] = []
if 'categories' not in data: data['categories'] = []
return data
except FileNotFoundError:
logging.critical(f"КРИТИЧЕСКАЯ ОШИБКА: Файл {file_path} не найден локально И не удалось его скачать с HF. Инициализация пустой структурой.")
# Не создаем пустой файл здесь!
return {'products': [], 'categories': []}
except json.JSONDecodeError:
logging.error(f"Ошибка декодирования JSON в локальном {file_path}. Файл может быть поврежден. Возврат пустой структуры.")
return {'products': [], 'categories': []}
except Exception as e:
logging.error(f"Неизвестная ошибка при загрузке данных ({file_path}): {e}", exc_info=True)
return {'products': [], 'categories': []}
def load_users():
file_path = USERS_FILE
logging.info(f"Попытка загрузки данных пользователей из {file_path}...")
download_success = download_db_from_hf(specific_file=file_path)
if download_success:
logging.info(f"Скачивание {file_path} с HF успешно (или файл отсутствовал на HF). Попытка загрузки локальной версии.")
else:
logging.warning(f"Не удалось скачать {file_path} с HF после всех попыток. Попытка загрузить существующий локальный файл (если есть).")
try:
lock = get_lock(file_path)
with lock:
with open(file_path, 'r', encoding='utf-8') as file:
users = json.load(file)
logging.info(f"Данные пользователей успешно загружены из локального файла {file_path}")
return users if isinstance(users, dict) else {}
except FileNotFoundError:
logging.critical(f"КРИТИЧЕСКАЯ ОШИБКА: Файл {file_path} не найден локально И не удалось его скачать с HF. Инициализация пустым словарем.")
# Не создаем пустой файл здесь!
return {}
except json.JSONDecodeError:
logging.error(f"Ошибка декодирования JSON в локальном {file_path}. Файл может быть поврежден. Возврат пустого словаря.")
return {}
except Exception as e:
logging.error(f"Неизвестная ошибка при загрузке пользователей ({file_path}): {e}", exc_info=True)
return {}
# --- Modified Upload Function with Safety Check ---
def upload_db_to_hf(specific_file=None):
if not HF_TOKEN_WRITE:
logging.warning("Переменная окружения HF_TOKEN (для записи) не установлена. Загрузка на Hugging Face пропущена.")
return
try:
api = HfApi()
files_to_upload = [specific_file] if specific_file else SYNC_FILES
logging.info(f"Начало загрузки файлов {files_to_upload} на HF репозиторий {REPO_ID}...")
for file_name in files_to_upload:
if os.path.exists(file_name):
# --- Safety Check ---
try:
lock = get_lock(file_name)
with lock: # Блокируем чтение на время проверки
with open(file_name, 'r', encoding='utf-8') as f:
content_check = json.load(f)
if file_name == DATA_FILE and content_check == {'products': [], 'categories': []}:
logging.warning(f"ПРЕДОТВРАЩЕНИЕ ЗАГРУЗКИ: Локальный файл {file_name} пуст (содержит только {'{products: [], categories: []}'}). Загрузка на HF пропущена.")
continue # Пропустить этот файл
if file_name == USERS_FILE and content_check == {}:
logging.warning(f"ПРЕДОТВРАЩЕНИЕ ЗАГРУЗКИ: Локальный файл {file_name} пуст (содержит только {'{}'}). Загрузка на HF пропущена.")
continue # Пропустить этот файл
except json.JSONDecodeError:
logging.error(f"Ошибка чтения JSON в локальном файле {file_name} перед загрузкой. Загрузка на HF для этого файла пропущена.")
continue
except Exception as e:
logging.error(f"Ошибка при проверке файла {file_name} перед загрузкой: {e}. Загрузка на HF для этого файла пропущена.")
continue
# --- End Safety Check ---
try:
lock = get_lock(file_name)
with lock: # Блокируем на время чтения для загрузки
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"Sync {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
)
logging.info(f"Файл {file_name} успешно загружен на Hugging Face.")
except Exception as e:
logging.error(f"Ошибка при загрузке файла {file_name} на Hugging Face: {e}")
else:
logging.warning(f"Файл {file_name} не найден локально, пропуск загрузки.")
logging.info("Загрузка файлов на HF завершена.")
except Exception as e:
logging.error(f"Общая ошибка при инициализации или загрузке на Hugging Face: {e}", exc_info=True)
# --- Modified Save Functions ---
def save_data(data):
try:
lock = get_lock(DATA_FILE)
with lock:
with open(DATA_FILE, 'w', encoding='utf-8') as file:
json.dump(data, file, ensure_ascii=False, indent=4)
logging.info(f"Данные успешно сохранены в {DATA_FILE}")
# Загрузка на HF вызывается после сохранения, но с проверкой внутри upload_db_to_hf
upload_db_to_hf(specific_file=DATA_FILE)
except filelock.Timeout:
logging.error(f"Не удалось получить блокировку для сохранения файла {DATA_FILE}. Сохранение и загрузка на HF пропущены.")
flash("Внимание: Не удалось сохранить изменения из-за блокировки файла. Попробуйте позже.", "error")
except Exception as e:
logging.error(f"Ошибка при сохранении данных в {DATA_FILE}: {e}", exc_info=True)
def save_users(users):
try:
lock = get_lock(USERS_FILE)
with lock:
with open(USERS_FILE, 'w', encoding='utf-8') as file:
json.dump(users, file, ensure_ascii=False, indent=4)
logging.info(f"Данные пользователей успешно сохранены в {USERS_FILE}")
# Загрузка на HF вызывается после сохранения, но с проверкой внутри upload_db_to_hf
upload_db_to_hf(specific_file=USERS_FILE)
except filelock.Timeout:
logging.error(f"Не удалось получить блокировку для сохранения файла {USERS_FILE}. Сохранение и загрузка на HF пропущены.")
flash("Внимание: Не удалось сохранить изменения пользователя из-за блокировки файла. Попробуйте позже.", "error")
except Exception as e:
logging.error(f"Ошибка при сохранении данных пользователей в {USERS_FILE}: {e}", exc_info=True)
# --- Periodic Backup ---
def periodic_backup():
backup_interval = 1800
logging.info(f"Настройка периодического резервного копирования каждые {backup_interval} секунд.")
while True:
time.sleep(backup_interval)
logging.info("Запуск периодического резервного копирования...")
# upload_db_to_hf уже содержит проверку на пустые файлы
upload_db_to_hf()
logging.info("Периодическое резервное копирование завершено.")
# --- Flask Routes (Без изменений в HTML/JS, только вызовы функций) ---
@app.route('/')
def catalog():
data = load_data()
all_products = data.get('products', [])
categories = data.get('categories', [])
is_authenticated = 'user' in session
products_in_stock = [p for p in all_products if p.get('in_stock', True)]
products_sorted = sorted(products_in_stock, key=lambda p: (not p.get('is_top', False), p.get('name', '').lower()))
catalog_html = '''
'''
return render_template_string(
catalog_html,
products=products_sorted,
categories=categories,
repo_id=REPO_ID,
is_authenticated=is_authenticated,
store_address=STORE_ADDRESS,
session=session,
currency_code=CURRENCY_CODE
)
@app.route('/product/')
def product_detail(index):
data = load_data()
all_products = data.get('products', [])
products_in_stock = [p for p in all_products if p.get('in_stock', True)]
products_sorted = sorted(products_in_stock, key=lambda p: (not p.get('is_top', False), p.get('name', '').lower()))
is_authenticated = 'user' in session
try:
product = products_sorted[index]
if not product.get('in_stock', True):
raise IndexError("Товар не в наличии")
except IndexError:
logging.warning(f"Попытка доступа к несуществующему или отсутствующему продукту с индексом {index}")
return "Товар не найден или отсутствует в наличии.", 404
detail_html = '''
{{ product['name'] }}
{% if product.get('photos') and product['photos']|length > 0 %}
{% for photo in product['photos'] %}
{% endfor %}
{% else %}
{% endif %}
{% if product.get('photos') and product['photos']|length > 1 %}
{% endif %}
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
{{ message }}
{% endfor %}
{% endif %}
{% endwith %}
Синхронизация с Датацентром
Резервное копирование происходит автоматически каждые 30 минут, а также после каждого сохранения данных (с проверкой на пустые файлы). Используйте эти кнопки для немедленной синхронизации.
{% if product.get('photos') and product.photos|length > 0 %}
{% else %}
{% endif %}
{{ product['name'] }}
{% if product.get('in_stock', True) %}
В наличии
{% else %}
Нет в наличии
{% endif %}
{% if product.get('is_top', False) %}
Топ
{% endif %}
{% if product.get('photos') and product['photos']|length > 1 %}
(Всего фото: {{ product['photos']|length }})
{% endif %}
Редактирование: {{ product['name'] }}
{% endfor %}
{% else %}
Товаров пока нет.
{% endif %}
'''
@app.route('/admin', methods=['GET', 'POST'])
def admin():
data = load_data()
products = data.get('products', [])
categories = data.get('categories', [])
users = load_users()
if request.method == 'POST':
action = request.form.get('action')
logging.info(f"Admin action received: {action}")
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)
categories.sort()
save_data(data)
logging.info(f"Категория '{category_name}' добавлена.")
flash(f"Категория '{category_name}' успешно добавлена.", 'success')
elif not category_name:
logging.warning("Попытка добавить пустую категорию.")
flash("Название категории не может быть пустым.", 'error')
else:
logging.warning(f"Категория '{category_name}' уже существует.")
flash(f"Категория '{category_name}' уже существует.", 'warning')
elif action == 'delete_category':
category_to_delete = request.form.get('category_name')
if category_to_delete and category_to_delete in categories:
categories.remove(category_to_delete)
updated_count = 0
for product in products:
if product.get('category') == category_to_delete:
product['category'] = 'Без категории'
updated_count += 1
save_data(data)
logging.info(f"Категория '{category_to_delete}' удалена. Обновлено товаров: {updated_count}.")
flash(f"Категория '{category_to_delete}' удалена.", 'success')
else:
logging.warning(f"Попытка удалить несуществующую или пустую категорию: {category_to_delete}")
flash(f"Не удалось удалить категорию '{category_to_delete}'.", 'error')
elif action == 'add_product':
name = request.form.get('name', '').strip()
price_str = request.form.get('price', '').replace(',', '.')
description = request.form.get('description', '').strip()
category = request.form.get('category')
photos_files = request.files.getlist('photos')
colors = [c.strip() for c in request.form.getlist('colors') if c.strip()]
in_stock = 'in_stock' in request.form
is_top = 'is_top' in request.form
if not name or not price_str:
flash("Название и цена товара обязательны.", 'error')
return redirect(url_for('admin'))
try:
price = round(float(price_str), 2)
if price < 0: price = 0
except ValueError:
flash("Неверный формат цены.", 'error')
return redirect(url_for('admin'))
photos_list = []
if photos_files and any(f.filename for f in photos_files):
if not HF_TOKEN_WRITE:
flash("HF_TOKEN для записи не установлен. Фотографии не будут загружены.", 'warning')
logging.warning("HF_TOKEN_WRITE не установлен, пропуск загрузки фото.")
else:
uploads_dir = 'uploads_temp'
os.makedirs(uploads_dir, exist_ok=True)
api = HfApi()
photo_limit = 10
uploaded_count = 0
for photo in photos_files:
if uploaded_count >= photo_limit:
logging.warning(f"Достигнут лимит фото ({photo_limit}), остальные фото проигнорированы.")
flash(f"Загружено только первые {photo_limit} фото.", "warning")
break
if photo and photo.filename:
try:
ext = os.path.splitext(photo.filename)[1].lower()
if ext not in ['.png', '.jpg', '.jpeg', '.gif', '.webp']:
logging.warning(f"Пропущен файл с неподдерживаемым расширением: {photo.filename}")
flash(f"Файл {photo.filename} имеет неподдерживаемый тип.", 'warning')
continue
safe_original_filename = secure_filename(os.path.splitext(photo.filename)[0])
photo_filename = f"{name.replace(' ','_')}_{safe_original_filename}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{ext}"
temp_path = os.path.join(uploads_dir, photo_filename)
photo.save(temp_path)
logging.info(f"Загрузка фото {photo_filename} на HF для товара {name}...")
lock = get_lock(os.path.join("photos", photo_filename)) # Lock for HF upload path
with lock:
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"Add photo for product {name}"
)
photos_list.append(photo_filename)
logging.info(f"Фото {photo_filename} успешно загружено.")
os.remove(temp_path)
uploaded_count += 1
except Exception as e:
logging.error(f"Ошибка загрузки фото {photo.filename} на HF: {e}", exc_info=True)
flash(f"Ошибка при загрузке фото {photo.filename}.", 'error')
elif photo and not photo.filename:
logging.warning("Получен пустой объект файла фото при добавлении товара.")
try:
if os.path.exists(uploads_dir) and not os.listdir(uploads_dir):
os.rmdir(uploads_dir)
except OSError as e:
logging.warning(f"Не удалось удалить временную папку {uploads_dir}: {e}")
new_product = {
'name': name, 'price': price, 'description': description,
'category': category if category in categories else 'Без категории',
'photos': photos_list, 'colors': colors,
'in_stock': in_stock, 'is_top': is_top
}
# Добавляем в начало списка, если Топ, иначе в конец (для примерной сортировки при добавлении)
if is_top:
products.insert(0, new_product)
else:
products.append(new_product)
save_data(data)
logging.info(f"Товар '{name}' добавлен.")
flash(f"Товар '{name}' успешно добавлен.", 'success')
elif action == 'edit_product':
index_str = request.form.get('index')
if index_str is None:
flash("Ошибка редактирования: индекс товара не передан.", 'error')
return redirect(url_for('admin'))
try:
index = int(index_str)
original_product_list = data.get('products', []) # Работаем с оригинальным списком
if not (0 <= index < len(original_product_list)): raise IndexError("Индекс вне диапазона")
product_to_edit = original_product_list[index]
original_name = product_to_edit.get('name', 'N/A')
except (ValueError, IndexError):
flash(f"Ошибка редактирования: неверный индекс товара '{index_str}'.", 'error')
logging.error(f"Ошибка редактирования: неверный индекс {index_str}. Всего товаров: {len(original_product_list)}")
return redirect(url_for('admin'))
product_to_edit['name'] = request.form.get('name', product_to_edit['name']).strip()
price_str = request.form.get('price', str(product_to_edit['price'])).replace(',', '.')
product_to_edit['description'] = request.form.get('description', product_to_edit['description']).strip()
category = request.form.get('category')
product_to_edit['category'] = category if category in categories else 'Без категории'
product_to_edit['colors'] = [c.strip() for c in request.form.getlist('colors') if c.strip()]
product_to_edit['in_stock'] = 'in_stock' in request.form
product_to_edit['is_top'] = 'is_top' in request.form
try:
price = round(float(price_str), 2)
if price < 0: price = 0
product_to_edit['price'] = price
except ValueError:
logging.warning(f"Неверный формат цены '{price_str}' при редактировании товара {original_name}. Цена не изменена.")
flash(f"Неверный формат цены для товара '{original_name}'. Цена не изменена.", 'warning')
photos_files = request.files.getlist('photos')
if photos_files and any(f.filename for f in photos_files):
if not HF_TOKEN_WRITE:
flash("HF_TOKEN для записи не установлен. Фотографии не будут обновлены.", 'warning')
logging.warning("HF_TOKEN_WRITE не установлен, пропуск обновления фото.")
else:
uploads_dir = 'uploads_temp'
os.makedirs(uploads_dir, exist_ok=True)
api = HfApi()
new_photos_list = []
photo_limit = 10
uploaded_count = 0
logging.info(f"Загрузка новых фото для товара {product_to_edit['name']}...")
for photo in photos_files:
if uploaded_count >= photo_limit:
logging.warning(f"Достигнут лимит фото ({photo_limit}), остальные фото проигнорированы.")
flash(f"Загружено только первые {photo_limit} фото.", "warning")
break
if photo and photo.filename:
try:
ext = os.path.splitext(photo.filename)[1].lower()
if ext not in ['.png', '.jpg', '.jpeg', '.gif', '.webp']:
logging.warning(f"Пропущен файл с неподдерживаемым расширением при редактировании: {photo.filename}")
flash(f"Файл {photo.filename} имеет неподдерживаемый тип и был пропущен.", 'warning')
continue
safe_original_filename = secure_filename(os.path.splitext(photo.filename)[0])
photo_filename = f"{product_to_edit['name'].replace(' ','_')}_{safe_original_filename}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{ext}"
temp_path = os.path.join(uploads_dir, photo_filename)
photo.save(temp_path)
logging.info(f"Загрузка нового фото {photo_filename} на HF...")
lock = get_lock(os.path.join("photos", photo_filename)) # Lock for HF upload path
with lock:
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"Update photo for product {product_to_edit['name']}")
new_photos_list.append(photo_filename)
logging.info(f"Новое фото {photo_filename} успешно загружено.")
os.remove(temp_path)
uploaded_count += 1
except Exception as e:
logging.error(f"Ошибка загрузки нового фото {photo.filename}: {e}", exc_info=True)
flash(f"Ошибка при загрузке нового фото {photo.filename}.", 'error')
try:
if os.path.exists(uploads_dir) and not os.listdir(uploads_dir):
os.rmdir(uploads_dir)
except OSError as e:
logging.warning(f"Не удалось удалить временную папку {uploads_dir}: {e}")
if new_photos_list:
logging.info(f"Список фото для товара {product_to_edit['name']} обновлен.")
old_photos = product_to_edit.get('photos', [])
if old_photos:
logging.info(f"Попытка удаления старых фото: {old_photos}")
try:
paths_to_delete = [f"photos/{p}" for p in old_photos]
# Lock deletion? Might be complex. HF Hub handles concurrent ops.
api.delete_files(
repo_id=REPO_ID,
paths_in_repo=paths_to_delete,
repo_type="dataset",
token=HF_TOKEN_WRITE,
commit_message=f"Delete old photos for product {product_to_edit['name']}"
)
logging.info(f"Старые фото для товара {product_to_edit['name']} удалены с HF.")
except Exception as e:
# Log error but don't block the main update
logging.error(f"Ошибка при удалении старых фото {old_photos} с HF: {e}", exc_info=True)
flash("Не удалось удалить старые фотографии с сервера (возможно, их уже нет).", "warning")
product_to_edit['photos'] = new_photos_list
flash("Фотографии товара успешно обновлены.", "success")
elif uploaded_count == 0 and any(f.filename for f in photos_files):
# This case means files were selected, but none were valid/uploaded
flash("Не удалось загрузить ни одну из выбранных новых фотографий (проверьте тип файлов).", "warning")
save_data(data)
logging.info(f"Товар '{original_name}' (индекс {index}) обновлен на '{product_to_edit['name']}'.")
flash(f"Товар '{product_to_edit['name']}' успешно обновлен.", 'success')
elif action == 'delete_product':
index_str = request.form.get('index')
if index_str is None:
flash("Ошибка удаления: индекс товара не передан.", 'error')
return redirect(url_for('admin'))
try:
index = int(index_str)
original_product_list = data.get('products', []) # Работаем с оригинальным списком
if not (0 <= index < len(original_product_list)): raise IndexError("Индекс вне диапазона")
deleted_product = original_product_list.pop(index)
product_name = deleted_product.get('name', 'N/A')
photos_to_delete = deleted_product.get('photos', [])
if photos_to_delete:
if not HF_TOKEN_WRITE:
flash(f"HF_TOKEN для записи не установлен. Фотографии товара '{product_name}' не будут удалены с сервера.", 'warning')
logging.warning(f"HF_TOKEN_WRITE не установлен, пропуск удаления фото для {product_name}.")
else:
logging.info(f"Попытка удаления фото товара '{product_name}' с HF: {photos_to_delete}")
try:
api = HfApi()
paths_to_delete = [f"photos/{p}" for p in photos_to_delete]
api.delete_files(
repo_id=REPO_ID,
paths_in_repo=paths_to_delete,
repo_type="dataset",
token=HF_TOKEN_WRITE,
commit_message=f"Delete photos for deleted product {product_name}"
)
logging.info(f"Фото товара '{product_name}' удалены с HF.")
except Exception as e:
# Log error but don't block the main delete
logging.error(f"Ошибка при удалении фото {photos_to_delete} для товара '{product_name}' с HF: {e}", exc_info=True)
flash(f"Не удалось удалить фото для товара '{product_name}' с сервера (возможно, их уже нет).", "warning")
save_data(data)
logging.info(f"Товар '{product_name}' (индекс {index}) удален.")
flash(f"Товар '{product_name}' удален.", 'success')
except (ValueError, IndexError):
flash(f"Ошибка удаления: неверный индекс товара '{index_str}'.", 'error')
logging.error(f"Ошибка удаления: неверный индекс {index_str}. Всего товаров: {len(original_product_list)}")
elif action == 'add_user':
login = request.form.get('login', '').strip()
password = request.form.get('password', '').strip() # Пароль не очищается от пробелов по краям намеренно
first_name = request.form.get('first_name', '').strip()
last_name = request.form.get('last_name', '').strip()
phone = request.form.get('phone', '').strip()
country = request.form.get('country', '').strip()
city = request.form.get('city', '').strip()
if not login or not password:
flash("Логин и пароль пользователя обязательны.", 'error')
return redirect(url_for('admin'))
if login in users:
flash(f"Пользователь с логином '{login}' уже существует.", 'error')
return redirect(url_for('admin'))
users[login] = {
'password': password,
'first_name': first_name, 'last_name': last_name,
'phone': phone,
'country': country, 'city': city
}
save_users(users)
logging.info(f"Пользователь '{login}' добавлен.")
flash(f"Пользователь '{login}' успешно добавлен.", 'success')
elif action == 'delete_user':
login_to_delete = request.form.get('login')
if login_to_delete and login_to_delete in users:
del users[login_to_delete]
save_users(users)
logging.info(f"Пользователь '{login_to_delete}' удален.")
flash(f"Пользователь '{login_to_delete}' удален.", 'success')
else:
logging.warning(f"Попытка удалить несуществующего или пустого пользователя: {login_to_delete}")
flash(f"Не удалось удалить пользователя '{login_to_delete}'.", 'error')
else:
logging.warning(f"Получено неизвестное действие в админ-панели: {action}")
flash(f"Неизвестное действие: {action}", 'warning')
return redirect(url_for('admin'))
except filelock.Timeout:
logging.error(f"Таймаут блокировки файла при обработке действия '{action}' в админ-панели.")
flash("Не удалось выполнить действие из-за конфликта доступа к файлу. Попробуйте снова.", 'error')
return redirect(url_for('admin'))
except Exception as e:
logging.error(f"Ошибка при обработке действия '{action}' в админ-панели: {e}", exc_info=True)
flash(f"Произошла внутренняя ошибка при выполнении действия '{action}'. Подробности в логе сервера.", 'error')
return redirect(url_for('admin'))
# Передаем оригинальный, несортированный список продуктов для сохранения индексов
original_product_list = data.get('products', [])
categories.sort()
sorted_users = dict(sorted(users.items()))
return render_template_string(
ADMIN_TEMPLATE,
products=original_product_list,
categories=categories,
users=sorted_users,
repo_id=REPO_ID,
currency_code=CURRENCY_CODE
)
@app.route('/force_upload', methods=['POST'])
def force_upload():
logging.info("Запущена принудительная загрузка данных на Hugging Face...")
try:
# upload_db_to_hf уже содержит проверку на пустые файлы
upload_db_to_hf()
flash("Попытка загрузки данных на Hugging Face инициирована (пустые файлы будут пропущены).", 'success')
except Exception as e:
logging.error(f"Ошибка при принудительной загрузке: {e}", exc_info=True)
flash(f"Ошибка при загрузке на Hugging Face: {e}", 'error')
return redirect(url_for('admin'))
@app.route('/force_download', methods=['POST'])
def force_download():
logging.info("Запущено принудительное скачивание данных с Hugging Face...")
try:
success = download_db_from_hf()
if success:
flash("Данные успешно скачаны с Hugging Face. Локальные файлы обновлены.", 'success')
# Перезагружаем данные в память после успешного скачивания
load_data()
load_users()
else:
flash("Не удалось скачать данные с Hugging Face после нескольких попыток. Локальные файлы не изменены.", 'error')
except Exception as e:
logging.error(f"Ошибка при принудительном скачивании: {e}", exc_info=True)
flash(f"Ошибка при скачивании с Hugging Face: {e}", 'error')
return redirect(url_for('admin'))
if __name__ == '__main__':
# Первоначальная загрузка при старте
load_data()
load_users()
if HF_TOKEN_WRITE:
backup_thread = threading.Thread(target=periodic_backup, daemon=True)
backup_thread.start()
logging.info("Поток периодического резервного копирования запущен.")
else:
logging.warning("Периодическое резервное копирование НЕ будет запущено (HF_TOKEN_WRITE не установлен).")
port = int(os.environ.get('PORT', 7860))
logging.info(f"Запуск Flask приложения на хосте 0.0.0.0 и порту {port}")
# debug=False важно для продакшена и работы с потоками/блокировками
app.run(debug=False, host='0.0.0.0', port=port)