Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 | 1x 1x 1x 52x 1x 52x 52x 52x 1x 267x 267x 151x 151x 151x 12x 12x 151x 255x 267x 1x 158x 158x 158x 154x 154x 158x 6x 6x 158x 1x 1x 1x 1x 28x 28x 26x 26x 28x 46x 46x 46x 46x 46x 46x 26x 28x 1x 49x 49x 39x 49x 1x 72x 72x 72x 72x 72x 72x 72x 72x 72x 1x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 1x 14x 14x 14x 14x 1x 6x 6x 6x 4x 2x 6x 6x 1x 4x 4x 4x 4x 4x 4x 4x 4x 4x 1x 20x 20x 20x 20x 1x 8x 8x 8x 8x 8x 8x 2x 8x 1x 4x 4x 4x 4x 4x 4x 4x 4x 4x 2x 2x 2x 2x 2x 4x 1x 70x 70x 68x 70x 62x 62x 62x 43x 43x 67x 19x 19x 19x 5x 70x 14x 14x 70x 1x | import { basename, dirname, normalizePath } from '../path.js';
import type { ByteRange, VFSAdapter, VFSListEntry, VFSStat } from '../types.js';
type DirEntries = AsyncIterable<[string, FileSystemHandle]>;
interface MovableHandle {
move?: (parent: FileSystemDirectoryHandle, name: string) => Promise<void>;
}
/**
* Shared implementation for the two handle-based browser backends: OPFS and
* the File System Access API expose the same `FileSystemDirectoryHandle`
* model, they differ only in how the root handle is obtained and in whether
* permissions can expire.
*/
export class HandleAdapter implements VFSAdapter {
readonly name: string;
readonly root: FileSystemDirectoryHandle;
constructor(root: FileSystemDirectoryHandle, name = root.name || 'handle') {
this.root = root;
this.name = name;
}
protected async dir(path: string, create = false): Promise<FileSystemDirectoryHandle | null> {
let handle = this.root;
for (const segment of normalizePath(path).split('/').filter(Boolean)) {
try {
handle = await handle.getDirectoryHandle(segment, { create });
} catch {
return null;
}
}
return handle;
}
protected async file(path: string, create = false): Promise<FileSystemFileHandle | null> {
const target = normalizePath(path);
const parent = await this.dir(dirname(target), create);
if (!parent) return null;
try {
return await parent.getFileHandle(basename(target), { create });
} catch {
return null;
}
}
async mkdir(path: string): Promise<void> {
await this.dir(path, true);
}
async list(path: string): Promise<VFSListEntry[]> {
const dir = await this.dir(path);
if (!dir) return [];
const base = normalizePath(path);
const out: VFSListEntry[] = [];
for await (const [name, handle] of dir as unknown as DirEntries) {
out.push({
name,
path: base ? `${base}/${name}` : name,
kind: handle.kind === 'directory' ? 'directory' : 'file',
});
}
return out;
}
async read(path: string): Promise<Uint8Array> {
const handle = await this.file(path);
if (!handle) throw new Error(`ENOENT: ${path}`);
return new Uint8Array(await (await handle.getFile()).arrayBuffer());
}
async write(path: string, data: Uint8Array): Promise<void> {
const handle = await this.file(path, true);
if (!handle) throw new Error(`cannot write ${path}`);
const writable = await handle.createWritable();
try {
await writable.write(data as unknown as BufferSource);
} finally {
await writable.close();
}
}
/**
* Native append: `keepExistingData` plus a seek to the end writes only the
* new bytes, instead of rewriting the file. This is what keeps extending the
* commit log cheap on OPFS and FSA.
*/
async append(path: string, data: Uint8Array): Promise<void> {
const handle = await this.file(path, true);
if (!handle) throw new Error(`cannot write ${path}`);
const size = (await handle.getFile()).size;
const writable = await handle.createWritable({ keepExistingData: true });
try {
await writable.write({ type: 'write', position: size, data: data as unknown as BufferSource });
} finally {
await writable.close();
}
}
/**
* `File` is a `Blob`, and slicing one is lazy: only the requested range is
* ever pulled off disk. This is the cheap path for reading a header or a
* trailer out of a large file.
*/
async readRange(path: string, range: ByteRange = {}): Promise<Uint8Array> {
const file = await this.getFile(path);
const blob = file.slice(range.start ?? 0, range.end ?? file.size);
return new Uint8Array(await blob.arrayBuffer());
}
async readStream(path: string, range: ByteRange = {}): Promise<ReadableStream<Uint8Array>> {
const file = await this.getFile(path);
const ranged =
range.start !== undefined || range.end !== undefined
? file.slice(range.start ?? 0, range.end ?? file.size)
: file;
return ranged.stream();
}
/**
* Wraps the handle's own writable rather than returning it: the two are the
* same thing on a real browser, but wrapping keeps the contract to a plain
* `WritableStream` and works with backends whose writable is only
* write/close shaped.
*/
async writeStream(path: string): Promise<WritableStream<Uint8Array>> {
const handle = await this.file(path, true);
if (!handle) throw new Error(`cannot write ${path}`);
const writable = await handle.createWritable();
return new WritableStream<Uint8Array>({
write: (chunk) => writable.write(chunk as unknown as BufferSource),
close: () => writable.close(),
abort: (reason) => writable.abort(reason),
});
}
private async getFile(path: string): Promise<File> {
const handle = await this.file(path);
if (!handle) throw new Error(`ENOENT: ${path}`);
return handle.getFile();
}
async delete(path: string): Promise<void> {
const target = normalizePath(path);
const parent = await this.dir(dirname(target));
if (!parent) return;
try {
await parent.removeEntry(basename(target), { recursive: true });
} catch {
// already gone
}
}
async rename(oldPath: string, newPath: string): Promise<void> {
const from = normalizePath(oldPath);
const to = normalizePath(newPath);
if (from === to) return;
const handle = await this.file(from);
if (!handle) throw new Error(`ENOENT: ${oldPath}`);
const targetDir = await this.dir(dirname(to), true);
if (!targetDir) throw new Error(`cannot create ${dirname(to)}`);
const movable = handle as FileSystemFileHandle & MovableHandle;
if (typeof movable.move === 'function') {
await movable.move(targetDir, basename(to));
return;
}
// No native move (Safari, older Chrome): fall back to copy + delete. The
// engine still sees a rename because intent was recorded by VFSNode.
await this.write(to, await this.read(from));
await this.delete(from);
}
async stat(path: string): Promise<VFSStat | null> {
const target = normalizePath(path);
if (target === '') return { kind: 'directory', size: 0, mtime: 0 };
const parent = await this.dir(dirname(target));
if (!parent) return null;
const name = basename(target);
try {
const handle = await parent.getFileHandle(name);
const file = await handle.getFile();
return { kind: 'file', size: file.size, mtime: file.lastModified };
} catch {
// not a file — maybe a directory
}
try {
await parent.getDirectoryHandle(name);
return { kind: 'directory', size: 0, mtime: 0 };
} catch {
return null;
}
}
}
|