-
Notifications
You must be signed in to change notification settings - Fork 19
Expand file tree
/
Copy pathfs.py
More file actions
258 lines (195 loc) · 7.92 KB
/
fs.py
File metadata and controls
258 lines (195 loc) · 7.92 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
"""
This module provides an API for mounting directories from the user's local
filesystem into the browser's virtual filesystem. This means Python code,
running in the browser, can read and write files on the user's local machine.
!!! warning
**This API only works in Chromium-based browsers** (Chrome, Edge,
Vivaldi, Brave, etc.) that support the
[File System Access API](https://wicg.github.io/file-system-access/).
The module maintains a `mounted` dictionary that tracks all currently mounted
paths and their associated filesystem handles.
```python
from pyscript import fs, document, when
# Mount a local directory to the `/local` mount point in the browser's
# virtual filesystem (may prompt user for permission).
await fs.mount("/local")
# Alternatively, mount on a button click event. This is important because
# if the call to `fs.mount` happens after a click or other transient event,
# the confirmation dialog will not be shown.
@when("click", "#mount-button")
async def handler(event):
await fs.mount("/another_dir")
# Work with files in the mounted directory as usual.
with open("/local/example.txt", "w") as f:
f.write("Hello from PyScript!")
# Ensure changes are written to local filesystem.
await fs.sync("/local")
# Clean up when done.
await fs.unmount("/local")
```
"""
import js
from _pyscript import fs as _fs, interpreter
from pyscript import window
from pyscript.ffi import to_js
from pyscript.context import RUNNING_IN_WORKER
# Worker-specific imports.
if RUNNING_IN_WORKER:
from pyscript.context import sync as sync_with_worker
from polyscript import IDBMap
mounted = {}
"""Global dictionary tracking mounted paths and their filesystem handles."""
async def _check_permission(details):
"""
Check if permission has been granted for a filesystem handler. Returns
the handler if permission is granted, otherwise None.
"""
handler = details.handler
options = details.options
permission = await handler.queryPermission(options)
return handler if permission == "granted" else None
async def mount(path, mode="readwrite", root="", id="pyscript"):
"""
Mount a directory from the local filesystem to the virtual filesystem
at the specified `path` mount point. The `mode` can be "readwrite" or
"read" to specify access level. The `root` parameter provides a hint
for the file picker starting location. The `id` parameter allows multiple
distinct mounts at the same path.
On first use, the browser will prompt the user to select a directory
and grant permission.
```python
from pyscript import fs
# Basic mount with default settings.
await fs.mount("/local")
# Mount with read-only access.
await fs.mount("/readonly", mode="read")
# Mount with a hint to start in Downloads folder.
await fs.mount("/downloads", root="downloads")
# Mount with a custom ID to track different directories.
await fs.mount("/project", id="my-project")
```
If called during a user interaction (like a button click), the
permission dialog may be skipped if permission was previously granted.
"""
js.console.warn("experimental pyscript.fs ⚠️")
# Check if path is already mounted with a different ID.
mount_key = f"{path}@{id}"
if path in mounted:
# Path already mounted - check if it's the same ID.
for existing_key in mounted.keys():
if existing_key.startswith(f"{path}@") and existing_key != mount_key:
raise ValueError(
f"Path '{path}' is already mounted with a different ID. "
f"Unmount it first or use a different path."
)
details = None
handler = None
options = {"id": id, "mode": mode}
if root != "":
options["startIn"] = root
if RUNNING_IN_WORKER:
fs_handler = sync_with_worker.storeFSHandler(mount_key, to_js(options))
# Handle both async and SharedArrayBuffer use cases.
if isinstance(fs_handler, bool):
success = fs_handler
else:
success = await fs_handler
if success:
idbm = IDBMap.new(_fs.NAMESPACE)
details = await idbm.get(mount_key)
handler = await _check_permission(details)
if handler is None:
# Force await in either async or sync scenario.
await js.Promise.resolve(sync_with_worker.getFSHandler(details.options))
handler = details.handler
else:
raise RuntimeError(_fs.ERROR)
else:
success = await _fs.idb.has(mount_key)
if success:
details = await _fs.idb.get(mount_key)
handler = await _check_permission(details)
if handler is None:
handler = await _fs.getFileSystemDirectoryHandle(details.options)
else:
js_options = to_js(options)
handler = await _fs.getFileSystemDirectoryHandle(js_options)
details = {"handler": handler, "options": js_options}
await _fs.idb.set(mount_key, to_js(details))
mounted[path] = await interpreter.mountNativeFS(path, handler)
async def sync(path):
"""
Synchronise the virtual and local filesystems for a mounted `path`.
This ensures all changes made in the browser's virtual filesystem are
written to the user's local filesystem, and vice versa.
```python
from pyscript import fs
await fs.mount("/local")
# Make changes to files.
with open("/local/data.txt", "w") as f:
f.write("Important data")
# Ensure changes are written to local disk.
await fs.sync("/local")
```
This is automatically called by unmount(), but you may want to call
it explicitly to ensure data persistence at specific points.
"""
if path not in mounted:
raise KeyError(
f"Path '{path}' is not mounted. " f"Use fs.mount() to mount it first."
)
await mounted[path].syncfs()
async def unmount(path):
"""
Unmount a directory, specified by `path`, from the virtual filesystem.
This synchronises any pending changes and then removes the mount point,
freeing up memory. The `path` can be reused for mounting a different
directory.
```python
from pyscript import fs
await fs.mount("/local")
# ... work with files ...
await fs.unmount("/local")
# Path can now be reused.
await fs.mount("/local", id="different-folder")
```
This automatically calls `sync()` before unmounting to ensure no data
is lost.
"""
if path not in mounted:
raise KeyError(f"Path '{path}' is not mounted. Cannot unmount.")
await sync(path)
interpreter._module.FS.unmount(path)
del mounted[path]
async def revoke(path, id="pyscript"):
"""
Revoke filesystem access permission and unmount for a given
`path` and `id` combination.
This removes the stored permission for accessing the user's local
filesystem at the specified path and ID. Unlike `unmount()`, which only
removes the mount point, `revoke()` also clears the permission so the
user will be prompted again on next mount.
```python
from pyscript import fs
await fs.mount("/local", id="my-app")
# ... work with files ...
# Revoke permission (user will be prompted again next time).
revoked = await fs.revoke("/local", id="my-app")
if revoked:
print("Permission revoked successfully")
```
After revoking, the user will need to grant permission again and
select a directory when `mount()` is called next time.
"""
mount_key = f"{path}@{id}"
if RUNNING_IN_WORKER:
handler_exists = sync_with_worker.deleteFSHandler(mount_key)
else:
handler_exists = await _fs.idb.has(mount_key)
if handler_exists:
handler_exists = await _fs.idb.delete(mount_key)
if handler_exists:
interpreter._module.FS.unmount(path)
if path in mounted:
del mounted[path]
return handler_exists