@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.
@@ -1,128 +1,18 @@
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
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _statewalker_webrun_files = require("@statewalker/webrun-files");
116
3
  //#region src/browser-files-api.ts
4
+ /**
5
+ * Browser filesystem implementation using the File System Access API.
6
+ *
7
+ * Provides FilesApi interface over browser's FileSystemDirectoryHandle.
8
+ * This enables web applications to read/write files in user-selected directories
9
+ * or the Origin Private File System (OPFS).
10
+ */
117
11
  var BrowserFilesApi = class {
118
12
  rootHandle;
119
13
  constructor(options) {
120
14
  this.rootHandle = options.rootHandle;
121
15
  }
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
16
  async getDirectoryHandle(path, options = {}) {
127
17
  const segments = path.split("/").filter((s) => s.length > 0);
128
18
  let current = this.rootHandle;
@@ -133,14 +23,10 @@ var BrowserFilesApi = class {
133
23
  }
134
24
  return current;
135
25
  }
136
- /**
137
- * Gets a file handle from a path.
138
- * Returns null if the file doesn't exist and create is false.
139
- */
140
26
  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);
27
+ const normalized = (0, _statewalker_webrun_files.normalizePath)(path);
28
+ const parentPath = (0, _statewalker_webrun_files.dirname)(normalized);
29
+ const fileName = (0, _statewalker_webrun_files.basename)(normalized);
144
30
  const parent = await this.getDirectoryHandle(parentPath, options);
145
31
  if (!parent) return null;
146
32
  try {
@@ -149,16 +35,53 @@ var BrowserFilesApi = class {
149
35
  return null;
150
36
  }
151
37
  }
