mirror of
https://github.com/saltbo/zpan.git
synced 2026-08-29 00:01:42 +08:00
perf(webdav): collapse repeated path lookups
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,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()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user