@componentor/fs 1.1.7
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 +742 -0
- package/dist/index.d.ts +544 -0
- package/dist/index.js +2551 -0
- package/dist/index.js.map +1 -0
- package/dist/opfs-hybrid.d.ts +198 -0
- package/dist/opfs-hybrid.js +2552 -0
- package/dist/opfs-hybrid.js.map +1 -0
- package/dist/opfs-worker-proxy.d.ts +224 -0
- package/dist/opfs-worker-proxy.js +274 -0
- package/dist/opfs-worker-proxy.js.map +1 -0
- package/dist/opfs-worker.js +2732 -0
- package/dist/opfs-worker.js.map +1 -0
- package/package.json +66 -0
- package/src/constants.ts +52 -0
- package/src/errors.ts +88 -0
- package/src/file-handle.ts +100 -0
- package/src/global.d.ts +57 -0
- package/src/handle-manager.ts +250 -0
- package/src/index.ts +1404 -0
- package/src/opfs-hybrid.ts +265 -0
- package/src/opfs-worker-proxy.ts +374 -0
- package/src/opfs-worker.ts +253 -0
- package/src/packed-storage.ts +426 -0
- package/src/path-utils.ts +97 -0
- package/src/streams.ts +109 -0
- package/src/symlink-manager.ts +329 -0
- package/src/types.ts +285 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2551 @@
|
|
|
1
|
+
// src/constants.ts
|
|
2
|
+
var constants = {
|
|
3
|
+
// File access modes
|
|
4
|
+
F_OK: 0,
|
|
5
|
+
R_OK: 4,
|
|
6
|
+
W_OK: 2,
|
|
7
|
+
X_OK: 1,
|
|
8
|
+
// Copy file flags
|
|
9
|
+
COPYFILE_EXCL: 1,
|
|
10
|
+
COPYFILE_FICLONE: 2,
|
|
11
|
+
COPYFILE_FICLONE_FORCE: 4,
|
|
12
|
+
// File open flags
|
|
13
|
+
O_RDONLY: 0,
|
|
14
|
+
O_WRONLY: 1,
|
|
15
|
+
O_RDWR: 2,
|
|
16
|
+
O_CREAT: 64,
|
|
17
|
+
O_EXCL: 128,
|
|
18
|
+
O_TRUNC: 512,
|
|
19
|
+
O_APPEND: 1024,
|
|
20
|
+
// File type masks
|
|
21
|
+
S_IFMT: 61440,
|
|
22
|
+
S_IFREG: 32768,
|
|
23
|
+
S_IFDIR: 16384,
|
|
24
|
+
S_IFLNK: 40960
|
|
25
|
+
};
|
|
26
|
+
function flagsToString(flags) {
|
|
27
|
+
if (typeof flags === "string") return flags;
|
|
28
|
+
const map = {
|
|
29
|
+
[constants.O_RDONLY]: "r",
|
|
30
|
+
[constants.O_WRONLY]: "w",
|
|
31
|
+
[constants.O_RDWR]: "r+",
|
|
32
|
+
[constants.O_CREAT | constants.O_WRONLY]: "w",
|
|
33
|
+
[constants.O_CREAT | constants.O_WRONLY | constants.O_TRUNC]: "w",
|
|
34
|
+
[constants.O_CREAT | constants.O_RDWR]: "w+",
|
|
35
|
+
[constants.O_APPEND | constants.O_WRONLY]: "a",
|
|
36
|
+
[constants.O_APPEND | constants.O_RDWR]: "a+"
|
|
37
|
+
};
|
|
38
|
+
return map[flags] || "r";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// src/errors.ts
|
|
42
|
+
var FSError = class extends Error {
|
|
43
|
+
code;
|
|
44
|
+
syscall;
|
|
45
|
+
path;
|
|
46
|
+
original;
|
|
47
|
+
constructor(message, code, options) {
|
|
48
|
+
super(message);
|
|
49
|
+
this.name = "FSError";
|
|
50
|
+
this.code = code;
|
|
51
|
+
this.syscall = options?.syscall;
|
|
52
|
+
this.path = options?.path;
|
|
53
|
+
this.original = options?.original;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
function createENOENT(path) {
|
|
57
|
+
return new FSError(`ENOENT: No such file or directory, '${path}'`, "ENOENT", { path });
|
|
58
|
+
}
|
|
59
|
+
function createEEXIST(path, operation) {
|
|
60
|
+
const message = `EEXIST: File exists, '${path}'`;
|
|
61
|
+
return new FSError(message, "EEXIST", { path });
|
|
62
|
+
}
|
|
63
|
+
function createEACCES(path, syscall) {
|
|
64
|
+
return new FSError(`EACCES: permission denied, access '${path}'`, "EACCES", { syscall, path });
|
|
65
|
+
}
|
|
66
|
+
function createEISDIR(path, operation = "operation") {
|
|
67
|
+
return new FSError(`EISDIR: illegal operation on a directory, ${operation} '${path}'`, "EISDIR", { path });
|
|
68
|
+
}
|
|
69
|
+
function createELOOP(path) {
|
|
70
|
+
return new FSError(`ELOOP: Too many symbolic links, '${path}'`, "ELOOP", { path });
|
|
71
|
+
}
|
|
72
|
+
function createEINVAL(path) {
|
|
73
|
+
return new FSError(`EINVAL: Invalid argument, '${path}'`, "EINVAL", { path });
|
|
74
|
+
}
|
|
75
|
+
function createECORRUPTED(path) {
|
|
76
|
+
return new FSError(`ECORRUPTED: Pack file integrity check failed, '${path}'`, "ECORRUPTED", { path });
|
|
77
|
+
}
|
|
78
|
+
function wrapError(err) {
|
|
79
|
+
if (err instanceof FSError) return err;
|
|
80
|
+
const error = err;
|
|
81
|
+
if (typeof error.code === "string") {
|
|
82
|
+
const fsErr = new FSError(error.message, error.code);
|
|
83
|
+
fsErr.original = error;
|
|
84
|
+
return fsErr;
|
|
85
|
+
}
|
|
86
|
+
const wrapped = new FSError(error.message || "Unknown error", "UNKNOWN");
|
|
87
|
+
wrapped.original = error;
|
|
88
|
+
return wrapped;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/path-utils.ts
|
|
92
|
+
var normalizeCache = /* @__PURE__ */ new Map();
|
|
93
|
+
var CACHE_MAX_SIZE = 1e3;
|
|
94
|
+
function normalize(path) {
|
|
95
|
+
if (path === void 0 || path === null) {
|
|
96
|
+
throw new TypeError("Path cannot be undefined or null");
|
|
97
|
+
}
|
|
98
|
+
if (typeof path !== "string") {
|
|
99
|
+
throw new TypeError(`Expected string path, got ${typeof path}`);
|
|
100
|
+
}
|
|
101
|
+
if (path === "") {
|
|
102
|
+
return "/";
|
|
103
|
+
}
|
|
104
|
+
const cached = normalizeCache.get(path);
|
|
105
|
+
if (cached !== void 0) {
|
|
106
|
+
return cached;
|
|
107
|
+
}
|
|
108
|
+
const parts = path.split("/");
|
|
109
|
+
const stack = [];
|
|
110
|
+
for (const part of parts) {
|
|
111
|
+
if (part === "" || part === ".") {
|
|
112
|
+
continue;
|
|
113
|
+
} else if (part === "..") {
|
|
114
|
+
if (stack.length > 0) stack.pop();
|
|
115
|
+
} else {
|
|
116
|
+
stack.push(part);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const result = "/" + stack.join("/");
|
|
120
|
+
if (normalizeCache.size >= CACHE_MAX_SIZE) {
|
|
121
|
+
const deleteCount = CACHE_MAX_SIZE / 4;
|
|
122
|
+
let count = 0;
|
|
123
|
+
for (const key of normalizeCache.keys()) {
|
|
124
|
+
if (count++ >= deleteCount) break;
|
|
125
|
+
normalizeCache.delete(key);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
normalizeCache.set(path, result);
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
function dirname(path) {
|
|
132
|
+
const normalized = normalize(path);
|
|
133
|
+
const parts = normalized.split("/").filter(Boolean);
|
|
134
|
+
if (parts.length < 2) return "/";
|
|
135
|
+
return "/" + parts.slice(0, -1).join("/");
|
|
136
|
+
}
|
|
137
|
+
function isRoot(path) {
|
|
138
|
+
const normalized = normalize(path);
|
|
139
|
+
return normalized === "/" || normalized === "";
|
|
140
|
+
}
|
|
141
|
+
function segments(path) {
|
|
142
|
+
return normalize(path).split("/").filter(Boolean);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/handle-manager.ts
|
|
146
|
+
var FILE_HANDLE_POOL_SIZE = 50;
|
|
147
|
+
var DIR_CACHE_MAX_SIZE = 200;
|
|
148
|
+
var HandleManager = class {
|
|
149
|
+
rootPromise;
|
|
150
|
+
dirCache = /* @__PURE__ */ new Map();
|
|
151
|
+
fileHandlePool = /* @__PURE__ */ new Map();
|
|
152
|
+
constructor() {
|
|
153
|
+
this.rootPromise = navigator.storage.getDirectory();
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Get the root directory handle
|
|
157
|
+
*/
|
|
158
|
+
async getRoot() {
|
|
159
|
+
return this.rootPromise;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Cache a directory handle with LRU eviction
|
|
163
|
+
*/
|
|
164
|
+
cacheDirHandle(path, handle) {
|
|
165
|
+
if (this.dirCache.size >= DIR_CACHE_MAX_SIZE) {
|
|
166
|
+
const firstKey = this.dirCache.keys().next().value;
|
|
167
|
+
if (firstKey) this.dirCache.delete(firstKey);
|
|
168
|
+
}
|
|
169
|
+
this.dirCache.set(path, handle);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Clear directory cache for a path and its children
|
|
173
|
+
*/
|
|
174
|
+
clearCache(path = "") {
|
|
175
|
+
const normalizedPath = normalize(path);
|
|
176
|
+
if (normalizedPath === "/" || normalizedPath === "") {
|
|
177
|
+
this.dirCache.clear();
|
|
178
|
+
this.fileHandlePool.clear();
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (this.dirCache.size > 0) {
|
|
182
|
+
for (const key of this.dirCache.keys()) {
|
|
183
|
+
if (key === normalizedPath || key.startsWith(normalizedPath + "/")) {
|
|
184
|
+
this.dirCache.delete(key);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (this.fileHandlePool.size > 0) {
|
|
189
|
+
for (const key of this.fileHandlePool.keys()) {
|
|
190
|
+
if (key === normalizedPath || key.startsWith(normalizedPath + "/")) {
|
|
191
|
+
this.fileHandlePool.delete(key);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Get a file handle from the pool or create a new one
|
|
198
|
+
*/
|
|
199
|
+
async getPooledFileHandle(path, create = false) {
|
|
200
|
+
const normalizedPath = normalize(path);
|
|
201
|
+
const pooled = this.fileHandlePool.get(normalizedPath);
|
|
202
|
+
if (pooled) {
|
|
203
|
+
return pooled;
|
|
204
|
+
}
|
|
205
|
+
const { fileHandle } = await this.getHandle(normalizedPath, { create });
|
|
206
|
+
if (!fileHandle) return null;
|
|
207
|
+
if (this.fileHandlePool.size >= FILE_HANDLE_POOL_SIZE) {
|
|
208
|
+
const firstKey = this.fileHandlePool.keys().next().value;
|
|
209
|
+
if (firstKey) this.fileHandlePool.delete(firstKey);
|
|
210
|
+
}
|
|
211
|
+
this.fileHandlePool.set(normalizedPath, fileHandle);
|
|
212
|
+
return fileHandle;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Invalidate a specific file handle from the pool
|
|
216
|
+
*/
|
|
217
|
+
invalidateFileHandle(path) {
|
|
218
|
+
const normalizedPath = normalize(path);
|
|
219
|
+
this.fileHandlePool.delete(normalizedPath);
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Get file or directory handle for a path
|
|
223
|
+
*/
|
|
224
|
+
async getHandle(path, opts = {}) {
|
|
225
|
+
const parts = segments(path);
|
|
226
|
+
if (parts.length === 0) {
|
|
227
|
+
const root = await this.rootPromise;
|
|
228
|
+
return { dir: root, name: "", fileHandle: null, dirHandle: root };
|
|
229
|
+
}
|
|
230
|
+
let dir = await this.rootPromise;
|
|
231
|
+
let currentPath = "";
|
|
232
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
233
|
+
currentPath += "/" + parts[i];
|
|
234
|
+
if (this.dirCache.has(currentPath)) {
|
|
235
|
+
dir = this.dirCache.get(currentPath);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
dir = await dir.getDirectoryHandle(parts[i], { create: opts.create });
|
|
240
|
+
this.cacheDirHandle(currentPath, dir);
|
|
241
|
+
} catch {
|
|
242
|
+
throw createENOENT(path);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const name = parts[parts.length - 1];
|
|
246
|
+
try {
|
|
247
|
+
if (opts.kind === "directory") {
|
|
248
|
+
const dirHandle = await dir.getDirectoryHandle(name, { create: opts.create });
|
|
249
|
+
return { dir, name, fileHandle: null, dirHandle };
|
|
250
|
+
} else {
|
|
251
|
+
const fileHandle = await dir.getFileHandle(name, { create: opts.create });
|
|
252
|
+
return { dir, name, fileHandle, dirHandle: null };
|
|
253
|
+
}
|
|
254
|
+
} catch {
|
|
255
|
+
if (!opts.create) {
|
|
256
|
+
return { dir, name, fileHandle: null, dirHandle: null };
|
|
257
|
+
}
|
|
258
|
+
throw createENOENT(path);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Get directory handle with caching
|
|
263
|
+
*/
|
|
264
|
+
async getDirectoryHandle(path) {
|
|
265
|
+
const normalizedPath = normalize(path);
|
|
266
|
+
if (normalizedPath === "/" || normalizedPath === "") {
|
|
267
|
+
return this.rootPromise;
|
|
268
|
+
}
|
|
269
|
+
if (this.dirCache.has(normalizedPath)) {
|
|
270
|
+
return this.dirCache.get(normalizedPath);
|
|
271
|
+
}
|
|
272
|
+
const parts = segments(normalizedPath);
|
|
273
|
+
let dir = await this.rootPromise;
|
|
274
|
+
let currentPath = "";
|
|
275
|
+
for (const part of parts) {
|
|
276
|
+
currentPath += "/" + part;
|
|
277
|
+
if (this.dirCache.has(currentPath)) {
|
|
278
|
+
dir = this.dirCache.get(currentPath);
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
dir = await dir.getDirectoryHandle(part);
|
|
282
|
+
this.cacheDirHandle(currentPath, dir);
|
|
283
|
+
}
|
|
284
|
+
return dir;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Ensure parent directory exists
|
|
288
|
+
*/
|
|
289
|
+
async ensureParentDir(path) {
|
|
290
|
+
const parentPath = dirname(path);
|
|
291
|
+
if (parentPath === "/" || parentPath === "") return;
|
|
292
|
+
const parts = segments(parentPath);
|
|
293
|
+
let dir = await this.rootPromise;
|
|
294
|
+
let currentPath = "";
|
|
295
|
+
for (const part of parts) {
|
|
296
|
+
currentPath += "/" + part;
|
|
297
|
+
if (this.dirCache.has(currentPath)) {
|
|
298
|
+
dir = this.dirCache.get(currentPath);
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
dir = await dir.getDirectoryHandle(part, { create: true });
|
|
302
|
+
this.cacheDirHandle(currentPath, dir);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Create directory (with automatic parent creation)
|
|
307
|
+
*/
|
|
308
|
+
async mkdir(path) {
|
|
309
|
+
const normalizedPath = normalize(path);
|
|
310
|
+
this.clearCache(normalizedPath);
|
|
311
|
+
const parts = segments(normalizedPath);
|
|
312
|
+
let dir = await this.rootPromise;
|
|
313
|
+
for (let i = 0; i < parts.length; i++) {
|
|
314
|
+
const part = parts[i];
|
|
315
|
+
const subPath = "/" + parts.slice(0, i + 1).join("/");
|
|
316
|
+
if (this.dirCache.has(subPath)) {
|
|
317
|
+
dir = this.dirCache.get(subPath);
|
|
318
|
+
} else {
|
|
319
|
+
dir = await dir.getDirectoryHandle(part, { create: true });
|
|
320
|
+
this.cacheDirHandle(subPath, dir);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
// src/symlink-manager.ts
|
|
327
|
+
var SYMLINK_FILE = "/.opfs-symlinks.json";
|
|
328
|
+
var MAX_SYMLINK_DEPTH = 10;
|
|
329
|
+
var SymlinkManager = class {
|
|
330
|
+
cache = null;
|
|
331
|
+
cacheCount = 0;
|
|
332
|
+
// Track count to avoid Object.keys() calls
|
|
333
|
+
resolvedCache = /* @__PURE__ */ new Map();
|
|
334
|
+
// Cache resolved paths
|
|
335
|
+
dirty = false;
|
|
336
|
+
handleManager;
|
|
337
|
+
useSync;
|
|
338
|
+
loadPromise = null;
|
|
339
|
+
// Avoid multiple concurrent loads
|
|
340
|
+
diskLoaded = false;
|
|
341
|
+
// Track if we've loaded from disk
|
|
342
|
+
constructor(handleManager, useSync) {
|
|
343
|
+
this.handleManager = handleManager;
|
|
344
|
+
this.useSync = useSync;
|
|
345
|
+
this.cache = {};
|
|
346
|
+
this.cacheCount = 0;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Reset all symlink state (called when root directory is cleared)
|
|
350
|
+
*/
|
|
351
|
+
reset() {
|
|
352
|
+
this.cache = {};
|
|
353
|
+
this.cacheCount = 0;
|
|
354
|
+
this.resolvedCache.clear();
|
|
355
|
+
this.dirty = false;
|
|
356
|
+
this.loadPromise = null;
|
|
357
|
+
this.diskLoaded = false;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Load symlinks from metadata file
|
|
361
|
+
* Uses loadPromise to avoid multiple concurrent disk reads
|
|
362
|
+
*/
|
|
363
|
+
async load() {
|
|
364
|
+
if (this.diskLoaded) return this.cache;
|
|
365
|
+
if (this.loadPromise) return this.loadPromise;
|
|
366
|
+
this.loadPromise = this.loadFromDisk();
|
|
367
|
+
const result = await this.loadPromise;
|
|
368
|
+
this.loadPromise = null;
|
|
369
|
+
return result;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Actually read from disk
|
|
373
|
+
*/
|
|
374
|
+
async loadFromDisk() {
|
|
375
|
+
try {
|
|
376
|
+
const { fileHandle } = await this.handleManager.getHandle(SYMLINK_FILE);
|
|
377
|
+
if (!fileHandle) {
|
|
378
|
+
this.diskLoaded = true;
|
|
379
|
+
return this.cache;
|
|
380
|
+
}
|
|
381
|
+
const file = await fileHandle.getFile();
|
|
382
|
+
const text = await file.text();
|
|
383
|
+
this.cache = JSON.parse(text);
|
|
384
|
+
this.cacheCount = Object.keys(this.cache).length;
|
|
385
|
+
this.diskLoaded = true;
|
|
386
|
+
} catch {
|
|
387
|
+
if (!this.cache) {
|
|
388
|
+
this.cache = {};
|
|
389
|
+
this.cacheCount = 0;
|
|
390
|
+
}
|
|
391
|
+
this.diskLoaded = true;
|
|
392
|
+
}
|
|
393
|
+
return this.cache;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Save symlinks to metadata file
|
|
397
|
+
*/
|
|
398
|
+
async save() {
|
|
399
|
+
if (!this.cache) return;
|
|
400
|
+
const data = JSON.stringify(this.cache);
|
|
401
|
+
const { fileHandle } = await this.handleManager.getHandle(SYMLINK_FILE, { create: true });
|
|
402
|
+
if (!fileHandle) return;
|
|
403
|
+
const buffer = new TextEncoder().encode(data);
|
|
404
|
+
if (this.useSync) {
|
|
405
|
+
const access = await fileHandle.createSyncAccessHandle();
|
|
406
|
+
access.truncate(0);
|
|
407
|
+
let written = 0;
|
|
408
|
+
while (written < buffer.length) {
|
|
409
|
+
written += access.write(buffer.subarray(written), { at: written });
|
|
410
|
+
}
|
|
411
|
+
access.close();
|
|
412
|
+
} else {
|
|
413
|
+
const writable = await fileHandle.createWritable();
|
|
414
|
+
await writable.write(buffer);
|
|
415
|
+
await writable.close();
|
|
416
|
+
}
|
|
417
|
+
this.dirty = false;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Flush pending changes if dirty
|
|
421
|
+
*/
|
|
422
|
+
async flush() {
|
|
423
|
+
if (this.dirty) {
|
|
424
|
+
await this.save();
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Resolve a path through symlinks
|
|
429
|
+
* Fast synchronous path when cache is already loaded
|
|
430
|
+
* Uses resolved cache for O(1) repeated lookups
|
|
431
|
+
*
|
|
432
|
+
* OPTIMIZATION: If we haven't loaded from disk yet AND no symlinks have been
|
|
433
|
+
* created in this session, we skip the disk check entirely. This makes pure
|
|
434
|
+
* file operations (no symlinks) very fast.
|
|
435
|
+
*/
|
|
436
|
+
async resolve(path, maxDepth = MAX_SYMLINK_DEPTH) {
|
|
437
|
+
if (this.cacheCount === 0) {
|
|
438
|
+
return path;
|
|
439
|
+
}
|
|
440
|
+
const cached = this.resolvedCache.get(path);
|
|
441
|
+
if (cached !== void 0) {
|
|
442
|
+
return cached;
|
|
443
|
+
}
|
|
444
|
+
return this.resolveSync(path, this.cache, maxDepth);
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Synchronous resolution helper - caches the result
|
|
448
|
+
*/
|
|
449
|
+
resolveSync(path, symlinks, maxDepth) {
|
|
450
|
+
let currentPath = path;
|
|
451
|
+
let depth = 0;
|
|
452
|
+
while (symlinks[currentPath] && depth < maxDepth) {
|
|
453
|
+
currentPath = symlinks[currentPath];
|
|
454
|
+
depth++;
|
|
455
|
+
}
|
|
456
|
+
if (depth >= maxDepth) {
|
|
457
|
+
throw createELOOP(path);
|
|
458
|
+
}
|
|
459
|
+
if (currentPath !== path) {
|
|
460
|
+
this.resolvedCache.set(path, currentPath);
|
|
461
|
+
}
|
|
462
|
+
return currentPath;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Clear the resolved path cache (called when symlinks change)
|
|
466
|
+
*/
|
|
467
|
+
clearResolvedCache() {
|
|
468
|
+
this.resolvedCache.clear();
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Check if a path is a symlink
|
|
472
|
+
*/
|
|
473
|
+
async isSymlink(path) {
|
|
474
|
+
const symlinks = await this.load();
|
|
475
|
+
return !!symlinks[path];
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Get symlink target
|
|
479
|
+
*/
|
|
480
|
+
async readlink(path) {
|
|
481
|
+
const normalizedPath = normalize(path);
|
|
482
|
+
const symlinks = await this.load();
|
|
483
|
+
if (!symlinks[normalizedPath]) {
|
|
484
|
+
throw createEINVAL(path);
|
|
485
|
+
}
|
|
486
|
+
return symlinks[normalizedPath];
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Create a symlink
|
|
490
|
+
*/
|
|
491
|
+
async symlink(target, path, checkExists) {
|
|
492
|
+
const normalizedPath = normalize(path);
|
|
493
|
+
const normalizedTarget = normalize(target);
|
|
494
|
+
const symlinks = await this.load();
|
|
495
|
+
if (symlinks[normalizedPath]) {
|
|
496
|
+
throw createEEXIST(normalizedPath);
|
|
497
|
+
}
|
|
498
|
+
await checkExists();
|
|
499
|
+
symlinks[normalizedPath] = normalizedTarget;
|
|
500
|
+
this.cacheCount++;
|
|
501
|
+
this.clearResolvedCache();
|
|
502
|
+
this.dirty = true;
|
|
503
|
+
await this.flush();
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Create multiple symlinks efficiently
|
|
507
|
+
*/
|
|
508
|
+
async symlinkBatch(links, checkExists) {
|
|
509
|
+
const symlinks = await this.load();
|
|
510
|
+
const normalizedLinks = links.map(({ target, path }) => ({
|
|
511
|
+
normalizedPath: normalize(path),
|
|
512
|
+
normalizedTarget: normalize(target)
|
|
513
|
+
}));
|
|
514
|
+
for (const { normalizedPath } of normalizedLinks) {
|
|
515
|
+
if (symlinks[normalizedPath]) {
|
|
516
|
+
throw createEEXIST(normalizedPath);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
await Promise.all(normalizedLinks.map(({ normalizedPath }) => checkExists(normalizedPath)));
|
|
520
|
+
for (const { normalizedPath, normalizedTarget } of normalizedLinks) {
|
|
521
|
+
symlinks[normalizedPath] = normalizedTarget;
|
|
522
|
+
}
|
|
523
|
+
this.cacheCount += links.length;
|
|
524
|
+
this.clearResolvedCache();
|
|
525
|
+
this.dirty = true;
|
|
526
|
+
await this.flush();
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Remove a symlink
|
|
530
|
+
*/
|
|
531
|
+
async unlink(path) {
|
|
532
|
+
const symlinks = await this.load();
|
|
533
|
+
if (symlinks[path]) {
|
|
534
|
+
delete symlinks[path];
|
|
535
|
+
this.cacheCount--;
|
|
536
|
+
this.clearResolvedCache();
|
|
537
|
+
this.dirty = true;
|
|
538
|
+
await this.flush();
|
|
539
|
+
return true;
|
|
540
|
+
}
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Rename/move a symlink
|
|
545
|
+
*/
|
|
546
|
+
async rename(oldPath, newPath) {
|
|
547
|
+
const symlinks = await this.load();
|
|
548
|
+
if (symlinks[oldPath]) {
|
|
549
|
+
const target = symlinks[oldPath];
|
|
550
|
+
delete symlinks[oldPath];
|
|
551
|
+
symlinks[newPath] = target;
|
|
552
|
+
this.clearResolvedCache();
|
|
553
|
+
this.dirty = true;
|
|
554
|
+
await this.flush();
|
|
555
|
+
return true;
|
|
556
|
+
}
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Get all symlinks in a directory
|
|
561
|
+
*/
|
|
562
|
+
async getSymlinksInDir(dirPath) {
|
|
563
|
+
const symlinks = await this.load();
|
|
564
|
+
const result = [];
|
|
565
|
+
for (const symlinkPath of Object.keys(symlinks)) {
|
|
566
|
+
const parts = symlinkPath.split("/").filter(Boolean);
|
|
567
|
+
const parentPath = "/" + parts.slice(0, -1).join("/");
|
|
568
|
+
if (parentPath === dirPath || dirPath === "/" && parts.length === 1) {
|
|
569
|
+
result.push(parts[parts.length - 1]);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return result;
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Check if path is the symlink metadata file
|
|
576
|
+
*/
|
|
577
|
+
isMetadataFile(name) {
|
|
578
|
+
return name === SYMLINK_FILE.replace(/^\/+/, "");
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
// src/packed-storage.ts
|
|
583
|
+
var CRC32_TABLE = new Uint32Array(256);
|
|
584
|
+
for (let i = 0; i < 256; i++) {
|
|
585
|
+
let c = i;
|
|
586
|
+
for (let j = 0; j < 8; j++) {
|
|
587
|
+
c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
588
|
+
}
|
|
589
|
+
CRC32_TABLE[i] = c;
|
|
590
|
+
}
|
|
591
|
+
function crc32(data) {
|
|
592
|
+
let crc = 4294967295;
|
|
593
|
+
for (let i = 0; i < data.length; i++) {
|
|
594
|
+
crc = CRC32_TABLE[(crc ^ data[i]) & 255] ^ crc >>> 8;
|
|
595
|
+
}
|
|
596
|
+
return (crc ^ 4294967295) >>> 0;
|
|
597
|
+
}
|
|
598
|
+
var PACK_FILE = "/.opfs-pack";
|
|
599
|
+
var PackedStorage = class {
|
|
600
|
+
handleManager;
|
|
601
|
+
useSync;
|
|
602
|
+
index = null;
|
|
603
|
+
indexLoaded = false;
|
|
604
|
+
constructor(handleManager, useSync) {
|
|
605
|
+
this.handleManager = handleManager;
|
|
606
|
+
this.useSync = useSync;
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Reset pack storage state (memory only)
|
|
610
|
+
*/
|
|
611
|
+
reset() {
|
|
612
|
+
this.index = null;
|
|
613
|
+
this.indexLoaded = false;
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Clear pack storage completely (deletes pack file from disk)
|
|
617
|
+
*/
|
|
618
|
+
async clear() {
|
|
619
|
+
this.index = null;
|
|
620
|
+
this.indexLoaded = false;
|
|
621
|
+
try {
|
|
622
|
+
const root = await this.handleManager.getRoot();
|
|
623
|
+
await root.removeEntry(PACK_FILE.replace(/^\//, ""));
|
|
624
|
+
} catch {
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Load pack index from disk (always reloads to support hybrid mode)
|
|
629
|
+
* Verifies CRC32 checksum for integrity
|
|
630
|
+
*/
|
|
631
|
+
async loadIndex() {
|
|
632
|
+
try {
|
|
633
|
+
const { fileHandle } = await this.handleManager.getHandle(PACK_FILE);
|
|
634
|
+
if (!fileHandle) {
|
|
635
|
+
return {};
|
|
636
|
+
}
|
|
637
|
+
if (this.useSync) {
|
|
638
|
+
const access = await fileHandle.createSyncAccessHandle();
|
|
639
|
+
const size = access.getSize();
|
|
640
|
+
if (size < 8) {
|
|
641
|
+
access.close();
|
|
642
|
+
return {};
|
|
643
|
+
}
|
|
644
|
+
const header = new Uint8Array(8);
|
|
645
|
+
access.read(header, { at: 0 });
|
|
646
|
+
const view = new DataView(header.buffer);
|
|
647
|
+
const indexLen = view.getUint32(0, true);
|
|
648
|
+
const storedCrc = view.getUint32(4, true);
|
|
649
|
+
const contentSize = size - 8;
|
|
650
|
+
const content = new Uint8Array(contentSize);
|
|
651
|
+
access.read(content, { at: 8 });
|
|
652
|
+
access.close();
|
|
653
|
+
const calculatedCrc = crc32(content);
|
|
654
|
+
if (calculatedCrc !== storedCrc) {
|
|
655
|
+
throw createECORRUPTED(PACK_FILE);
|
|
656
|
+
}
|
|
657
|
+
const indexJson = new TextDecoder().decode(content.subarray(0, indexLen));
|
|
658
|
+
return JSON.parse(indexJson);
|
|
659
|
+
} else {
|
|
660
|
+
const file = await fileHandle.getFile();
|
|
661
|
+
const data = new Uint8Array(await file.arrayBuffer());
|
|
662
|
+
if (data.length < 8) {
|
|
663
|
+
return {};
|
|
664
|
+
}
|
|
665
|
+
const view = new DataView(data.buffer);
|
|
666
|
+
const indexLen = view.getUint32(0, true);
|
|
667
|
+
const storedCrc = view.getUint32(4, true);
|
|
668
|
+
const content = data.subarray(8);
|
|
669
|
+
const calculatedCrc = crc32(content);
|
|
670
|
+
if (calculatedCrc !== storedCrc) {
|
|
671
|
+
throw createECORRUPTED(PACK_FILE);
|
|
672
|
+
}
|
|
673
|
+
const indexJson = new TextDecoder().decode(content.subarray(0, indexLen));
|
|
674
|
+
return JSON.parse(indexJson);
|
|
675
|
+
}
|
|
676
|
+
} catch {
|
|
677
|
+
return {};
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Check if a path exists in the pack
|
|
682
|
+
*/
|
|
683
|
+
async has(path) {
|
|
684
|
+
const index = await this.loadIndex();
|
|
685
|
+
return path in index;
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Get file size from pack (for stat)
|
|
689
|
+
*/
|
|
690
|
+
async getSize(path) {
|
|
691
|
+
const index = await this.loadIndex();
|
|
692
|
+
const entry = index[path];
|
|
693
|
+
return entry ? entry.size : null;
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Read a file from the pack
|
|
697
|
+
*/
|
|
698
|
+
async read(path) {
|
|
699
|
+
const index = await this.loadIndex();
|
|
700
|
+
const entry = index[path];
|
|
701
|
+
if (!entry) return null;
|
|
702
|
+
const { fileHandle } = await this.handleManager.getHandle(PACK_FILE);
|
|
703
|
+
if (!fileHandle) return null;
|
|
704
|
+
const buffer = new Uint8Array(entry.size);
|
|
705
|
+
if (this.useSync) {
|
|
706
|
+
const access = await fileHandle.createSyncAccessHandle();
|
|
707
|
+
access.read(buffer, { at: entry.offset });
|
|
708
|
+
access.close();
|
|
709
|
+
} else {
|
|
710
|
+
const file = await fileHandle.getFile();
|
|
711
|
+
const data = new Uint8Array(await file.arrayBuffer());
|
|
712
|
+
buffer.set(data.subarray(entry.offset, entry.offset + entry.size));
|
|
713
|
+
}
|
|
714
|
+
return buffer;
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Read multiple files from the pack in a single operation
|
|
718
|
+
* Loads index once, reads all data in parallel
|
|
719
|
+
*/
|
|
720
|
+
async readBatch(paths) {
|
|
721
|
+
const results = /* @__PURE__ */ new Map();
|
|
722
|
+
if (paths.length === 0) return results;
|
|
723
|
+
const index = await this.loadIndex();
|
|
724
|
+
const toRead = [];
|
|
725
|
+
for (const path of paths) {
|
|
726
|
+
const entry = index[path];
|
|
727
|
+
if (entry) {
|
|
728
|
+
toRead.push({ path, offset: entry.offset, size: entry.size });
|
|
729
|
+
} else {
|
|
730
|
+
results.set(path, null);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
if (toRead.length === 0) return results;
|
|
734
|
+
const { fileHandle } = await this.handleManager.getHandle(PACK_FILE);
|
|
735
|
+
if (!fileHandle) {
|
|
736
|
+
for (const { path } of toRead) {
|
|
737
|
+
results.set(path, null);
|
|
738
|
+
}
|
|
739
|
+
return results;
|
|
740
|
+
}
|
|
741
|
+
if (this.useSync) {
|
|
742
|
+
const access = await fileHandle.createSyncAccessHandle();
|
|
743
|
+
for (const { path, offset, size } of toRead) {
|
|
744
|
+
const buffer = new Uint8Array(size);
|
|
745
|
+
access.read(buffer, { at: offset });
|
|
746
|
+
results.set(path, buffer);
|
|
747
|
+
}
|
|
748
|
+
access.close();
|
|
749
|
+
} else {
|
|
750
|
+
const file = await fileHandle.getFile();
|
|
751
|
+
const data = new Uint8Array(await file.arrayBuffer());
|
|
752
|
+
for (const { path, offset, size } of toRead) {
|
|
753
|
+
const buffer = new Uint8Array(size);
|
|
754
|
+
buffer.set(data.subarray(offset, offset + size));
|
|
755
|
+
results.set(path, buffer);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
return results;
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Write multiple files to the pack in a single operation
|
|
762
|
+
* This is the key optimization - 100 files become 1 write!
|
|
763
|
+
* Includes CRC32 checksum for integrity verification.
|
|
764
|
+
* Note: This replaces the entire pack with the new entries
|
|
765
|
+
*/
|
|
766
|
+
async writeBatch(entries) {
|
|
767
|
+
if (entries.length === 0) return;
|
|
768
|
+
const encoder = new TextEncoder();
|
|
769
|
+
let totalDataSize = 0;
|
|
770
|
+
for (const { data } of entries) {
|
|
771
|
+
totalDataSize += data.length;
|
|
772
|
+
}
|
|
773
|
+
const newIndex = {};
|
|
774
|
+
let headerSize = 8;
|
|
775
|
+
let prevHeaderSize = 0;
|
|
776
|
+
while (headerSize !== prevHeaderSize) {
|
|
777
|
+
prevHeaderSize = headerSize;
|
|
778
|
+
let currentOffset = headerSize;
|
|
779
|
+
for (const { path, data } of entries) {
|
|
780
|
+
newIndex[path] = { offset: currentOffset, size: data.length };
|
|
781
|
+
currentOffset += data.length;
|
|
782
|
+
}
|
|
783
|
+
const indexBuf = encoder.encode(JSON.stringify(newIndex));
|
|
784
|
+
headerSize = 8 + indexBuf.length;
|
|
785
|
+
}
|
|
786
|
+
const finalIndexBuf = encoder.encode(JSON.stringify(newIndex));
|
|
787
|
+
const totalSize = headerSize + totalDataSize;
|
|
788
|
+
const packBuffer = new Uint8Array(totalSize);
|
|
789
|
+
const view = new DataView(packBuffer.buffer);
|
|
790
|
+
packBuffer.set(finalIndexBuf, 8);
|
|
791
|
+
for (const { path, data } of entries) {
|
|
792
|
+
const entry = newIndex[path];
|
|
793
|
+
packBuffer.set(data, entry.offset);
|
|
794
|
+
}
|
|
795
|
+
const content = packBuffer.subarray(8);
|
|
796
|
+
const checksum = crc32(content);
|
|
797
|
+
view.setUint32(0, finalIndexBuf.length, true);
|
|
798
|
+
view.setUint32(4, checksum, true);
|
|
799
|
+
await this.writePackFile(packBuffer);
|
|
800
|
+
this.index = newIndex;
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Write the pack file to OPFS
|
|
804
|
+
*/
|
|
805
|
+
async writePackFile(data) {
|
|
806
|
+
const { fileHandle } = await this.handleManager.getHandle(PACK_FILE, { create: true });
|
|
807
|
+
if (!fileHandle) return;
|
|
808
|
+
if (this.useSync) {
|
|
809
|
+
const access = await fileHandle.createSyncAccessHandle();
|
|
810
|
+
access.truncate(data.length);
|
|
811
|
+
access.write(data, { at: 0 });
|
|
812
|
+
access.close();
|
|
813
|
+
} else {
|
|
814
|
+
const writable = await fileHandle.createWritable();
|
|
815
|
+
await writable.write(data);
|
|
816
|
+
await writable.close();
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* Remove a path from the pack index
|
|
821
|
+
* Note: Doesn't reclaim space, just removes from index and recalculates CRC32
|
|
822
|
+
*/
|
|
823
|
+
async remove(path) {
|
|
824
|
+
const index = await this.loadIndex();
|
|
825
|
+
if (!(path in index)) return false;
|
|
826
|
+
delete index[path];
|
|
827
|
+
const { fileHandle } = await this.handleManager.getHandle(PACK_FILE);
|
|
828
|
+
if (!fileHandle) return true;
|
|
829
|
+
const encoder = new TextEncoder();
|
|
830
|
+
const newIndexBuf = encoder.encode(JSON.stringify(index));
|
|
831
|
+
if (this.useSync) {
|
|
832
|
+
const access = await fileHandle.createSyncAccessHandle();
|
|
833
|
+
const size = access.getSize();
|
|
834
|
+
const oldHeader = new Uint8Array(8);
|
|
835
|
+
access.read(oldHeader, { at: 0 });
|
|
836
|
+
const oldIndexLen = new DataView(oldHeader.buffer).getUint32(0, true);
|
|
837
|
+
const dataStart = 8 + oldIndexLen;
|
|
838
|
+
const dataSize = size - dataStart;
|
|
839
|
+
const dataPortion = new Uint8Array(dataSize);
|
|
840
|
+
if (dataSize > 0) {
|
|
841
|
+
access.read(dataPortion, { at: dataStart });
|
|
842
|
+
}
|
|
843
|
+
const newContent = new Uint8Array(newIndexBuf.length + dataSize);
|
|
844
|
+
newContent.set(newIndexBuf, 0);
|
|
845
|
+
if (dataSize > 0) {
|
|
846
|
+
newContent.set(dataPortion, newIndexBuf.length);
|
|
847
|
+
}
|
|
848
|
+
const checksum = crc32(newContent);
|
|
849
|
+
const newHeader = new Uint8Array(8);
|
|
850
|
+
const view = new DataView(newHeader.buffer);
|
|
851
|
+
view.setUint32(0, newIndexBuf.length, true);
|
|
852
|
+
view.setUint32(4, checksum, true);
|
|
853
|
+
const newFile = new Uint8Array(8 + newContent.length);
|
|
854
|
+
newFile.set(newHeader, 0);
|
|
855
|
+
newFile.set(newContent, 8);
|
|
856
|
+
access.truncate(newFile.length);
|
|
857
|
+
access.write(newFile, { at: 0 });
|
|
858
|
+
access.close();
|
|
859
|
+
} else {
|
|
860
|
+
const file = await fileHandle.getFile();
|
|
861
|
+
const oldData = new Uint8Array(await file.arrayBuffer());
|
|
862
|
+
if (oldData.length < 8) return true;
|
|
863
|
+
const oldIndexLen = new DataView(oldData.buffer).getUint32(0, true);
|
|
864
|
+
const dataStart = 8 + oldIndexLen;
|
|
865
|
+
const dataPortion = oldData.subarray(dataStart);
|
|
866
|
+
const newContent = new Uint8Array(newIndexBuf.length + dataPortion.length);
|
|
867
|
+
newContent.set(newIndexBuf, 0);
|
|
868
|
+
newContent.set(dataPortion, newIndexBuf.length);
|
|
869
|
+
const checksum = crc32(newContent);
|
|
870
|
+
const newFile = new Uint8Array(8 + newContent.length);
|
|
871
|
+
const view = new DataView(newFile.buffer);
|
|
872
|
+
view.setUint32(0, newIndexBuf.length, true);
|
|
873
|
+
view.setUint32(4, checksum, true);
|
|
874
|
+
newFile.set(newContent, 8);
|
|
875
|
+
const writable = await fileHandle.createWritable();
|
|
876
|
+
await writable.write(newFile);
|
|
877
|
+
await writable.close();
|
|
878
|
+
}
|
|
879
|
+
return true;
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Check if pack file is being used (has entries)
|
|
883
|
+
*/
|
|
884
|
+
async isEmpty() {
|
|
885
|
+
const index = await this.loadIndex();
|
|
886
|
+
return Object.keys(index).length === 0;
|
|
887
|
+
}
|
|
888
|
+
};
|
|
889
|
+
|
|
890
|
+
// src/file-handle.ts
|
|
891
|
+
function createFileHandle(resolvedPath, initialPosition, context) {
|
|
892
|
+
let position = initialPosition;
|
|
893
|
+
return {
|
|
894
|
+
fd: Math.floor(Math.random() * 1e6),
|
|
895
|
+
async read(buffer, offset = 0, length = buffer.length, pos = null) {
|
|
896
|
+
const readPos = pos !== null ? pos : position;
|
|
897
|
+
const data = await context.readFile(resolvedPath);
|
|
898
|
+
const bytesToRead = Math.min(length, data.length - readPos);
|
|
899
|
+
buffer.set(data.subarray(readPos, readPos + bytesToRead), offset);
|
|
900
|
+
if (pos === null) position += bytesToRead;
|
|
901
|
+
return { bytesRead: bytesToRead, buffer };
|
|
902
|
+
},
|
|
903
|
+
async write(buffer, offset = 0, length = buffer.length, pos = null) {
|
|
904
|
+
const writePos = pos !== null ? pos : position;
|
|
905
|
+
let existingData = new Uint8Array(0);
|
|
906
|
+
try {
|
|
907
|
+
existingData = await context.readFile(resolvedPath);
|
|
908
|
+
} catch (e) {
|
|
909
|
+
if (e.code !== "ENOENT") throw e;
|
|
910
|
+
}
|
|
911
|
+
const dataToWrite = buffer.subarray(offset, offset + length);
|
|
912
|
+
const newSize = Math.max(existingData.length, writePos + length);
|
|
913
|
+
const newData = new Uint8Array(newSize);
|
|
914
|
+
newData.set(existingData, 0);
|
|
915
|
+
newData.set(dataToWrite, writePos);
|
|
916
|
+
await context.writeFile(resolvedPath, newData);
|
|
917
|
+
if (pos === null) position += length;
|
|
918
|
+
return { bytesWritten: length, buffer };
|
|
919
|
+
},
|
|
920
|
+
async close() {
|
|
921
|
+
},
|
|
922
|
+
async stat() {
|
|
923
|
+
return context.stat(resolvedPath);
|
|
924
|
+
},
|
|
925
|
+
async truncate(len = 0) {
|
|
926
|
+
return context.truncate(resolvedPath, len);
|
|
927
|
+
},
|
|
928
|
+
async sync() {
|
|
929
|
+
},
|
|
930
|
+
async datasync() {
|
|
931
|
+
},
|
|
932
|
+
async readFile(options) {
|
|
933
|
+
return context.readFile(resolvedPath, options);
|
|
934
|
+
},
|
|
935
|
+
async writeFile(data, options) {
|
|
936
|
+
return context.writeFile(resolvedPath, data, options);
|
|
937
|
+
},
|
|
938
|
+
async appendFile(data, options) {
|
|
939
|
+
return context.appendFile(resolvedPath, data, options);
|
|
940
|
+
},
|
|
941
|
+
[Symbol.asyncDispose]: async function() {
|
|
942
|
+
}
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// src/streams.ts
|
|
947
|
+
function createReadStream(path, options, context) {
|
|
948
|
+
const { start = 0, end = Infinity, highWaterMark = 64 * 1024 } = options;
|
|
949
|
+
let position = start;
|
|
950
|
+
let closed = false;
|
|
951
|
+
let cachedData = null;
|
|
952
|
+
return new ReadableStream({
|
|
953
|
+
async pull(controller) {
|
|
954
|
+
if (closed) {
|
|
955
|
+
controller.close();
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
try {
|
|
959
|
+
if (cachedData === null) {
|
|
960
|
+
cachedData = await context.readFile(path);
|
|
961
|
+
}
|
|
962
|
+
const endPos = Math.min(end, cachedData.length);
|
|
963
|
+
const chunk = cachedData.subarray(position, Math.min(position + highWaterMark, endPos));
|
|
964
|
+
if (chunk.length === 0 || position >= endPos) {
|
|
965
|
+
controller.close();
|
|
966
|
+
closed = true;
|
|
967
|
+
cachedData = null;
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
position += chunk.length;
|
|
971
|
+
controller.enqueue(chunk);
|
|
972
|
+
} catch (err) {
|
|
973
|
+
controller.error(err);
|
|
974
|
+
}
|
|
975
|
+
},
|
|
976
|
+
cancel() {
|
|
977
|
+
closed = true;
|
|
978
|
+
cachedData = null;
|
|
979
|
+
}
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
function createWriteStream(path, options, context) {
|
|
983
|
+
const { flags = "w", start = 0 } = options;
|
|
984
|
+
const chunks = [];
|
|
985
|
+
let position = start;
|
|
986
|
+
return new WritableStream({
|
|
987
|
+
async write(chunk) {
|
|
988
|
+
chunks.push({ data: chunk, position });
|
|
989
|
+
position += chunk.length;
|
|
990
|
+
},
|
|
991
|
+
async close() {
|
|
992
|
+
let existingData = new Uint8Array(0);
|
|
993
|
+
if (!flags.includes("w")) {
|
|
994
|
+
try {
|
|
995
|
+
existingData = await context.readFile(path);
|
|
996
|
+
} catch (e) {
|
|
997
|
+
if (e.code !== "ENOENT") throw e;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
let maxSize = existingData.length;
|
|
1001
|
+
for (const { data, position: position2 } of chunks) {
|
|
1002
|
+
maxSize = Math.max(maxSize, position2 + data.length);
|
|
1003
|
+
}
|
|
1004
|
+
const finalData = new Uint8Array(maxSize);
|
|
1005
|
+
if (!flags.includes("w")) {
|
|
1006
|
+
finalData.set(existingData, 0);
|
|
1007
|
+
}
|
|
1008
|
+
for (const { data, position: position2 } of chunks) {
|
|
1009
|
+
finalData.set(data, position2);
|
|
1010
|
+
}
|
|
1011
|
+
await context.writeFile(path, finalData);
|
|
1012
|
+
}
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// src/opfs-worker-proxy.ts
|
|
1017
|
+
var OPFSWorker = class {
|
|
1018
|
+
worker = null;
|
|
1019
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
1020
|
+
nextId = 1;
|
|
1021
|
+
readyPromise;
|
|
1022
|
+
readyResolve;
|
|
1023
|
+
/** File system constants */
|
|
1024
|
+
constants = constants;
|
|
1025
|
+
constructor(options = {}) {
|
|
1026
|
+
this.readyPromise = new Promise((resolve) => {
|
|
1027
|
+
this.readyResolve = resolve;
|
|
1028
|
+
});
|
|
1029
|
+
this.initWorker(options);
|
|
1030
|
+
}
|
|
1031
|
+
initWorker(options) {
|
|
1032
|
+
const { workerUrl, workerOptions = { type: "module" } } = options;
|
|
1033
|
+
if (workerUrl) {
|
|
1034
|
+
this.worker = new Worker(workerUrl, workerOptions);
|
|
1035
|
+
} else {
|
|
1036
|
+
throw new Error(
|
|
1037
|
+
'OPFSWorker requires a workerUrl option pointing to the worker script. Example: new OPFSWorker({ workerUrl: new URL("./opfs-worker.js", import.meta.url) })'
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
this.worker.onmessage = (event) => {
|
|
1041
|
+
const { id, type, result, error } = event.data;
|
|
1042
|
+
if (type === "ready") {
|
|
1043
|
+
this.readyResolve();
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
if (id !== void 0) {
|
|
1047
|
+
const pending = this.pendingRequests.get(id);
|
|
1048
|
+
if (pending) {
|
|
1049
|
+
this.pendingRequests.delete(id);
|
|
1050
|
+
if (error) {
|
|
1051
|
+
const fsError = new FSError(error.message, error.code || "UNKNOWN");
|
|
1052
|
+
pending.reject(fsError);
|
|
1053
|
+
} else {
|
|
1054
|
+
pending.resolve(result);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
};
|
|
1059
|
+
this.worker.onerror = (event) => {
|
|
1060
|
+
console.error("[OPFSWorker] Worker error:", event);
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
/**
|
|
1064
|
+
* Wait for the worker to be ready
|
|
1065
|
+
*/
|
|
1066
|
+
async ready() {
|
|
1067
|
+
return this.readyPromise;
|
|
1068
|
+
}
|
|
1069
|
+
/**
|
|
1070
|
+
* Terminate the worker
|
|
1071
|
+
*/
|
|
1072
|
+
terminate() {
|
|
1073
|
+
if (this.worker) {
|
|
1074
|
+
this.worker.terminate();
|
|
1075
|
+
this.worker = null;
|
|
1076
|
+
for (const [, pending] of this.pendingRequests) {
|
|
1077
|
+
pending.reject(new Error("Worker terminated"));
|
|
1078
|
+
}
|
|
1079
|
+
this.pendingRequests.clear();
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
call(method, args, transfer) {
|
|
1083
|
+
return new Promise((resolve, reject) => {
|
|
1084
|
+
if (!this.worker) {
|
|
1085
|
+
reject(new Error("Worker not initialized or terminated"));
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
const id = this.nextId++;
|
|
1089
|
+
this.pendingRequests.set(id, {
|
|
1090
|
+
resolve,
|
|
1091
|
+
reject
|
|
1092
|
+
});
|
|
1093
|
+
const message = { id, method, args };
|
|
1094
|
+
if (transfer && transfer.length > 0) {
|
|
1095
|
+
this.worker.postMessage(message, transfer);
|
|
1096
|
+
} else {
|
|
1097
|
+
this.worker.postMessage(message);
|
|
1098
|
+
}
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
// File operations
|
|
1102
|
+
async readFile(path, options) {
|
|
1103
|
+
const result = await this.call("readFile", [path, options]);
|
|
1104
|
+
return result;
|
|
1105
|
+
}
|
|
1106
|
+
async writeFile(path, data, options) {
|
|
1107
|
+
await this.call("writeFile", [path, data, options]);
|
|
1108
|
+
}
|
|
1109
|
+
async readFileBatch(paths) {
|
|
1110
|
+
return this.call("readFileBatch", [paths]);
|
|
1111
|
+
}
|
|
1112
|
+
async writeFileBatch(entries) {
|
|
1113
|
+
await this.call("writeFileBatch", [entries]);
|
|
1114
|
+
}
|
|
1115
|
+
async appendFile(path, data, options) {
|
|
1116
|
+
await this.call("appendFile", [path, data, options]);
|
|
1117
|
+
}
|
|
1118
|
+
async copyFile(src, dest, mode) {
|
|
1119
|
+
await this.call("copyFile", [src, dest, mode]);
|
|
1120
|
+
}
|
|
1121
|
+
async unlink(path) {
|
|
1122
|
+
await this.call("unlink", [path]);
|
|
1123
|
+
}
|
|
1124
|
+
async truncate(path, len) {
|
|
1125
|
+
await this.call("truncate", [path, len]);
|
|
1126
|
+
}
|
|
1127
|
+
// Directory operations
|
|
1128
|
+
async mkdir(path) {
|
|
1129
|
+
await this.call("mkdir", [path]);
|
|
1130
|
+
}
|
|
1131
|
+
async rmdir(path) {
|
|
1132
|
+
await this.call("rmdir", [path]);
|
|
1133
|
+
}
|
|
1134
|
+
async readdir(path, options) {
|
|
1135
|
+
const result = await this.call("readdir", [path, options]);
|
|
1136
|
+
if (options?.withFileTypes && Array.isArray(result)) {
|
|
1137
|
+
return result.map((item) => {
|
|
1138
|
+
if (typeof item === "object" && "name" in item) {
|
|
1139
|
+
const entry = item;
|
|
1140
|
+
return {
|
|
1141
|
+
name: entry.name,
|
|
1142
|
+
isFile: () => entry._isFile ?? false,
|
|
1143
|
+
isDirectory: () => entry._isDir ?? false,
|
|
1144
|
+
isSymbolicLink: () => entry._isSymlink ?? false
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
return item;
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
return result;
|
|
1151
|
+
}
|
|
1152
|
+
async cp(src, dest, options) {
|
|
1153
|
+
await this.call("cp", [src, dest, options]);
|
|
1154
|
+
}
|
|
1155
|
+
async rm(path, options) {
|
|
1156
|
+
await this.call("rm", [path, options]);
|
|
1157
|
+
}
|
|
1158
|
+
// Stat operations
|
|
1159
|
+
async stat(path) {
|
|
1160
|
+
const result = await this.call("stat", [path]);
|
|
1161
|
+
return this.deserializeStats(result);
|
|
1162
|
+
}
|
|
1163
|
+
async lstat(path) {
|
|
1164
|
+
const result = await this.call("lstat", [path]);
|
|
1165
|
+
return this.deserializeStats(result);
|
|
1166
|
+
}
|
|
1167
|
+
deserializeStats(data) {
|
|
1168
|
+
const ctime = new Date(data.ctime);
|
|
1169
|
+
const mtime = new Date(data.mtime);
|
|
1170
|
+
return {
|
|
1171
|
+
type: data.type,
|
|
1172
|
+
size: data.size,
|
|
1173
|
+
mode: data.mode,
|
|
1174
|
+
ctime,
|
|
1175
|
+
ctimeMs: data.ctimeMs,
|
|
1176
|
+
mtime,
|
|
1177
|
+
mtimeMs: data.mtimeMs,
|
|
1178
|
+
target: data.target,
|
|
1179
|
+
isFile: () => data.type === "file",
|
|
1180
|
+
isDirectory: () => data.type === "dir",
|
|
1181
|
+
isSymbolicLink: () => data.type === "symlink"
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
async exists(path) {
|
|
1185
|
+
return this.call("exists", [path]);
|
|
1186
|
+
}
|
|
1187
|
+
async access(path, mode) {
|
|
1188
|
+
await this.call("access", [path, mode]);
|
|
1189
|
+
}
|
|
1190
|
+
async statfs(path) {
|
|
1191
|
+
return this.call("statfs", [path]);
|
|
1192
|
+
}
|
|
1193
|
+
async du(path) {
|
|
1194
|
+
return this.call("du", [path]);
|
|
1195
|
+
}
|
|
1196
|
+
// Symlink operations
|
|
1197
|
+
async symlink(target, path) {
|
|
1198
|
+
await this.call("symlink", [target, path]);
|
|
1199
|
+
}
|
|
1200
|
+
async readlink(path) {
|
|
1201
|
+
return this.call("readlink", [path]);
|
|
1202
|
+
}
|
|
1203
|
+
async symlinkBatch(links) {
|
|
1204
|
+
await this.call("symlinkBatch", [links]);
|
|
1205
|
+
}
|
|
1206
|
+
async realpath(path) {
|
|
1207
|
+
return this.call("realpath", [path]);
|
|
1208
|
+
}
|
|
1209
|
+
// Other operations
|
|
1210
|
+
async rename(oldPath, newPath) {
|
|
1211
|
+
await this.call("rename", [oldPath, newPath]);
|
|
1212
|
+
}
|
|
1213
|
+
async mkdtemp(prefix) {
|
|
1214
|
+
return this.call("mkdtemp", [prefix]);
|
|
1215
|
+
}
|
|
1216
|
+
async chmod(path, mode) {
|
|
1217
|
+
await this.call("chmod", [path, mode]);
|
|
1218
|
+
}
|
|
1219
|
+
async chown(path, uid, gid) {
|
|
1220
|
+
await this.call("chown", [path, uid, gid]);
|
|
1221
|
+
}
|
|
1222
|
+
async utimes(path, atime, mtime) {
|
|
1223
|
+
await this.call("utimes", [path, atime, mtime]);
|
|
1224
|
+
}
|
|
1225
|
+
async lutimes(path, atime, mtime) {
|
|
1226
|
+
await this.call("lutimes", [path, atime, mtime]);
|
|
1227
|
+
}
|
|
1228
|
+
/**
|
|
1229
|
+
* Reset internal caches to free memory
|
|
1230
|
+
* Useful for long-running benchmarks or after bulk operations
|
|
1231
|
+
*/
|
|
1232
|
+
async resetCache() {
|
|
1233
|
+
await this.call("resetCache", []);
|
|
1234
|
+
}
|
|
1235
|
+
/**
|
|
1236
|
+
* Force full garbage collection by reinitializing the OPFS instance in the worker
|
|
1237
|
+
* This completely releases all handles and caches, preventing memory leaks in long-running operations
|
|
1238
|
+
* More aggressive than resetCache() - use when resetCache() isn't sufficient
|
|
1239
|
+
*/
|
|
1240
|
+
async gc() {
|
|
1241
|
+
await this.call("gc", []);
|
|
1242
|
+
}
|
|
1243
|
+
};
|
|
1244
|
+
|
|
1245
|
+
// src/opfs-hybrid.ts
|
|
1246
|
+
var OPFSHybrid = class {
|
|
1247
|
+
mainFs;
|
|
1248
|
+
workerFs = null;
|
|
1249
|
+
readBackend;
|
|
1250
|
+
writeBackend;
|
|
1251
|
+
workerUrl;
|
|
1252
|
+
workerReady = null;
|
|
1253
|
+
verbose;
|
|
1254
|
+
constructor(options = {}) {
|
|
1255
|
+
this.readBackend = options.read ?? "main";
|
|
1256
|
+
this.writeBackend = options.write ?? "worker";
|
|
1257
|
+
this.workerUrl = options.workerUrl;
|
|
1258
|
+
this.verbose = options.verbose ?? false;
|
|
1259
|
+
this.mainFs = new OPFS({ useSync: false, verbose: this.verbose });
|
|
1260
|
+
if (this.readBackend === "worker" || this.writeBackend === "worker") {
|
|
1261
|
+
if (!this.workerUrl) {
|
|
1262
|
+
throw new Error("workerUrl is required when using worker backend");
|
|
1263
|
+
}
|
|
1264
|
+
this.workerFs = new OPFSWorker({ workerUrl: this.workerUrl });
|
|
1265
|
+
this.workerReady = this.workerFs.ready();
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
/**
|
|
1269
|
+
* Wait for all backends to be ready
|
|
1270
|
+
*/
|
|
1271
|
+
async ready() {
|
|
1272
|
+
if (this.workerReady) {
|
|
1273
|
+
await this.workerReady;
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
/**
|
|
1277
|
+
* Terminate worker if active
|
|
1278
|
+
*/
|
|
1279
|
+
terminate() {
|
|
1280
|
+
if (this.workerFs) {
|
|
1281
|
+
this.workerFs.terminate();
|
|
1282
|
+
this.workerFs = null;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
getReadFs() {
|
|
1286
|
+
if (this.readBackend === "worker" && this.workerFs) {
|
|
1287
|
+
return this.workerFs;
|
|
1288
|
+
}
|
|
1289
|
+
return this.mainFs;
|
|
1290
|
+
}
|
|
1291
|
+
getWriteFs() {
|
|
1292
|
+
if (this.writeBackend === "worker" && this.workerFs) {
|
|
1293
|
+
return this.workerFs;
|
|
1294
|
+
}
|
|
1295
|
+
return this.mainFs;
|
|
1296
|
+
}
|
|
1297
|
+
// ============ Read Operations ============
|
|
1298
|
+
async readFile(path, options) {
|
|
1299
|
+
return this.getReadFs().readFile(path, options);
|
|
1300
|
+
}
|
|
1301
|
+
async readFileBatch(paths) {
|
|
1302
|
+
return this.getReadFs().readFileBatch(paths);
|
|
1303
|
+
}
|
|
1304
|
+
async readdir(path, options) {
|
|
1305
|
+
return this.getReadFs().readdir(path, options);
|
|
1306
|
+
}
|
|
1307
|
+
async stat(path) {
|
|
1308
|
+
return this.getReadFs().stat(path);
|
|
1309
|
+
}
|
|
1310
|
+
async lstat(path) {
|
|
1311
|
+
return this.getReadFs().lstat(path);
|
|
1312
|
+
}
|
|
1313
|
+
async exists(path) {
|
|
1314
|
+
return this.getReadFs().exists(path);
|
|
1315
|
+
}
|
|
1316
|
+
async access(path, mode) {
|
|
1317
|
+
return this.getReadFs().access(path, mode);
|
|
1318
|
+
}
|
|
1319
|
+
async readlink(path) {
|
|
1320
|
+
return this.getReadFs().readlink(path);
|
|
1321
|
+
}
|
|
1322
|
+
async realpath(path) {
|
|
1323
|
+
return this.getReadFs().realpath(path);
|
|
1324
|
+
}
|
|
1325
|
+
async statfs(path) {
|
|
1326
|
+
return this.getReadFs().statfs(path);
|
|
1327
|
+
}
|
|
1328
|
+
async du(path) {
|
|
1329
|
+
return this.getReadFs().du(path);
|
|
1330
|
+
}
|
|
1331
|
+
// ============ Write Operations ============
|
|
1332
|
+
async writeFile(path, data, options) {
|
|
1333
|
+
return this.getWriteFs().writeFile(path, data, options);
|
|
1334
|
+
}
|
|
1335
|
+
async writeFileBatch(entries) {
|
|
1336
|
+
return this.getWriteFs().writeFileBatch(entries);
|
|
1337
|
+
}
|
|
1338
|
+
async appendFile(path, data, options) {
|
|
1339
|
+
return this.getWriteFs().appendFile(path, data, options);
|
|
1340
|
+
}
|
|
1341
|
+
async mkdir(path) {
|
|
1342
|
+
return this.getWriteFs().mkdir(path);
|
|
1343
|
+
}
|
|
1344
|
+
async rmdir(path) {
|
|
1345
|
+
if (this.readBackend !== this.writeBackend && this.workerFs) {
|
|
1346
|
+
await this.workerFs.rmdir(path);
|
|
1347
|
+
this.mainFs.resetCache();
|
|
1348
|
+
} else {
|
|
1349
|
+
return this.getWriteFs().rmdir(path);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
async unlink(path) {
|
|
1353
|
+
return this.getWriteFs().unlink(path);
|
|
1354
|
+
}
|
|
1355
|
+
async truncate(path, len) {
|
|
1356
|
+
return this.getWriteFs().truncate(path, len);
|
|
1357
|
+
}
|
|
1358
|
+
async symlink(target, path) {
|
|
1359
|
+
if (this.readBackend !== this.writeBackend && this.workerFs) {
|
|
1360
|
+
await this.workerFs.symlink(target, path);
|
|
1361
|
+
this.mainFs.resetCache();
|
|
1362
|
+
} else {
|
|
1363
|
+
return this.getWriteFs().symlink(target, path);
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
async symlinkBatch(symlinks) {
|
|
1367
|
+
if (this.readBackend !== this.writeBackend && this.workerFs) {
|
|
1368
|
+
await this.workerFs.symlinkBatch(symlinks);
|
|
1369
|
+
this.mainFs.resetCache();
|
|
1370
|
+
} else {
|
|
1371
|
+
return this.getWriteFs().symlinkBatch(symlinks);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
async rename(oldPath, newPath) {
|
|
1375
|
+
return this.getWriteFs().rename(oldPath, newPath);
|
|
1376
|
+
}
|
|
1377
|
+
async copyFile(src, dest, mode) {
|
|
1378
|
+
return this.getWriteFs().copyFile(src, dest, mode);
|
|
1379
|
+
}
|
|
1380
|
+
async cp(src, dest, options) {
|
|
1381
|
+
return this.getWriteFs().cp(src, dest, options);
|
|
1382
|
+
}
|
|
1383
|
+
async rm(path, options) {
|
|
1384
|
+
return this.getWriteFs().rm(path, options);
|
|
1385
|
+
}
|
|
1386
|
+
async chmod(path, mode) {
|
|
1387
|
+
return this.getWriteFs().chmod(path, mode);
|
|
1388
|
+
}
|
|
1389
|
+
async chown(path, uid, gid) {
|
|
1390
|
+
return this.getWriteFs().chown(path, uid, gid);
|
|
1391
|
+
}
|
|
1392
|
+
async utimes(path, atime, mtime) {
|
|
1393
|
+
return this.getWriteFs().utimes(path, atime, mtime);
|
|
1394
|
+
}
|
|
1395
|
+
async lutimes(path, atime, mtime) {
|
|
1396
|
+
return this.getWriteFs().lutimes(path, atime, mtime);
|
|
1397
|
+
}
|
|
1398
|
+
async mkdtemp(prefix) {
|
|
1399
|
+
return this.getWriteFs().mkdtemp(prefix);
|
|
1400
|
+
}
|
|
1401
|
+
/**
|
|
1402
|
+
* Reset internal caches on both backends
|
|
1403
|
+
*/
|
|
1404
|
+
async resetCache() {
|
|
1405
|
+
this.mainFs.resetCache();
|
|
1406
|
+
if (this.workerFs) {
|
|
1407
|
+
await this.workerFs.resetCache();
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
/**
|
|
1411
|
+
* Force full garbage collection on both backends
|
|
1412
|
+
* More aggressive than resetCache() - reinitializes the worker's OPFS instance
|
|
1413
|
+
*/
|
|
1414
|
+
async gc() {
|
|
1415
|
+
this.mainFs.resetCache();
|
|
1416
|
+
if (this.workerFs) {
|
|
1417
|
+
await this.workerFs.gc();
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
};
|
|
1421
|
+
|
|
1422
|
+
// src/index.ts
|
|
1423
|
+
var OPFS = class {
|
|
1424
|
+
useSync;
|
|
1425
|
+
verbose;
|
|
1426
|
+
handleManager;
|
|
1427
|
+
symlinkManager;
|
|
1428
|
+
packedStorage;
|
|
1429
|
+
watchCallbacks = /* @__PURE__ */ new Map();
|
|
1430
|
+
tmpCounter = 0;
|
|
1431
|
+
/** Hybrid instance when workerUrl is provided */
|
|
1432
|
+
hybrid = null;
|
|
1433
|
+
/** File system constants */
|
|
1434
|
+
constants = constants;
|
|
1435
|
+
constructor(options = {}) {
|
|
1436
|
+
const { useSync = true, verbose = false, workerUrl, read, write } = options;
|
|
1437
|
+
this.verbose = verbose;
|
|
1438
|
+
if (workerUrl) {
|
|
1439
|
+
this.hybrid = new OPFSHybrid({
|
|
1440
|
+
workerUrl,
|
|
1441
|
+
read: read ?? "main",
|
|
1442
|
+
write: write ?? "worker",
|
|
1443
|
+
verbose
|
|
1444
|
+
});
|
|
1445
|
+
this.useSync = false;
|
|
1446
|
+
this.handleManager = new HandleManager();
|
|
1447
|
+
this.symlinkManager = new SymlinkManager(this.handleManager, false);
|
|
1448
|
+
this.packedStorage = new PackedStorage(this.handleManager, false);
|
|
1449
|
+
} else {
|
|
1450
|
+
this.useSync = useSync && typeof FileSystemFileHandle !== "undefined" && "createSyncAccessHandle" in FileSystemFileHandle.prototype;
|
|
1451
|
+
this.handleManager = new HandleManager();
|
|
1452
|
+
this.symlinkManager = new SymlinkManager(this.handleManager, this.useSync);
|
|
1453
|
+
this.packedStorage = new PackedStorage(this.handleManager, this.useSync);
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
/**
|
|
1457
|
+
* Wait for the filesystem to be ready (only needed for hybrid mode)
|
|
1458
|
+
*/
|
|
1459
|
+
async ready() {
|
|
1460
|
+
if (this.hybrid) {
|
|
1461
|
+
await this.hybrid.ready();
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
/**
|
|
1465
|
+
* Terminate any background workers (only needed for hybrid mode)
|
|
1466
|
+
*/
|
|
1467
|
+
terminate() {
|
|
1468
|
+
if (this.hybrid) {
|
|
1469
|
+
this.hybrid.terminate();
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
log(method, ...args) {
|
|
1473
|
+
if (this.verbose) {
|
|
1474
|
+
console.log(`[OPFS] ${method}:`, ...args);
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
logError(method, err) {
|
|
1478
|
+
if (this.verbose) {
|
|
1479
|
+
console.error(`[OPFS] ${method} error:`, err);
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
/**
|
|
1483
|
+
* Execute tasks with limited concurrency to avoid overwhelming the system
|
|
1484
|
+
* @param items - Array of items to process
|
|
1485
|
+
* @param maxConcurrent - Maximum number of concurrent operations (default: 10)
|
|
1486
|
+
* @param taskFn - Function to execute for each item
|
|
1487
|
+
*/
|
|
1488
|
+
async limitConcurrency(items, maxConcurrent, taskFn) {
|
|
1489
|
+
if (items.length === 0) return;
|
|
1490
|
+
if (items.length <= 2) {
|
|
1491
|
+
for (const item of items) {
|
|
1492
|
+
await taskFn(item);
|
|
1493
|
+
}
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
1496
|
+
if (items.length <= maxConcurrent) {
|
|
1497
|
+
await Promise.all(items.map(taskFn));
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
const queue = [...items];
|
|
1501
|
+
const workers = Array.from({ length: maxConcurrent }).map(async () => {
|
|
1502
|
+
while (queue.length) {
|
|
1503
|
+
const item = queue.shift();
|
|
1504
|
+
if (item !== void 0) await taskFn(item);
|
|
1505
|
+
}
|
|
1506
|
+
});
|
|
1507
|
+
await Promise.all(workers);
|
|
1508
|
+
}
|
|
1509
|
+
/**
|
|
1510
|
+
* Read file contents
|
|
1511
|
+
*/
|
|
1512
|
+
async readFile(path, options = {}) {
|
|
1513
|
+
if (this.hybrid) {
|
|
1514
|
+
return this.hybrid.readFile(path, options);
|
|
1515
|
+
}
|
|
1516
|
+
this.log("readFile", path, options);
|
|
1517
|
+
try {
|
|
1518
|
+
const normalizedPath = normalize(path);
|
|
1519
|
+
const resolvedPath = await this.symlinkManager.resolve(normalizedPath);
|
|
1520
|
+
let fileHandle = null;
|
|
1521
|
+
try {
|
|
1522
|
+
fileHandle = await this.handleManager.getPooledFileHandle(resolvedPath);
|
|
1523
|
+
} catch {
|
|
1524
|
+
}
|
|
1525
|
+
if (fileHandle) {
|
|
1526
|
+
let buffer;
|
|
1527
|
+
if (this.useSync) {
|
|
1528
|
+
const access = await fileHandle.createSyncAccessHandle();
|
|
1529
|
+
const size = access.getSize();
|
|
1530
|
+
buffer = new Uint8Array(size);
|
|
1531
|
+
access.read(buffer);
|
|
1532
|
+
access.close();
|
|
1533
|
+
} else {
|
|
1534
|
+
const file = await fileHandle.getFile();
|
|
1535
|
+
buffer = new Uint8Array(await file.arrayBuffer());
|
|
1536
|
+
}
|
|
1537
|
+
return options.encoding ? new TextDecoder(options.encoding).decode(buffer) : buffer;
|
|
1538
|
+
}
|
|
1539
|
+
const packedData = await this.packedStorage.read(resolvedPath);
|
|
1540
|
+
if (packedData) {
|
|
1541
|
+
return options.encoding ? new TextDecoder(options.encoding).decode(packedData) : packedData;
|
|
1542
|
+
}
|
|
1543
|
+
throw createENOENT(path);
|
|
1544
|
+
} catch (err) {
|
|
1545
|
+
this.logError("readFile", err);
|
|
1546
|
+
throw wrapError(err);
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
/**
|
|
1550
|
+
* Read multiple files efficiently in a batch operation
|
|
1551
|
+
* Uses packed storage batch read (single index load), falls back to individual files
|
|
1552
|
+
* Returns results in the same order as input paths
|
|
1553
|
+
*/
|
|
1554
|
+
async readFileBatch(paths) {
|
|
1555
|
+
if (this.hybrid) {
|
|
1556
|
+
return this.hybrid.readFileBatch(paths);
|
|
1557
|
+
}
|
|
1558
|
+
this.log("readFileBatch", `${paths.length} files`);
|
|
1559
|
+
if (paths.length === 0) return [];
|
|
1560
|
+
try {
|
|
1561
|
+
const resolvedPaths = await Promise.all(
|
|
1562
|
+
paths.map(async (path) => {
|
|
1563
|
+
const normalizedPath = normalize(path);
|
|
1564
|
+
return this.symlinkManager.resolve(normalizedPath);
|
|
1565
|
+
})
|
|
1566
|
+
);
|
|
1567
|
+
const packedResults = await this.packedStorage.readBatch(resolvedPaths);
|
|
1568
|
+
const results = new Array(paths.length);
|
|
1569
|
+
const needsIndividualRead = [];
|
|
1570
|
+
for (let i = 0; i < paths.length; i++) {
|
|
1571
|
+
const packedData = packedResults.get(resolvedPaths[i]);
|
|
1572
|
+
if (packedData) {
|
|
1573
|
+
results[i] = { path: paths[i], data: packedData };
|
|
1574
|
+
} else {
|
|
1575
|
+
needsIndividualRead.push({ index: i, resolvedPath: resolvedPaths[i] });
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
if (needsIndividualRead.length > 0) {
|
|
1579
|
+
await Promise.all(
|
|
1580
|
+
needsIndividualRead.map(async ({ index, resolvedPath }) => {
|
|
1581
|
+
try {
|
|
1582
|
+
const fileHandle = await this.handleManager.getPooledFileHandle(resolvedPath);
|
|
1583
|
+
if (!fileHandle) {
|
|
1584
|
+
results[index] = { path: paths[index], data: null, error: createENOENT(paths[index]) };
|
|
1585
|
+
return;
|
|
1586
|
+
}
|
|
1587
|
+
let buffer;
|
|
1588
|
+
if (this.useSync) {
|
|
1589
|
+
const access = await fileHandle.createSyncAccessHandle();
|
|
1590
|
+
const size = access.getSize();
|
|
1591
|
+
buffer = new Uint8Array(size);
|
|
1592
|
+
access.read(buffer);
|
|
1593
|
+
access.close();
|
|
1594
|
+
} else {
|
|
1595
|
+
const file = await fileHandle.getFile();
|
|
1596
|
+
buffer = new Uint8Array(await file.arrayBuffer());
|
|
1597
|
+
}
|
|
1598
|
+
results[index] = { path: paths[index], data: buffer };
|
|
1599
|
+
} catch (err) {
|
|
1600
|
+
results[index] = { path: paths[index], data: null, error: err };
|
|
1601
|
+
}
|
|
1602
|
+
})
|
|
1603
|
+
);
|
|
1604
|
+
}
|
|
1605
|
+
return results;
|
|
1606
|
+
} catch (err) {
|
|
1607
|
+
this.logError("readFileBatch", err);
|
|
1608
|
+
throw wrapError(err);
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
/**
|
|
1612
|
+
* Write data to a file
|
|
1613
|
+
*/
|
|
1614
|
+
async writeFile(path, data, options = {}) {
|
|
1615
|
+
if (this.hybrid) {
|
|
1616
|
+
return this.hybrid.writeFile(path, data, options);
|
|
1617
|
+
}
|
|
1618
|
+
this.log("writeFile", path);
|
|
1619
|
+
try {
|
|
1620
|
+
const normalizedPath = normalize(path);
|
|
1621
|
+
const resolvedPath = await this.symlinkManager.resolve(normalizedPath);
|
|
1622
|
+
const { fileHandle } = await this.handleManager.getHandle(resolvedPath, { create: true });
|
|
1623
|
+
const buffer = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
1624
|
+
if (this.useSync) {
|
|
1625
|
+
const access = await fileHandle.createSyncAccessHandle();
|
|
1626
|
+
access.truncate(buffer.length);
|
|
1627
|
+
access.write(buffer, { at: 0 });
|
|
1628
|
+
access.close();
|
|
1629
|
+
} else {
|
|
1630
|
+
const writable = await fileHandle.createWritable();
|
|
1631
|
+
await writable.write(buffer);
|
|
1632
|
+
await writable.close();
|
|
1633
|
+
}
|
|
1634
|
+
} catch (err) {
|
|
1635
|
+
this.logError("writeFile", err);
|
|
1636
|
+
throw wrapError(err);
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
/**
|
|
1640
|
+
* Write multiple files efficiently in a batch operation
|
|
1641
|
+
* Uses packed storage (single file) for maximum performance
|
|
1642
|
+
*/
|
|
1643
|
+
async writeFileBatch(entries) {
|
|
1644
|
+
if (this.hybrid) {
|
|
1645
|
+
return this.hybrid.writeFileBatch(entries);
|
|
1646
|
+
}
|
|
1647
|
+
this.log("writeFileBatch", `${entries.length} files`);
|
|
1648
|
+
if (entries.length === 0) return;
|
|
1649
|
+
try {
|
|
1650
|
+
const encoder = new TextEncoder();
|
|
1651
|
+
const packEntries = await Promise.all(
|
|
1652
|
+
entries.map(async ({ path, data }) => {
|
|
1653
|
+
const normalizedPath = normalize(path);
|
|
1654
|
+
const resolvedPath = await this.symlinkManager.resolve(normalizedPath);
|
|
1655
|
+
return {
|
|
1656
|
+
path: resolvedPath,
|
|
1657
|
+
data: typeof data === "string" ? encoder.encode(data) : data
|
|
1658
|
+
};
|
|
1659
|
+
})
|
|
1660
|
+
);
|
|
1661
|
+
await this.packedStorage.writeBatch(packEntries);
|
|
1662
|
+
} catch (err) {
|
|
1663
|
+
this.logError("writeFileBatch", err);
|
|
1664
|
+
throw wrapError(err);
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
/**
|
|
1668
|
+
* Create a directory
|
|
1669
|
+
*/
|
|
1670
|
+
async mkdir(path) {
|
|
1671
|
+
if (this.hybrid) {
|
|
1672
|
+
return this.hybrid.mkdir(path);
|
|
1673
|
+
}
|
|
1674
|
+
this.log("mkdir", path);
|
|
1675
|
+
try {
|
|
1676
|
+
await this.handleManager.mkdir(path);
|
|
1677
|
+
} catch (err) {
|
|
1678
|
+
this.logError("mkdir", err);
|
|
1679
|
+
throw wrapError(err);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
/**
|
|
1683
|
+
* Remove a directory
|
|
1684
|
+
*/
|
|
1685
|
+
async rmdir(path) {
|
|
1686
|
+
if (this.hybrid) {
|
|
1687
|
+
return this.hybrid.rmdir(path);
|
|
1688
|
+
}
|
|
1689
|
+
this.log("rmdir", path);
|
|
1690
|
+
try {
|
|
1691
|
+
const normalizedPath = normalize(path);
|
|
1692
|
+
this.handleManager.clearCache(normalizedPath);
|
|
1693
|
+
if (isRoot(normalizedPath)) {
|
|
1694
|
+
const root = await this.handleManager.getRoot();
|
|
1695
|
+
const entries = [];
|
|
1696
|
+
for await (const [name2] of root.entries()) {
|
|
1697
|
+
entries.push(name2);
|
|
1698
|
+
}
|
|
1699
|
+
await this.limitConcurrency(
|
|
1700
|
+
entries,
|
|
1701
|
+
10,
|
|
1702
|
+
(name2) => root.removeEntry(name2, { recursive: true })
|
|
1703
|
+
);
|
|
1704
|
+
this.symlinkManager.reset();
|
|
1705
|
+
this.packedStorage.reset();
|
|
1706
|
+
return;
|
|
1707
|
+
}
|
|
1708
|
+
const pathSegments = segments(normalizedPath);
|
|
1709
|
+
const name = pathSegments.pop();
|
|
1710
|
+
let dir = await this.handleManager.getRoot();
|
|
1711
|
+
for (const part of pathSegments) {
|
|
1712
|
+
dir = await dir.getDirectoryHandle(part);
|
|
1713
|
+
}
|
|
1714
|
+
try {
|
|
1715
|
+
await dir.removeEntry(name, { recursive: true });
|
|
1716
|
+
} catch {
|
|
1717
|
+
throw createENOENT(path);
|
|
1718
|
+
}
|
|
1719
|
+
} catch (err) {
|
|
1720
|
+
this.logError("rmdir", err);
|
|
1721
|
+
throw wrapError(err);
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
/**
|
|
1725
|
+
* Remove a file or symlink
|
|
1726
|
+
*/
|
|
1727
|
+
async unlink(path) {
|
|
1728
|
+
if (this.hybrid) {
|
|
1729
|
+
return this.hybrid.unlink(path);
|
|
1730
|
+
}
|
|
1731
|
+
this.log("unlink", path);
|
|
1732
|
+
try {
|
|
1733
|
+
const normalizedPath = normalize(path);
|
|
1734
|
+
this.handleManager.clearCache(normalizedPath);
|
|
1735
|
+
const isSymlink = await this.symlinkManager.isSymlink(normalizedPath);
|
|
1736
|
+
if (isSymlink) {
|
|
1737
|
+
await this.symlinkManager.unlink(normalizedPath);
|
|
1738
|
+
return;
|
|
1739
|
+
}
|
|
1740
|
+
const inPack = await this.packedStorage.has(normalizedPath);
|
|
1741
|
+
if (inPack) {
|
|
1742
|
+
await this.packedStorage.remove(normalizedPath);
|
|
1743
|
+
return;
|
|
1744
|
+
}
|
|
1745
|
+
const { dir, name, fileHandle } = await this.handleManager.getHandle(normalizedPath);
|
|
1746
|
+
if (!fileHandle) throw createENOENT(path);
|
|
1747
|
+
try {
|
|
1748
|
+
await dir.removeEntry(name);
|
|
1749
|
+
} catch {
|
|
1750
|
+
throw createENOENT(path);
|
|
1751
|
+
}
|
|
1752
|
+
} catch (err) {
|
|
1753
|
+
this.logError("unlink", err);
|
|
1754
|
+
throw wrapError(err);
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
/**
|
|
1758
|
+
* Read directory contents
|
|
1759
|
+
*/
|
|
1760
|
+
async readdir(path, options) {
|
|
1761
|
+
if (this.hybrid) {
|
|
1762
|
+
return this.hybrid.readdir(path, options);
|
|
1763
|
+
}
|
|
1764
|
+
this.log("readdir", path, options);
|
|
1765
|
+
try {
|
|
1766
|
+
const normalizedPath = normalize(path);
|
|
1767
|
+
const resolvedPath = await this.symlinkManager.resolve(normalizedPath);
|
|
1768
|
+
const dir = await this.handleManager.getDirectoryHandle(resolvedPath);
|
|
1769
|
+
const withFileTypes = options?.withFileTypes === true;
|
|
1770
|
+
const symlinksInDir = await this.symlinkManager.getSymlinksInDir(resolvedPath);
|
|
1771
|
+
const hasSymlinks = symlinksInDir.length > 0;
|
|
1772
|
+
const symlinkSet = hasSymlinks ? new Set(symlinksInDir) : null;
|
|
1773
|
+
const entryNames = /* @__PURE__ */ new Set();
|
|
1774
|
+
const entries = [];
|
|
1775
|
+
for await (const [name, handle] of dir.entries()) {
|
|
1776
|
+
if (this.symlinkManager.isMetadataFile(name)) continue;
|
|
1777
|
+
entryNames.add(name);
|
|
1778
|
+
if (withFileTypes) {
|
|
1779
|
+
const isSymlink = hasSymlinks && symlinkSet.has(name);
|
|
1780
|
+
entries.push({
|
|
1781
|
+
name,
|
|
1782
|
+
isFile: () => !isSymlink && handle.kind === "file",
|
|
1783
|
+
isDirectory: () => !isSymlink && handle.kind === "directory",
|
|
1784
|
+
isSymbolicLink: () => isSymlink
|
|
1785
|
+
});
|
|
1786
|
+
} else {
|
|
1787
|
+
entries.push(name);
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
if (hasSymlinks) {
|
|
1791
|
+
for (const name of symlinksInDir) {
|
|
1792
|
+
if (!entryNames.has(name)) {
|
|
1793
|
+
if (withFileTypes) {
|
|
1794
|
+
entries.push({
|
|
1795
|
+
name,
|
|
1796
|
+
isFile: () => false,
|
|
1797
|
+
isDirectory: () => false,
|
|
1798
|
+
isSymbolicLink: () => true
|
|
1799
|
+
});
|
|
1800
|
+
} else {
|
|
1801
|
+
entries.push(name);
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
return entries;
|
|
1807
|
+
} catch (err) {
|
|
1808
|
+
this.logError("readdir", err);
|
|
1809
|
+
throw wrapError(err);
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
/**
|
|
1813
|
+
* Get file/directory statistics (follows symlinks)
|
|
1814
|
+
*/
|
|
1815
|
+
async stat(path) {
|
|
1816
|
+
if (this.hybrid) {
|
|
1817
|
+
return this.hybrid.stat(path);
|
|
1818
|
+
}
|
|
1819
|
+
this.log("stat", path);
|
|
1820
|
+
try {
|
|
1821
|
+
const normalizedPath = normalize(path);
|
|
1822
|
+
const resolvedPath = await this.symlinkManager.resolve(normalizedPath);
|
|
1823
|
+
const defaultDate = /* @__PURE__ */ new Date(0);
|
|
1824
|
+
if (isRoot(resolvedPath)) {
|
|
1825
|
+
return {
|
|
1826
|
+
type: "dir",
|
|
1827
|
+
size: 0,
|
|
1828
|
+
mode: 16877,
|
|
1829
|
+
ctime: defaultDate,
|
|
1830
|
+
ctimeMs: 0,
|
|
1831
|
+
mtime: defaultDate,
|
|
1832
|
+
mtimeMs: 0,
|
|
1833
|
+
isFile: () => false,
|
|
1834
|
+
isDirectory: () => true,
|
|
1835
|
+
isSymbolicLink: () => false
|
|
1836
|
+
};
|
|
1837
|
+
}
|
|
1838
|
+
const pathSegments = segments(resolvedPath);
|
|
1839
|
+
const name = pathSegments.pop();
|
|
1840
|
+
let dir = await this.handleManager.getRoot();
|
|
1841
|
+
for (const part of pathSegments) {
|
|
1842
|
+
try {
|
|
1843
|
+
dir = await dir.getDirectoryHandle(part);
|
|
1844
|
+
} catch {
|
|
1845
|
+
throw createENOENT(path);
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
const [fileResult, dirResult] = await Promise.allSettled([
|
|
1849
|
+
dir.getFileHandle(name),
|
|
1850
|
+
dir.getDirectoryHandle(name)
|
|
1851
|
+
]);
|
|
1852
|
+
if (fileResult.status === "fulfilled") {
|
|
1853
|
+
const fileHandle = fileResult.value;
|
|
1854
|
+
const file = await fileHandle.getFile();
|
|
1855
|
+
const mtime = file.lastModified ? new Date(file.lastModified) : defaultDate;
|
|
1856
|
+
return {
|
|
1857
|
+
type: "file",
|
|
1858
|
+
size: file.size,
|
|
1859
|
+
mode: 33188,
|
|
1860
|
+
ctime: mtime,
|
|
1861
|
+
ctimeMs: mtime.getTime(),
|
|
1862
|
+
mtime,
|
|
1863
|
+
mtimeMs: mtime.getTime(),
|
|
1864
|
+
isFile: () => true,
|
|
1865
|
+
isDirectory: () => false,
|
|
1866
|
+
isSymbolicLink: () => false
|
|
1867
|
+
};
|
|
1868
|
+
}
|
|
1869
|
+
if (dirResult.status === "fulfilled") {
|
|
1870
|
+
return {
|
|
1871
|
+
type: "dir",
|
|
1872
|
+
size: 0,
|
|
1873
|
+
mode: 16877,
|
|
1874
|
+
ctime: defaultDate,
|
|
1875
|
+
ctimeMs: 0,
|
|
1876
|
+
mtime: defaultDate,
|
|
1877
|
+
mtimeMs: 0,
|
|
1878
|
+
isFile: () => false,
|
|
1879
|
+
isDirectory: () => true,
|
|
1880
|
+
isSymbolicLink: () => false
|
|
1881
|
+
};
|
|
1882
|
+
}
|
|
1883
|
+
const packedSize = await this.packedStorage.getSize(resolvedPath);
|
|
1884
|
+
if (packedSize !== null) {
|
|
1885
|
+
return {
|
|
1886
|
+
type: "file",
|
|
1887
|
+
size: packedSize,
|
|
1888
|
+
mode: 33188,
|
|
1889
|
+
ctime: defaultDate,
|
|
1890
|
+
ctimeMs: 0,
|
|
1891
|
+
mtime: defaultDate,
|
|
1892
|
+
mtimeMs: 0,
|
|
1893
|
+
isFile: () => true,
|
|
1894
|
+
isDirectory: () => false,
|
|
1895
|
+
isSymbolicLink: () => false
|
|
1896
|
+
};
|
|
1897
|
+
}
|
|
1898
|
+
throw createENOENT(path);
|
|
1899
|
+
} catch (err) {
|
|
1900
|
+
this.logError("stat", err);
|
|
1901
|
+
throw wrapError(err);
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
/**
|
|
1905
|
+
* Get file/directory statistics (does not follow symlinks)
|
|
1906
|
+
*/
|
|
1907
|
+
async lstat(path) {
|
|
1908
|
+
if (this.hybrid) {
|
|
1909
|
+
return this.hybrid.lstat(path);
|
|
1910
|
+
}
|
|
1911
|
+
this.log("lstat", path);
|
|
1912
|
+
try {
|
|
1913
|
+
const normalizedPath = normalize(path);
|
|
1914
|
+
const isSymlink = await this.symlinkManager.isSymlink(normalizedPath);
|
|
1915
|
+
if (isSymlink) {
|
|
1916
|
+
const target = await this.symlinkManager.readlink(normalizedPath);
|
|
1917
|
+
return {
|
|
1918
|
+
type: "symlink",
|
|
1919
|
+
target,
|
|
1920
|
+
size: target.length,
|
|
1921
|
+
mode: 41471,
|
|
1922
|
+
ctime: /* @__PURE__ */ new Date(0),
|
|
1923
|
+
ctimeMs: 0,
|
|
1924
|
+
mtime: /* @__PURE__ */ new Date(0),
|
|
1925
|
+
mtimeMs: 0,
|
|
1926
|
+
isFile: () => false,
|
|
1927
|
+
isDirectory: () => false,
|
|
1928
|
+
isSymbolicLink: () => true
|
|
1929
|
+
};
|
|
1930
|
+
}
|
|
1931
|
+
return this.stat(path);
|
|
1932
|
+
} catch (err) {
|
|
1933
|
+
this.logError("lstat", err);
|
|
1934
|
+
throw wrapError(err);
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
/**
|
|
1938
|
+
* Rename a file or directory
|
|
1939
|
+
*/
|
|
1940
|
+
async rename(oldPath, newPath) {
|
|
1941
|
+
if (this.hybrid) {
|
|
1942
|
+
return this.hybrid.rename(oldPath, newPath);
|
|
1943
|
+
}
|
|
1944
|
+
this.log("rename", oldPath, newPath);
|
|
1945
|
+
try {
|
|
1946
|
+
const normalizedOld = normalize(oldPath);
|
|
1947
|
+
const normalizedNew = normalize(newPath);
|
|
1948
|
+
this.handleManager.clearCache(normalizedOld);
|
|
1949
|
+
this.handleManager.clearCache(normalizedNew);
|
|
1950
|
+
const renamed = await this.symlinkManager.rename(normalizedOld, normalizedNew);
|
|
1951
|
+
if (renamed) return;
|
|
1952
|
+
const stat = await this.stat(normalizedOld);
|
|
1953
|
+
if (stat.isFile()) {
|
|
1954
|
+
const [data] = await Promise.all([
|
|
1955
|
+
this.readFile(normalizedOld),
|
|
1956
|
+
this.handleManager.ensureParentDir(normalizedNew)
|
|
1957
|
+
]);
|
|
1958
|
+
await this.writeFile(normalizedNew, data);
|
|
1959
|
+
await this.unlink(normalizedOld);
|
|
1960
|
+
} else if (stat.isDirectory()) {
|
|
1961
|
+
await this.mkdir(normalizedNew);
|
|
1962
|
+
const entries = await this.readdir(normalizedOld);
|
|
1963
|
+
await this.limitConcurrency(
|
|
1964
|
+
entries,
|
|
1965
|
+
10,
|
|
1966
|
+
(entry) => this.rename(`${normalizedOld}/${entry}`, `${normalizedNew}/${entry}`)
|
|
1967
|
+
);
|
|
1968
|
+
await this.rmdir(normalizedOld);
|
|
1969
|
+
}
|
|
1970
|
+
} catch (err) {
|
|
1971
|
+
this.logError("rename", err);
|
|
1972
|
+
throw wrapError(err);
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
/**
|
|
1976
|
+
* Create a symbolic link
|
|
1977
|
+
*/
|
|
1978
|
+
async symlink(target, path) {
|
|
1979
|
+
if (this.hybrid) {
|
|
1980
|
+
return this.hybrid.symlink(target, path);
|
|
1981
|
+
}
|
|
1982
|
+
this.log("symlink", target, path);
|
|
1983
|
+
try {
|
|
1984
|
+
const normalizedPath = normalize(path);
|
|
1985
|
+
this.handleManager.clearCache(normalizedPath);
|
|
1986
|
+
await this.symlinkManager.symlink(target, path, async () => {
|
|
1987
|
+
const { fileHandle, dirHandle } = await this.handleManager.getHandle(normalizedPath);
|
|
1988
|
+
if (fileHandle || dirHandle) {
|
|
1989
|
+
throw createEEXIST(path);
|
|
1990
|
+
}
|
|
1991
|
+
});
|
|
1992
|
+
} catch (err) {
|
|
1993
|
+
this.logError("symlink", err);
|
|
1994
|
+
throw wrapError(err);
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
/**
|
|
1998
|
+
* Read symlink target
|
|
1999
|
+
*/
|
|
2000
|
+
async readlink(path) {
|
|
2001
|
+
if (this.hybrid) {
|
|
2002
|
+
return this.hybrid.readlink(path);
|
|
2003
|
+
}
|
|
2004
|
+
this.log("readlink", path);
|
|
2005
|
+
try {
|
|
2006
|
+
return await this.symlinkManager.readlink(path);
|
|
2007
|
+
} catch (err) {
|
|
2008
|
+
this.logError("readlink", err);
|
|
2009
|
+
throw wrapError(err);
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
/**
|
|
2013
|
+
* Create multiple symlinks efficiently
|
|
2014
|
+
*/
|
|
2015
|
+
async symlinkBatch(links) {
|
|
2016
|
+
if (this.hybrid) {
|
|
2017
|
+
return this.hybrid.symlinkBatch(links);
|
|
2018
|
+
}
|
|
2019
|
+
this.log("symlinkBatch", links.length, "links");
|
|
2020
|
+
try {
|
|
2021
|
+
for (const { path } of links) {
|
|
2022
|
+
this.handleManager.clearCache(normalize(path));
|
|
2023
|
+
}
|
|
2024
|
+
await this.symlinkManager.symlinkBatch(links, async (normalizedPath) => {
|
|
2025
|
+
try {
|
|
2026
|
+
const { fileHandle, dirHandle } = await this.handleManager.getHandle(normalizedPath);
|
|
2027
|
+
if (fileHandle || dirHandle) {
|
|
2028
|
+
throw createEEXIST(normalizedPath);
|
|
2029
|
+
}
|
|
2030
|
+
} catch (err) {
|
|
2031
|
+
if (err.code === "ENOENT") return;
|
|
2032
|
+
throw err;
|
|
2033
|
+
}
|
|
2034
|
+
});
|
|
2035
|
+
} catch (err) {
|
|
2036
|
+
this.logError("symlinkBatch", err);
|
|
2037
|
+
throw wrapError(err);
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
/**
|
|
2041
|
+
* Check file accessibility
|
|
2042
|
+
*/
|
|
2043
|
+
async access(path, mode = constants.F_OK) {
|
|
2044
|
+
if (this.hybrid) {
|
|
2045
|
+
return this.hybrid.access(path, mode);
|
|
2046
|
+
}
|
|
2047
|
+
this.log("access", path, mode);
|
|
2048
|
+
try {
|
|
2049
|
+
const normalizedPath = normalize(path);
|
|
2050
|
+
await this.stat(normalizedPath);
|
|
2051
|
+
} catch (err) {
|
|
2052
|
+
this.logError("access", err);
|
|
2053
|
+
throw createEACCES(path);
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
/**
|
|
2057
|
+
* Append data to a file
|
|
2058
|
+
*/
|
|
2059
|
+
async appendFile(path, data, options = {}) {
|
|
2060
|
+
if (this.hybrid) {
|
|
2061
|
+
return this.hybrid.appendFile(path, data, options);
|
|
2062
|
+
}
|
|
2063
|
+
this.log("appendFile", path);
|
|
2064
|
+
try {
|
|
2065
|
+
const normalizedPath = normalize(path);
|
|
2066
|
+
const resolvedPath = await this.symlinkManager.resolve(normalizedPath);
|
|
2067
|
+
let existingData = new Uint8Array(0);
|
|
2068
|
+
try {
|
|
2069
|
+
const result = await this.readFile(resolvedPath);
|
|
2070
|
+
existingData = result instanceof Uint8Array ? result : new TextEncoder().encode(result);
|
|
2071
|
+
} catch (err) {
|
|
2072
|
+
if (err.code !== "ENOENT") throw err;
|
|
2073
|
+
}
|
|
2074
|
+
const newData = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
2075
|
+
const combined = new Uint8Array(existingData.length + newData.length);
|
|
2076
|
+
combined.set(existingData, 0);
|
|
2077
|
+
combined.set(newData, existingData.length);
|
|
2078
|
+
await this.writeFile(resolvedPath, combined, options);
|
|
2079
|
+
} catch (err) {
|
|
2080
|
+
this.logError("appendFile", err);
|
|
2081
|
+
throw wrapError(err);
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
/**
|
|
2085
|
+
* Copy a file
|
|
2086
|
+
*/
|
|
2087
|
+
async copyFile(src, dest, mode = 0) {
|
|
2088
|
+
if (this.hybrid) {
|
|
2089
|
+
return this.hybrid.copyFile(src, dest, mode);
|
|
2090
|
+
}
|
|
2091
|
+
this.log("copyFile", src, dest, mode);
|
|
2092
|
+
try {
|
|
2093
|
+
const normalizedSrc = normalize(src);
|
|
2094
|
+
const normalizedDest = normalize(dest);
|
|
2095
|
+
const resolvedSrc = await this.symlinkManager.resolve(normalizedSrc);
|
|
2096
|
+
if (mode & constants.COPYFILE_EXCL) {
|
|
2097
|
+
try {
|
|
2098
|
+
await this.stat(normalizedDest);
|
|
2099
|
+
throw createEEXIST(dest);
|
|
2100
|
+
} catch (err) {
|
|
2101
|
+
if (err.code !== "ENOENT") throw err;
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
const [data] = await Promise.all([
|
|
2105
|
+
this.readFile(resolvedSrc),
|
|
2106
|
+
this.handleManager.ensureParentDir(normalizedDest)
|
|
2107
|
+
]);
|
|
2108
|
+
await this.writeFile(normalizedDest, data);
|
|
2109
|
+
} catch (err) {
|
|
2110
|
+
this.logError("copyFile", err);
|
|
2111
|
+
throw wrapError(err);
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
/**
|
|
2115
|
+
* Copy files/directories recursively
|
|
2116
|
+
*/
|
|
2117
|
+
async cp(src, dest, options = {}) {
|
|
2118
|
+
if (this.hybrid) {
|
|
2119
|
+
return this.hybrid.cp(src, dest, options);
|
|
2120
|
+
}
|
|
2121
|
+
this.log("cp", src, dest, options);
|
|
2122
|
+
try {
|
|
2123
|
+
const normalizedSrc = normalize(src);
|
|
2124
|
+
const normalizedDest = normalize(dest);
|
|
2125
|
+
const { recursive = false, force = false, errorOnExist = false } = options;
|
|
2126
|
+
const srcStat = await this.stat(normalizedSrc);
|
|
2127
|
+
if (srcStat.isDirectory()) {
|
|
2128
|
+
if (!recursive) {
|
|
2129
|
+
throw createEISDIR(src);
|
|
2130
|
+
}
|
|
2131
|
+
let destExists = false;
|
|
2132
|
+
try {
|
|
2133
|
+
await this.stat(normalizedDest);
|
|
2134
|
+
destExists = true;
|
|
2135
|
+
if (errorOnExist && !force) {
|
|
2136
|
+
throw createEEXIST(dest);
|
|
2137
|
+
}
|
|
2138
|
+
} catch (err) {
|
|
2139
|
+
if (err.code !== "ENOENT") throw err;
|
|
2140
|
+
}
|
|
2141
|
+
if (!destExists) {
|
|
2142
|
+
await this.mkdir(normalizedDest);
|
|
2143
|
+
}
|
|
2144
|
+
const entries = await this.readdir(normalizedSrc);
|
|
2145
|
+
await this.limitConcurrency(
|
|
2146
|
+
entries,
|
|
2147
|
+
10,
|
|
2148
|
+
(entry) => this.cp(`${normalizedSrc}/${entry}`, `${normalizedDest}/${entry}`, options)
|
|
2149
|
+
);
|
|
2150
|
+
} else {
|
|
2151
|
+
if (errorOnExist) {
|
|
2152
|
+
try {
|
|
2153
|
+
await this.stat(normalizedDest);
|
|
2154
|
+
throw createEEXIST(dest);
|
|
2155
|
+
} catch (err) {
|
|
2156
|
+
if (err.code !== "ENOENT") throw err;
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
await this.copyFile(normalizedSrc, normalizedDest);
|
|
2160
|
+
}
|
|
2161
|
+
} catch (err) {
|
|
2162
|
+
this.logError("cp", err);
|
|
2163
|
+
throw wrapError(err);
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
/**
|
|
2167
|
+
* Check if path exists
|
|
2168
|
+
*/
|
|
2169
|
+
async exists(path) {
|
|
2170
|
+
if (this.hybrid) {
|
|
2171
|
+
return this.hybrid.exists(path);
|
|
2172
|
+
}
|
|
2173
|
+
this.log("exists", path);
|
|
2174
|
+
try {
|
|
2175
|
+
await this.stat(normalize(path));
|
|
2176
|
+
return true;
|
|
2177
|
+
} catch {
|
|
2178
|
+
return false;
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
/**
|
|
2182
|
+
* Resolve symlinks to get real path
|
|
2183
|
+
*/
|
|
2184
|
+
async realpath(path) {
|
|
2185
|
+
if (this.hybrid) {
|
|
2186
|
+
return this.hybrid.realpath(path);
|
|
2187
|
+
}
|
|
2188
|
+
this.log("realpath", path);
|
|
2189
|
+
const normalizedPath = normalize(path);
|
|
2190
|
+
return this.symlinkManager.resolve(normalizedPath);
|
|
2191
|
+
}
|
|
2192
|
+
/**
|
|
2193
|
+
* Remove files and directories
|
|
2194
|
+
*/
|
|
2195
|
+
async rm(path, options = {}) {
|
|
2196
|
+
if (this.hybrid) {
|
|
2197
|
+
return this.hybrid.rm(path, options);
|
|
2198
|
+
}
|
|
2199
|
+
this.log("rm", path, options);
|
|
2200
|
+
try {
|
|
2201
|
+
const normalizedPath = normalize(path);
|
|
2202
|
+
const { recursive = false, force = false } = options;
|
|
2203
|
+
try {
|
|
2204
|
+
const stat = await this.lstat(normalizedPath);
|
|
2205
|
+
if (stat.isSymbolicLink()) {
|
|
2206
|
+
await this.unlink(normalizedPath);
|
|
2207
|
+
} else if (stat.isDirectory()) {
|
|
2208
|
+
if (!recursive) {
|
|
2209
|
+
throw createEISDIR(path);
|
|
2210
|
+
}
|
|
2211
|
+
await this.rmdir(normalizedPath);
|
|
2212
|
+
} else {
|
|
2213
|
+
await this.unlink(normalizedPath);
|
|
2214
|
+
}
|
|
2215
|
+
} catch (err) {
|
|
2216
|
+
if (err.code === "ENOENT" && force) {
|
|
2217
|
+
return;
|
|
2218
|
+
}
|
|
2219
|
+
throw err;
|
|
2220
|
+
}
|
|
2221
|
+
} catch (err) {
|
|
2222
|
+
this.logError("rm", err);
|
|
2223
|
+
throw wrapError(err);
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
/**
|
|
2227
|
+
* Truncate file to specified length
|
|
2228
|
+
*/
|
|
2229
|
+
async truncate(path, len = 0) {
|
|
2230
|
+
if (this.hybrid) {
|
|
2231
|
+
return this.hybrid.truncate(path, len);
|
|
2232
|
+
}
|
|
2233
|
+
this.log("truncate", path, len);
|
|
2234
|
+
try {
|
|
2235
|
+
const normalizedPath = normalize(path);
|
|
2236
|
+
const resolvedPath = await this.symlinkManager.resolve(normalizedPath);
|
|
2237
|
+
this.handleManager.clearCache(resolvedPath);
|
|
2238
|
+
const { fileHandle } = await this.handleManager.getHandle(resolvedPath);
|
|
2239
|
+
if (!fileHandle) throw createENOENT(path);
|
|
2240
|
+
if (this.useSync) {
|
|
2241
|
+
const access = await fileHandle.createSyncAccessHandle();
|
|
2242
|
+
access.truncate(len);
|
|
2243
|
+
access.close();
|
|
2244
|
+
} else {
|
|
2245
|
+
const file = await fileHandle.getFile();
|
|
2246
|
+
const data = new Uint8Array(await file.arrayBuffer());
|
|
2247
|
+
const finalData = new Uint8Array(len);
|
|
2248
|
+
const copyLen = Math.min(len, data.length);
|
|
2249
|
+
if (copyLen > 0) {
|
|
2250
|
+
finalData.set(data.subarray(0, copyLen), 0);
|
|
2251
|
+
}
|
|
2252
|
+
const writable = await fileHandle.createWritable();
|
|
2253
|
+
await writable.write(finalData);
|
|
2254
|
+
await writable.close();
|
|
2255
|
+
}
|
|
2256
|
+
} catch (err) {
|
|
2257
|
+
this.logError("truncate", err);
|
|
2258
|
+
throw wrapError(err);
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
/**
|
|
2262
|
+
* Create a unique temporary directory
|
|
2263
|
+
*/
|
|
2264
|
+
async mkdtemp(prefix) {
|
|
2265
|
+
if (this.hybrid) {
|
|
2266
|
+
return this.hybrid.mkdtemp(prefix);
|
|
2267
|
+
}
|
|
2268
|
+
this.log("mkdtemp", prefix);
|
|
2269
|
+
try {
|
|
2270
|
+
const normalizedPrefix = normalize(prefix);
|
|
2271
|
+
const suffix = `${Date.now()}-${++this.tmpCounter}-${Math.random().toString(36).slice(2, 8)}`;
|
|
2272
|
+
const path = `${normalizedPrefix}${suffix}`;
|
|
2273
|
+
await this.mkdir(path);
|
|
2274
|
+
return path;
|
|
2275
|
+
} catch (err) {
|
|
2276
|
+
this.logError("mkdtemp", err);
|
|
2277
|
+
throw wrapError(err);
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
/**
|
|
2281
|
+
* Change file mode (no-op for OPFS compatibility)
|
|
2282
|
+
*/
|
|
2283
|
+
async chmod(path, mode) {
|
|
2284
|
+
if (this.hybrid) {
|
|
2285
|
+
return this.hybrid.chmod(path, mode);
|
|
2286
|
+
}
|
|
2287
|
+
this.log("chmod", path, mode);
|
|
2288
|
+
await this.stat(normalize(path));
|
|
2289
|
+
}
|
|
2290
|
+
/**
|
|
2291
|
+
* Change file owner (no-op for OPFS compatibility)
|
|
2292
|
+
*/
|
|
2293
|
+
async chown(path, uid, gid) {
|
|
2294
|
+
if (this.hybrid) {
|
|
2295
|
+
return this.hybrid.chown(path, uid, gid);
|
|
2296
|
+
}
|
|
2297
|
+
this.log("chown", path, uid, gid);
|
|
2298
|
+
await this.stat(normalize(path));
|
|
2299
|
+
}
|
|
2300
|
+
/**
|
|
2301
|
+
* Update file timestamps (no-op for OPFS compatibility)
|
|
2302
|
+
*/
|
|
2303
|
+
async utimes(path, atime, mtime) {
|
|
2304
|
+
if (this.hybrid) {
|
|
2305
|
+
return this.hybrid.utimes(path, atime, mtime);
|
|
2306
|
+
}
|
|
2307
|
+
this.log("utimes", path, atime, mtime);
|
|
2308
|
+
await this.stat(normalize(path));
|
|
2309
|
+
}
|
|
2310
|
+
/**
|
|
2311
|
+
* Update symlink timestamps (no-op)
|
|
2312
|
+
*/
|
|
2313
|
+
async lutimes(path, atime, mtime) {
|
|
2314
|
+
if (this.hybrid) {
|
|
2315
|
+
return this.hybrid.lutimes(path, atime, mtime);
|
|
2316
|
+
}
|
|
2317
|
+
this.log("lutimes", path, atime, mtime);
|
|
2318
|
+
await this.lstat(normalize(path));
|
|
2319
|
+
}
|
|
2320
|
+
/**
|
|
2321
|
+
* Open file and return FileHandle
|
|
2322
|
+
*/
|
|
2323
|
+
async open(path, flags = "r", mode = 438) {
|
|
2324
|
+
this.log("open", path, flags, mode);
|
|
2325
|
+
try {
|
|
2326
|
+
const normalizedPath = normalize(path);
|
|
2327
|
+
const flagStr = flagsToString(flags);
|
|
2328
|
+
const shouldCreate = flagStr.includes("w") || flagStr.includes("a") || flagStr.includes("+");
|
|
2329
|
+
const shouldTruncate = flagStr.includes("w");
|
|
2330
|
+
const shouldAppend = flagStr.includes("a");
|
|
2331
|
+
if (shouldCreate) {
|
|
2332
|
+
await this.handleManager.ensureParentDir(normalizedPath);
|
|
2333
|
+
}
|
|
2334
|
+
const resolvedPath = await this.symlinkManager.resolve(normalizedPath);
|
|
2335
|
+
const { fileHandle } = await this.handleManager.getHandle(resolvedPath, { create: shouldCreate });
|
|
2336
|
+
if (!fileHandle && !shouldCreate) {
|
|
2337
|
+
throw createENOENT(path);
|
|
2338
|
+
}
|
|
2339
|
+
if (shouldTruncate && fileHandle) {
|
|
2340
|
+
await this.truncate(resolvedPath, 0);
|
|
2341
|
+
}
|
|
2342
|
+
const initialPosition = shouldAppend ? (await this.stat(resolvedPath)).size : 0;
|
|
2343
|
+
return createFileHandle(resolvedPath, initialPosition, {
|
|
2344
|
+
readFile: (p, o) => this.readFile(p, o),
|
|
2345
|
+
writeFile: (p, d) => this.writeFile(p, d),
|
|
2346
|
+
stat: (p) => this.stat(p),
|
|
2347
|
+
truncate: (p, l) => this.truncate(p, l),
|
|
2348
|
+
appendFile: (p, d, o) => this.appendFile(p, d, o)
|
|
2349
|
+
});
|
|
2350
|
+
} catch (err) {
|
|
2351
|
+
this.logError("open", err);
|
|
2352
|
+
throw wrapError(err);
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
/**
|
|
2356
|
+
* Open directory for iteration
|
|
2357
|
+
*/
|
|
2358
|
+
async opendir(path) {
|
|
2359
|
+
this.log("opendir", path);
|
|
2360
|
+
try {
|
|
2361
|
+
const normalizedPath = normalize(path);
|
|
2362
|
+
const entries = await this.readdir(normalizedPath, { withFileTypes: true });
|
|
2363
|
+
let index = 0;
|
|
2364
|
+
return {
|
|
2365
|
+
path: normalizedPath,
|
|
2366
|
+
async read() {
|
|
2367
|
+
if (index >= entries.length) return null;
|
|
2368
|
+
return entries[index++];
|
|
2369
|
+
},
|
|
2370
|
+
async close() {
|
|
2371
|
+
index = entries.length;
|
|
2372
|
+
},
|
|
2373
|
+
async *[Symbol.asyncIterator]() {
|
|
2374
|
+
for (const entry of entries) {
|
|
2375
|
+
yield entry;
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
};
|
|
2379
|
+
} catch (err) {
|
|
2380
|
+
this.logError("opendir", err);
|
|
2381
|
+
throw wrapError(err);
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
/**
|
|
2385
|
+
* Watch for file changes
|
|
2386
|
+
*/
|
|
2387
|
+
watch(path, options = {}) {
|
|
2388
|
+
this.log("watch", path, options);
|
|
2389
|
+
const normalizedPath = normalize(path);
|
|
2390
|
+
const { recursive = false, signal } = options;
|
|
2391
|
+
const callbacks = /* @__PURE__ */ new Set();
|
|
2392
|
+
const id = /* @__PURE__ */ Symbol("watcher");
|
|
2393
|
+
this.watchCallbacks.set(id, { path: normalizedPath, callbacks, recursive });
|
|
2394
|
+
if (signal) {
|
|
2395
|
+
signal.addEventListener("abort", () => {
|
|
2396
|
+
this.watchCallbacks.delete(id);
|
|
2397
|
+
});
|
|
2398
|
+
}
|
|
2399
|
+
const self = this;
|
|
2400
|
+
return {
|
|
2401
|
+
close() {
|
|
2402
|
+
self.watchCallbacks.delete(id);
|
|
2403
|
+
},
|
|
2404
|
+
ref() {
|
|
2405
|
+
return this;
|
|
2406
|
+
},
|
|
2407
|
+
unref() {
|
|
2408
|
+
return this;
|
|
2409
|
+
},
|
|
2410
|
+
[Symbol.asyncIterator]() {
|
|
2411
|
+
const queue = [];
|
|
2412
|
+
let resolver = null;
|
|
2413
|
+
callbacks.add((eventType, filename) => {
|
|
2414
|
+
const event = { eventType, filename };
|
|
2415
|
+
if (resolver) {
|
|
2416
|
+
resolver({ value: event, done: false });
|
|
2417
|
+
resolver = null;
|
|
2418
|
+
} else {
|
|
2419
|
+
queue.push(event);
|
|
2420
|
+
}
|
|
2421
|
+
});
|
|
2422
|
+
return {
|
|
2423
|
+
next() {
|
|
2424
|
+
if (queue.length > 0) {
|
|
2425
|
+
return Promise.resolve({ value: queue.shift(), done: false });
|
|
2426
|
+
}
|
|
2427
|
+
return new Promise((resolve) => {
|
|
2428
|
+
resolver = resolve;
|
|
2429
|
+
});
|
|
2430
|
+
},
|
|
2431
|
+
return() {
|
|
2432
|
+
return Promise.resolve({ done: true, value: void 0 });
|
|
2433
|
+
}
|
|
2434
|
+
};
|
|
2435
|
+
}
|
|
2436
|
+
};
|
|
2437
|
+
}
|
|
2438
|
+
/**
|
|
2439
|
+
* Create read stream
|
|
2440
|
+
*/
|
|
2441
|
+
createReadStream(path, options = {}) {
|
|
2442
|
+
this.log("createReadStream", path, options);
|
|
2443
|
+
const normalizedPath = normalize(path);
|
|
2444
|
+
return createReadStream(normalizedPath, options, {
|
|
2445
|
+
readFile: (p) => this.readFile(p)
|
|
2446
|
+
});
|
|
2447
|
+
}
|
|
2448
|
+
/**
|
|
2449
|
+
* Create write stream
|
|
2450
|
+
*/
|
|
2451
|
+
createWriteStream(path, options = {}) {
|
|
2452
|
+
this.log("createWriteStream", path, options);
|
|
2453
|
+
const normalizedPath = normalize(path);
|
|
2454
|
+
return createWriteStream(normalizedPath, options, {
|
|
2455
|
+
readFile: (p) => this.readFile(p),
|
|
2456
|
+
writeFile: (p, d) => this.writeFile(p, d)
|
|
2457
|
+
});
|
|
2458
|
+
}
|
|
2459
|
+
/**
|
|
2460
|
+
* Get file statistics (alias for stat)
|
|
2461
|
+
*/
|
|
2462
|
+
async backFile(path) {
|
|
2463
|
+
this.log("backFile", path);
|
|
2464
|
+
try {
|
|
2465
|
+
return await this.stat(normalize(path));
|
|
2466
|
+
} catch (err) {
|
|
2467
|
+
if (err.code === "ENOENT") throw err;
|
|
2468
|
+
throw createENOENT(path);
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
/**
|
|
2472
|
+
* Get disk usage for a path
|
|
2473
|
+
*/
|
|
2474
|
+
async du(path) {
|
|
2475
|
+
if (this.hybrid) {
|
|
2476
|
+
return this.hybrid.du(path);
|
|
2477
|
+
}
|
|
2478
|
+
this.log("du", path);
|
|
2479
|
+
const normalizedPath = normalize(path);
|
|
2480
|
+
const stat = await this.stat(normalizedPath);
|
|
2481
|
+
return { path: normalizedPath, size: stat.size };
|
|
2482
|
+
}
|
|
2483
|
+
/**
|
|
2484
|
+
* Get filesystem statistics (similar to Node.js fs.statfs)
|
|
2485
|
+
* Uses the Storage API to get quota and usage information
|
|
2486
|
+
* Note: Values are estimates for the entire origin, not per-path
|
|
2487
|
+
*/
|
|
2488
|
+
async statfs(path) {
|
|
2489
|
+
if (this.hybrid) {
|
|
2490
|
+
return this.hybrid.statfs(path);
|
|
2491
|
+
}
|
|
2492
|
+
this.log("statfs", path);
|
|
2493
|
+
try {
|
|
2494
|
+
if (path) {
|
|
2495
|
+
await this.stat(normalize(path));
|
|
2496
|
+
}
|
|
2497
|
+
if (typeof navigator === "undefined" || !navigator.storage?.estimate) {
|
|
2498
|
+
throw new Error("Storage API not available");
|
|
2499
|
+
}
|
|
2500
|
+
const estimate = await navigator.storage.estimate();
|
|
2501
|
+
const usage = estimate.usage ?? 0;
|
|
2502
|
+
const quota = estimate.quota ?? 0;
|
|
2503
|
+
const bsize = 4096;
|
|
2504
|
+
return {
|
|
2505
|
+
type: 0,
|
|
2506
|
+
bsize,
|
|
2507
|
+
blocks: Math.floor(quota / bsize),
|
|
2508
|
+
bfree: Math.floor((quota - usage) / bsize),
|
|
2509
|
+
bavail: Math.floor((quota - usage) / bsize),
|
|
2510
|
+
files: 0,
|
|
2511
|
+
ffree: 0,
|
|
2512
|
+
usage,
|
|
2513
|
+
quota
|
|
2514
|
+
};
|
|
2515
|
+
} catch (err) {
|
|
2516
|
+
this.logError("statfs", err);
|
|
2517
|
+
throw wrapError(err);
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
/**
|
|
2521
|
+
* Reset internal caches
|
|
2522
|
+
* Useful when external processes modify the filesystem
|
|
2523
|
+
*/
|
|
2524
|
+
resetCache() {
|
|
2525
|
+
if (this.hybrid) {
|
|
2526
|
+
this.hybrid.resetCache();
|
|
2527
|
+
return;
|
|
2528
|
+
}
|
|
2529
|
+
this.symlinkManager.reset();
|
|
2530
|
+
this.packedStorage.reset();
|
|
2531
|
+
this.handleManager.clearCache();
|
|
2532
|
+
}
|
|
2533
|
+
/**
|
|
2534
|
+
* Force full garbage collection
|
|
2535
|
+
* Releases all handles and caches, reinitializes the worker in hybrid mode
|
|
2536
|
+
* Use this for long-running operations to prevent memory leaks
|
|
2537
|
+
*/
|
|
2538
|
+
async gc() {
|
|
2539
|
+
if (this.hybrid) {
|
|
2540
|
+
await this.hybrid.gc();
|
|
2541
|
+
return;
|
|
2542
|
+
}
|
|
2543
|
+
this.symlinkManager.reset();
|
|
2544
|
+
await this.packedStorage.clear();
|
|
2545
|
+
this.handleManager.clearCache();
|
|
2546
|
+
}
|
|
2547
|
+
};
|
|
2548
|
+
|
|
2549
|
+
export { OPFSHybrid, constants, OPFS as default };
|
|
2550
|
+
//# sourceMappingURL=index.js.map
|
|
2551
|
+
//# sourceMappingURL=index.js.map
|