testdeep123 commited on
Commit
4280a1e
·
verified ·
1 Parent(s): 1be9faa

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1093 -187
app.py CHANGED
@@ -1,241 +1,1147 @@
 
1
  import os
2
- import shutil
3
- import zipfile
4
- import threading
5
- import time
6
- from flask import Flask, request, render_template_string, redirect, url_for
7
- import gdown
8
- from huggingface_hub import HfApi, login, upload_folder, list_repo_files
9
-
10
  # Ensure Hugging Face cache writes to tmp
11
  os.environ["HF_HOME"] = "/tmp/hf_home"
12
 
 
 
 
 
 
 
 
 
 
 
 
13
  # Environment variables
14
- FOLDER_URL = os.getenv("FOLDER_URL")
15
- REPO_ID = os.getenv("REPO_ID")
16
- TOKEN = os.getenv("HF_TOKEN")
17
- GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY") # for future Drive API use
18
 
19
  # Directories
20
- DOWNLOAD_DIR = "/tmp/backups"
21
- EXTRACT_DIR = "/tmp/extracted_backups"
 
22
 
23
- # Application state
24
- last_backup_time = "Never"
25
- schedule_interval = 0
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
  app = Flask(__name__)
28
 
29
- def run_backup():
30
- global last_backup_time
31
- log = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  try:
33
- log.append("Starting backup process")
 
 
34
  shutil.rmtree(DOWNLOAD_DIR, ignore_errors=True)
35
  shutil.rmtree(EXTRACT_DIR, ignore_errors=True)
36
  os.makedirs(DOWNLOAD_DIR, exist_ok=True)
37
  os.makedirs(EXTRACT_DIR, exist_ok=True)
 
38
 
39
- log.append(f"Downloading from {FOLDER_URL}")
40
- gdown.download_folder(url=FOLDER_URL, output=DOWNLOAD_DIR, use_cookies=False, quiet=True)
41
- log.append("Download complete")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
 
 
 
43
  for root, _, files in os.walk(DOWNLOAD_DIR):
44
- for name in files:
45
- if name.endswith(".zip"):
46
- path = os.path.join(root, name)
47
- with zipfile.ZipFile(path) as z:
48
- z.extractall(EXTRACT_DIR)
49
- log.append(f"Extracted {name}")
50
-
51
- bad = os.path.join(EXTRACT_DIR, "world_nither")
52
- good = os.path.join(EXTRACT_DIR, "world_nether")
53
- if os.path.exists(bad) and not os.path.exists(good):
54
- os.rename(bad, good)
55
- log.append("Fixed world_nether typo")
56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  login(token=TOKEN)
58
  api = HfApi()
59
- log.append("Authenticated with Hugging Face")
60
 
61
  api.create_repo(repo_id=REPO_ID, repo_type="dataset", private=False, exist_ok=True, token=TOKEN)
62
- log.append(f"Repository ready {REPO_ID}")
63
-
64
- sections = {
65
  "world": os.path.join(EXTRACT_DIR, "world"),
66
  "world_nether": os.path.join(EXTRACT_DIR, "world_nether"),
67
  "world_the_end": os.path.join(EXTRACT_DIR, "world_the_end"),
68
- "plugins": os.path.join(EXTRACT_DIR, "plugins")
69
- }
70
- for key, path in sections.items():
71
- if os.path.isdir(path):
72
- log.append(f"Uploading section {key}")
73
- upload_folder(
74
- repo_id=REPO_ID,
75
- folder_path=path,
76
- repo_type="dataset",
77
- token=TOKEN,
78
- path_in_repo=key,
79
- commit_message=f"Add {key}"
80
- )
81
- log.append(f"Uploaded {key}")
82
- else:
83
- log.append(f"Skipped missing section {key}")
84
-
85
- last_backup_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
86
- log.append(f"Backup finished at {last_backup_time}")
 
 
87
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  except Exception as e:
89
- log.append(f"Error {e}")
90
-
91
- return "\n".join(log)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
 
 
93
  def schedule_loop():
94
  while True:
95
- if schedule_interval > 0:
96
  run_backup()
97
- time.sleep(schedule_interval * 60)
98
- else:
99
- time.sleep(5)
100
 
 
101
  threading.Thread(target=schedule_loop, daemon=True).start()
102
 
103
- HTML = """
104
- <!doctype html>
105
- <html>
 
 
 
 
 
 
106
  <head>
107
- <meta charset="utf-8">
108
- <meta name="viewport" content="width=device-width, initial-scale=1">
109
- <title>Backup & Dataset Manager</title>
110
- <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
111
- <style>
112
- body { padding: 2rem; }
113
- .log-area { height: 300px; overflow-y: scroll; background: #f9f9f9; padding: 1rem; border-radius: .5rem; font-family: monospace; white-space: pre-wrap; }
114
- .nav-link { cursor: pointer; }
115
- </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  </head>
117
  <body>
118
- <h1 class="mb-4">Backup & Dataset Manager</h1>
119
- <ul class="nav nav-tabs mb-3" id="mainTabs">
120
- <li class="nav-item">
121
- <a class="nav-link active" data-bs-target="#backupTab">Backup Control</a>
122
- </li>
123
- <li class="nav-item">
124
- <a class="nav-link" data-bs-target="#datasetTab">Manage Dataset</a>
125
- </li>
126
- </ul>
127
-
128
- <div class="tab-content">
129
- <div id="backupTab" class="tab-pane fade show active">
130
- <form method="post" action="/">
131
- <div class="mb-3">
132
- <label class="form-label">Interval Minutes</label>
133
- <input type="number" class="form-control" name="interval" value="{{ interval }}" min="1">
134
  </div>
135
- <button class="btn btn-primary">Set Schedule</button>
136
- <button class="btn btn-secondary ms-2" name="manual_run" value="1">Run Now</button>
137
- </form>
138
- <div class="mt-4">
139
- <strong>Last Backup:</strong> {{ last_run }}<br>
140
- <div class="log-area mt-2">{{ status }}</div>
141
- </div>
142
- </div>
143
 
144
- <div id="datasetTab" class="tab-pane fade">
145
- <form method="post" action="/manage">
146
- <div class="mb-3">
147
- <label class="form-label">Drive File or Folder URL</label>
148
- <input type="url" class="form-control" name="drive_url" placeholder="Enter Google Drive link">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  </div>
150
- <div class="mb-3">
151
- <label class="form-label">Destination Path in Dataset</label>
152
- <input type="text" class="form-control" name="dest_path" placeholder="eg world_extra">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
  </div>
154
- <button class="btn btn-success">Upload to Dataset</button>
155
- </form>
156
- <hr>
157
- <h5>Current Dataset Contents</h5>
158
- <ul class="list-group">
159
- {% for file in files %}
160
- <li class="list-group-item">{{ file }}</li>
161
- {% endfor %}
162
- </ul>
163
- {% if manage_status %}
164
- <div class="alert alert-info mt-3">{{ manage_status }}</div>
165
- {% endif %}
166
  </div>
167
- </div>
168
-
169
- <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
170
- <script>
171
- document.querySelectorAll('.nav-link').forEach(function(el) {
172
- el.addEventListener('click', function() {
173
- document.querySelectorAll('.nav-link').forEach(function(x) { x.classList.remove('active') });
174
- document.querySelectorAll('.tab-pane').forEach(function(x) { x.classList.remove('show','active') });
175
- el.classList.add('active');
176
- document.querySelector(el.getAttribute('data-bs-target')).classList.add('show','active');
177
- });
178
- });
179
- </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  </body>
181
  </html>
182
- """
183
 
