testdeep123 commited on
Commit
2b24f3d
·
verified ·
1 Parent(s): c6089ac

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +450 -128
app.py CHANGED
@@ -1,160 +1,482 @@
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
7
- from flask import Flask, request, render_template_string
 
8
  import gdown
9
- from huggingface_hub import HfApi, login, upload_folder
 
10
 
11
- # Environment variables (set these in your Space settings)
12
- FOLDER_URL = os.getenv("FOLDER_URL")
13
- REPO_ID = os.getenv("REPO_ID")
14
- TOKEN = os.getenv("HF_TOKEN")
15
 
16
- # Directories in writable tmp
 
 
 
 
 
17
  DOWNLOAD_DIR = "/tmp/backups"
18
- EXTRACT_DIR = "/tmp/extracted_backups"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
- # Global state
21
- last_backup_time = "Never"
22
- schedule_interval = 0
 
 
 
 
 
 
23
 
 
24
  app = Flask(__name__)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
- # Backup logic with detailed logging
27
- def run_backup():
28
- global last_backup_time
29
- log_entries = []
30
  try:
31
- log_entries.append("Starting backup process...")
 
 
 
 
 
32
  shutil.rmtree(DOWNLOAD_DIR, ignore_errors=True)
33
  shutil.rmtree(EXTRACT_DIR, ignore_errors=True)
34
- os.makedirs(DOWNLOAD_DIR, exist_ok=True)
35
  os.makedirs(EXTRACT_DIR, exist_ok=True)
36
- log_entries.append(f"Reset directories")
37
 
38
- log_entries.append(f"Downloading: {FOLDER_URL}")
39
  gdown.download_folder(url=FOLDER_URL, output=DOWNLOAD_DIR, use_cookies=False, quiet=True)
40
- log_entries.append("Download finished")
41
 
 
42
  for root, _, files in os.walk(DOWNLOAD_DIR):
43
  for f in files:
44
  if f.endswith(".zip"):
45
- zp = os.path.join(root, f)
46
- with zipfile.ZipFile(zp) as z:
47
  z.extractall(EXTRACT_DIR)
48
- log_entries.append(f"Extracted: {f}")
49
-
50
- # Fix folder typo
51
- bad = os.path.join(EXTRACT_DIR, "world_nither")
52
- good = os.path.join(EXTRACT_DIR, "world_nether")
53
- if os.path.exists(bad) and not os.path.exists(good):
54
- os.rename(bad, good)
55
- log_entries.append("Fixed folder name typo")
56
-
57
- login(token=TOKEN)
58
- api = HfApi()
59
- log_entries.append("Logged into Hugging Face")
60
-
61
- api.create_repo(repo_id=REPO_ID, repo_type="dataset", private=False, exist_ok=True, token=TOKEN)
62
- log_entries.append(f"Dataset ready: {REPO_ID}")
63
-
64
- subfolders = {
65
- "world": os.path.join(EXTRACT_DIR, "world"),
66
- "world_nether": os.path.join(EXTRACT_DIR, "world_nether"),
67
- "world_the_end": os.path.join(EXTRACT_DIR, "world_the_end"),
68
- "plugins": os.path.join(EXTRACT_DIR, "plugins")
69
- }
70
- for name, path in subfolders.items():
71
- if os.path.exists(path):
72
- log_entries.append(f"Uploading {name}")
73
- upload_folder(
74
- repo_id=REPO_ID,
75
- folder_path=path,
76
- repo_type="dataset",
77
- token=TOKEN,
78
- path_in_repo=name,
79
- commit_message=f"add {name}"
80
- )
81
- log_entries.append(f"Uploaded {name}")
82
  else:
83
- log_entries.append(f"Missing {name}, skipped")
 
 
 
 
 
84
 
85
- last_backup_time = time.ctime()
86
- log_entries.append(f"Backup done at {last_backup_time}")
87
  except Exception as e:
