mirror of
https://github.com/thunderheronight/splat-workshop.git
synced 2026-10-17 14:01:33 +02:00
Versions: take a version off the Frame (its gallery folder and backup copy, nothing else)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7e64f039d6
commit
dfaa6a4a1c
4 files changed
+96
No files matched your search
@@ -44,6 +44,7 @@ STATE = STATE_DIR / "state.json"
|
||||
FRAME = CFG.frame_host
|
||||
FRAME_VIDEOS = "Videos/ArcturusVisionCamera"
|
||||
FRAME_SCENES = ".local/share/Steam/steamapps/common/ArcturusVisionGaussianXR/Scenes"
|
||||
FRAME_BACKUP = "Documents/GaussianSplats" # to-frame.sh keeps a copy of every scene here
|
||||
SSH = ["ssh", "-o", "ConnectTimeout=4", "-o", "BatchMode=yes", FRAME]
|
||||
HOSTNAME = socket.gethostname().lower()
|
||||
# Names the server may be reached under (protects against DNS rebinding); IP addresses always work
|
||||
@@ -818,6 +819,40 @@ def start_versions(p: dict):
|
||||
return None
|
||||
|
||||
|
||||
def remove_from_frame(slug: str, version: str):
|
||||
"""Take a version off the Frame: its gallery folder and the backup copy. Only a folder this version
|
||||
got from Splat Workshop (remembered, or named exactly <NN>_<scene>-<version>) – never anything else."""
|
||||
if not FRAME:
|
||||
return err("err.no_frame")
|
||||
d = scene_dir(slug) if slug and not slug.startswith(".") else None
|
||||
if not d or not version or version.startswith(".") or Path(version).name != version:
|
||||
return err("err.no_version")
|
||||
meta = read_meta(slug)
|
||||
folders = meta.get("version_folders") or {}
|
||||
folder = folders.get(version)
|
||||
fr = frame_listing(force=True)
|
||||
if fr is None:
|
||||
return err("err.frame_offline")
|
||||
if not folder:
|
||||
folder = next((x for x in fr["scenes"] if re.fullmatch(r"\d\d_" + re.escape(f"{slug}-{version}"), x)), None)
|
||||
if not folder or not re.fullmatch(r"\d\d_[A-Za-z0-9._-]+", folder) or folder not in fr["scenes"]:
|
||||
return err("err.not_on_frame_version")
|
||||
if (u := uploads.get(f"{slug}#{version}")) and u["proc"].poll() is None:
|
||||
return err("err.upload_busy")
|
||||
try:
|
||||
r = subprocess.run(SSH + [f"rm -rf -- {FRAME_SCENES}/{shq(folder)} {FRAME_BACKUP}/{shq(folder)}"],
|
||||
capture_output=True, text=True, timeout=30)
|
||||
except subprocess.TimeoutExpired:
|
||||
return err("err.frame_offline")
|
||||
if r.returncode != 0:
|
||||
return err("err.frame_failed", detail=(r.stderr.strip().splitlines() or [""])[-1])
|
||||
folders.pop(version, None)
|
||||
write_meta(slug, version_folders=folders)
|
||||
uploads.pop(f"{slug}#{version}", None)
|
||||
frame_cache["t"] = 0
|
||||
return None
|
||||
|
||||
|
||||
def delete_version(slug: str, key: str):
|
||||
d = scene_dir(slug) if slug and not slug.startswith(".") else None
|
||||
vdir = d / "versions" / Path(key).name if d and key and not key.startswith(".") else None
|
||||
@@ -1747,6 +1782,8 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
e = start_upload(p.get("slug", ""), p.get("version", ""))
|
||||
elif path == "/api/versions":
|
||||
e = start_versions(p)
|
||||
elif path == "/api/frame/remove":
|
||||
e = remove_from_frame(p.get("slug", ""), p.get("version", ""))
|
||||
elif path == "/api/version/delete":
|
||||
e = delete_version(p.get("slug", ""), p.get("version", ""))
|
||||
elif path == "/api/pick":
|
||||
|
||||
@@ -132,3 +132,46 @@ def test_one_job_at_a_time():
|
||||
assert server.start_job({"path": str(rec), "name": "Other", "quality": "fast", "upload": False}) is None
|
||||
time.sleep(1)
|
||||
assert server.start_versions({"slug": "Room", "sizes": "10000"})["code"] == "err.job_busy"
|
||||
|
||||
|
||||
def upload(api, slug, key):
|
||||
assert api.post("/api/upload", {"slug": slug, "version": key})[0] == 200
|
||||
end = time.time() + 60
|
||||
while (u := server.upload_status(f"{slug}#{key}")) and u["running"] and time.time() < end:
|
||||
time.sleep(0.2)
|
||||
assert server.upload_status(f"{slug}#{key}")["ok"]
|
||||
|
||||
|
||||
def test_taking_a_version_off_the_frame_removes_only_that(api, frame):
|
||||
room()
|
||||
make("Room", "10000,20000")
|
||||
upload(api, "Room", "0.01m")
|
||||
upload(api, "Room", "0.02m")
|
||||
scenes, backup = frame / server.FRAME_SCENES, frame / server.FRAME_BACKUP
|
||||
(scenes / "04_BurtLakeHouse").mkdir() # an example scene of Gaussian XR
|
||||
(scenes / "30_Room-0.01m-old").mkdir() # someone else's folder with a similar name
|
||||
assert api.post("/api/frame/remove", {"slug": "Room", "version": "0.01m"})[0] == 200
|
||||
assert sorted(p.name for p in scenes.iterdir()) == ["04_BurtLakeHouse", "21_Room-0.02m", "30_Room-0.01m-old"]
|
||||
assert not (backup / "20_Room-0.01m").exists() and (backup / "21_Room-0.02m").exists()
|
||||
assert "0.01m" not in json.loads((RUNS / "Room.ui.json").read_text())["version_folders"]
|
||||
v = {x["key"]: x for x in server.versions_of("Room")}
|
||||
assert v["0.01m"]["on_frame"] is False and v["0.02m"]["on_frame"] # still on the PC, off the Frame
|
||||
status, r = api.post("/api/frame/remove", {"slug": "Room", "version": "0.01m"})
|
||||
assert status == 400 and r["error"]["code"] == "err.not_on_frame_version"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("version", ["", "..", "../0.02m", ".hidden"])
|
||||
def test_taking_off_the_frame_only_with_a_proper_version(api, frame, version):
|
||||
room()
|
||||
assert api.post("/api/frame/remove", {"slug": "Room", "version": version})[0] == 400
|
||||
|
||||
|
||||
def test_taking_off_a_frame_that_is_off(api, frame):
|
||||
room()
|
||||
make("Room", "10000")
|
||||
upload(api, "Room", "0.01m")
|
||||
os.environ["FAKE_FRAME_OFF"] = "1"
|
||||
status, r = api.post("/api/frame/remove", {"slug": "Room", "version": "0.01m"})
|
||||
assert status == 400 and r["error"]["code"] == "err.frame_offline"
|
||||
os.environ.pop("FAKE_FRAME_OFF")
|
||||
assert (frame / server.FRAME_SCENES / "20_Room-0.01m").exists()
|
||||
@@ -201,6 +201,10 @@ const I18N = {
|
||||
"ver.confirm": "Move the version “{name}” ({size}) to the trash? A copy on the Frame is not touched.",
|
||||
"job.sub.versions": "{n} versions · for {time}", "err.no_version": "This version does not exist",
|
||||
"err.no_versions": "Pick at least one size",
|
||||
"ver.unframe": "Off the Frame", "ver.unframe.title": "Remove this version from the Frame's gallery (it stays on the PC)",
|
||||
"ver.unframe.confirm": "Remove “{name}” from the Frame? Its folder in the gallery and the backup copy on the Frame are deleted; the version stays on this PC.",
|
||||
"ver.unframed": "“{name}” removed from the Frame", "err.not_on_frame_version": "This version is not on the Frame",
|
||||
"err.frame_offline": "The Frame is not reachable", "err.frame_failed": "The Frame refused",
|
||||
"err.plan_failed": "The floor plan could not be drawn", "sc.replacing": "Being computed again – stays until the new one is done", "err.plan_busy": "Other floor plans are being drawn right now, try again in a moment",
|
||||
"plan.measure": "📏 Measure", "plan.measure.title": "Measure distances in the floor plan",
|
||||
"plan.measure.hint": "Click to set points · right-click removes the last one · horizontal distances, about ±5 cm",
|
||||
@@ -411,6 +415,10 @@ const I18N = {
|
||||
"ver.confirm": "Fassung „{name}“ ({size}) in den Papierkorb verschieben? Eine Kopie auf der Frame bleibt unberührt.",
|
||||
"job.sub.versions": "{n} Fassungen · seit {time}", "err.no_version": "Diese Fassung gibt es nicht",
|
||||
"err.no_versions": "Mindestens eine Größe wählen",
|
||||
"ver.unframe": "Von der Frame nehmen", "ver.unframe.title": "Diese Fassung aus der Galerie der Frame entfernen (auf dem PC bleibt sie)",
|
||||
"ver.unframe.confirm": "„{name}“ von der Frame nehmen? Ihr Ordner in der Galerie und die Sicherungskopie auf der Frame werden gelöscht; auf dem PC bleibt die Fassung.",
|
||||
"ver.unframed": "„{name}“ von der Frame genommen", "err.not_on_frame_version": "Diese Fassung liegt nicht auf der Frame",
|
||||
"err.frame_offline": "Die Frame ist nicht erreichbar", "err.frame_failed": "Die Frame hat abgelehnt",
|
||||
"err.plan_failed": "Der Grundriss konnte nicht gezeichnet werden", "sc.replacing": "Wird neu gerechnet – bleibt, bis die neue fertig ist", "err.plan_busy": "Gerade werden andere Grundrisse gezeichnet, gleich noch einmal versuchen",
|
||||
"plan.measure": "📏 Messen", "plan.measure.title": "Abstände im Grundriss messen",
|
||||
"plan.measure.hint": "Klicken setzt Punkte · Rechtsklick nimmt den letzten zurück · waagerechte Abstände, etwa ±5 cm genau",
|
||||
|
||||
@@ -1102,6 +1102,7 @@ async function drawVersions(slug) {
|
||||
<span class="muted" style="font-size:12.5px">${tr("million", {n: num(v.splats / 1e6, 2)})} · ${mb(v.size)}${v.stale ? " · " + tr("ver.stale") : ""}</span>
|
||||
<span class="sp"></span>${frameState({upload: v.upload, on_frame: v.on_frame})}
|
||||
${S.frameConfigured ? `<button class="small" data-vup="${esc(v.key)}" ${v.upload && v.upload.running ? "disabled" : ""}>${tr(v.on_frame ? "sc.reupload" : "sc.toFrame")}</button>` : ""}
|
||||
${v.on_frame ? `<button class="small" data-vunframe="${esc(v.key)}" title="${tr("ver.unframe.title")}">${tr("ver.unframe")}</button>` : ""}
|
||||
<button class="small" data-vdel="${esc(v.key)}" title="${tr("ver.delete")}" aria-label="${tr("ver.delete")}">${tr("sc.delete")}</button>
|
||||
</div>`).join("") : `<p class="faint" style="font-size:13px;margin:0">${tr("ver.none")}</p>`;
|
||||
$("#verList").querySelectorAll("[data-vup]").forEach(b => b.onclick = async () => {
|
||||
@@ -1109,6 +1110,13 @@ async function drawVersions(slug) {
|
||||
if (!res.ok) { toast(trErr(res.error)); return; }
|
||||
toast(tr("sc.uploadStarted")); setTimeout(() => drawVersions(slug), 1500);
|
||||
});
|
||||
$("#verList").querySelectorAll("[data-vunframe]").forEach(b => b.onclick = async () => {
|
||||
const v = list.find(x => x.key === b.dataset.vunframe);
|
||||
if (!confirm(tr("ver.unframe.confirm", {name: v.label}))) return;
|
||||
const res = await api("/api/frame/remove", {slug, version: v.key});
|
||||
if (!res.ok) { toast(trErr(res.error)); return; }
|
||||
toast(tr("ver.unframed", {name: v.label})); drawVersions(slug);
|
||||
});
|
||||
$("#verList").querySelectorAll("[data-vdel]").forEach(b => b.onclick = async () => {
|
||||
const v = list.find(x => x.key === b.dataset.vdel);
|
||||
if (!confirm(tr("ver.confirm", {name: v.label, size: mb(v.size)}))) return;
|
||||
|
||||
Reference in new issue
Block a user