@statewalker/webrun-files-browser 0.5.0 → 0.7.1

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 CHANGED
@@ -1,10 +1,10 @@
1
1
  # @statewalker/webrun-files-browser
2
2
 
3
- Browser File System Access API implementation for the `@statewalker/webrun-files` package.
3
+ Browser File System Access API implementation of the `FilesApi` interface from `@statewalker/webrun-files`.
4
4
 
5
5
  ## Overview
6
6
 
7
- This package provides an `IFilesApi` implementation that works in modern browsers using the [File System Access API](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API). It allows web applications to read and write files to:
7
+ This package provides a `FilesApi` implementation that works in modern browsers using the [File System Access API](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API). It allows web applications to read and write files to:
8
8
 
9
9
  - **User-selected directories** via `showDirectoryPicker()`
10
10
  - **Origin Private File System (OPFS)** - a sandboxed filesystem that persists between sessions
@@ -20,100 +20,95 @@ npm install @statewalker/webrun-files-browser @statewalker/webrun-files
20
20
  ### With User-Selected Directory
21
21
 
22
22
  ```typescript
23
- import { openBrowserFilesApi } from "@statewalker/webrun-files-browser";
24
- import { FilesApi } from "@statewalker/webrun-files";
23
+ import { openBrowserFilesApi } from '@statewalker/webrun-files-browser';
24
+ import { readText, writeText } from '@statewalker/webrun-files';
25
25
 
26
26
  // Opens a directory picker dialog (requires user gesture)
27
- const browserFs = await openBrowserFilesApi();
28
- const api = new FilesApi(browserFs);
27
+ const files = await openBrowserFilesApi();
29
28
 
30
29
  // Write a file
31
- await api.write("/notes/hello.txt", [
32
- new TextEncoder().encode("Hello, World!")
33
- ]);
30
+ await writeText(files, '/notes/hello.txt', 'Hello, World!');
34
31
 
35
32
  // Read a file
36
- const content = await api.readFile("/notes/hello.txt");
37
- console.log(new TextDecoder().decode(content)); // "Hello, World!"
33
+ const content = await readText(files, '/notes/hello.txt');
34
+ console.log(content); // "Hello, World!"
38
35
 
39
36
  // List directory contents
40
- for await (const entry of api.list("/notes")) {
37
+ for await (const entry of files.list('/notes')) {
41
38
  console.log(entry.name, entry.kind);
42
39
  }
43
40
  ```
44
41
 
42
+ ### With Persistent Directory Handle
43
+
44
+ Persist the directory handle using IndexedDB (via `idb-keyval` or similar) to avoid prompting the user every time:
45
+
46
+ ```typescript
47
+ import { openBrowserFilesApi } from '@statewalker/webrun-files-browser';
48
+ import { get, set, del } from 'idb-keyval';
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
+ ```
59
+
45
60
  ### With Origin Private File System (OPFS)
46
61
 
47
62
  OPFS provides a sandboxed filesystem that doesn't require user interaction:
48
63
 
49
64
  ```typescript
50
- import { getOPFSFilesApi } from "@statewalker/webrun-files-browser";
51
- import { FilesApi } from "@statewalker/webrun-files";
65
+ import { getOPFSFilesApi } from '@statewalker/webrun-files-browser';
66
+ import { writeText } from '@statewalker/webrun-files';
52
67
 
53
68
  // No user gesture required
54
- const opfsFs = await getOPFSFilesApi();
55
- const api = new FilesApi(opfsFs);
69
+ const files = await getOPFSFilesApi();
56
70
 
57
- // Use the same API as above
58
- await api.write("/data/config.json", [
59
- new TextEncoder().encode('{"theme": "dark"}')
60
- ]);
71
+ // Use the same API
72
+ await writeText(files, '/data/config.json', '{"theme": "dark"}');
61
73
  ```
62
74
 
63
75
  ### With Custom Directory Handle
64
76
 