152
- /**
153
- * Lists entries in a directory.
154
- */
155
- async *list(file, options = {}) {
156
- const normalized = (0, __statewalker_webrun_files.resolveFileRef)(file);
38
+ async *read(path, options) {
39
+ const fileHandle = await this.getFileHandle(path);
40
+ if (!fileHandle) return;
41
+ try {
42
+ const file = await fileHandle.getFile();
43
+ const start = options?.start ?? 0;
44
+ const length = options?.length;
45
+ const end = length !== void 0 ? start + length : file.size;
46
+ if (start >= file.size) return;
47
+ const bufferSize = 8192;
48
+ let position = start;
49
+ const actualEnd = Math.min(end, file.size);
50
+ while (position < actualEnd) {
51
+ const remaining = actualEnd - position;
52
+ const toRead = Math.min(bufferSize, remaining);
53
+ const buffer = await file.slice(position, position + toRead).arrayBuffer();
54
+ yield new Uint8Array(buffer);
55
+ position += buffer.byteLength;
56
+ }
57
+ } catch {
58
+ return;
59
+ }
60
+ }
61
+ async write(path, content) {
62
+ const normalized = (0, _statewalker_webrun_files.normalizePath)(path);
63
+ const parentPath = (0, _statewalker_webrun_files.dirname)(normalized);
64
+ const fileName = (0, _statewalker_webrun_files.basename)(normalized);
65
+ const parent = await this.getDirectoryHandle(parentPath, { create: true });
66
+ if (!parent) throw new Error(`Cannot create directory: ${parentPath}`);
67
+ const writable = await (await parent.getFileHandle(fileName, { create: true })).createWritable();
68
+ try {
69
+ for await (const chunk of content) await writable.write(chunk);
70
+ } finally {
71
+ await writable.close();
72
+ }
73
+ }
74
+ async mkdir(path) {
75
+ const normalized = (0, _statewalker_webrun_files.normalizePath)(path);
76
+ await this.getDirectoryHandle(normalized, { create: true });
77
+ }
78
+ async *list(path, options) {
79
+ const normalized = (0, _statewalker_webrun_files.normalizePath)(path);
157
80
  const dirHandle = await this.getDirectoryHandle(normalized);
158
81
  if (!dirHandle) return;
159
- const { recursive = false } = options;
82
+ const recursive = options?.recursive ?? false;
160
83
  for await (const [name, handle] of dirHandle.entries()) {
161
- const entryPath = (0, __statewalker_webrun_files.joinPath)(normalized, name);
84
+ const entryPath = (0, _statewalker_webrun_files.joinPath)(normalized, name);
162
85
  const isDirectory = handle.kind === "directory";
163
86
  const info = {
164
87
  kind: isDirectory ? "directory" : "file",
@@ -167,25 +90,18 @@ var BrowserFilesApi = class {
167
90
  lastModified: 0
168
91
  };
169
92
  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;
93
+ const file = await handle.getFile();
94
+ info.size = file.size;
95
+ info.lastModified = file.lastModified;
174
96
  } catch {}
175
97
  yield info;
176
98
  if (recursive && isDirectory) yield* this.list(entryPath, options);
177
99
  }
178
100
  }
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);
101
+ async stats(path) {
102
+ const normalized = (0, _statewalker_webrun_files.normalizePath)(path);
185
103
  if (normalized === "/") return {
186
104
  kind: "directory",
187
- name: "",
188
- path: "/",
189
105
  lastModified: 0
190
106
  };
191
107
  const fileHandle = await this.getFileHandle(normalized);
@@ -193,29 +109,24 @@ var BrowserFilesApi = class {
193
109
  const f = await fileHandle.getFile();
194
110
  return {
195
111
  kind: "file",
196
- name: fileName,
197
- path: normalized,
198
112
  size: f.size,
199
- lastModified: f.lastModified,
200
- type: f.type
113
+ lastModified: f.lastModified
201
114
  };
202
115
  } catch {
203
116
  return;
204
117
  }
205
118
  if (await this.getDirectoryHandle(normalized)) return {
206
119
  kind: "directory",
207
- name: fileName,
208
- path: normalized,
209
120
  lastModified: 0
210
121
  };
211
122
  }
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);
123
+ async exists(path) {
124
+ return await this.stats(path) !== void 0;
125
+ }
126
+ async remove(path) {
127
+ const normalized = (0, _statewalker_webrun_files.normalizePath)(path);
128
+ const parentPath = (0, _statewalker_webrun_files.dirname)(normalized);
129
+ const name = (0, _statewalker_webrun_files.basename)(normalized);
219
130
  const parent = await this.getDirectoryHandle(parentPath);
220
131
  if (!parent) return false;
221
132
  try {
@@ -225,52 +136,35 @@ var BrowserFilesApi = class {
225
136
  return false;
226
137
  }
227
138
  }
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
139
  async move(source, target) {
140
+ const sourcePath = (0, _statewalker_webrun_files.normalizePath)(source);
141
+ const targetPath = (0, _statewalker_webrun_files.normalizePath)(target);
142
+ const targetParentPath = (0, _statewalker_webrun_files.dirname)(targetPath);
143
+ const targetName = (0, _statewalker_webrun_files.basename)(targetPath);
144
+ const sourceStats = await this.stats(sourcePath);
145
+ if (!sourceStats) return false;
146
+ const sourceHandle = sourceStats.kind === "file" ? await this.getFileHandle(sourcePath) : await this.getDirectoryHandle(sourcePath);
147
+ if (!sourceHandle) return false;
148
+ if (typeof sourceHandle.move === "function") try {
149
+ const targetParent = await this.getDirectoryHandle(targetParentPath, { create: true });
150
+ if (!targetParent) return false;
151
+ await sourceHandle.move(targetParent, targetName);
152
+ return true;
153
+ } catch {}
256
154
  if (!await this.copy(source, target)) return false;
257
155
  return this.remove(source);
258
156
  }
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") {
157
+ async copy(source, target) {
158
+ const sourcePath = (0, _statewalker_webrun_files.normalizePath)(source);
159
+ const targetPath = (0, _statewalker_webrun_files.normalizePath)(target);
160
+ const sourceStats = await this.stats(source);
161
+ if (!sourceStats) return false;
162
+ if (sourceStats.kind === "file") {
269
163
  const sourceHandle = await this.getFileHandle(sourcePath);
270
164
  if (!sourceHandle) return false;
271
165
  const sourceFile = await sourceHandle.getFile();
272
- const targetParentPath = (0, __statewalker_webrun_files.dirname)(targetPath);
273
- const targetFileName = (0, __statewalker_webrun_files.basename)(targetPath);
166
+ const targetParentPath = (0, _statewalker_webrun_files.dirname)(targetPath);
167
+ const targetFileName = (0, _statewalker_webrun_files.basename)(targetPath);
274
168
  const targetParent = await this.getDirectoryHandle(targetParentPath, { create: true });
275
169
  if (!targetParent) return false;
276
170
  const writable = await (await targetParent.getFileHandle(targetFileName, { create: true })).createWritable();
@@ -286,7 +180,6 @@ var BrowserFilesApi = class {
286
180
  }
287
181
  return true;
288
182
  }
289
- if (!recursive) return false;
290
183
  await this.getDirectoryHandle(targetPath, { create: true });
291
184
  for await (const entry of this.list(source, { recursive: true })) {
292
185
  if (entry.kind === "directory") continue;
@@ -294,8 +187,8 @@ var BrowserFilesApi = class {
294
187
  const entryHandle = await this.getFileHandle(entry.path);
295
188
  if (!entryHandle) continue;
296
189
  const entryFile = await entryHandle.getFile();
297
- const newTargetParentPath = (0, __statewalker_webrun_files.dirname)(newTargetPath);
298
- const newTargetFileName = (0, __statewalker_webrun_files.basename)(newTargetPath);
190
+ const newTargetParentPath = (0, _statewalker_webrun_files.dirname)(newTargetPath);
191
+ const newTargetFileName = (0, _statewalker_webrun_files.basename)(newTargetPath);
299
192
  const newTargetParent = await this.getDirectoryHandle(newTargetParentPath, { create: true });
300
193
  if (!newTargetParent) continue;
301
194
  const writable = await (await newTargetParent.getFileHandle(newTargetFileName, { create: true })).createWritable();
@@ -314,22 +207,79 @@ var BrowserFilesApi = class {
314
207
  }
315
208
  };
316
209
  /**
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
210
  * Gets a BrowserFilesApi instance backed by the Origin Private File System (OPFS).
325
- * OPFS provides a sandboxed filesystem that persists between sessions.
326
211
  */
327
212
  async function getOPFSFilesApi() {
328
213
  return new BrowserFilesApi({ rootHandle: await navigator.storage.getDirectory() });
329
214
  }
330
-
331
215
  //#endregion
332
- exports.BrowserFileHandle = BrowserFileHandle;
216
+ //#region src/open-browser-files-api.ts
217
+ /**
218
+ * Utilities for opening and managing browser file system access.
219
+ *
220
+ * Provides functions for permission verification, handle accessibility checking,
221
+ * and opening directory pickers with optional persistent storage support.
222
+ */
223
+ /**
224
+ * Checks if a file system handle is still accessible by performing a real read operation.
225
+ * Handles may become inaccessible if the underlying file/directory was moved or deleted.
226
+ *
227
+ * @param fileHandle - The file or directory handle to check
228
+ * @returns True if the handle is accessible, false otherwise
229
+ */
230
+ async function isHandlerAccessible(fileHandle) {
231
+ try {
232
+ if (fileHandle.kind === "file") await fileHandle.getFile();
233
+ else for await (const _item of fileHandle.values()) break;
234
+ return true;
235
+ } catch {
236
+ return false;
237
+ }
238
+ }
239
+ /**
240
+ * Verifies and requests file system permissions for a handle.
241
+ * First checks if permission was already granted, then requests if needed.
242
+ *
243
+ * @param fileHandle - The file or directory handle to verify permissions for
244
+ * @param readWrite - Whether to request read-write access (true) or read-only (false)
245
+ * @returns True if permission is granted, false otherwise
246
+ */
247
+ async function verifyPermission(fileHandle, readWrite = false) {
248
+ const handle = fileHandle;
249
+ const options = {};
250
+ if (readWrite) options.mode = "readwrite";
251
+ if (await handle.queryPermission(options) === "granted") return true;
252
+ if (await handle.requestPermission(options) === "granted") return true;
253
+ return false;
254
+ }
255
+ /**
256
+ * Opens a browser directory picker and returns a BrowserFilesApi instance.
257
+ * Only works in secure contexts (HTTPS) and requires user gesture.
258
+ *
259
+ * Supports persistent storage of directory handles via optional get/set/del functions.
260
+ * When persistence is configured, will attempt to reuse a previously selected directory.
261
+ *
262
+ * @param options - Configuration options for opening the files API
263
+ * @returns A BrowserFilesApi instance for the selected directory
264
+ * @throws Error if access is not granted or the directory is not accessible
265
+ */
266
+ async function openBrowserFilesApi(options = {}) {
267
+ const { handlerKey = "root-dir", readwrite = true, get = async () => void 0, set = async () => {}, del = async () => {} } = options;
268
+ let rootHandle = await get(handlerKey);
269
+ if (!rootHandle) {
270
+ rootHandle = await globalThis.showDirectoryPicker();
271
+ await set(handlerKey, rootHandle);
272
+ }
273
+ if (!await verifyPermission(rootHandle, readwrite)) throw new Error("Access was not granted");
274
+ if (!await isHandlerAccessible(rootHandle)) {
275
+ await del(handlerKey);
276
+ throw new Error("Cannot access the folder. Please try again.");
277
+ }
278
+ return new BrowserFilesApi({ rootHandle });
279
+ }
280
+ //#endregion
333
281
  exports.BrowserFilesApi = BrowserFilesApi;
334
282
  exports.getOPFSFilesApi = getOPFSFilesApi;
335
- exports.openBrowserFilesApi = openBrowserFilesApi;
283
+ exports.isHandlerAccessible = isHandlerAccessible;
284
+ exports.openBrowserFilesApi = openBrowserFilesApi;
285
+ exports.verifyPermission = verifyPermission;