opfs-worker 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,765 @@
1
+ import { expose } from 'comlink';
2
+ import { decodeBuffer } from './utils/encoder';
3
+ import { FileNotFoundError, OPFSError, OPFSNotMountedError, PathError } from './utils/errors';
4
+ import { calculateFileHash, checkOPFSSupport, joinPath, readFileData, splitPath, writeFileData } from './utils/helpers';
5
+ /**
6
+ * OPFS (Origin Private File System) File System implementation
7
+ *
8
+ * This class provides a high-level interface for working with the browser's
9
+ * Origin Private File System API, offering file and directory operations
10
+ * similar to Node.js fs module.
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * const fs = new OPFSFileSystem();
15
+ * await fs.init('/my-app');
16
+ * await fs.writeFile('/data/config.json', JSON.stringify({ theme: 'dark' }));
17
+ * const config = await fs.readFile('/data/config.json');
18
+ * ```
19
+ */
20
+ export class OPFSWorker {
21
+ /** Root directory handle for the file system */
22
+ root = null;
23
+ /**
24
+ * Creates a new OPFSFileSystem instance
25
+ *
26
+ * @throws {OPFSError} If OPFS is not supported in the current browser
27
+ */
28
+ constructor() {
29
+ checkOPFSSupport();
30
+ }
31
+ /**
32
+ * Initialize the file system within a given directory
33
+ *
34
+ * This method sets up the root directory for all subsequent operations.
35
+ * It must be called before any other file system operations.
36
+ *
37
+ * @param root - The root path for the file system (default: '/')
38
+ * @returns Promise that resolves to true if initialization was successful
39
+ * @throws {OPFSError} If initialization fails
40
+ *
41
+ * @example
42
+ * ```typescript
43
+ * const fs = new OPFSFileSystem();
44
+ * const success = await fs.init('/my-app');
45
+ * ```
46
+ */
47
+ async mount(root = '/') {
48
+ try {
49
+ const rootDir = await navigator.storage.getDirectory();
50
+ this.root = await this.getDirectoryHandle(root, true, rootDir);
51
+ return true;
52
+ }
53
+ catch (error) {
54
+ console.error(error);
55
+ throw new OPFSError('Failed to initialize OPFS', 'INIT_FAILED');
56
+ }
57
+ }
58
+ /**
59
+ * Get a directory handle from a path
60
+ *
61
+ * Navigates through the directory structure to find or create a directory
62
+ * at the specified path.
63
+ *
64
+ * @param path - The path to the directory (string or array of segments)
65
+ * @param create - Whether to create the directory if it doesn't exist (default: false)
66
+ * @param from - The directory to start from (default: root directory)
67
+ * @returns Promise that resolves to the directory handle
68
+ * @throws {OPFSError} If the directory cannot be accessed or created
69
+ *
70
+ * @example
71
+ * ```typescript
72
+ * const docsDir = await fs.getDirectoryHandle('/users/john/documents', true);
73
+ * const docsDir2 = await fs.getDirectoryHandle(['users', 'john', 'documents'], true);
74
+ * ```
75
+ */
76
+ async getDirectoryHandle(path, create = false, from = this.root) {
77
+ if (!from) {
78
+ throw new OPFSNotMountedError();
79
+ }
80
+ const segments = Array.isArray(path) ? path : splitPath(path);
81
+ let current = from;
82
+ for (const segment of segments) {
83
+ current = await current.getDirectoryHandle(segment, { create });
84
+ }
85
+ return current;
86
+ }
87
+ /**
88
+ * Get a file handle from a path
89
+ *
90
+ * Navigates to the parent directory and retrieves or creates a file handle
91
+ * for the specified file path.
92
+ *
93
+ * @param path - The path to the file (string or array of segments)
94
+ * @param create - Whether to create the file if it doesn't exist (default: false)
95
+ * @param from - The directory to start from (default: root directory)
96
+ * @returns Promise that resolves to the file handle
97
+ * @throws {PathError} If the path is empty
98
+ * @throws {OPFSError} If the file cannot be accessed or created
99
+ *
100
+ * @example
101
+ * ```typescript
102
+ * const fileHandle = await fs.getFileHandle('/config/settings.json', true);
103
+ * const fileHandle2 = await fs.getFileHandle(['config', 'settings.json'], true);
104
+ * ```
105
+ */
106
+ async getFileHandle(path, create = false, from = this.root) {
107
+ if (!from) {
108
+ throw new OPFSNotMountedError();
109
+ }
110
+ const segments = splitPath(path);
111
+ if (segments.length === 0) {
112
+ throw new PathError('Path must not be empty', Array.isArray(path) ? path.join('/') : path);
113
+ }
114
+ const fileName = segments.pop();
115
+ const dir = await this.getDirectoryHandle(segments, create, from);
116
+ return dir.getFileHandle(fileName, { create });
117
+ }
118
+ /**
119
+ * Recursively list all files and directories with their stats
120
+ *
121
+ * Traverses the entire file system starting from the root and returns
122
+ * a Map containing all paths and their corresponding file statistics.
123
+ *
124
+ * @param options - Options for indexing
125
+ * @param options.includeHash - Whether to calculate file hash (default: false)
126
+ * @param options.hashAlgorithm - Hash algorithm to use (default: 'SHA-1', fastest)
127
+ * @returns Promise that resolves to a Map of path => FileStat
128
+ * @throws {OPFSError} If the indexing operation fails
129
+ *
130
+ * @example
131
+ * ```typescript
132
+ * // Basic index without hash
133
+ * const index = await fs.index();
134
+ *
135
+ * // Index with file hash
136
+ * const indexWithHash = await fs.index({
137
+ * includeHash: true,
138
+ * hashAlgorithm: 'SHA-1'
139
+ * });
140
+ *
141
+ * // Iterate through all files and directories
142
+ * for (const [path, stat] of index) {
143
+ * console.log(`${path}: ${stat.isFile ? 'file' : 'directory'} (${stat.size} bytes)`);
144
+ * if (stat.hash) console.log(` Hash: ${stat.hash}`);
145
+ * }
146
+ *
147
+ * // Get specific file stats
148
+ * const fileStats = index.get('/data/config.json');
149
+ * if (fileStats) {
150
+ * console.log(`File size: ${fileStats.size} bytes`);
151
+ * if (fileStats.hash) console.log(`Hash: ${fileStats.hash}`);
152
+ * }
153
+ * ```
154
+ */
155
+ async index(options) {
156
+ const result = new Map();
157
+ const walk = async (dirPath) => {
158
+ const items = await this.readdir(dirPath, { withFileTypes: true });
159
+ for (const item of items) {
160
+ const fullPath = `${dirPath === '/' ? '' : dirPath}/${item.name}`;
161
+ try {
162
+ const stat = await this.stat(fullPath, options);
163
+ result.set(fullPath, stat);
164
+ if (stat.isDirectory) {
165
+ await walk(fullPath);
166
+ }
167
+ }
168
+ catch (err) {
169
+ console.warn(`Skipping broken entry: ${fullPath}`, err);
170
+ }
171
+ }
172
+ };
173
+ // Add root directory
174
+ result.set('/', {
175
+ kind: 'directory',
176
+ size: 0,
177
+ mtime: new Date(0).toISOString(),
178
+ ctime: new Date(0).toISOString(),
179
+ isFile: false,
180
+ isDirectory: true,
181
+ });
182
+ await walk('/');
183
+ return result;
184
+ }
185
+ async readFile(path, encoding = 'utf-8') {
186
+ try {
187
+ const fileHandle = await this.getFileHandle(path, false);
188
+ const buffer = await readFileData(fileHandle);
189
+ if (encoding === 'binary') {
190
+ return buffer;
191
+ }
192
+ return decodeBuffer(buffer, encoding);
193
+ }
194
+ catch (err) {
195
+ console.error(err);
196
+ throw new FileNotFoundError(path);
197
+ }
198
+ }
199
+ /**
200
+ * Write data to a file
201
+ *
202
+ * Creates or overwrites a file with the specified data. If the file already
203
+ * exists, it will be truncated before writing.
204
+ *
205
+ * @param path - The path to the file to write
206
+ * @param data - The data to write to the file (string, Uint8Array, or ArrayBuffer)
207
+ * @param encoding - The encoding to use when writing string data (default: 'utf-8')
208
+ * @returns Promise that resolves when the write operation is complete
209
+ * @throws {OPFSError} If writing the file fails
210
+ *
211
+ * @example
212
+ * ```typescript
213
+ * // Write text data
214
+ * await fs.writeFile('/config/settings.json', JSON.stringify({ theme: 'dark' }));
215
+ *
216
+ * // Write binary data
217
+ * const binaryData = new Uint8Array([1, 2, 3, 4, 5]);
218
+ * await fs.writeFile('/data/binary.dat', binaryData);
219
+ *
220
+ * // Write with specific encoding
221
+ * await fs.writeFile('/data/utf16.txt', 'Hello World', 'utf-16le');
222
+ * ```
223
+ */
224
+ async writeFile(path, data, encoding) {
225
+ const fileHandle = await this.getFileHandle(path, true);
226
+ await writeFileData(fileHandle, data, encoding, { truncate: true });
227
+ }
228
+ /**
229
+ * Append data to a file
230
+ *
231
+ * Adds data to the end of an existing file. If the file doesn't exist,
232
+ * it will be created.
233
+ *
234
+ * @param path - The path to the file to append to
235
+ * @param data - The data to append to the file (string, Uint8Array, or ArrayBuffer)
236
+ * @param encoding - The encoding to use when appending string data (default: 'utf-8')
237
+ * @returns Promise that resolves when the append operation is complete
238
+ * @throws {OPFSError} If appending to the file fails
239
+ *
240
+ * @example
241
+ * ```typescript
242
+ * // Append text to a log file
243
+ * await fs.appendFile('/logs/app.log', `[${new Date().toISOString()}] User logged in\n`);
244
+ *
245
+ * // Append binary data
246
+ * const additionalData = new Uint8Array([6, 7, 8]);
247
+ * await fs.appendFile('/data/binary.dat', additionalData);
248
+ * ```
249
+ */
250
+ async appendFile(path, data, encoding) {
251
+ const fileHandle = await this.getFileHandle(path, true);
252
+ await writeFileData(fileHandle, data, encoding, { append: true });
253
+ }
254
+ /**
255
+ * Create a directory
256
+ *
257
+ * Creates a new directory at the specified path. If the recursive option
258
+ * is enabled, parent directories will be created as needed.
259
+ *
260
+ * @param path - The path where the directory should be created
261
+ * @param options - Options for directory creation
262
+ * @param options.recursive - Whether to create parent directories if they don't exist (default: false)
263
+ * @returns Promise that resolves when the directory is created
264
+ * @throws {OPFSError} If the directory cannot be created
265
+ *
266
+ * @example
267
+ * ```typescript
268
+ * // Create a single directory
269
+ * await fs.mkdir('/users/john');
270
+ *
271
+ * // Create nested directories
272
+ * await fs.mkdir('/users/john/documents/projects', { recursive: true });
273
+ * ```
274
+ */
275
+ async mkdir(path, options) {
276
+ if (!this.root) {
277
+ throw new OPFSNotMountedError();
278
+ }
279
+ const recursive = options?.recursive ?? false;
280
+ const segments = splitPath(path);
281
+ let current = this.root;
282
+ for (let i = 0; i < segments.length; i++) {
283
+ const segment = segments[i];
284
+ try {
285
+ current = await current.getDirectoryHandle(segment, { create: recursive || i === segments.length - 1 });
286
+ }
287
+ catch (e) {
288
+ if (e.name === 'NotFoundError') {
289
+ throw new OPFSError(`Parent directory does not exist: ${joinPath(segments.slice(0, i + 1))}`, 'ENOENT');
290
+ }
291
+ if (e.name === 'TypeMismatchError') {
292
+ throw new OPFSError(`Path segment is not a directory: ${segment}`, 'ENOTDIR');
293
+ }
294
+ throw new OPFSError('Failed to create directory', 'MKDIR_FAILED');
295
+ }
296
+ }
297
+ }
298
+ /**
299
+ * Get file or directory stats
300
+ *
301
+ * Retrieves metadata about a file or directory, including size, modification time,
302
+ * type information, and optionally file hashes.
303
+ *
304
+ * @param path - The path to the file or directory
305
+ * @param options - Options for stat operation
306
+ * @param options.includeHash - Whether to calculate file hash (default: false, only for files)
307
+ * @param options.hashAlgorithm - Hash algorithm to use (default: 'SHA-1', fastest)
308
+ * @returns Promise that resolves to file/directory statistics
309
+ * @throws {OPFSError} If the file or directory does not exist or cannot be accessed
310
+ *
311
+ * @example
312
+ * ```typescript
313
+ * // Basic stats
314
+ * const stats = await fs.stat('/config/settings.json');
315
+ * console.log(`File size: ${stats.size} bytes`);
316
+ * console.log(`Is file: ${stats.isFile}`);
317
+ * console.log(`Modified: ${stats.mtime}`);
318
+ *
319
+ * // Stats with hash (SHA-1 is fastest)
320
+ * const statsWithHash = await fs.stat('/config/settings.json', {
321
+ * includeHash: true,
322
+ * hashAlgorithm: 'SHA-1'
323
+ * });
324
+ * console.log(`Hash: ${statsWithHash.hash}`);
325
+ * ```
326
+ */
327
+ async stat(path, options) {
328
+ const segments = splitPath(path);
329
+ const name = segments.pop();
330
+ const parentDir = await this.getDirectoryHandle(segments, false);
331
+ const includeHash = options?.includeHash ?? false;
332
+ const hashAlgorithm = options?.hashAlgorithm ?? 'SHA-1';
333
+ // Get as file first
334
+ try {
335
+ const fileHandle = await parentDir.getFileHandle(name, { create: false });
336
+ const file = await fileHandle.getFile();
337
+ const baseStat = {
338
+ kind: 'file',
339
+ size: file.size,
340
+ mtime: new Date(file.lastModified).toISOString(),
341
+ ctime: new Date(file.lastModified).toISOString(),
342
+ isFile: true,
343
+ isDirectory: false,
344
+ };
345
+ // Calculate hash if requested
346
+ if (includeHash) {
347
+ try {
348
+ const buffer = new Uint8Array(await file.arrayBuffer());
349
+ const hash = await calculateFileHash(buffer, hashAlgorithm);
350
+ baseStat.hash = hash;
351
+ }
352
+ catch (error) {
353
+ console.warn(`Failed to calculate hash for ${path}:`, error);
354
+ }
355
+ }
356
+ return baseStat;
357
+ }
358
+ catch (e) {
359
+ if (e.name !== 'TypeMismatchError' && e.name !== 'NotFoundError') {
360
+ throw new OPFSError('Failed to stat (file)', 'STAT_FAILED');
361
+ }
362
+ }
363
+ // Get as directory
364
+ try {
365
+ await parentDir.getDirectoryHandle(name, { create: false });
366
+ return {
367
+ kind: 'directory',
368
+ size: 0,
369
+ mtime: new Date(0).toISOString(),
370
+ ctime: new Date(0).toISOString(),
371
+ isFile: false,
372
+ isDirectory: true,
373
+ // Directories don't have hashes
374
+ };
375
+ }
376
+ catch (e) {
377
+ if (e.name === 'NotFoundError') {
378
+ throw new OPFSError(`No such file or directory: ${path}`, 'ENOENT');
379
+ }
380
+ throw new OPFSError('Failed to stat (directory)', 'STAT_FAILED');
381
+ }
382
+ }
383
+ async readdir(path, options) {
384
+ const withTypes = options?.withFileTypes ?? false;
385
+ const dir = await this.getDirectoryHandle(path, false);
386
+ // Use type assertion to access the entries() method
387
+ if (withTypes) {
388
+ const results = [];
389
+ for await (const [name, handle] of dir.entries()) {
390
+ const isFile = handle.kind === 'file';
391
+ results.push({
392
+ name,
393
+ kind: handle.kind,
394
+ isFile,
395
+ isDirectory: !isFile,
396
+ });
397
+ }
398
+ return results;
399
+ }
400
+ else {
401
+ const results = [];
402
+ for await (const [name] of dir.entries()) {
403
+ results.push(name);
404
+ }
405
+ return results;
406
+ }
407
+ }
408
+ /**
409
+ * Check if a file or directory exists
410
+ *
411
+ * Verifies if a file or directory exists at the specified path.
412
+ *
413
+ * @param path - The path to check
414
+ * @returns Promise that resolves to true if the file or directory exists, false otherwise
415
+ *
416
+ * @example
417
+ * ```typescript
418
+ * const exists = await fs.exists('/config/settings.json');
419
+ * console.log(`File exists: ${exists}`);
420
+ * ```
421
+ */
422
+ async exists(path) {
423
+ const segments = splitPath(path);
424
+ const name = segments.pop();
425
+ let dir = null;
426
+ try {
427
+ dir = await this.getDirectoryHandle(segments, false);
428
+ }
429
+ catch (e) {
430
+ if (e.name === 'NotFoundError' || e.name === 'TypeMismatchError') {
431
+ dir = null;
432
+ }
433
+ throw e;
434
+ }
435
+ if (!dir || !name) {
436
+ return false;
437
+ }
438
+ // Get as file
439
+ try {
440
+ await dir.getFileHandle(name, { create: false });
441
+ return true;
442
+ }
443
+ catch (e) {
444
+ if (e.name !== 'NotFoundError' && e.name !== 'TypeMismatchError') {
445
+ throw e;
446
+ }
447
+ }
448
+ // Get as directory
449
+ try {
450
+ await dir.getDirectoryHandle(name, { create: false });
451
+ return true;
452
+ }
453
+ catch (e) {
454
+ if (e.name !== 'NotFoundError' && e.name !== 'TypeMismatchError') {
455
+ throw e;
456
+ }
457
+ }
458
+ return false;
459
+ }
460
+ /**
461
+ * Clear all contents of a directory without removing the directory itself
462
+ *
463
+ * Removes all files and subdirectories within the specified directory,
464
+ * but keeps the directory itself.
465
+ *
466
+ * @param path - The path to the directory to clear (default: '/')
467
+ * @returns Promise that resolves when all contents are removed
468
+ * @throws {OPFSError} If the operation fails
469
+ *
470
+ * @example
471
+ * ```typescript
472
+ * // Clear root directory contents
473
+ * await fs.clear('/');
474
+ *
475
+ * // Clear specific directory contents
476
+ * await fs.clear('/data');
477
+ * ```
478
+ */
479
+ async clear(path = '/') {
480
+ try {
481
+ const items = await this.readdir(path, { withFileTypes: true });
482
+ for (const item of items) {
483
+ const itemPath = `${path === '/' ? '' : path}/${item.name}`;
484
+ await this.remove(itemPath, { recursive: true });
485
+ }
486
+ }
487
+ catch (error) {
488
+ if (error instanceof OPFSError) {
489
+ throw error;
490
+ }
491
+ throw new OPFSError(`Failed to clear directory: ${path}`, 'CLEAR_FAILED');
492
+ }
493
+ }
494
+ /**
495
+ * Remove files and directories
496
+ *
497
+ * Removes files and directories. Similar to Node.js fs.rm().
498
+ *
499
+ * @param path - The path to remove
500
+ * @param options - Options for removal
501
+ * @param options.recursive - Whether to remove directories and their contents recursively (default: false)
502
+ * @param options.force - Whether to ignore errors if the path doesn't exist (default: false)
503
+ * @returns Promise that resolves when the removal is complete
504
+ * @throws {OPFSError} If the removal fails
505
+ *
506
+ * @example
507
+ * ```typescript
508
+ * // Remove a file
509
+ * await fs.rm('/path/to/file.txt');
510
+ *
511
+ * // Remove a directory and all its contents
512
+ * await fs.rm('/path/to/directory', { recursive: true });
513
+ *
514
+ * // Remove with force (ignore if doesn't exist)
515
+ * await fs.rm('/maybe/exists', { force: true });
516
+ * ```
517
+ */
518
+ async remove(path, options) {
519
+ const recursive = options?.recursive ?? false;
520
+ const force = options?.force ?? false;
521
+ const segments = splitPath(path);
522
+ const name = segments.pop();
523
+ if (!name) {
524
+ throw new PathError('Invalid path', path);
525
+ }
526
+ const parent = await this.getDirectoryHandle(segments, false);
527
+ try {
528
+ await parent.removeEntry(name, { recursive });
529
+ }
530
+ catch (e) {
531
+ if (e.name === 'NotFoundError') {
532
+ if (!force) {
533
+ throw new OPFSError(`No such file or directory: ${path}`, 'ENOENT');
534
+ }
535
+ }
536
+ else if (e.name === 'InvalidModificationError') {
537
+ throw new OPFSError(`Directory not empty: ${path}. Use recursive option to force removal.`, 'ENOTEMPTY');
538
+ }
539
+ else if (e.name === 'TypeMismatchError' && !recursive) {
540
+ throw new OPFSError(`Cannot remove directory without recursive option: ${path}`, 'EISDIR');
541
+ }
542
+ else {
543
+ throw new OPFSError(`Failed to remove path: ${path}`, 'RM_FAILED');
544
+ }
545
+ }
546
+ }
547
+ /**
548
+ * Resolve a path to an absolute path
549
+ *
550
+ * Resolves relative paths and normalizes path segments (like '..' and '.').
551
+ * Similar to Node.js fs.realpath() but without symlink resolution since OPFS doesn't support symlinks.
552
+ *
553
+ * @param path - The path to resolve
554
+ * @returns Promise that resolves to the absolute normalized path
555
+ * @throws {FileNotFoundError} If the path does not exist
556
+ * @throws {OPFSError} If path resolution fails
557
+ *
558
+ * @example
559
+ * ```typescript
560
+ * // Resolve relative path
561
+ * const absolute = await fs.realpath('./config/../data/file.txt');
562
+ * console.log(absolute); // '/data/file.txt'
563
+ * ```
564
+ */
565
+ async realpath(path) {
566
+ try {
567
+ const segments = splitPath(path);
568
+ const normalizedSegments = [];
569
+ for (const segment of segments) {
570
+ if (segment === '.' || segment === '') {
571
+ // Skip current directory references and empty segments
572
+ continue;
573
+ }
574
+ else if (segment === '..') {
575
+ if (normalizedSegments.length === 0) {
576
+ throw new OPFSError('Path escapes root', 'EINVAL');
577
+ }
578
+ // Go up one directory
579
+ if (normalizedSegments.length > 0) {
580
+ normalizedSegments.pop();
581
+ }
582
+ }
583
+ else {
584
+ // Regular segment
585
+ normalizedSegments.push(segment);
586
+ }
587
+ }
588
+ const normalizedPath = joinPath(normalizedSegments);
589
+ const exists = await this.exists(normalizedPath);
590
+ if (!exists) {
591
+ throw new FileNotFoundError(normalizedPath);
592
+ }
593
+ return normalizedPath;
594
+ }
595
+ catch (error) {
596
+ if (error instanceof OPFSError) {
597
+ throw error;
598
+ }
599
+ throw new OPFSError(`Failed to resolve path: ${path}`, 'REALPATH_FAILED');
600
+ }
601
+ }
602
+ /**
603
+ * Rename a file or directory
604
+ *
605
+ * Changes the name of a file or directory. If the target path already exists,
606
+ * it will be replaced.
607
+ *
608
+ * @param oldPath - The current path of the file or directory
609
+ * @param newPath - The new path for the file or directory
610
+ * @returns Promise that resolves when the rename operation is complete
611
+ * @throws {OPFSError} If the rename operation fails
612
+ *
613
+ * @example
614
+ * ```typescript
615
+ * await fs.rename('/old/path/file.txt', '/new/path/renamed.txt');
616
+ * ```
617
+ */
618
+ async rename(oldPath, newPath) {
619
+ try {
620
+ // Check if source exists
621
+ const sourceExists = await this.exists(oldPath);
622
+ if (!sourceExists) {
623
+ throw new FileNotFoundError(oldPath);
624
+ }
625
+ await this.copy(oldPath, newPath, { recursive: true });
626
+ await this.remove(oldPath, { recursive: true });
627
+ }
628
+ catch (error) {
629
+ if (error instanceof OPFSError) {
630
+ throw error;
631
+ }
632
+ throw new OPFSError(`Failed to rename from ${oldPath} to ${newPath}`, 'RENAME_FAILED');
633
+ }
634
+ }
635
+ /**
636
+ * Copy files and directories
637
+ *
638
+ * Copies files and directories. Similar to Node.js fs.cp().
639
+ *
640
+ * @param source - The source path to copy from
641
+ * @param destination - The destination path to copy to
642
+ * @param options - Options for copying
643
+ * @param options.recursive - Whether to copy directories recursively (default: false)
644
+ * @param options.force - Whether to overwrite existing files (default: true)
645
+ * @returns Promise that resolves when the copy operation is complete
646
+ * @throws {OPFSError} If the copy operation fails
647
+ *
648
+ * @example
649
+ * ```typescript
650
+ * // Copy a file
651
+ * await fs.cp('/source/file.txt', '/dest/file.txt');
652
+ *
653
+ * // Copy a directory and all its contents
654
+ * await fs.cp('/source/dir', '/dest/dir', { recursive: true });
655
+ *
656
+ * // Copy without overwriting existing files
657
+ * await fs.cp('/source', '/dest', { recursive: true, force: false });
658
+ * ```
659
+ */
660
+ async copy(source, destination, options) {
661
+ try {
662
+ const recursive = options?.recursive ?? false;
663
+ const force = options?.force ?? true;
664
+ const sourceExists = await this.exists(source);
665
+ if (!sourceExists) {
666
+ throw new OPFSError(`Source does not exist: ${source}`, 'ENOENT');
667
+ }
668
+ // Check if destination exists and handle accordingly
669
+ const destExists = await this.exists(destination);
670
+ if (destExists && !force) {
671
+ throw new OPFSError(`Destination already exists: ${destination}`, 'EEXIST');
672
+ }
673
+ // Get source stats to determine if it's a file or directory
674
+ const sourceStats = await this.stat(source);
675
+ if (sourceStats.isFile) {
676
+ // Copy file
677
+ const content = await this.readFile(source, 'binary');
678
+ await this.writeFile(destination, content);
679
+ }
680
+ else {
681
+ // Copy directory
682
+ if (!recursive) {
683
+ throw new OPFSError(`Cannot copy directory without recursive option: ${source}`, 'EISDIR');
684
+ }
685
+ // Create destination directory
686
+ await this.mkdir(destination, { recursive: true });
687
+ // Copy all contents
688
+ const items = await this.readdir(source, { withFileTypes: true });
689
+ for (const item of items) {
690
+ const sourceItemPath = `${source}/${item.name}`;
691
+ const destItemPath = `${destination}/${item.name}`;
692
+ // Recursively copy each item
693
+ await this.copy(sourceItemPath, destItemPath, { recursive: true, force });
694
+ }
695
+ }
696
+ }
697
+ catch (error) {
698
+ if (error instanceof OPFSError) {
699
+ throw error;
700
+ }
701
+ throw new OPFSError(`Failed to copy from ${source} to ${destination}`, 'CP_FAILED');
702
+ }
703
+ }
704
+ /**
705
+ * Synchronize the file system with external data
706
+ *
707
+ * Syncs the file system with an array of entries containing paths and data.
708
+ * This is useful for importing data from external sources or syncing with remote data.
709
+ *
710
+ * @param entries - Array of [path, data] tuples to sync
711
+ * @param options - Options for synchronization
712
+ * @param options.cleanBefore - Whether to clear the file system before syncing (default: false)
713
+ * @returns Promise that resolves when synchronization is complete
714
+ * @throws {OPFSError} If the synchronization fails
715
+ *
716
+ * @example
717
+ * ```typescript
718
+ * // Sync with external data
719
+ * const entries: [string, string | Uint8Array | Blob][] = [
720
+ * ['/config.json', JSON.stringify({ theme: 'dark' })],
721
+ * ['/data/binary.dat', new Uint8Array([1, 2, 3, 4])],
722
+ * ['/upload.txt', new Blob(['file content'], { type: 'text/plain' })]
723
+ * ];
724
+ *
725
+ * // Sync without clearing existing files
726
+ * await fs.sync(entries);
727
+ *
728
+ * // Clean file system and then sync
729
+ * await fs.sync(entries, { cleanBefore: true });
730
+ * ```
731
+ */
732
+ async sync(entries, options) {
733
+ try {
734
+ const cleanBefore = options?.cleanBefore ?? false;
735
+ // Clear file system if requested
736
+ if (cleanBefore) {
737
+ await this.clear('/');
738
+ }
739
+ // Process each entry
740
+ for (const [path, data] of entries) {
741
+ // Normalize path to ensure it starts with /
742
+ const normalizedPath = path.startsWith('/') ? path : `/${path}`;
743
+ // Convert data to appropriate format
744
+ let fileData;
745
+ if (data instanceof Blob) {
746
+ // Convert Blob to Uint8Array
747
+ const arrayBuffer = await data.arrayBuffer();
748
+ fileData = new Uint8Array(arrayBuffer);
749
+ }
750
+ else {
751
+ fileData = data;
752
+ }
753
+ // Write the file (this will create directories as needed)
754
+ await this.writeFile(normalizedPath, fileData);
755
+ }
756
+ }
757
+ catch (error) {
758
+ if (error instanceof OPFSError) {
759
+ throw error;
760
+ }
761
+ throw new OPFSError('Failed to sync file system', 'SYNC_FAILED');
762
+ }
763
+ }
764
+ }
765
+ expose(new OPFSWorker());