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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +492 -1064
app.py CHANGED
@@ -1,1147 +1,575 @@
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)
 
 
 
1
  import os
2
+ import shutil
3
+ import zipfile
4
+ import threading
5
+ import time
6
+ import humanize
7
+ from flask import Flask, request, jsonify, render_template_string
8
  import gdown
9
+ from huggingface_hub import HfApi, login, upload_folder, hf_hub_url
10
+ from huggingface_hub.utils import HfHubHTTPError
 
 
 
 
11
 
12
+ # --- Configuration & Initialization ---
 
 
 
 
13
 
14
+ # Ensure Hugging Face cache and other temp data writes to /tmp
15
+ os.environ["HF_HOME"] = "/tmp/hf_home"
16
  DOWNLOAD_DIR = "/tmp/backups"
17
  EXTRACT_DIR = "/tmp/extracted_backups"
 
18
 
19
+ # Environment variables (set these in your Space secrets)
20
+ FOLDER_URL = os.getenv("FOLDER_URL")
21
+ 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 ---
36
  app = Flask(__name__)
37
+ api = HfApi()
38
 
39
+ # --- HTML, CSS, JS Template ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
+ HTML_TEMPLATE = """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  <!DOCTYPE html>
43
+ <html lang="en" data-bs-theme="dark">
44
  <head>
45
+ <meta charset="utf-8">
46
+ <meta name="viewport" content="width=device-width, initial-scale=1">
47
+ <title>Backup & Dataset Controller</title>
48
+ <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
49
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
50
+
51
+ <style>
52
+ body {
53
+ background-color: #212529; /* Dark background */
54
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
+ .log-box {
57
+ height: 300px;
58
+ overflow-y: auto;
59
+ font-family: 'Courier New', Courier, monospace;
60
+ font-size: 0.85rem;
61
+ color: #f8f9fa;
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;
74
+ font-weight: 700;
75
+ line-height: 1;
76
+ color: #fff;
77
+ text-align: center;
78
+ white-space: nowrap;
79
+ vertical-align: baseline;
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">
115
+ <div class="card h-100 shadow-sm">
116
+ <div class="card-header d-flex justify-content-between align-items-center">
117
+ <h5 class="mb-0"><i class="fas fa-shield-alt me-2"></i>Backup Controls</h5>
118
+ <div id="backup-status-indicator" class="status-badge" data-bs-toggle="tooltip" title="Current Status">Idle</div>
119
  </div>
120
+ <div class="card-body">
121
+ <div class="d-grid gap-2 mb-4">
122
+ <button id="run-now-btn" class="btn btn-lg btn-success">
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>
130
+ <input type="number" class="form-control" id="interval-input" placeholder="0 to disable" min="0">
 
 
131
  </div>
132
+ <div class="col-auto align-self-end">
133
+ <button type="submit" class="btn btn-primary">
134
+ <i class="fas fa-save me-2"></i>Set
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>
142
+ <strong id="last-run-time">Never</strong>
143
+ </li>
144
+ <li class="list-group-item d-flex justify-content-between bg-transparent">
145
+ <span>Next Scheduled:</span>
146
+ <strong id="next-run-time">N/A</strong>
147
+ </li>
148
+ </ul>
149
+ </div>
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">
162
+ <div class="card-header d-flex justify-content-between align-items-center">
163
+ <h5 class="mb-0"><i class="fas fa-database me-2"></i>Dataset Management</h5>
164
+ <a href="https://huggingface.co/datasets/{{ repo_id }}" target="_blank" class="btn btn-sm btn-outline-info">
165
+ View on Hub <i class="fas fa-external-link-alt"></i>
166
+ </a>
 
 
 
 
 
 
 
 
 
 
 
 
167
  </div>
168
+ <div class="card-body">
169
+ <div class="d-flex justify-content-between align-items-center mb-3">
170
+ <p class="text-muted mb-0">Files in <strong>{{ repo_id }}</strong></p>
171
+ <button id="refresh-files-btn" class="btn btn-sm btn-secondary">
172
+ <i class="fas fa-sync-alt me-1"></i> Refresh
173
+ </button>
174
+ </div>
175
+ <div id="files-list-container">
176
+ <div id="files-loader" class="text-center p-4" style="display: none;">
177
+ <div class="spinner-border text-primary" role="status">
178
+ <span class="visually-hidden">Loading...</span>
 
179
  </div>
180
  </div>
181
+ <table class="table table-hover">
182
+ <thead>
183
+ <tr>
184
+ <th>File Path</th>
185
+ <th>Size</th>
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>
195
  </div>
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');
209
+ const logOutput = document.getElementById('log-output');
210
+ const statusIndicator = document.getElementById('backup-status-indicator');
211
+ const lastRunTimeEl = document.getElementById('last-run-time');
212
+ const nextRunTimeEl = document.getElementById('next-run-time');
213
+ const refreshFilesBtn = document.getElementById('refresh-files-btn');
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) {
229
+ console.error(`API call to ${endpoint} failed:`, error);
230
+ alert(`Error: ${error.message}`);
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...`;
260
+ } else {
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) {
269
+ updateLog(data.backup_log);
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;
286
+ await apiCall('/api/set-schedule', {
287
+ method: 'POST',
288
+ headers: { 'Content-Type': 'application/json' },
289
+ body: JSON.stringify({ interval: parseInt(interval, 10) || 0 }),
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) {
304
+ if (data.files.length === 0) {
305
+ filesListBody.innerHTML = '<tr><td colspan="3" class="text-center text-muted">No files found in repository.</td></tr>';
306
  return;
307
  }
308
+ data.files.forEach(file => {
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>
318
+ <button class="btn btn-sm btn-outline-danger delete-btn" data-filename="${file.name}" title="Delete File">
319
+ <i class="fas fa-trash-alt"></i>
320
+ </button>
321
+ </td>
322
+ `;
323
+ filesListBody.appendChild(row);
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):
409
+ for f in files:
410
+ if f.endswith(".zip"):
411
+ zp = os.path.join(root, f)
412
+ with zipfile.ZipFile(zp) as z:
413
+ z.extractall(EXTRACT_DIR)
414
+ log(f"Extracted: {f}")
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"
465
+
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)
481
+ else:
482
+ app_state["next_backup_time"] = "Scheduler disabled"
483
+ time.sleep(5)
484
 
485
+ # --- Flask Routes (API Endpoints) ---
 
 
 
 
 
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 = []
531
+ for filename in 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})
544
  except Exception as e:
545
+ return jsonify({"status": "error", "message": str(e)}), 500
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)