Skip to content

Commit d472e65

Browse files
committed
feat: US-011 - Add copy and readDirStat optimizations
1 parent 0cadea9 commit d472e65

7 files changed

Lines changed: 138 additions & 10 deletions

File tree

packages/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export { KernelError, defaultTermios, noopKernelLogger } from "./kernel/types.js
3737
export type {
3838
VirtualFileSystem,
3939
VirtualDirEntry,
40+
VirtualDirStatEntry,
4041
VirtualStat,
4142
} from "./kernel/vfs.js";
4243

packages/core/src/kernel/mount-table.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import { KernelError } from "./types.js";
10-
import type { VirtualDirEntry, VirtualFileSystem, VirtualStat } from "./vfs.js";
10+
import type { VirtualDirEntry, VirtualDirStatEntry, VirtualFileSystem, VirtualStat } from "./vfs.js";
1111

1212
export interface MountOptions {
1313
readOnly?: boolean;
@@ -382,6 +382,50 @@ export class MountTable implements VirtualFileSystem {
382382
await mount.fs.fsync?.(relativePath);
383383
}
384384

385+
async copy(srcPath: string, dstPath: string): Promise<void> {
386+
const srcResolved = this.resolve(srcPath);
387+
const dstResolved = this.resolve(dstPath);
388+
389+
if (srcResolved.mount !== dstResolved.mount) {
390+
throw new KernelError(
391+
"EXDEV",
392+
`copy across mounts: ${srcPath} -> ${dstPath}`,
393+
);
394+
}
395+
396+
this.assertWritable(srcResolved.mount, dstPath);
397+
398+
if (srcResolved.mount.fs.copy) {
399+
return srcResolved.mount.fs.copy(
400+
srcResolved.relativePath,
401+
dstResolved.relativePath,
402+
);
403+
}
404+
405+
// Fallback: readFile + writeFile.
406+
const content = await srcResolved.mount.fs.readFile(srcResolved.relativePath);
407+
await srcResolved.mount.fs.writeFile(dstResolved.relativePath, content);
408+
}
409+
410+
async readDirStat(path: string): Promise<VirtualDirStatEntry[]> {
411+
const { mount, relativePath } = this.resolve(path);
412+
413+
if (mount.fs.readDirStat) {
414+
return mount.fs.readDirStat(relativePath);
415+
}
416+
417+
// Fallback: readDirWithTypes + stat for each entry.
418+
const entries = await mount.fs.readDirWithTypes(relativePath);
419+
const normalized = normalizePath(path);
420+
const results: VirtualDirStatEntry[] = [];
421+
for (const entry of entries) {
422+
const entryPath = normalized === "/" ? `/${entry.name}` : `${normalized}/${entry.name}`;
423+
const stat = await mount.fs.stat(entryPath);
424+
results.push({ ...entry, stat });
425+
}
426+
return results;
427+
}
428+
385429
// -----------------------------------------------------------------------
386430
// Helpers
387431
// -----------------------------------------------------------------------

packages/core/src/kernel/vfs.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ export interface VirtualDirEntry {
1313
ino?: number;
1414
}
1515

16+
export interface VirtualDirStatEntry extends VirtualDirEntry {
17+
stat: VirtualStat;
18+
}
19+
1620
export interface VirtualStat {
1721
mode: number;
1822
size: number;
@@ -72,4 +76,10 @@ export interface VirtualFileSystem {
7276

7377
/** Flush buffered writes for the given path to durable storage. */
7478
fsync?(path: string): Promise<void>;
79+
80+
/** Copy a file within the same filesystem. */
81+
copy?(srcPath: string, dstPath: string): Promise<void>;
82+
83+
/** Combined readdir + stat. Avoids N+1 queries for directory listings. */
84+
readDirStat?(path: string): Promise<VirtualDirStatEntry[]>;
7585
}

packages/core/src/vfs/chunked-vfs.ts

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
*/
1313

1414
import { KernelError } from "../kernel/types.js";
15-
import type { VirtualDirEntry, VirtualFileSystem, VirtualStat } from "../kernel/vfs.js";
15+
import type { VirtualDirEntry, VirtualDirStatEntry, VirtualFileSystem, VirtualStat } from "../kernel/vfs.js";
1616
import type { FsBlockStore, FsMetadataStore, InodeMeta } from "./types.js";
1717

1818
// ---------------------------------------------------------------------------
@@ -1202,5 +1202,78 @@ export function createChunkedVfs(options: ChunkedVfsOptions): VirtualFileSystem
12021202
};
12031203
}
12041204

