josephrw commited on
Commit
4465ff2
Β·
verified Β·
1 Parent(s): b74d107

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +403 -15
app.py CHANGED
@@ -2,15 +2,21 @@
2
  """
3
  LocalSpace Deployer β€” Hugging Face Space (Vanilla)
4
 
 
 
 
5
  Pure FastAPI + HTML/JS. No Gradio, no Streamlit, no API keys.
6
- Upload a DMG, get a public download link. The Space IS the host.
7
  """
8
  from __future__ import annotations
9
 
10
  import hashlib
11
  import json
12
  import os
 
13
  import re
 
 
14
  import time
15
  import uuid
16
  from pathlib import Path
@@ -19,19 +25,23 @@ from typing import Any
19
  from fastapi import FastAPI, File, Request, UploadFile
20
  from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
21
  from fastapi.staticfiles import StaticFiles
22
- from fastapi.templating import Jinja2Templates
 
 
 
 
 
 
23
 
24
  UPLOAD_DIR = Path("/tmp/uploads")
25
  UPLOAD_DIR.mkdir(exist_ok=True)
26
-
 
27
  DB_PATH = Path("/tmp/apps.json")
28
 
29
  app = FastAPI(title="LocalSpace Deployer")
30
-
31
- # Serve static files
32
  app.mount("/static", StaticFiles(directory="static"), name="static")
33
 
34
- # In-memory store (persisted to JSON)
35
  _store: dict[str, dict[str, Any]] = {}
36
 
37
 
@@ -61,10 +71,122 @@ def _hash_file(path: Path) -> str:
61
  return h.hexdigest()
62
 
63
 
64
- _load_db()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
 
66
 
67
- # ─── Frontend ───────────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
  @app.get("/", response_class=HTMLResponse)
70
  def index(request: Request) -> HTMLResponse:
@@ -72,8 +194,6 @@ def index(request: Request) -> HTMLResponse:
72
  return HTMLResponse(content=f.read())
73
 
74
 
75
- # ─── API ────────────────────────────────────────────────────────────────────
76
-
77
  @app.post("/api/upload")
78
  async def upload_file(file: UploadFile = File(...)) -> JSONResponse:
79
  if not file.filename or not file.filename.lower().endswith(".dmg"):
@@ -81,7 +201,6 @@ async def upload_file(file: UploadFile = File(...)) -> JSONResponse:
81
 
82
  app_id = str(uuid.uuid4())
83
  slug = _slugify(file.filename)
84
- # ensure unique slug
85
  base_slug = slug
86
  counter = 1
87
  while any(a.get("slug") == slug for a in _store.values()):
@@ -99,6 +218,8 @@ async def upload_file(file: UploadFile = File(...)) -> JSONResponse:
99
  sha256 = _hash_file(dest)
100
  size = dest.stat().st_size
101
 
 
 
102
  entry = {
103
  "app_id": app_id,
104
  "slug": slug,
@@ -106,6 +227,10 @@ async def upload_file(file: UploadFile = File(...)) -> JSONResponse:
106
  "size": size,
107
  "sha256": sha256,
108
  "download_url": f"/api/download/{app_id}",
 
 
 
 
109
  "created_at": time.time(),
110
  }
111
  _store[app_id] = entry
@@ -113,7 +238,7 @@ async def upload_file(file: UploadFile = File(...)) -> JSONResponse:
113
 
114
  return JSONResponse({
115
  "app": entry,
116
- "message": "DMG uploaded successfully.",
117
  }, status_code=201)
118
 
119
 
@@ -148,12 +273,275 @@ def download_app(app_id: str):
148
  )
149
 
150
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  @app.delete("/api/apps/{app_id}")
152
  def delete_app(app_id: str) -> JSONResponse:
153
  entry = _store.pop(app_id, None)
154
  if entry:
155
- path = UPLOAD_DIR / f"{app_id}.dmg"
156
- if path.exists():
157
- path.unlink()
 
 
 