88
- log_entries.append(f"Error: {str(e)}")
89
- return "<br>".join(log_entries)
 
 
 
90
 
91
- # Scheduler thread
92
- def schedule_loop():
93
  while True:
94
- if schedule_interval > 0:
95
- run_backup()
96
- time.sleep(schedule_interval * 60)
 
 
 
 
 
97
  else:
98
- time.sleep(5)
99
 
100
- threading.Thread(target=schedule_loop, daemon=True).start()
 
 
 
101
 
102
- # HTML UI
103
- HTML = '''
104
- <!DOCTYPE html>
105
- <html>
106
- <head>
107
- <meta charset="utf-8">
108
- <meta name="viewport" content="width=device-width, initial-scale=1">
109
- <title>Backup Panel</title>
110
- <style>
111
- body { font-family: sans-serif; padding: 20px; max-width: 600px; margin: auto; }
112
- h2 { font-size: 22px; }
113
- input, button { width: 100%; padding: 12px; margin: 8px 0; font-size: 16px; border-radius: 6px; border: 1px solid #ccc; }
114
- .status { background: #f0f0f0; padding: 15px; border-radius: 8px; margin-top: 20px; white-space: pre-wrap; }
115
- </style>
116
- </head>
117
- <body>
118
- <h2>Minecraft Backup Controller</h2>
119
- <form method="post">
120
- <label>Interval (minutes):</label>
121
- <input type="number" name="interval" value="{{ interval }}" min="1">
122
- <button type="submit">Set Timer</button>
123
- </form>
124
- <form method="post">
125
- <input type="hidden" name="manual_run" value="1">
126
- <button type="submit">Run Now</button>
127
- </form>
128
- <div class="status">
129
- Last: {{ last_run }}<br>
130
- Log:<br>{{ status|safe }}
131
- </div>
132
- </body>
133
- </html>
134
- '''
135
 
136
- @app.route("/", methods=["GET", "POST"])
137
- def index():
138
- global schedule_interval
139
- status = ""
140
- if request.method == "POST":
141
- if request.form.get("manual_run"):
142
- status = run_backup()
143
- else:
144
- try:
145
- schedule_interval = int(request.form.get("interval", "0"))
146
- status = f"Timer set: every {schedule_interval} min"
147
- except:
148
- status = "Invalid interval"
149
- return render_template_string(HTML, last_run=last_backup_time, interval=schedule_interval, status=status)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
 
 
151
  if __name__ == "__main__":
152
- app.run(host="0.0.0.0", port=7860)
153
-
154
- # Dockerfile
155
- # ----------
156
- # FROM python:3.10
157
- # WORKDIR /app
158
- # COPY app.py .
159
- # RUN pip install flask huggingface_hub gdown
160
- # CMD ["python", "app.py"]
 
1
  # app.py
2
  import os
3
+ import shutil
4
+ import zipfile
5
+ import threading
6
+ import time
7
+ import logging
8
+ from flask import Flask, request, jsonify, render_template_string
9
  import gdown
10
+ from huggingface_hub import HfApi, login
11
+ from huggingface_hub.utils import HfHubHTTPError
12
 
13
+ # --- CONFIGURATION ---
14
+ # Ensure Hugging Face cache and other temp files write to the writable /tmp directory
15
+ os.environ["HF_HOME"] = "/tmp/hf_home"
16
+ os.environ["GDOWN_CACHE_DIR"] = "/tmp/gdown_cache"
17
 
18
+ # Environment variables (set these in your Space secret settings)
19
+ FOLDER_URL = os.getenv("FOLDER_URL", "YOUR_GOOGLE_DRIVE_FOLDER_URL_HERE")
20
+ REPO_ID = os.getenv("REPO_ID", "your-hf-username/your-dataset-name")
21
+ TOKEN = os.getenv("HF_TOKEN")
22
+
23
+ # Directories in writable /tmp
24
  DOWNLOAD_DIR = "/tmp/backups"
