cronjob / app.py
testdeep123's picture
Update app.py
4280a1e verified
raw
history blame
38.8 kB
# 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
@app.route("/")
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"
)
@app.route("/api/backup", methods=["POST"])
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"})
@app.route("/api/settings", methods=["POST"])
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)})
@app.route("/api/gdrive/refresh", methods=["POST"])
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)})
@app.route("/api/gdrive/download", methods=["POST"])
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)})
@app.route("/api/history")
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"]
})
@app.route("/api/status")
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)