opfs-worker 1.3.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +89 -106
- package/dist/{facade.d.ts → OPFSFacade.d.ts} +22 -5
- package/dist/OPFSFacade.d.ts.map +1 -0
- package/dist/{worker.d.ts → OPFSWorker.d.ts} +10 -6
- package/dist/OPFSWorker.d.ts.map +1 -0
- package/dist/assets/worker.entry-DUlEoroc.js.map +1 -0
- package/dist/createOPFSWorker.d.ts +17 -0
- package/dist/createOPFSWorker.d.ts.map +1 -0
- package/dist/helpers-DNj8ZoMu.cjs +4 -0
- package/dist/helpers-DNj8ZoMu.cjs.map +1 -0
- package/dist/{helpers-Ca2c767K.js → helpers-WY2jfbOT.js} +221 -221
- package/dist/helpers-WY2jfbOT.js.map +1 -0
- package/dist/index.cjs +296 -285
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +369 -340
- package/dist/index.js.map +1 -1
- package/dist/index.pure.cjs +2 -0
- package/dist/index.pure.cjs.map +1 -0
- package/dist/index.pure.d.ts +6 -0
- package/dist/index.pure.d.ts.map +1 -0
- package/dist/{raw.js → index.pure.js} +241 -186
- package/dist/index.pure.js.map +1 -0
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/helpers.d.ts +11 -1
- package/dist/utils/helpers.d.ts.map +1 -1
- package/dist/worker.entry.d.ts +2 -0
- package/dist/worker.entry.d.ts.map +1 -0
- package/docs/api-reference.md +815 -0
- package/docs/file-descriptors.md +696 -0
- package/docs/types.md +232 -0
- package/package.json +14 -8
- package/src/OPFSFacade.ts +460 -0
- package/src/OPFSWorker.ts +1544 -0
- package/src/createOPFSWorker.ts +57 -0
- package/src/index.pure.ts +7 -0
- package/src/index.ts +15 -0
- package/src/types.ts +99 -0
- package/src/utils/encoder.ts +205 -0
- package/src/utils/errors.ts +297 -0
- package/src/utils/helpers.ts +465 -0
- package/src/worker.entry.ts +6 -0
- package/dist/assets/worker-1Wh1cr7P.js.map +0 -1
- package/dist/assets/worker-BeJaVyBV.js.map +0 -1
- package/dist/assets/worker-DYYLzR1Y.js.map +0 -1
- package/dist/facade.d.ts.map +0 -1
- package/dist/helpers-BuGfPAg0.js +0 -1439
- package/dist/helpers-BuGfPAg0.js.map +0 -1
- package/dist/helpers-CF7A2WQG.cjs +0 -4
- package/dist/helpers-CF7A2WQG.cjs.map +0 -1
- package/dist/helpers-CIiblZ8d.cjs +0 -4
- package/dist/helpers-CIiblZ8d.cjs.map +0 -1
- package/dist/helpers-CKOebsbw.js +0 -1423
- package/dist/helpers-CKOebsbw.js.map +0 -1
- package/dist/helpers-Ca2c767K.js.map +0 -1
- package/dist/helpers-Dwc92hv9.cjs +0 -4
- package/dist/helpers-Dwc92hv9.cjs.map +0 -1
- package/dist/raw.cjs +0 -2
- package/dist/raw.cjs.map +0 -1
- package/dist/raw.d.ts +0 -13
- package/dist/raw.d.ts.map +0 -1
- package/dist/raw.js.map +0 -1
- package/dist/worker.d.ts.map +0 -1
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { wrap } from 'comlink';
|
|
2
|
+
|
|
3
|
+
import WorkerCtor from './worker.entry?worker&inline';
|
|
4
|
+
|
|
5
|
+
import type { OPFSOptions, RemoteOPFSWorker } from './types';
|
|
6
|
+
|
|
7
|
+
const InlineWorker = WorkerCtor as new (options?: WorkerOptions) => Worker;
|
|
8
|
+
|
|
9
|
+
export interface RawWorker {
|
|
10
|
+
/** Comlink proxy to `OPFSWorker` (bytes in / bytes out) */
|
|
11
|
+
fs: RemoteOPFSWorker;
|
|
12
|
+
/** Underlying browser Worker */
|
|
13
|
+
worker: Worker;
|
|
14
|
+
/** Calls worker `dispose()` then `terminate()` */
|
|
15
|
+
dispose: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** A `BroadcastChannel` instance can't cross the wire — send its name instead. */
|
|
19
|
+
function applyWorkerOptions(fs: Pick<RemoteOPFSWorker, 'setOptions'>, options?: OPFSOptions): void {
|
|
20
|
+
if (!options) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (options.broadcastChannel instanceof BroadcastChannel) {
|
|
25
|
+
options.broadcastChannel = options.broadcastChannel.name;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
void fs.setOptions(options);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Mode 2: spawn the inlined worker and get a Comlink proxy to `OPFSWorker`
|
|
33
|
+
* (bytes in / bytes out), without the facade.
|
|
34
|
+
*
|
|
35
|
+
* `OPFSFacade` is built on top of this — copy it if you want your own facade.
|
|
36
|
+
*/
|
|
37
|
+
export function createOPFSWorker(options?: OPFSOptions): RawWorker {
|
|
38
|
+
const worker = new InlineWorker();
|
|
39
|
+
const fs = wrap<RemoteOPFSWorker>(worker);
|
|
40
|
+
|
|
41
|
+
applyWorkerOptions(fs, options);
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
fs,
|
|
45
|
+
worker,
|
|
46
|
+
dispose() {
|
|
47
|
+
void (async() => {
|
|
48
|
+
try {
|
|
49
|
+
await fs.dispose();
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
worker.terminate();
|
|
53
|
+
}
|
|
54
|
+
})();
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export * from './types';
|
|
2
|
+
export * from './utils/errors';
|
|
3
|
+
export * from './utils/helpers';
|
|
4
|
+
export * from './utils/encoder';
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
OPFSFacade,
|
|
8
|
+
OPFSFileSystem,
|
|
9
|
+
createOPFS,
|
|
10
|
+
// 1.x aliases
|
|
11
|
+
createWorker,
|
|
12
|
+
} from './OPFSFacade';
|
|
13
|
+
|
|
14
|
+
export { createOPFSWorker } from './createOPFSWorker';
|
|
15
|
+
export type { RawWorker } from './createOPFSWorker';
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { OPFSWorker } from './OPFSWorker';
|
|
2
|
+
import type { Remote } from 'comlink';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Type for paths that can be either a string or URI
|
|
6
|
+
*/
|
|
7
|
+
export type PathLike = string | URL;
|
|
8
|
+
|
|
9
|
+
export type Kind = 'file' | 'directory';
|
|
10
|
+
|
|
11
|
+
export type StringEncoding = 'ascii'
|
|
12
|
+
| 'utf8'
|
|
13
|
+
| 'utf-8'
|
|
14
|
+
| 'utf16le'
|
|
15
|
+
| 'utf-16le'
|
|
16
|
+
| 'ucs2'
|
|
17
|
+
| 'ucs-2'
|
|
18
|
+
| 'base64'
|
|
19
|
+
| 'latin1'
|
|
20
|
+
| 'hex';
|
|
21
|
+
|
|
22
|
+
export type BinaryEncoding = 'binary';
|
|
23
|
+
|
|
24
|
+
export type Encoding = StringEncoding | BinaryEncoding;
|
|
25
|
+
|
|
26
|
+
export interface FileStat {
|
|
27
|
+
kind: Kind;
|
|
28
|
+
size: number;
|
|
29
|
+
mtime: string; // ISO string
|
|
30
|
+
ctime: string; // ISO string
|
|
31
|
+
isFile: boolean;
|
|
32
|
+
isDirectory: boolean;
|
|
33
|
+
/** Hash of file content (only for files, undefined for directories) */
|
|
34
|
+
hash?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface DirentData {
|
|
38
|
+
name: string;
|
|
39
|
+
kind: 'file' | 'directory';
|
|
40
|
+
isFile: boolean;
|
|
41
|
+
isDirectory: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export enum WatchEventType {
|
|
45
|
+
Added = 'added',
|
|
46
|
+
Changed = 'changed',
|
|
47
|
+
Removed = 'removed'
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface WatchEvent {
|
|
51
|
+
namespace: string;
|
|
52
|
+
path: string;
|
|
53
|
+
type: WatchEventType;
|
|
54
|
+
isDirectory: boolean;
|
|
55
|
+
timestamp: string;
|
|
56
|
+
hash?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type { OPFSWorker };
|
|
60
|
+
export type RemoteOPFSWorker = Remote<OPFSWorker>;
|
|
61
|
+
|
|
62
|
+
export interface OPFSOptions {
|
|
63
|
+
/** Root path for the file system (default: '/') */
|
|
64
|
+
root?: string;
|
|
65
|
+
/** Namespace for the events (default: 'opfs-worker:${root}') */
|
|
66
|
+
namespace?: string;
|
|
67
|
+
/** Hash algorithm for file hashing, or false/null to disable (default: 'etag') */
|
|
68
|
+
hashAlgorithm?: null | false | 'etag' | 'SHA-1' | 'SHA-256' | 'SHA-384' | 'SHA-512';
|
|
69
|
+
/** Maximum file size in bytes for hashing (default: 50MB) */
|
|
70
|
+
maxFileSize?: number;
|
|
71
|
+
/** Custom name for the broadcast channel (default: 'opfs-worker') */
|
|
72
|
+
broadcastChannel?: string | BroadcastChannel | null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface RenameOptions {
|
|
76
|
+
/** Whether to overwrite existing files (default: false) */
|
|
77
|
+
overwrite?: boolean;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface WatchOptions {
|
|
81
|
+
/** Whether to watch recursively (default: true) */
|
|
82
|
+
recursive?: boolean;
|
|
83
|
+
/** Glob patterns to include in watching (minimatch syntax, default: ['**']) */
|
|
84
|
+
include?: string | string[];
|
|
85
|
+
/** Glob patterns to exclude from watching (minimatch syntax, default: []) */
|
|
86
|
+
exclude?: string | string[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface FileOpenOptions {
|
|
90
|
+
create?: boolean;
|
|
91
|
+
exclusive?: boolean;
|
|
92
|
+
truncate?: boolean;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface WatchSnapshot {
|
|
96
|
+
pattern: string;
|
|
97
|
+
include: string[];
|
|
98
|
+
exclude: string[];
|
|
99
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { ValidationError } from './errors';
|
|
2
|
+
|
|
3
|
+
import type { Encoding } from '../types';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Common binary file extensions
|
|
8
|
+
*/
|
|
9
|
+
export const BINARY_FILE_EXTENSIONS = [
|
|
10
|
+
// Images
|
|
11
|
+
'.jpg',
|
|
12
|
+
'.jpeg',
|
|
13
|
+
'.png',
|
|
14
|
+
'.gif',
|
|
15
|
+
'.bmp',
|
|
16
|
+
'.webp',
|
|
17
|
+
'.ico',
|
|
18
|
+
'.tiff',
|
|
19
|
+
'.tga',
|
|
20
|
+
// Audio
|
|
21
|
+
'.mp3',
|
|
22
|
+
'.wav',
|
|
23
|
+
'.ogg',
|
|
24
|
+
'.flac',
|
|
25
|
+
'.aac',
|
|
26
|
+
'.wma',
|
|
27
|
+
'.m4a',
|
|
28
|
+
// Video
|
|
29
|
+
'.mp4',
|
|
30
|
+
'.avi',
|
|
31
|
+
'.mov',
|
|
32
|
+
'.wmv',
|
|
33
|
+
'.flv',
|
|
34
|
+
'.webm',
|
|
35
|
+
'.mkv',
|
|
36
|
+
'.m4v',
|
|
37
|
+
// Documents
|
|
38
|
+
'.pdf',
|
|
39
|
+
'.doc',
|
|
40
|
+
'.docx',
|
|
41
|
+
'.xls',
|
|
42
|
+
'.xlsx',
|
|
43
|
+
'.ppt',
|
|
44
|
+
'.pptx',
|
|
45
|
+
// Archives
|
|
46
|
+
'.zip',
|
|
47
|
+
'.rar',
|
|
48
|
+
'.7z',
|
|
49
|
+
'.tar',
|
|
50
|
+
'.gz',
|
|
51
|
+
'.bz2',
|
|
52
|
+
// Executables
|
|
53
|
+
'.exe',
|
|
54
|
+
'.dll',
|
|
55
|
+
'.so',
|
|
56
|
+
'.dylib',
|
|
57
|
+
// Other binary formats
|
|
58
|
+
'.dat',
|
|
59
|
+
'.db',
|
|
60
|
+
'.sqlite',
|
|
61
|
+
'.bin',
|
|
62
|
+
'.obj',
|
|
63
|
+
'.fbx',
|
|
64
|
+
'.3ds',
|
|
65
|
+
] as const;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Check if a file extension indicates a binary file
|
|
69
|
+
*
|
|
70
|
+
* @param path - The file path or filename
|
|
71
|
+
* @returns True if the file extension suggests binary content
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```typescript
|
|
75
|
+
* isBinaryFileExtension('/path/to/image.jpg'); // true
|
|
76
|
+
* isBinaryFileExtension('/path/to/document.txt'); // false
|
|
77
|
+
* isBinaryFileExtension('data.bin'); // true
|
|
78
|
+
* isBinaryFileExtension('data'); // true
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export function isBinaryFileExtension(path: string): boolean {
|
|
82
|
+
const i = path.lastIndexOf('.');
|
|
83
|
+
|
|
84
|
+
if (i <= 0) {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const ext = path.slice(i).toLowerCase();
|
|
89
|
+
|
|
90
|
+
return BINARY_FILE_EXTENSIONS.includes(ext as any);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function encodeString(data: string, encoding: Encoding = 'utf-8'): Uint8Array {
|
|
94
|
+
switch (encoding) {
|
|
95
|
+
case 'utf8':
|
|
96
|
+
case 'utf-8':
|
|
97
|
+
return new TextEncoder().encode(data);
|
|
98
|
+
|
|
99
|
+
case 'utf16le':
|
|
100
|
+
case 'utf-16le':
|
|
101
|
+
case 'ucs2':
|
|
102
|
+
case 'ucs-2':
|
|
103
|
+
return encodeUtf16LE(data);
|
|
104
|
+
|
|
105
|
+
case 'ascii':
|
|
106
|
+
return encodeAscii(data);
|
|
107
|
+
|
|
108
|
+
case 'latin1':
|
|
109
|
+
return encodeLatin1(data);
|
|
110
|
+
|
|
111
|
+
case 'binary':
|
|
112
|
+
return Uint8Array.from(data, char => char.charCodeAt(0));
|
|
113
|
+
|
|
114
|
+
case 'base64':
|
|
115
|
+
return Uint8Array.from(atob(data), c => c.charCodeAt(0));
|
|
116
|
+
|
|
117
|
+
case 'hex':
|
|
118
|
+
if (!/^[\da-f]+$/i.test(data) || data.length % 2 !== 0) {
|
|
119
|
+
throw new ValidationError('format', 'Invalid hex string');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return Uint8Array.from(data.match(/.{1,2}/g)!.map(b => parseInt(b, 16)));
|
|
123
|
+
|
|
124
|
+
default:
|
|
125
|
+
console.warn('Encoding not supported, falling back to UTF-8');
|
|
126
|
+
|
|
127
|
+
return new TextEncoder().encode(data);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function decodeBuffer(buffer: Uint8Array, encoding: Encoding = 'utf-8'): string {
|
|
132
|
+
// eslint-disable-next-line ts/switch-exhaustiveness-check
|
|
133
|
+
switch (encoding) {
|
|
134
|
+
case 'utf8':
|
|
135
|
+
case 'utf-8':
|
|
136
|
+
return new TextDecoder().decode(buffer);
|
|
137
|
+
|
|
138
|
+
case 'utf16le':
|
|
139
|
+
case 'utf-16le':
|
|
140
|
+
case 'ucs2':
|
|
141
|
+
case 'ucs-2':
|
|
142
|
+
return decodeUtf16LE(buffer);
|
|
143
|
+
|
|
144
|
+
case 'latin1':
|
|
145
|
+
return String.fromCharCode(...buffer);
|
|
146
|
+
|
|
147
|
+
case 'ascii':
|
|
148
|
+
return String.fromCharCode(...buffer.map(b => b & 0x7F));
|
|
149
|
+
|
|
150
|
+
case 'base64':
|
|
151
|
+
return btoa(String.fromCharCode(...buffer));
|
|
152
|
+
|
|
153
|
+
case 'hex':
|
|
154
|
+
return Array.from(buffer).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
155
|
+
|
|
156
|
+
default:
|
|
157
|
+
console.warn('Unsupported encoding, falling back to UTF-8');
|
|
158
|
+
|
|
159
|
+
return new TextDecoder().decode(buffer);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function encodeUtf16LE(str: string): Uint8Array {
|
|
164
|
+
const buf = new Uint8Array(str.length * 2);
|
|
165
|
+
|
|
166
|
+
for (let i = 0; i < str.length; i++) {
|
|
167
|
+
const code = str.charCodeAt(i);
|
|
168
|
+
|
|
169
|
+
buf[(i * 2)] = code & 0xFF;
|
|
170
|
+
buf[(i * 2) + 1] = code >> 8;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return buf;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function decodeUtf16LE(buf: Uint8Array): string {
|
|
177
|
+
if (buf.length % 2 !== 0) {
|
|
178
|
+
console.warn('Invalid UTF-16LE buffer length, truncating last byte');
|
|
179
|
+
buf = buf.slice(0, buf.length - 1);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const codeUnits = new Uint16Array(buf.buffer, buf.byteOffset, buf.byteLength / 2);
|
|
183
|
+
|
|
184
|
+
return String.fromCharCode(...codeUnits);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function encodeLatin1(str: string): Uint8Array {
|
|
188
|
+
const buf = new Uint8Array(str.length);
|
|
189
|
+
|
|
190
|
+
for (let i = 0; i < str.length; i++) {
|
|
191
|
+
buf[i] = str.charCodeAt(i) & 0xFF;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return buf;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function encodeAscii(str: string): Uint8Array {
|
|
198
|
+
const buf = new Uint8Array(str.length);
|
|
199
|
+
|
|
200
|
+
for (let i = 0; i < str.length; i++) {
|
|
201
|
+
buf[i] = str.charCodeAt(i) & 0x7F;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return buf;
|
|
205
|
+
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error code to numeric errno mapping (Node.js compatible)
|
|
3
|
+
*/
|
|
4
|
+
const ERROR_CODE_TO_ERRNO: Record<string, number> = {
|
|
5
|
+
ENOENT: -2, // No such file or directory
|
|
6
|
+
EISDIR: -21, // Is a directory
|
|
7
|
+
ENOTDIR: -20, // Not a directory
|
|
8
|
+
EACCES: -13, // Permission denied
|
|
9
|
+
EEXIST: -17, // File exists
|
|
10
|
+
ENOTEMPTY: -39, // Directory not empty
|
|
11
|
+
EINVAL: -22, // Invalid argument
|
|
12
|
+
EIO: -5, // I/O error
|
|
13
|
+
ENOSPC: -28, // No space left on device
|
|
14
|
+
EBUSY: -16, // Device or resource busy
|
|
15
|
+
EINTR: -4, // Interrupted system call
|
|
16
|
+
ENOTSUP: -95, // Operation not supported
|
|
17
|
+
ERANGE: -34, // Result too large
|
|
18
|
+
EBADF: -9, // Bad file descriptor
|
|
19
|
+
EROOT: -1, // Custom: Cannot remove root directory
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Base error class for all OPFS-related errors (Node.js SystemError compatible)
|
|
24
|
+
*/
|
|
25
|
+
export class OPFSError extends Error {
|
|
26
|
+
public readonly errno: number;
|
|
27
|
+
public readonly syscall?: string;
|
|
28
|
+
public readonly path?: string;
|
|
29
|
+
|
|
30
|
+
constructor(
|
|
31
|
+
message: string,
|
|
32
|
+
code: string,
|
|
33
|
+
path?: string,
|
|
34
|
+
syscall?: string,
|
|
35
|
+
cause?: any
|
|
36
|
+
) {
|
|
37
|
+
super(message, { cause });
|
|
38
|
+
this.name = code;
|
|
39
|
+
this.errno = ERROR_CODE_TO_ERRNO[code] || -1;
|
|
40
|
+
this.path = path;
|
|
41
|
+
this.syscall = syscall;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Error thrown when OPFS is not supported in the current browser
|
|
47
|
+
*/
|
|
48
|
+
export class OPFSNotSupportedError extends OPFSError {
|
|
49
|
+
constructor(cause?: unknown) {
|
|
50
|
+
super('OPFS is not supported in this browser', 'ENOTSUP', undefined, undefined, cause);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Error thrown for invalid paths or path traversal attempts
|
|
56
|
+
*/
|
|
57
|
+
export class PathError extends OPFSError {
|
|
58
|
+
constructor(message: string, path: string, cause?: unknown) {
|
|
59
|
+
super(message, 'INVALID_PATH', path, 'access', cause);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Error thrown when files or directories don't exist
|
|
65
|
+
*/
|
|
66
|
+
export class ExistenceError extends OPFSError {
|
|
67
|
+
constructor(type: 'file' | 'directory' | 'source', path: string, cause?: unknown) {
|
|
68
|
+
const messages = {
|
|
69
|
+
file: `File not found: ${ path }`,
|
|
70
|
+
directory: `Directory not found: ${ path }`,
|
|
71
|
+
source: `Source does not exist: ${ path }`,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
super(messages[type], 'ENOENT', path, 'access', cause);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Error thrown when permission is denied for an operation
|
|
80
|
+
*/
|
|
81
|
+
export class PermissionError extends OPFSError {
|
|
82
|
+
constructor(path: string, operation: string, cause?: unknown) {
|
|
83
|
+
super(`Permission denied for ${ operation } on: ${ path }`, 'EACCES', path, operation, cause);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Error thrown when an operation fails due to insufficient storage
|
|
89
|
+
*/
|
|
90
|
+
export class StorageError extends OPFSError {
|
|
91
|
+
constructor(message: string, path?: string, cause?: unknown) {
|
|
92
|
+
super(message, 'ENOSPC', path, 'write', cause);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Error thrown when a file is busy (locked by another operation)
|
|
98
|
+
*/
|
|
99
|
+
export class FileBusyError extends OPFSError {
|
|
100
|
+
constructor(path: string, cause?: unknown) {
|
|
101
|
+
super(`File is busy: ${ path }`, 'EBUSY', path, 'open', cause);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Error thrown when file/directory type expectations don't match
|
|
107
|
+
*/
|
|
108
|
+
export class FileTypeError extends OPFSError {
|
|
109
|
+
constructor(actualType: 'file' | 'directory', path: string, cause?: unknown) {
|
|
110
|
+
const message = actualType === 'directory'
|
|
111
|
+
? `Is a directory: ${ path }`
|
|
112
|
+
: `Not a directory: ${ path }`;
|
|
113
|
+
const code = actualType === 'directory' ? 'EISDIR' : 'ENOTDIR';
|
|
114
|
+
|
|
115
|
+
super(message, code, path, 'access', cause);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Error thrown for validation failures (invalid arguments, formats, etc.)
|
|
121
|
+
*/
|
|
122
|
+
export class ValidationError extends OPFSError {
|
|
123
|
+
constructor(type: 'argument' | 'format' | 'descriptor' | 'overflow', message: string, path?: string, cause?: unknown) {
|
|
124
|
+
const codes = {
|
|
125
|
+
argument: 'EINVAL',
|
|
126
|
+
format: 'INVALID_FORMAT',
|
|
127
|
+
descriptor: 'EBADF',
|
|
128
|
+
overflow: 'ERANGE',
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
super(message, codes[type], path, 'validate', cause);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Error thrown when an operation is aborted
|
|
137
|
+
*/
|
|
138
|
+
export class OperationAbortedError extends OPFSError {
|
|
139
|
+
constructor(path: string, cause?: unknown) {
|
|
140
|
+
super(`Operation aborted: ${ path }`, 'EINTR', path, 'interrupt', cause);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Error thrown for I/O operation failures
|
|
146
|
+
*/
|
|
147
|
+
export class IOError extends OPFSError {
|
|
148
|
+
constructor(message: string, path?: string, cause?: unknown) {
|
|
149
|
+
super(message, 'EIO', path, 'io', cause);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Error thrown when an operation is not supported
|
|
155
|
+
*/
|
|
156
|
+
export class OperationNotSupportedError extends OPFSError {
|
|
157
|
+
constructor(path: string, cause?: unknown) {
|
|
158
|
+
super(`Operation not supported: ${ path }`, 'ENOTSUP', path, 'operation', cause);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Error thrown when directory operations fail
|
|
164
|
+
*/
|
|
165
|
+
export class DirectoryOperationError extends OPFSError {
|
|
166
|
+
constructor(code: string, path: string, cause?: unknown) {
|
|
167
|
+
const messages = {
|
|
168
|
+
RM_FAILED: `Failed to remove entry: ${ path }`,
|
|
169
|
+
ENOTEMPTY: `Directory not empty: ${ path }. Use recursive option to force removal.`,
|
|
170
|
+
EROOT: 'Cannot remove root directory',
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
super(messages[code as keyof typeof messages] || `Directory operation failed: ${ path }`, code, path, 'unlink', cause);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Error thrown when OPFS initialization fails
|
|
180
|
+
*/
|
|
181
|
+
export class InitializationFailedError extends OPFSError {
|
|
182
|
+
constructor(path: string, cause?: unknown) {
|
|
183
|
+
super('Failed to initialize OPFS', 'INIT_FAILED', path, 'init', cause);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Error thrown when file system operations fail
|
|
189
|
+
*/
|
|
190
|
+
export class FileSystemOperationError extends OPFSError {
|
|
191
|
+
constructor(operation: string, path: string, cause?: unknown) {
|
|
192
|
+
super(`Failed to ${ operation }: ${ path }`, `${ operation.toUpperCase() }_FAILED`, path, operation, cause);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Error thrown when a file or directory already exists
|
|
198
|
+
*/
|
|
199
|
+
export class AlreadyExistsError extends OPFSError {
|
|
200
|
+
constructor(path: string, cause?: unknown) {
|
|
201
|
+
super(`Destination already exists: ${ path }`, 'EEXIST', path, 'open', cause);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Create an OPFSError with file descriptor context
|
|
207
|
+
*
|
|
208
|
+
* @param operation - The operation that failed (e.g., 'read', 'write', 'close')
|
|
209
|
+
* @param fd - The file descriptor number
|
|
210
|
+
* @param path - The file path
|
|
211
|
+
* @param error - The underlying error (optional)
|
|
212
|
+
* @returns OPFSError with appropriate context
|
|
213
|
+
*/
|
|
214
|
+
export function createFDError(operation: string, fd: number, path: string, error?: any): OPFSError {
|
|
215
|
+
const errorCode = `${ operation.toUpperCase() }_FAILED` as 'READ_FAILED' | 'WRITE_FAILED' | 'CLOSE_FAILED';
|
|
216
|
+
|
|
217
|
+
return new OPFSError(`Failed to ${ operation } file descriptor: ${ fd }`, errorCode, path, operation, error);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Context for mapping DOM errors to OPFS errors
|
|
222
|
+
*/
|
|
223
|
+
export interface MapDomErrorContext {
|
|
224
|
+
/** File path for context-specific errors */
|
|
225
|
+
path?: string;
|
|
226
|
+
/** Whether the operation involves a directory (for TypeMismatchError) */
|
|
227
|
+
isDirectory?: boolean;
|
|
228
|
+
/** Type of existence for NotFoundError (default: 'file') */
|
|
229
|
+
existenceType?: 'file' | 'directory';
|
|
230
|
+
/** When 'remove', maps InvalidModificationError to ENOTEMPTY and default to RM_FAILED */
|
|
231
|
+
operation?: 'remove';
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Map DOM exceptions to OPFS error codes
|
|
236
|
+
*
|
|
237
|
+
* @param error - The DOM exception to map
|
|
238
|
+
* @param context - Context information for better error mapping
|
|
239
|
+
* @returns OPFSError with appropriate error code
|
|
240
|
+
*/
|
|
241
|
+
export function mapDomError(error: any, context?: MapDomErrorContext): OPFSError {
|
|
242
|
+
const path = context?.path;
|
|
243
|
+
const isDirectory = context?.isDirectory;
|
|
244
|
+
const existenceType = context?.existenceType ?? 'file';
|
|
245
|
+
const operation = context?.operation;
|
|
246
|
+
|
|
247
|
+
switch (error.name) {
|
|
248
|
+
case 'InvalidStateError':
|
|
249
|
+
return new FileBusyError(path || 'unknown', error);
|
|
250
|
+
|
|
251
|
+
case 'QuotaExceededError':
|
|
252
|
+
return new StorageError(`No space left on device: ${ path || 'unknown' }`, path, error);
|
|
253
|
+
|
|
254
|
+
case 'NotFoundError':
|
|
255
|
+
return new ExistenceError(existenceType, path!, error);
|
|
256
|
+
|
|
257
|
+
case 'TypeMismatchError':
|
|
258
|
+
if (isDirectory !== undefined) {
|
|
259
|
+
if (isDirectory) {
|
|
260
|
+
return new FileTypeError('directory', path || 'unknown', error);
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
return new FileTypeError('file', path || 'unknown', error);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Fall through to default for ambiguous cases
|
|
268
|
+
return new ValidationError('argument', `Type mismatch: ${ path || 'unknown' }`, path, error);
|
|
269
|
+
|
|
270
|
+
case 'NotAllowedError':
|
|
271
|
+
case 'SecurityError':
|
|
272
|
+
return new PermissionError(path!, 'unknown', error);
|
|
273
|
+
|
|
274
|
+
case 'InvalidModificationError':
|
|
275
|
+
if (operation === 'remove') {
|
|
276
|
+
return new DirectoryOperationError('ENOTEMPTY', path!, error);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return new ValidationError('argument', `Invalid modification: ${ path || 'unknown' }`, path, error);
|
|
280
|
+
|
|
281
|
+
case 'AbortError':
|
|
282
|
+
return new OperationAbortedError(path || 'unknown', error);
|
|
283
|
+
|
|
284
|
+
case 'OperationError':
|
|
285
|
+
return new IOError(`Operation failed: ${ path || 'unknown' }`, path, error);
|
|
286
|
+
|
|
287
|
+
case 'TypeError':
|
|
288
|
+
return new OperationNotSupportedError(path || 'unknown', error);
|
|
289
|
+
|
|
290
|
+
default:
|
|
291
|
+
if (operation === 'remove') {
|
|
292
|
+
return new DirectoryOperationError('RM_FAILED', path!, error);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return new IOError(`I/O error: ${ path || 'unknown' }`, path, error);
|
|
296
|
+
}
|
|
297
|
+
}
|