25
+ EXTRACT_DIR = "/tmp/extracted_backups"
26
+
27
+ # --- HTML TEMPLATE WITH EMBEDDED CSS AND JAVASCRIPT ---
28
+ HTML_TEMPLATE = """
29
+ <!DOCTYPE html>
30
+ <html lang="en" data-theme="dark">
31
+ <head>
32
+ <meta charset="UTF-8">
33
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
34
+ <title>HF Backup & Manager</title>
35
+ <!-- Pico.css for a clean, modern look -->
36
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@1/css/pico.min.css">
37
+ <!-- Embedded Custom Styles -->
38
+ <style>
39
+ :root {
40
+ --pico-card-background-color: #1e2025;
41
+ --pico-card-border-color: #33363d;
42
+ }
43
+ body {
44
+ padding: 1rem;
45
+ }
46
+ main.container {
47
+ max-width: 1000px;
48
+ padding-top: 1rem;
49
+ }
50
+ header {
51
+ text-align: center;
52
+ margin-bottom: 2rem;
53
+ }
54
+ article {
55
+ padding: 1.5rem;
56
+ }
57
+ .grid {
58
+ grid-template-columns: 1fr;
59
+ gap: 1.5rem;
60
+ }
61
+ @media (min-width: 992px) {
62
+ .grid {
63
+ grid-template-columns: 1fr 1fr;
64
+ }
65
+ }
66
+ .log-box {
67
+ background-color: #111317;
68
+ border: 1px solid var(--pico-card-border-color);
69
+ border-radius: var(--pico-border-radius);
70
+ padding: 1rem;
71
+ height: 200px;
72
+ overflow-y: auto;
73
+ font-family: monospace;
74
+ font-size: 0.875em;
75
+ white-space: pre-wrap;
76
+ word-break: break-all;
77
+ }
78
+ #status-text.idle { color: var(--pico-color-green-400); }
79
+ #status-text.running { color: var(--pico-color-amber-400); }
80
+ #status-text.error { color: var(--pico-color-red-400); }
81
+ button {
82
+ display: flex;
83
+ align-items: center;
84
+ justify-content: center;
85
+ gap: 0.75rem;
86
+ }
87
+ .spinner {
88
+ border: 3px solid rgba(255, 255, 255, 0.2);
89
+ border-top: 3px solid var(--pico-primary);
90
+ border-radius: 50%;
91
+ width: 16px;
92
+ height: 16px;
93
+ animation: spin 1s linear infinite;
94
+ }
95
+ @keyframes spin {
96
+ 0% { transform: rotate(0deg); }
97
+ 100% { transform: rotate(360deg); }
98
+ }
99
+ .file-manager-container {
100
+ max-height: 400px;
101
+ overflow-y: auto;
102
+ margin-top: 1rem;
103
+ }
104
+ .file-manager-container table button {
105
+ margin: 0;
106
+ padding: 0.25rem 0.5rem;
107
+ background-color: var(--pico-color-red-600);
108
+ border-color: var(--pico-color-red-600);
109
+ }
110
+ small {
111
+ display: block;
112
+ margin-top: -0.5rem;
113
+ margin-bottom: 1rem;
114
+ color: var(--pico-secondary-text);
115
+ }
116
+ </style>
117
+ </head>
118
+ <body>
119
+ <main class="container">
120
+ <header>
121
+ <hgroup>
122
+ <h1>Hugging Face Backup & Manager</h1>
123
+ <p>Automate server backups and manage your dataset on the Hub.</p>
124
+ </hgroup>
125
+ </header>
126
+
127
+ <div class="grid">
128
+ <article>
129
+ <hgroup>
130
+ <h2>Control Panel</h2>
131
+ <h3>Manage your backup tasks and schedule.</h3>
132
+ </hgroup>
133
+ <button id="run-now-btn" onclick="runNow()">
134
+ <span id="run-now-spinner" class="spinner" style="display: none;"></span>
135
+ Run Backup Now
136
+ </button>
137
+ <small>Manually trigger a full backup cycle.</small>
138
+ <form id="schedule-form" onsubmit="setSchedule(event)">
139
+ <label for="interval">Automatic Backup Interval (minutes)</label>
140
+ <input type="number" id="interval" name="interval" placeholder="0" min="0">
141
+ <small>Set to 0 to disable automatic backups.</small>
142
+ <button type="submit">Set Schedule</button>
143
+ </form>
144
+ </article>
145
+
146
+ <article>
147
+ <hgroup>
148
+ <h2>Live Status</h2>
149
+ <h3 id="status-text">Status: Fetching...</h3>
150
+ </hgroup>
151
+ <p><strong>Last Successful Backup:</strong> <span id="last-backup-time">Never</span></p>
152
+ <p><strong>Current Schedule:</strong> Every <span id="current-schedule">...</span> minutes</p>
153
+ <strong>Logs:</strong>
154
+ <pre id="logs" class="log-box"></pre>
155
+ </article>
156
+ </div>
157
+
158
+ <article>
159
+ <hgroup>
160
+ <h2>Dataset File Manager</h2>
161
+ <h3>Manage files in your repository: <a href="https://huggingface.co/datasets/{{ repo_id }}" target="_blank">{{ repo_id }}</a></h3>
162
+ </hgroup>
163
+ <button id="refresh-files-btn" onclick="fetchRepoFiles()" aria-busy="false">Refresh File List</button>
164
+ <div id="file-manager" class="file-manager-container">
165
+ <p>Loading files...</p>
166
+ </div>
167
+ </article>
168
+
169
+ </main>
170
+
171
+ <!-- Embedded JavaScript -->
172
+ <script>
173
+ const runNowBtn = document.getElementById('run-now-btn');
174
+ const runNowSpinner = document.getElementById('run-now-spinner');
175
+ const statusText = document.getElementById('status-text');
176
+ const lastBackupTime = document.getElementById('last-backup-time');
177
+ const currentSchedule = document.getElementById('current-schedule');
178
+ const scheduleInput = document.getElementById('interval');
179
+ const logsBox = document.getElementById('logs');
180
+ const fileManagerDiv = document.getElementById('file-manager');
181
+ const refreshFilesBtn = document.getElementById('refresh-files-btn');
182
+
183
+ async function fetchAPI(url, options = {}) {
184
+ try {
185
+ const response = await fetch(url, options);
186
+ if (!response.ok) {
187
+ const errorData = await response.json();
188
+ throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
189
+ }
190
+ return await response.json();
191
+ } catch (error) {
192
+ console.error(`API Error on ${url}:`, error);
193
+ throw error;
194
+ }
195
+ }
196
+
197
+ async function fetchStatus() {
198
+ try {
199
+ const data = await fetchAPI('/api/status');
200
+ updateStatusUI(data);
201
+ } catch (error) {
202
+ statusText.textContent = "Status: Connection Error";
203
+ statusText.className = "error";
204
+ }
205
+ }
206
+
207
+ async function runNow() {
208
+ if (runNowBtn.disabled) return;
209
+ try {
210
+ await fetchAPI('/api/start-backup', { method: 'POST' });
211
+ } catch (error) {
212
+ alert(`Failed to start backup: ${error.message}`);
213
+ }
214
+ }
215
+
216
+ async function setSchedule(event) {
217
+ event.preventDefault();
218
+ const interval = parseInt(scheduleInput.value, 10);
219
+ if (isNaN(interval) || interval < 0) {
220
+ alert("Please enter a valid non-negative number for the interval.");
221
+ return;
222
+ }
223
+ try {
224
+ await fetchAPI('/api/set-schedule', {
225
+ method: 'POST',
226
+ headers: { 'Content-Type': 'application/json' },
227
+ body: JSON.stringify({ interval })
228
+ });
229
+ } catch (error) {
230
+ alert(`Error setting schedule: ${error.message}`);
231
+ }
232
+ }
233
+
234
+ async function fetchRepoFiles() {
235
+ refreshFilesBtn.setAttribute('aria-busy', 'true');
236
+ try {
237
+ const data = await fetchAPI('/api/repo-files');
238
+ renderFileManager(data.files);
239
+ } catch (error) {
240
+ fileManagerDiv.innerHTML = `<p style="color: var(--pico-color-red-500);">Error loading files: ${error.message}</p>`;
241
+ } finally {
242
+ refreshFilesBtn.setAttribute('aria-busy', 'false');
243
+ }
244
+ }
245
+
246
+ async function deleteFile(path) {
247
+ if (!confirm(`Are you sure you want to permanently delete "${path}"? This cannot be undone.`)) return;
248
+ try {
249
+ await fetchAPI('/api/delete-file', {
250
+ method: 'POST',
251
+ headers: { 'Content-Type': 'application/json' },
252
+ body: JSON.stringify({ path })
253
+ });
254
+ await fetchRepoFiles();
255
+ } catch (error) {
256
+ alert(`Error deleting file: ${error.message}`);
257
+ }
258
+ }
259
+
260
+ function updateStatusUI(data) {
261
+ statusText.textContent = `Status: ${data.status}`;
262
+ statusText.className = data.status.toLowerCase();
263
+
264
+ const isRunning = data.status === 'Running';
265
+ runNowBtn.disabled = isRunning;
266
+ runNowSpinner.style.display = isRunning ? 'inline-block' : 'none';
267
+
268
+ lastBackupTime.textContent = data.last_backup_time;
269
+ const interval = data.schedule_interval;
270
+ currentSchedule.textContent = interval > 0 ? `${interval}` : '0 (disabled)';
271
+ if (document.activeElement !== scheduleInput) {
272
+ scheduleInput.value = interval > 0 ? interval : '';
273
+ }
274
+
275
+ const newLogs = data.logs.join('\\n');
276
+ if (logsBox.textContent !== newLogs) {
277
+ logsBox.textContent = newLogs;
278
+ logsBox.scrollTop = logsBox.scrollHeight;
279
+ }
280
+ }
281
+
282
+ function renderFileManager(files) {
283
+ if (!files || files.length === 0) {
284
+ fileManagerDiv.innerHTML = "<p>No files found in the repository.</p>";
285
+ return;
286
+ }
287
+ let html = '<table><thead><tr><th>File Path</th><th style="text-align: right;">Action</th></tr></thead><tbody>';
288
+ files.forEach(file => {
289
+ html += `
290
+ <tr>
291
+ <td>${file}</td>
292
+ <td style="text-align: right;"><button class="outline secondary" onclick="deleteFile('${file}')">Delete</button></td>
293
+ </tr>
294
+ `;
295
+ });
296
+ html += '</tbody></table>';
297
+ fileManagerDiv.innerHTML = html;
298
+ }
299
 
300
+ document.addEventListener('DOMContentLoaded', () => {
301
+ fetchStatus();
302
+ fetchRepoFiles();
303
+ setInterval(fetchStatus, 3000);
304
+ });
305
+ </script>
306
+ </body>
307
+ </html>
308
+ """
309
 
