perf(webdav): collapse repeated path lookups

This commit is contained in:
saltbo
2026-07-26 16:51:06 -04:00
parent 05f41ef606
commit 7c16915de0
4 changed files with 78 additions and 49 deletions
+46 -34
View File
@@ -1,5 +1,5 @@
import { ObjectStatus } from '@shared/constants'
import { and, asc, desc, eq, isNull } from 'drizzle-orm'
import { and, asc, desc, eq, getTableColumns, isNull } from 'drizzle-orm'
import { member, organization } from '../../db/auth-schema'
import { matters } from '../../db/schema'
import type { Database } from '../../platform/interface'
@@ -12,30 +12,32 @@ import {
} from '../../usecases/ports'
type WorkspaceRow = Pick<WebDavWorkspace, 'id' | 'name' | 'slug'>
type WorkspaceMatterRow = {
workspace: WorkspaceRow
matter: Matter | null
}
export function createWebDavPathRepo(db: Database): WebDavPathRepo {
async function getUserWorkspace(userId: string, pathSegment: string): Promise<WebDavWorkspace | null> {
const workspaces = toWebDavWorkspaces(await userWorkspaceRows(db, userId))
return (
workspaces.find(
(workspace) =>
workspace.slug === pathSegment || workspace.id === pathSegment || workspace.pathSegment === pathSegment,
) ?? null
)
}
async function resolveWebDavPath(userId: string, rawPath: string): Promise<WebDavTarget> {
const parts = decodeDavPath(rawPath)
if (parts.length === 0) return { workspace: null, mountRoot: true, parent: '', name: '', matter: null }
const workspace = await getUserWorkspace(userId, parts[0])
if (!workspace) throw new WebDavPathError('Workspace not found', 404)
if (parts.length === 1) return { workspace, mountRoot: false, parent: '', name: '', matter: null }
const matterParts = parts.slice(1)
const name = matterParts.at(-1) ?? ''
const parent = matterParts.slice(0, -1).join('/')
const matter = await findMatterByPath(db, workspace.id, parent, name)
const rows =
parts.length === 1
? (await userWorkspaceRows(db, userId)).map((workspace) => ({ workspace, matter: null }))
: await userWorkspaceMatterRows(db, userId, parent, name)
const workspaces = toWebDavWorkspaces(rows.map((row) => row.workspace))
const workspace =
workspaces.find(
(candidate) => candidate.slug === parts[0] || candidate.id === parts[0] || candidate.pathSegment === parts[0],
) ?? null
if (!workspace) throw new WebDavPathError('Workspace not found', 404)
if (parts.length === 1) return { workspace, mountRoot: false, parent: '', name: '', matter: null }
const matter = rows.find((row) => row.workspace.id === workspace.id)?.matter ?? null
return { workspace, mountRoot: false, parent, name, matter }
}
@@ -79,6 +81,34 @@ async function userWorkspaceRows(db: Database, userId: string): Promise<Workspac
.orderBy(asc(organization.name), asc(organization.slug))
}
async function userWorkspaceMatterRows(
db: Database,
userId: string,
parent: string,
name: string,
): Promise<WorkspaceMatterRow[]> {
return db
.select({
workspace: { id: organization.id, name: organization.name, slug: organization.slug },
matter: getTableColumns(matters),
})
.from(member)
.innerJoin(organization, eq(organization.id, member.organizationId))
.leftJoin(
matters,
and(
eq(matters.orgId, organization.id),
eq(matters.parent, parent),
eq(matters.name, name),
eq(matters.status, ObjectStatus.ACTIVE),
isNull(matters.trashedAt),
isNull(matters.purgedAt),
),
)
.where(eq(member.userId, userId))
.orderBy(asc(organization.name), asc(organization.slug))
}
function toWebDavWorkspaces(rows: WorkspaceRow[]): WebDavWorkspace[] {
const preferredSegments = new Map<string, number>()
for (const row of rows) {
@@ -134,21 +164,3 @@ function decodeSegment(segment: string): string {
function isSafeDavPathSegment(segment: string): boolean {
return Boolean(segment) && segment !== '.' && segment !== '..' && !segment.includes('/') && !segment.includes('\\')
}
async function findMatterByPath(db: Database, orgId: string, parent: string, name: string): Promise<Matter | null> {
const rows = await db
.select()
.from(matters)
.where(
and(
eq(matters.orgId, orgId),
eq(matters.parent, parent),
eq(matters.name, name),
eq(matters.status, ObjectStatus.ACTIVE),
isNull(matters.trashedAt),
isNull(matters.purgedAt),
),
)
.limit(1)
return rows[0] ?? null
}
+19
View File
@@ -799,6 +799,25 @@ describe('WebDAV API', () => {
])
})
it('does not resolve a missing GET target again for failure auditing', async () => {
const { app, db, auth, deps } = await createTestApp()
await authedHeaders(app)
const workspace = await org(db)
const account = await userAccount(db)
const key = await apiKey(auth, account.id, { webdav: ['read'] })
const resolve = vi.spyOn(deps.webdavPath, 'resolveExistingWebDavPath')
const res = await app.request(`/dav/${workspace.slug}/._missing.txt`, {
method: 'GET',
headers: basicHeaders(account.email, key, {
'User-Agent': 'WebDAVFS/3.0.0 (03008000) Darwin/24.6.0 (arm64)',
}),
})
expect(res.status).toBe(404)
expect(resolve).toHaveBeenCalledTimes(1)
})
it('GET supports valid byte ranges and rejects invalid ranges [spec: webdav/get-range]', async () => {
const { app, db, auth } = await createTestApp()
await authedHeaders(app)
+10 -15
View File
@@ -664,7 +664,7 @@ async function processWebDavAudit(
): Promise<void> {
const actor = transferAuditActor(c.get('principal'))
if (c.req.method === 'GET' && isDownloadFailureStatus(c.res.status)) {
const target = await webDavDownloadAuditTarget(c, userId)
const target = c.get('webDavDownloadAuditTarget')
if (target) {
await recordDownloadFailure(c.get('deps'), actor, target, transferFailureReason(c))
}
@@ -821,20 +821,6 @@ async function webDavUploadedTarget(c: DavContext, userId: string): Promise<Tran
}
}
async function webDavDownloadAuditTarget(c: DavContext, userId: string): Promise<TransferAuditTarget | null> {
const resolved = await resolveWebDavDownload(c.get('deps'), { userId, rawPath: davPath(c) })
if (!resolved.ok) return null
return {
orgId: resolved.workspace.id,
targetType: 'file',
targetId: resolved.matter.id,
targetName: resolved.matter.name,
bytes: requestedWebDavBytes(c, resolved.matter),
source: 'webdav_download',
metadata: { matterId: resolved.matter.id, storageId: resolved.storage.id },
}
}
function requestedWebDavBytes(c: DavContext, matter: NonNullable<WebDavTarget['matter']>): number {
const size = matter.size ?? 0
if (!ifRangeMatches(c.req.header('If-Range'), matter)) return size
@@ -991,6 +977,15 @@ async function readFile(c: DavContext, auth: DavAuth): Promise<Response> {
}
}
const { matter, workspace, storage } = resolved
c.set('webDavDownloadAuditTarget', {
orgId: workspace.id,
targetType: 'file',
targetId: matter.id,
targetName: matter.name,
bytes: requestedWebDavBytes(c, matter),
source: 'webdav_download',
metadata: { matterId: matter.id, storageId: storage.id },
})
const precondition = preconditionResponse(c, matter)
if (precondition) return precondition
+3
View File
@@ -3,6 +3,7 @@ import type { Auth } from '../auth'
import type { WebDavMountPath } from '../domain/webdav-public-url'
import type { Platform } from '../platform/interface'
import type { Deps } from '../usecases/deps'
import type { TransferAuditTarget } from '../usecases/transfer-activity'
export type Env = {
Variables: {
@@ -18,6 +19,7 @@ export type Env = {
webDavDomain: string
webDavMountPath: WebDavMountPath
webDavTrace: string[]
webDavDownloadAuditTarget: TransferAuditTarget | null
// Structured detail for the access log on a failed request. Set by `jsonError`
// (via `app.onError`); read by the accessLog middleware so every 4xx/5xx carries
// its reason + full message, not just unhandled crashes.
@@ -70,5 +72,6 @@ export const platformMiddleware = (platform: Platform, auth: Auth) =>
c.set('webDavDomain', '')
c.set('webDavMountPath', '/dav')
c.set('webDavTrace', [])
c.set('webDavDownloadAuditTarget', null)
await next()
})