158
  _save_db()
159
  return JSONResponse({"ok": True})
 
2
  """
3
  LocalSpace Deployer β€” Hugging Face Space (Vanilla)
4
 
5
+ Drag a DMG. It gets OPENED on the server: extracted, inspected, and its
6
+ app bundle metadata is displayed. The Space itself hosts the DMG contents.
7
+
8
  Pure FastAPI + HTML/JS. No Gradio, no Streamlit, no API keys.
9
+ Public by default.
10
  """
11
  from __future__ import annotations
12
 
13
  import hashlib
14
  import json
15
  import os
16
+ import plistlib
17
  import re
18
+ import shutil
19
+ import subprocess
20
  import time
21
  import uuid
22
  from pathlib import Path
 
25
  from fastapi import FastAPI, File, Request, UploadFile
26
  from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
27
  from fastapi.staticfiles import StaticFiles
28
+
29
+ # Try biplist for binary plists, fall back to plistlib
30
+ try:
31
+ import biplist
32
+ HAS_BIPLIST = True
33
+ except ImportError:
34
+ HAS_BIPLIST = False
35
 
36
  UPLOAD_DIR = Path("/tmp/uploads")
37
  UPLOAD_DIR.mkdir(exist_ok=True)
38
+ EXTRACT_DIR = Path("/tmp/extracted")
39
+ EXTRACT_DIR.mkdir(exist_ok=True)
40
  DB_PATH = Path("/tmp/apps.json")
41
 
42
  app = FastAPI(title="LocalSpace Deployer")
 
 
43
  app.mount("/static", StaticFiles(directory="static"), name="static")
44
 
 
45
  _store: dict[str, dict[str, Any]] = {}
46
 
47
 
 
71
  return h.hexdigest()
72
 
73
 
74
+ def _read_plist(path: Path) -> dict[str, Any]:
75
+ """Read a plist file (XML or binary)."""
76
+ try:
77
+ with open(path, "rb") as f:
78
+ data = f.read()
79
+ try:
80
+ return plistlib.loads(data)
81
+ except Exception:
82
+ pass
83
+ if HAS_BIPLIST:
84
+ try:
85
+ return biplist.readPlistFromString(data)
86
+ except Exception:
87
+ pass
88
+ except Exception:
89
+ pass
90
+ return {}
91
+
92
+
93
+ def _find_app_bundles(root: Path) -> list[Path]:
94
+ """Find all .app directories under root."""
95
+ apps = []
96
+ for p in root.rglob("*.app"):
97
+ if p.is_dir():
98
+ apps.append(p)
99
+ return apps
100
+
101
+
102
+ def _extract_dmg(dmg_path: Path, dest: Path) -> bool:
103
+ """Extract DMG using 7z. Returns True on success."""
104
+ try:
105
+ dest.mkdir(parents=True, exist_ok=True)
106
+ result = subprocess.run(
107
+ ["7z", "x", "-y", "-o" + str(dest), str(dmg_path)],
108
+ capture_output=True,
109
+ text=True,
110
+ timeout=120,
111
+ )
112
+ return result.returncode == 0
113
+ except Exception:
114
+ return False
115
+
116
+
117
+ def _inspect_app_bundle(app_path: Path) -> dict[str, Any]:
118
+ """Read Info.plist and extract metadata from an .app bundle."""
119
+ info_plist = app_path / "Contents" / "Info.plist"
120
+ if not info_plist.exists():
121
+ info_plist = app_path / "Info.plist"
122
+
123
+ info = _read_plist(info_plist) if info_plist.exists() else {}
124
+
125
+ icon_name = info.get("CFBundleIconFile", "")
126
+ icon_path = None
127
+ if icon_name:
128
+ icons_dir = app_path / "Contents" / "Resources"
129
+ if icons_dir.exists():
130
+ icns = icons_dir / (icon_name if icon_name.endswith(".icns") else icon_name + ".icns")
131
+ if icns.exists():
132
+ icon_path = str(icns)
133
+
134
+ files = []
135
+ if app_path.exists():
136
+ for f in sorted(app_path.rglob("*")):
137
+ if f.is_file():
138
+ try:
139
+ rel = str(f.relative_to(app_path))
140
+ files.append(rel)
141
+ except ValueError:
142
+ pass
143
+
144
+ return {
145
+ "bundle_name": app_path.name,
146
+ "bundle_id": info.get("CFBundleIdentifier", ""),
147
+ "display_name": info.get("CFBundleDisplayName", "") or info.get("CFBundleName", ""),
148
+ "version": info.get("CFBundleShortVersionString", "") or info.get("CFBundleVersion", ""),
149
+ "min_os_version": info.get("LSMinimumSystemVersion", ""),
150
+ "executable": info.get("CFBundleExecutable", ""),
151
+ "icon_path": icon_path,
152
+ "info_plist": info,
153
+ "file_count": len(files),
154
+ "files": files[:200],
155
+ }
156
 
