@flighthq/filesystem 0.1.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/dist/filesystem.d.ts +45 -0
- package/dist/filesystem.d.ts.map +1 -0
- package/dist/filesystem.js +760 -0
- package/dist/filesystem.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/package.json +38 -0
- package/src/filesystem.test.ts +909 -0
|
@@ -0,0 +1,760 @@
|
|
|
1
|
+
import { getWebFileSystemHandle } from '@flighthq/dialog';
|
|
2
|
+
// Appends text to a file, creating it when missing. Returns false when the host denies access.
|
|
3
|
+
export function appendTextFile(path, data) {
|
|
4
|
+
return getFileSystemBackend().appendTextFile(path, data);
|
|
5
|
+
}
|
|
6
|
+
// True when the file/directory at `path` can be accessed in the given mode. Web returns false for
|
|
7
|
+
// 'executable'; 'readable' and 'writable' are best-effort via fileExists / createWritable probe.
|
|
8
|
+
export function canAccessFile(path, mode) {
|
|
9
|
+
return getFileSystemBackend().canAccessFile(path, mode);
|
|
10
|
+
}
|
|
11
|
+
// Copies a file from `from` to `to`. Returns false when the source is missing or access is denied.
|
|
12
|
+
export function copyFile(from, to) {
|
|
13
|
+
return getFileSystemBackend().copy(from, to);
|
|
14
|
+
}
|
|
15
|
+
// Creates a symbolic link at `linkPath` pointing to `target`. Returns false when the operation is
|
|
16
|
+
// unsupported (web/OPFS always returns false — OPFS has no symlinks; native-only capability).
|
|
17
|
+
export function createFileSymlink(target, linkPath) {
|
|
18
|
+
return getFileSystemBackend().createFileSymlink(target, linkPath);
|
|
19
|
+
}
|
|
20
|
+
// Builds the default web backend over the Origin Private File System (OPFS). `path` is treated as a
|
|
21
|
+
// '/'-separated relative path within the OPFS root. Every API touch is guarded and wrapped in try/catch;
|
|
22
|
+
// when navigator.storage.getDirectory is absent (e.g. jsdom) all ops resolve to sentinels (null/false/[]).
|
|
23
|
+
export function createWebFileSystemBackend() {
|
|
24
|
+
return {
|
|
25
|
+
async readTextFile(path) {
|
|
26
|
+
const handle = await getWebFileHandle(path, false);
|
|
27
|
+
if (handle === null)
|
|
28
|
+
return null;
|
|
29
|
+
try {
|
|
30
|
+
const file = await handle.getFile();
|
|
31
|
+
return await file.text();
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
async writeTextFile(path, data) {
|
|
38
|
+
return writeWebFile(path, data);
|
|
39
|
+
},
|
|
40
|
+
async readBinaryFile(path) {
|
|
41
|
+
const handle = await getWebFileHandle(path, false);
|
|
42
|
+
if (handle === null)
|
|
43
|
+
return null;
|
|
44
|
+
try {
|
|
45
|
+
const file = await handle.getFile();
|
|
46
|
+
return new Uint8Array(await file.arrayBuffer());
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
async readBinaryFileRange(path, offset, length) {
|
|
53
|
+
const handle = await getWebFileHandle(path, false);
|
|
54
|
+
if (handle === null)
|
|
55
|
+
return null;
|
|
56
|
+
try {
|
|
57
|
+
const file = await handle.getFile();
|
|
58
|
+
if (offset >= file.size)
|
|
59
|
+
return new Uint8Array(0);
|
|
60
|
+
const slice = file.slice(offset, offset + length);
|
|
61
|
+
return new Uint8Array(await slice.arrayBuffer());
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
async writeBinaryFile(path, data) {
|
|
68
|
+
// Copy into a fresh buffer; the Readonly<Uint8Array> input must not be mutated and write() needs a
|
|
69
|
+
// BufferSource it can consume.
|
|
70
|
+
return writeWebFile(path, data.slice());
|
|
71
|
+
},
|
|
72
|
+
async fileExists(path) {
|
|
73
|
+
return (await getWebFileHandle(path, false)) !== null;
|
|
74
|
+
},
|
|
75
|
+
async directoryExists(path) {
|
|
76
|
+
const root = await getWebRoot();
|
|
77
|
+
if (root === null)
|
|
78
|
+
return false;
|
|
79
|
+
return (await getWebDirectoryHandle(root, splitWebPath(path), false)) !== null;
|
|
80
|
+
},
|
|
81
|
+
async removeFile(path) {
|
|
82
|
+
return writeWebRemove(path, false);
|
|
83
|
+
},
|
|
84
|
+
async removeDirectory(path, recursive = false) {
|
|
85
|
+
const root = await getWebRoot();
|
|
86
|
+
if (root === null)
|
|
87
|
+
return false;
|
|
88
|
+
const segments = splitWebPath(path);
|
|
89
|
+
if (segments.length === 0)
|
|
90
|
+
return false;
|
|
91
|
+
// Verify the target is actually a directory before removing it.
|
|
92
|
+
const parent = await getWebDirectoryHandle(root, segments.slice(0, -1), false);
|
|
93
|
+
if (parent === null)
|
|
94
|
+
return false;
|
|
95
|
+
try {
|
|
96
|
+
await parent.removeEntry(segments[segments.length - 1], { recursive });
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
async makeDirectory(path) {
|
|
104
|
+
const root = await getWebRoot();
|
|
105
|
+
if (root === null)
|
|
106
|
+
return false;
|
|
107
|
+
const created = await getWebDirectoryHandle(root, splitWebPath(path), true);
|
|
108
|
+
return created !== null;
|
|
109
|
+
},
|
|
110
|
+
async readDirectory(path) {
|
|
111
|
+
const root = await getWebRoot();
|
|
112
|
+
if (root === null)
|
|
113
|
+
return [];
|
|
114
|
+
const dir = await getWebDirectoryHandle(root, splitWebPath(path), false);
|
|
115
|
+
if (dir === null)
|
|
116
|
+
return [];
|
|
117
|
+
const entries = [];
|
|
118
|
+
try {
|
|
119
|
+
const base = normalizeWebPath(path);
|
|
120
|
+
for await (const [name, handle] of asAsyncEntries(dir)) {
|
|
121
|
+
const isDirectory = handle.kind === 'directory';
|
|
122
|
+
entries.push({ name, path: base === '' ? name : `${base}/${name}`, isDirectory });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
return entries;
|
|
129
|
+
},
|
|
130
|
+
async readDirectoryRecursive(path, options) {
|
|
131
|
+
const root = await getWebRoot();
|
|
132
|
+
if (root === null)
|
|
133
|
+
return [];
|
|
134
|
+
const dir = await getWebDirectoryHandle(root, splitWebPath(path), false);
|
|
135
|
+
if (dir === null)
|
|
136
|
+
return [];
|
|
137
|
+
const base = normalizeWebPath(path);
|
|
138
|
+
const results = [];
|
|
139
|
+
try {
|
|
140
|
+
await walkWebDirectory(dir, base, results, 0, options?.maxDepth ?? Infinity);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
return results;
|
|
146
|
+
},
|
|
147
|
+
async statFile(path) {
|
|
148
|
+
const fileHandle = await getWebFileHandle(path, false);
|
|
149
|
+
if (fileHandle !== null) {
|
|
150
|
+
try {
|
|
151
|
+
const file = await fileHandle.getFile();
|
|
152
|
+
// OPFS exposes no creation time; fall back to lastModified for files, 0 for directories.
|
|
153
|
+
return {
|
|
154
|
+
size: file.size,
|
|
155
|
+
isDirectory: false,
|
|
156
|
+
modifiedTime: file.lastModified,
|
|
157
|
+
createdTime: file.lastModified,
|
|
158
|
+
isSymlink: false,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const root = await getWebRoot();
|
|
166
|
+
if (root === null)
|
|
167
|
+
return null;
|
|
168
|
+
const dir = await getWebDirectoryHandle(root, splitWebPath(path), false);
|
|
169
|
+
if (dir === null)
|
|
170
|
+
return null;
|
|
171
|
+
return { size: 0, isDirectory: true, modifiedTime: 0, createdTime: 0, isSymlink: false };
|
|
172
|
+
},
|
|
173
|
+
async rename(from, to) {
|
|
174
|
+
// OPFS has no native rename; copy then remove the source.
|
|
175
|
+
if (!(await this.copy(from, to)))
|
|
176
|
+
return false;
|
|
177
|
+
return writeWebRemove(from, false);
|
|
178
|
+
},
|
|
179
|
+
async copy(from, to) {
|
|
180
|
+
const root = await getWebRoot();
|
|
181
|
+
if (root === null)
|
|
182
|
+
return false;
|
|
183
|
+
const handle = await getWebFileHandle(from, false);
|
|
184
|
+
if (handle === null)
|
|
185
|
+
return false;
|
|
186
|
+
try {
|
|
187
|
+
const file = await handle.getFile();
|
|
188
|
+
const bytes = new Uint8Array(await file.arrayBuffer());
|
|
189
|
+
return writeWebFile(to, bytes);
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
async appendTextFile(path, data) {
|
|
196
|
+
const handle = await getWebFileHandle(path, false);
|
|
197
|
+
let existing = '';
|
|
198
|
+
if (handle !== null) {
|
|
199
|
+
try {
|
|
200
|
+
existing = await (await handle.getFile()).text();
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
existing = '';
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return writeWebFile(path, existing + data);
|
|
207
|
+
},
|
|
208
|
+
async openFileReadStream(path) {
|
|
209
|
+
const handle = await getWebFileHandle(path, false);
|
|
210
|
+
if (handle === null)
|
|
211
|
+
return null;
|
|
212
|
+
try {
|
|
213
|
+
const file = await handle.getFile();
|
|
214
|
+
return file.stream();
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
async openFileWriteStream(path) {
|
|
221
|
+
const handle = await getWebFileHandle(path, true);
|
|
222
|
+
if (handle === null || typeof handle.createWritable !== 'function')
|
|
223
|
+
return null;
|
|
224
|
+
try {
|
|
225
|
+
return (await handle.createWritable());
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
async writeFileAtomic(path, data) {
|
|
232
|
+
// OPFS has no OS-level atomic rename; write to a temp sibling then overwrite the destination.
|
|
233
|
+
// This is best-effort (not crash-safe) but avoids partial-write corruption under normal conditions.
|
|
234
|
+
const tmpPath = path + '.__atomic_tmp__';
|
|
235
|
+
const payload = typeof data === 'string' ? data : data.slice();
|
|
236
|
+
if (!(await writeWebFile(tmpPath, payload)))
|
|
237
|
+
return false;
|
|
238
|
+
// Copy temp content into the real destination, then remove temp.
|
|
239
|
+
const tmpHandle = await getWebFileHandle(tmpPath, false);
|
|
240
|
+
if (tmpHandle !== null) {
|
|
241
|
+
try {
|
|
242
|
+
const file = await tmpHandle.getFile();
|
|
243
|
+
const bytes = new Uint8Array(await file.arrayBuffer());
|
|
244
|
+
const ok = await writeWebFile(path, bytes);
|
|
245
|
+
await writeWebRemove(tmpPath, false);
|
|
246
|
+
return ok;
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
await writeWebRemove(tmpPath, false);
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return false;
|
|
254
|
+
},
|
|
255
|
+
async createFileSymlink() {
|
|
256
|
+
// OPFS has no symbolic links; this capability is native-only.
|
|
257
|
+
return false;
|
|
258
|
+
},
|
|
259
|
+
async readFileSymlink() {
|
|
260
|
+
// OPFS has no symbolic links; always returns null on web.
|
|
261
|
+
return null;
|
|
262
|
+
},
|
|
263
|
+
async getFileRealPath() {
|
|
264
|
+
// OPFS paths are already canonical; realpath is not meaningful on web.
|
|
265
|
+
return null;
|
|
266
|
+
},
|
|
267
|
+
async getFilePermissions() {
|
|
268
|
+
// POSIX-style permissions have no OPFS equivalent; always returns null on web.
|
|
269
|
+
return null;
|
|
270
|
+
},
|
|
271
|
+
async setFilePermissions() {
|
|
272
|
+
// OPFS has no file permissions model; always returns false on web.
|
|
273
|
+
return false;
|
|
274
|
+
},
|
|
275
|
+
async canAccessFile(path, mode) {
|
|
276
|
+
if (mode === 'executable')
|
|
277
|
+
return false;
|
|
278
|
+
if (mode === 'readable') {
|
|
279
|
+
const handle = await getWebFileHandle(path, false);
|
|
280
|
+
if (handle !== null)
|
|
281
|
+
return true;
|
|
282
|
+
const root = await getWebRoot();
|
|
283
|
+
if (root === null)
|
|
284
|
+
return false;
|
|
285
|
+
return (await getWebDirectoryHandle(root, splitWebPath(path), false)) !== null;
|
|
286
|
+
}
|
|
287
|
+
// writable: probe whether createWritable succeeds (and immediately abort it).
|
|
288
|
+
const handle = await getWebFileHandle(path, false);
|
|
289
|
+
if (handle === null)
|
|
290
|
+
return false;
|
|
291
|
+
try {
|
|
292
|
+
const writable = await handle.createWritable();
|
|
293
|
+
await writable.abort();
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
async getFileSystemUsage() {
|
|
301
|
+
if (typeof navigator === 'undefined')
|
|
302
|
+
return null;
|
|
303
|
+
const storage = navigator.storage;
|
|
304
|
+
if (storage === undefined || typeof storage.estimate !== 'function')
|
|
305
|
+
return null;
|
|
306
|
+
try {
|
|
307
|
+
const estimate = await storage.estimate();
|
|
308
|
+
return {
|
|
309
|
+
usedBytes: estimate.usage ?? 0,
|
|
310
|
+
quotaBytes: estimate.quota ?? 0,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
},
|
|
317
|
+
watch() {
|
|
318
|
+
// OPFS exposes no change notifications; a native host is required to deliver file watch events.
|
|
319
|
+
return () => { };
|
|
320
|
+
},
|
|
321
|
+
getPath() {
|
|
322
|
+
// The web platform has no well-known host directories; native hosts override this.
|
|
323
|
+
return '';
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
// True when a directory exists at `path`. Returns false when missing or access is denied.
|
|
328
|
+
// Edge case on the web backend: passing an empty string ('') resolves to the OPFS root, which
|
|
329
|
+
// always exists — directoryExists('') returns true on web. Use an explicit non-empty path.
|
|
330
|
+
export function directoryExists(path) {
|
|
331
|
+
return getFileSystemBackend().directoryExists(path);
|
|
332
|
+
}
|
|
333
|
+
// True when a file exists at `path`. Returns false when the host lacks access.
|
|
334
|
+
export function fileExists(path) {
|
|
335
|
+
return getFileSystemBackend().fileExists(path);
|
|
336
|
+
}
|
|
337
|
+
// Returns all entries under `rootPath` whose name or path matches the given glob pattern.
|
|
338
|
+
// Supports '*' (any chars within a segment), '**' (any depth), and '?' (single char).
|
|
339
|
+
// Composes with readDirectoryRecursive; [] sentinel for missing or access denied.
|
|
340
|
+
export async function findFiles(rootPath, pattern) {
|
|
341
|
+
const all = await getFileSystemBackend().readDirectoryRecursive(rootPath);
|
|
342
|
+
if (all.length === 0)
|
|
343
|
+
return [];
|
|
344
|
+
const re = globToRegExp(pattern);
|
|
345
|
+
return all.filter((entry) => re.test(entry.name) || re.test(entry.path));
|
|
346
|
+
}
|
|
347
|
+
// Returns the base name of a path (the final segment, with extension). e.g. 'foo/bar.txt' → 'bar.txt'.
|
|
348
|
+
export function getFileBaseName(path) {
|
|
349
|
+
const segments = splitWebPath(path);
|
|
350
|
+
return segments.length === 0 ? '' : segments[segments.length - 1];
|
|
351
|
+
}
|
|
352
|
+
// Returns the directory portion of a path (all segments before the last). e.g. 'foo/bar.txt' → 'foo'.
|
|
353
|
+
export function getFileDirectoryName(path) {
|
|
354
|
+
const segments = splitWebPath(path);
|
|
355
|
+
if (segments.length <= 1)
|
|
356
|
+
return '';
|
|
357
|
+
return segments.slice(0, -1).join('/');
|
|
358
|
+
}
|
|
359
|
+
// Returns the file extension including the leading dot, or '' if none. e.g. 'foo/bar.txt' → '.txt'.
|
|
360
|
+
export function getFileExtensionName(path) {
|
|
361
|
+
const base = getFileBaseName(path);
|
|
362
|
+
const dot = base.lastIndexOf('.');
|
|
363
|
+
return dot > 0 ? base.slice(dot) : '';
|
|
364
|
+
}
|
|
365
|
+
// Returns permission attributes for `path`, or null when permissions are not available.
|
|
366
|
+
// OPFS has no permission model; web always returns null. Native backends return chmod-style data.
|
|
367
|
+
export function getFilePermissions(path) {
|
|
368
|
+
return getFileSystemBackend().getFilePermissions(path);
|
|
369
|
+
}
|
|
370
|
+
// Resolves a path to its canonical (symlink-free) absolute path, or null when the path is missing,
|
|
371
|
+
// access is denied, or symlinks are unsupported (web always returns null).
|
|
372
|
+
export function getFileRealPath(path) {
|
|
373
|
+
return getFileSystemBackend().getFileRealPath(path);
|
|
374
|
+
}
|
|
375
|
+
// The active file system backend, or a lazily-created web (OPFS) default. There is always a backend.
|
|
376
|
+
export function getFileSystemBackend() {
|
|
377
|
+
if (_backend === null)
|
|
378
|
+
_backend = createWebFileSystemBackend();
|
|
379
|
+
return _backend;
|
|
380
|
+
}
|
|
381
|
+
// Resolves a well-known host directory to an absolute path, or '' on web / when unavailable.
|
|
382
|
+
export function getFileSystemPath(kind) {
|
|
383
|
+
return getFileSystemBackend().getPath(kind);
|
|
384
|
+
}
|
|
385
|
+
// Returns disk or quota usage for the active file system. Web implements over
|
|
386
|
+
// navigator.storage.estimate(); native over statvfs. Returns null when unavailable.
|
|
387
|
+
export function getFileSystemUsage() {
|
|
388
|
+
return getFileSystemBackend().getFileSystemUsage();
|
|
389
|
+
}
|
|
390
|
+
// True when `path` is an absolute path (starts with '/' or a drive letter on Windows e.g. 'C:').
|
|
391
|
+
export function isAbsoluteFilePath(path) {
|
|
392
|
+
if (path.length === 0)
|
|
393
|
+
return false;
|
|
394
|
+
if (path[0] === '/')
|
|
395
|
+
return true;
|
|
396
|
+
// Windows drive letter: e.g. 'C:\' or 'C:/'
|
|
397
|
+
if (path.length >= 2 && /^[A-Za-z]:/.test(path))
|
|
398
|
+
return true;
|
|
399
|
+
return false;
|
|
400
|
+
}
|
|
401
|
+
// Joins path segments with '/', normalizing redundant separators and '.' segments.
|
|
402
|
+
// e.g. joinFilePath('foo', 'bar', 'baz.txt') → 'foo/bar/baz.txt'
|
|
403
|
+
export function joinFilePath(...segments) {
|
|
404
|
+
const parts = [];
|
|
405
|
+
for (const segment of segments) {
|
|
406
|
+
for (const part of segment.split('/')) {
|
|
407
|
+
if (part === '' || part === '.')
|
|
408
|
+
continue;
|
|
409
|
+
parts.push(part);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
const prefix = segments.length > 0 && segments[0] !== undefined && segments[0].startsWith('/') ? '/' : '';
|
|
413
|
+
return prefix + parts.join('/');
|
|
414
|
+
}
|
|
415
|
+
// Creates a directory (and parents) at `path`. Returns false when the host denies access.
|
|
416
|
+
export function makeDirectory(path) {
|
|
417
|
+
return getFileSystemBackend().makeDirectory(path);
|
|
418
|
+
}
|
|
419
|
+
// Normalizes a path: collapses redundant separators, removes '.' segments, preserves leading '/'.
|
|
420
|
+
// e.g. normalizeFilePath('foo//./bar') → 'foo/bar'
|
|
421
|
+
export function normalizeFilePath(path) {
|
|
422
|
+
const parts = splitWebPath(path);
|
|
423
|
+
const prefix = path.startsWith('/') ? '/' : '';
|
|
424
|
+
return prefix + parts.join('/');
|
|
425
|
+
}
|
|
426
|
+
// Opens a ReadableStream over the file at `path`. Returns null when the file is missing, access is
|
|
427
|
+
// denied, or streaming is not supported by the active backend. OPFS implements via File.stream().
|
|
428
|
+
export function openFileReadStream(path) {
|
|
429
|
+
return getFileSystemBackend().openFileReadStream(path);
|
|
430
|
+
}
|
|
431
|
+
// Opens a WritableStream to the file at `path`, creating it when absent. Returns null when access is
|
|
432
|
+
// denied or streaming is not supported. OPFS implements via FileSystemFileHandle.createWritable().
|
|
433
|
+
export function openFileWriteStream(path) {
|
|
434
|
+
return getFileSystemBackend().openFileWriteStream(path);
|
|
435
|
+
}
|
|
436
|
+
// Reads a file as bytes, or null when missing or access is denied.
|
|
437
|
+
export function readBinaryFile(path) {
|
|
438
|
+
return getFileSystemBackend().readBinaryFile(path);
|
|
439
|
+
}
|
|
440
|
+
// Reads a byte slice of a file at `offset` with `length` bytes. Returns an empty Uint8Array for
|
|
441
|
+
// out-of-range access, null for missing or access denied.
|
|
442
|
+
export function readBinaryFileRange(path, offset, length) {
|
|
443
|
+
return getFileSystemBackend().readBinaryFileRange(path, offset, length);
|
|
444
|
+
}
|
|
445
|
+
// Reads bytes from a FileDialogHandle produced by @flighthq/dialog on web via the File System
|
|
446
|
+
// Access API, or falls back to the OPFS backend by name when the native handle is unavailable.
|
|
447
|
+
// On native hosts (Electron/Tauri), delegates to readBinaryFile using handle.path.
|
|
448
|
+
// Returns null when the handle is unreadable or the path is unavailable.
|
|
449
|
+
export async function readDialogHandleBinaryFile(handle) {
|
|
450
|
+
// On native hosts, path is a real file-system path; delegate to the backend directly.
|
|
451
|
+
if (handle.path !== null)
|
|
452
|
+
return getFileSystemBackend().readBinaryFile(handle.path);
|
|
453
|
+
// On web: use the live FileSystemFileHandle stashed by the dialog backend, if any.
|
|
454
|
+
const fsHandle = getWebFileSystemHandle(handle);
|
|
455
|
+
if (fsHandle !== null) {
|
|
456
|
+
try {
|
|
457
|
+
const file = await fsHandle.getFile();
|
|
458
|
+
return new Uint8Array(await file.arrayBuffer());
|
|
459
|
+
}
|
|
460
|
+
catch {
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
// Fallback: try OPFS by file name (only works if the file was previously written to OPFS).
|
|
465
|
+
if (handle.name === '')
|
|
466
|
+
return null;
|
|
467
|
+
return getFileSystemBackend().readBinaryFile(handle.name);
|
|
468
|
+
}
|
|
469
|
+
// Reads text from a FileDialogHandle produced by @flighthq/dialog on web via the File System
|
|
470
|
+
// Access API, or falls back to the OPFS backend by name when the native handle is unavailable.
|
|
471
|
+
// On native hosts (Electron/Tauri), delegates to readTextFile using handle.path.
|
|
472
|
+
// Returns null when the handle is unreadable or the path is unavailable.
|
|
473
|
+
export async function readDialogHandleTextFile(handle) {
|
|
474
|
+
// On native hosts, path is a real file-system path; delegate to the backend directly.
|
|
475
|
+
if (handle.path !== null)
|
|
476
|
+
return getFileSystemBackend().readTextFile(handle.path);
|
|
477
|
+
// On web: use the live FileSystemFileHandle stashed by the dialog backend, if any.
|
|
478
|
+
const fsHandle = getWebFileSystemHandle(handle);
|
|
479
|
+
if (fsHandle !== null) {
|
|
480
|
+
try {
|
|
481
|
+
const file = await fsHandle.getFile();
|
|
482
|
+
return await file.text();
|
|
483
|
+
}
|
|
484
|
+
catch {
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
// Fallback: try OPFS by file name (only works if the file was previously written to OPFS).
|
|
489
|
+
if (handle.name === '')
|
|
490
|
+
return null;
|
|
491
|
+
return getFileSystemBackend().readTextFile(handle.name);
|
|
492
|
+
}
|
|
493
|
+
// Lists directory entries (one level only), or [] when missing or access is denied.
|
|
494
|
+
export function readDirectory(path) {
|
|
495
|
+
return getFileSystemBackend().readDirectory(path);
|
|
496
|
+
}
|
|
497
|
+
// Depth-first walk returning all descendants with full relative paths. [] sentinel for missing/denied.
|
|
498
|
+
export function readDirectoryRecursive(path, options) {
|
|
499
|
+
return getFileSystemBackend().readDirectoryRecursive(path, options);
|
|
500
|
+
}
|
|
501
|
+
// Reads the target of a symbolic link at `path`. Returns null when path is not a symlink, is
|
|
502
|
+
// missing, or symlinks are unsupported (web/OPFS always returns null).
|
|
503
|
+
export function readFileSymlink(path) {
|
|
504
|
+
return getFileSystemBackend().readFileSymlink(path);
|
|
505
|
+
}
|
|
506
|
+
// Reads a file as text, or null when missing or access is denied.
|
|
507
|
+
export function readTextFile(path) {
|
|
508
|
+
return getFileSystemBackend().readTextFile(path);
|
|
509
|
+
}
|
|
510
|
+
// Removes a directory at `path`. When recursive is false (default), fails on non-empty directories.
|
|
511
|
+
// Returns false when missing or access is denied.
|
|
512
|
+
export function removeDirectory(path, recursive) {
|
|
513
|
+
return getFileSystemBackend().removeDirectory(path, recursive);
|
|
514
|
+
}
|
|
515
|
+
// Removes a file at `path`. Returns false when missing or access is denied.
|
|
516
|
+
// To remove a directory, use removeDirectory.
|
|
517
|
+
export function removeFile(path) {
|
|
518
|
+
return getFileSystemBackend().removeFile(path);
|
|
519
|
+
}
|
|
520
|
+
// Renames or moves a file from `from` to `to`. Returns false when the source is missing or access is denied.
|
|
521
|
+
export function renameFile(from, to) {
|
|
522
|
+
return getFileSystemBackend().rename(from, to);
|
|
523
|
+
}
|
|
524
|
+
// Sets file permissions for `path`. Returns false when unsupported (web/OPFS always returns false;
|
|
525
|
+
// native POSIX backends use chmod). A no-op on platforms without a permissions model.
|
|
526
|
+
export function setFilePermissions(path, permissions) {
|
|
527
|
+
return getFileSystemBackend().setFilePermissions(path, permissions);
|
|
528
|
+
}
|
|
529
|
+
// Installs a native host file system backend; pass null to fall back to the web (OPFS) default.
|
|
530
|
+
export function setFileSystemBackend(backend) {
|
|
531
|
+
_backend = backend;
|
|
532
|
+
}
|
|
533
|
+
// Reads metadata for `path`, or null when missing or access is denied.
|
|
534
|
+
export function statFile(path) {
|
|
535
|
+
return getFileSystemBackend().statFile(path);
|
|
536
|
+
}
|
|
537
|
+
// Watches `path` for create/modify/delete changes, returning an unsubscribe function. On web the
|
|
538
|
+
// returned function is a no-op because OPFS exposes no change notifications.
|
|
539
|
+
export function watchPath(path, listener) {
|
|
540
|
+
return getFileSystemBackend().watch(path, listener);
|
|
541
|
+
}
|
|
542
|
+
// Writes bytes to a file, creating parent directories. Returns false when the host denies access.
|
|
543
|
+
export function writeBinaryFile(path, data) {
|
|
544
|
+
return getFileSystemBackend().writeBinaryFile(path, data);
|
|
545
|
+
}
|
|
546
|
+
// Writes all chunks from an async iterable to `path`, creating the file when absent. Each chunk is
|
|
547
|
+
// flushed through the backend's write stream; the whole payload is never held in memory at once.
|
|
548
|
+
// Returns false when the file cannot be opened or a chunk write fails.
|
|
549
|
+
export async function writeBinaryFileChunks(path, chunks) {
|
|
550
|
+
const stream = await getFileSystemBackend().openFileWriteStream(path);
|
|
551
|
+
if (stream === null)
|
|
552
|
+
return false;
|
|
553
|
+
const writer = stream.getWriter();
|
|
554
|
+
try {
|
|
555
|
+
for await (const chunk of chunks) {
|
|
556
|
+
await writer.write(chunk.slice());
|
|
557
|
+
}
|
|
558
|
+
await writer.close();
|
|
559
|
+
return true;
|
|
560
|
+
}
|
|
561
|
+
catch {
|
|
562
|
+
await writer.abort();
|
|
563
|
+
return false;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
// Writes bytes to a FileDialogHandle produced by @flighthq/dialog (typically a save-file handle).
|
|
567
|
+
// On web, uses the live writable FileSystemFileHandle stashed by the dialog backend when available.
|
|
568
|
+
// On native hosts (Electron/Tauri), delegates to writeBinaryFile using handle.path.
|
|
569
|
+
// Returns false when the handle is not writable or the path is unavailable.
|
|
570
|
+
export async function writeDialogHandleBinaryFile(handle, data) {
|
|
571
|
+
// On native hosts, path is a real file-system path; delegate to the backend directly.
|
|
572
|
+
if (handle.path !== null)
|
|
573
|
+
return getFileSystemBackend().writeBinaryFile(handle.path, data);
|
|
574
|
+
// On web: use the live FileSystemFileHandle stashed by the dialog backend, if any.
|
|
575
|
+
const fsHandle = getWebFileSystemHandle(handle);
|
|
576
|
+
if (fsHandle === null)
|
|
577
|
+
return false;
|
|
578
|
+
try {
|
|
579
|
+
const writable = await fsHandle.createWritable();
|
|
580
|
+
await writable.write(data.slice());
|
|
581
|
+
await writable.close();
|
|
582
|
+
return true;
|
|
583
|
+
}
|
|
584
|
+
catch {
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
// Writes text to a FileDialogHandle produced by @flighthq/dialog (typically a save-file handle).
|
|
589
|
+
// On web, uses the live writable FileSystemFileHandle stashed by the dialog backend when available.
|
|
590
|
+
// On native hosts (Electron/Tauri), delegates to writeTextFile using handle.path.
|
|
591
|
+
// Returns false when the handle is not writable or the path is unavailable.
|
|
592
|
+
export async function writeDialogHandleTextFile(handle, data) {
|
|
593
|
+
// On native hosts, path is a real file-system path; delegate to the backend directly.
|
|
594
|
+
if (handle.path !== null)
|
|
595
|
+
return getFileSystemBackend().writeTextFile(handle.path, data);
|
|
596
|
+
// On web: use the live FileSystemFileHandle stashed by the dialog backend, if any.
|
|
597
|
+
const fsHandle = getWebFileSystemHandle(handle);
|
|
598
|
+
if (fsHandle === null)
|
|
599
|
+
return false;
|
|
600
|
+
try {
|
|
601
|
+
const writable = await fsHandle.createWritable();
|
|
602
|
+
await writable.write(data);
|
|
603
|
+
await writable.close();
|
|
604
|
+
return true;
|
|
605
|
+
}
|
|
606
|
+
catch {
|
|
607
|
+
return false;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
// Atomic write: writes data to a temp sibling and moves it into place in one operation. Avoids
|
|
611
|
+
// partial-write corruption under normal conditions. On web (OPFS), the rename is copy+remove
|
|
612
|
+
// (not OS-atomic) — documented as best-effort. Returns false when the write or rename fails.
|
|
613
|
+
export function writeFileAtomic(path, data) {
|
|
614
|
+
return getFileSystemBackend().writeFileAtomic(path, data);
|
|
615
|
+
}
|
|
616
|
+
// Writes text to a file, creating parent directories. Returns false when the host denies access.
|
|
617
|
+
export function writeTextFile(path, data) {
|
|
618
|
+
return getFileSystemBackend().writeTextFile(path, data);
|
|
619
|
+
}
|
|
620
|
+
let _backend = null;
|
|
621
|
+
// The OPFS root, or null when the API is absent (non-secure context, jsdom). Never throws.
|
|
622
|
+
async function getWebRoot() {
|
|
623
|
+
if (typeof navigator === 'undefined')
|
|
624
|
+
return null;
|
|
625
|
+
const storage = navigator.storage;
|
|
626
|
+
if (storage === undefined || typeof storage.getDirectory !== 'function')
|
|
627
|
+
return null;
|
|
628
|
+
try {
|
|
629
|
+
return await storage.getDirectory();
|
|
630
|
+
}
|
|
631
|
+
catch {
|
|
632
|
+
return null;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
async function getWebDirectoryHandle(root, segments, create) {
|
|
636
|
+
let current = root;
|
|
637
|
+
try {
|
|
638
|
+
for (const segment of segments) {
|
|
639
|
+
current = await current.getDirectoryHandle(segment, { create });
|
|
640
|
+
}
|
|
641
|
+
return current;
|
|
642
|
+
}
|
|
643
|
+
catch {
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
async function getWebFileHandle(path, create) {
|
|
648
|
+
const root = await getWebRoot();
|
|
649
|
+
if (root === null)
|
|
650
|
+
return null;
|
|
651
|
+
const segments = splitWebPath(path);
|
|
652
|
+
if (segments.length === 0)
|
|
653
|
+
return null;
|
|
654
|
+
const parent = await getWebDirectoryHandle(root, segments.slice(0, -1), create);
|
|
655
|
+
if (parent === null)
|
|
656
|
+
return null;
|
|
657
|
+
try {
|
|
658
|
+
return await parent.getFileHandle(segments[segments.length - 1], { create });
|
|
659
|
+
}
|
|
660
|
+
catch {
|
|
661
|
+
return null;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
// Recursively walks a directory handle, appending FileEntry results into `out`.
|
|
665
|
+
// `depth` is the current depth (0 = entries inside the root of the walk); `maxDepth` limits descent.
|
|
666
|
+
async function walkWebDirectory(dir, basePath, out, depth, maxDepth) {
|
|
667
|
+
for await (const [name, handle] of asAsyncEntries(dir)) {
|
|
668
|
+
const entryPath = basePath === '' ? name : `${basePath}/${name}`;
|
|
669
|
+
const isDirectory = handle.kind === 'directory';
|
|
670
|
+
out.push({ name, path: entryPath, isDirectory });
|
|
671
|
+
if (isDirectory && depth < maxDepth) {
|
|
672
|
+
await walkWebDirectory(handle, entryPath, out, depth + 1, maxDepth);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
// Removes a path from the OPFS tree. When `isDirectory` is true, only attempts removal via directory
|
|
677
|
+
// handle to enforce the file/directory verb split. When false, removes any entry type (files only).
|
|
678
|
+
async function writeWebRemove(path, isDirectory) {
|
|
679
|
+
const root = await getWebRoot();
|
|
680
|
+
if (root === null)
|
|
681
|
+
return false;
|
|
682
|
+
const segments = splitWebPath(path);
|
|
683
|
+
if (segments.length === 0)
|
|
684
|
+
return false;
|
|
685
|
+
try {
|
|
686
|
+
const parent = await getWebDirectoryHandle(root, segments.slice(0, -1), false);
|
|
687
|
+
if (parent === null)
|
|
688
|
+
return false;
|
|
689
|
+
// For removeFile, verify target is not a directory before removing.
|
|
690
|
+
if (!isDirectory) {
|
|
691
|
+
const fileHandle = await getWebFileHandle(path, false);
|
|
692
|
+
if (fileHandle === null)
|
|
693
|
+
return false;
|
|
694
|
+
}
|
|
695
|
+
await parent.removeEntry(segments[segments.length - 1], { recursive: false });
|
|
696
|
+
return true;
|
|
697
|
+
}
|
|
698
|
+
catch {
|
|
699
|
+
return false;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
async function writeWebFile(path, data) {
|
|
703
|
+
const handle = await getWebFileHandle(path, true);
|
|
704
|
+
if (handle === null || typeof handle.createWritable !== 'function')
|
|
705
|
+
return false;
|
|
706
|
+
try {
|
|
707
|
+
const writable = await handle.createWritable();
|
|
708
|
+
// Uint8Array<ArrayBufferLike> and string are both valid chunk inputs, but the union widens past
|
|
709
|
+
// FileSystemWriteChunkType's overloads; cast to the lib.dom chunk type at the write boundary.
|
|
710
|
+
await writable.write(data);
|
|
711
|
+
await writable.close();
|
|
712
|
+
return true;
|
|
713
|
+
}
|
|
714
|
+
catch {
|
|
715
|
+
return false;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
function asAsyncEntries(dir) {
|
|
719
|
+
return dir.entries();
|
|
720
|
+
}
|
|
721
|
+
// Converts a glob pattern (supporting *, **, and ?) to a RegExp. Each segment is matched case-sensitively.
|
|
722
|
+
// '*' matches any characters except '/', '**' matches any characters including '/', '?' matches one char.
|
|
723
|
+
function globToRegExp(pattern) {
|
|
724
|
+
let re = '^';
|
|
725
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
726
|
+
const ch = pattern[i];
|
|
727
|
+
if (ch === '*') {
|
|
728
|
+
if (pattern[i + 1] === '*') {
|
|
729
|
+
// '**' matches any path including separators.
|
|
730
|
+
re += '.*';
|
|
731
|
+
i++;
|
|
732
|
+
// Skip optional trailing separator after '**'.
|
|
733
|
+
if (pattern[i + 1] === '/')
|
|
734
|
+
i++;
|
|
735
|
+
}
|
|
736
|
+
else {
|
|
737
|
+
// '*' matches within one path segment.
|
|
738
|
+
re += '[^/]*';
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
else if (ch === '?') {
|
|
742
|
+
re += '[^/]';
|
|
743
|
+
}
|
|
744
|
+
else if (/[.+^${}()|[\]\\]/.test(ch)) {
|
|
745
|
+
re += '\\' + ch;
|
|
746
|
+
}
|
|
747
|
+
else {
|
|
748
|
+
re += ch;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
re += '$';
|
|
752
|
+
return new RegExp(re);
|
|
753
|
+
}
|
|
754
|
+
function normalizeWebPath(path) {
|
|
755
|
+
return splitWebPath(path).join('/');
|
|
756
|
+
}
|
|
757
|
+
function splitWebPath(path) {
|
|
758
|
+
return path.split('/').filter((segment) => segment !== '' && segment !== '.');
|
|
759
|
+
}
|
|
760
|
+
//# sourceMappingURL=filesystem.js.map
|