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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +60 -331
app.py CHANGED
@@ -10,25 +10,23 @@ 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
  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 ---
@@ -36,7 +34,6 @@ app = Flask(__name__)
36
  api = HfApi()
37
 
38
  # --- HTML, CSS, JS Template ---
39
-
40
  HTML_TEMPLATE = """
41
  <!DOCTYPE html>
42
  <html lang="en" data-bs-theme="dark">
@@ -46,50 +43,8 @@ HTML_TEMPLATE = """
46
  <title>Backup & Dataset Controller</title>
47
  <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
48
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
49
-
50
  <style>
51
- body {
52
- background-color: #212529; /* Dark background */
53
- }
54
- .log-box {
55
- height: 300px;
56
- overflow-y: auto;
57
- font-family: 'Courier New', Courier, monospace;
58
- font-size: 0.85rem;
59
- color: #f8f9fa;
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;
70
- font-weight: 700;
71
- line-height: 1;
72
- color: #fff;
73
- text-align: center;
74
- white-space: nowrap;
75
- vertical-align: baseline;
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>
@@ -99,7 +54,6 @@ HTML_TEMPLATE = """
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">
104
  <div class="card h-100 shadow-sm">
105
  <div class="card-header d-flex justify-content-between align-items-center">
@@ -107,69 +61,35 @@ HTML_TEMPLATE = """
107
  <div id="backup-status-indicator" class="status-badge" data-bs-toggle="tooltip" title="Current Status">Idle</div>
108
  </div>
109
  <div class="card-body">
110
- <div class="d-grid gap-2 mb-4">
111
- <button id="run-now-btn" class="btn btn-lg btn-success">
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>
118
- <input type="number" class="form-control" id="interval-input" placeholder="0 to disable" min="0">
119
- </div>
120
- <div class="col-auto align-self-end">
121
- <button type="submit" class="btn btn-primary">
122
- <i class="fas fa-save me-2"></i>Set
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>
129
- <strong id="last-run-time">Never</strong>
130
- </li>
131
- <li class="list-group-item d-flex justify-content-between bg-transparent">
132
- <span>Next Scheduled:</span>
133
- <strong id="next-run-time">N/A</strong>
134
- </li>
135
  </ul>
136
  </div>
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">
146
  <div class="card-header d-flex justify-content-between align-items-center">
147
  <h5 class="mb-0"><i class="fas fa-database me-2"></i>Dataset Management</h5>
148
- <a href="https://huggingface.co/datasets/{{ repo_id }}" target="_blank" class="btn btn-sm btn-outline-info">
149
- View on Hub <i class="fas fa-external-link-alt"></i>
150
- </a>
151
  </div>
152
  <div class="card-body">
153
  <div class="d-flex justify-content-between align-items-center mb-3">
154
  <p class="text-muted mb-0">Files in <strong>{{ repo_id }}</strong></p>
155
- <button id="refresh-files-btn" class="btn btn-sm btn-secondary">
156
- <i class="fas fa-sync-alt me-1"></i> Refresh
157
- </button>
158
  </div>
159
  <div id="files-list-container">
160
- <div id="files-loader" class="text-center p-4" style="display: none;">
161
- <div class="spinner-border text-primary" role="status">
162
- <span class="visually-hidden">Loading...</span>
163
- </div>
164
- </div>
165
  <table class="table table-hover">
166
- <thead>
167
- <tr>
168
- <th>File Path</th>
169
- <th>Size</th>
170
- <th>Actions</th>
171
- </tr>
172
- </thead>
173
  <tbody id="files-list"></tbody>
174
  </table>
175
  </div>
@@ -180,140 +100,13 @@ HTML_TEMPLATE = """
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');
187
- const logOutput = document.getElementById('log-output');
188
- const statusIndicator = document.getElementById('backup-status-indicator');
189
- const lastRunTimeEl = document.getElementById('last-run-time');
190
- const nextRunTimeEl = document.getElementById('next-run-time');
191
- const refreshFilesBtn = document.getElementById('refresh-files-btn');
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) {
204
- console.error(`API call to ${endpoint} failed:`, error);
205
- alert(`Error: ${error.message}`);
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...`;
225
- } else {
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) {
232
- updateLog(data.backup_log);
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;
244
- await apiCall('/api/set-schedule', {
245
- method: 'POST',
246
- headers: { 'Content-Type': 'application/json' },
247
- body: JSON.stringify({ interval: parseInt(interval, 10) || 0 }),
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) {
259
- if (data.files.length === 0) {
260
- filesListBody.innerHTML = '<tr><td colspan="3" class="text-center text-muted">No files found in repository.</td></tr>';
261
- return;
262
- }
263
- data.files.forEach(file => {
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>
271
- <button class="btn btn-sm btn-outline-danger delete-btn" data-filename="${file.name}" title="Delete File">
272
- <i class="fas fa-trash-alt"></i>
273
- </button>
274
- </td>
275
- `;
276
- filesListBody.appendChild(row);
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"
@@ -322,117 +115,57 @@ def run_backup_job():
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):
336
- for f in files:
337
  if f.endswith(".zip"):
338
- zp = os.path.join(root, f)
339
- with zipfile.ZipFile(zp) as z:
340
- z.extractall(EXTRACT_DIR)
341
- log(f"Extracted: {f}")
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"
377
 
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)
390
- else:
391
- app_state["next_backup_time"] = "Scheduler disabled"
392
- time.sleep(5)
393
 
394
  # --- Flask Routes (API Endpoints) ---
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():
@@ -441,7 +174,10 @@ def list_repo_files():
441
  files_details = []
442
  for filename in repo_files:
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({
@@ -449,25 +185,18 @@ def list_repo_files():
449
  "url": hf_hub_url(repo_id=REPO_ID, filename=filename, repo_type="dataset")
450
  })
451
  return jsonify({"status": "ok", "files": files_details})
452
- except Exception as e:
453
- return jsonify({"status": "error", "message": str(e)}), 500
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)
 
10
  from huggingface_hub.utils import HfHubHTTPError
11
 
12
  # --- Configuration & Initialization ---
 
 
13
  os.environ["HF_HOME"] = "/tmp/hf_home"
14
  DOWNLOAD_DIR = "/tmp/backups"
15
  EXTRACT_DIR = "/tmp/extracted_backups"
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
 
22
  # --- Global State Management ---
23
  app_state = {
24
+ "backup_status": "idle",
25
  "backup_log": ["Awaiting first run."],
26
  "last_backup_time": "Never",
27
  "next_backup_time": "Scheduler disabled",
28
+ "schedule_interval_minutes": 0,
29
+ "scheduler_thread": None
30
  }
31
 
32
  # --- Flask App Setup ---
 
34
  api = HfApi()
35
 
36
  # --- HTML, CSS, JS Template ---
 
37
  HTML_TEMPLATE = """
38
  <!DOCTYPE html>
39
  <html lang="en" data-bs-theme="dark">
 
43
  <title>Backup & Dataset Controller</title>
44
  <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
45
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
 
46
  <style>
47
+ body{background-color:#212529}.log-box{height:300px;overflow-y:auto;font-family:'Courier New',Courier,monospace;font-size:.85rem;color:#f8f9fa;background-color:#111315!important;border-top:1px solid #495057}.log-box div{padding:2px 5px;border-bottom:1px solid #343a40}.status-badge{padding:.35em .65em;font-size:.75em;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem;transition:background-color .3s ease-in-out}.status-idle{background-color:#6c757d}.status-running{background-color:#0d6efd}.status-success{background-color:#198754}.status-error{background-color:#dc3545}#files-list-container{max-height:450px;overflow-y:auto}.btn i,.btn .spinner-border{pointer-events:none}.card{border:1px solid rgba(255,255,255,.1)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  </style>
49
  </head>
50
  <body>
 
54
  <span class="fs-4">Minecraft Backup & Dataset Controller</span>
55
  </header>
56
  <div class="row g-4">
 
57
  <div class="col-lg-6">
58
  <div class="card h-100 shadow-sm">
59
  <div class="card-header d-flex justify-content-between align-items-center">
 
61
  <div id="backup-status-indicator" class="status-badge" data-bs-toggle="tooltip" title="Current Status">Idle</div>
62
  </div>
63
  <div class="card-body">
64
+ <div class="d-grid gap-2 mb-4"><button id="run-now-btn" class="btn btn-lg btn-success"><i class="fas fa-play-circle me-2"></i>Run Backup Now</button></div>
 
 
 
 
65
  <form id="schedule-form" class="row g-2 align-items-center">
66
+ <div class="col"><label for="interval-input" class="form-label">Schedule Interval (minutes)</label><input type="number" class="form-control" id="interval-input" placeholder="0 to disable" min="0"></div>
67
+ <div class="col-auto align-self-end"><button type="submit" class="btn btn-primary"><i class="fas fa-save me-2"></i>Set</button></div>
 
 
 
 
 
 
 
68
  </form>
69
  <ul class="list-group list-group-flush mt-4">
70
+ <li class="list-group-item d-flex justify-content-between bg-transparent"><span>Last Backup:</span><strong id="last-run-time">Never</strong></li>
71
+ <li class="list-group-item d-flex justify-content-between bg-transparent"><span>Next Scheduled:</span><strong id="next-run-time">N/A</strong></li>
 
 
 
 
 
 
72
  </ul>
73
  </div>
74
+ <div class="card-footer"><strong><i class="fas fa-clipboard-list me-2"></i>Live Log</strong></div>
 
 
75
  <div id="log-output" class="log-box card-body"></div>
76
  </div>
77
  </div>
 
78
  <div class="col-lg-6">
79
  <div class="card h-100 shadow-sm">
80
  <div class="card-header d-flex justify-content-between align-items-center">
81
  <h5 class="mb-0"><i class="fas fa-database me-2"></i>Dataset Management</h5>
82
+ <a href="https://huggingface.co/datasets/{{ repo_id }}" target="_blank" class="btn btn-sm btn-outline-info">View on Hub <i class="fas fa-external-link-alt"></i></a>
 
 
83
  </div>
84
  <div class="card-body">
85
  <div class="d-flex justify-content-between align-items-center mb-3">
86
  <p class="text-muted mb-0">Files in <strong>{{ repo_id }}</strong></p>
87
+ <button id="refresh-files-btn" class="btn btn-sm btn-secondary"><i class="fas fa-sync-alt me-1"></i> Refresh</button>
 
 
88
  </div>
89
  <div id="files-list-container">
90
+ <div id="files-loader" class="text-center p-4" style="display: none;"><div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div></div>
 
 
 
 
91
  <table class="table table-hover">
92
+ <thead><tr><th>File Path</th><th>Size</th><th>Actions</th></tr></thead>
 
 
 
 
 
 
93
  <tbody id="files-list"></tbody>
94
  </table>
95
  </div>
 
100
  </div>
101
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
102
  <script>
103
+ document.addEventListener('DOMContentLoaded',()=>{const e=document.getElementById("run-now-btn"),t=document.getElementById("schedule-form"),n=document.getElementById("interval-input"),o=document.getElementById("log-output"),i=document.getElementById("backup-status-indicator"),s=document.getElementById("last-run-time"),a=document.getElementById("next-run-time"),l=document.getElementById("refresh-files-btn"),c=document.getElementById("files-list"),d=document.getElementById("files-loader");async function r(e,t={}){try{const n=await fetch(e,t);if(!n.ok){const e=await n.json().catch(()=>({message:`HTTP error! Status: ${n.status}`}));throw new Error(e.message)}return n.json()}catch(e){return console.error(`API call to ${e} failed:`,e),alert(`Error: ${e.message}`),null}}function u(e){o.innerHTML=e.map(e=>`<div>${e.replace(/</g,"<").replace(/>/g,">")}</div>`).join(""),o.scrollTop=o.scrollHeight}function p(t){i.textContent=t.backup_status.charAt(0).toUpperCase()+t.backup_status.slice(1),i.className="status-badge",i.classList.add(`status-${t.backup_status}`),s.textContent=t.last_backup_time,a.textContent=t.next_backup_time,document.activeElement!==n&&(n.value=t.schedule_interval_minutes>0?t.schedule_interval_minutes:""),e.disabled="running"===t.backup_status,e.innerHTML="running"===t.backup_status?'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Running...':'<i class="fas fa-play-circle me-2"></i>Run Backup Now'}async function m(){const e=await r("/api/status");e&&(u(e.backup_log),p(e))}async function h(){if(e.disabled)return;await r("/api/run-backup",{method:"POST"})&&m()}async function f(e){e.preventDefault();const t=n.value;await r("/api/set-schedule",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({interval:parseInt(t,10)||0})}),m()}async function g(){d.style.display="block",c.innerHTML="",l.disabled=!0;const e=await r("/api/list-files");d.style.display="none",l.disabled=!1,e&&e.files&&(0===e.files.length?c.innerHTML='<tr><td colspan="3" class="text-center text-muted">No files found in repository.</td></tr>':e.files.forEach(e=>{const t=document.createElement("tr");t.innerHTML=`\n <td class="text-break">\n <a href="${e.url}" target="_blank" title="${e.name}">${e.name}</a>\n </td>\n <td>${e.size}</td>\n <td>\n <button class="btn btn-sm btn-outline-danger delete-btn" data-filename="${e.name}" title="Delete File">\n <i class="fas fa-trash-alt"></i>\n </button>\n </td>\n `,c.appendChild(t)}))}async function y(e){const t=e.target.closest(".delete-btn");if(!t)return;const n=t.dataset.filename;if(!confirm(`Are you sure you want to permanently delete "${n}"?`))return;t.disabled=!0,t.innerHTML='<span class="spinner-border spinner-border-sm"></span>';await r("/api/delete-file",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({filename:n})})?g():(t.disabled=!1,t.innerHTML='<i class="fas fa-trash-alt"></i>')}e.addEventListener("click",h),t.addEventListener("submit",f),l.addEventListener("click",g),c.addEventListener("click",y);const b=[].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));b.map(function(e){return new bootstrap.Tooltip(e)}),m(),g(),setInterval(m,3e3)});
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  </script>
105
  </body>