157
 
158
+ def _open_dmg(app_id: str, dmg_path: Path) -> dict[str, Any]:
159
+ """Open a DMG: extract it, find apps, inspect them."""
160
+ extract_to = EXTRACT_DIR / app_id
161
+ if extract_to.exists():
162
+ shutil.rmtree(extract_to)
163
+
164
+ success = _extract_dmg(dmg_path, extract_to)
165
+ if not success:
166
+ return {"opened": False, "error": "Extraction failed. DMG may be encrypted or use an unsupported format."}
167
+
168
+ apps = _find_app_bundles(extract_to)
169
+ inspected = [_inspect_app_bundle(a) for a in apps]
170
+
171
+ tree = []
172
+ for item in sorted(extract_to.iterdir()):
173
+ tree.append({
174
+ "name": item.name,
175
+ "type": "directory" if item.is_dir() else "file",
176
+ "size": item.stat().st_size if item.is_file() else 0,
177
+ })
178
+
179
+ return {
180
+ "opened": True,
181
+ "extracted_path": str(extract_to),
182
+ "apps_found": len(inspected),
183
+ "apps": inspected,
184
+ "tree": tree,
185
+ }
186
+
187
+
188
+ _load_db()
189
+
190
 
191
  @app.get("/", response_class=HTMLResponse)
192
  def index(request: Request) -> HTMLResponse:
 
194
  return HTMLResponse(content=f.read())
195
 
196
 
 
 
197
  @app.post("/api/upload")
198
  async def upload_file(file: UploadFile = File(...)) -> JSONResponse:
199
  if not file.filename or not file.filename.lower().endswith(".dmg"):
 
201
 
202
  app_id = str(uuid.uuid4())
203
  slug = _slugify(file.filename)
 
204
  base_slug = slug
205
  counter = 1
206
  while any(a.get("slug") == slug for a in _store.values()):
 
218
  sha256 = _hash_file(dest)
219
  size = dest.stat().st_size
220
 
221
+ opened = _open_dmg(app_id, dest)
222
+
223
  entry = {
224
  "app_id": app_id,
225
  "slug": slug,
 
227
  "size": size,
228
  "sha256": sha256,
229
  "download_url": f"/api/download/{app_id}",
230
+ "opened": opened.get("opened", False),
231
+ "apps_found": opened.get("apps_found", 0),
232
+ "apps": opened.get("apps", []),
233
+ "tree": opened.get("tree", []),
234
  "created_at": time.time(),
235
  }
236
  _store[app_id] = entry
 
238
 
239
  return JSONResponse({
240
  "app": entry,
241
+ "message": "DMG uploaded and opened." if entry["opened"] else "DMG uploaded but could not be opened.",
242
  }, status_code=201)
243
 
244
 
 
273
  )
274
 
275
 
