testdeep123 commited on
Commit
b3566cd
·
verified ·
1 Parent(s): d6fe2d7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +32 -134
app.py CHANGED
@@ -22,14 +22,13 @@ REPO_ID = os.getenv("REPO_ID")
22
  TOKEN = os.getenv("HF_TOKEN")
23
 
24
  # --- Global State Management ---
25
- # Using a dictionary to hold state is thread-safe in CPython for simple reads/writes
26
  app_state = {
27
  "backup_status": "idle", # idle, running, success, error
28
  "backup_log": ["Awaiting first run."],
29
  "last_backup_time": "Never",
30
  "next_backup_time": "Scheduler disabled",
31
  "schedule_interval_minutes": 0, # 0 means disabled
32
- "scheduler_thread": None
33
  }
34
 
35
  # --- Flask App Setup ---
@@ -52,7 +51,6 @@ HTML_TEMPLATE = """
52
  body {
53
  background-color: #212529; /* Dark background */
54
  }
55
-
56
  .log-box {
57
  height: 300px;
58
  overflow-y: auto;
@@ -62,12 +60,10 @@ HTML_TEMPLATE = """
62
  background-color: #111315 !important;
63
  border-top: 1px solid #495057;
64
  }
65
-
66
  .log-box div {
67
  padding: 2px 5px;
68
  border-bottom: 1px solid #343a40;
69
  }