106
  </html>
107
  """
108
 
109
  # --- Core Backup Logic ---
 
110
  def run_backup_job():
111
  global app_state
112
  app_state["backup_status"] = "running"
 
115
  print(message)
116
  app_state["backup_log"].append(message)
117
  try:
118
+ log("Resetting temporary directories..."); shutil.rmtree(DOWNLOAD_DIR,ignore_errors=True); shutil.rmtree(EXTRACT_DIR,ignore_errors=True); os.makedirs(DOWNLOAD_DIR,exist_ok=True); os.makedirs(EXTRACT_DIR,exist_ok=True)
119
+ log("Downloading from Google Drive folder..."); gdown.download_folder(url=FOLDER_URL,output=DOWNLOAD_DIR,use_cookies=False,quiet=True); log("Download finished.")
120
+ log("Extracting zip archives..."); extracted_count = 0
121
+ for r,_,f_list in os.walk(DOWNLOAD_DIR):
122
+ for f in f_list:
 
 
 
 
 
 
 
123
  if f.endswith(".zip"):
124
+ zp=os.path.join(r,f); z=zipfile.ZipFile(zp); z.extractall(EXTRACT_DIR); z.close(); log(f"Extracted: {f}"); extracted_count += 1
125
+ if extracted_count == 0: log("Warning: No .zip files found to extract.")
126
+ bad_p=os.path.join(EXTRACT_DIR,"world_nither"); good_p=os.path.join(EXTRACT_DIR,"world_nether")
127
+ if os.path.exists(bad_p) and not os.path.exists(good_p): os.rename(bad_p,good_p); log("Fixed typo: 'world_nither'->'world_nether'")
128
+ log("Logging into Hugging Face Hub..."); login(token=TOKEN)
129
+ log(f"Ensuring repo '{REPO_ID}' exists..."); api.create_repo(repo_id=REPO_ID,repo_type="dataset",private=False,exist_ok=True); log("Repo ready.")
130
+ s_up={"world":os.path.join(EXTRACT_DIR,"world"),"world_nether":os.path.join(EXTRACT_DIR,"world_nether"),"world_the_end":os.path.join(EXTRACT_DIR,"world_the_end"),"plugins":os.path.join(EXTRACT_DIR,"plugins")}
131
+ for n,p in s_up.items():
132
+ if os.path.exists(p): log(f"Uploading '{n}'..."); upload_folder(repo_id=REPO_ID,folder_path=p,repo_type="dataset",path_in_repo=n,commit_message=f"Backup: {n}"); log(f"'{n}' uploaded.")
133
+ else: log(f"Skipping '{n}': not found.")
134
+ app_state["last_backup_time"]=time.strftime("%Y-%m-%d %H:%M:%S %Z"); log(f"Backup done at {app_state['last_backup_time']}."); app_state["backup_status"]="success"
135
+ except Exception as e: log(f"AN ERROR OCCURRED: {str(e)}"); app_state["backup_status"]="error"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
 
137
  # --- Scheduler Thread ---
 
138
  def scheduler_loop():
 
139
  while True:
140
+ interval=app_state["schedule_interval_minutes"]
141
+ if interval>0:
142
+ if app_state["backup_status"]!="running": run_backup_job()
143
+ next_run=time.time()+interval*60; app_state["next_backup_time"]=time.strftime("%Y-%m-%d %H:%M:%S %Z",time.localtime(next_run)); time.sleep(interval*60)
144
+ else: app_state["next_backup_time"]="Scheduler disabled"; time.sleep(5)
 
 
 
 
 
145
 
146
  # --- Flask Routes (API Endpoints) ---
 
147
  @app.route("/")
148
+ def index(): return render_template_string(HTML_TEMPLATE,repo_id=REPO_ID)
 
149
 
 
 
 
150
  @app.route("/api/status", methods=["GET"])
151
  def get_status():
152
+ state_for_json = {k:v for k,v in app_state.items() if k!="scheduler_thread"}
 
 
 
 
 
 
153
  return jsonify(state_for_json)
 
154
 
155
  @app.route("/api/run-backup", methods=["POST"])
156
  def start_backup():
157
+ if app_state["backup_status"]=="running": return jsonify({"status":"error","message":"Backup already in progress."}),409
158
+ threading.Thread(target=run_backup_job,daemon=True).start()
159
+ return jsonify({"status":"ok","message":"Backup started."})
 
160
 
161
  @app.route("/api/set-schedule", methods=["POST"])
162
  def set_schedule():
163
  try:
164
+ interval=int(request.json.get("interval",0)); app_state["schedule_interval_minutes"]=interval
165
+ if interval>0: next_run=time.time()+interval*60; app_state["next_backup_time"]=time.strftime("%Y-%m-%d %H:%M:%S %Z",time.localtime(next_run))
166
+ else: app_state["next_backup_time"]="Scheduler disabled"
167
+ return jsonify({"status":"ok","message":f"Schedule set to {interval} minutes."})
168
+ except(ValueError,TypeError): return jsonify({"status":"error","message":"Invalid interval."}),400
 
 
 
 
 
 
169
 
170
  @app.route("/api/list-files", methods=["GET"])
171
  def list_repo_files():
 
174
  files_details = []
175
  for filename in repo_files:
176
  try:
177
+ # ==================================
178
+ # THIS IS THE CORRECTED LINE
179
+ # ==================================
180
+ info = api.file_info(repo_id=REPO_ID, path_in_repo=filename, repo_type="dataset")
181
  size = humanize.naturalsize(info.size) if info.size else "0 B"
182
  except HfHubHTTPError: size = "N/A"
183
  files_details.append({
 
185
  "url": hf_hub_url(repo_id=REPO_ID, filename=filename, repo_type="dataset")
186
  })
187
  return jsonify({"status": "ok", "files": files_details})
188
+ except Exception as e: return jsonify({"status": "error", "message": str(e)}), 500
 
189
 
190
  @app.route("/api/delete-file", methods=["POST"])
191
  def delete_repo_file():
192
  filename = request.json.get("filename")
193
+ if not filename: return jsonify({"status":"error","message":"Filename not provided."}),400
 
194
  try:
195
+ api.delete_file(repo_id=REPO_ID,path_in_repo=filename,repo_type="dataset",commit_message=f"Deleted file: {filename}")
196
+ return jsonify({"status":"ok","message":f"Successfully deleted '{filename}'."})
197
+ except Exception as e: return jsonify({"status":"error","message":str(e)}),500
 
 
 
 
198
 
199
  # --- Main Execution ---
200
+ if __name__=="__main__":
201
+ app_state["scheduler_thread"]=threading.Thread(target=scheduler_loop,daemon=True); app_state["scheduler_thread"].start()
202
+ app.run(host="0.0.0.0",port=7860)