From b4e9a3093c1c5702d1c7c0a52b7ba318b7a20f2c Mon Sep 17 00:00:00 2001 From: Andrea Giammarchi Date: Wed, 6 Aug 2025 14:40:52 +0200 Subject: [PATCH] Fix #2338 - Added explicit fs.revoke(path) (#2368) --- core/src/stdlib/pyscript.js | 2 +- core/src/stdlib/pyscript/fs.py | 22 ++++++++++++++++++++++ core/src/sync.js | 13 ++++++++++++- core/tests/manual/fs/index.py | 4 +++- core/types/sync.d.ts | 10 ++++++++-- 5 files changed, 46 insertions(+), 5 deletions(-) diff --git a/core/src/stdlib/pyscript.js b/core/src/stdlib/pyscript.js index 2e36a28f..e44ef958 100644 --- a/core/src/stdlib/pyscript.js +++ b/core/src/stdlib/pyscript.js @@ -7,7 +7,7 @@ export default { "fetch.py": "import json,js\nfrom pyscript.util import as_bytearray\nclass _Response:\n\tdef __init__(A,response):A._response=response\n\tdef __getattr__(A,attr):return getattr(A._response,attr)\n\tasync def arrayBuffer(B):\n\t\tA=await B._response.arrayBuffer()\n\t\tif hasattr(A,'to_py'):return A.to_py()\n\t\treturn memoryview(as_bytearray(A))\n\tasync def blob(A):return await A._response.blob()\n\tasync def bytearray(A):B=await A._response.arrayBuffer();return as_bytearray(B)\n\tasync def json(A):return json.loads(await A.text())\n\tasync def text(A):return await A._response.text()\nclass _DirectResponse:\n\t@staticmethod\n\tdef setup(promise,response):A=promise;A._response=_Response(response);return A._response\n\tdef __init__(B,promise):A=promise;B._promise=A;A._response=None;A.arrayBuffer=B.arrayBuffer;A.blob=B.blob;A.bytearray=B.bytearray;A.json=B.json;A.text=B.text\n\tasync def _response(A):\n\t\tif not A._promise._response:await A._promise\n\t\treturn A._promise._response\n\tasync def arrayBuffer(A):B=await A._response();return await B.arrayBuffer()\n\tasync def blob(A):B=await A._response();return await B.blob()\n\tasync def bytearray(A):B=await A._response();return await B.bytearray()\n\tasync def json(A):B=await A._response();return await B.json()\n\tasync def text(A):B=await A._response();return await B.text()\ndef fetch(url,**B):C=js.JSON.parse(json.dumps(B));D=lambda response,*B:_DirectResponse.setup(A,response);A=js.fetch(url,C).then(D);_DirectResponse(A);return A", "ffi.py": "try:\n\timport js;from pyodide.ffi import create_proxy as _cp,to_js as _py_tjs;from_entries=js.Object.fromEntries\n\tdef _tjs(value,**A):\n\t\tB='dict_converter'\n\t\tif not hasattr(A,B):A[B]=from_entries\n\t\treturn _py_tjs(value,**A)\nexcept:from jsffi import create_proxy as _cp;from jsffi import to_js as _tjs\ncreate_proxy=_cp\nto_js=_tjs\ntry:\n\tfrom polyscript import ffi as _ffi;direct=_ffi.direct;gather=_ffi.gather;query=_ffi.query\n\tdef assign(source,*B):\n\t\tA=source\n\t\tfor C in B:_ffi.assign(A,to_js(C))\n\t\treturn A\nexcept:\n\timport js;_assign=js.Object.assign;direct=lambda source:source\n\tdef assign(source,*B):\n\t\tA=source\n\t\tfor C in B:_assign(A,to_js(C))\n\t\treturn A", "flatted.py": "import json as _json\nclass _Known:\n\tdef __init__(A):A.key=[];A.value=[]\nclass _String:\n\tdef __init__(A,value):A.value=value\ndef _array_keys(value):\n\tA=[];B=0\n\tfor C in value:A.append(B);B+=1\n\treturn A\ndef _object_keys(value):\n\tA=[]\n\tfor B in value:A.append(B)\n\treturn A\ndef _is_array(value):return isinstance(value,(list,tuple))\ndef _is_object(value):return isinstance(value,dict)\ndef _is_string(value):return isinstance(value,str)\ndef _index(known,input,value):B=value;A=known;input.append(B);C=str(len(input)-1);A.key.append(B);A.value.append(C);return C\ndef _loop(keys,input,known,output):\n\tA=output\n\tfor B in keys:\n\t\tC=A[B]\n\t\tif isinstance(C,_String):_ref(B,input[int(C.value)],input,known,A)\n\treturn A\ndef _ref(key,value,input,known,output):\n\tB=known;A=value\n\tif _is_array(A)and A not in B:B.append(A);A=_loop(_array_keys(A),input,B,A)\n\telif _is_object(A)and A not in B:B.append(A);A=_loop(_object_keys(A),input,B,A)\n\toutput[key]=A\ndef _relate(known,input,value):\n\tB=known;A=value\n\tif _is_string(A)or _is_array(A)or _is_object(A):\n\t\ttry:return B.value[B.key.index(A)]\n\t\texcept:return _index(B,input,A)\n\treturn A\ndef _transform(known,input,value):\n\tB=known;A=value\n\tif _is_array(A):\n\t\tC=[]\n\t\tfor F in A:C.append(_relate(B,input,F))\n\t\treturn C\n\tif _is_object(A):\n\t\tD={}\n\t\tfor E in A:D[E]=_relate(B,input,A[E])\n\t\treturn D\n\treturn A\ndef _wrap(value):\n\tA=value\n\tif _is_string(A):return _String(A)\n\tif _is_array(A):\n\t\tB=0\n\t\tfor D in A:A[B]=_wrap(D);B+=1\n\telif _is_object(A):\n\t\tfor C in A:A[C]=_wrap(A[C])\n\treturn A\ndef parse(value,*C,**D):\n\tA=value;E=_json.loads(A,*C,**D);B=[]\n\tfor A in E:B.append(_wrap(A))\n\tinput=[]\n\tfor A in B:\n\t\tif isinstance(A,_String):input.append(A.value)\n\t\telse:input.append(A)\n\tA=input[0]\n\tif _is_array(A):return _loop(_array_keys(A),input,[A],A)\n\tif _is_object(A):return _loop(_object_keys(A),input,[A],A)\n\treturn A\ndef stringify(value,*D,**E):\n\tB=_Known();input=[];C=[];A=int(_index(B,input,value))\n\twhile A Promise.all(urls.map((url) => import(url)))')()\n\texcept:message='Unable to use `window` or `document` -> https://docs.pyscript.net/latest/faq/#sharedarraybuffer';globalThis.console.warn(message);window=NotSupported('pyscript.window',message);document=NotSupported('pyscript.document',message);js_import=None\n\tsync=polyscript.xworker.sync\n\tdef current_target():return polyscript.target\nelse:\n\timport _pyscript;from _pyscript import PyWorker as _PyWorker,js_import;from pyscript.ffi import to_js\n\tdef PyWorker(url,**A):return _PyWorker(url,to_js(A))\n\twindow=globalThis;document=globalThis.document;sync=NotSupported('pyscript.sync','pyscript.sync works only when running in a worker')\n\tdef current_target():return _pyscript.target", "media.py": "from pyscript import window\nfrom pyscript.ffi import to_js\nclass Device:\n\tdef __init__(A,device):A._dom_element=device\n\t@property\n\tdef id(self):return self._dom_element.deviceId\n\t@property\n\tdef group(self):return self._dom_element.groupId\n\t@property\n\tdef kind(self):return self._dom_element.kind\n\t@property\n\tdef label(self):return self._dom_element.label\n\tdef __getitem__(A,key):return getattr(A,key)\n\t@classmethod\n\tasync def load(E,audio=False,video=True):\n\t\tC='video';B=video;A={};A['audio']=audio\n\t\tif isinstance(B,bool):A[C]=B\n\t\telse:\n\t\t\tA[C]={}\n\t\t\tfor D in B:A[C][D]=B[D]\n\t\treturn await window.navigator.mediaDevices.getUserMedia(to_js(A))\n\tasync def get_stream(A):B=A.kind.replace('input','').replace('output','');C={B:{'deviceId':{'exact':A.id}}};return await A.load(**C)\nasync def list_devices():return[Device(A)for A in await window.navigator.mediaDevices.enumerateDevices()]", "storage.py": "_C='memoryview'\n_B='bytearray'\n_A='generic'\nfrom polyscript import storage as _storage\nfrom pyscript.flatted import parse as _parse\nfrom pyscript.flatted import stringify as _stringify\ndef _to_idb(value):\n\tA=value\n\tif A is None:return _stringify(['null',0])\n\tif isinstance(A,(bool,float,int,str,list,dict,tuple)):return _stringify([_A,A])\n\tif isinstance(A,bytearray):return _stringify([_B,list(A)])\n\tif isinstance(A,memoryview):return _stringify([_C,list(A)])\n\tB=f\"Unexpected value: {A}\";raise TypeError(B)\ndef _from_idb(value):\n\tC=value;A,B=_parse(C)\n\tif A=='null':return\n\tif A==_A:return B\n\tif A==_B:return bytearray(B)\n\tif A==_C:return memoryview(bytearray(B))\n\treturn C\nclass Storage(dict):\n\tdef __init__(B,store):A=store;super().__init__({A:_from_idb(B)for(A,B)in A.entries()});B.__store__=A\n\tdef __delitem__(A,attr):A.__store__.delete(attr);super().__delitem__(attr)\n\tdef __setitem__(B,attr,value):A=value;B.__store__.set(attr,_to_idb(A));super().__setitem__(attr,A)\n\tdef clear(A):A.__store__.clear();super().clear()\n\tasync def sync(A):await A.__store__.sync()\nasync def storage(name='',storage_class=Storage):\n\tif not name:A='The storage name must be defined';raise ValueError(A)\n\treturn storage_class(await _storage(f\"@pyscript/{name}\"))", diff --git a/core/src/stdlib/pyscript/fs.py b/core/src/stdlib/pyscript/fs.py index 6dd302f9..98b1f393 100644 --- a/core/src/stdlib/pyscript/fs.py +++ b/core/src/stdlib/pyscript/fs.py @@ -49,6 +49,28 @@ async def mount(path, mode="readwrite", root="", id="pyscript"): mounted[path] = await interpreter.mountNativeFS(path, handler) +async def revoke(path, id="pyscript"): + from _pyscript import fs, interpreter + from pyscript.magic_js import ( + RUNNING_IN_WORKER, + sync, + ) + + uid = f"{path}@{id}" + + if RUNNING_IN_WORKER: + had = sync.deleteFSHandler(uid) + else: + had = await fs.idb.has(uid) + if had: + had = await fs.idb.delete(uid) + + if had: + interpreter._module.FS.unmount(path) + + return had + + async def sync(path): await mounted[path].syncfs() diff --git a/core/src/sync.js b/core/src/sync.js index 10d73439..8e271535 100644 --- a/core/src/sync.js +++ b/core/src/sync.js @@ -16,7 +16,7 @@ export default { * Ask a user action via dialog and returns the directory handler once granted. * @param {string} uid * @param {{id?:string, mode?:"read"|"readwrite", hint?:"desktop"|"documents"|"downloads"|"music"|"pictures"|"videos"}} options - * @returns {boolean} + * @returns {Promise} */ async storeFSHandler(uid, options = {}) { if (await idb.has(uid)) return true; @@ -28,4 +28,15 @@ export default { () => false, ); }, + + /** + * Explicitly remove the unique identifier for the FS handler. + * @param {string} uid + * @returns {Promise} + */ + async deleteFSHandler(uid) { + const had = await idb.has(uid); + if (had) await idb.delete(uid); + return had; + }, }; diff --git a/core/tests/manual/fs/index.py b/core/tests/manual/fs/index.py index 5bff05bf..651d708b 100644 --- a/core/tests/manual/fs/index.py +++ b/core/tests/manual/fs/index.py @@ -19,6 +19,8 @@ if TEST == "implicit": await fs.sync("/persistent") + # await fs.revoke("/persistent") + elif not RUNNING_IN_WORKER: from pyscript import document @@ -39,7 +41,7 @@ elif not RUNNING_IN_WORKER: js.alert("unable to grant access") async def unmount(event): - await fs.unmount("/persistent") + await fs.revoke("/persistent") button.textContent = "mount" button.onclick = mount diff --git a/core/types/sync.d.ts b/core/types/sync.d.ts index 484122d3..15924c64 100644 --- a/core/types/sync.d.ts +++ b/core/types/sync.d.ts @@ -9,12 +9,18 @@ declare namespace _default { * Ask a user action via dialog and returns the directory handler once granted. * @param {string} uid * @param {{id?:string, mode?:"read"|"readwrite", hint?:"desktop"|"documents"|"downloads"|"music"|"pictures"|"videos"}} options - * @returns {boolean} + * @returns {Promise} */ function storeFSHandler(uid: string, options?: { id?: string; mode?: "read" | "readwrite"; hint?: "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos"; - }): boolean; + }): Promise; + /** + * Explicitly remove the unique identifier for the FS handler. + * @param {string} uid + * @returns {Promise} + */ + function deleteFSHandler(uid: string): Promise; } export default _default;