e / app.py
Eluza133's picture
Update app.py
1dab35c verified
raw
history blame
22.3 kB
from flask import Flask, render_template_string, request, redirect, url_for, session, flash, jsonify
from flask_caching import Cache
import json
import os
import logging
import threading
import time
from datetime import datetime
from huggingface_hub import HfApi, hf_hub_download
from werkzeug.utils import secure_filename
import random
import string
app = Flask(__name__)
app.secret_key = os.getenv("FLASK_SECRET_KEY", "supersecretkey")
DATA_FILE = 'cloud_data.json'
REPO_ID = "Eluza133/Z1e1u"
HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") or HF_TOKEN_WRITE
ADMIN_PASSWORD = "87132morflot"
MAX_STORAGE_GB = 500
cache = Cache(app, config={'CACHE_TYPE': 'simple'})
logging.basicConfig(level=logging.INFO)
# Проверка токена
if not HF_TOKEN_WRITE:
logging.error("HF_TOKEN_WRITE не установлен. Убедитесь, что переменная окружения HF_TOKEN задана.")
else:
logging.info("HF_TOKEN_WRITE успешно установлен (первые 5 символов: {0}...)".format(HF_TOKEN_WRITE[:5]))
# Функции для работы с базой данных и Hugging Face
@cache.memoize(timeout=300)
def load_data():
try:
download_db_from_hf()
with open(DATA_FILE, 'r', encoding='utf-8') as file:
data = json.load(file)
if not isinstance(data, dict):
logging.warning("Данные не в формате dict, инициализация пустой базы")
return {'users': {}, 'files': {}}
data.setdefault('users', {})
data.setdefault('files', {})
for token, user_data in data['users'].items():
if 'folders' in user_data:
user_data['files'] = user_data['folders'].get('root', {}).get('files', [])
del user_data['folders']
if 'storage_used' not in user_data:
user_data['storage_used'] = 0
logging.info("Данные успешно загружены")
return data
except Exception as e:
logging.error(f"Ошибка при загрузке данных: {e}")
return {'users': {}, 'files': {}}
def save_data(data):
try:
with open(DATA_FILE, 'w', encoding='utf-8') as file:
json.dump(data, file, ensure_ascii=False, indent=4)
upload_db_to_hf()
cache.clear()
logging.info("Данные сохранены и загружены на HF")
except Exception as e:
logging.error(f"Ошибка при сохранении данных: {e}")
raise
def upload_db_to_hf():
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("База данных загружена на Hugging Face")
except Exception as e:
logging.error(f"Ошибка при загрузке базы данных: {e}")
def download_db_from_hf():
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("База данных скачана с Hugging Face")
except Exception as e:
logging.error(f"Ошибка при скачивании базы данных: {e}")
if not os.path.exists(DATA_FILE):
with open(DATA_FILE, 'w', encoding='utf-8') as f:
json.dump({'users': {}, 'files': {}}, f)
def periodic_backup():
while True:
upload_db_to_hf()
time.sleep(1800)
# Генерация 13-значного токена
def generate_token():
return ''.join(random.choices(string.ascii_letters + string.digits, k=13))
# Определение типа файла для предпросмотра
def get_file_type(filename):
video_extensions = ('.mp4', '.mov', '.avi')
image_extensions = ('.jpg', '.jpeg', '.png', '.gif')
if filename.lower().endswith(video_extensions):
return 'video'
elif filename.lower().endswith(image_extensions):
return 'image'
return 'other'
# Базовый стиль CSS
BASE_STYLE = '''
:root {
--primary: #ff4d6d;
--secondary: #00ddeb;
--accent: #8b5cf6;
--background-light: #f5f6fa;
--background-dark: #1a1625;
--card-bg: rgba(255, 255, 255, 0.95);
--card-bg-dark: rgba(40, 35, 60, 0.95);
--text-light: #2a1e5a;
--text-dark: #e8e1ff;
--shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
--glass-bg: rgba(255, 255, 255, 0.15);
--transition: all 0.3s ease;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Inter', sans-serif;
background: var(--background-light);
color: var(--text-light);
line-height: 1.6;
}
body.dark {
background: var(--background-dark);
color: var(--text-dark);
}
.container {
margin: 20px auto;
max-width: 1200px;
padding: 25px;
background: var(--card-bg);
border-radius: 20px;
box-shadow: var(--shadow);
}
body.dark .container {
background: var(--card-bg-dark);
}
h1 {
font-size: 2em;
font-weight: 800;
text-align: center;
margin-bottom: 25px;
background: linear-gradient(135deg, var(--primary), var(--accent));
-webkit-background-clip: text;
color: transparent;
}
input, textarea, select {
width: 100%;
padding: 14px;
margin: 12px 0;
border: none;
border-radius: 14px;
background: var(--glass-bg);
color: var(--text-light);
font-size: 1.1em;
box-shadow: inset 0 3px 10px rgba(0, 0, 0, 0.1);
}
body.dark input, body.dark textarea, body.dark select {
color: var(--text-dark);
}
input:focus, textarea:focus, select:focus {
outline: none;
box-shadow: 0 0 0 4px var(--primary);
}
.btn {
padding: 14px 28px;
background: var(--primary);
color: white;
border: none;
border-radius: 14px;
cursor: pointer;
font-size: 1.1em;
font-weight: 600;
transition: var(--transition);
box-shadow: var(--shadow);
display: inline-block;
text-decoration: none;
}
.btn:hover {
transform: scale(1.05);
background: #e6415f;
}
.download-btn {
background: var(--secondary);
margin-top: 10px;
}
.download-btn:hover {
background: #00b8c5;
}
.flash {
color: var(--secondary);
text-align: center;
margin-bottom: 15px;
}
.file-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
margin-top: 20px;
}
.file-item {
background: var(--card-bg);
padding: 15px;
border-radius: 16px;
box-shadow: var(--shadow);
text-align: center;
transition: var(--transition);
}
body.dark .file-item {
background: var(--card-bg-dark);
}
.file-item:hover {
transform: translateY(-5px);
}
.file-preview {
max-width: 100%;
max-height: 200px;
object-fit: cover;
border-radius: 10px;
margin-bottom: 10px;
loading: lazy;
}
.file-item p {
font-size: 0.9em;
margin: 5px 0;
}
.file-item a {
color: var(--primary);
text-decoration: none;
}
.file-item a:hover {
color: var(--accent);
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.85);
z-index: 2000;
justify-content: center;
align-items: center;
}
.modal img, .modal video {
max-width: 95%;
max-height: 95%;
object-fit: contain;
border-radius: 20px;
box-shadow: var(--shadow);
}
.upload-progress, .storage-indicator {
width: 100%;
margin: 20px 0;
}
.upload-progress {
display: none;
}
.progress-bar-container, .storage-bar-container {
width: 100%;
height: 20px;
background: var(--glass-bg);
border-radius: 10px;
overflow: hidden;
position: relative;
}
.progress-bar, .storage-bar {
height: 100%;
width: 0%;
background: linear-gradient(90deg, var(--primary), var(--accent));
transition: width 0.3s ease;
}
.progress-text, .storage-text {
position: absolute;
width: 100%;
text-align: center;
font-size: 0.9em;
color: var(--text-light);
top: 50%;
transform: translateY(-50%);
}
body.dark .progress-text, body.dark .storage-text {
color: var(--text-dark);
}
@media (max-width: 768px) {
.file-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 480px) {
.file-grid {
grid-template-columns: 1fr;
}
.btn {
padding: 12px 20px;
font-size: 1em;
}
}
'''
# Регистрация через /admhosto
@app.route('/admhosto', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
password = request.form.get('password')
if password == ADMIN_PASSWORD:
token = generate_token()
data = load_data()
data['users'][token] = {
'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'files': [],
'storage_used': 0
}
save_data(data)
flash(f'Ваш токен: {token}. Сохраните его!')
return redirect(url_for('register'))
else:
flash('Неверный пароль!')
html = '''
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Регистрация - Zues Cloud</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
<style>''' + BASE_STYLE + '''</style>
</head>
<body>
<div class="container">
<h1>Регистрация в Zues Cloud</h1>
{% with messages = get_flashed_messages() %}
{% if messages %}
{% for message in messages %}
<div class="flash">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST">
<input type="password" name="password" placeholder="Введите пароль" required>
<button type="submit" class="btn">Зарегистрироваться</button>
</form>
</div>
</body>
</html>
'''
return render_template_string(html)
# Главная страница с вводом токена
@app.route('/', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
token = request.form.get('token')
data = load_data()
if token in data['users'] and len(token) == 13:
session['token'] = token
return redirect(url_for('dashboard'))
else:
flash('Неверный токен! Токен должен быть 13 символов.')
html = '''
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Zues Cloud</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
<style>''' + BASE_STYLE + '''</style>
</head>
<body>
<div class="container">
<h1>Zues Cloud</h1>
{% with messages = get_flashed_messages() %}
{% if messages %}
{% for message in messages %}
<div class="flash">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST" id="login-form">
<input type="text" name="token" placeholder="Введите ваш токен" required>
<button type="submit" class="btn">Войти</button>
</form>
<p style="margin-top: 20px;">Нет токена? <a href="{{ url_for('register') }}">Зарегистрируйтесь</a></p>
</div>
<script>
document.getElementById('login-form').onsubmit = function(e) {
const token = e.target.querySelector('input[name="token"]').value;
localStorage.setItem('token', token);
};
</script>
</body>
</html>
'''
return render_template_string(html)
# Личный dashboard
@app.route('/dashboard', methods=['GET', 'POST'])
def dashboard():
if 'token' not in session:
flash('Пожалуйста, войдите!')
return redirect(url_for('login'))
token = session['token']
data = load_data()
if token not in data['users']:
session.pop('token', None)
flash('Токен недействителен!')
return redirect(url_for('login'))
user_files = data['users'][token]['files']
storage_used = data['users'][token]['storage_used']
max_storage_bytes = MAX_STORAGE_GB * 1024 * 1024 * 1024
storage_percent = (storage_used / max_storage_bytes) * 100 if max_storage_bytes > 0 else 0
storage_used_gb = storage_used / (1024 * 1024 * 1024)
storage_remaining_gb = MAX_STORAGE_GB - storage_used_gb
if request.method == 'POST':
if 'upload_files' in request.files:
files = request.files.getlist('files')
total_size = sum(f.content_length for f in files if f)
if storage_used + total_size > max_storage_bytes:
flash('Недостаточно места для загрузки файлов!')
else:
for file in files:
if file and file.filename:
filename = secure_filename(file.filename)
temp_path = os.path.join('uploads', filename)
os.makedirs('uploads', exist_ok=True)
file.save(temp_path)
file_size = os.path.getsize(temp_path)
api = HfApi()
file_path = f"cloud_files/{token}/{filename}"
try:
logging.info(f"Попытка загрузки файла: {file_path} в репозиторий {REPO_ID}")
if not HF_TOKEN_WRITE:
raise ValueError("HF_TOKEN_WRITE не установлен")
logging.info(f"Используемый токен: {HF_TOKEN_WRITE[:5]}... (скрыт для безопасности)")
# Проверка доступности репозитория
api.repo_info(repo_id=REPO_ID, token=HF_TOKEN_WRITE)
api.upload_file(
path_or_fileobj=temp_path,
path_in_repo=file_path,
repo_id=REPO_ID,
repo_type="dataset",
token=HF_TOKEN_WRITE,
commit_message=f"Загружен файл {filename} для {token}"
)
logging.info(f"Файл {filename} успешно загружен в Hugging Face")
except Exception as e:
logging.error(f"Ошибка при загрузке файла на Hugging Face: {str(e)}")
flash(f"Ошибка при загрузке файла {filename}: {str(e)}")
if os.path.exists(temp_path):
os.remove(temp_path)
continue
file_info = {
'filename': filename,
'path': file_path,
'type': get_file_type(filename),
'upload_date': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'size': file_size
}
user_files.append(file_info)
data['users'][token]['storage_used'] += file_size
if os.path.exists(temp_path):
os.remove(temp_path)
save_data(data)
flash('Файлы успешно загружены!')
return redirect(url_for('dashboard'))
html = '''
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard - Zues Cloud</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
<style>''' + BASE_STYLE + '''</style>
</head>
<body>
<div class="container">
<h1>Zues Cloud Dashboard</h1>
<p>Токен: {{ token }}</p>
<div class="storage-indicator">
<p>Использовано места: {{ "%.2f" % storage_used_gb }} ГБ из {{ MAX_STORAGE_GB }} ГБ (Осталось: {{ "%.2f" % storage_remaining_gb }} ГБ)</p>
<div class="storage-bar-container">
<div class="storage-bar" style="width: {{ storage_percent }}%;"></div>
<span class="storage-text">{{ "%.1f" % storage_percent }}%</span>
</div>
</div>
<h2 style="margin-top: 20px;">Загрузить файлы</h2>
{% with messages = get_flashed_messages() %}
{% if messages %}
{% for message in messages %}
<div class="flash">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST" enctype="multipart/form-data" id="upload-form">
<input type="file" name="files" multiple required>
<button type="submit" name="upload_files" class="btn">Загрузить</button>
</form>
<div class="upload-progress" id="upload-progress">
<div class="progress-bar-container">
<div class="progress-bar" id="progress-bar"></div>
<span class="progress-text" id="progress-text">0%</span>
</div>
</div>
<h2 style="margin-top: 30px;">Файлы</h2>
<div class="file-grid">
{% for file in user_files %}
<div class="file-item">
{% if file['type'] == 'video' %}
<video class="file-preview" preload="metadata" muted loading="lazy" onclick="openModal('https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ file['path'] }}', true)">
<source src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ file['path'] }}" type="video/mp4">
</video>
{% elif file['type'] == 'image' %}
<img class="file-preview" src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ file['path'] }}" alt="{{ file['filename'] }}" loading="lazy" onclick="openModal(this.src, false)">
{% else %}
<p><a href="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ file['path'] }}" target="_blank">{{ file['filename'] }}</a></p>
{% endif %}
<p>{{ file['upload_date'] }} ({{ "%.2f" % (file['size'] / (1024 * 1024)) }} МБ)</p>
<a href="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ file['path'] }}" class="btn download-btn" download="{{ file['filename'] }}">Скачать</a>
</div>
{% endfor %}
{% if not user_files %}
<p>Файлов нет.</p>
{% endif %}
</div>
<a href="{{ url_for('logout') }}" class="btn" style="margin-top: 20px;">Выйти</a>
</div>
<div class="modal" id="mediaModal" onclick="closeModal(event)">
<div id="modalContent"></div>
</div>
<script>
function openModal(src, isVideo) {
const modal = document.getElementById('mediaModal');
const modalContent = document.getElementById('modalContent');
if (isVideo) {
modalContent.innerHTML = `<video controls><source src="${src}" type="video/mp4"></video>`;
} else {
modalContent.innerHTML = `<img src="${src}">`;
}
modal.style.display = 'flex';
}
function closeModal(event) {
if (event.target.tagName !== 'IMG' && event.target.tagName !== 'VIDEO') {
const modal = document.getElementById('mediaModal');
modal.style.display = 'none';
modal.innerHTML = '<div id="modalContent"></div>';
}
}
document.getElementById('upload-form').onsubmit = function(e) {
const progressContainer = document.getElementById('upload-progress');
progressContainer.style.display = 'block';
const progressBar = document.getElementById('progress-bar');
const progressText = document.getElementById('progress-text');
let percent = 0;
const interval = setInterval(() => {
percent += 10;
if (percent > 100) percent = 100;
progressBar.style.width = percent + '%';
progressText.textContent = percent + '%';
if (percent === 100) clearInterval(interval);
}, 200);
};
</script>
</body>
</html>
'''
return render_template_string(
html,
token=token,
user_files=user_files,
repo_id=REPO_ID,
storage_used_gb=storage_used_gb,
storage_remaining_gb=storage_remaining_gb,
storage_percent=storage_percent,
MAX_STORAGE_GB=MAX_STORAGE_GB
)
# Выход
@app.route('/logout')
def logout():
session.pop('token', None)
return redirect(url_for('login'))
if __name__ == '__main__':
threading.Thread(target=periodic_backup, daemon=True).start()
app.run(debug=True, host='0.0.0.0', port=7860)