|
|
from flask import Flask, render_template_string, request, redirect, url_for |
|
|
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 |
|
|
|
|
|
app = Flask(__name__) |
|
|
DATA_FILE = 'products.json' |
|
|
|
|
|
|
|
|
REPO_ID = "Testbase1/testsett" |
|
|
HF_TOKEN_WRITE = os.getenv("HF_TOKEN") |
|
|
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") |
|
|
|
|
|
|
|
|
logging.basicConfig(level=logging.DEBUG) |
|
|
|
|
|
|
|
|
def load_data(): |
|
|
"""Загружает данные из JSON-файла или скачивает их из репозитория Hugging Face.""" |
|
|
try: |
|
|
|
|
|
download_db_from_hf() |
|
|
with open(DATA_FILE, 'r', encoding='utf-8') as file: |
|
|
return json.load(file) |
|
|
except FileNotFoundError: |
|
|
logging.warning("Локальный файл базы данных не найден после скачивания.") |
|
|
return [] |
|
|
except json.JSONDecodeError: |
|
|
logging.error("Ошибка: Невозможно декодировать JSON файл.") |
|
|
return [] |
|
|
except RepositoryNotFoundError: |
|
|
logging.error("Репозиторий не найден. Создание локальной базы данных.") |
|
|
return [] |
|
|
except Exception as e: |
|
|
logging.error(f"Произошла ошибка при загрузке данных: {e}") |
|
|
return [] |
|
|
|
|
|
|
|
|
def save_data(data): |
|
|
"""Сохраняет данные в JSON-файл.""" |
|
|
try: |
|
|
with open(DATA_FILE, 'w', encoding='utf-8') as file: |
|
|
json.dump(data, file, ensure_ascii=False, indent=4) |
|
|
except Exception as e: |
|
|
logging.error(f"Ошибка при сохранении данных: {e}") |
|
|
raise |
|
|
|
|
|
|
|
|
def upload_db_to_hf(): |
|
|
"""Загружает JSON-файл базы данных в репозиторий Hugging Face.""" |
|
|
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}") |
|
|
|
|
|
def download_db_from_hf(): |
|
|
"""Скачивает JSON-файл базы данных из репозитория Hugging Face.""" |
|
|
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 |
|
|
) |
|
|
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 periodic_backup(): |
|
|
"""Периодически вызывает функцию upload_db_to_hf() каждые 100 секунд.""" |
|
|
while True: |
|
|
upload_db_to_hf() |
|
|
time.sleep(15) |
|
|
|
|
|
|
|
|
@app.route('/') |
|
|
def catalog(): |
|
|
products = load_data() |
|
|
catalog_html = ''' |
|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>Каталог</title> |
|
|
<style> |
|
|
body { |
|
|
font-family: Arial, sans-serif; |
|
|
margin: 20px; |
|
|
background-color: #f9f9f9; |
|
|
} |
|
|
h1 { |
|
|
color: #333; |
|
|
} |
|
|
.product { |
|
|
background-color: #fff; |
|
|
border: 1px solid #ddd; |
|
|
padding: 15px; |
|
|
margin-bottom: 10px; |
|
|
border-radius: 5px; |
|
|
} |
|
|
.product h2 { |
|
|
margin-top: 0; |
|
|
color: #555; |
|
|
} |
|
|
.product p { |
|
|
color: #777; |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<h1>Каталог товаров</h1> |
|
|
{% for product in products %} |
|
|
<div class="product"> |
|
|
<h2>{{ product['name'] }}</h2> |
|
|
<p><strong>Цена:</strong> {{ product['price'] }} руб.</p> |
|
|
<p><strong>Описание:</strong> {{ product['description'] }}</p> |
|
|
</div> |
|
|
{% endfor %} |
|
|
</body> |
|
|
</html> |
|
|
''' |
|
|
return render_template_string(catalog_html, products=products) |
|
|
|
|
|
@app.route('/admin', methods=['GET', 'POST']) |
|
|
def admin(): |
|
|
if request.method == 'POST': |
|
|
name = request.form.get('name') |
|
|
price = request.form.get('price') |
|
|
description = request.form.get('description') |
|
|
|
|
|
logging.debug(f"Полученные данные из формы: name={name}, price={price}, description={description}") |
|
|
|
|
|
if name and price and description: |
|
|
try: |
|
|
price = float(price.replace(',', '.')) |
|
|
except ValueError: |
|
|
logging.error("Ошибка: Цена должна быть числом.") |
|
|
return "Ошибка: Цена должна быть числом.", 400 |
|
|
|
|
|
products = load_data() |
|
|
products.append({ |
|
|
'name': name, |
|
|
'price': price, |
|
|
'description': description |
|
|
}) |
|
|
|
|
|
try: |
|
|
save_data(products) |
|
|
except Exception as e: |
|
|
return f"Ошибка при сохранении данных: {e}", 500 |
|
|
|
|
|
return redirect(url_for('admin')) |
|
|
|
|
|
products = load_data() |
|
|
admin_html = ''' |
|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>Админ-панель</title> |
|
|
<style> |
|
|
body { |
|
|
font-family: Arial, sans-serif; |
|
|
margin: 20px; |
|
|
background-color: #f9f9f9; |
|
|
} |
|
|
h1 { |
|
|
color: #333; |
|
|
} |
|
|
form { |
|
|
background-color: #fff; |
|
|
padding: 20px; |
|
|
border: 1px solid #ddd; |
|
|
border-radius: 5px; |
|
|
max-width: 400px; |
|
|
} |
|
|
label { |
|
|
display: block; |
|
|
margin-top: 10px; |
|
|
color: #555; |
|
|
} |
|
|
input, textarea { |
|
|
width: 100%; |
|
|
padding: 8px; |
|
|
margin-top: 5px; |
|
|
border: 1px solid #ddd; |
|
|
border-radius: 4px; |
|
|
} |
|
|
button { |
|
|
margin-top: 15px; |
|
|
padding: 10px 15px; |
|
|
background-color: #28a745; |
|
|
color: white; |
|
|
border: none; |
|
|
border-radius: 4px; |
|
|
cursor: pointer; |
|
|
} |
|
|
button:hover { |
|
|
background-color: #218838; |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<h1>Добавление товара</h1> |
|
|
<form method="POST"> |
|
|
<label for="name">Название товара:</label> |
|
|
<input type="text" id="name" name="name" required> |
|
|
|
|
|
<label for="price">Цена:</label> |
|
|
<input type="number" id="price" name="price" step="0.01" required> |
|
|
|
|
|
<label for="description">Описание:</label> |
|
|
<textarea id="description" name="description" rows="4" required></textarea> |
|
|
|
|
|
<button type="submit">Добавить товар</button> |
|
|
</form> |
|
|
|
|
|
<h2>Управление базой данных</h2> |
|
|
|
|
|
<!-- Кнопки для резервной копии и скачивания --> |
|
|
<form method="POST" action="{{ url_for('backup') }}"> |
|
|
<button type="submit">Создать резервную копию</button> |
|
|
</form> |
|
|
|
|
|
<form method="GET" action="{{ url_for('download') }}"> |
|
|
<button type="submit">Скачать базу данных</button> |
|
|
</form> |
|
|
|
|
|
</body> |
|
|
</html> |
|
|
''' |
|
|
return render_template_string(admin_html) |
|
|
|
|
|
@app.route('/backup', methods=['POST']) |
|
|
def backup(): |
|
|
"""Маршрут для создания резервной копии.""" |
|
|
upload_db_to_hf() |
|
|
return "Резервная копия успешно создана.", 200 |
|
|
|
|
|
@app.route('/download', methods=['GET']) |
|
|
def download(): |
|
|
"""Маршрут для скачивания базы данных.""" |
|
|
download_db_from_hf() |
|
|
return "База данных успешно скачана.", 200 |
|
|
|
|
|
if __name__ == '__main__': |
|
|
|
|
|
backup_thread = threading.Thread(target=periodic_backup, daemon=True) |
|
|
backup_thread.start() |
|
|
|
|
|
|
|
|
try: |
|
|
load_data() |
|
|
except Exception as e: |
|
|
logging.error(f"Не удалось загрузить базу данных при запуске: {e}") |
|
|
|
|
|
|
|
|
|
|
|
app.run(debug=True, host='0.0.0.0', port=7860) |
|
|
|