1205+
// Add copy method.
1206+
vfs.copy = async (srcPath: string, dstPath: string): Promise<void> => {
1207+
const srcIno = await resolveIno(srcPath);
1208+
const srcMeta = await requireInode(srcIno);
1209+
if (srcMeta.type === "directory") {
1210+
throw new KernelError("EISDIR", `illegal operation on a directory: '${srcPath}'`);
1211+
}
1212+
1213+
const { parentIno, name } = await ensureParents(dstPath);
1214+
const existingDstIno = await metadata.lookup(parentIno, name);
1215+
if (existingDstIno !== null) {
1216+
throw new KernelError("EEXIST", `file already exists: '${dstPath}'`);
1217+
}
1218+
1219+
await metadata.transaction(async () => {
1220+
const dstIno = await metadata.createInode({
1221+
type: "file",
1222+
mode: srcMeta.mode,
1223+
uid: srcMeta.uid,
1224+
gid: srcMeta.gid,
1225+
});
1226+
await metadata.createDentry(parentIno, name, dstIno, "file");
1227+
1228+
if (srcMeta.storageMode === "inline") {
1229+
const content = srcMeta.inlineContent
1230+
? new Uint8Array(srcMeta.inlineContent)
1231+
: new Uint8Array(0);
1232+
await metadata.updateInode(dstIno, {
1233+
nlink: 1,
1234+
size: srcMeta.size,
1235+
storageMode: "inline",
1236+
inlineContent: content,
1237+
});
1238+
} else {
1239+
// Chunked: copy each block.
1240+
const chunkEntries = await metadata.getAllChunkKeys(srcIno);
1241+
for (const entry of chunkEntries) {
1242+
const newKey = blockKey(dstIno, entry.chunkIndex);
1243+
if (blocks.copy) {
1244+
await blocks.copy(entry.key, newKey);
1245+
} else {
1246+
const data = await blocks.read(entry.key);
1247+
await blocks.write(newKey, data);
1248+
}
1249+
await metadata.setChunkKey(dstIno, entry.chunkIndex, newKey);
1250+
}
1251+
await metadata.updateInode(dstIno, {
1252+
nlink: 1,
1253+
size: srcMeta.size,
1254+
storageMode: "chunked",
1255+
inlineContent: null,
1256+
});
1257+
}
1258+
});
1259+
};
1260+
1261+
// Add readDirStat method.
1262+
vfs.readDirStat = async (path: string): Promise<VirtualDirStatEntry[]> => {
1263+
const ino = await resolveIno(path);
1264+
const meta = await requireInode(ino);
1265+
if (meta.type !== "directory") {
1266+
throw new KernelError("ENOTDIR", `not a directory: '${path}'`);
1267+
}
1268+
const entries = await metadata.listDirWithStats(ino);
1269+
return entries.map((e) => ({
1270+
name: e.name,
1271+
isDirectory: e.type === "directory",
1272+
isSymbolicLink: e.type === "symlink",
1273+
ino: e.ino,
1274+
stat: inodeMetaToStat(e.stat),
1275+
}));
1276+
};
1277+
12051278
return vfs;
12061279
}

packages/core/test/vfs/chunked-vfs-conformance.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ defineVfsConformanceTests({
2727
mkdir: true,
2828
removeDir: true,
2929
fsync: false,
30-
copy: false,
31-
readDirStat: false,
30+
copy: true,
31+
readDirStat: true,
3232
},
3333
inlineThreshold: INLINE_THRESHOLD,
3434
chunkSize: CHUNK_SIZE,
@@ -56,8 +56,8 @@ defineVfsConformanceTests({
5656
mkdir: true,
5757
removeDir: true,
5858
fsync: true,
59-
copy: false,
60-
readDirStat: false,
59+
copy: true,
60+
readDirStat: true,
6161
},
6262
inlineThreshold: INLINE_THRESHOLD,
6363
chunkSize: CHUNK_SIZE,

packages/core/test/vfs/host-chunked-vfs-conformance.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ defineVfsConformanceTests({
3939
mkdir: true,
4040
removeDir: true,
4141
fsync: false,
42-
copy: false,
43-
readDirStat: false,
42+
copy: true,
43+
readDirStat: true,
4444
},
4545
inlineThreshold: INLINE_THRESHOLD,
4646
chunkSize: CHUNK_SIZE,

packages/core/test/vfs/sqlite-chunked-vfs-conformance.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ defineVfsConformanceTests({
2727
mkdir: true,
2828
removeDir: true,
2929
fsync: false,
30-
copy: false,
31-
readDirStat: false,
30+
copy: true,
31+
readDirStat: true,
3232
},
3333
inlineThreshold: INLINE_THRESHOLD,
3434
chunkSize: CHUNK_SIZE,

0 commit comments

Comments
 (0)