From a75443d8bb6bb14b433b328666711ce8fab3fccf Mon Sep 17 00:00:00 2001 From: Edouard Durand Date: Sun, 27 Sep 2026 00:42:03 +0000 Subject: [PATCH] Files tab refreshes when workflows change The server tells everyone who can open the folder when a workflow is saved, moved or deleted, or folders are created, renamed, moved, deleted or have their access changed; the Files tab reloads the folder on screen. --- CHANGELOG.md | 1 + rigshare_core/hub.py | 10 ++++++++++ rigshare_core/routes.py | 11 ++++++++++- tests/python/test_server.py | 24 ++++++++++++++++++++++++ web/rigshare/files.js | 9 ++++++++- 5 files changed, 53 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2044332..b39a883 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- The Files tab didn't show a workflow you had just saved into the folder on screen until you left the folder and came back. It now refreshes whenever workflows are saved, moved, renamed or deleted, or folders change, including by other people (each person is only told about folders they can open) - When the session had ended (logged out elsewhere, expired, cookie cleared), a ComfyUI page already open or restored from the browser cache showed "API key missing" errors instead of going back to the login screen. The page is no longer cached, and it returns to the login screen as soon as a request says login is required ## 1.2.2 (2026-09-26) diff --git a/rigshare_core/hub.py b/rigshare_core/hub.py index f94e3da..b9e169b 100644 --- a/rigshare_core/hub.py +++ b/rigshare_core/hub.py @@ -13,6 +13,7 @@ import asyncio import hashlib import json import logging +import posixpath import secrets import time from collections import OrderedDict @@ -365,6 +366,14 @@ class Hub: return True return self.workspace.role(rel, client.kind, key, client.perms) is not None + async def files_changed(self, *paths): + """Workflow files changed in these folders (userdata paths): tell whoever can + open one of them, so their Files tab reloads.""" + folders = {posixpath.dirname(p) if p.endswith(".json") else p for p in paths if p} + for client in list(self.clients.values()): + if client.kind and any(self.path_access(f, client) for f in folders): + await self.send(client, {"type": "files_changed"}) + def who_sees(self, path): """Connected clients that may open ``path``; call before moving it.""" return [c for c in self.clients.values() if c.kind and self.path_access(path, c)] @@ -382,6 +391,7 @@ class Hub: key = (renamed or {}).get(client.key, client.key) if client.kind == "user" else client.key notes.append((client, self.path_access(new, client, key=key))) self.rekey_rooms("file:" + old, "file:" + new) + await self.files_changed(posixpath.dirname(old), posixpath.dirname(new), new) for client, follows in notes: await self.send(client, {"type": "path_moved", "from": old, "to": new} if follows else {"type": "path_closed", "from": old}) diff --git a/rigshare_core/routes.py b/rigshare_core/routes.py index c0ce2dd..0acbdac 100644 --- a/rigshare_core/routes.py +++ b/rigshare_core/routes.py @@ -7,6 +7,7 @@ import ipaddress import json import logging import os +import posixpath import time from urllib.parse import unquote from collections import defaultdict @@ -417,7 +418,10 @@ def setup(server, store, hub): async def ws_mkdir(request): body = await request.json() err, path = ws_call(lambda: ws().make_dir(body.get("parent", ""), body.get("name"), *who(request))) - return err or web.json_response({"path": path}) + if err: + return err + await hub.files_changed("workflows/" + posixpath.dirname(path)) + return web.json_response({"path": path}) @routes.post("/rigshare/api/workspace/rename-folder") async def ws_rename_dir(request): @@ -449,6 +453,7 @@ def setup(server, store, hub): if err: return err await hub.drop_rooms("file:workflows/" + path, display(request)) + await hub.files_changed("workflows/" + posixpath.dirname(path)) return web.json_response({"ok": True}) @routes.get("/rigshare/api/workspace/folder") @@ -473,6 +478,7 @@ def setup(server, store, hub): except ValueError as e: return error(str(e)) log.info(f"[RigShare] {key} created folder {folder}") + await hub.files_changed("workflows/shared") return web.json_response({"path": folder}) @routes.patch("/rigshare/api/workspace/folders") @@ -501,6 +507,7 @@ def setup(server, store, hub): except ValueError as e: return error(str(e)) await hub.refresh_rooms_access() + await hub.files_changed("workflows/shared") # the folder's access changed who sees it return web.json_response({"ok": True, "path": folder}) @routes.delete("/rigshare/api/workspace/folders") @@ -807,6 +814,7 @@ def setup(server, store, hub): # Every save of a workflow keeps a snapshot, named after when it was saved. if userdata and userdata[0] == "save" and getattr(response, "status", 0) == 200: snapshot_saved_file(request, userdata[1], await request.read()) + await hub.files_changed(userdata[1]) # Workflow listings only show what this person may open. if userdata and userdata[0] == "list" and not trusted(request.remote or "") and hub.workspace: response = filter_listing(request, response, userdata[1]) @@ -815,6 +823,7 @@ def setup(server, store, hub): await hub.path_moved(userdata[1], userdata[2]) if userdata and userdata[0] == "delete" and getattr(response, "status", 0) in (200, 204): await hub.drop_rooms("file:" + userdata[1], display(request)) + await hub.files_changed(userdata[1]) # Hide API templates: rewrite the template index ComfyUI serves. if (store.config.get("hide_api_templates") and request.method == "GET" and path.startswith("/templates/index") and path.endswith(".json")): diff --git a/tests/python/test_server.py b/tests/python/test_server.py index 13ded5b..80fbd6c 100644 --- a/tests/python/test_server.py +++ b/tests/python/test_server.py @@ -522,3 +522,27 @@ def test_ended_session_goes_back_to_login(run, tmp_path): r = await h.get(rig.url("/"), headers={"Accept": "text/html"}, allow_redirects=False) assert r.status == 302 and r.headers["Location"].startswith("/rigshare/login") run(scenario()) + + +def test_file_changes_reach_who_can_see_them(run): + async def scenario(): + async with rig_server() as rig: + await rig.setup_admin() + alice = await rig.add_user("alice", edit=True) + await rig.add_user("bob", edit=True) + a, b = await rig.ws("alice"), await rig.ws("bob") + h = rig.http + + async def changed(sock): + return any(m["type"] == "files_changed" for m in await sock.drain()) + + await h.post(rig.url(f"/api/userdata/{enc('workflows/users/alice/mine.json')}"), data=b'{"nodes": []}', headers=alice) + assert await changed(a) and not await changed(b), "a private save only tells its owner" + await h.post(rig.url(f"/api/userdata/{enc('workflows/shared/team.json')}"), data=b'{"nodes": []}', headers=alice) + assert await changed(a) and await changed(b) + await h.post(rig.url("/rigshare/api/workspace/mkdir"), json={"parent": "@shared", "name": "Crew"}, headers=alice) + assert await changed(b), "new shared folder" + await h.post(rig.url(f"/api/userdata/{enc('workflows/shared/team.json')}/move/{enc('workflows/shared/Crew/team.json')}"), headers=rig.headers["boss"]) + assert await changed(b), "moves" + await a.close(); await b.close() + run(scenario()) diff --git a/web/rigshare/files.js b/web/rigshare/files.js index dcfae9a..6ce1d8a 100644 --- a/web/rigshare/files.js +++ b/web/rigshare/files.js @@ -35,7 +35,14 @@ export class FilesBrowser { h("div", { class: "rs-fb-tools" }, this.search), this.list); // The sidebar browser lives as long as the page: keep its live dots current. - if (mode === "browse") client.on("presence", () => { if (this.listing && this.el.isConnected) this.renderList(); }); + if (mode === "browse") { + client.on("presence", () => { if (this.listing && this.el.isConnected) this.renderList(); }); + // Someone (maybe you, from the Save dialog) saved, moved or deleted a workflow: reload. + client.on("files_changed", () => { + clearTimeout(this.changedTimer); + this.changedTimer = setTimeout(() => { if (this.el.isConnected && !this.accessFor) this.reload(); }, 250); + }); + } this.setStart(start); }