184
- @app.route("/", methods=["GET","POST"])
 
185
  def index():
186
- global schedule_interval
187
- status = ""
188
- if request.method == "POST":
189
- if "manual_run" in request.form:
190
- status = run_backup()
191
- else:
192
- try:
193
- schedule_interval = int(request.form.get("interval", "0"))
194
- status = f"Scheduled every {schedule_interval} minutes"
195
- except:
196
- status = "Invalid interval"
197
- return render_template_string(
198
- HTML,
199
- last_run=last_backup_time,
200
- interval=schedule_interval,
201
- status=status,
202
- files=[],
203
- manage_status=None
204
  )
205
 
206
- @app.route("/manage", methods=["POST"])
207
- def manage():
208
- drive_url = request.form.get("drive_url")
209
- dest_path = request.form.get("dest_path") or ""
210
- manage_log = ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
  try:
212
- # download then upload
213
- tmp = os.path.join(DOWNLOAD_DIR, "upload_tmp")
214
- shutil.rmtree(tmp, ignore_errors=True)
215
- os.makedirs(tmp, exist_ok=True)
216
- gdown.download(url=drive_url, output=tmp, quiet=True)
217
- login(token=TOKEN)
218
- upload_folder(
219
- repo_id=REPO_ID,
220
- folder_path=tmp,
221
- repo_type="dataset",
222
- token=TOKEN,
223
- path_in_repo=dest_path,
224
- commit_message=f"Upload via UI to {dest_path or 'root'}"
225
- )
226
- manage_log = "Upload successful"
227
  except Exception as e:
