@statewalker/webrun-files-browser 0.5.0
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/LICENSE +21 -0
- package/README.md +203 -0
- package/dist/browser-file-handle.d.ts +42 -0
- package/dist/browser-file-handle.d.ts.map +1 -0
- package/dist/browser-files-api.d.ts +69 -0
- package/dist/browser-files-api.d.ts.map +1 -0
- package/dist/cjs/index.cjs +335 -0
- package/dist/esm/index.js +332 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/package.json +52 -0
- package/src/browser-file-handle.ts +194 -0
- package/src/browser-files-api.ts +330 -0
- package/src/index.ts +12 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 statewalker
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# @statewalker/webrun-files-browser
|
|
2
|
+
|
|
3
|
+
Browser File System Access API implementation for the `@statewalker/webrun-files` package.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
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:
|
|
8
|
+
|
|
9
|
+
- **User-selected directories** via `showDirectoryPicker()`
|
|
10
|
+
- **Origin Private File System (OPFS)** - a sandboxed filesystem that persists between sessions
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @statewalker/webrun-files-browser @statewalker/webrun-files
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
### With User-Selected Directory
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { openBrowserFilesApi } from "@statewalker/webrun-files-browser";
|
|
24
|
+
import { FilesApi } from "@statewalker/webrun-files";
|
|
25
|
+
|
|
26
|
+
// Opens a directory picker dialog (requires user gesture)
|
|
27
|
+
const browserFs = await openBrowserFilesApi();
|
|
28
|
+
const api = new FilesApi(browserFs);
|
|
29
|
+
|
|
30
|
+
// Write a file
|
|
31
|
+
await api.write("/notes/hello.txt", [
|
|
32
|
+
new TextEncoder().encode("Hello, World!")
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
// Read a file
|
|
36
|
+
const content = await api.readFile("/notes/hello.txt");
|
|
37
|
+
console.log(new TextDecoder().decode(content)); // "Hello, World!"
|
|
38
|
+
|
|
39
|
+
// List directory contents
|
|
40
|
+
for await (const entry of api.list("/notes")) {
|
|
41
|
+
console.log(entry.name, entry.kind);
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### With Origin Private File System (OPFS)
|
|
46
|
+
|
|
47
|
+
OPFS provides a sandboxed filesystem that doesn't require user interaction:
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import { getOPFSFilesApi } from "@statewalker/webrun-files-browser";
|
|
51
|
+
import { FilesApi } from "@statewalker/webrun-files";
|
|
52
|
+
|
|
53
|
+
// No user gesture required
|
|
54
|
+
const opfsFs = await getOPFSFilesApi();
|
|
55
|
+
const api = new FilesApi(opfsFs);
|
|
56
|
+
|
|
57
|
+
// Use the same API as above
|
|
58
|
+
await api.write("/data/config.json", [
|
|
59
|
+
new TextEncoder().encode('{"theme": "dark"}')
|
|
60
|
+
]);
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### With Custom Directory Handle
|
|
64
|
+
|
|
65
|
+
You can also create a `BrowserFilesApi` instance with any `FileSystemDirectoryHandle`:
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
import { BrowserFilesApi } from "@statewalker/webrun-files-browser";
|
|
69
|
+
import { FilesApi } from "@statewalker/webrun-files";
|
|
70
|
+
|
|
71
|
+
// Get a directory handle from drag & drop, IndexedDB, etc.
|
|
72
|
+
const directoryHandle = await getDirectoryHandleSomehow();
|
|
73
|
+
|
|
74
|
+
const browserFs = new BrowserFilesApi({ rootHandle: directoryHandle });
|
|
75
|
+
const api = new FilesApi(browserFs);
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## API Reference
|
|
79
|
+
|
|
80
|
+
### BrowserFilesApi
|
|
81
|
+
|
|
82
|
+
Main class implementing `IFilesApi` for browser environments.
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
interface BrowserFilesApiOptions {
|
|
86
|
+
rootHandle: FileSystemDirectoryHandle;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
class BrowserFilesApi implements IFilesApi {
|
|
90
|
+
constructor(options: BrowserFilesApiOptions);
|
|
91
|
+
|
|
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>;
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Helper Functions
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
// Opens a directory picker and returns a BrowserFilesApi instance
|
|
124
|
+
function openBrowserFilesApi(): Promise<BrowserFilesApi>;
|
|
125
|
+
|
|
126
|
+
// Returns a BrowserFilesApi backed by the Origin Private File System
|
|
127
|
+
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
|
+
|
|
140
|
+
### Reading Files
|
|
141
|
+
|
|
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();
|
|
151
|
+
```
|
|
152
|
+
|
|
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
|
|
162
|
+
|
|
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
|
+
```
|
|
168
|
+
|
|
169
|
+
## Browser Support
|
|
170
|
+
|
|
171
|
+
The File System Access API is supported in:
|
|
172
|
+
|
|
173
|
+
- Chrome/Edge 86+
|
|
174
|
+
- Opera 72+
|
|
175
|
+
- Chrome for Android 86+
|
|
176
|
+
|
|
177
|
+
For other browsers, you may need a polyfill like [native-file-system-adapter](https://github.com/nicolo-ribaudo/native-file-system-adapter).
|
|
178
|
+
|
|
179
|
+
## Security Considerations
|
|
180
|
+
|
|
181
|
+
- **User gesture required**: `showDirectoryPicker()` must be called from a user gesture (click, key press)
|
|
182
|
+
- **Secure context**: The API only works in secure contexts (HTTPS or localhost)
|
|
183
|
+
- **Permission prompts**: Users must grant permission to access directories
|
|
184
|
+
- **OPFS isolation**: Origin Private File System is isolated per-origin and not accessible to the user
|
|
185
|
+
|
|
186
|
+
## Testing
|
|
187
|
+
|
|
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:
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
import { getOriginPrivateDirectory } from "native-file-system-adapter";
|
|
192
|
+
import { BrowserFilesApi } from "@statewalker/webrun-files-browser";
|
|
193
|
+
|
|
194
|
+
const rootHandle = await getOriginPrivateDirectory(
|
|
195
|
+
import("native-file-system-adapter/src/adapters/memory.js")
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
const api = new BrowserFilesApi({ rootHandle });
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## License
|
|
202
|
+
|
|
203
|
+
MIT
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser FileHandle implementation using File System Access API.
|
|
3
|
+
*
|
|
4
|
+
* Provides random access read/write operations for browser files.
|
|
5
|
+
* Uses File.slice() for efficient partial reads and FileSystemWritableFileStream
|
|
6
|
+
* for writes.
|
|
7
|
+
*/
|
|
8
|
+
import type { AppendOptions, BinaryStream, FileHandle, ReadStreamOptions, WriteStreamOptions } from "@statewalker/webrun-files";
|
|
9
|
+
export interface BrowserFileHandleOptions {
|
|
10
|
+
fileHandle: FileSystemFileHandle;
|
|
11
|
+
initialFile: File;
|
|
12
|
+
}
|
|
13
|
+
export declare class BrowserFileHandle implements FileHandle {
|
|
14
|
+
private fileHandle;
|
|
15
|
+
private file;
|
|
16
|
+
private _size;
|
|
17
|
+
private closed;
|
|
18
|
+
constructor(options: BrowserFileHandleOptions);
|
|
19
|
+
get size(): number;
|
|
20
|
+
close(): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* Appends data to the end of the file.
|
|
23
|
+
*/
|
|
24
|
+
appendFile(data: BinaryStream, options?: AppendOptions): Promise<number>;
|
|
25
|
+
/**
|
|
26
|
+
* Creates a read stream for the file.
|
|
27
|
+
* Uses File.slice() for efficient partial reads.
|
|
28
|
+
*/
|
|
29
|
+
createReadStream(options?: ReadStreamOptions): AsyncGenerator<Uint8Array>;
|
|
30
|
+
/**
|
|
31
|
+
* Writes data to the file starting at the specified position.
|
|
32
|
+
* If start > 0, preserves content before start position.
|
|
33
|
+
* Truncates file at start position before writing.
|
|
34
|
+
*/
|
|
35
|
+
createWriteStream(data: BinaryStream, options?: WriteStreamOptions): Promise<number>;
|
|
36
|
+
/**
|
|
37
|
+
* Random access read from file at a specific position.
|
|
38
|
+
* Uses File.slice() for efficient partial reads.
|
|
39
|
+
*/
|
|
40
|
+
read(buffer: Uint8Array, offset: number, length: number, position: number): Promise<number>;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=browser-file-handle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-file-handle.d.ts","sourceRoot":"","sources":["../src/browser-file-handle.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EACV,aAAa,EACb,YAAY,EACZ,UAAU,EACV,iBAAiB,EACjB,kBAAkB,EACnB,MAAM,2BAA2B,CAAC;AAGnC,MAAM,WAAW,wBAAwB;IACvC,UAAU,EAAE,oBAAoB,CAAC;IACjC,WAAW,EAAE,IAAI,CAAC;CACnB;AAED,qBAAa,iBAAkB,YAAW,UAAU;IAClD,OAAO,CAAC,UAAU,CAAuB;IACzC,OAAO,CAAC,IAAI,CAAc;IAC1B,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,MAAM,CAAS;gBAEX,OAAO,EAAE,wBAAwB;IAM7C,IAAI,IAAI,IAAI,MAAM,CAEjB;IAEK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAK5B;;OAEG;IACG,UAAU,CAAC,IAAI,EAAE,YAAY,EAAE,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,MAAM,CAAC;IAgClF;;;OAGG;IACI,gBAAgB,CAAC,OAAO,GAAE,iBAAsB,GAAG,cAAc,CAAC,UAAU,CAAC;IAmCpF;;;;OAIG;IACG,iBAAiB,CAAC,IAAI,EAAE,YAAY,EAAE,OAAO,GAAE,kBAAuB,GAAG,OAAO,CAAC,MAAM,CAAC;IAyC9F;;;OAGG;IACG,IAAI,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CA0BlG"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser implementation of IFilesApi using the File System Access API.
|
|
3
|
+
*
|
|
4
|
+
* Provides a filesystem-like interface over browser's FileSystemDirectoryHandle.
|
|
5
|
+
* Works with directories obtained via showDirectoryPicker() or OPFS.
|
|
6
|
+
*/
|
|
7
|
+
import type { CopyOptions, FileHandle, FileInfo, FileRef, IFilesApi, ListOptions } from "@statewalker/webrun-files";
|
|
8
|
+
export interface BrowserFilesApiOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Root FileSystemDirectoryHandle to use as the filesystem root.
|
|
11
|
+
* Can be obtained via showDirectoryPicker(), navigator.storage.getDirectory(), etc.
|
|
12
|
+
*/
|
|
13
|
+
rootHandle: FileSystemDirectoryHandle;
|
|
14
|
+
}
|
|
15
|
+
export declare class BrowserFilesApi implements IFilesApi {
|
|
16
|
+
private rootHandle;
|
|
17
|
+
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
|
+
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
|
+
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>;
|
|
58
|
+
}
|
|
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
|
+
/**
|
|
65
|
+
* Gets a BrowserFilesApi instance backed by the Origin Private File System (OPFS).
|
|
66
|
+
* OPFS provides a sandboxed filesystem that persists between sessions.
|
|
67
|
+
*/
|
|
68
|
+
export declare function getOPFSFilesApi(): Promise<BrowserFilesApi>;
|
|
69
|
+
//# sourceMappingURL=browser-files-api.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
let __statewalker_webrun_files = require("@statewalker/webrun-files");
|
|
2
|
+
|
|
3
|
+
//#region src/browser-file-handle.ts
|
|
4
|
+
var BrowserFileHandle = class {
|
|
5
|
+
fileHandle;
|
|
6
|
+
file;
|
|
7
|
+
_size;
|
|
8
|
+
closed = false;
|
|
9
|
+
constructor(options) {
|
|
10
|
+
this.fileHandle = options.fileHandle;
|
|
11
|
+
this.file = options.initialFile;
|
|
12
|
+
this._size = options.initialFile.size;
|
|
13
|
+
}
|
|
14
|
+
get size() {
|
|
15
|
+
return this._size;
|
|
16
|
+
}
|
|
17
|
+
async close() {
|
|
18
|
+
this.closed = true;
|
|
19
|
+
this.file = null;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Appends data to the end of the file.
|
|
23
|
+
*/
|
|
24
|
+
async appendFile(data, options = {}) {
|
|
25
|
+
if (this.closed) throw new Error("FileHandle is closed");
|
|
26
|
+
const { signal } = options;
|
|
27
|
+
const writable = await this.fileHandle.createWritable({ keepExistingData: true });
|
|
28
|
+
let bytesWritten = 0;
|
|
29
|
+
try {
|
|
30
|
+
await writable.seek(this._size);
|
|
31
|
+
for await (const chunk of (0, __statewalker_webrun_files.toBinaryAsyncIterable)(data)) {
|
|
32
|
+
if (signal?.aborted) throw new Error("Operation aborted");
|
|
33
|
+
await writable.write(chunk);
|
|
34
|
+
bytesWritten += chunk.length;
|
|
35
|
+
}
|
|
36
|
+
} finally {
|
|
37
|
+
await writable.close();
|
|
38
|
+
}
|
|
39
|
+
this.file = await this.fileHandle.getFile();
|
|
40
|
+
this._size = this.file.size;
|
|
41
|
+
return bytesWritten;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Creates a read stream for the file.
|
|
45
|
+
* Uses File.slice() for efficient partial reads.
|
|
46
|
+
*/
|
|
47
|
+
async *createReadStream(options = {}) {
|
|
48
|
+
if (this.closed) throw new Error("FileHandle is closed");
|
|
49
|
+
if (!this.file) {
|
|
50
|
+
this.file = await this.fileHandle.getFile();
|
|
51
|
+
this._size = this.file.size;
|
|
52
|
+
}
|
|
53
|
+
const { start = 0, end = Infinity, signal } = options;
|
|
54
|
+
const bufferSize = 8192;
|
|
55
|
+
const actualEnd = Math.min(end, this._size);
|
|
56
|
+
if (start >= actualEnd || this._size === 0) return;
|
|
57
|
+
let position = start;
|
|
58
|
+
while (position < actualEnd) {
|
|
59
|
+
if (signal?.aborted) throw new Error("Operation aborted");
|
|
60
|
+
const remaining = actualEnd - position;
|
|
61
|
+
const toRead = Math.min(bufferSize, remaining);
|
|
62
|
+
const buffer = await this.file.slice(position, position + toRead).arrayBuffer();
|
|
63
|
+
yield new Uint8Array(buffer);
|
|
64
|
+
position += buffer.byteLength;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Writes data to the file starting at the specified position.
|
|
69
|
+
* If start > 0, preserves content before start position.
|
|
70
|
+
* Truncates file at start position before writing.
|
|
71
|
+
*/
|
|
72
|
+
async createWriteStream(data, options = {}) {
|
|
73
|
+
if (this.closed) throw new Error("FileHandle is closed");
|
|
74
|
+
const { start = 0, signal } = options;
|
|
75
|
+
const writable = await this.fileHandle.createWritable({ keepExistingData: start > 0 });
|
|
76
|
+
let bytesWritten = 0;
|
|
77
|
+
try {
|
|
78
|
+
if (start > 0) {
|
|
79
|
+
await writable.seek(start);
|
|
80
|
+
await writable.truncate(start);
|
|
81
|
+
}
|
|
82
|
+
for await (const chunk of (0, __statewalker_webrun_files.toBinaryAsyncIterable)(data)) {
|
|
83
|
+
if (signal?.aborted) throw new Error("Operation aborted");
|
|
84
|
+
await writable.write(chunk);
|
|
85
|
+
bytesWritten += chunk.length;
|
|
86
|
+
}
|
|
87
|
+
} finally {
|
|
88
|
+
await writable.close();
|
|
89
|
+
}
|
|
90
|
+
this.file = await this.fileHandle.getFile();
|
|
91
|
+
this._size = this.file.size;
|
|
92
|
+
return bytesWritten;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Random access read from file at a specific position.
|
|
96
|
+
* Uses File.slice() for efficient partial reads.
|
|
97
|
+
*/
|
|
98
|
+
async read(buffer, offset, length, position) {
|
|
99
|
+
if (this.closed) throw new Error("FileHandle is closed");
|
|
100
|
+
if (!this.file) {
|
|
101
|
+
this.file = await this.fileHandle.getFile();
|
|
102
|
+
this._size = this.file.size;
|
|
103
|
+
}
|
|
104
|
+
let len = Math.min(length, buffer.length - offset);
|
|
105
|
+
len = Math.min(len, this._size - position);
|
|
106
|
+
len = Math.max(0, len);
|
|
107
|
+
if (len === 0) return 0;
|
|
108
|
+
const arrayBuffer = await this.file.slice(position, position + len).arrayBuffer();
|
|
109
|
+
const data = new Uint8Array(arrayBuffer);
|
|
110
|
+
buffer.set(data, offset);
|
|
111
|
+
return len;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
//#endregion
|
|
116
|
+
//#region src/browser-files-api.ts
|
|
117
|
+
var BrowserFilesApi = class {
|
|
118
|
+
rootHandle;
|
|
119
|
+
constructor(options) {
|
|
120
|
+
this.rootHandle = options.rootHandle;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Navigates to a directory handle from a path.
|
|
124
|
+
* Returns null if the directory doesn't exist and create is false.
|
|
125
|
+
*/
|
|
126
|
+
async getDirectoryHandle(path, options = {}) {
|
|
127
|
+
const segments = path.split("/").filter((s) => s.length > 0);
|
|
128
|
+
let current = this.rootHandle;
|
|
129
|
+
for (const segment of segments) try {
|
|
130
|
+
current = await current.getDirectoryHandle(segment, options);
|
|
131
|
+
} catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
return current;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Gets a file handle from a path.
|
|
138
|
+
* Returns null if the file doesn't exist and create is false.
|
|
139
|
+
*/
|
|
140
|
+
async getFileHandle(path, options = {}) {
|
|
141
|
+
const normalized = (0, __statewalker_webrun_files.resolveFileRef)(path);
|
|
142
|
+
const parentPath = (0, __statewalker_webrun_files.dirname)(normalized);
|
|
143
|
+
const fileName = (0, __statewalker_webrun_files.basename)(normalized);
|
|
144
|
+
const parent = await this.getDirectoryHandle(parentPath, options);
|
|
145
|
+
if (!parent) return null;
|
|
146
|
+
try {
|
|
147
|
+
return await parent.getFileHandle(fileName, options);
|
|
148
|
+
} catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Lists entries in a directory.
|
|
154
|
+
*/
|
|
155
|
+
async *list(file, options = {}) {
|
|
156
|
+
const normalized = (0, __statewalker_webrun_files.resolveFileRef)(file);
|
|
157
|
+
const dirHandle = await this.getDirectoryHandle(normalized);
|
|
158
|
+
if (!dirHandle) return;
|
|
159
|
+
const { recursive = false } = options;
|
|
160
|
+
for await (const [name, handle] of dirHandle.entries()) {
|
|
161
|
+
const entryPath = (0, __statewalker_webrun_files.joinPath)(normalized, name);
|
|
162
|
+
const isDirectory = handle.kind === "directory";
|
|
163
|
+
const info = {
|
|
164
|
+
kind: isDirectory ? "directory" : "file",
|
|
165
|
+
name,
|
|
166
|
+
path: entryPath,
|
|
167
|
+
lastModified: 0
|
|
168
|
+
};
|
|
169
|
+
if (!isDirectory) try {
|
|
170
|
+
const file$1 = await handle.getFile();
|
|
171
|
+
info.size = file$1.size;
|
|
172
|
+
info.lastModified = file$1.lastModified;
|
|
173
|
+
info.type = file$1.type;
|
|
174
|
+
} catch {}
|
|
175
|
+
yield info;
|
|
176
|
+
if (recursive && isDirectory) yield* this.list(entryPath, options);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Gets file or directory stats.
|
|
181
|
+
*/
|
|
182
|
+
async stats(file) {
|
|
183
|
+
const normalized = (0, __statewalker_webrun_files.resolveFileRef)(file);
|
|
184
|
+
const fileName = (0, __statewalker_webrun_files.basename)(normalized);
|
|
185
|
+
if (normalized === "/") return {
|
|
186
|
+
kind: "directory",
|
|
187
|
+
name: "",
|
|
188
|
+
path: "/",
|
|
189
|
+
lastModified: 0
|
|
190
|
+
};
|
|
191
|
+
const fileHandle = await this.getFileHandle(normalized);
|
|
192
|
+
if (fileHandle) try {
|
|
193
|
+
const f = await fileHandle.getFile();
|
|
194
|
+
return {
|
|
195
|
+
kind: "file",
|
|
196
|
+
name: fileName,
|
|
197
|
+
path: normalized,
|
|
198
|
+
size: f.size,
|
|
199
|
+
lastModified: f.lastModified,
|
|
200
|
+
type: f.type
|
|
201
|
+
};
|
|
202
|
+
} catch {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (await this.getDirectoryHandle(normalized)) return {
|
|
206
|
+
kind: "directory",
|
|
207
|
+
name: fileName,
|
|
208
|
+
path: normalized,
|
|
209
|
+
lastModified: 0
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Removes a file or directory.
|
|
214
|
+
*/
|
|
215
|
+
async remove(file) {
|
|
216
|
+
const normalized = (0, __statewalker_webrun_files.resolveFileRef)(file);
|
|
217
|
+
const parentPath = (0, __statewalker_webrun_files.dirname)(normalized);
|
|
218
|
+
const name = (0, __statewalker_webrun_files.basename)(normalized);
|
|
219
|
+
const parent = await this.getDirectoryHandle(parentPath);
|
|
220
|
+
if (!parent) return false;
|
|
221
|
+
try {
|
|
222
|
+
await parent.removeEntry(name, { recursive: true });
|
|
223
|
+
return true;
|
|
224
|
+
} catch {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Opens a file for reading and writing.
|
|
230
|
+
* Creates the file and parent directories if they don't exist.
|
|
231
|
+
*/
|
|
232
|
+
async open(file) {
|
|
233
|
+
const normalized = (0, __statewalker_webrun_files.resolveFileRef)(file);
|
|
234
|
+
const parentPath = (0, __statewalker_webrun_files.dirname)(normalized);
|
|
235
|
+
const fileName = (0, __statewalker_webrun_files.basename)(normalized);
|
|
236
|
+
const parent = await this.getDirectoryHandle(parentPath, { create: true });
|
|
237
|
+
if (!parent) throw new Error(`Cannot create directory: ${parentPath}`);
|
|
238
|
+
const fileHandle = await parent.getFileHandle(fileName, { create: true });
|
|
239
|
+
return new BrowserFileHandle({
|
|
240
|
+
fileHandle,
|
|
241
|
+
initialFile: await fileHandle.getFile()
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Creates a directory (and parent directories if needed).
|
|
246
|
+
*/
|
|
247
|
+
async mkdir(file) {
|
|
248
|
+
const normalized = (0, __statewalker_webrun_files.resolveFileRef)(file);
|
|
249
|
+
await this.getDirectoryHandle(normalized, { create: true });
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Moves a file or directory using copy + delete.
|
|
253
|
+
* The File System Access API doesn't have a native move operation.
|
|
254
|
+
*/
|
|
255
|
+
async move(source, target) {
|
|
256
|
+
if (!await this.copy(source, target)) return false;
|
|
257
|
+
return this.remove(source);
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Copies a file or directory.
|
|
261
|
+
*/
|
|
262
|
+
async copy(source, target, options = {}) {
|
|
263
|
+
const sourcePath = (0, __statewalker_webrun_files.resolveFileRef)(source);
|
|
264
|
+
const targetPath = (0, __statewalker_webrun_files.resolveFileRef)(target);
|
|
265
|
+
const { recursive = true } = options;
|
|
266
|
+
const sourceInfo = await this.stats(source);
|
|
267
|
+
if (!sourceInfo) return false;
|
|
268
|
+
if (sourceInfo.kind === "file") {
|
|
269
|
+
const sourceHandle = await this.getFileHandle(sourcePath);
|
|
270
|
+
if (!sourceHandle) return false;
|
|
271
|
+
const sourceFile = await sourceHandle.getFile();
|
|
272
|
+
const targetParentPath = (0, __statewalker_webrun_files.dirname)(targetPath);
|
|
273
|
+
const targetFileName = (0, __statewalker_webrun_files.basename)(targetPath);
|
|
274
|
+
const targetParent = await this.getDirectoryHandle(targetParentPath, { create: true });
|
|
275
|
+
if (!targetParent) return false;
|
|
276
|
+
const writable = await (await targetParent.getFileHandle(targetFileName, { create: true })).createWritable();
|
|
277
|
+
try {
|
|
278
|
+
const reader = sourceFile.stream().getReader();
|
|
279
|
+
while (true) {
|
|
280
|
+
const { done, value } = await reader.read();
|
|
281
|
+
if (done) break;
|
|
282
|
+
await writable.write(value);
|
|
283
|
+
}
|
|
284
|
+
} finally {
|
|
285
|
+
await writable.close();
|
|
286
|
+
}
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
if (!recursive) return false;
|
|
290
|
+
await this.getDirectoryHandle(targetPath, { create: true });
|
|
291
|
+
for await (const entry of this.list(source, { recursive: true })) {
|
|
292
|
+
if (entry.kind === "directory") continue;
|
|
293
|
+
const newTargetPath = targetPath + entry.path.substring(sourcePath.length);
|
|
294
|
+
const entryHandle = await this.getFileHandle(entry.path);
|
|
295
|
+
if (!entryHandle) continue;
|
|
296
|
+
const entryFile = await entryHandle.getFile();
|
|
297
|
+
const newTargetParentPath = (0, __statewalker_webrun_files.dirname)(newTargetPath);
|
|
298
|
+
const newTargetFileName = (0, __statewalker_webrun_files.basename)(newTargetPath);
|
|
299
|
+
const newTargetParent = await this.getDirectoryHandle(newTargetParentPath, { create: true });
|
|
300
|
+
if (!newTargetParent) continue;
|
|
301
|
+
const writable = await (await newTargetParent.getFileHandle(newTargetFileName, { create: true })).createWritable();
|
|
302
|
+
try {
|
|
303
|
+
const reader = entryFile.stream().getReader();
|
|
304
|
+
while (true) {
|
|
305
|
+
const { done, value } = await reader.read();
|
|
306
|
+
if (done) break;
|
|
307
|
+
await writable.write(value);
|
|
308
|
+
}
|
|
309
|
+
} finally {
|
|
310
|
+
await writable.close();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
/**
|
|
317
|
+
* Opens a browser directory picker and returns a BrowserFilesApi instance.
|
|
318
|
+
* Only works in secure contexts (HTTPS) and requires user gesture.
|
|
319
|
+
*/
|
|
320
|
+
async function openBrowserFilesApi() {
|
|
321
|
+
return new BrowserFilesApi({ rootHandle: await globalThis.showDirectoryPicker() });
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Gets a BrowserFilesApi instance backed by the Origin Private File System (OPFS).
|
|
325
|
+
* OPFS provides a sandboxed filesystem that persists between sessions.
|
|
326
|
+
*/
|
|
327
|
+
async function getOPFSFilesApi() {
|
|
328
|
+
return new BrowserFilesApi({ rootHandle: await navigator.storage.getDirectory() });
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
//#endregion
|
|
332
|
+
exports.BrowserFileHandle = BrowserFileHandle;
|
|
333
|
+
exports.BrowserFilesApi = BrowserFilesApi;
|
|
334
|
+
exports.getOPFSFilesApi = getOPFSFilesApi;
|
|
335
|
+
exports.openBrowserFilesApi = openBrowserFilesApi;
|