import { buildMultistatus, emptyProp, escapeXml, type PropValue } from "@/worker/dav/multistatus"; import { davProp, defaultAllpropNames } from "@/worker/dav/props"; import type { FileDeadPropRow, FileLockRow, FileNodeRow } from "@/worker/db/file-dav-do/schema"; import type { FileDavNodeView } from "@/worker/objects/file-dav-object"; import type { PropfindRequest } from "@/worker/xml/dav-request"; import type { QName } from "@/worker/xml/namespaces"; import { DAV_NS } from "@/worker/xml/namespaces"; import { lockXml } from "@/worker/webdav/lock-xml"; export function httpDate(ms: number): string { return new Date(ms).toUTCString(); } function creationDate(ms: number): string { return new Date(ms).toISOString(); } function displayName(href: string): string { if (href === "/files/") return "files"; const trimmed = href.endsWith("/") ? href.slice(0, -1) : href; const name = trimmed.slice(trimmed.lastIndexOf("/") + 1); return decodeURIComponent(name); } function supportedLockXml(): string { return [ "", "", ].join(""); } function liveProp(node: FileNodeRow, href: string, locks: FileLockRow[], prop: QName): PropValue | null { if (prop.nsUri !== DAV_NS) return null; switch (prop.localName) { case "creationdate": return { qname: prop, valueXml: escapeXml(creationDate(node.createdAtMs)) }; case "displayname": return { qname: prop, valueXml: escapeXml(displayName(href)) }; case "getcontentlanguage": return node.contentLanguage ? { qname: prop, valueXml: escapeXml(node.contentLanguage) } : null; case "getcontentlength": return node.kind === "file" ? { qname: prop, valueXml: String(node.size) } : null; case "getcontenttype": return node.kind === "file" && node.contentType ? { qname: prop, valueXml: escapeXml(node.contentType) } : null; case "getetag": return node.kind === "file" && node.etag ? { qname: prop, valueXml: escapeXml(node.etag) } : null; case "getlastmodified": return { qname: prop, valueXml: escapeXml(httpDate(node.modifiedAtMs)) }; case "lockdiscovery": return { qname: prop, valueXml: locks.map(lockXml).join("") }; case "resourcetype": return { qname: prop, valueXml: node.kind === "collection" ? "" : "" }; case "supportedlock": return { qname: prop, valueXml: supportedLockXml() }; default: return null; } } function deadProp(deadProps: FileDeadPropRow[], prop: QName): PropValue | null { const row = deadProps.find((candidate) => candidate.nsUri === prop.nsUri && candidate.localName === prop.localName); return row ? { qname: prop, valueXml: row.xmlValue } : null; } function defaultPropsForNode(node: FileNodeRow): QName[] { if (node.kind === "file") return defaultAllpropNames(); return [ davProp("creationdate"), davProp("displayname"), davProp("getlastmodified"), davProp("lockdiscovery"), davProp("resourcetype"), davProp("supportedlock"), ]; } export function nodePropstats(node: FileDavNodeView, request: PropfindRequest) { if (request.kind === "propname") { const names = [ ...defaultPropsForNode(node.node).map(emptyProp), ...node.deadProps.map((prop) => emptyProp({ nsUri: prop.nsUri, localName: prop.localName })), ]; return [{ status: 200, props: names }]; } const requested = request.kind === "allprop" ? [ ...defaultPropsForNode(node.node), ...request.includeProps, ...node.deadProps.map((prop) => ({ nsUri: prop.nsUri, localName: prop.localName })), ] : request.props; const ok: PropValue[] = []; const missing: PropValue[] = []; for (const prop of requested) { const value = liveProp(node.node, node.href, node.locks, prop) ?? deadProp(node.deadProps, prop); if (value) ok.push(value); else missing.push(emptyProp(prop)); } return [ { status: 200, props: ok }, { status: 404, props: missing }, ]; } export function multistatusXml(responses: Parameters[0]): string { return buildMultistatus(responses); }