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);
}