70
-
71
  .status-badge {
72
  padding: 0.35em 0.65em;
73
  font-size: .75em;
@@ -80,35 +76,28 @@ HTML_TEMPLATE = """
80
  border-radius: 0.25rem;
81
  transition: background-color 0.3s ease-in-out;
82
  }
83
-
84
  .status-idle { background-color: #6c757d; } /* Gray */
85
  .status-running { background-color: #0d6efd; } /* Blue */
86
  .status-success { background-color: #198754; } /* Green */
87
  .status-error { background-color: #dc3545; } /* Red */
88
-
89
  #files-list-container {
90
  max-height: 450px;
91
  overflow-y: auto;
92
  }
93
-
94
  .btn i, .btn .spinner-border {
95
  pointer-events: none; /* Make clicks on icons pass through to the button */
96
  }
97
-
98
  .card {
99
  border: 1px solid rgba(255, 255, 255, 0.1);
100
  }
101
  </style>
102
-
103
  </head>
104
  <body>
105
-
106
  <div class="container my-4">
107
  <header class="d-flex align-items-center pb-3 mb-4 border-bottom border-secondary">
108
  <i class="fas fa-server fa-2x me-3 text-info"></i>
109
  <span class="fs-4">Minecraft Backup & Dataset Controller</span>
110
  </header>
111
-
112
  <div class="row g-4">
113
  <!-- Left Panel: Backup Controls -->
114
  <div class="col-lg-6">
@@ -123,7 +112,6 @@ HTML_TEMPLATE = """
123
  <i class="fas fa-play-circle me-2"></i>Run Backup Now
124
  </button>
125
  </div>
126
-
127
  <form id="schedule-form" class="row g-2 align-items-center">
128
  <div class="col">
129
  <label for="interval-input" class="form-label">Schedule Interval (minutes)</label>
@@ -135,7 +123,6 @@ HTML_TEMPLATE = """
135
  </button>
136
  </div>
137
  </form>
138
-
139
  <ul class="list-group list-group-flush mt-4">
140
  <li class="list-group-item d-flex justify-content-between bg-transparent">
141
  <span>Last Backup:</span>
@@ -150,12 +137,9 @@ HTML_TEMPLATE = """
150
  <div class="card-footer">
151
  <strong><i class="fas fa-clipboard-list me-2"></i>Live Log</strong>
152
  </div>
153
- <div id="log-output" class="log-box card-body">
154
- <!-- Logs will be populated here by JavaScript -->
155
- </div>
156
  </div>
157
  </div>
158
-
159
  <!-- Right Panel: Dataset Management -->
160
  <div class="col-lg-6">
161
  <div class="card h-100 shadow-sm">
@@ -186,9 +170,7 @@ HTML_TEMPLATE = """
186
  <th>Actions</th>
187
  </tr>
188
  </thead>
189
- <tbody id="files-list">
190
- <!-- File list will be populated here -->
191
- </tbody>
192
  </table>
193
  </div>
194
  </div>
@@ -196,13 +178,9 @@ HTML_TEMPLATE = """
196
  </div>
197
  </div>
198
  </div>
199
-
200
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
201
-
202
  <script>
203
  document.addEventListener('DOMContentLoaded', () => {
204
-
205
- // --- DOM Elements ---
206
  const runNowBtn = document.getElementById('run-now-btn');
207
  const scheduleForm = document.getElementById('schedule-form');
208
  const intervalInput = document.getElementById('interval-input');
@@ -214,15 +192,12 @@ document.addEventListener('DOMContentLoaded', () => {
214
  const filesListBody = document.getElementById('files-list');
215
  const filesLoader = document.getElementById('files-loader');
216
 
217
- let statusInterval;
218
-
219
- // --- API Helper ---
220
  async function apiCall(endpoint, options = {}) {
221
  try {
222
  const response = await fetch(endpoint, options);
223
  if (!response.ok) {
224
- const errorData = await response.json();
225
- throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
226
  }
227
  return response.json();
228
  } catch (error) {
@@ -231,29 +206,19 @@ document.addEventListener('DOMContentLoaded', () => {
231
  return null;
232
  }
233
  }
234
-
235
- // --- UI Update Functions ---
236
  function updateLog(logs) {
237
- logOutput.innerHTML = logs.map(line => `<div>${line}</div>`).join('');
238
  logOutput.scrollTop = logOutput.scrollHeight;
239
  }
240
-
241
  function updateStatusUI(data) {
242
- // Status Badge
243
  statusIndicator.textContent = data.backup_status.charAt(0).toUpperCase() + data.backup_status.slice(1);
244
- statusIndicator.className = 'status-badge'; // Reset classes
245
  statusIndicator.classList.add(`status-${data.backup_status}`);
246
-
247
- // Timestamps
248
  lastRunTimeEl.textContent = data.last_backup_time;
249
  nextRunTimeEl.textContent = data.next_backup_time;
250
-
251
- // Interval Input
252
  if (document.activeElement !== intervalInput) {
253
  intervalInput.value = data.schedule_interval_minutes > 0 ? data.schedule_interval_minutes : '';
254
  }
255
-
256
- // Button state
257
  runNowBtn.disabled = data.backup_status === 'running';
258
  if (data.backup_status === 'running') {
259
  runNowBtn.innerHTML = `<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Running...`;
@@ -261,8 +226,6 @@ document.addEventListener('DOMContentLoaded', () => {
261
  runNowBtn.innerHTML = `<i class="fas fa-play-circle me-2"></i>Run Backup Now`;
262
  }
263
  }
264
-
265
- // --- Core Functions ---
266
  async function fetchStatus() {
267
  const data = await apiCall('/api/status');
268
  if (data) {
@@ -270,16 +233,11 @@ document.addEventListener('DOMContentLoaded', () => {
270
  updateStatusUI(data);
271
  }
272
  }
273
-
274
  async function runBackup() {
275
  if (runNowBtn.disabled) return;
276
-
277
  const data = await apiCall('/api/run-backup', { method: 'POST' });
278
- if (data) {
279
- fetchStatus(); // Immediately update status
280
- }
281
  }
282
-
283
  async function setSchedule(event) {
284
  event.preventDefault();
285
  const interval = intervalInput.value;
@@ -290,14 +248,11 @@ document.addEventListener('DOMContentLoaded', () => {
290
  });
291
  fetchStatus();
292
  }
293
-
294
  async function listFiles() {
295
  filesLoader.style.display = 'block';
296
  filesListBody.innerHTML = '';
297
  refreshFilesBtn.disabled = true;
298
-
299
  const data = await apiCall('/api/list-files');
300
-
301
  filesLoader.style.display = 'none';
302
  refreshFilesBtn.disabled = false;
303
  if (data && data.files) {
@@ -309,9 +264,7 @@ document.addEventListener('DOMContentLoaded', () => {
309
  const row = document.createElement('tr');
310
  row.innerHTML = `
311
  <td class="text-break">
312
- <a href="${file.url}" target="_blank" title="${file.name}">
313
- ${file.name}
314
- </a>
315
  </td>
316
  <td>${file.size}</td>
317
  <td>
@@ -324,85 +277,59 @@ document.addEventListener('DOMContentLoaded', () => {
324
  });
325
  }
326
  }
327
-
328
  async function deleteFile(event) {
329
  const button = event.target.closest('.delete-btn');
330
  if (!button) return;
331
-
332
  const filename = button.dataset.filename;
333
- if (!confirm(`Are you sure you want to permanently delete "${filename}"?`)) {
334
- return;
335
- }
336
-
337
  button.disabled = true;
338
  button.innerHTML = `<span class="spinner-border spinner-border-sm"></span>`;
339
-
340
  const data = await apiCall('/api/delete-file', {
341
  method: 'POST',
342
  headers: { 'Content-Type': 'application/json' },
343
  body: JSON.stringify({ filename }),
344
  });
345
-
346
- if (data) {
347
- listFiles();
348
- } else {
349
  button.disabled = false;
350
  button.innerHTML = `<i class="fas fa-trash-alt"></i>`;
351
  }
352
  }
353
-
354
- // --- Event Listeners & Initializers ---
355
  runNowBtn.addEventListener('click', runBackup);
356
  scheduleForm.addEventListener('submit', setSchedule);
357
  refreshFilesBtn.addEventListener('click', listFiles);
358
  filesListBody.addEventListener('click', deleteFile);
359
-
360
- // Initialize tooltips
361
  const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
362
  tooltipTriggerList.map(function (tooltipTriggerEl) {
363
  return new bootstrap.Tooltip(tooltipTriggerEl);
364
  });
365
-
366
- // Initial data load and periodic polling
367
  fetchStatus();
368
  listFiles();
369
- statusInterval = setInterval(fetchStatus, 3000); // Poll for status every 3 seconds
370
-
371
  });
372
  </script>
373
-
374
  </body>
375
  </html>
376
  """
377
 
378
-
379
  # --- Core Backup Logic ---
380
 
381
  def run_backup_job():
382
- """The main backup logic, designed to be run in a background thread."""
383
  global app_state
384
  app_state["backup_status"] = "running"
385
  app_state["backup_log"] = ["Starting backup process..."]
386
-
387
  def log(message):
388
  print(message)
389
  app_state["backup_log"].append(message)
390
-
391
  try:
392
- # 1. Clean up old directories
393
  log("Resetting temporary directories...")
394
  shutil.rmtree(DOWNLOAD_DIR, ignore_errors=True)
395
  shutil.rmtree(EXTRACT_DIR, ignore_errors=True)
396
  os.makedirs(DOWNLOAD_DIR, exist_ok=True)
397
  os.makedirs(EXTRACT_DIR, exist_ok=True)
398
- log("Directories reset.")
399
-
400
- # 2. Download from Google Drive
401
- log(f"Downloading from Google Drive folder...")
402
  gdown.download_folder(url=FOLDER_URL, output=DOWNLOAD_DIR, use_cookies=False, quiet=True)
403
  log("Download finished.")
404
-
405
- # 3. Extract downloaded zip files
406
  log("Extracting zip archives...")
407
  extracted_count = 0
408
  for root, _, files in os.walk(DOWNLOAD_DIR):
@@ -415,50 +342,35 @@ def run_backup_job():
415
  extracted_count += 1
416
  if extracted_count == 0:
417
  log("Warning: No .zip files found to extract.")
418
-
419
- # 4. Fix potential folder name typo
420
  bad_path = os.path.join(EXTRACT_DIR, "world_nither")
421
  good_path = os.path.join(EXTRACT_DIR, "world_nether")
422
  if os.path.exists(bad_path) and not os.path.exists(good_path):
423
  os.rename(bad_path, good_path)
424
  log("Fixed folder name typo: 'world_nither' -> 'world_nether'")
425
-
426
- # 5. Log in to Hugging Face
427
  log("Logging into Hugging Face Hub...")
428
  login(token=TOKEN)
429
- log("Login successful.")
430
-
431
- # 6. Ensure repository exists
432
  log(f"Ensuring dataset repository '{REPO_ID}' exists...")
433
  api.create_repo(repo_id=REPO_ID, repo_type="dataset", private=False, exist_ok=True)
434
  log("Repository is ready.")
435
-
436
- # 7. Upload specified subfolders
437
  subfolders_to_upload = {
438
  "world": os.path.join(EXTRACT_DIR, "world"),
439
  "world_nether": os.path.join(EXTRACT_DIR, "world_nether"),
440
  "world_the_end": os.path.join(EXTRACT_DIR, "world_the_end"),
441
  "plugins": os.path.join(EXTRACT_DIR, "plugins")
442
  }
443
-
444
  for name, path in subfolders_to_upload.items():
445
  if os.path.exists(path):
446
  log(f"Uploading '{name}'...")
447
  upload_folder(
448
- repo_id=REPO_ID,
449
- folder_path=path,
450
- repo_type="dataset",
451
- path_in_repo=name,
452
- commit_message=f"Backup update for {name}"
453
  )
454
  log(f"'{name}' uploaded successfully.")
455
  else:
456
  log(f"Skipping '{name}' - directory not found.")
457
-
458
  app_state["last_backup_time"] = time.strftime("%Y-%m-%d %H:%M:%S %Z")
459
  log(f"Backup completed successfully at {app_state['last_backup_time']}.")
460
  app_state["backup_status"] = "success"
461
-
462
  except Exception as e:
463
  log(f"AN ERROR OCCURRED: {str(e)}")
464
  app_state["backup_status"] = "error"
@@ -466,15 +378,12 @@ def run_backup_job():
466
  # --- Scheduler Thread ---
467
 
468
  def scheduler_loop():
469
- """Periodically triggers the backup job based on the set interval."""
470
  global app_state
471
  while True:
472
  interval = app_state["schedule_interval_minutes"]
473
  if interval > 0:
474
  if app_state["backup_status"] != "running":
475
- print(f"Scheduler triggering backup. Interval: {interval} mins.")
476
  run_backup_job()
477
-
478
  next_run_timestamp = time.time() + interval * 60
479
  app_state["next_backup_time"] = time.strftime("%Y-%m-%d %H:%M:%S %Z", time.localtime(next_run_timestamp))
480
  time.sleep(interval * 60)
@@ -486,45 +395,47 @@ def scheduler_loop():
486
 
487
  @app.route("/")
488
  def index():
489
- """Serves the main HTML page by rendering the template string."""
490
  return render_template_string(HTML_TEMPLATE, repo_id=REPO_ID)
491
 
 
 
 
492
  @app.route("/api/status", methods=["GET"])
493
  def get_status():
494
- """Provides the current status of the application to the frontend."""
495
- return jsonify(dict(app_state))
 
 
 
 
 
 
 
496
 
497
  @app.route("/api/run-backup", methods=["POST"])
498
  def start_backup():
499
- """Triggers a manual backup run in a background thread."""
500
  if app_state["backup_status"] == "running":
501
  return jsonify({"status": "error", "message": "A backup is already in progress."}), 409
502
-
503
  threading.Thread(target=run_backup_job, daemon=True).start()
504
  return jsonify({"status": "ok", "message": "Backup process started."})
505
 
506
  @app.route("/api/set-schedule", methods=["POST"])
507
  def set_schedule():
508
- """Sets the backup interval."""
509
  try:
510
  interval = int(request.json.get("interval", 0))
511
- if interval < 0:
512
- raise ValueError("Interval must be non-negative.")
513
  app_state["schedule_interval_minutes"] = interval
514
-
515
  if interval > 0:
516
  next_run_timestamp = time.time() + interval * 60
517
  app_state["next_backup_time"] = time.strftime("%Y-%m-%d %H:%M:%S %Z", time.localtime(next_run_timestamp))
518
  else:
519
  app_state["next_backup_time"] = "Scheduler disabled"
520
-
521
  return jsonify({"status": "ok", "message": f"Schedule set to {interval} minutes."})
522
  except (ValueError, TypeError):
523
  return jsonify({"status": "error", "message": "Invalid interval value."}), 400
524
 
525
  @app.route("/api/list-files", methods=["GET"])
526
  def list_repo_files():
527
- """Lists all files in the dataset repository."""
528
  try:
529
  repo_files = api.list_repo_files(repo_id=REPO_ID, repo_type="dataset")
530
  files_details = []
@@ -532,12 +443,9 @@ def list_repo_files():
532
  try:
533
  info = api.get_repo_file_info(repo_id=REPO_ID, path_in_repo=filename, repo_type="dataset")
534
  size = humanize.naturalsize(info.size) if info.size else "0 B"
535
- except HfHubHTTPError:
536
- size = "N/A"
537
-
538
  files_details.append({
539
- "name": filename,
540
- "size": size,
541
  "url": hf_hub_url(repo_id=REPO_ID, filename=filename, repo_type="dataset")
542
  })
543
  return jsonify({"status": "ok", "files": files_details})
@@ -546,30 +454,20 @@ def list_repo_files():
546
 
547
  @app.route("/api/delete-file", methods=["POST"])
548
  def delete_repo_file():
549
- """Deletes a specific file from the dataset repository."""
550
  filename = request.json.get("filename")
551
  if not filename:
552
  return jsonify({"status": "error", "message": "Filename not provided."}), 400
553
-
554
  try:
555
  api.delete_file(
556
- repo_id=REPO_ID,
557
- path_in_repo=filename,
558
- repo_type="dataset",
559
  commit_message=f"Deleted file: {filename}"
560
  )
561
  return jsonify({"status": "ok", "message": f"Successfully deleted '{filename}'."})
562
- except HfHubHTTPError as e:
563
- return jsonify({"status": "error", "message": f"File not found or permission error: {e}"}), 404
564
  except Exception as e:
565
  return jsonify({"status": "error", "message": str(e)}), 500
566
 
567
-
568
  # --- Main Execution ---
569
  if __name__ == "__main__":
570
- # Start the scheduler in a background thread
571
  app_state["scheduler_thread"] = threading.Thread(target=scheduler_loop, daemon=True)
572
  app_state["scheduler_thread"].start()
573
-
574
- # Start the Flask web server
575
  app.run(host="0.0.0.0", port=7860)
 
22
  TOKEN = os.getenv("HF_TOKEN")
23
 
24
  # --- Global State Management ---
 
25
  app_state = {
26
  "backup_status": "idle", # idle, running, success, error
27
  "backup_log": ["Awaiting first run."],
28
  "last_backup_time": "Never",
29
  "next_backup_time": "Scheduler disabled",
30
  "schedule_interval_minutes": 0, # 0 means disabled
31
+ "scheduler_thread": None # This will hold the non-serializable Thread object
32
  }
33
 
34
  # --- Flask App Setup ---
 
51
  body {
52
  background-color: #212529; /* Dark background */
53
  }
 
54
  .log-box {
55
  height: 300px;
56
  overflow-y: auto;
 
60
  background-color: #111315 !important;
61
  border-top: 1px solid #495057;
62
  }
 
63
  .log-box div {
64
  padding: 2px 5px;
65
  border-bottom: 1px solid #343a40;
66
  }
 
67
  .status-badge {
68
  padding: 0.35em 0.65em;
69
  font-size: .75em;
 
76
  border-radius: 0.25rem;
77
  transition: background-color 0.3s ease-in-out;
78
  }
 
79
  .status-idle { background-color: #6c757d; } /* Gray */
80
  .status-running { background-color: #0d6efd; } /* Blue */
81
  .status-success { background-color: #198754; } /* Green */
82
  .status-error { background-color: #dc3545; } /* Red */
 
83
  #files-list-container {
84
  max-height: 450px;
85
  overflow-y: auto;
86
  }
 
87
  .btn i, .btn .spinner-border {
88
  pointer-events: none; /* Make clicks on icons pass through to the button */
89
  }
 
90
  .card {
91
  border: 1px solid rgba(255, 255, 255, 0.1);
92
  }
93
  </style>
 
94
  </head>
95
  <body>
 
96
  <div class="container my-4">
97
  <header class="d-flex align-items-center pb-3 mb-4 border-bottom border-secondary">
98
  <i class="fas fa-server fa-2x me-3 text-info"></i>
99
  <span class="fs-4">Minecraft Backup & Dataset Controller</span>
100
  </header>
 
101
  <div class="row g-4">
102
  <!-- Left Panel: Backup Controls -->
103
  <div class="col-lg-6">
 
112
  <i class="fas fa-play-circle me-2"></i>Run Backup Now
113
  </button>
114
  </div>
 
115
  <form id="schedule-form" class="row g-2 align-items-center">
116
  <div class="col">
117
  <label for="interval-input" class="form-label">Schedule Interval (minutes)</label>
 
123
  </button>
124
  </div>
125
  </form>
 
126
  <ul class="list-group list-group-flush mt-4">
127
  <li class="list-group-item d-flex justify-content-between bg-transparent">
128
  <span>Last Backup:</span>
 
137
  <div class="card-footer">
138
  <strong><i class="fas fa-clipboard-list me-2"></i>Live Log</strong>
139
  </div>
140
+ <div id="log-output" class="log-box card-body"></div>
 
 
141
  </div>
142
  </div>
 
143
  <!-- Right Panel: Dataset Management -->
144
  <div class="col-lg-6">
145
  <div class="card h-100 shadow-sm">
 
170
  <th>Actions</th>
171
  </tr>
172
  </thead>
173
+ <tbody id="files-list"></tbody>
 
 
174
  </table>
175
  </div>
176
  </div>
 
178
  </div>
179
  </div>
180
  </div>
 
181
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
 
182
  <script>
183
  document.addEventListener('DOMContentLoaded', () => {
 
 
184
  const runNowBtn = document.getElementById('run-now-btn');
185
  const scheduleForm = document.getElementById('schedule-form');
186
  const intervalInput = document.getElementById('interval-input');
 
192
  const filesListBody = document.getElementById('files-list');
193
  const filesLoader = document.getElementById('files-loader');
194
 
 
 
 
195
  async function apiCall(endpoint, options = {}) {
196
  try {
197
  const response = await fetch(endpoint, options);
198
  if (!response.ok) {
199
+ const errorData = await response.json().catch(() => ({ message: `HTTP error! Status: ${response.status}` }));
200
+ throw new Error(errorData.message);
201
  }
202
  return response.json();
203
  } catch (error) {
 
206
  return null;
207
  }
208
  }
 
 
209
  function updateLog(logs) {
210
+ logOutput.innerHTML = logs.map(line => `<div>${line.replace(/</g, "<").replace(/>/g, ">")}</div>`).join('');
211
  logOutput.scrollTop = logOutput.scrollHeight;
212
  }
 
213
  function updateStatusUI(data) {
 
214
  statusIndicator.textContent = data.backup_status.charAt(0).toUpperCase() + data.backup_status.slice(1);
215
+ statusIndicator.className = 'status-badge';
216
  statusIndicator.classList.add(`status-${data.backup_status}`);
 
 
217
  lastRunTimeEl.textContent = data.last_backup_time;
218
  nextRunTimeEl.textContent = data.next_backup_time;
 
 
219
  if (document.activeElement !== intervalInput) {
220
  intervalInput.value = data.schedule_interval_minutes > 0 ? data.schedule_interval_minutes : '';
221
  }
 
 
222
  runNowBtn.disabled = data.backup_status === 'running';
223
  if (data.backup_status === 'running') {
224
  runNowBtn.innerHTML = `<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Running...`;
 
226
  runNowBtn.innerHTML = `<i class="fas fa-play-circle me-2"></i>Run Backup Now`;
227
  }
228
  }
 
 
229
  async function fetchStatus() {
230
  const data = await apiCall('/api/status');
231
  if (data) {
 
233
  updateStatusUI(data);
234
  }
235
  }
 
236
  async function runBackup() {
237
  if (runNowBtn.disabled) return;
 
238
  const data = await apiCall('/api/run-backup', { method: 'POST' });
239
+ if (data) fetchStatus();
 
 
240
  }
 
241
  async function setSchedule(event) {
242
  event.preventDefault();
243
  const interval = intervalInput.value;
 
248
  });
249
  fetchStatus();
250
  }
 
251
  async function listFiles() {
252
  filesLoader.style.display = 'block';
253
  filesListBody.innerHTML = '';
254
  refreshFilesBtn.disabled = true;
 
255
  const data = await apiCall('/api/list-files');
 
256
  filesLoader.style.display = 'none';
257
  refreshFilesBtn.disabled = false;
258
  if (data && data.files) {
 
264
  const row = document.createElement('tr');
265
  row.innerHTML = `
266
  <td class="text-break">
267
+ <a href="${file.url}" target="_blank" title="${file.name}">${file.name}</a>
 
 
268
  </td>
269
  <td>${file.size}</td>
270
  <td>
 
277
  });
278
  }
279
  }
 
280
  async function deleteFile(event) {
281
  const button = event.target.closest('.delete-btn');
282
  if (!button) return;
 
283
  const filename = button.dataset.filename;
284
+ if (!confirm(`Are you sure you want to permanently delete "${filename}"?`)) return;
 
 
 
285
  button.disabled = true;
286
  button.innerHTML = `<span class="spinner-border spinner-border-sm"></span>`;
 
287
  const data = await apiCall('/api/delete-file', {
288
  method: 'POST',
289
  headers: { 'Content-Type': 'application/json' },
290
  body: JSON.stringify({ filename }),
291
  });
292
+ if (data) listFiles();
293
+ else {
 
 
294
  button.disabled = false;
295
  button.innerHTML = `<i class="fas fa-trash-alt"></i>`;
296
  }
297
  }
 
 
298
  runNowBtn.addEventListener('click', runBackup);
299
  scheduleForm.addEventListener('submit', setSchedule);
300
  refreshFilesBtn.addEventListener('click', listFiles);
301
  filesListBody.addEventListener('click', deleteFile);
 
 
302
  const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
303
  tooltipTriggerList.map(function (tooltipTriggerEl) {
304
  return new bootstrap.Tooltip(tooltipTriggerEl);
305
  });
 
 
306
  fetchStatus();
307
  listFiles();
308
+ setInterval(fetchStatus, 3000);
 
309
  });
310
  </script>
 
311
  </body>
312
  </html>
313
  """
314
 
 
315
  # --- Core Backup Logic ---
316
 
317
  def run_backup_job():
 
318
  global app_state
319
  app_state["backup_status"] = "running"
320
  app_state["backup_log"] = ["Starting backup process..."]
 
321
  def log(message):
322
  print(message)
323
  app_state["backup_log"].append(message)
 
324
  try:
 
325
  log("Resetting temporary directories...")
326
  shutil.rmtree(DOWNLOAD_DIR, ignore_errors=True)
327
  shutil.rmtree(EXTRACT_DIR, ignore_errors=True)
328
  os.makedirs(DOWNLOAD_DIR, exist_ok=True)
329
  os.makedirs(EXTRACT_DIR, exist_ok=True)
330
+ log("Downloading from Google Drive folder...")
 
 
 
331
  gdown.download_folder(url=FOLDER_URL, output=DOWNLOAD_DIR, use_cookies=False, quiet=True)
332
  log("Download finished.")
 
 
333
  log("Extracting zip archives...")
334
  extracted_count = 0
335
  for root, _, files in os.walk(DOWNLOAD_DIR):
 
342
  extracted_count += 1
343
  if extracted_count == 0:
344
  log("Warning: No .zip files found to extract.")
 
 
345
  bad_path = os.path.join(EXTRACT_DIR, "world_nither")
346
  good_path = os.path.join(EXTRACT_DIR, "world_nether")
347
  if os.path.exists(bad_path) and not os.path.exists(good_path):
348
  os.rename(bad_path, good_path)
349
  log("Fixed folder name typo: 'world_nither' -> 'world_nether'")
 
 
350
  log("Logging into Hugging Face Hub...")
351
  login(token=TOKEN)
 
 
 
352
  log(f"Ensuring dataset repository '{REPO_ID}' exists...")
353
  api.create_repo(repo_id=REPO_ID, repo_type="dataset", private=False, exist_ok=True)
354
  log("Repository is ready.")
 
 
355
  subfolders_to_upload = {
356
  "world": os.path.join(EXTRACT_DIR, "world"),
357
  "world_nether": os.path.join(EXTRACT_DIR, "world_nether"),
358
  "world_the_end": os.path.join(EXTRACT_DIR, "world_the_end"),
359
  "plugins": os.path.join(EXTRACT_DIR, "plugins")
360
  }
 
361
  for name, path in subfolders_to_upload.items():
362
  if os.path.exists(path):
363
  log(f"Uploading '{name}'...")
364
  upload_folder(
365
+ repo_id=REPO_ID, folder_path=path, repo_type="dataset",
366
+ path_in_repo=name, commit_message=f"Backup update for {name}"
 
 
 
367
  )
368
  log(f"'{name}' uploaded successfully.")
369
  else:
370
  log(f"Skipping '{name}' - directory not found.")
 
371
  app_state["last_backup_time"] = time.strftime("%Y-%m-%d %H:%M:%S %Z")
372
  log(f"Backup completed successfully at {app_state['last_backup_time']}.")
373
  app_state["backup_status"] = "success"
 
374
  except Exception as e:
375
  log(f"AN ERROR OCCURRED: {str(e)}")
376
  app_state["backup_status"] = "error"
 
378
  # --- Scheduler Thread ---
379
 
380
  def scheduler_loop():
 
381
  global app_state
382
  while True:
383
  interval = app_state["schedule_interval_minutes"]
384
  if interval > 0:
385
  if app_state["backup_status"] != "running":
 
386
  run_backup_job()
 
387
  next_run_timestamp = time.time() + interval * 60
388
  app_state["next_backup_time"] = time.strftime("%Y-%m-%d %H:%M:%S %Z", time.localtime(next_run_timestamp))
389
  time.sleep(interval * 60)
 
395
 
396
  @app.route("/")
397
  def index():
 
398
  return render_template_string(HTML_TEMPLATE, repo_id=REPO_ID)
399
 
400
+ # ===================================================================
401
+ # THIS IS THE CORRECTED FUNCTION
402
+ # ===================================================================
403
  @app.route("/api/status", methods=["GET"])
404
  def get_status():
405
+ """Provides a JSON-safe snapshot of the application state."""
406
+ # Create a copy of the state dictionary that EXCLUDES the non-serializable thread object.
407
+ state_for_json = {
408
+ key: value
409
+ for key, value in app_state.items()
410
+ if key != "scheduler_thread"
411
+ }
412
+ return jsonify(state_for_json)
413
+ # ===================================================================
414
 
415
  @app.route("/api/run-backup", methods=["POST"])
416
  def start_backup():
 
417
  if app_state["backup_status"] == "running":
418
  return jsonify({"status": "error", "message": "A backup is already in progress."}), 409
 
419
  threading.Thread(target=run_backup_job, daemon=True).start()
420
  return jsonify({"status": "ok", "message": "Backup process started."})
421
 
422
  @app.route("/api/set-schedule", methods=["POST"])
423
  def set_schedule():
 
424
  try:
425
  interval = int(request.json.get("interval", 0))
426
+ if interval < 0: raise ValueError()
 
427
  app_state["schedule_interval_minutes"] = interval
 
428
  if interval > 0:
429
  next_run_timestamp = time.time() + interval * 60
430
  app_state["next_backup_time"] = time.strftime("%Y-%m-%d %H:%M:%S %Z", time.localtime(next_run_timestamp))
431
  else:
432
  app_state["next_backup_time"] = "Scheduler disabled"
 
433
  return jsonify({"status": "ok", "message": f"Schedule set to {interval} minutes."})
434
  except (ValueError, TypeError):
435
  return jsonify({"status": "error", "message": "Invalid interval value."}), 400
436
 
437
  @app.route("/api/list-files", methods=["GET"])
438
  def list_repo_files():
 
439
  try:
440
  repo_files = api.list_repo_files(repo_id=REPO_ID, repo_type="dataset")
441
  files_details = []
 
443
  try:
444
  info = api.get_repo_file_info(repo_id=REPO_ID, path_in_repo=filename, repo_type="dataset")
445
  size = humanize.naturalsize(info.size) if info.size else "0 B"
446
+ except HfHubHTTPError: size = "N/A"
 
 
447
  files_details.append({
448
+ "name": filename, "size": size,
 
449
  "url": hf_hub_url(repo_id=REPO_ID, filename=filename, repo_type="dataset")
450
  })
451
  return jsonify({"status": "ok", "files": files_details})
 
454
 
455
  @app.route("/api/delete-file", methods=["POST"])
456
  def delete_repo_file():
 
457
  filename = request.json.get("filename")
458
  if not filename:
459
  return jsonify({"status": "error", "message": "Filename not provided."}), 400
 
460
  try:
461
  api.delete_file(
462
+ repo_id=REPO_ID, path_in_repo=filename, repo_type="dataset",
 
 
463
  commit_message=f"Deleted file: {filename}"
464
  )
465
  return jsonify({"status": "ok", "message": f"Successfully deleted '{filename}'."})
 
 
466
  except Exception as e:
467
  return jsonify({"status": "error", "message": str(e)}), 500
468
 
 
469
  # --- Main Execution ---
470
  if __name__ == "__main__":
 
471
  app_state["scheduler_thread"] = threading.Thread(target=scheduler_loop, daemon=True)
472
  app_state["scheduler_thread"].start()
 
 
473
  app.run(host="0.0.0.0", port=7860)