310
+ # --- FLASK APP & STATE MANAGEMENT ---
311
  app = Flask(__name__)
312
+ logging.basicConfig(level=logging.INFO)
313
+
314
+ app_state = {
315
+ "status": "Idle", # Idle, Running, Error
316
+ "logs": [],
317
+ "last_backup_time": "Never",
318
+ "schedule_interval": 0, # in minutes
319
+ "scheduler_thread": None,
320
+ "lock": threading.Lock(),
321
+ }
322
+
323
+ # --- HUGGING FACE HELPER CLASS ---
324
+ class HFManager:
325
+ def __init__(self, token, repo_id, repo_type="dataset"):
326
+ if not token:
327
+ raise ValueError("Hugging Face token (HF_TOKEN) is not set.")
328
+ self.token = token
329
+ self.repo_id = repo_id
330
+ self.repo_type = repo_type
331
+ self.api = HfApi()
332
+ login(token=self.token)
333
+
334
+ def ensure_repo_exists(self):
335
+ self.api.create_repo(repo_id=self.repo_id, repo_type=self.repo_type, exist_ok=True, token=self.token)
336
+
337
+ def list_files(self):
338
+ try:
339
+ return sorted(self.api.list_repo_files(repo_id=self.repo_id, repo_type=self.repo_type, token=self.token))
340
+ except HfHubHTTPError as e:
341
+ if e.response.status_code == 404: return []
342
+ raise e
343
+
344
+ def delete_file(self, path_in_repo):
345
+ self.api.delete_file(path_in_repo, repo_id=self.repo_id, repo_type=self.repo_type, token=self.token, commit_message=f"Delete file: {path_in_repo}")
346
+
347
+ def upload(self, folder_path, path_in_repo, commit_message):
348
+ self.api.upload_folder(repo_id=self.repo_id, folder_path=folder_path, repo_type=self.repo_type, token=self.token, path_in_repo=path_in_repo, commit_message=commit_message)
349
+
350
+ # --- BACKUP LOGIC ---
351
+ def run_backup_job():
352
+ with app_state['lock']:
353
+ if app_state["status"] == "Running":
354
+ app_state['logs'].append("Backup is already in progress. Skipping scheduled run.")
355
+ return
356
+ app_state["status"] = "Running"
357
+ app_state["logs"] = ["Starting backup process..."]
358
+
359
+ log_entry = lambda msg: app_state['logs'].append(f"[{time.strftime('%H:%M:%S')}] {msg}")
360
 
 
 
 
 
