refactor(vfs): reuse combined directory metadata
CI / format (pull_request) Successful in 2m21s
CI / lint (pull_request) Successful in 2m35s
CI / install (pull_request) Successful in 9m0s
CI / typetest (pull_request) Successful in 2m35s
CI / node-tests (pull_request) Successful in 3m4s
CI / typecheck (pull_request) Successful in 3m13s
CI / browser-tests (pull_request) Successful in 4m46s

Co-Authored-By: gpt-5.6-sol <noreply@openai.com>
This commit is contained in:
2026-08-04 00:51:42 +00:00
co-authored by Codex
parent e21ad5f24f
commit 93b2dff943
5 changed files with 60 additions and 12 deletions
+1
View File
@@ -137,3 +137,4 @@ The test suite for `packages/http` was mostly generated by Claude Code, which al
- **`@webnet/taildrive` — share-bearing peer discovery**: `gpt-5.6-sol` added a Webnet-only `listDrivePeersWithShares(ipn)` helper that probes the existing candidates through the shared WebDAV client, positively retains peers exporting at least one share, bounds concurrent and malformed responses, and exposes a reusable per-peer `DAVClient` factory for a later whole-tailnet `AsyncVFS` hierarchy. Focused tests cover populated, empty, invalid, unreachable, and ordered peer results.
- **`@webnet/taildrive` — peer-probe review hardening**: `gpt-5.6-sol` addressed autonomous review findings by covering the IPN dial phase with the probe deadline, closing a connection that arrives after timeout, and requiring a valid root-directory response before accepting a nonempty WebDAV listing. Regression coverage includes a permanently stalled dial and a malformed 207 response that omits the requested root.
- **`@webnet/taildrive` — discovery API review follow-up**: `gpt-5.6-sol` renamed the narrower discovery helper to `listDrivePeersWithShares` to distinguish it from `IpnClient.listDrivePeers`, and added the conventional package-root export alongside the existing client and server subpaths.
- **`@webnet/vfs` — combined stat and directory listing**: `gpt-5.6-sol` added the optional `AsyncVFS.statAndReaddir()` operation, with optimized Memory, File System Access, WebDAV, and SMB2 implementations. Drive PROPFIND/COPY/MOVE and delete handling, FTP server listings, SMB2 recursive operations, and Taildrive peer discovery reuse combined metadata where available while retaining inline fallbacks for other VFS implementations. Conformance, browser, protocol, malformed-response, request-count, and integration coverage preserve existing `readdir()` semantics and verify files return their own stat with no entries.
+16 -1
View File
@@ -659,7 +659,20 @@ suite("DAV server HTTP", () => {
const vfs = new MemoryVFS()
await vfs.mkdir("/src")
await vfs.writeFile("/src/f.txt", streamOf("x"))
const { fetch, close } = makeHttpPair(minimalVfs(vfs))
const statPaths: string[] = []
const combinedPaths: string[] = []
const fallback: AsyncVFS = {
...minimalVfs(vfs),
stat: (path) => {
statPaths.push(path)
return vfs.stat(path)
},
statAndReaddir: (path) => {
combinedPaths.push(path)
return vfs.statAndReaddir(path)
},
}
const { fetch, close } = makeHttpPair(fallback)
try {
const res = await fetch("http://localhost/src", {
method: "COPY",
@@ -668,6 +681,8 @@ suite("DAV server HTTP", () => {
assert.equal(res.status, 201)
if (res.hasBody) await res.bytes()
assert.equal(await readAllText(await vfs.readFile("/dst/f.txt")), "x")
assert.deepEqual(combinedPaths, ["/src"])
assert.deepEqual(statPaths, ["/dst"])
} finally {
await close()
}
+19 -9
View File
@@ -1,26 +1,38 @@
import type { Context } from "@webnet/http/server"
import type { AsyncVFS, Stat } from "@webnet/vfs"
import type { AsyncVFS, Stat, StatAndReaddirResult } from "@webnet/vfs"
import { VFSError } from "@webnet/vfs"
import { hrefToVfsPath } from "../../common/utils.js"
import type { DAVServerOptions } from "../types.js"
async function statAndReaddir(vfs: AsyncVFS, path: string): Promise<StatAndReaddirResult> {
if (vfs.statAndReaddir) return vfs.statAndReaddir(path)
const self = await vfs.stat(path)
return { self, entries: self.isDirectory ? await vfs.readdir(path) : [] }
}
async function copyRecursive(
vfs: AsyncVFS,
src: string,
dest: string,
overwrite: boolean,
stat: Stat,
knownStat?: Stat,
): Promise<void> {
let stat = knownStat
let entries: Stat[] | undefined
if (!stat) {
const result = await statAndReaddir(vfs, src)
stat = result.self
entries = result.entries
}
if (stat.isDirectory) {
try {
await vfs.mkdir(dest)
} catch (e) {
if (!(e instanceof VFSError && e.code === "already-exists")) throw e
}
const children = await vfs.readdir(src)
const children = entries ?? (await vfs.readdir(src))
for (const child of children) {
const childDest = dest + "/" + child.name
await copyRecursive(vfs, child.path, childDest, overwrite, child)
await copyRecursive(vfs, child.path, childDest, child)
}
} else {
const stream = await vfs.readFile(src)
@@ -73,19 +85,17 @@ export async function handleCopyMove(
await vfs.delete(destVfsPath, true)
}
const srcStat = await vfs.stat(vfsPath)
if (method === "COPY") {
if (vfs.copy) {
await vfs.copy(vfsPath, destVfsPath, { overwrite })
} else {
await copyRecursive(vfs, vfsPath, destVfsPath, overwrite, srcStat)
await copyRecursive(vfs, vfsPath, destVfsPath)
}
} else {
if (vfs.move) {
await vfs.move(vfsPath, destVfsPath, { overwrite })
} else {
await copyRecursive(vfs, vfsPath, destVfsPath, overwrite, srcStat)
await copyRecursive(vfs, vfsPath, destVfsPath)
await vfs.delete(vfsPath, true)
}
}
+2 -2
View File
@@ -522,7 +522,7 @@ export class SMB2Client implements AsyncVFS, StateTransferable<SMB2TransferState
}
async copy(src: string, dest: string, opts?: { overwrite?: boolean }): Promise<void> {
const st = await this.stat(src)
const { self: st, entries } = await this.statAndReaddir(src)
const srcKey = smbPath(src)
const destKey = smbPath(dest)
if (destKey === srcKey || destKey.startsWith(srcKey + "\\"))
@@ -540,7 +540,7 @@ export class SMB2Client implements AsyncVFS, StateTransferable<SMB2TransferState
}
if (st.isDirectory) {
await this.mkdir(dest)
for (const entry of await this.readdir(src)) {
for (const entry of entries) {
await this.copy(entry.path, joinPath(dest, entry.name), opts)
}
return
+22
View File
@@ -1131,6 +1131,28 @@ suite("smb2 client e2e", () => {
}
})
test("recursive copy combines source directory metadata and enumeration", async () => {
const { client, server, stop } = setup()
try {
await client.mkdir("/source")
await client.mkdir("/source/sub")
await client.writeFile("/source/sub/file.txt", streamOf(new TextEncoder().encode("payload")))
const before = server.commands.length
await client.copy("/source", "/destination")
const commands = server.commands.slice(before)
assert.equal(commands.filter((command) => command === Command.CREATE).length, 10)
assert.equal(commands.filter((command) => command === Command.QUERY_DIRECTORY).length, 4)
assert.equal(
new TextDecoder().decode(await readAll(await client.readFile("/destination/sub/file.txt"))),
"payload",
)
} finally {
await stop()
}
})
test("copy with overwrite replaces a destination of a different type", async () => {
const { client, stop } = setup()
try {