228
- manage_log = f"Error {e}"
229
- api = HfApi()
230
- files = api.list_repo_files(repo_id=REPO_ID, repo_type="dataset", token=TOKEN)
231
- return render_template_string(
232
- HTML,
233
- last_run=last_backup_time,
234
- interval=schedule_interval,
235
- status="",
236
- files=files,
237
- manage_status=manage_log
238
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
 
240
  if __name__ == "__main__":
241
- app.run(host="0.0.0.0", port=7860)
 
 
 
 
 
1
+ # app.py
2
  import os
 
 
 
 
 
 
 
 
3
  # Ensure Hugging Face cache writes to tmp
4
  os.environ["HF_HOME"] = "/tmp/hf_home"
5
 
6
+ import shutil, zipfile, threading, time, json, uuid
7
+ from datetime import datetime, timedelta
8
+ from flask import Flask, request, render_template_string, jsonify
9
+ import gdown
10
+ from huggingface_hub import HfApi, login, upload_folder, list_repo_files
11
+ from googleapiclient.discovery import build
12
+ from google.oauth2.credentials import Credentials
13
+ from google_auth_oauthlib.flow import InstalledAppFlow
14
+ from google.auth.transport.requests import Request
15
+ import pickle
16
+
17
  # Environment variables
18
+ FOLDER_URL = os.getenv("FOLDER_URL")
19
+ REPO_ID = os.getenv("REPO_ID")
20
+ TOKEN = os.getenv("HF_TOKEN")
21
+ GOOGLE_CREDENTIALS = os.getenv("GOOGLE_CREDENTIALS") # JSON string of credentials
22
 
23
  # Directories
24
+ DOWNLOAD_DIR = "/tmp/backups"
25
+ EXTRACT_DIR = "/tmp/extracted_backups"
26
+ GDRIVE_DIR = "/tmp/gdrive_files"
27
 
28
+ # Global state
29
+ app_state = {
30
+ "last_backup_time": "Never",
31
+ "schedule_interval": 0,
32
+ "status": "Ready",
33
+ "backup_history": [],
34
+ "gdrive_connected": False,
35
+ "auto_cleanup": True,
36
+ "max_backups": 10,
37
+ "notification_enabled": True,
38
+ "backup_running": False,
39
+ "total_backups": 0,
40
+ "last_error": None,
41
+ "gdrive_files": []
42
+ }
43
 
44
  app = Flask(__name__)
45
 
46
+ # Google Drive Integration
47
+ class GDriveManager:
48
+ def __init__(self):
49
+ self.service = None
50
+ self.creds = None
51
+
52
+ def authenticate(self):
53
+ try:
54
+ if GOOGLE_CREDENTIALS:
55
+ creds_data = json.loads(GOOGLE_CREDENTIALS)
56
+ self.creds = Credentials.from_authorized_user_info(creds_data)
57
+ self.service = build('drive', 'v3', credentials=self.creds)
58
+ app_state["gdrive_connected"] = True
59
+ return True
60
+ except Exception as e:
61
+ app_state["last_error"] = f"GDrive auth failed: {str(e)}"
62
+ return False
63
+
64
+ def list_files(self, folder_id=None):
65
+ if not self.service:
66
+ return []
67
+ try:
68
+ query = f"'{folder_id}' in parents" if folder_id else "mimeType='application/zip' or mimeType='application/x-zip-compressed'"
69
+ results = self.service.files().list(
70
+ q=query,
71
+ pageSize=50,
72
+ fields="files(id, name, size, modifiedTime, mimeType)"
73
+ ).execute()
74
+ return results.get('files', [])
75
+ except Exception as e:
76
+ app_state["last_error"] = f"GDrive list error: {str(e)}"
77
+ return []
78
+
79
+ def download_file(self, file_id, filename):
80
+ if not self.service:
81
+ return False
82
+ try:
83
+ os.makedirs(GDRIVE_DIR, exist_ok=True)
84
+ request = self.service.files().get_media(fileId=file_id)
85
+ with open(os.path.join(GDRIVE_DIR, filename), 'wb') as f:
86
+ downloader = MediaIoBaseDownload(f, request)
87
+ done = False
88
+ while done is False:
89
+ status, done = downloader.next_chunk()
90
+ return True
91
+ except Exception as e:
92
+ app_state["last_error"] = f"GDrive download error: {str(e)}"
93
+ return False
94
+
95
+ gdrive = GDriveManager()
96
+
97
+ # Enhanced backup logic
98
+ def run_backup(source="gdrive"):
99
+ global app_state
100
+ if app_state["backup_running"]:
101
+ return {"status": "error", "message": "Backup already running"}
102
+
103
+ app_state["backup_running"] = True
104
+ log_entries = []
105
+ backup_id = str(uuid.uuid4())[:8]
106
+ start_time = datetime.now()
107
+
108
  try:
109
+ log_entries.append(f"[{start_time.strftime('%H:%M:%S')}] Starting backup #{backup_id}")
110
+
111
+ # Clean directories
112
  shutil.rmtree(DOWNLOAD_DIR, ignore_errors=True)
113
  shutil.rmtree(EXTRACT_DIR, ignore_errors=True)
114
  os.makedirs(DOWNLOAD_DIR, exist_ok=True)
115
  os.makedirs(EXTRACT_DIR, exist_ok=True)
116
+ log_entries.append("Directories prepared")
117
 
118
+ # Download based on source
119
+ if source == "gdrive" and app_state["gdrive_connected"]:
120
+ log_entries.append("Downloading from Google Drive...")
121
+ gdrive_files = gdrive.list_files()
122
+ for file in gdrive_files[:5]: # Limit to 5 recent files
123
+ if gdrive.download_file(file['id'], file['name']):
124
+ log_entries.append(f"Downloaded: {file['name']}")
125
+
126
+ # Move gdrive files to download dir
127
+ if os.path.exists(GDRIVE_DIR):
128
+ for f in os.listdir(GDRIVE_DIR):
129
+ shutil.move(os.path.join(GDRIVE_DIR, f), os.path.join(DOWNLOAD_DIR, f))
130
+ else:
131
+ log_entries.append(f"Downloading from URL: {FOLDER_URL}")
132
+ gdown.download_folder(url=FOLDER_URL, output=DOWNLOAD_DIR, use_cookies=False, quiet=True)
133
+
134
+ log_entries.append("Download completed")
135
 
136
+ # Extract archives
137
+ extracted_count = 0
138
  for root, _, files in os.walk(DOWNLOAD_DIR):
139
+ for f in files:
140
+ if f.endswith(('.zip', '.rar', '.7z')):
141
+ zp = os.path.join(root, f)
142
+ try:
143
+ with zipfile.ZipFile(zp) as z:
144
+ z.extractall(EXTRACT_DIR)
145
+ extracted_count += 1
146
+ log_entries.append(f"Extracted: {f}")
147
+ except Exception as e:
148
+ log_entries.append(f"Failed to extract {f}: {str(e)}")
 
 
149
 
150
+ # Fix common folder naming issues
151
+ fixes = [
152
+ ("world_nither", "world_nether"),
153
+ ("world_end", "world_the_end"),
154
+ ("plugin", "plugins")
155
+ ]
156
+ for bad, good in fixes:
157
+ bad_path = os.path.join(EXTRACT_DIR, bad)
158
+ good_path = os.path.join(EXTRACT_DIR, good)
159
+ if os.path.exists(bad_path) and not os.path.exists(good_path):
160
+ os.rename(bad_path, good_path)
161
+ log_entries.append(f"Fixed folder: {bad} → {good}")
162
+
163
+ # Upload to Hugging Face
164
  login(token=TOKEN)
165
  api = HfApi()
166
+ log_entries.append("Connected to Hugging Face")
167
 
168
  api.create_repo(repo_id=REPO_ID, repo_type="dataset", private=False, exist_ok=True, token=TOKEN)
169
+
170
+ subfolders = {
 
171
  "world": os.path.join(EXTRACT_DIR, "world"),
172
  "world_nether": os.path.join(EXTRACT_DIR, "world_nether"),
173
  "world_the_end": os.path.join(EXTRACT_DIR, "world_the_end"),
174
+ "plugins": os.path.join(EXTRACT_DIR, "plugins"),
175
+ "logs": os.path.join(EXTRACT_DIR, "logs"),
176
+ "config": os.path.join(EXTRACT_DIR, "config")
177
+ }
178
+
179
+ uploaded_folders = []
180
+ for name, path in subfolders.items():
181
+ if os.path.exists(path):
182
+ try:
183
+ upload_folder(
184
+ repo_id=REPO_ID,
185
+ folder_path=path,
186
+ repo_type="dataset",
187
+ token=TOKEN,
188
+ path_in_repo=name,
189
+ commit_message=f"Backup #{backup_id} - {name} - {datetime.now().strftime('%Y-%m-%d %H:%M')}"
190
+ )
191
+ uploaded_folders.append(name)
192
+ log_entries.append(f" Uploaded: {name}")
193
+ except Exception as e:
194
+ log_entries.append(f"✗ Failed to upload {name}: {str(e)}")
195
 
196
+ # Update state
197
+ end_time = datetime.now()
198
+ duration = (end_time - start_time).total_seconds()
199
+
200
+ backup_record = {
201
+ "id": backup_id,
202
+ "timestamp": end_time.isoformat(),
203
+ "duration": f"{duration:.1f}s",
204
+ "source": source,
205
+ "folders": uploaded_folders,
206
+ "files_extracted": extracted_count,
207
+ "status": "success"
208
+ }
209
+
210
+ app_state["backup_history"].insert(0, backup_record)
211
+ app_state["last_backup_time"] = end_time.strftime("%Y-%m-%d %H:%M:%S")
212
+ app_state["total_backups"] += 1
213
+ app_state["last_error"] = None
214
+
215
+ # Auto-cleanup old backups
216
+ if app_state["auto_cleanup"] and len(app_state["backup_history"]) > app_state["max_backups"]:
217
+ app_state["backup_history"] = app_state["backup_history"][:app_state["max_backups"]]
218
+
219
+ log_entries.append(f"✓ Backup completed in {duration:.1f}s")
220
+
221
  except Exception as e:
222
+ error_msg = str(e)
223
+ log_entries.append(f"✗ Error: {error_msg}")
224
+ app_state["last_error"] = error_msg
225
+ backup_record = {
226
+ "id": backup_id,
227
+ "timestamp": datetime.now().isoformat(),
228
+ "duration": f"{(datetime.now() - start_time).total_seconds():.1f}s",
229
+ "source": source,
230
+ "status": "failed",
231
+ "error": error_msg
232
+ }
233
+ app_state["backup_history"].insert(0, backup_record)
234
+
235
+ finally:
236
+ app_state["backup_running"] = False
237
+
238
+ return {
239
+ "status": "success" if not app_state["last_error"] else "error",
240
+ "log": log_entries,
241
+ "backup_id": backup_id
242
+ }
243
 
244
+ # Scheduler
245
  def schedule_loop():
246
  while True:
247
+ if app_state["schedule_interval"] > 0 and not app_state["backup_running"]:
248
  run_backup()
249
+ time.sleep(60 if app_state["schedule_interval"] > 0 else 30)
 
 
250
 
251
+ # Start scheduler thread
252
  threading.Thread(target=schedule_loop, daemon=True).start()
253
 
254
+ # Initialize Google Drive
255
+ gdrive.authenticate()
256
+ if app_state["gdrive_connected"]:
257
+ app_state["gdrive_files"] = gdrive.list_files()
258
+
259
+ # HTML Template
260
+ HTML_TEMPLATE = '''
261
+ <!DOCTYPE html>
262
+ <html lang="en">
263
  <head>
264
+ <meta charset="UTF-8">
265
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
266
+ <title>Minecraft Backup Manager Pro</title>
267
+ <style>
268
+ * {
269
+ margin: 0;
270
+ padding: 0;
271
+ box-sizing: border-box;
272
+ }
273
+
274
+ :root {
275
+ --primary: #2563eb;
276
+ --primary-dark: #1d4ed8;
277
+ --secondary: #64748b;
278
+ --success: #10b981;
279
+ --warning: #f59e0b;
280
+ --error: #ef4444;
281
+ --bg-primary: #0f172a;
282
+ --bg-secondary: #1e293b;
283
+ --bg-card: #334155;
284
+ --text-primary: #f8fafc;
285
+ --text-secondary: #cbd5e1;
286
+ --border: #475569;
287
+ --shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
288
+ }
289
+
290
+ body {
291
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
292
+ background: linear-gradient(135deg, var(--bg-primary) 0%, #1e293b 100%);
293
+ color: var(--text-primary);
294
+ min-height: 100vh;
295
+ line-height: 1.6;
296
+ }
297
+
298
+ .container {
299
+ max-width: 1400px;
300
+ margin: 0 auto;
301
+ padding: 20px;
302
+ }
303
+
304
+ .header {
305
+ text-align: center;
306
+ margin-bottom: 40px;
307
+ padding: 30px 0;
308
+ }
309
+
310
+ .header h1 {
311
+ font-size: 2.5rem;
312
+ font-weight: 700;
313
+ background: linear-gradient(135deg, var(--primary) 0%, #06b6d4 100%);
314
+ -webkit-background-clip: text;
315
+ -webkit-text-fill-color: transparent;
316
+ margin-bottom: 10px;
317
+ }
318
+
319
+ .header p {
320
+ color: var(--text-secondary);
321
+ font-size: 1.1rem;
322
+ }
323
+
324
+ .status-bar {
325
+ display: grid;
326
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
327
+ gap: 20px;
328
+ margin-bottom: 30px;
329
+ }
330
+
331
+ .status-card {
332
+ background: var(--bg-card);
333
+ border-radius: 12px;
334
+ padding: 20px;
335
+ box-shadow: var(--shadow);
336
+ border: 1px solid var(--border);
337
+ }
338
+
339
+ .status-card h3 {
340
+ font-size: 0.9rem;
341
+ text-transform: uppercase;
342
+ letter-spacing: 1px;
343
+ color: var(--text-secondary);
344
+ margin-bottom: 8px;
345
+ }
346
+
347
+ .status-value {
348
+ font-size: 1.8rem;
349
+ font-weight: 600;
350
+ color: var(--text-primary);
351
+ }
352
+
353
+ .status-indicator {
354
+ display: inline-block;
355
+ width: 12px;
356
+ height: 12px;
357
+ border-radius: 50%;
358
+ margin-right: 8px;
359
+ }
360
+
361
+ .status-online { background: var(--success); }
362
+ .status-offline { background: var(--error); }
363
+ .status-running { background: var(--warning); animation: pulse 2s infinite; }
364
+
365
+ @keyframes pulse {
366
+ 0%, 100% { opacity: 1; }
367
+ 50% { opacity: 0.5; }
368
+ }
369
+
370
+ .main-grid {
371
+ display: grid;
372
+ grid-template-columns: 1fr 1fr;
373
+ gap: 30px;
374
+ margin-bottom: 30px;
375
+ }
376
+
377
+ .card {
378
+ background: var(--bg-secondary);
379
+ border-radius: 16px;
380
+ padding: 30px;
381
+ box-shadow: var(--shadow);
382
+ border: 1px solid var(--border);
383
+ }
384
+
385
+ .card h2 {
386
+ font-size: 1.5rem;
387
+ margin-bottom: 20px;
388
+ color: var(--text-primary);
389
+ display: flex;
390
+ align-items: center;
391
+ gap: 10px;
392
+ }
393
+
394
+ .form-group {
395
+ margin-bottom: 20px;
396
+ }
397
+
398
+ .form-group label {
399
+ display: block;
400
+ margin-bottom: 8px;
401
+ font-weight: 500;
402
+ color: var(--text-secondary);
403
+ }
404
+
405
+ .form-control {
406
+ width: 100%;
407
+ padding: 12px 16px;
408
+ border: 2px solid var(--border);
409
+ border-radius: 8px;
410
+ background: var(--bg-card);
411
+ color: var(--text-primary);
412
+ font-size: 1rem;
413
+ transition: all 0.3s ease;
414
+ }
415
+
416
+ .form-control:focus {
417
+ outline: none;
418
+ border-color: var(--primary);
419
+ box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
420
+ }
421
+
422
+ .btn {
423
+ display: inline-flex;
424
+ align-items: center;
425
+ justify-content: center;
426
+ gap: 8px;
427
+ padding: 12px 24px;
428
+ border: none;
429
+ border-radius: 8px;
430
+ font-size: 1rem;
431
+ font-weight: 500;
432
+ cursor: pointer;
433
+ transition: all 0.3s ease;
434
+ text-decoration: none;
435
+ min-height: 48px;
436
+ }
437
+
438
+ .btn-primary {
439
+ background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
440
+ color: white;
441
+ }
442
+
443
+ .btn-primary:hover {
444
+ transform: translateY(-2px);
445
+ box-shadow: 0 8px 25px rgba(37, 99, 235, 0.3);
446
+ }
447
+
448
+ .btn-success {
449
+ background: linear-gradient(135deg, var(--success) 0%, #059669 100%);
450
+ color: white;
451
+ }
452
+
453
+ .btn-success:hover {
454
+ transform: translateY(-2px);
455
+ box-shadow: 0 8px 25px rgba(16, 185, 129, 0.3);
456
+ }
457
+
458
+ .btn-warning {
459
+ background: linear-gradient(135deg, var(--warning) 0%, #d97706 100%);
460
+ color: white;
461
+ }
462
+
463
+ .btn-full {
464
+ width: 100%;
465
+ margin-bottom: 15px;
466
+ }
467
+
468
+ .btn:disabled {
469
+ opacity: 0.6;
470
+ cursor: not-allowed;
471
+ transform: none !important;
472
+ }
473
+
474
+ .logs {
475
+ background: var(--bg-primary);
476
+ border: 1px solid var(--border);
477
+ border-radius: 8px;
478
+ padding: 20px;
479
+ max-height: 400px;
480
+ overflow-y: auto;
481
+ font-family: 'Courier New', monospace;
482
+ font-size: 0.9rem;
483
+ line-height: 1.4;
484
+ }
485
+
486
+ .log-entry {
487
+ margin-bottom: 5px;
488
+ padding: 4px 0;
489
+ }
490
+
491
+ .log-success { color: var(--success); }
492
+ .log-error { color: var(--error); }
493
+ .log-warning { color: var(--warning); }
494
+
495
+ .history-grid {
496
+ display: grid;
497
+ gap: 15px;
498
+ max-height: 500px;
499
+ overflow-y: auto;
500
+ }
501
+
502
+ .history-item {
503
+ background: var(--bg-card);
504
+ border: 1px solid var(--border);
505
+ border-radius: 8px;
506
+ padding: 15px;
507
+ display: flex;
508
+ justify-content: space-between;
509
+ align-items: center;
510
+ }
511
+
512
+ .history-info h4 {
513
+ margin-bottom: 5px;
514
+ color: var(--text-primary);
515
+ }
516
+
517
+ .history-meta {
518
+ font-size: 0.9rem;
519
+ color: var(--text-secondary);
520
+ }
521
+
522
+ .badge {
523
+ padding: 4px 12px;
524
+ border-radius: 20px;
525
+ font-size: 0.8rem;
526
+ font-weight: 500;
527
+ }
528
+
529
+ .badge-success {
530
+ background: rgba(16, 185, 129, 0.2);
531
+ color: var(--success);
532
+ }
533
+
534
+ .badge-error {
535
+ background: rgba(239, 68, 68, 0.2);
536
+ color: var(--error);
537
+ }
538
+
539
+ .gdrive-files {
540
+ max-height: 300px;
541
+ overflow-y: auto;
542
+ }
543
+
544
+ .file-item {
545
+ display: flex;
546
+ justify-content: space-between;
547
+ align-items: center;
548
+ padding: 12px;
549
+ border: 1px solid var(--border);
550
+ border-radius: 8px;
551
+ margin-bottom: 10px;
552
+ background: var(--bg-card);
553
+ }
554
+
555
+ .file-info h4 {
556
+ margin-bottom: 4px;
557
+ color: var(--text-primary);
558
+ }
559
+
560
+ .file-meta {
561
+ font-size: 0.8rem;
562
+ color: var(--text-secondary);
563
+ }
564
+
565
+ .loading {
566
+ display: inline-block;
567
+ width: 20px;
568
+ height: 20px;
569
+ border: 3px solid rgba(255, 255, 255, 0.3);
570
+ border-radius: 50%;
571
+ border-top-color: var(--primary);
572
+ animation: spin 1s ease-in-out infinite;
573
+ }
574
+
575
+ @keyframes spin {
576
+ to { transform: rotate(360deg); }
577
+ }
578
+
579
+ .toast {
580
+ position: fixed;
581
+ top: 20px;
582
+ right: 20px;
583
+ padding: 15px 20px;
584
+ border-radius: 8px;
585
+ color: white;
586
+ font-weight: 500;
587
+ z-index: 1000;
588
+ opacity: 0;
589
+ transform: translateX(100%);
590
+ transition: all 0.3s ease;
591
+ }
592
+
593
+ .toast.show {
594
+ opacity: 1;
595
+ transform: translateX(0);
596
+ }
597
+
598
+ .toast-success { background: var(--success); }
599
+ .toast-error { background: var(--error); }
600
+
601
+ @media (max-width: 768px) {
602
+ .main-grid {
603
+ grid-template-columns: 1fr;
604
+ }
605
+
606
+ .status-bar {
607
+ grid-template-columns: 1fr;
608
+ }
609
+
610
+ .container {
611
+ padding: 15px;
612
+ }
613
+
614
+ .header h1 {
615
+ font-size: 2rem;
616
+ }
617
+
618
+ .card {
619
+ padding: 20px;
620
+ }
621
+ }
622
+
623
+ .progress-bar {
624
+ width: 100%;
625
+ height: 6px;
626
+ background: var(--border);
627
+ border-radius: 3px;
628
+ overflow: hidden;
629
+ margin-top: 10px;
630
+ }
631
+
632
+ .progress-fill {
633
+ height: 100%;
634
+ background: linear-gradient(90deg, var(--primary), var(--success));
635
+ transition: width 0.3s ease;
636
+ border-radius: 3px;
637
+ }
638
+
639
+ .settings-grid {
640
+ display: grid;
641
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
642
+ gap: 20px;
643
+ }
644
+
645
+ .switch {
646
+ position: relative;
647
+ display: inline-block;
648
+ width: 50px;
649
+ height: 24px;
650
+ }
651
+
652
+ .switch input {
653
+ opacity: 0;
654
+ width: 0;
655
+ height: 0;
656
+ }
657
+
658
+ .slider {
659
+ position: absolute;
660
+ cursor: pointer;
661
+ top: 0;
662
+ left: 0;
663
+ right: 0;
664
+ bottom: 0;
665
+ background-color: var(--border);
666
+ transition: .4s;
667
+ border-radius: 24px;
668
+ }
669
+
670
+ .slider:before {
671
+ position: absolute;
672
+ content: "";
673
+ height: 18px;
674
+ width: 18px;
675
+ left: 3px;
676
+ bottom: 3px;
677
+ background-color: white;
678
+ transition: .4s;
679
+ border-radius: 50%;
680
+ }
681
+
682
+ input:checked + .slider {
683
+ background-color: var(--primary);
684
+ }
685
+
686
+ input:checked + .slider:before {
687
+ transform: translateX(26px);
688
+ }
689
+ </style>
690
  </head>
691
  <body>
692
+ <div class="container">
693
+ <div class="header">
694
+ <h1>🎮 Minecraft Backup Manager Pro</h1>
695
+ <p>Advanced backup automation with Google Drive integration</p>
 
 
 
 
 
 
 
 
 
 
 
 
696
  </div>
 
 
 
 
 
 
 
 
697
 
698
+ <div class="status-bar">
699
+ <div class="status-card">
700
+ <h3>System Status</h3>
701
+ <div class="status-value">
702
+ <span class="status-indicator" id="systemStatus"></span>
703
+ <span id="statusText">Ready</span>
704
+ </div>
705
+ </div>
706
+ <div class="status-card">
707
+ <h3>Total Backups</h3>
708
+ <div class="status-value" id="totalBackups">{{ total_backups }}</div>
709
+ </div>
710
+ <div class="status-card">
711
+ <h3>Last Backup</h3>
712
+ <div class="status-value" style="font-size: 1.2rem;" id="lastBackup">{{ last_backup_time }}</div>
713
+ </div>
714
+ <div class="status-card">
715
+ <h3>Google Drive</h3>
716
+ <div class="status-value">
717
+ <span class="status-indicator" id="gdriveStatus"></span>
718
+ <span id="gdriveText">{{ 'Connected' if gdrive_connected else 'Offline' }}</span>
719
+ </div>
720
+ </div>
721
  </div>
722
+
723
+ <div class="main-grid">
724
+ <div class="card">
725
+ <h2>🚀 Quick Actions</h2>
726
+ <button class="btn btn-success btn-full" onclick="runBackup('gdrive')" id="backupBtn">
727
+ <span>📁 Backup from Google Drive</span>
728
+ <div class="loading" id="backupLoader" style="display: none;"></div>
729
+ </button>
730
+ <button class="btn btn-primary btn-full" onclick="runBackup('url')">
731
+ <span>🔗 Backup from URL</span>
732
+ </button>
733
+ <button class="btn btn-warning btn-full" onclick="refreshGDrive()">
734
+ <span>🔄 Refresh Google Drive</span>
735
+ </button>
736
+ </div>
737
+
738
+ <div class="card">
739
+ <h2>⚙️ Automation Settings</h2>
740
+ <form id="settingsForm">
741
+ <div class="form-group">
742
+ <label for="interval">Backup Interval (minutes)</label>
743
+ <input type="number" id="interval" class="form-control" value="{{ schedule_interval }}" min="0" max="1440">
744
+ </div>
745
+ <div class="settings-grid">
746
+ <div class="form-group">
747
+ <label>Auto Cleanup</label>
748
+ <label class="switch">
749
+ <input type="checkbox" id="autoCleanup" {{ 'checked' if auto_cleanup else '' }}>
750
+ <span class="slider"></span>
751
+ </label>
752
+ </div>
753
+ <div class="form-group">
754
+ <label for="maxBackups">Max Backups</label>
755
+ <input type="number" id="maxBackups" class="form-control" value="{{ max_backups }}" min="1" max="50">
756
+ </div>
757
+ </div>
758
+ <button type="submit" class="btn btn-primary btn-full">Save Settings</button>
759
+ </form>
760
+ </div>
761
+ </div>
762
+
763
+ <div class="main-grid">
764
+ <div class="card">
765
+ <h2>📊 Backup History</h2>
766
+ <div class="history-grid" id="historyContainer">
767
+ {% for backup in backup_history %}
768
+ <div class="history-item">
769
+ <div class="history-info">
770
+ <h4>Backup #{{ backup.id }}</h4>
771
+ <div class="history-meta">
772
+ {{ backup.timestamp }} • {{ backup.duration }}
773
+ {% if backup.folders %}
774
+ • {{ backup.folders|length }} folders
775
+ {% endif %}
776
+ </div>
777
+ </div>
778
+ <span class="badge badge-{{ 'success' if backup.status == 'success' else 'error' }}">
779
+ {{ backup.status.title() }}
780
+ </span>
781
+ </div>
782
+ {% endfor %}
783
+ </div>
784
+ </div>
785
+
786
+ <div class="card">
787
+ <h2>☁️ Google Drive Files</h2>
788
+ <div class="gdrive-files" id="gdriveFiles">
789
+ {% for file in gdrive_files %}
790
+ <div class="file-item">
791
+ <div class="file-info">
792
+ <h4>{{ file.name }}</h4>
793
+ <div class="file-meta">
794
+ {{ file.get('size', 'Unknown size') }} •
795
+ {{ file.get('modifiedTime', 'Unknown date') }}
796
+ </div>
797
+ </div>
798
+ <button class="btn btn-primary" onclick="downloadGDriveFile('{{ file.id }}', '{{ file.name }}')">
799
+ Download
800
+ </button>
801
+ </div>
802
+ {% endfor %}
803
+ </div>
804
+ </div>
805
+ </div>
806
+
807
+ <div class="card">
808
+ <h2>📝 Activity Logs</h2>
809
+ <div class="logs" id="logsContainer">
810
+ <div class="log-entry">System initialized and ready for backups...</div>
811
+ <div class="log-entry">Google Drive connection: {{ 'Active' if gdrive_connected else 'Inactive' }}</div>
812
+ <div class="log-entry">Hugging Face repository: {{ repo_id }}</div>
813
+ </div>
814
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
815
  </div>
816
+
817
+ <div id="toast" class="toast"></div>
818
+
819
+ <script>
820
+ let backupRunning = false;
821
+ let refreshInterval;
822
+
823
+ // Initialize page
824
+ document.addEventListener('DOMContentLoaded', function() {
825
+ updateStatus();
826
+ startStatusUpdates();
827
+
828
+ // Settings form
829
+ document.getElementById('settingsForm').addEventListener('submit', function(e) {
830
+ e.preventDefault();
831
+ saveSettings();
832
+ });
833
+ });
834
+
835
+ function updateStatus() {
836
+ const systemStatus = document.getElementById('systemStatus');
837
+ const statusText = document.getElementById('statusText');
838
+ const gdriveStatus = document.getElementById('gdriveStatus');
839
+
840
+ if (backupRunning) {
841
+ systemStatus.className = 'status-indicator status-running';
842
+ statusText.textContent = 'Running';
843
+ } else {
844
+ systemStatus.className = 'status-indicator status-online';
845
+ statusText.textContent = 'Ready';
846
+ }
847
+
848
+ gdriveStatus.className = `status-indicator ${{{ 'status-online' if gdrive_connected else 'status-offline' }}}`;
849
+ }
850
+
851
+ function startStatusUpdates() {
852
+ refreshInterval = setInterval(updateStatus, 2000);
853
+ }
854
+
855
+ async function runBackup(source = 'gdrive') {
856
+ if (backupRunning) {
857
+ showToast('Backup already running!', 'error');
858
+ return;
859
+ }
860
+
861
+ backupRunning = true;
862
+ const backupBtn = document.getElementById('backupBtn');
863
+ const loader = document.getElementById('backupLoader');
864
+
865
+ backupBtn.disabled = true;
866
+ loader.style.display = 'inline-block';
867
+
868
+ updateStatus();
869
+
870
+ try {
871
+ const response = await fetch('/api/backup', {
872
+ method: 'POST',
873
+ headers: {
874
+ 'Content-Type': 'application/json',
875
+ },
876
+ body: JSON.stringify({ source: source })
877
+ });
878
+
879
+ const result = await response.json();
880
+
881
+ if (result.status === 'success') {
882
+ showToast('Backup completed successfully!', 'success');
883
+ updateLogs(result.log);
884
+ refreshHistory();
885
+ } else {
886
+ showToast('Backup failed: ' + result.message, 'error');
887
+ updateLogs(result.log || ['Backup failed']);
888
+ }
889
+ } catch (error) {
890
+ showToast('Network error: ' + error.message, 'error');
891
+ addLog('Network error: ' + error.message, 'error');
892
+ } finally {
893
+ backupRunning = false;
894
+ backupBtn.disabled = false;
895
+ loader.style.display = 'none';
896
+ updateStatus();
897
+ }
898
+ }
899
+
900
+ async function saveSettings() {
901
+ const settings = {
902
+ interval: parseInt(document.getElementById('interval').value),
903
+ auto_cleanup: document.getElementById('autoCleanup').checked,
904
+ max_backups: parseInt(document.getElementById('maxBackups').value)
905
+ };
906
+
907
+ try {
908
+ const response = await fetch('/api/settings', {
909
+ method: 'POST',
910
+ headers: {
911
+ 'Content-Type': 'application/json',
912
+ },
913
+ body: JSON.stringify(settings)
914
+ });
915
+
916
+ const result = await response.json();
917
+
918
+ if (result.status === 'success') {
919
+ showToast('Settings saved successfully!', 'success');
920
+ } else {
921
+ showToast('Failed to save settings', 'error');
922
+ }
923
+ } catch (error) {
924
+ showToast('Network error: ' + error.message, 'error');
925
+ }
926
+ }
927
+
928
+ async function refreshGDrive() {
929
+ try {
930
+ const response = await fetch('/api/gdrive/refresh', {
931
+ method: 'POST'
932
+ });
933
+ const result = await response.json();
934
+
935
+ if (result.status === 'success') {
936
+ showToast('Google Drive refreshed!', 'success');
937
+ location.reload(); // Refresh page to show new files
938
+ } else {
939
+ showToast('Failed to refresh Google Drive', 'error');
940
+ }
941
+ } catch (error) {
942
+ showToast('Network error: ' + error.message, 'error');
943
+ }
944
+ }
945
+
946
+ async function downloadGDriveFile(fileId, fileName) {
947
+ try {
948
+ const response = await fetch('/api/gdrive/download', {
949
+ method: 'POST',
950
+ headers: {
951
+ 'Content-Type': 'application/json',
952
+ },
953
+ body: JSON.stringify({ file_id: fileId, filename: fileName })
954
+ });
955
+
956
+ const result = await response.json();
957
+
958
+ if (result.status === 'success') {
959
+ showToast(`Downloaded ${fileName}`, 'success');
960
+ } else {
961
+ showToast('Download failed', 'error');
962
+ }
963
+ } catch (error) {
964
+ showToast('Network error: ' + error.message, 'error');
965
+ }
966
+ }
967
+
968
+ function updateLogs(logs) {
969
+ const container = document.getElementById('logsContainer');
970
+ logs.forEach(log => addLog(log));
971
+ }
972
+
973
+ function addLog(message, type = 'info') {
974
+ const container = document.getElementById('logsContainer');
975
+ const entry = document.createElement('div');
976
+ entry.className = `log-entry log-${type}`;
977
+ entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
978
+ container.appendChild(entry);
979
+ container.scrollTop = container.scrollHeight;
980
+ }
981
+
982
+ async function refreshHistory() {
983
+ try {
984
+ const response = await fetch('/api/history');
985
+ const result = await response.json();
986
+
987
+ const container = document.getElementById('historyContainer');
988
+ container.innerHTML = '';
989
+
990
+ result.history.forEach(backup => {
991
+ const item = document.createElement('div');
992
+ item.className = 'history-item';
993
+ item.innerHTML = `
994
+ <div class="history-info">
995
+ <h4>Backup #${backup.id}</h4>
996
+ <div class="history-meta">
997
+ ${backup.timestamp} • ${backup.duration}
998
+ ${backup.folders ? ` • ${backup.folders.length} folders` : ''}
999
+ </div>
1000
+ </div>
1001
+ <span class="badge badge-${backup.status === 'success' ? 'success' : 'error'}">
1002
+ ${backup.status.charAt(0).toUpperCase() + backup.status.slice(1)}
1003
+ </span>
1004
+ `;
1005
+ container.appendChild(item);
1006
+ });
1007
+
1008
+ document.getElementById('totalBackups').textContent = result.total_backups;
1009
+ document.getElementById('lastBackup').textContent = result.last_backup_time;
1010
+ } catch (error) {
1011
+ console.error('Failed to refresh history:', error);
1012
+ }
1013
+ }
1014
+
1015
+ function showToast(message, type = 'success') {
1016
+ const toast = document.getElementById('toast');
1017
+ toast.textContent = message;
1018
+ toast.className = `toast toast-${type} show`;
1019
+
1020
+ setTimeout(() => {
1021
+ toast.classList.remove('show');
1022
+ }, 4000);
1023
+ }
1024
+
1025
+ // Auto-refresh every 30 seconds
1026
+ setInterval(refreshHistory, 30000);
1027
+ </script>
1028
  </body>
1029
  </html>
1030
+ '''
1031
 
1032
+ # API Routes
1033
+ @app.route("/")
1034
  def index():
1035
+ return render_template_string(HTML_TEMPLATE,
1036
+ last_backup_time=app_state["last_backup_time"],
1037
+ schedule_interval=app_state["schedule_interval"],
1038
+ backup_history=app_state["backup_history"][:10],
1039
+ gdrive_connected=app_state["gdrive_connected"],
1040
+ gdrive_files=app_state["gdrive_files"][:10],
1041
+ auto_cleanup=app_state["auto_cleanup"],
1042
+ max_backups=app_state["max_backups"],
1043
+ total_backups=app_state["total_backups"],
1044
+ repo_id=REPO_ID or "Not configured"
 
 
 
 
 
 
 
 
1045
  )
1046
 
1047
+ @app.route("/api/backup", methods=["POST"])
1048
+ def api_backup():
1049
+ if app_state["backup_running"]:
1050
+ return jsonify({"status": "error", "message": "Backup already running"})
1051
+
1052
+ data = request.get_json() or {}
1053
+ source = data.get("source", "gdrive")
1054
+
1055
+ # Run backup in background thread
1056
+ def backup_thread():
1057
+ result = run_backup(source)
1058
+ return result
1059
+
1060
+ thread = threading.Thread(target=backup_thread)
1061
+ thread.start()
1062
+ thread.join(timeout=300) # 5 minute timeout
1063
+
1064
+ if thread.is_alive():
1065
+ return jsonify({"status": "error", "message": "Backup timeout"})
1066
+
1067
+ return jsonify({"status": "success", "message": "Backup initiated"})
1068
+
1069
+ @app.route("/api/settings", methods=["POST"])
1070
+ def api_settings():
1071
  try:
1072
+ data = request.get_json()
1073
+ app_state["schedule_interval"] = data.get("interval", 0)
1074
+ app_state["auto_cleanup"] = data.get("auto_cleanup", True)
1075
+ app_state["max_backups"] = data.get("max_backups", 10)
1076
+ return jsonify({"status": "success"})
 
 
 
 
 
 
 
 
 
 
1077
  except Exception as e:
1078
+ return jsonify({"status": "error", "message": str(e)})
1079
+
1080
+ @app.route("/api/gdrive/refresh", methods=["POST"])
1081
+ def api_gdrive_refresh():
1082
+ try:
1083
+ if gdrive.authenticate():
1084
+ app_state["gdrive_files"] = gdrive.list_files()
1085
+ return jsonify({"status": "success", "files": len(app_state["gdrive_files"])})
1086
+ else:
1087
+ return jsonify({"status": "error", "message": "Authentication failed"})
1088
+ except Exception as e:
1089
+ return jsonify({"status": "error", "message": str(e)})
1090
+
1091
+ @app.route("/api/gdrive/download", methods=["POST"])
1092
+ def api_gdrive_download():
1093
+ try:
1094
+ data = request.get_json()
1095
+ file_id = data.get("file_id")
1096
+ filename = data.get("filename")
1097
+
1098
+ if gdrive.download_file(file_id, filename):
1099
+ return jsonify({"status": "success"})
1100
+ else:
1101
+ return jsonify({"status": "error", "message": "Download failed"})
1102
+ except Exception as e:
1103
+ return jsonify({"status": "error", "message": str(e)})
1104
+
1105
+ @app.route("/api/history")
1106
+ def api_history():
1107
+ return jsonify({
1108
+ "status": "success",
1109
+ "history": app_state["backup_history"],
1110
+ "total_backups": app_state["total_backups"],
1111
+ "last_backup_time": app_state["last_backup_time"]
1112
+ })
1113
+
1114
+ @app.route("/api/status")
1115
+ def api_status():
1116
+ try:
1117
+ # Get HF repo info
1118
+ hf_status = "Connected"
1119
+ repo_files = 0
1120
+ try:
1121
+ if TOKEN and REPO_ID:
1122
+ api = HfApi()
1123
+ files = list_repo_files(repo_id=REPO_ID, repo_type="dataset", token=TOKEN)
1124
+ repo_files = len(list(files))
1125
+ except:
1126
+ hf_status = "Error"
1127
+
1128
+ return jsonify({
1129
+ "status": "success",
1130
+ "system_status": "running" if app_state["backup_running"] else "ready",
1131
+ "gdrive_status": "connected" if app_state["gdrive_connected"] else "disconnected",
1132
+ "hf_status": hf_status,
1133
+ "repo_files": repo_files,
1134
+ "last_backup": app_state["last_backup_time"],
1135
+ "total_backups": app_state["total_backups"],
1136
+ "schedule_interval": app_state["schedule_interval"],
1137
+ "last_error": app_state["last_error"]
1138
+ })
1139
+ except Exception as e:
1140
+ return jsonify({"status": "error", "message": str(e)})
1141
 
1142
  if __name__ == "__main__":
1143
+ print("🚀 Minecraft Backup Manager Pro starting...")
1144
+ print(f"📁 Repository: {REPO_ID}")
1145
+ print(f"☁️ Google Drive: {'Connected' if app_state['gdrive_connected'] else 'Not configured'}")
1146
+ print(f"🔧 Server running on http://0.0.0.0:7860")
1147
+ app.run(host="0.0.0.0", port=7860, debug=False)