65
- You can also create a `BrowserFilesApi` instance with any `FileSystemDirectoryHandle`:
77
+ Create a `BrowserFilesApi` instance with any `FileSystemDirectoryHandle`:
66
78
 
67
79
  ```typescript
68
- import { BrowserFilesApi } from "@statewalker/webrun-files-browser";
69
- import { FilesApi } from "@statewalker/webrun-files";
80
+ import { BrowserFilesApi } from '@statewalker/webrun-files-browser';
70
81
 
71
82
  // Get a directory handle from drag & drop, IndexedDB, etc.
72
83
  const directoryHandle = await getDirectoryHandleSomehow();
73
84
 
74
- const browserFs = new BrowserFilesApi({ rootHandle: directoryHandle });
75
- const api = new FilesApi(browserFs);
85
+ const files = new BrowserFilesApi({ rootHandle: directoryHandle });
76
86
  ```
77
87
 
78
88
  ## API Reference
79
89
 
80
90
  ### BrowserFilesApi
81
91
 
82
- Main class implementing `IFilesApi` for browser environments.
92
+ Main class implementing `FilesApi` for browser environments.
83
93
 
84
94
  ```typescript
85
95
  interface BrowserFilesApiOptions {
86
96
  rootHandle: FileSystemDirectoryHandle;
87
97
  }
88
98
 
89
- class BrowserFilesApi implements IFilesApi {
99
+ class BrowserFilesApi implements FilesApi {
90
100
  constructor(options: BrowserFilesApiOptions);
91
101
 
92
- // Core IFilesApi methods
93
- list(file: FileRef, options?: ListOptions): AsyncGenerator<FileInfo>;
94
- stats(file: FileRef): Promise<FileInfo | undefined>;
95
- remove(file: FileRef): Promise<boolean>;
96
- open(file: FileRef): Promise<FileHandle>;
97
-
98
- // Optional methods (implemented)
99
- mkdir(file: FileRef): Promise<void>;
100
- move(source: FileRef, target: FileRef): Promise<boolean>;
101
- copy(source: FileRef, target: FileRef, options?: CopyOptions): Promise<boolean>;
102
- }
103
- ```
104
-
105
- ### BrowserFileHandle
106
-
107
- File handle class for random access read/write operations.
108
-
109
- ```typescript
110
- class BrowserFileHandle implements FileHandle {
111
- readonly size: number;
112
-
113
- close(): Promise<void>;
114
- appendFile(data: BinaryStream, options?: AppendOptions): Promise<number>;
115
- createReadStream(options?: ReadStreamOptions): AsyncGenerator<Uint8Array>;
116
- createWriteStream(data: BinaryStream, options?: WriteStreamOptions): Promise<number>;
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>;
117
112
  }
118
113
  ```
119
114
 
