Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -1,160 +1,482 @@
|
|
1 |
# app.py
|
2 |
import os
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
import
|
7 |
-
|
|
|
8 |
import gdown
|
9 |
-
from huggingface_hub import HfApi, login
|
|
|
10 |
|
11 |
-
#
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
|
16 |
-
#
|
|
|
|
|
|
|
|
|
|
|
17 |
DOWNLOAD_DIR = "/tmp/backups"
|
18 |
-
EXTRACT_DIR
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
19 |
|
20 |
-
|
21 |
-
|
22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
39 |
gdown.download_folder(url=FOLDER_URL, output=DOWNLOAD_DIR, use_cookies=False, quiet=True)
|
40 |
-
|
41 |
|
|
|
42 |
for root, _, files in os.walk(DOWNLOAD_DIR):
|
43 |
for f in files:
|
44 |
if f.endswith(".zip"):
|
45 |
-
|
46 |
-
with zipfile.ZipFile(
|
47 |
z.extractall(EXTRACT_DIR)
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
84 |
|
85 |
-
last_backup_time = time.ctime()
|
86 |
-
log_entries.append(f"Backup done at {last_backup_time}")
|
87 |
except Exception as e:
|
88 |
-
|
89 |
-
|
|
|
|
|
|
|
90 |
|
91 |
-
#
|
92 |
-
def
|
93 |
while True:
|
94 |
-
|
95 |
-
|
96 |
-
|
|
|
|
|
|
|
|
|
|
|
97 |
else:
|
98 |
-
time.sleep(
|
99 |
|
100 |
-
|
|
|
|
|
|
|
101 |
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
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=["
|
137 |
-
def
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
|
144 |
-
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
|
149 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
150 |
|
|
|
151 |
if __name__ == "__main__":
|
152 |
-
|
153 |
-
|
154 |
-
|
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)
|
|
|
|
|
|
|
|
|
|
|
|