361
  try:
362
+ if not FOLDER_URL or "YOUR_GOOGLE_DRIVE" in FOLDER_URL:
363
+ raise ValueError("FOLDER_URL is not set. Please set it in your Space secrets.")
364
+ if not TOKEN:
365
+ raise ValueError("HF_TOKEN is not set. Please set it in your Space secrets.")
366
+
367
+ log_entry("Cleaning up temporary directories...")
368
  shutil.rmtree(DOWNLOAD_DIR, ignore_errors=True)
369
  shutil.rmtree(EXTRACT_DIR, ignore_errors=True)
 
370
  os.makedirs(EXTRACT_DIR, exist_ok=True)
 
371
 
372
+ log_entry(f"Downloading from Google Drive...")
373
  gdown.download_folder(url=FOLDER_URL, output=DOWNLOAD_DIR, use_cookies=False, quiet=True)
374
+ log_entry("Download finished.")
375
 
376
+ extracted_files = False
377
  for root, _, files in os.walk(DOWNLOAD_DIR):
378
  for f in files:
379
  if f.endswith(".zip"):
380
+ zip_path = os.path.join(root, f)
381
+ with zipfile.ZipFile(zip_path, 'r') as z:
382
  z.extractall(EXTRACT_DIR)
383
+ log_entry(f"Extracted: {f}")
384
+ extracted_files = True
385
+ if not extracted_files:
386
+ log_entry("Warning: No .zip files found to extract.")
387
+
388
+ bad_path, good_path = os.path.join(EXTRACT_DIR, "world_nither"), os.path.join(EXTRACT_DIR, "world_nether")
389
+ if os.path.exists(bad_path):
390
+ os.rename(bad_path, good_path)
391
+ log_entry("Fixed 'world_nither' typo to 'world_nether'.")
392
+
393
+ hf_manager = HFManager(TOKEN, REPO_ID)
394
+ hf_manager.ensure_repo_exists()
395
+ log_entry(f"Repo ready: {REPO_ID}")
396
+
397
+ for name in ["world", "world_nether", "world_the_end", "plugins"]:
398
+ local_path = os.path.join(EXTRACT_DIR, name)
399
+ if os.path.exists(local_path):
400
+ log_entry(f"Uploading '{name}'...")
401
+ hf_manager.upload(local_path, name, f"Backup update for {name}")
402
+ log_entry(f"Successfully uploaded '{name}'.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
403
  else:
404
+ log_entry(f"Source folder '{name}' not found, skipping.")
405
+
406
+ with app_state['lock']:
407
+ app_state["last_backup_time"] = time.strftime('%Y-%m-%d %H:%M:%S %Z')
408
+ log_entry(f"Backup complete!")
409
+ app_state["status"] = "Idle"
410
 
 
 
411
  except Exception as e:
412
+ error_message = f"An error occurred: {str(e)}"
413
+ logging.error(error_message, exc_info=True)
414
+ with app_state['lock']:
415
+ app_state["logs"].append(f"ERROR: {error_message}")
416
+ app_state["status"] = "Error"
417
 
418
+ # --- SCHEDULER THREAD ---
419
+ def scheduler_loop():
420
  while True:
421
+ with app_state['lock']:
422
+ interval_minutes = app_state['schedule_interval']
423
+ if interval_minutes > 0:
424
+ next_run_time = time.time() + interval_minutes * 60
425
+ run_backup_job()
426
+ sleep_duration = next_run_time - time.time()
427
+ if sleep_duration > 0:
428
+ time.sleep(sleep_duration)
429
  else:
430
+ time.sleep(15)
431
 
432
+ # --- FLASK ROUTES ---
433
+ @app.route("/")
434
+ def index():
435
+ return render_template_string(HTML_TEMPLATE, repo_id=REPO_ID)
436
 
437
+ @app.route("/api/status")
438
+ def status():
439
+ with app_state['lock']:
440
+ return jsonify(dict(app_state))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
441
 
442
+ @app.route("/api/start-backup", methods=["POST"])
443
+ def start_backup():
444
+ threading.Thread(target=run_backup_job).start()
445
+ return jsonify({"message": "Backup process initiated."})
446
+
447
+ @app.route("/api/set-schedule", methods=["POST"])
448
+ def set_schedule():
449
+ try:
450
+ interval = int(request.json.get("interval", 0))
451
+ if interval < 0: raise ValueError("Interval cannot be negative.")
452
+ with app_state['lock']:
453
+ app_state['schedule_interval'] = interval
454
+ return jsonify({"message": f"Schedule updated to {interval} minutes."})
455
+ except (ValueError, TypeError):
456
+ return jsonify({"error": "Invalid interval. Please provide a non-negative integer."}), 400
457
+
458
+ @app.route("/api/repo-files")
459
+ def get_repo_files():
460
+ try:
461
+ hf_manager = HFManager(TOKEN, REPO_ID)
462
+ return jsonify({"files": hf_manager.list_files()})
463
+ except Exception as e:
464
+ return jsonify({"error": str(e)}), 500
465
+
466
+ @app.route("/api/delete-file", methods=["POST"])
467
+ def delete_repo_file():
468
+ path = request.json.get("path")
469
+ if not path:
470
+ return jsonify({"error": "File path not provided."}), 400
471
+ try:
472
+ hf_manager = HFManager(TOKEN, REPO_ID)
473
+ hf_manager.delete_file(path)
474
+ return jsonify({"message": f"Successfully deleted {path}"})
475
+ except Exception as e:
476
+ return jsonify({"error": str(e)}), 500
477
 
478
+ # --- MAIN EXECUTION ---
479
  if __name__ == "__main__":
480
+ app_state["scheduler_thread"] = threading.Thread(target=scheduler_loop, daemon=True)
481
+ app_state["scheduler_thread"].start()
482
+ app.run(host="0.0.0.0", port=7860)