@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 +98 -98
- package/dist/browser-files-api.d.ts +19 -47
- package/dist/browser-files-api.d.ts.map +1 -1
- package/dist/cjs/index.cjs +161 -211
- package/dist/esm/index.js +149 -201
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/open-browser-files-api.d.ts +67 -0
- package/dist/open-browser-files-api.d.ts.map +1 -0
- package/package.json +15 -14
- package/src/browser-files-api.ts +123 -103
- package/src/index.ts +7 -4
- package/src/open-browser-files-api.ts +148 -0
- package/LICENSE +0 -21
- package/dist/browser-file-handle.d.ts +0 -42
- package/dist/browser-file-handle.d.ts.map +0 -1
- package/src/browser-file-handle.ts +0 -194
package/src/browser-files-api.ts
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Browser implementation of
|
|
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
7
|
|
|
8
8
|
import type {
|
|
9
|
-
CopyOptions,
|
|
10
|
-
FileHandle,
|
|
11
9
|
FileInfo,
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
FileStats,
|
|
11
|
+
FilesApi,
|
|
14
12
|
ListOptions,
|
|
13
|
+
ReadOptions,
|
|
15
14
|
} from "@statewalker/webrun-files";
|
|
16
|
-
import { basename, dirname, joinPath,
|
|
17
|
-
|
|
15
|
+
import { basename, dirname, joinPath, normalizePath } from "@statewalker/webrun-files";
|
|
16
|
+
|
|
17
|
+
/** Chrome 110+ exposes move() on FileSystemHandle; not yet in TS DOM types. */
|
|
18
|
+
interface MovableHandle extends FileSystemHandle {
|
|
19
|
+
move(destination: FileSystemDirectoryHandle, name: string): Promise<void>;
|
|
20
|
+
}
|
|
18
21
|
|
|
19
22
|
export interface BrowserFilesApiOptions {
|
|
20
23
|
/**
|
|
@@ -24,17 +27,20 @@ export interface BrowserFilesApiOptions {
|
|
|
24
27
|
rootHandle: FileSystemDirectoryHandle;
|
|
25
28
|
}
|
|
26
29
|
|
|
27
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Browser filesystem implementation using the File System Access API.
|
|
32
|
+
*
|
|
33
|
+
* Provides FilesApi interface over browser's FileSystemDirectoryHandle.
|
|
34
|
+
* This enables web applications to read/write files in user-selected directories
|
|
35
|
+
* or the Origin Private File System (OPFS).
|
|
36
|
+
*/
|
|
37
|
+
export class BrowserFilesApi implements FilesApi {
|
|
28
38
|
private rootHandle: FileSystemDirectoryHandle;
|
|
29
39
|
|
|
30
40
|
constructor(options: BrowserFilesApiOptions) {
|
|
31
41
|
this.rootHandle = options.rootHandle;
|
|
32
42
|
}
|
|
33
43
|
|
|
34
|
-
/**
|
|
35
|
-
* Navigates to a directory handle from a path.
|
|
36
|
-
* Returns null if the directory doesn't exist and create is false.
|
|
37
|
-
*/
|
|
38
44
|
private async getDirectoryHandle(
|
|
39
45
|
path: string,
|
|
40
46
|
options: { create?: boolean } = {},
|
|
@@ -53,15 +59,11 @@ export class BrowserFilesApi implements IFilesApi {
|
|
|
53
59
|
return current;
|
|
54
60
|
}
|
|
55
61
|
|
|
56
|
-
/**
|
|
57
|
-
* Gets a file handle from a path.
|
|
58
|
-
* Returns null if the file doesn't exist and create is false.
|
|
59
|
-
*/
|
|
60
62
|
private async getFileHandle(
|
|
61
63
|
path: string,
|
|
62
64
|
options: { create?: boolean } = {},
|
|
63
65
|
): Promise<FileSystemFileHandle | null> {
|
|
64
|
-
const normalized =
|
|
66
|
+
const normalized = normalizePath(path);
|
|
65
67
|
const parentPath = dirname(normalized);
|
|
66
68
|
const fileName = basename(normalized);
|
|
67
69
|
|
|
@@ -75,15 +77,71 @@ export class BrowserFilesApi implements IFilesApi {
|
|
|
75
77
|
}
|
|
76
78
|
}
|
|
77
79
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
async *read(path: string, options?: ReadOptions): AsyncIterable<Uint8Array> {
|
|
81
|
+
const fileHandle = await this.getFileHandle(path);
|
|
82
|
+
if (!fileHandle) return;
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const file = await fileHandle.getFile();
|
|
86
|
+
const start = options?.start ?? 0;
|
|
87
|
+
const length = options?.length;
|
|
88
|
+
const end = length !== undefined ? start + length : file.size;
|
|
89
|
+
|
|
90
|
+
if (start >= file.size) return;
|
|
91
|
+
|
|
92
|
+
const bufferSize = 8192;
|
|
93
|
+
let position = start;
|
|
94
|
+
const actualEnd = Math.min(end, file.size);
|
|
95
|
+
|
|
96
|
+
while (position < actualEnd) {
|
|
97
|
+
const remaining = actualEnd - position;
|
|
98
|
+
const toRead = Math.min(bufferSize, remaining);
|
|
99
|
+
const slice = file.slice(position, position + toRead);
|
|
100
|
+
const buffer = await slice.arrayBuffer();
|
|
101
|
+
yield new Uint8Array(buffer);
|
|
102
|
+
position += buffer.byteLength;
|
|
103
|
+
}
|
|
104
|
+
} catch {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async write(
|
|
110
|
+
path: string,
|
|
111
|
+
content: Iterable<Uint8Array> | AsyncIterable<Uint8Array>,
|
|
112
|
+
): Promise<void> {
|
|
113
|
+
const normalized = normalizePath(path);
|
|
114
|
+
const parentPath = dirname(normalized);
|
|
115
|
+
const fileName = basename(normalized);
|
|
116
|
+
|
|
117
|
+
const parent = await this.getDirectoryHandle(parentPath, { create: true });
|
|
118
|
+
if (!parent) {
|
|
119
|
+
throw new Error(`Cannot create directory: ${parentPath}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const fileHandle = await parent.getFileHandle(fileName, { create: true });
|
|
123
|
+
const writable = await fileHandle.createWritable();
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
for await (const chunk of content) {
|
|
127
|
+
await writable.write(chunk as Uint8Array<ArrayBuffer>);
|
|
128
|
+
}
|
|
129
|
+
} finally {
|
|
130
|
+
await writable.close();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async mkdir(path: string): Promise<void> {
|
|
135
|
+
const normalized = normalizePath(path);
|
|
136
|
+
await this.getDirectoryHandle(normalized, { create: true });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async *list(path: string, options?: ListOptions): AsyncIterable<FileInfo> {
|
|
140
|
+
const normalized = normalizePath(path);
|
|
83
141
|
const dirHandle = await this.getDirectoryHandle(normalized);
|
|
84
142
|
if (!dirHandle) return;
|
|
85
143
|
|
|
86
|
-
const
|
|
144
|
+
const recursive = options?.recursive ?? false;
|
|
87
145
|
|
|
88
146
|
for await (const [name, handle] of dirHandle.entries()) {
|
|
89
147
|
const entryPath = joinPath(normalized, name);
|
|
@@ -102,7 +160,6 @@ export class BrowserFilesApi implements IFilesApi {
|
|
|
102
160
|
const file = await fileHandle.getFile();
|
|
103
161
|
info.size = file.size;
|
|
104
162
|
info.lastModified = file.lastModified;
|
|
105
|
-
info.type = file.type;
|
|
106
163
|
} catch {
|
|
107
164
|
// Ignore errors getting file metadata
|
|
108
165
|
}
|
|
@@ -116,19 +173,12 @@ export class BrowserFilesApi implements IFilesApi {
|
|
|
116
173
|
}
|
|
117
174
|
}
|
|
118
175
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
*/
|
|
122
|
-
async stats(file: FileRef): Promise<FileInfo | undefined> {
|
|
123
|
-
const normalized = resolveFileRef(file);
|
|
124
|
-
const fileName = basename(normalized);
|
|
176
|
+
async stats(path: string): Promise<FileStats | undefined> {
|
|
177
|
+
const normalized = normalizePath(path);
|
|
125
178
|
|
|
126
|
-
// Handle root directory
|
|
127
179
|
if (normalized === "/") {
|
|
128
180
|
return {
|
|
129
181
|
kind: "directory",
|
|
130
|
-
name: "",
|
|
131
|
-
path: "/",
|
|
132
182
|
lastModified: 0,
|
|
133
183
|
};
|
|
134
184
|
}
|
|
@@ -140,11 +190,8 @@ export class BrowserFilesApi implements IFilesApi {
|
|
|
140
190
|
const f = await fileHandle.getFile();
|
|
141
191
|
return {
|
|
142
192
|
kind: "file",
|
|
143
|
-
name: fileName,
|
|
144
|
-
path: normalized,
|
|
145
193
|
size: f.size,
|
|
146
194
|
lastModified: f.lastModified,
|
|
147
|
-
type: f.type,
|
|
148
195
|
};
|
|
149
196
|
} catch {
|
|
150
197
|
return undefined;
|
|
@@ -156,8 +203,6 @@ export class BrowserFilesApi implements IFilesApi {
|
|
|
156
203
|
if (dirHandle) {
|
|
157
204
|
return {
|
|
158
205
|
kind: "directory",
|
|
159
|
-
name: fileName,
|
|
160
|
-
path: normalized,
|
|
161
206
|
lastModified: 0,
|
|
162
207
|
};
|
|
163
208
|
}
|
|
@@ -165,11 +210,13 @@ export class BrowserFilesApi implements IFilesApi {
|
|
|
165
210
|
return undefined;
|
|
166
211
|
}
|
|
167
212
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
213
|
+
async exists(path: string): Promise<boolean> {
|
|
214
|
+
const stats = await this.stats(path);
|
|
215
|
+
return stats !== undefined;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async remove(path: string): Promise<boolean> {
|
|
219
|
+
const normalized = normalizePath(path);
|
|
173
220
|
const parentPath = dirname(normalized);
|
|
174
221
|
const name = basename(normalized);
|
|
175
222
|
|
|
@@ -184,65 +231,52 @@ export class BrowserFilesApi implements IFilesApi {
|
|
|
184
231
|
}
|
|
185
232
|
}
|
|
186
233
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const normalized = resolveFileRef(file);
|
|
193
|
-
const parentPath = dirname(normalized);
|
|
194
|
-
const fileName = basename(normalized);
|
|
195
|
-
|
|
196
|
-
// Ensure parent directory exists
|
|
197
|
-
const parent = await this.getDirectoryHandle(parentPath, { create: true });
|
|
198
|
-
if (!parent) {
|
|
199
|
-
throw new Error(`Cannot create directory: ${parentPath}`);
|
|
200
|
-
}
|
|
234
|
+
async move(source: string, target: string): Promise<boolean> {
|
|
235
|
+
const sourcePath = normalizePath(source);
|
|
236
|
+
const targetPath = normalizePath(target);
|
|
237
|
+
const targetParentPath = dirname(targetPath);
|
|
238
|
+
const targetName = basename(targetPath);
|
|
201
239
|
|
|
202
|
-
//
|
|
203
|
-
const
|
|
204
|
-
|
|
240
|
+
// Try native move() (available in Chrome 110+, Edge 110+)
|
|
241
|
+
const sourceStats = await this.stats(sourcePath);
|
|
242
|
+
if (!sourceStats) return false;
|
|
205
243
|
|
|
206
|
-
|
|
207
|
-
|
|
244
|
+
const sourceHandle: FileSystemHandle | null =
|
|
245
|
+
sourceStats.kind === "file"
|
|
246
|
+
? await this.getFileHandle(sourcePath)
|
|
247
|
+
: await this.getDirectoryHandle(sourcePath);
|
|
248
|
+
if (!sourceHandle) return false;
|
|
208
249
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
250
|
+
if (typeof (sourceHandle as MovableHandle).move === "function") {
|
|
251
|
+
try {
|
|
252
|
+
const targetParent = await this.getDirectoryHandle(targetParentPath, { create: true });
|
|
253
|
+
if (!targetParent) return false;
|
|
254
|
+
await (sourceHandle as MovableHandle).move(targetParent, targetName);
|
|
255
|
+
return true;
|
|
256
|
+
} catch {
|
|
257
|
+
// Fall through to copy-then-delete
|
|
258
|
+
}
|
|
259
|
+
}
|
|
216
260
|
|
|
217
|
-
/**
|
|
218
|
-
* Moves a file or directory using copy + delete.
|
|
219
|
-
* The File System Access API doesn't have a native move operation.
|
|
220
|
-
*/
|
|
221
|
-
async move(source: FileRef, target: FileRef): Promise<boolean> {
|
|
222
261
|
const copied = await this.copy(source, target);
|
|
223
262
|
if (!copied) return false;
|
|
224
263
|
return this.remove(source);
|
|
225
264
|
}
|
|
226
265
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
async copy(source: FileRef, target: FileRef, options: CopyOptions = {}): Promise<boolean> {
|
|
231
|
-
const sourcePath = resolveFileRef(source);
|
|
232
|
-
const targetPath = resolveFileRef(target);
|
|
233
|
-
const { recursive = true } = options;
|
|
266
|
+
async copy(source: string, target: string): Promise<boolean> {
|
|
267
|
+
const sourcePath = normalizePath(source);
|
|
268
|
+
const targetPath = normalizePath(target);
|
|
234
269
|
|
|
235
|
-
const
|
|
236
|
-
if (!
|
|
270
|
+
const sourceStats = await this.stats(source);
|
|
271
|
+
if (!sourceStats) return false;
|
|
237
272
|
|
|
238
|
-
if (
|
|
273
|
+
if (sourceStats.kind === "file") {
|
|
239
274
|
// Copy single file
|
|
240
275
|
const sourceHandle = await this.getFileHandle(sourcePath);
|
|
241
276
|
if (!sourceHandle) return false;
|
|
242
277
|
|
|
243
278
|
const sourceFile = await sourceHandle.getFile();
|
|
244
279
|
|
|
245
|
-
// Create target file
|
|
246
280
|
const targetParentPath = dirname(targetPath);
|
|
247
281
|
const targetFileName = basename(targetPath);
|
|
248
282
|
|
|
@@ -253,7 +287,6 @@ export class BrowserFilesApi implements IFilesApi {
|
|
|
253
287
|
const writable = await targetHandle.createWritable();
|
|
254
288
|
|
|
255
289
|
try {
|
|
256
|
-
// Stream the file content
|
|
257
290
|
const reader = sourceFile.stream().getReader();
|
|
258
291
|
while (true) {
|
|
259
292
|
const { done, value } = await reader.read();
|
|
@@ -267,13 +300,9 @@ export class BrowserFilesApi implements IFilesApi {
|
|
|
267
300
|
return true;
|
|
268
301
|
}
|
|
269
302
|
|
|
270
|
-
// Copy directory
|
|
271
|
-
if (!recursive) return false;
|
|
272
|
-
|
|
273
|
-
// Create target directory
|
|
303
|
+
// Copy directory recursively
|
|
274
304
|
await this.getDirectoryHandle(targetPath, { create: true });
|
|
275
305
|
|
|
276
|
-
// Copy all entries recursively
|
|
277
306
|
for await (const entry of this.list(source, { recursive: true })) {
|
|
278
307
|
if (entry.kind === "directory") continue;
|
|
279
308
|
|
|
@@ -291,7 +320,9 @@ export class BrowserFilesApi implements IFilesApi {
|
|
|
291
320
|
const newTargetParent = await this.getDirectoryHandle(newTargetParentPath, { create: true });
|
|
292
321
|
if (!newTargetParent) continue;
|
|
293
322
|
|
|
294
|
-
const newTargetHandle = await newTargetParent.getFileHandle(newTargetFileName, {
|
|
323
|
+
const newTargetHandle = await newTargetParent.getFileHandle(newTargetFileName, {
|
|
324
|
+
create: true,
|
|
325
|
+
});
|
|
295
326
|
const writable = await newTargetHandle.createWritable();
|
|
296
327
|
|
|
297
328
|
try {
|
|
@@ -310,19 +341,8 @@ export class BrowserFilesApi implements IFilesApi {
|
|
|
310
341
|
}
|
|
311
342
|
}
|
|
312
343
|
|
|
313
|
-
/**
|
|
314
|
-
* Opens a browser directory picker and returns a BrowserFilesApi instance.
|
|
315
|
-
* Only works in secure contexts (HTTPS) and requires user gesture.
|
|
316
|
-
*/
|
|
317
|
-
export async function openBrowserFilesApi(): Promise<BrowserFilesApi> {
|
|
318
|
-
// Cast to any since TypeScript's lib.dom.d.ts may not include showDirectoryPicker
|
|
319
|
-
const rootHandle = await (globalThis as unknown as { showDirectoryPicker: () => Promise<FileSystemDirectoryHandle> }).showDirectoryPicker();
|
|
320
|
-
return new BrowserFilesApi({ rootHandle });
|
|
321
|
-
}
|
|
322
|
-
|
|
323
344
|
/**
|
|
324
345
|
* Gets a BrowserFilesApi instance backed by the Origin Private File System (OPFS).
|
|
325
|
-
* OPFS provides a sandboxed filesystem that persists between sessions.
|
|
326
346
|
*/
|
|
327
347
|
export async function getOPFSFilesApi(): Promise<BrowserFilesApi> {
|
|
328
348
|
const rootHandle = await navigator.storage.getDirectory();
|
package/src/index.ts
CHANGED
|
@@ -4,9 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
export {
|
|
6
6
|
BrowserFilesApi,
|
|
7
|
-
getOPFSFilesApi,
|
|
8
|
-
openBrowserFilesApi,
|
|
9
7
|
type BrowserFilesApiOptions,
|
|
8
|
+
getOPFSFilesApi,
|
|
10
9
|
} from "./browser-files-api.js";
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
export {
|
|
11
|
+
isHandlerAccessible,
|
|
12
|
+
type OpenBrowserFilesApiOptions,
|
|
13
|
+
openBrowserFilesApi,
|
|
14
|
+
verifyPermission,
|
|
15
|
+
} from "./open-browser-files-api.js";
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for opening and managing browser file system access.
|
|
3
|
+
*
|
|
4
|
+
* Provides functions for permission verification, handle accessibility checking,
|
|
5
|
+
* and opening directory pickers with optional persistent storage support.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { BrowserFilesApi } from "./browser-files-api.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Extended FileSystemHandle interface with permission methods.
|
|
12
|
+
* These are part of the File System Access API but not fully typed in TypeScript's lib.dom.d.ts.
|
|
13
|
+
*/
|
|
14
|
+
interface FileSystemHandleWithPermissions {
|
|
15
|
+
queryPermission(descriptor?: { mode?: "read" | "readwrite" }): Promise<PermissionState>;
|
|
16
|
+
requestPermission(descriptor?: { mode?: "read" | "readwrite" }): Promise<PermissionState>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Options for opening a browser files API with persistent storage support.
|
|
21
|
+
*/
|
|
22
|
+
export interface OpenBrowserFilesApiOptions {
|
|
23
|
+
/**
|
|
24
|
+
* Key used to store the directory handler in persistent storage.
|
|
25
|
+
* @default "root-dir"
|
|
26
|
+
*/
|
|
27
|
+
handlerKey?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Whether to request read-write access (true) or read-only (false).
|
|
30
|
+
* @default true
|
|
31
|
+
*/
|
|
32
|
+
readwrite?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Gets a stored directory handler by key.
|
|
35
|
+
* @default Returns undefined (no persistence)
|
|
36
|
+
*/
|
|
37
|
+
get?: (key: string) => Promise<FileSystemDirectoryHandle | undefined>;
|
|
38
|
+
/**
|
|
39
|
+
* Stores a directory handler with a key.
|
|
40
|
+
* @default No-op (no persistence)
|
|
41
|
+
*/
|
|
42
|
+
set?: (key: string, handler: FileSystemDirectoryHandle) => Promise<void>;
|
|
43
|
+
/**
|
|
44
|
+
* Deletes a stored directory handler by key.
|
|
45
|
+
* @default No-op (no persistence)
|
|
46
|
+
*/
|
|
47
|
+
del?: (key: string) => Promise<void>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Checks if a file system handle is still accessible by performing a real read operation.
|
|
52
|
+
* Handles may become inaccessible if the underlying file/directory was moved or deleted.
|
|
53
|
+
*
|
|
54
|
+
* @param fileHandle - The file or directory handle to check
|
|
55
|
+
* @returns True if the handle is accessible, false otherwise
|
|
56
|
+
*/
|
|
57
|
+
export async function isHandlerAccessible(
|
|
58
|
+
fileHandle: FileSystemFileHandle | FileSystemDirectoryHandle,
|
|
59
|
+
): Promise<boolean> {
|
|
60
|
+
try {
|
|
61
|
+
if (fileHandle.kind === "file") {
|
|
62
|
+
await (fileHandle as FileSystemFileHandle).getFile();
|
|
63
|
+
} else {
|
|
64
|
+
// For directories, iterate to check accessibility
|
|
65
|
+
for await (const _item of (fileHandle as FileSystemDirectoryHandle).values()) {
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return true;
|
|
70
|
+
} catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Verifies and requests file system permissions for a handle.
|
|
77
|
+
* First checks if permission was already granted, then requests if needed.
|
|
78
|
+
*
|
|
79
|
+
* @param fileHandle - The file or directory handle to verify permissions for
|
|
80
|
+
* @param readWrite - Whether to request read-write access (true) or read-only (false)
|
|
81
|
+
* @returns True if permission is granted, false otherwise
|
|
82
|
+
*/
|
|
83
|
+
export async function verifyPermission(
|
|
84
|
+
fileHandle: FileSystemFileHandle | FileSystemDirectoryHandle,
|
|
85
|
+
readWrite: boolean = false,
|
|
86
|
+
): Promise<boolean> {
|
|
87
|
+
const handle = fileHandle as unknown as FileSystemHandleWithPermissions;
|
|
88
|
+
const options: { mode?: "read" | "readwrite" } = {};
|
|
89
|
+
if (readWrite) {
|
|
90
|
+
options.mode = "readwrite";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Check if permission was already granted
|
|
94
|
+
if ((await handle.queryPermission(options)) === "granted") {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Request permission if not already granted
|
|
99
|
+
if ((await handle.requestPermission(options)) === "granted") {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Opens a browser directory picker and returns a BrowserFilesApi instance.
|
|
108
|
+
* Only works in secure contexts (HTTPS) and requires user gesture.
|
|
109
|
+
*
|
|
110
|
+
* Supports persistent storage of directory handles via optional get/set/del functions.
|
|
111
|
+
* When persistence is configured, will attempt to reuse a previously selected directory.
|
|
112
|
+
*
|
|
113
|
+
* @param options - Configuration options for opening the files API
|
|
114
|
+
* @returns A BrowserFilesApi instance for the selected directory
|
|
115
|
+
* @throws Error if access is not granted or the directory is not accessible
|
|
116
|
+
*/
|
|
117
|
+
export async function openBrowserFilesApi(
|
|
118
|
+
options: OpenBrowserFilesApiOptions = {},
|
|
119
|
+
): Promise<BrowserFilesApi> {
|
|
120
|
+
const {
|
|
121
|
+
handlerKey = "root-dir",
|
|
122
|
+
readwrite = true,
|
|
123
|
+
get = async () => undefined,
|
|
124
|
+
set = async () => {},
|
|
125
|
+
del = async () => {},
|
|
126
|
+
} = options;
|
|
127
|
+
|
|
128
|
+
let rootHandle = await get(handlerKey);
|
|
129
|
+
|
|
130
|
+
if (!rootHandle) {
|
|
131
|
+
// Cast to any since TypeScript's lib.dom.d.ts may not include showDirectoryPicker
|
|
132
|
+
rootHandle = await (
|
|
133
|
+
globalThis as unknown as { showDirectoryPicker: () => Promise<FileSystemDirectoryHandle> }
|
|
134
|
+
).showDirectoryPicker();
|
|
135
|
+
await set(handlerKey, rootHandle);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!(await verifyPermission(rootHandle, readwrite))) {
|
|
139
|
+
throw new Error("Access was not granted");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!(await isHandlerAccessible(rootHandle))) {
|
|
143
|
+
await del(handlerKey);
|
|
144
|
+
throw new Error("Cannot access the folder. Please try again.");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return new BrowserFilesApi({ rootHandle });
|
|
148
|
+
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
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.
|
|
@@ -1,42 +0,0 @@
|
|
|
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
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|