276
+ @app.get("/api/browse/{app_id}/{path:path}")
277
+ def browse_extracted(app_id: str, path: str):
278
+ entry = _store.get(app_id)
279
+ if not entry:
280
+ return JSONResponse({"error": "Not found"}, status_code=404)
281
+
282
+ safe_path = Path(path).name if not path else path
283
+ base = EXTRACT_DIR / app_id
284
+ target = base / safe_path
285
+
286
+ try:
287
+ target.resolve().relative_to(base.resolve())
288
+ except ValueError:
289
+ return JSONResponse({"error": "Access denied"}, status_code=403)
290
+
291
+ if not target.exists():
292
+ return JSONResponse({"error": "Not found"}, status_code=404)
293
+
294
+ if target.is_dir():
295
+ items = []
296
+ for item in sorted(target.iterdir()):
297
+ items.append({
298
+ "name": item.name,
299
+ "type": "directory" if item.is_dir() else "file",
300
+ "size": item.stat().st_size if item.is_file() else 0,
301
+ })
302
+ return JSONResponse({"items": items})
303
+
304
+ return FileResponse(path=target)
305
+
306
+
307
+ # ─── Preview extracted HTML content in iframe ──────────────────────────────
308
+
309
+ @app.get("/api/preview/{app_id}/{path:path}")
310
+ def preview_content(app_id: str, path: str):
311
+ """Serve extracted DMG content for iframe preview."""
312
+ entry = _store.get(app_id)
313
+ if not entry:
314
+ return JSONResponse({"error": "Not found"}, status_code=404)
315
+
316
+ base = EXTRACT_DIR / app_id
317
+ if path:
318
+ target = base / path
319
+ else:
320
+ # Default to index.html if no path
321
+ target = base / "index.html"
322
+ if not target.exists():
323
+ # Find any HTML file
324
+ for f in base.rglob("*.html"):
325
+ target = f
326
+ break
327
+
328
+ if not target.exists():
329
+ return JSONResponse({"error": "Not found"}, status_code=404)
330
+
331
+ # Security check
332
+ try:
333
+ target.resolve().relative_to(base.resolve())
334
+ except ValueError:
335
+ return JSONResponse({"error": "Access denied"}, status_code=403)
336
+
337
+ if target.is_dir():
338
+ # Look for index.html in directory
339
+ idx = target / "index.html"
340
+ if idx.exists():
341
+ target = idx
342
+ else:
343
+ return JSONResponse({"error": "No index.html"}, status_code=404)
344
+
345
+ mime_types = {
346
+ ".html": "text/html",
347
+ ".htm": "text/html",
348
+ ".js": "application/javascript",
349
+ ".css": "text/css",
350
+ ".png": "image/png",
351
+ ".jpg": "image/jpeg",
352
+ ".jpeg": "image/jpeg",
353
+ ".gif": "image/gif",
354
+ ".svg": "image/svg+xml",
355
+ ".json": "application/json",
356
+ ".woff2": "font/woff2",
357
+ ".woff": "font/woff",
358
+ ".ttf": "font/ttf",
359
+ }
360
+ suffix = target.suffix.lower()
361
+ media_type = mime_types.get(suffix, "application/octet-stream")
362
+
363
+ # For HTML, inject base tag to handle relative paths
364
+ if media_type == "text/html":
365
+ content = target.read_text(errors="replace")
366
+ # Inject base tag after <head>
367
+ if "<base" not in content:
368
+ rel_dir = str(target.parent.relative_to(base)) if target.parent != base else ""
369
+ base_tag = f'<base href="/api/preview/{app_id}/{rel_dir}/">' if rel_dir else f'<base href="/api/preview/{app_id}/">'
370
+ content = content.replace("<head>", f"<head>{base_tag}", 1)
371
+ content = content.replace("<HEAD>", f"<HEAD>{base_tag}", 1)
372
+ return HTMLResponse(content=content, media_type=media_type)
373
+
374
+ return FileResponse(path=target, media_type=media_type)
375
+
376
+
377
+ # ─── Create DMG from iframe URL (Web App β†’ macOS App) ─────────────────────
378
+
379
+ WEBAPP_DIR = Path("/tmp/webapps")
380
+ WEBAPP_DIR.mkdir(exist_ok=True)
381
+
382
+
383
+ def _create_app_bundle(url: str, app_name: str, bundle_id: str, version: str) -> Path:
384
+ """Create a minimal macOS .app bundle that opens a URL."""
385
+ bundle_root = WEBAPP_DIR / f"{app_name}.app"
386
+ if bundle_root.exists():
387
+ shutil.rmtree(bundle_root)
388
+
389
+ contents = bundle_root / "Contents"
390
+ macos = contents / "MacOS"
391
+ resources = contents / "Resources"
392
+ macos.mkdir(parents=True)
393
+ resources.mkdir(parents=True)
394
+
395
+ # Info.plist
396
+ plist = {
397
+ "CFBundleDevelopmentRegion": "en",
398
+ "CFBundleExecutable": app_name.replace(" ", ""),
399
+ "CFBundleIdentifier": bundle_id,
400
+ "CFBundleInfoDictionaryVersion": "6.0",
401
+ "CFBundleName": app_name,
402
+ "CFBundlePackageType": "APPL",
403
+ "CFBundleShortVersionString": version,
404
+ "CFBundleVersion": version,
405
+ "LSMinimumSystemVersion": "10.15",
406
+ "LSUIElement": False,
407
+ }
408
+ with open(contents / "Info.plist", "wb") as f:
409
+ plistlib.dump(plist, f)
410
+
411
+ # Wrapper script that opens URL
412
+ script_path = macos / app_name.replace(" ", "")
413
+ script_content = f'''#!/bin/bash
414
+ # Auto-generated web app wrapper
415
+ URL="{url}"
416
+ if command -v open >/dev/null 2>&1; then
417
+ open "$URL"
418
+ else
419
+ # Fallback for Linux testing
420
+ xdg-open "$URL" 2>/dev/null || python3 -m webbrowser "$URL"
421
+ fi
422
+ '''
423
+ script_path.write_text(script_content)
424
+ script_path.chmod(0o755)
425
+
426
+ # Create a local HTML file as backup/embedded view
427
+ html_path = resources / "index.html"
428
+ html_path.write_text(f'''<!DOCTYPE html>
429
+ <html><head><meta charset="utf-8"><title>{app_name}</title>
430
+ <style>body{{margin:0;height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;font-family:sans-serif;background:#0a0a0f;color:#e2e2e8}}iframe{{width:95%;height:85%;border:1px solid #333;border-radius:8px}}</style></head>
431
+ <body><h2>{app_name}</h2><p>Loading {url}...</p><iframe src="{url}" sandbox="allow-scripts allow-same-origin allow-popups allow-forms"></iframe></body></html>
432
+ ''')
433
+
434
+ return bundle_root
435
+
436
+
437
+ def _create_dmg_from_app(app_bundle: Path, output_name: str) -> Path | None:
438
+ """Best-effort DMG creation. Returns path to DMG or None."""
439
+ dmg_path = WEBAPP_DIR / f"{output_name}.dmg"
440
+
441
+ # Try genisoimage + dmg if available (Linux)
442
+ try:
443
+ iso_path = WEBAPP_DIR / f"{output_name}.iso"
444
+ result = subprocess.run(
445
+ ["genisoimage", "-D", "-V", output_name, "-no-pad", "-r", "-apple",
446
+ "-o", str(iso_path), str(app_bundle)],
447
+ capture_output=True, text=True, timeout=30,
448
+ )
449
+ if result.returncode == 0:
450
+ # Try converting ISO to DMG
451
+ dmg_result = subprocess.run(
452
+ ["dmg", "iso", str(iso_path), str(dmg_path)],
453
+ capture_output=True, text=True, timeout=30,
454
+ )
455
+ if dmg_result.returncode == 0 and dmg_path.exists():
456
+ iso_path.unlink(missing_ok=True)
457
+ return dmg_path
458
+ except FileNotFoundError:
459
+ pass
460
+ except Exception:
461
+ pass
462
+
463
+ # Fallback: create a ZIP that user can extract on Mac and run hdiutil
464
+ zip_path = WEBAPP_DIR / f"{output_name}.zip"
465
+ shutil.make_archive(
466
+ base_name=str(WEBAPP_DIR / output_name),
467
+ format="zip",
468
+ root_dir=str(app_bundle.parent),
469
+ base_dir=app_bundle.name,
470
+ )
471
+ if zip_path.exists():
472
+ return zip_path
473
+
474
+ return None
475
+
476
+
477
+ @app.post("/api/create-webapp")
478
+ async def create_webapp(request: Request) -> JSONResponse:
479
+ """Create a macOS app bundle + DMG from a URL."""
480
+ try:
481
+ data = await request.json()
482
+ except Exception:
483
+ return JSONResponse({"error": "Invalid JSON"}, status_code=400)
484
+
485
+ url = data.get("url", "").strip()
486
+ app_name = data.get("app_name", "WebApp").strip()
487
+ bundle_id = data.get("bundle_id", "app.localspace.webapp").strip()
488
+ version = data.get("version", "1.0.0").strip()
489
+
490
+ if not url:
491
+ return JSONResponse({"error": "URL is required"}, status_code=400)
492
+ if not app_name:
493
+ return JSONResponse({"error": "App name is required"}, status_code=400)
494
+
495
+ # Sanitize
496
+ app_name_safe = re.sub(r'[^a-zA-Z0-9 ]+', '', app_name).strip()
497
+ if not app_name_safe:
498
+ app_name_safe = "WebApp"
499
+
500
+ try:
501
+ bundle = _create_app_bundle(url, app_name_safe, bundle_id, version)
502
+ pkg = _create_dmg_from_app(bundle, app_name_safe.replace(" ", "-"))
503
+
504
+ if pkg is None:
505
+ return JSONResponse({"error": "Failed to create package"}, status_code=500)
506
+
507
+ pkg_id = str(uuid.uuid4())
508
+ dest = UPLOAD_DIR / f"{pkg_id}{pkg.suffix}"
509
+ shutil.copy2(pkg, dest)
510
+
511
+ entry = {
512
+ "app_id": pkg_id,
513
+ "slug": _slugify(app_name_safe),
514
+ "filename": dest.name,
515
+ "size": dest.stat().st_size,
516
+ "sha256": _hash_file(dest),
517
+ "download_url": f"/api/download/{pkg_id}",
518
+ "source_url": url,
519
+ "app_name": app_name_safe,
520
+ "bundle_id": bundle_id,
521
+ "version": version,
522
+ "is_webapp": True,
523
+ "created_at": time.time(),
524
+ }
525
+ _store[pkg_id] = entry
526
+ _save_db()
527
+
528
+ return JSONResponse({
529
+ "app": entry,
530
+ "message": f"'{app_name_safe}' packaged. Download and extract on macOS, then run 'hdiutil create -srcfolder {app_name_safe}.app {app_name_safe}.dmg' to convert to DMG." if pkg.suffix == ".zip" else f"'{app_name_safe}' DMG created.",
531
+ }, status_code=201)
532
+ except Exception as exc:
533
+ return JSONResponse({"error": str(exc)}, status_code=500)
534
+
535
+
536
  @app.delete("/api/apps/{app_id}")
537
  def delete_app(app_id: str) -> JSONResponse:
538
  entry = _store.pop(app_id, None)
539
  if entry:
540
+ dmg = UPLOAD_DIR / f"{app_id}.dmg"
541
+ if dmg.exists():
542
+ dmg.unlink()
543
+ extracted = EXTRACT_DIR / app_id
544
+ if extracted.exists():
545
+ shutil.rmtree(extracted)
546
  _save_db()
547
  return JSONResponse({"ok": True})