@@ -121,60 +116,44 @@ class BrowserFileHandle implements FileHandle {
121
116
 
122
117
  ```typescript
123
118
  // Opens a directory picker and returns a BrowserFilesApi instance
124
- function openBrowserFilesApi(): Promise<BrowserFilesApi>;
119
+ function openBrowserFilesApi(options?: OpenBrowserFilesApiOptions): Promise<BrowserFilesApi>;
120
+
121
+ interface OpenBrowserFilesApiOptions {
122
+ handlerKey?: string; // Key for storing the handle (default: "root-dir")
123
+ readwrite?: boolean; // Request read-write access (default: true)
124
+ get?: (key: string) => Promise<FileSystemDirectoryHandle | undefined>;
125
+ set?: (key: string, handler: FileSystemDirectoryHandle) => Promise<void>;
126
+ del?: (key: string) => Promise<void>;
127
+ }
125
128
 
126
129
  // Returns a BrowserFilesApi backed by the Origin Private File System
127
130
  function getOPFSFilesApi(): Promise<BrowserFilesApi>;
128
- ```
129
-
130
- ## How It Works
131
-
132
- ### Directory Navigation
133
-
134
- The implementation navigates the filesystem by traversing `FileSystemDirectoryHandle` entries. Paths like `/docs/notes/file.txt` are resolved by getting directory handles step by step:
135
-
136
- ```
137
- root → "docs" → "notes" → "file.txt"
138
- ```
139
131
 
140
- ### Reading Files
132
+ // Checks if a file system handle is still accessible
133
+ function isHandlerAccessible(
134
+ fileHandle: FileSystemFileHandle | FileSystemDirectoryHandle
135
+ ): Promise<boolean>;
141
136
 
142
- Files are read using `File.slice()` for efficient partial reads. This enables random access without loading the entire file into memory:
143
-
144
- ```typescript
145
- const handle = await api.open("/large-file.bin");
146
- // Read only bytes 1000-2000
147
- for await (const chunk of handle.createReadStream({ start: 1000, end: 2000 })) {
148
- console.log(chunk);
149
- }
150
- await handle.close();
137
+ // Verifies and requests file system permissions for a handle
138
+ function verifyPermission(
139
+ fileHandle: FileSystemFileHandle | FileSystemDirectoryHandle,
140
+ readWrite?: boolean // default: false
141
+ ): Promise<boolean>;
151
142
  ```
152
143
 
153
- ### Writing Files
154
-
155
- Writes use `FileSystemWritableFileStream`:
156
-
157
- - **Full writes** create a new writable stream and write all data
158
- - **Partial writes** (with `start` option) preserve content before the start position
159
- - **Appends** seek to the end of the file before writing
160
-
161
- ### Directory Creation
144
+ ## Native Move Support
162
145
 
163
- Directories are created automatically when writing files. You can also create empty directories:
164
-
165
- ```typescript
166
- await api.mkdir("/path/to/new/directory");
167
- ```
146
+ 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()`, the method falls back to copy-then-delete.
168
147
 
169
148
  ## Browser Support
170
149
 
171
150
  The File System Access API is supported in:
172
151
 
173
- - Chrome/Edge 86+
152
+ - Chrome/Edge 86+ (native `move()` from 110+)
174
153
  - Opera 72+
175
154
  - Chrome for Android 86+
176
155
 
177
- For other browsers, you may need a polyfill like [native-file-system-adapter](https://github.com/nicolo-ribaudo/native-file-system-adapter).
156
+ For other browsers, you may need a polyfill like [native-file-system-adapter](https://github.com/jimmywarting/native-file-system-adapter).
178
157
 
179
158
  ## Security Considerations
180
159
 
@@ -183,19 +162,40 @@ For other browsers, you may need a polyfill like [native-file-system-adapter](ht
183
162
  - **Permission prompts**: Users must grant permission to access directories
184
163
  - **OPFS isolation**: Origin Private File System is isolated per-origin and not accessible to the user
185
164
 
186
- ## Testing
165
+ ## Permission and Accessibility Utilities
187
166
 
188
- For testing in Node.js environments, we use [native-file-system-adapter](https://github.com/jimmywarting/native-file-system-adapter) which provides a memory backend:
167
+ When working with persistent directory handles:
189
168
 
190
169
  ```typescript
191
- import { getOriginPrivateDirectory } from "native-file-system-adapter";
192
- import { BrowserFilesApi } from "@statewalker/webrun-files-browser";
170
+ import { verifyPermission, isHandlerAccessible } from '@statewalker/webrun-files-browser';
171
+
172
+ // Check if we have read-write permission
173
+ const hasPermission = await verifyPermission(directoryHandle, true);
193
174
 
194
- const rootHandle = await getOriginPrivateDirectory(
195
- import("native-file-system-adapter/src/adapters/memory.js")
196
- );
175
+ // Check if the directory still exists and is accessible
176
+ const isAccessible = await isHandlerAccessible(directoryHandle);
177
+
178
+ if (!hasPermission) {
179
+ console.log('Permission denied - user may need to re-grant access');
180
+ }
181
+
182
+ if (!isAccessible) {
183
+ console.log('Directory no longer accessible - may have been deleted or moved');
184
+ }
185
+ ```
186
+
187
+ ## Testing
188
+
189
+ For testing in Node.js environments, use [native-file-system-adapter](https://github.com/jimmywarting/native-file-system-adapter) which provides a memory backend:
190
+
191
+ ```typescript
192
+ import { getOriginPrivateDirectory } from 'native-file-system-adapter';
193
+ // @ts-expect-error - no type declarations
194
+ import * as driver from 'native-file-system-adapter/src/adapters/memory.js';
195
+ import { BrowserFilesApi } from '@statewalker/webrun-files-browser';
197
196
 
198
- const api = new BrowserFilesApi({ rootHandle });
197
+ const rootHandle = await getOriginPrivateDirectory(driver);
198
+ const files = new BrowserFilesApi({ rootHandle });
199
199
  ```
200
200
 
201
201
  ## License
@@ -1,10 +1,10 @@
1
1
  /**
2
- * Browser implementation of IFilesApi using the File System Access API.
2
+ * Browser implementation of FilesApi using the File System Access API.
3
3
  *
4
4
  * Provides a filesystem-like interface over browser's FileSystemDirectoryHandle.
5
5
  * Works with directories obtained via showDirectoryPicker() or OPFS.
6
6
  */
7
- import type { CopyOptions, FileHandle, FileInfo, FileRef, IFilesApi, ListOptions } from "@statewalker/webrun-files";
7
+ import type { FileInfo, FileStats, FilesApi, ListOptions, ReadOptions } from "@statewalker/webrun-files";
8
8
  export interface BrowserFilesApiOptions {
9
9
  /**
10
10
  * Root FileSystemDirectoryHandle to use as the filesystem root.
@@ -12,58 +12,30 @@ export interface BrowserFilesApiOptions {
12
12
  */
13
13
  rootHandle: FileSystemDirectoryHandle;
14
14
  }
15
- export declare class BrowserFilesApi implements IFilesApi {
15
+ /**
16
+ * Browser filesystem implementation using the File System Access API.
17
+ *
18
+ * Provides FilesApi interface over browser's FileSystemDirectoryHandle.
19
+ * This enables web applications to read/write files in user-selected directories
20
+ * or the Origin Private File System (OPFS).
21
+ */
22
+ export declare class BrowserFilesApi implements FilesApi {
16
23
  private rootHandle;
17
24
  constructor(options: BrowserFilesApiOptions);
18
- /**
19
- * Navigates to a directory handle from a path.
20
- * Returns null if the directory doesn't exist and create is false.
21
- */
22
25
  private getDirectoryHandle;
23
- /**
24
- * Gets a file handle from a path.
25
- * Returns null if the file doesn't exist and create is false.
26
- */
27
26
  private getFileHandle;
28
- /**
29
- * Lists entries in a directory.
30
- */
31
- list(file: FileRef, options?: ListOptions): AsyncGenerator<FileInfo>;
32
- /**
33
- * Gets file or directory stats.
34
- */
35
- stats(file: FileRef): Promise<FileInfo | undefined>;
36
- /**
37
- * Removes a file or directory.
38
- */
39
- remove(file: FileRef): Promise<boolean>;
40
- /**
41
- * Opens a file for reading and writing.
42
- * Creates the file and parent directories if they don't exist.
43
- */
44
- open(file: FileRef): Promise<FileHandle>;
45
- /**
46
- * Creates a directory (and parent directories if needed).
47
- */
48
- mkdir(file: FileRef): Promise<void>;
49
- /**
50
- * Moves a file or directory using copy + delete.
51
- * The File System Access API doesn't have a native move operation.
52
- */
53
- move(source: FileRef, target: FileRef): Promise<boolean>;
54
- /**
55
- * Copies a file or directory.
56
- */
57
- copy(source: FileRef, target: FileRef, options?: CopyOptions): Promise<boolean>;
27
+ read(path: string, options?: ReadOptions): AsyncIterable<Uint8Array>;
28
+ write(path: string, content: Iterable<Uint8Array> | AsyncIterable<Uint8Array>): Promise<void>;
29
+ mkdir(path: string): Promise<void>;
30
+ list(path: string, options?: ListOptions): AsyncIterable<FileInfo>;
31
+ stats(path: string): Promise<FileStats | undefined>;
32
+ exists(path: string): Promise<boolean>;
33
+ remove(path: string): Promise<boolean>;
34
+ move(source: string, target: string): Promise<boolean>;
35
+ copy(source: string, target: string): Promise<boolean>;
58
36
  }
59
- /**
60
- * Opens a browser directory picker and returns a BrowserFilesApi instance.
61
- * Only works in secure contexts (HTTPS) and requires user gesture.
62
- */
63
- export declare function openBrowserFilesApi(): Promise<BrowserFilesApi>;
64
37
  /**
65
38
  * Gets a BrowserFilesApi instance backed by the Origin Private File System (OPFS).
66
- * OPFS provides a sandboxed filesystem that persists between sessions.
67
39
  */
68
40
  export declare function getOPFSFilesApi(): Promise<BrowserFilesApi>;
69
41
  //# sourceMappingURL=browser-files-api.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"browser-files-api.d.ts","sourceRoot":"","sources":["../src/browser-files-api.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EACV,WAAW,EACX,UAAU,EACV,QAAQ,EACR,OAAO,EACP,SAAS,EACT,WAAW,EACZ,MAAM,2BAA2B,CAAC;AAInC,MAAM,WAAW,sBAAsB;IACrC;;;OAGG;IACH,UAAU,EAAE,yBAAyB,CAAC;CACvC;AAED,qBAAa,eAAgB,YAAW,SAAS;IAC/C,OAAO,CAAC,UAAU,CAA4B;gBAElC,OAAO,EAAE,sBAAsB;IAI3C;;;OAGG;YACW,kBAAkB;IAkBhC;;;OAGG;YACW,aAAa;IAkB3B;;OAEG;IACI,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,GAAE,WAAgB,GAAG,cAAc,CAAC,QAAQ,CAAC;IAsC/E;;OAEG;IACG,KAAK,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC;IA8CzD;;OAEG;IACG,MAAM,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAgB7C;;;OAGG;IACG,IAAI,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,UAAU,CAAC;IAkB9C;;OAEG;IACG,KAAK,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAKzC;;;OAGG;IACG,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAM9D;;OAEG;IACG,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,OAAO,CAAC;CAiF1F;AAED;;;GAGG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,eAAe,CAAC,CAIpE;AAED;;;GAGG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC,eAAe,CAAC,CAGhE"}
1
+ {"version":3,"file":"browser-files-api.d.ts","sourceRoot":"","sources":["../src/browser-files-api.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EACV,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,WAAW,EACX,WAAW,EACZ,MAAM,2BAA2B,CAAC;AAQnC,MAAM,WAAW,sBAAsB;IACrC;;;OAGG;IACH,UAAU,EAAE,yBAAyB,CAAC;CACvC;AAED;;;;;;GAMG;AACH,qBAAa,eAAgB,YAAW,QAAQ;IAC9C,OAAO,CAAC,UAAU,CAA4B;gBAElC,OAAO,EAAE,sBAAsB;YAI7B,kBAAkB;YAkBlB,aAAa;IAkBpB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,aAAa,CAAC,UAAU,CAAC;IA6BrE,KAAK,CACT,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,aAAa,CAAC,UAAU,CAAC,GACxD,OAAO,CAAC,IAAI,CAAC;IAsBV,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKjC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,aAAa,CAAC,QAAQ,CAAC;IAqCnE,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IAqCnD,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAKtC,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAgBtC,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAgCtD,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CA4E7D;AAED;;GAEG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC,eAAe,CAAC,CAGhE"}