Spaces:
Running
Running
# app.py | |
import os | |
# Ensure Hugging Face cache writes to tmp | |
os.environ["HF_HOME"] = "/tmp/hf_home" | |
import shutil, zipfile, threading, time, json, uuid | |
from datetime import datetime, timedelta | |
from flask import Flask, request, render_template_string, jsonify | |
import gdown | |
from huggingface_hub import HfApi, login, upload_folder, list_repo_files | |
from googleapiclient.discovery import build | |
from google.oauth2.credentials import Credentials | |
from google_auth_oauthlib.flow import InstalledAppFlow | |
from google.auth.transport.requests import Request | |
import pickle | |
# Environment variables | |
FOLDER_URL = os.getenv("FOLDER_URL") | |
REPO_ID = os.getenv("REPO_ID") | |
TOKEN = os.getenv("HF_TOKEN") | |
GOOGLE_CREDENTIALS = os.getenv("GOOGLE_CREDENTIALS") # JSON string of credentials | |
# Directories | |
DOWNLOAD_DIR = "/tmp/backups" | |
EXTRACT_DIR = "/tmp/extracted_backups" | |
GDRIVE_DIR = "/tmp/gdrive_files" | |
# Global state | |
app_state = { | |
"last_backup_time": "Never", | |
"schedule_interval": 0, | |
"status": "Ready", | |
"backup_history": [], | |
"gdrive_connected": False, | |
"auto_cleanup": True, | |
"max_backups": 10, | |
"notification_enabled": True, | |
"backup_running": False, | |
"total_backups": 0, | |
"last_error": None, | |
"gdrive_files": [] | |
} | |
app = Flask(__name__) | |
# Google Drive Integration | |
class GDriveManager: | |
def __init__(self): | |
self.service = None | |
self.creds = None | |
def authenticate(self): | |
try: | |
if GOOGLE_CREDENTIALS: | |
creds_data = json.loads(GOOGLE_CREDENTIALS) | |
self.creds = Credentials.from_authorized_user_info(creds_data) | |
self.service = build('drive', 'v3', credentials=self.creds) | |
app_state["gdrive_connected"] = True | |
return True | |
except Exception as e: | |
app_state["last_error"] = f"GDrive auth failed: {str(e)}" | |
return False | |
def list_files(self, folder_id=None): | |
if not self.service: | |
return [] | |
try: | |
query = f"'{folder_id}' in parents" if folder_id else "mimeType='application/zip' or mimeType='application/x-zip-compressed'" | |
results = self.service.files().list( | |
q=query, | |
pageSize=50, | |
fields="files(id, name, size, modifiedTime, mimeType)" | |
).execute() | |
return results.get('files', []) | |
except Exception as e: | |
app_state["last_error"] = f"GDrive list error: {str(e)}" | |
return [] | |
def download_file(self, file_id, filename): | |
if not self.service: | |
return False | |
try: | |
os.makedirs(GDRIVE_DIR, exist_ok=True) | |
request = self.service.files().get_media(fileId=file_id) | |
with open(os.path.join(GDRIVE_DIR, filename), 'wb') as f: | |
downloader = MediaIoBaseDownload(f, request) | |
done = False | |
while done is False: | |
status, done = downloader.next_chunk() | |
return True | |
except Exception as e: | |
app_state["last_error"] = f"GDrive download error: {str(e)}" | |
return False | |
gdrive = GDriveManager() | |
# Enhanced backup logic | |
def run_backup(source="gdrive"): | |
global app_state | |
if app_state["backup_running"]: | |
return {"status": "error", "message": "Backup already running"} | |
app_state["backup_running"] = True | |
log_entries = [] | |
backup_id = str(uuid.uuid4())[:8] | |
start_time = datetime.now() | |
try: | |
log_entries.append(f"[{start_time.strftime('%H:%M:%S')}] Starting backup #{backup_id}") | |
# Clean directories | |
shutil.rmtree(DOWNLOAD_DIR, ignore_errors=True) | |
shutil.rmtree(EXTRACT_DIR, ignore_errors=True) | |
os.makedirs(DOWNLOAD_DIR, exist_ok=True) | |
os.makedirs(EXTRACT_DIR, exist_ok=True) | |
log_entries.append("Directories prepared") | |
# Download based on source | |
if source == "gdrive" and app_state["gdrive_connected"]: | |
log_entries.append("Downloading from Google Drive...") | |
gdrive_files = gdrive.list_files() | |
for file in gdrive_files[:5]: # Limit to 5 recent files | |
if gdrive.download_file(file['id'], file['name']): | |
log_entries.append(f"Downloaded: {file['name']}") | |
# Move gdrive files to download dir | |
if os.path.exists(GDRIVE_DIR): | |
for f in os.listdir(GDRIVE_DIR): | |
shutil.move(os.path.join(GDRIVE_DIR, f), os.path.join(DOWNLOAD_DIR, f)) | |
else: | |
log_entries.append(f"Downloading from URL: {FOLDER_URL}") | |
gdown.download_folder(url=FOLDER_URL, output=DOWNLOAD_DIR, use_cookies=False, quiet=True) | |
log_entries.append("Download completed") | |
# Extract archives | |
extracted_count = 0 | |
for root, _, files in os.walk(DOWNLOAD_DIR): | |
for f in files: | |
if f.endswith(('.zip', '.rar', '.7z')): | |
zp = os.path.join(root, f) | |
try: | |
with zipfile.ZipFile(zp) as z: | |
z.extractall(EXTRACT_DIR) | |
extracted_count += 1 | |
log_entries.append(f"Extracted: {f}") | |
except Exception as e: | |
log_entries.append(f"Failed to extract {f}: {str(e)}") | |
# Fix common folder naming issues | |
fixes = [ | |
("world_nither", "world_nether"), | |
("world_end", "world_the_end"), | |
("plugin", "plugins") | |
] | |
for bad, good in fixes: | |
bad_path = os.path.join(EXTRACT_DIR, bad) | |
good_path = os.path.join(EXTRACT_DIR, good) | |
if os.path.exists(bad_path) and not os.path.exists(good_path): | |
os.rename(bad_path, good_path) | |
log_entries.append(f"Fixed folder: {bad} → {good}") | |
# Upload to Hugging Face | |
login(token=TOKEN) | |
api = HfApi() | |
log_entries.append("Connected to Hugging Face") | |
api.create_repo(repo_id=REPO_ID, repo_type="dataset", private=False, exist_ok=True, token=TOKEN) | |
subfolders = { | |
"world": os.path.join(EXTRACT_DIR, "world"), | |
"world_nether": os.path.join(EXTRACT_DIR, "world_nether"), | |
"world_the_end": os.path.join(EXTRACT_DIR, "world_the_end"), | |
"plugins": os.path.join(EXTRACT_DIR, "plugins"), | |
"logs": os.path.join(EXTRACT_DIR, "logs"), | |
"config": os.path.join(EXTRACT_DIR, "config") | |
} | |
uploaded_folders = [] | |
for name, path in subfolders.items(): | |
if os.path.exists(path): | |
try: | |
upload_folder( | |
repo_id=REPO_ID, | |
folder_path=path, | |
repo_type="dataset", | |
token=TOKEN, | |
path_in_repo=name, | |
commit_message=f"Backup #{backup_id} - {name} - {datetime.now().strftime('%Y-%m-%d %H:%M')}" | |
) | |
uploaded_folders.append(name) | |
log_entries.append(f"✓ Uploaded: {name}") | |
except Exception as e: | |
log_entries.append(f"✗ Failed to upload {name}: {str(e)}") | |
# Update state | |
end_time = datetime.now() | |
duration = (end_time - start_time).total_seconds() | |
backup_record = { | |
"id": backup_id, | |
"timestamp": end_time.isoformat(), | |
"duration": f"{duration:.1f}s", | |
"source": source, | |
"folders": uploaded_folders, | |
"files_extracted": extracted_count, | |
"status": "success" | |
} | |
app_state["backup_history"].insert(0, backup_record) | |
app_state["last_backup_time"] = end_time.strftime("%Y-%m-%d %H:%M:%S") | |
app_state["total_backups"] += 1 | |
app_state["last_error"] = None | |
# Auto-cleanup old backups | |
if app_state["auto_cleanup"] and len(app_state["backup_history"]) > app_state["max_backups"]: | |
app_state["backup_history"] = app_state["backup_history"][:app_state["max_backups"]] | |
log_entries.append(f"✓ Backup completed in {duration:.1f}s") | |
except Exception as e: | |
error_msg = str(e) | |
log_entries.append(f"✗ Error: {error_msg}") | |
app_state["last_error"] = error_msg | |
backup_record = { | |
"id": backup_id, | |
"timestamp": datetime.now().isoformat(), | |
"duration": f"{(datetime.now() - start_time).total_seconds():.1f}s", | |
"source": source, | |
"status": "failed", | |
"error": error_msg | |
} | |
app_state["backup_history"].insert(0, backup_record) | |
finally: | |
app_state["backup_running"] = False | |
return { | |
"status": "success" if not app_state["last_error"] else "error", | |
"log": log_entries, | |
"backup_id": backup_id | |
} | |
# Scheduler | |
def schedule_loop(): | |
while True: | |
if app_state["schedule_interval"] > 0 and not app_state["backup_running"]: | |
run_backup() | |
time.sleep(60 if app_state["schedule_interval"] > 0 else 30) | |
# Start scheduler thread | |
threading.Thread(target=schedule_loop, daemon=True).start() | |
# Initialize Google Drive | |
gdrive.authenticate() | |
if app_state["gdrive_connected"]: | |
app_state["gdrive_files"] = gdrive.list_files() | |
# HTML Template | |
HTML_TEMPLATE = ''' | |
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Minecraft Backup Manager Pro</title> | |
<style> | |
* { | |
margin: 0; | |
padding: 0; | |
box-sizing: border-box; | |
} | |
:root { | |
--primary: #2563eb; | |
--primary-dark: #1d4ed8; | |
--secondary: #64748b; | |
--success: #10b981; | |
--warning: #f59e0b; | |
--error: #ef4444; | |
--bg-primary: #0f172a; | |
--bg-secondary: #1e293b; | |
--bg-card: #334155; | |
--text-primary: #f8fafc; | |
--text-secondary: #cbd5e1; | |
--border: #475569; | |
--shadow: 0 10px 25px rgba(0, 0, 0, 0.3); | |
} | |
body { | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
background: linear-gradient(135deg, var(--bg-primary) 0%, #1e293b 100%); | |
color: var(--text-primary); | |
min-height: 100vh; | |
line-height: 1.6; | |
} | |
.container { | |
max-width: 1400px; | |
margin: 0 auto; | |
padding: 20px; | |
} | |
.header { | |
text-align: center; | |
margin-bottom: 40px; | |
padding: 30px 0; | |
} | |
.header h1 { | |
font-size: 2.5rem; | |
font-weight: 700; | |
background: linear-gradient(135deg, var(--primary) 0%, #06b6d4 100%); | |
-webkit-background-clip: text; | |
-webkit-text-fill-color: transparent; | |
margin-bottom: 10px; | |
} | |
.header p { | |
color: var(--text-secondary); | |
font-size: 1.1rem; | |
} | |
.status-bar { | |
display: grid; | |
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); | |
gap: 20px; | |
margin-bottom: 30px; | |
} | |
.status-card { | |
background: var(--bg-card); | |
border-radius: 12px; | |
padding: 20px; | |
box-shadow: var(--shadow); | |
border: 1px solid var(--border); | |
} | |
.status-card h3 { | |
font-size: 0.9rem; | |
text-transform: uppercase; | |
letter-spacing: 1px; | |
color: var(--text-secondary); | |
margin-bottom: 8px; | |
} | |
.status-value { | |
font-size: 1.8rem; | |
font-weight: 600; | |
color: var(--text-primary); | |
} | |
.status-indicator { | |
display: inline-block; | |
width: 12px; | |
height: 12px; | |
border-radius: 50%; | |
margin-right: 8px; | |
} | |
.status-online { background: var(--success); } | |
.status-offline { background: var(--error); } | |
.status-running { background: var(--warning); animation: pulse 2s infinite; } | |
@keyframes pulse { | |
0%, 100% { opacity: 1; } | |
50% { opacity: 0.5; } | |
} | |
.main-grid { | |
display: grid; | |
grid-template-columns: 1fr 1fr; | |
gap: 30px; | |
margin-bottom: 30px; | |
} | |
.card { | |
background: var(--bg-secondary); | |
border-radius: 16px; | |
padding: 30px; | |
box-shadow: var(--shadow); | |
border: 1px solid var(--border); | |
} | |
.card h2 { | |
font-size: 1.5rem; | |
margin-bottom: 20px; | |
color: var(--text-primary); | |
display: flex; | |
align-items: center; | |
gap: 10px; | |
} | |
.form-group { | |
margin-bottom: 20px; | |
} | |
.form-group label { | |
display: block; | |
margin-bottom: 8px; | |
font-weight: 500; | |
color: var(--text-secondary); | |
} | |
.form-control { | |
width: 100%; | |
padding: 12px 16px; | |
border: 2px solid var(--border); | |
border-radius: 8px; | |
background: var(--bg-card); | |
color: var(--text-primary); | |
font-size: 1rem; | |
transition: all 0.3s ease; | |
} | |
.form-control:focus { | |
outline: none; | |
border-color: var(--primary); | |
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); | |
} | |
.btn { | |
display: inline-flex; | |
align-items: center; | |
justify-content: center; | |
gap: 8px; | |
padding: 12px 24px; | |
border: none; | |
border-radius: 8px; | |
font-size: 1rem; | |
font-weight: 500; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
text-decoration: none; | |
min-height: 48px; | |
} | |
.btn-primary { | |
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%); | |
color: white; | |
} | |
.btn-primary:hover { | |
transform: translateY(-2px); | |
box-shadow: 0 8px 25px rgba(37, 99, 235, 0.3); | |
} | |
.btn-success { | |
background: linear-gradient(135deg, var(--success) 0%, #059669 100%); | |
color: white; | |
} | |
.btn-success:hover { | |
transform: translateY(-2px); | |
box-shadow: 0 8px 25px rgba(16, 185, 129, 0.3); | |
} | |
.btn-warning { | |
background: linear-gradient(135deg, var(--warning) 0%, #d97706 100%); | |
color: white; | |
} | |
.btn-full { | |
width: 100%; | |
margin-bottom: 15px; | |
} | |
.btn:disabled { | |
opacity: 0.6; | |
cursor: not-allowed; | |
transform: none !important; | |
} | |
.logs { | |
background: var(--bg-primary); | |
border: 1px solid var(--border); | |
border-radius: 8px; | |
padding: 20px; | |
max-height: 400px; | |
overflow-y: auto; | |
font-family: 'Courier New', monospace; | |
font-size: 0.9rem; | |
line-height: 1.4; | |
} | |
.log-entry { | |
margin-bottom: 5px; | |
padding: 4px 0; | |
} | |
.log-success { color: var(--success); } | |
.log-error { color: var(--error); } | |
.log-warning { color: var(--warning); } | |
.history-grid { | |
display: grid; | |
gap: 15px; | |
max-height: 500px; | |
overflow-y: auto; | |
} | |
.history-item { | |
background: var(--bg-card); | |
border: 1px solid var(--border); | |
border-radius: 8px; | |
padding: 15px; | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
} | |
.history-info h4 { | |
margin-bottom: 5px; | |
color: var(--text-primary); | |
} | |
.history-meta { | |
font-size: 0.9rem; | |
color: var(--text-secondary); | |
} | |
.badge { | |
padding: 4px 12px; | |
border-radius: 20px; | |
font-size: 0.8rem; | |
font-weight: 500; | |
} | |
.badge-success { | |
background: rgba(16, 185, 129, 0.2); | |
color: var(--success); | |
} | |
.badge-error { | |
background: rgba(239, 68, 68, 0.2); | |
color: var(--error); | |
} | |
.gdrive-files { | |
max-height: 300px; | |
overflow-y: auto; | |
} | |
.file-item { | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
padding: 12px; | |
border: 1px solid var(--border); | |
border-radius: 8px; | |
margin-bottom: 10px; | |
background: var(--bg-card); | |
} | |
.file-info h4 { | |
margin-bottom: 4px; | |
color: var(--text-primary); | |
} | |
.file-meta { | |
font-size: 0.8rem; | |
color: var(--text-secondary); | |
} | |
.loading { | |
display: inline-block; | |
width: 20px; | |
height: 20px; | |
border: 3px solid rgba(255, 255, 255, 0.3); | |
border-radius: 50%; | |
border-top-color: var(--primary); | |
animation: spin 1s ease-in-out infinite; | |
} | |
@keyframes spin { | |
to { transform: rotate(360deg); } | |
} | |
.toast { | |
position: fixed; | |
top: 20px; | |
right: 20px; | |
padding: 15px 20px; | |
border-radius: 8px; | |
color: white; | |
font-weight: 500; | |
z-index: 1000; | |
opacity: 0; | |
transform: translateX(100%); | |
transition: all 0.3s ease; | |
} | |
.toast.show { | |
opacity: 1; | |
transform: translateX(0); | |
} | |
.toast-success { background: var(--success); } | |
.toast-error { background: var(--error); } | |
@media (max-width: 768px) { | |
.main-grid { | |
grid-template-columns: 1fr; | |
} | |
.status-bar { | |
grid-template-columns: 1fr; | |
} | |
.container { | |
padding: 15px; | |
} | |
.header h1 { | |
font-size: 2rem; | |
} | |
.card { | |
padding: 20px; | |
} | |
} | |
.progress-bar { | |
width: 100%; | |
height: 6px; | |
background: var(--border); | |
border-radius: 3px; | |
overflow: hidden; | |
margin-top: 10px; | |
} | |
.progress-fill { | |
height: 100%; | |
background: linear-gradient(90deg, var(--primary), var(--success)); | |
transition: width 0.3s ease; | |
border-radius: 3px; | |
} | |
.settings-grid { | |
display: grid; | |
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |
gap: 20px; | |
} | |
.switch { | |
position: relative; | |
display: inline-block; | |
width: 50px; | |
height: 24px; | |
} | |
.switch input { | |
opacity: 0; | |
width: 0; | |
height: 0; | |
} | |
.slider { | |
position: absolute; | |
cursor: pointer; | |
top: 0; | |
left: 0; | |
right: 0; | |
bottom: 0; | |
background-color: var(--border); | |
transition: .4s; | |
border-radius: 24px; | |
} | |
.slider:before { | |
position: absolute; | |
content: ""; | |
height: 18px; | |
width: 18px; | |
left: 3px; | |
bottom: 3px; | |
background-color: white; | |
transition: .4s; | |
border-radius: 50%; | |
} | |
input:checked + .slider { | |
background-color: var(--primary); | |
} | |
input:checked + .slider:before { | |
transform: translateX(26px); | |
} | |
</style> | |
</head> | |
<body> | |
<div class="container"> | |
<div class="header"> | |
<h1>🎮 Minecraft Backup Manager Pro</h1> | |
<p>Advanced backup automation with Google Drive integration</p> | |
</div> | |
<div class="status-bar"> | |
<div class="status-card"> | |
<h3>System Status</h3> | |
<div class="status-value"> | |
<span class="status-indicator" id="systemStatus"></span> | |
<span id="statusText">Ready</span> | |
</div> | |
</div> | |
<div class="status-card"> | |
<h3>Total Backups</h3> | |
<div class="status-value" id="totalBackups">{{ total_backups }}</div> | |
</div> | |
<div class="status-card"> | |
<h3>Last Backup</h3> | |
<div class="status-value" style="font-size: 1.2rem;" id="lastBackup">{{ last_backup_time }}</div> | |
</div> | |
<div class="status-card"> | |
<h3>Google Drive</h3> | |
<div class="status-value"> | |
<span class="status-indicator" id="gdriveStatus"></span> | |
<span id="gdriveText">{{ 'Connected' if gdrive_connected else 'Offline' }}</span> | |
</div> | |
</div> | |
</div> | |
<div class="main-grid"> | |
<div class="card"> | |
<h2>🚀 Quick Actions</h2> | |
<button class="btn btn-success btn-full" onclick="runBackup('gdrive')" id="backupBtn"> | |
<span>📁 Backup from Google Drive</span> | |
<div class="loading" id="backupLoader" style="display: none;"></div> | |
</button> | |
<button class="btn btn-primary btn-full" onclick="runBackup('url')"> | |
<span>🔗 Backup from URL</span> | |
</button> | |
<button class="btn btn-warning btn-full" onclick="refreshGDrive()"> | |
<span>🔄 Refresh Google Drive</span> | |
</button> | |
</div> | |
<div class="card"> | |
<h2>⚙️ Automation Settings</h2> | |
<form id="settingsForm"> | |
<div class="form-group"> | |
<label for="interval">Backup Interval (minutes)</label> | |
<input type="number" id="interval" class="form-control" value="{{ schedule_interval }}" min="0" max="1440"> | |
</div> | |
<div class="settings-grid"> | |
<div class="form-group"> | |
<label>Auto Cleanup</label> | |
<label class="switch"> | |
<input type="checkbox" id="autoCleanup" {{ 'checked' if auto_cleanup else '' }}> | |
<span class="slider"></span> | |
</label> | |
</div> | |
<div class="form-group"> | |
<label for="maxBackups">Max Backups</label> | |
<input type="number" id="maxBackups" class="form-control" value="{{ max_backups }}" min="1" max="50"> | |
</div> | |
</div> | |
<button type="submit" class="btn btn-primary btn-full">Save Settings</button> | |
</form> | |
</div> | |
</div> | |
<div class="main-grid"> | |
<div class="card"> | |
<h2>📊 Backup History</h2> | |
<div class="history-grid" id="historyContainer"> | |
{% for backup in backup_history %} | |
<div class="history-item"> | |
<div class="history-info"> | |
<h4>Backup #{{ backup.id }}</h4> | |
<div class="history-meta"> | |
{{ backup.timestamp }} • {{ backup.duration }} | |
{% if backup.folders %} | |
• {{ backup.folders|length }} folders | |
{% endif %} | |
</div> | |
</div> | |
<span class="badge badge-{{ 'success' if backup.status == 'success' else 'error' }}"> | |
{{ backup.status.title() }} | |
</span> | |
</div> | |
{% endfor %} | |
</div> | |
</div> | |
<div class="card"> | |
<h2>☁️ Google Drive Files</h2> | |
<div class="gdrive-files" id="gdriveFiles"> | |
{% for file in gdrive_files %} | |
<div class="file-item"> | |
<div class="file-info"> | |
<h4>{{ file.name }}</h4> | |
<div class="file-meta"> | |
{{ file.get('size', 'Unknown size') }} • | |
{{ file.get('modifiedTime', 'Unknown date') }} | |
</div> | |
</div> | |
<button class="btn btn-primary" onclick="downloadGDriveFile('{{ file.id }}', '{{ file.name }}')"> | |
Download | |
</button> | |
</div> | |
{% endfor %} | |
</div> | |
</div> | |
</div> | |
<div class="card"> | |
<h2>📝 Activity Logs</h2> | |
<div class="logs" id="logsContainer"> | |
<div class="log-entry">System initialized and ready for backups...</div> | |
<div class="log-entry">Google Drive connection: {{ 'Active' if gdrive_connected else 'Inactive' }}</div> | |
<div class="log-entry">Hugging Face repository: {{ repo_id }}</div> | |
</div> | |
</div> | |
</div> | |
<div id="toast" class="toast"></div> | |
<script> | |
let backupRunning = false; | |
let refreshInterval; | |
// Initialize page | |
document.addEventListener('DOMContentLoaded', function() { | |
updateStatus(); | |
startStatusUpdates(); | |
// Settings form | |
document.getElementById('settingsForm').addEventListener('submit', function(e) { | |
e.preventDefault(); | |
saveSettings(); | |
}); | |
}); | |
function updateStatus() { | |
const systemStatus = document.getElementById('systemStatus'); | |
const statusText = document.getElementById('statusText'); | |
const gdriveStatus = document.getElementById('gdriveStatus'); | |
if (backupRunning) { | |
systemStatus.className = 'status-indicator status-running'; | |
statusText.textContent = 'Running'; | |
} else { | |
systemStatus.className = 'status-indicator status-online'; | |
statusText.textContent = 'Ready'; | |
} | |
gdriveStatus.className = `status-indicator ${{{ 'status-online' if gdrive_connected else 'status-offline' }}}`; | |
} | |
function startStatusUpdates() { | |
refreshInterval = setInterval(updateStatus, 2000); | |
} | |
async function runBackup(source = 'gdrive') { | |
if (backupRunning) { | |
showToast('Backup already running!', 'error'); | |
return; | |
} | |
backupRunning = true; | |
const backupBtn = document.getElementById('backupBtn'); | |
const loader = document.getElementById('backupLoader'); | |
backupBtn.disabled = true; | |
loader.style.display = 'inline-block'; | |
updateStatus(); | |
try { | |
const response = await fetch('/api/backup', { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json', | |
}, | |
body: JSON.stringify({ source: source }) | |
}); | |
const result = await response.json(); | |
if (result.status === 'success') { | |
showToast('Backup completed successfully!', 'success'); | |
updateLogs(result.log); | |
refreshHistory(); | |
} else { | |
showToast('Backup failed: ' + result.message, 'error'); | |
updateLogs(result.log || ['Backup failed']); | |
} | |
} catch (error) { | |
showToast('Network error: ' + error.message, 'error'); | |
addLog('Network error: ' + error.message, 'error'); | |
} finally { | |
backupRunning = false; | |
backupBtn.disabled = false; | |
loader.style.display = 'none'; | |
updateStatus(); | |
} | |
} | |
async function saveSettings() { | |
const settings = { | |
interval: parseInt(document.getElementById('interval').value), | |
auto_cleanup: document.getElementById('autoCleanup').checked, | |
max_backups: parseInt(document.getElementById('maxBackups').value) | |
}; | |
try { | |
const response = await fetch('/api/settings', { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json', | |
}, | |
body: JSON.stringify(settings) | |
}); | |
const result = await response.json(); | |
if (result.status === 'success') { | |
showToast('Settings saved successfully!', 'success'); | |
} else { | |
showToast('Failed to save settings', 'error'); | |
} | |
} catch (error) { | |
showToast('Network error: ' + error.message, 'error'); | |
} | |
} | |
async function refreshGDrive() { | |
try { | |
const response = await fetch('/api/gdrive/refresh', { | |
method: 'POST' | |
}); | |
const result = await response.json(); | |
if (result.status === 'success') { | |
showToast('Google Drive refreshed!', 'success'); | |
location.reload(); // Refresh page to show new files | |
} else { | |
showToast('Failed to refresh Google Drive', 'error'); | |
} | |
} catch (error) { | |
showToast('Network error: ' + error.message, 'error'); | |
} | |
} | |
async function downloadGDriveFile(fileId, fileName) { | |
try { | |
const response = await fetch('/api/gdrive/download', { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json', | |
}, | |
body: JSON.stringify({ file_id: fileId, filename: fileName }) | |
}); | |
const result = await response.json(); | |
if (result.status === 'success') { | |
showToast(`Downloaded ${fileName}`, 'success'); | |
} else { | |
showToast('Download failed', 'error'); | |
} | |
} catch (error) { | |
showToast('Network error: ' + error.message, 'error'); | |
} | |
} | |
function updateLogs(logs) { | |
const container = document.getElementById('logsContainer'); | |
logs.forEach(log => addLog(log)); | |
} | |
function addLog(message, type = 'info') { | |
const container = document.getElementById('logsContainer'); | |
const entry = document.createElement('div'); | |
entry.className = `log-entry log-${type}`; | |
entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`; | |
container.appendChild(entry); | |
container.scrollTop = container.scrollHeight; | |
} | |
async function refreshHistory() { | |
try { | |
const response = await fetch('/api/history'); | |
const result = await response.json(); | |
const container = document.getElementById('historyContainer'); | |
container.innerHTML = ''; | |
result.history.forEach(backup => { | |
const item = document.createElement('div'); | |
item.className = 'history-item'; | |
item.innerHTML = ` | |
<div class="history-info"> | |
<h4>Backup #${backup.id}</h4> | |
<div class="history-meta"> | |
${backup.timestamp} • ${backup.duration} | |
${backup.folders ? ` • ${backup.folders.length} folders` : ''} | |
</div> | |
</div> | |
<span class="badge badge-${backup.status === 'success' ? 'success' : 'error'}"> | |
${backup.status.charAt(0).toUpperCase() + backup.status.slice(1)} | |
</span> | |
`; | |
container.appendChild(item); | |
}); | |
document.getElementById('totalBackups').textContent = result.total_backups; | |
document.getElementById('lastBackup').textContent = result.last_backup_time; | |
} catch (error) { | |
console.error('Failed to refresh history:', error); | |
} | |
} | |
function showToast(message, type = 'success') { | |
const toast = document.getElementById('toast'); | |
toast.textContent = message; | |
toast.className = `toast toast-${type} show`; | |
setTimeout(() => { | |
toast.classList.remove('show'); | |
}, 4000); | |
} | |
// Auto-refresh every 30 seconds | |
setInterval(refreshHistory, 30000); | |
</script> | |
</body> | |
</html> | |
''' | |
# API Routes | |
def index(): | |
return render_template_string(HTML_TEMPLATE, | |
last_backup_time=app_state["last_backup_time"], | |
schedule_interval=app_state["schedule_interval"], | |
backup_history=app_state["backup_history"][:10], | |
gdrive_connected=app_state["gdrive_connected"], | |
gdrive_files=app_state["gdrive_files"][:10], | |
auto_cleanup=app_state["auto_cleanup"], | |
max_backups=app_state["max_backups"], | |
total_backups=app_state["total_backups"], | |
repo_id=REPO_ID or "Not configured" | |
) | |
def api_backup(): | |
if app_state["backup_running"]: | |
return jsonify({"status": "error", "message": "Backup already running"}) | |
data = request.get_json() or {} | |
source = data.get("source", "gdrive") | |
# Run backup in background thread | |
def backup_thread(): | |
result = run_backup(source) | |
return result | |
thread = threading.Thread(target=backup_thread) | |
thread.start() | |
thread.join(timeout=300) # 5 minute timeout | |
if thread.is_alive(): | |
return jsonify({"status": "error", "message": "Backup timeout"}) | |
return jsonify({"status": "success", "message": "Backup initiated"}) | |
def api_settings(): | |
try: | |
data = request.get_json() | |
app_state["schedule_interval"] = data.get("interval", 0) | |
app_state["auto_cleanup"] = data.get("auto_cleanup", True) | |
app_state["max_backups"] = data.get("max_backups", 10) | |
return jsonify({"status": "success"}) | |
except Exception as e: | |
return jsonify({"status": "error", "message": str(e)}) | |
def api_gdrive_refresh(): | |
try: | |
if gdrive.authenticate(): | |
app_state["gdrive_files"] = gdrive.list_files() | |
return jsonify({"status": "success", "files": len(app_state["gdrive_files"])}) | |
else: | |
return jsonify({"status": "error", "message": "Authentication failed"}) | |
except Exception as e: | |
return jsonify({"status": "error", "message": str(e)}) | |
def api_gdrive_download(): | |
try: | |
data = request.get_json() | |
file_id = data.get("file_id") | |
filename = data.get("filename") | |
if gdrive.download_file(file_id, filename): | |
return jsonify({"status": "success"}) | |
else: | |
return jsonify({"status": "error", "message": "Download failed"}) | |
except Exception as e: | |
return jsonify({"status": "error", "message": str(e)}) | |
def api_history(): | |
return jsonify({ | |
"status": "success", | |
"history": app_state["backup_history"], | |
"total_backups": app_state["total_backups"], | |
"last_backup_time": app_state["last_backup_time"] | |
}) | |
def api_status(): | |
try: | |
# Get HF repo info | |
hf_status = "Connected" | |
repo_files = 0 | |
try: | |
if TOKEN and REPO_ID: | |
api = HfApi() | |
files = list_repo_files(repo_id=REPO_ID, repo_type="dataset", token=TOKEN) | |
repo_files = len(list(files)) | |
except: | |
hf_status = "Error" | |
return jsonify({ | |
"status": "success", | |
"system_status": "running" if app_state["backup_running"] else "ready", | |
"gdrive_status": "connected" if app_state["gdrive_connected"] else "disconnected", | |
"hf_status": hf_status, | |
"repo_files": repo_files, | |
"last_backup": app_state["last_backup_time"], | |
"total_backups": app_state["total_backups"], | |
"schedule_interval": app_state["schedule_interval"], | |
"last_error": app_state["last_error"] | |
}) | |
except Exception as e: | |
return jsonify({"status": "error", "message": str(e)}) | |
if __name__ == "__main__": | |
print("🚀 Minecraft Backup Manager Pro starting...") | |
print(f"📁 Repository: {REPO_ID}") | |
print(f"☁️ Google Drive: {'Connected' if app_state['gdrive_connected'] else 'Not configured'}") | |
print(f"🔧 Server running on http://0.0.0.0:7860") | |
app.run(host="0.0.0.0", port=7860, debug=False) |