Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -1,202 +1,68 @@
|
|
1 |
import os
|
2 |
-
import
|
3 |
-
import
|
4 |
-
import
|
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 |
-
#
|
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 ---
|
33 |
app = Flask(__name__)
|
34 |
api = HfApi()
|
35 |
|
36 |
-
#
|
37 |
-
|
38 |
-
<!
|
39 |
-
<
|
40 |
-
<
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
</
|
50 |
-
<
|
51 |
-
<
|
52 |
-
|
53 |
-
|
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">
|
60 |
-
<h5 class="mb-0"><i class="fas fa-shield-alt me-2"></i>Backup Controls</h5>
|
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>
|
96 |
-
</div>
|
97 |
-
</div>
|
98 |
-
</div>
|
99 |
-
</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"
|
113 |
-
app_state["backup_log"] = ["Starting backup process..."]
|
114 |
-
def log(message):
|
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():
|
149 |
-
|
150 |
-
|
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("/
|
171 |
-
def
|
172 |
-
|
173 |
-
|
174 |
-
|
175 |
-
|
176 |
-
|
177 |
-
|
178 |
-
|
179 |
-
|
180 |
-
|
181 |
-
|
182 |
-
|
183 |
-
files_details.append({
|
184 |
-
"name": filename, "size": size,
|
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("/
|
191 |
-
def
|
192 |
-
|
193 |
-
if
|
194 |
-
|
195 |
-
|
196 |
-
|
197 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
198 |
|
199 |
-
|
200 |
-
|
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)
|
|
|
1 |
import os
|
2 |
+
from flask import Flask, request, send_file, redirect, url_for, render_template_string
|
3 |
+
from huggingface_hub import HfApi, upload_file
|
4 |
+
import tempfile
|
|
|
|
|
|
|
|
|
|
|
|
|
5 |
|
6 |
+
# Get credentials from environment
|
|
|
|
|
|
|
|
|
|
|
|
|
7 |
REPO_ID = os.getenv("REPO_ID")
|
8 |
TOKEN = os.getenv("HF_TOKEN")
|
9 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
10 |
app = Flask(__name__)
|
11 |
api = HfApi()
|
12 |
|
13 |
+
# Basic HTML template with upload form
|
14 |
+
TEMPLATE = """
|
15 |
+
<!doctype html>
|
16 |
+
<title>Hugging Face Dataset Browser</title>
|
17 |
+
<h2>Files in {{ repo_id }}</h2>
|
18 |
+
<ul>
|
19 |
+
{% for file in files %}
|
20 |
+
<li>
|
21 |
+
{{ file }} —
|
22 |
+
<a href="{{ url_for('download_file', filename=file) }}">Download</a>
|
23 |
+
</li>
|
24 |
+
{% endfor %}
|
25 |
+
</ul>
|
26 |
+
<h3>Upload File</h3>
|
27 |
+
<form method="POST" action="/upload" enctype="multipart/form-data">
|
28 |
+
<input type="file" name="file">
|
29 |
+
<input type="submit" value="Upload">
|
30 |
+
</form>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
31 |
"""
|
32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
33 |
@app.route("/")
|
34 |
+
def index():
|
35 |
+
files = api.list_repo_files(repo_id=REPO_ID, repo_type="dataset", token=TOKEN)
|
36 |
+
return render_template_string(TEMPLATE, files=files, repo_id=REPO_ID)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
37 |
|
38 |
+
@app.route("/download/<path:filename>")
|
39 |
+
def download_file(filename):
|
40 |
+
url = api.hf_hub_url(REPO_ID, filename, repo_type="dataset")
|
41 |
+
import requests
|
42 |
+
headers = {"Authorization": f"Bearer {TOKEN}"}
|
43 |
+
r = requests.get(url, headers=headers)
|
44 |
+
if r.status_code == 200:
|
45 |
+
tmp = tempfile.NamedTemporaryFile(delete=False)
|
46 |
+
tmp.write(r.content)
|
47 |
+
tmp.close()
|
48 |
+
return send_file(tmp.name, as_attachment=True, download_name=os.path.basename(filename))
|
49 |
+
else:
|
50 |
+
return f"Failed to download {filename}", 404
|
|
|
|
|
|
|
|
|
|
|
|
|
51 |
|
52 |
+
@app.route("/upload", methods=["POST"])
|
53 |
+
def upload():
|
54 |
+
file = request.files["file"]
|
55 |
+
if file:
|
56 |
+
temp_path = os.path.join(tempfile.gettempdir(), file.filename)
|
57 |
+
file.save(temp_path)
|
58 |
+
upload_file(
|
59 |
+
path_or_fileobj=temp_path,
|
60 |
+
path_in_repo=file.filename,
|
61 |
+
repo_id=REPO_ID,
|
62 |
+
repo_type="dataset",
|
63 |
+
token=TOKEN
|
64 |
+
)
|
65 |
+
return redirect(url_for("index"))
|
66 |
|
67 |
+
if __name__ == "__main__":
|
68 |
+
app.run(debug=True, host="0.0.0.0", port=7860)
|
|
|
|