| 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])) |
|
|
| |
| @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) |
|
|
| |
| 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' |
|
|
| |
| 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; |
| } |
| } |
| ''' |
|
|
| |
| @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) |
|
|
| |
| @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) |