@statewalker/webrun-files-browser 0.10.1 → 0.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -150
- package/package.json +8 -4
package/README.md
CHANGED
|
@@ -1,210 +1,145 @@
|
|
|
1
1
|
# @statewalker/webrun-files-browser
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## What it is
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
`BrowserFilesApi`, an implementation of `FilesApi` from `@statewalker/webrun-files` over a
|
|
6
|
+
[File System Access API](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API)
|
|
7
|
+
directory handle, plus helpers to obtain one: a user-picked directory (with optional persistence
|
|
8
|
+
of the handle) or the Origin Private File System (OPFS).
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
## Why it exists
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
|
|
12
|
+
Web apps that edit local projects, or need private persistent storage, get a
|
|
13
|
+
`FileSystemDirectoryHandle` from the browser, whose API is handle-based, unordered and full of
|
|
14
|
+
permission states. This package turns the handle into the same `FilesApi` the rest of the code
|
|
15
|
+
already uses, and wraps the picker, permission and stale-handle checks a real app needs around it.
|
|
11
16
|
|
|
12
|
-
##
|
|
17
|
+
## How to use
|
|
13
18
|
|
|
14
19
|
```bash
|
|
15
|
-
|
|
20
|
+
pnpm add @statewalker/webrun-files-browser @statewalker/webrun-files
|
|
16
21
|
```
|
|
17
22
|
|
|
18
|
-
|
|
23
|
+
One entry point, `@statewalker/webrun-files-browser`: ESM (`dist/esm/index.js`), CommonJS
|
|
24
|
+
(`dist/cjs/index.cjs`), types (`dist/index.d.ts`); sources in `src/`. Browsers with the File
|
|
25
|
+
System Access API; requires a secure context (HTTPS or localhost).
|
|
19
26
|
|
|
20
|
-
|
|
27
|
+
| Export | What it gives |
|
|
28
|
+
| --- | --- |
|
|
29
|
+
| `new BrowserFilesApi({ rootHandle })` | A `FilesApi` over any `FileSystemDirectoryHandle` |
|
|
30
|
+
| `openBrowserFilesApi(options?)` | Reuses a stored handle or shows `showDirectoryPicker()`, checks permission and accessibility |
|
|
31
|
+
| `getOPFSFilesApi()` | A `BrowserFilesApi` over `navigator.storage.getDirectory()` |
|
|
32
|
+
| `verifyPermission(handle, readWrite = false)` | Queries, then requests, `read` or `readwrite` permission |
|
|
33
|
+
| `isHandlerAccessible(handle)` | Whether the handle still points at something readable |
|
|
34
|
+
|
|
35
|
+
`OpenBrowserFilesApiOptions`:
|
|
36
|
+
|
|
37
|
+
| Option | Default | |
|
|
38
|
+
| --- | --- | --- |
|
|
39
|
+
| `handlerKey` | `"root-dir"` | Key under which the handle is stored |
|
|
40
|
+
| `readwrite` | `true` | Request read-write (`true`) or read-only access |
|
|
41
|
+
| `get(key)` | returns `undefined` | Load a stored handle |
|
|
42
|
+
| `set(key, handle)` | no-op | Store a newly picked handle |
|
|
43
|
+
| `del(key)` | no-op | Forget a handle that is no longer accessible |
|
|
44
|
+
|
|
45
|
+
## Examples
|
|
46
|
+
|
|
47
|
+
### A user-picked directory
|
|
21
48
|
|
|
22
49
|
```typescript
|
|
23
50
|
import { openBrowserFilesApi } from '@statewalker/webrun-files-browser';
|
|
24
51
|
import { readText, writeText } from '@statewalker/webrun-files';
|
|
25
52
|
|
|
26
|
-
//
|
|
53
|
+
// Call from a click handler: showDirectoryPicker() needs a user gesture
|
|
27
54
|
const files = await openBrowserFilesApi();
|
|
28
55
|
|
|
29
|
-
// Write a file
|
|
30
56
|
await writeText(files, '/notes/hello.txt', 'Hello, World!');
|
|
31
|
-
|
|
32
|
-
// Read a file
|
|
33
|
-
const content = await readText(files, '/notes/hello.txt');
|
|
34
|
-
console.log(content); // "Hello, World!"
|
|
35
|
-
|
|
36
|
-
// List directory contents
|
|
37
|
-
for await (const entry of files.list('/notes')) {
|
|
38
|
-
console.log(entry.name, entry.kind);
|
|
39
|
-
}
|
|
57
|
+
console.log(await readText(files, '/notes/hello.txt'));
|
|
40
58
|
```
|
|
41
59
|
|
|
42
|
-
###
|
|
43
|
-
|
|
44
|
-
Persist the directory handle using IndexedDB (via `idb-keyval` or similar) to avoid prompting the user every time:
|
|
60
|
+
### Remember the directory across sessions
|
|
45
61
|
|
|
46
62
|
```typescript
|
|
47
63
|
import { openBrowserFilesApi } from '@statewalker/webrun-files-browser';
|
|
48
|
-
import { get, set
|
|
49
|
-
|
|
50
|
-
// Opens directory picker if no stored handle, otherwise reuses stored one
|
|
51
|
-
const files = await openBrowserFilesApi({
|
|
52
|
-
handlerKey: 'my-project-dir', // Key for storing the handle
|
|
53
|
-
readwrite: true, // Request read-write access
|
|
54
|
-
get, // Retrieve stored handle
|
|
55
|
-
set, // Store the handle
|
|
56
|
-
del, // Delete stored handle if inaccessible
|
|
57
|
-
});
|
|
58
|
-
```
|
|
64
|
+
import { del, get, set } from 'idb-keyval';
|
|
59
65
|
|
|
60
|
-
|
|
66
|
+
const files = await openBrowserFilesApi({ handlerKey: 'my-project-dir', readwrite: true, get, set, del });
|
|
67
|
+
```
|
|
61
68
|
|
|
62
|
-
OPFS
|
|
69
|
+
### OPFS, no prompt
|
|
63
70
|
|
|
64
71
|
```typescript
|
|
65
72
|
import { getOPFSFilesApi } from '@statewalker/webrun-files-browser';
|
|
66
73
|
import { writeText } from '@statewalker/webrun-files';
|
|
67
74
|
|
|
68
|
-
// No user gesture required
|
|
69
75
|
const files = await getOPFSFilesApi();
|
|
70
|
-
|
|
71
|
-
// Use the same API
|
|
72
76
|
await writeText(files, '/data/config.json', '{"theme": "dark"}');
|
|
73
77
|
```
|
|
74
78
|
|
|
75
|
-
###
|
|
76
|
-
|
|
77
|
-
Create a `BrowserFilesApi` instance with any `FileSystemDirectoryHandle`:
|
|
78
|
-
|
|
79
|
-
```typescript
|
|
80
|
-
import { BrowserFilesApi } from '@statewalker/webrun-files-browser';
|
|
81
|
-
|
|
82
|
-
// Get a directory handle from drag & drop, IndexedDB, etc.
|
|
83
|
-
const directoryHandle = await getDirectoryHandleSomehow();
|
|
84
|
-
|
|
85
|
-
const files = new BrowserFilesApi({ rootHandle: directoryHandle });
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
## API Reference
|
|
89
|
-
|
|
90
|
-
### BrowserFilesApi
|
|
91
|
-
|
|
92
|
-
Main class implementing `FilesApi` for browser environments.
|
|
79
|
+
### Any handle (drag and drop, IndexedDB)
|
|
93
80
|
|
|
94
81
|
```typescript
|
|
95
|
-
|
|
96
|
-
rootHandle: FileSystemDirectoryHandle;
|
|
97
|
-
}
|
|
82
|
+
import { BrowserFilesApi, isHandlerAccessible, verifyPermission } from '@statewalker/webrun-files-browser';
|
|
98
83
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
// All FilesApi methods
|
|
103
|
-
read(path: string, options?: ReadOptions): AsyncIterable<Uint8Array>;
|
|
104
|
-
write(path: string, content: Iterable<Uint8Array> | AsyncIterable<Uint8Array>): Promise<void>;
|
|
105
|
-
mkdir(path: string): Promise<void>;
|
|
106
|
-
list(path: string, options?: ListOptions): AsyncIterable<FileInfo>;
|
|
107
|
-
stats(path: string): Promise<FileStats | undefined>;
|
|
108
|
-
exists(path: string): Promise<boolean>;
|
|
109
|
-
remove(path: string): Promise<boolean>;
|
|
110
|
-
move(source: string, target: string): Promise<boolean>; // uses native move when available
|
|
111
|
-
copy(source: string, target: string): Promise<boolean>;
|
|
84
|
+
if ((await verifyPermission(handle, true)) && (await isHandlerAccessible(handle))) {
|
|
85
|
+
const files = new BrowserFilesApi({ rootHandle: handle });
|
|
112
86
|
}
|
|
113
87
|
```
|
|
114
88
|
|
|
115
|
-
###
|
|
89
|
+
### Testing in Node.js
|
|
116
90
|
|
|
117
91
|
```typescript
|
|
118
|
-
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
interface OpenBrowserFilesApiOptions {
|
|
123
|
-
handlerKey?: string; // Key for storing the handle (default: "root-dir")
|
|
124
|
-
readwrite?: boolean; // Request read-write access (default: true)
|
|
125
|
-
get?: (key: string) => Promise<FileSystemDirectoryHandle | undefined>;
|
|
126
|
-
set?: (key: string, handler: FileSystemDirectoryHandle) => Promise<void>;
|
|
127
|
-
del?: (key: string) => Promise<void>;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Returns a BrowserFilesApi backed by the Origin Private File System
|
|
131
|
-
function getOPFSFilesApi(): Promise<BrowserFilesApi>;
|
|
132
|
-
|
|
133
|
-
// Checks if a file system handle is still accessible
|
|
134
|
-
function isHandlerAccessible(
|
|
135
|
-
fileHandle: FileSystemFileHandle | FileSystemDirectoryHandle
|
|
136
|
-
): Promise<boolean>;
|
|
92
|
+
import { getOriginPrivateDirectory } from 'native-file-system-adapter';
|
|
93
|
+
// @ts-expect-error - no type declarations
|
|
94
|
+
import * as driver from 'native-file-system-adapter/src/adapters/memory.js';
|
|
95
|
+
import { BrowserFilesApi } from '@statewalker/webrun-files-browser';
|
|
137
96
|
|
|
138
|
-
|
|
139
|
-
function verifyPermission(
|
|
140
|
-
fileHandle: FileSystemFileHandle | FileSystemDirectoryHandle,
|
|
141
|
-
readWrite?: boolean // default: false
|
|
142
|
-
): Promise<boolean>;
|
|
97
|
+
const files = new BrowserFilesApi({ rootHandle: await getOriginPrivateDirectory(driver) });
|
|
143
98
|
```
|
|
144
99
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
Directory handles enumerate in no defined order, so `list()` reads each directory (fetching each
|
|
148
|
-
file's `File` for its size and time) and yields entries in path order, as every `FilesApi` does. A
|
|
149
|
-
recursive listing reads a directory only when the listing reaches it, and with `{ after }` skips any
|
|
150
|
-
directory whose whole subtree sorts at or before the cursor.
|
|
151
|
-
|
|
152
|
-
## Native Move Support
|
|
153
|
-
|
|
154
|
-
The `move()` method attempts to use the browser's native [`FileSystemHandle.move()`](https://developer.mozilla.org/en-US/docs/Web/API/FileSystemHandle/move) when available (Chrome 110+, Edge 110+). This provides an atomic, in-place rename/move without copying file data. On older browsers or polyfills that lack native `move()`, or when the native call throws, the method falls back to copy-then-delete, which is not atomic.
|
|
155
|
-
|
|
156
|
-
## Browser Support
|
|
157
|
-
|
|
158
|
-
The File System Access API is supported in:
|
|
159
|
-
|
|
160
|
-
- Chrome/Edge 86+ (native `move()` from 110+)
|
|
161
|
-
- Opera 72+
|
|
162
|
-
- Chrome for Android 86+
|
|
163
|
-
|
|
164
|
-
For other browsers, you may need a polyfill like [native-file-system-adapter](https://github.com/jimmywarting/native-file-system-adapter).
|
|
100
|
+
This is how the package's own tests run.
|
|
165
101
|
|
|
166
|
-
##
|
|
102
|
+
## Internals
|
|
167
103
|
|
|
168
|
-
|
|
169
|
-
- **Secure context**: The API only works in secure contexts (HTTPS or localhost)
|
|
170
|
-
- **Permission prompts**: Users must grant permission to access directories
|
|
171
|
-
- **OPFS isolation**: Origin Private File System is isolated per-origin and not accessible to the user
|
|
104
|
+
### How calls map to handles
|
|
172
105
|
|
|
173
|
-
|
|
106
|
+
- `read` slices the `File` from `getFile()` into 8 KiB `arrayBuffer()` reads over the requested range.
|
|
107
|
+
- `write` creates parents, then streams every chunk into `createWritable()` and closes it.
|
|
108
|
+
- `remove` is `removeEntry(name, { recursive: true })` on the parent.
|
|
109
|
+
- `move` uses the native `FileSystemHandle.move()` where it exists (Chromium 110+), which renames
|
|
110
|
+
in place without copying. Without it, or when it throws, `move` copies then removes, which is
|
|
111
|
+
not atomic.
|
|
174
112
|
|
|
175
|
-
|
|
113
|
+
### Why listings read every file
|
|
176
114
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const hasPermission = await verifyPermission(directoryHandle, true);
|
|
182
|
-
|
|
183
|
-
// Check if the directory still exists and is accessible
|
|
184
|
-
const isAccessible = await isHandlerAccessible(directoryHandle);
|
|
115
|
+
Directory handles enumerate in no defined order, and every `FilesApi` must list in `comparePaths`
|
|
116
|
+
order. `list()` reads each directory, calls `getFile()` per file for its size and time, and yields
|
|
117
|
+
entries sorted. A recursive listing reads a directory only when the listing reaches it, and skips
|
|
118
|
+
subtrees that sort at or before `after`. A large directory costs one `getFile()` per file.
|
|
185
119
|
|
|
186
|
-
|
|
187
|
-
console.log('Permission denied - user may need to re-grant access');
|
|
188
|
-
}
|
|
120
|
+
### What breaks, and how it looks
|
|
189
121
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
122
|
+
- **`openBrowserFilesApi` throws** `Access was not granted` when the permission prompt is refused,
|
|
123
|
+
and `Cannot access the folder. Please try again.` when a stored handle points at a moved or
|
|
124
|
+
deleted directory (it calls `del(handlerKey)` first, so the next call shows the picker).
|
|
125
|
+
- **No user gesture, no picker.** Calling `openBrowserFilesApi()` without a stored handle outside a
|
|
126
|
+
click or key handler rejects with the browser's `SecurityError`.
|
|
127
|
+
- **A failed write still commits.** If the content iterable throws midway, `write` closes the
|
|
128
|
+
writable stream in a `finally`, which saves the bytes written so far instead of aborting.
|
|
129
|
+
- **Read errors look like missing files.** `read` yields nothing when the handle or file cannot be
|
|
130
|
+
read.
|
|
194
131
|
|
|
195
|
-
|
|
132
|
+
### Browser support
|
|
196
133
|
|
|
197
|
-
|
|
134
|
+
The File System Access API with `showDirectoryPicker()` exists in Chromium-based browsers (Chrome,
|
|
135
|
+
Edge, Opera). OPFS (`getOPFSFilesApi`) is more widely available. For other environments, such as
|
|
136
|
+
tests, [native-file-system-adapter](https://github.com/jimmywarting/native-file-system-adapter)
|
|
137
|
+
provides compatible handles.
|
|
198
138
|
|
|
199
|
-
|
|
200
|
-
import { getOriginPrivateDirectory } from 'native-file-system-adapter';
|
|
201
|
-
// @ts-expect-error - no type declarations
|
|
202
|
-
import * as driver from 'native-file-system-adapter/src/adapters/memory.js';
|
|
203
|
-
import { BrowserFilesApi } from '@statewalker/webrun-files-browser';
|
|
139
|
+
### Dependencies
|
|
204
140
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
```
|
|
141
|
+
`@statewalker/webrun-files` only. `native-file-system-adapter` is a development dependency for
|
|
142
|
+
tests.
|
|
208
143
|
|
|
209
144
|
## License
|
|
210
145
|
|
package/package.json
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@statewalker/webrun-files-browser",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Browser File System Access API implementation for webrun-files",
|
|
7
|
-
"
|
|
7
|
+
"bugs": {
|
|
8
|
+
"url": "https://github.com/statewalker/webrun-files/issues"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/statewalker/webrun-files/tree/main/packages/webrun-files-browser#readme",
|
|
8
11
|
"author": {
|
|
9
12
|
"name": "Mikhail Kotelnikov",
|
|
10
13
|
"email": "mikhail.kotelnikov@gmail.com"
|
|
@@ -12,7 +15,8 @@
|
|
|
12
15
|
"license": "MIT",
|
|
13
16
|
"repository": {
|
|
14
17
|
"type": "git",
|
|
15
|
-
"url": "git
|
|
18
|
+
"url": "git+https://github.com/statewalker/webrun-files.git",
|
|
19
|
+
"directory": "packages/webrun-files-browser"
|
|
16
20
|
},
|
|
17
21
|
"main": "./dist/cjs/index.cjs",
|
|
18
22
|
"module": "./dist/esm/index.js",
|
|
@@ -30,7 +34,7 @@
|
|
|
30
34
|
"src"
|
|
31
35
|
],
|
|
32
36
|
"dependencies": {
|
|
33
|
-
"@statewalker/webrun-files": "^0.10.
|
|
37
|
+
"@statewalker/webrun-files": "^0.10.2"
|
|
34
38
|
},
|
|
35
39
|
"devDependencies": {
|
|
36
40
|
"@types/node": "^26.6.4",
|