@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/src/errors.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom error class for filesystem errors
|
|
3
|
+
*/
|
|
4
|
+
export class FSError extends Error {
|
|
5
|
+
code: string
|
|
6
|
+
syscall?: string
|
|
7
|
+
path?: string
|
|
8
|
+
original?: Error
|
|
9
|
+
|
|
10
|
+
constructor(message: string, code: string, options?: { syscall?: string; path?: string; original?: Error }) {
|
|
11
|
+
super(message)
|
|
12
|
+
this.name = 'FSError'
|
|
13
|
+
this.code = code
|
|
14
|
+
this.syscall = options?.syscall
|
|
15
|
+
this.path = options?.path
|
|
16
|
+
this.original = options?.original
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Create ENOENT (No such file or directory) error
|
|
22
|
+
*/
|
|
23
|
+
export function createENOENT(path: string): FSError {
|
|
24
|
+
return new FSError(`ENOENT: No such file or directory, '${path}'`, 'ENOENT', { path })
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Create EEXIST (File exists) error
|
|
29
|
+
*/
|
|
30
|
+
export function createEEXIST(path: string, operation?: string): FSError {
|
|
31
|
+
const message = operation
|
|
32
|
+
? `EEXIST: file already exists, ${operation} '${path}'`
|
|
33
|
+
: `EEXIST: File exists, '${path}'`
|
|
34
|
+
return new FSError(message, 'EEXIST', { path })
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Create EACCES (Permission denied) error
|
|
39
|
+
*/
|
|
40
|
+
export function createEACCES(path: string, syscall?: string): FSError {
|
|
41
|
+
return new FSError(`EACCES: permission denied, access '${path}'`, 'EACCES', { syscall, path })
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Create EISDIR (Is a directory) error
|
|
46
|
+
*/
|
|
47
|
+
export function createEISDIR(path: string, operation = 'operation'): FSError {
|
|
48
|
+
return new FSError(`EISDIR: illegal operation on a directory, ${operation} '${path}'`, 'EISDIR', { path })
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Create ELOOP (Too many symbolic links) error
|
|
53
|
+
*/
|
|
54
|
+
export function createELOOP(path: string): FSError {
|
|
55
|
+
return new FSError(`ELOOP: Too many symbolic links, '${path}'`, 'ELOOP', { path })
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Create EINVAL (Invalid argument) error
|
|
60
|
+
*/
|
|
61
|
+
export function createEINVAL(path: string): FSError {
|
|
62
|
+
return new FSError(`EINVAL: Invalid argument, '${path}'`, 'EINVAL', { path })
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Create ECORRUPTED (Data corruption detected) error
|
|
67
|
+
*/
|
|
68
|
+
export function createECORRUPTED(path: string): FSError {
|
|
69
|
+
return new FSError(`ECORRUPTED: Pack file integrity check failed, '${path}'`, 'ECORRUPTED', { path })
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Wrap an error with a standard code if it doesn't have one
|
|
74
|
+
*/
|
|
75
|
+
export function wrapError(err: unknown): FSError {
|
|
76
|
+
if (err instanceof FSError) return err
|
|
77
|
+
|
|
78
|
+
const error = err as Error
|
|
79
|
+
if (typeof (error as FSError).code === 'string') {
|
|
80
|
+
const fsErr = new FSError(error.message, (error as FSError).code)
|
|
81
|
+
fsErr.original = error
|
|
82
|
+
return fsErr
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const wrapped = new FSError(error.message || 'Unknown error', 'UNKNOWN')
|
|
86
|
+
wrapped.original = error
|
|
87
|
+
return wrapped
|
|
88
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { FileHandle, ReadResult, WriteResult, Stats, ReadFileOptions, WriteFileOptions } from './types.js'
|
|
2
|
+
|
|
3
|
+
export interface FileHandleContext {
|
|
4
|
+
readFile(path: string, options?: ReadFileOptions): Promise<string | Uint8Array>
|
|
5
|
+
writeFile(path: string, data: string | Uint8Array, options?: WriteFileOptions): Promise<void>
|
|
6
|
+
appendFile(path: string, data: string | Uint8Array, options?: WriteFileOptions): Promise<void>
|
|
7
|
+
stat(path: string): Promise<Stats>
|
|
8
|
+
truncate(path: string, len: number): Promise<void>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Create a FileHandle-like object for the open() method
|
|
13
|
+
*/
|
|
14
|
+
export function createFileHandle(
|
|
15
|
+
resolvedPath: string,
|
|
16
|
+
initialPosition: number,
|
|
17
|
+
context: FileHandleContext
|
|
18
|
+
): FileHandle {
|
|
19
|
+
let position = initialPosition
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
fd: Math.floor(Math.random() * 1000000),
|
|
23
|
+
|
|
24
|
+
async read(
|
|
25
|
+
buffer: Uint8Array,
|
|
26
|
+
offset = 0,
|
|
27
|
+
length = buffer.length,
|
|
28
|
+
pos: number | null = null
|
|
29
|
+
): Promise<ReadResult> {
|
|
30
|
+
const readPos = pos !== null ? pos : position
|
|
31
|
+
const data = await context.readFile(resolvedPath) as Uint8Array
|
|
32
|
+
const bytesToRead = Math.min(length, data.length - readPos)
|
|
33
|
+
buffer.set(data.subarray(readPos, readPos + bytesToRead), offset)
|
|
34
|
+
if (pos === null) position += bytesToRead
|
|
35
|
+
return { bytesRead: bytesToRead, buffer }
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
async write(
|
|
39
|
+
buffer: Uint8Array,
|
|
40
|
+
offset = 0,
|
|
41
|
+
length = buffer.length,
|
|
42
|
+
pos: number | null = null
|
|
43
|
+
): Promise<WriteResult> {
|
|
44
|
+
const writePos = pos !== null ? pos : position
|
|
45
|
+
let existingData = new Uint8Array(0)
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
existingData = await context.readFile(resolvedPath) as Uint8Array
|
|
49
|
+
} catch (e) {
|
|
50
|
+
if ((e as { code?: string }).code !== 'ENOENT') throw e
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const dataToWrite = buffer.subarray(offset, offset + length)
|
|
54
|
+
const newSize = Math.max(existingData.length, writePos + length)
|
|
55
|
+
const newData = new Uint8Array(newSize)
|
|
56
|
+
newData.set(existingData, 0)
|
|
57
|
+
newData.set(dataToWrite, writePos)
|
|
58
|
+
|
|
59
|
+
await context.writeFile(resolvedPath, newData)
|
|
60
|
+
if (pos === null) position += length
|
|
61
|
+
return { bytesWritten: length, buffer }
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
async close(): Promise<void> {
|
|
65
|
+
// No-op for OPFS
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
async stat(): Promise<Stats> {
|
|
69
|
+
return context.stat(resolvedPath)
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
async truncate(len = 0): Promise<void> {
|
|
73
|
+
return context.truncate(resolvedPath, len)
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
async sync(): Promise<void> {
|
|
77
|
+
// No-op for OPFS (writes are already persisted)
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
async datasync(): Promise<void> {
|
|
81
|
+
// No-op for OPFS
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
async readFile(options?: ReadFileOptions): Promise<string | Uint8Array> {
|
|
85
|
+
return context.readFile(resolvedPath, options)
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
async writeFile(data: string | Uint8Array, options?: WriteFileOptions): Promise<void> {
|
|
89
|
+
return context.writeFile(resolvedPath, data, options)
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
async appendFile(data: string | Uint8Array, options?: WriteFileOptions): Promise<void> {
|
|
93
|
+
return context.appendFile(resolvedPath, data, options)
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
[Symbol.asyncDispose]: async function(): Promise<void> {
|
|
97
|
+
// No-op for OPFS
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
package/src/global.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// File System Access API type declarations
|
|
2
|
+
|
|
3
|
+
interface FileSystemSyncAccessHandle {
|
|
4
|
+
read(buffer: ArrayBufferView, options?: { at?: number }): number
|
|
5
|
+
write(buffer: ArrayBufferView, options?: { at?: number }): number
|
|
6
|
+
truncate(newSize: number): void
|
|
7
|
+
getSize(): number
|
|
8
|
+
flush(): void
|
|
9
|
+
close(): void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface FileSystemFileHandle {
|
|
13
|
+
readonly kind: 'file'
|
|
14
|
+
readonly name: string
|
|
15
|
+
getFile(): Promise<File>
|
|
16
|
+
createWritable(options?: { keepExistingData?: boolean }): Promise<FileSystemWritableFileStream>
|
|
17
|
+
createSyncAccessHandle(): Promise<FileSystemSyncAccessHandle>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface FileSystemDirectoryHandle {
|
|
21
|
+
readonly kind: 'directory'
|
|
22
|
+
readonly name: string
|
|
23
|
+
getFileHandle(name: string, options?: { create?: boolean }): Promise<FileSystemFileHandle>
|
|
24
|
+
getDirectoryHandle(name: string, options?: { create?: boolean }): Promise<FileSystemDirectoryHandle>
|
|
25
|
+
removeEntry(name: string, options?: { recursive?: boolean }): Promise<void>
|
|
26
|
+
resolve(possibleDescendant: FileSystemHandle): Promise<string[] | null>
|
|
27
|
+
entries(): AsyncIterableIterator<[string, FileSystemHandle]>
|
|
28
|
+
keys(): AsyncIterableIterator<string>
|
|
29
|
+
values(): AsyncIterableIterator<FileSystemHandle>
|
|
30
|
+
[Symbol.asyncIterator](): AsyncIterableIterator<[string, FileSystemHandle]>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface FileSystemWritableFileStream extends WritableStream {
|
|
34
|
+
write(data: ArrayBuffer | ArrayBufferView | Blob | string | WriteParams): Promise<void>
|
|
35
|
+
seek(position: number): Promise<void>
|
|
36
|
+
truncate(size: number): Promise<void>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface WriteParams {
|
|
40
|
+
type: 'write' | 'seek' | 'truncate'
|
|
41
|
+
data?: ArrayBuffer | ArrayBufferView | Blob | string
|
|
42
|
+
position?: number
|
|
43
|
+
size?: number
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type FileSystemHandle = FileSystemFileHandle | FileSystemDirectoryHandle
|
|
47
|
+
|
|
48
|
+
interface StorageManager {
|
|
49
|
+
getDirectory(): Promise<FileSystemDirectoryHandle>
|
|
50
|
+
estimate(): Promise<StorageEstimate>
|
|
51
|
+
persist(): Promise<boolean>
|
|
52
|
+
persisted(): Promise<boolean>
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface Navigator {
|
|
56
|
+
storage: StorageManager
|
|
57
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { normalize, segments, dirname } from './path-utils.js'
|
|
2
|
+
import { createENOENT } from './errors.js'
|
|
3
|
+
|
|
4
|
+
export interface HandleResult {
|
|
5
|
+
dir: FileSystemDirectoryHandle
|
|
6
|
+
name: string
|
|
7
|
+
fileHandle: FileSystemFileHandle | null
|
|
8
|
+
dirHandle: FileSystemDirectoryHandle | null
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface GetHandleOptions {
|
|
12
|
+
create?: boolean
|
|
13
|
+
kind?: 'file' | 'directory'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const FILE_HANDLE_POOL_SIZE = 50
|
|
17
|
+
const DIR_CACHE_MAX_SIZE = 200
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Manages OPFS handles with caching for improved performance
|
|
21
|
+
*/
|
|
22
|
+
export class HandleManager {
|
|
23
|
+
private rootPromise: Promise<FileSystemDirectoryHandle>
|
|
24
|
+
private dirCache: Map<string, FileSystemDirectoryHandle> = new Map()
|
|
25
|
+
private fileHandlePool: Map<string, FileSystemFileHandle> = new Map()
|
|
26
|
+
|
|
27
|
+
constructor() {
|
|
28
|
+
this.rootPromise = navigator.storage.getDirectory()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get the root directory handle
|
|
33
|
+
*/
|
|
34
|
+
async getRoot(): Promise<FileSystemDirectoryHandle> {
|
|
35
|
+
return this.rootPromise
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Cache a directory handle with LRU eviction
|
|
40
|
+
*/
|
|
41
|
+
private cacheDirHandle(path: string, handle: FileSystemDirectoryHandle): void {
|
|
42
|
+
if (this.dirCache.size >= DIR_CACHE_MAX_SIZE) {
|
|
43
|
+
// Delete oldest entry (first key in Map maintains insertion order)
|
|
44
|
+
const firstKey = this.dirCache.keys().next().value
|
|
45
|
+
if (firstKey) this.dirCache.delete(firstKey)
|
|
46
|
+
}
|
|
47
|
+
this.dirCache.set(path, handle)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Clear directory cache for a path and its children
|
|
52
|
+
*/
|
|
53
|
+
clearCache(path = ''): void {
|
|
54
|
+
const normalizedPath = normalize(path)
|
|
55
|
+
|
|
56
|
+
// For root path, just clear everything
|
|
57
|
+
if (normalizedPath === '/' || normalizedPath === '') {
|
|
58
|
+
this.dirCache.clear()
|
|
59
|
+
this.fileHandlePool.clear()
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Clear directory cache
|
|
64
|
+
if (this.dirCache.size > 0) {
|
|
65
|
+
for (const key of this.dirCache.keys()) {
|
|
66
|
+
if (key === normalizedPath || key.startsWith(normalizedPath + '/')) {
|
|
67
|
+
this.dirCache.delete(key)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Clear file handle pool for affected paths
|
|
73
|
+
if (this.fileHandlePool.size > 0) {
|
|
74
|
+
for (const key of this.fileHandlePool.keys()) {
|
|
75
|
+
if (key === normalizedPath || key.startsWith(normalizedPath + '/')) {
|
|
76
|
+
this.fileHandlePool.delete(key)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get a file handle from the pool or create a new one
|
|
84
|
+
*/
|
|
85
|
+
async getPooledFileHandle(path: string, create = false): Promise<FileSystemFileHandle | null> {
|
|
86
|
+
const normalizedPath = normalize(path)
|
|
87
|
+
|
|
88
|
+
// Check pool first
|
|
89
|
+
const pooled = this.fileHandlePool.get(normalizedPath)
|
|
90
|
+
if (pooled) {
|
|
91
|
+
return pooled
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Get handle the normal way
|
|
95
|
+
const { fileHandle } = await this.getHandle(normalizedPath, { create })
|
|
96
|
+
if (!fileHandle) return null
|
|
97
|
+
|
|
98
|
+
// Add to pool with LRU eviction
|
|
99
|
+
if (this.fileHandlePool.size >= FILE_HANDLE_POOL_SIZE) {
|
|
100
|
+
// Delete oldest entry (first key in Map maintains insertion order)
|
|
101
|
+
const firstKey = this.fileHandlePool.keys().next().value
|
|
102
|
+
if (firstKey) this.fileHandlePool.delete(firstKey)
|
|
103
|
+
}
|
|
104
|
+
this.fileHandlePool.set(normalizedPath, fileHandle)
|
|
105
|
+
|
|
106
|
+
return fileHandle
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Invalidate a specific file handle from the pool
|
|
111
|
+
*/
|
|
112
|
+
invalidateFileHandle(path: string): void {
|
|
113
|
+
const normalizedPath = normalize(path)
|
|
114
|
+
this.fileHandlePool.delete(normalizedPath)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get file or directory handle for a path
|
|
119
|
+
*/
|
|
120
|
+
async getHandle(path: string, opts: GetHandleOptions = {}): Promise<HandleResult> {
|
|
121
|
+
// Use segments() for optimized path parsing (leverages normalize cache)
|
|
122
|
+
const parts = segments(path)
|
|
123
|
+
|
|
124
|
+
// Handle root or empty path
|
|
125
|
+
if (parts.length === 0) {
|
|
126
|
+
const root = await this.rootPromise
|
|
127
|
+
return { dir: root, name: '', fileHandle: null, dirHandle: root }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let dir = await this.rootPromise
|
|
131
|
+
let currentPath = ''
|
|
132
|
+
|
|
133
|
+
// Navigate to parent directory using cache
|
|
134
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
135
|
+
currentPath += '/' + parts[i]
|
|
136
|
+
|
|
137
|
+
// Check cache first for better performance
|
|
138
|
+
if (this.dirCache.has(currentPath)) {
|
|
139
|
+
dir = this.dirCache.get(currentPath)!
|
|
140
|
+
continue
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
dir = await dir.getDirectoryHandle(parts[i], { create: opts.create })
|
|
145
|
+
this.cacheDirHandle(currentPath, dir)
|
|
146
|
+
} catch {
|
|
147
|
+
throw createENOENT(path)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const name = parts[parts.length - 1]
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
if (opts.kind === 'directory') {
|
|
155
|
+
const dirHandle = await dir.getDirectoryHandle(name, { create: opts.create })
|
|
156
|
+
return { dir, name, fileHandle: null, dirHandle }
|
|
157
|
+
} else {
|
|
158
|
+
const fileHandle = await dir.getFileHandle(name, { create: opts.create })
|
|
159
|
+
return { dir, name, fileHandle, dirHandle: null }
|
|
160
|
+
}
|
|
161
|
+
} catch {
|
|
162
|
+
if (!opts.create) {
|
|
163
|
+
return { dir, name, fileHandle: null, dirHandle: null }
|
|
164
|
+
}
|
|
165
|
+
throw createENOENT(path)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Get directory handle with caching
|
|
171
|
+
*/
|
|
172
|
+
async getDirectoryHandle(path: string): Promise<FileSystemDirectoryHandle> {
|
|
173
|
+
const normalizedPath = normalize(path)
|
|
174
|
+
|
|
175
|
+
if (normalizedPath === '/' || normalizedPath === '') {
|
|
176
|
+
return this.rootPromise
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Check cache first
|
|
180
|
+
if (this.dirCache.has(normalizedPath)) {
|
|
181
|
+
return this.dirCache.get(normalizedPath)!
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const parts = segments(normalizedPath)
|
|
185
|
+
let dir = await this.rootPromise
|
|
186
|
+
let currentPath = ''
|
|
187
|
+
|
|
188
|
+
for (const part of parts) {
|
|
189
|
+
currentPath += '/' + part
|
|
190
|
+
|
|
191
|
+
if (this.dirCache.has(currentPath)) {
|
|
192
|
+
dir = this.dirCache.get(currentPath)!
|
|
193
|
+
continue
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
dir = await dir.getDirectoryHandle(part)
|
|
197
|
+
this.cacheDirHandle(currentPath, dir)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return dir
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Ensure parent directory exists
|
|
205
|
+
*/
|
|
206
|
+
async ensureParentDir(path: string): Promise<void> {
|
|
207
|
+
const parentPath = dirname(path)
|
|
208
|
+
if (parentPath === '/' || parentPath === '') return
|
|
209
|
+
|
|
210
|
+
const parts = segments(parentPath)
|
|
211
|
+
let dir = await this.rootPromise
|
|
212
|
+
let currentPath = ''
|
|
213
|
+
|
|
214
|
+
for (const part of parts) {
|
|
215
|
+
currentPath += '/' + part
|
|
216
|
+
|
|
217
|
+
// Check cache first for better performance
|
|
218
|
+
if (this.dirCache.has(currentPath)) {
|
|
219
|
+
dir = this.dirCache.get(currentPath)!
|
|
220
|
+
continue
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
dir = await dir.getDirectoryHandle(part, { create: true })
|
|
224
|
+
this.cacheDirHandle(currentPath, dir)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Create directory (with automatic parent creation)
|
|
230
|
+
*/
|
|
231
|
+
async mkdir(path: string): Promise<void> {
|
|
232
|
+
const normalizedPath = normalize(path)
|
|
233
|
+
this.clearCache(normalizedPath)
|
|
234
|
+
|
|
235
|
+
const parts = segments(normalizedPath)
|
|
236
|
+
let dir = await this.rootPromise
|
|
237
|
+
|
|
238
|
+
for (let i = 0; i < parts.length; i++) {
|
|
239
|
+
const part = parts[i]
|
|
240
|
+
const subPath = '/' + parts.slice(0, i + 1).join('/')
|
|
241
|
+
|
|
242
|
+
if (this.dirCache.has(subPath)) {
|
|
243
|
+
dir = this.dirCache.get(subPath)!
|
|
244
|
+
} else {
|
|
245
|
+
dir = await dir.getDirectoryHandle(part, { create: true })
|
|
246
|
+
this.cacheDirHandle(subPath, dir)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|