Skip to content
File

Blob: src/worker/webdav/props.ts

typescript110 lines
1import { buildMultistatus, emptyProp, escapeXml, type PropValue } from "@/worker/dav/multistatus";
2import { davProp, defaultAllpropNames } from "@/worker/dav/props";
3import type { FileDeadPropRow, FileLockRow, FileNodeRow } from "@/worker/db/file-dav-do/schema";
4import type { FileDavNodeView } from "@/worker/objects/file-dav-object";
5import type { PropfindRequest } from "@/worker/xml/dav-request";
6import type { QName } from "@/worker/xml/namespaces";
7import { DAV_NS } from "@/worker/xml/namespaces";
8import { lockXml } from "@/worker/webdav/lock-xml";
9 
10export function httpDate(ms: number): string {
11 return new Date(ms).toUTCString();
12}
13 
14function creationDate(ms: number): string {
15 return new Date(ms).toISOString();
16}
17 
18function displayName(href: string): string {
19 if (href === "/files/") return "files";
20 const trimmed = href.endsWith("/") ? href.slice(0, -1) : href;
21 const name = trimmed.slice(trimmed.lastIndexOf("/") + 1);
22 return decodeURIComponent(name);
23}
24 
25function supportedLockXml(): string {
26 return [
27 "<D:lockentry><D:lockscope><D:exclusive/></D:lockscope><D:locktype><D:write/></D:locktype></D:lockentry>",
28 "<D:lockentry><D:lockscope><D:shared/></D:lockscope><D:locktype><D:write/></D:locktype></D:lockentry>",
29 ].join("");
30}
31 
32function liveProp(node: FileNodeRow, href: string, locks: FileLockRow[], prop: QName): PropValue | null {
33 if (prop.nsUri !== DAV_NS) return null;
34 switch (prop.localName) {
35 case "creationdate":
36 return { qname: prop, valueXml: escapeXml(creationDate(node.createdAtMs)) };
37 case "displayname":
38 return { qname: prop, valueXml: escapeXml(displayName(href)) };
39 case "getcontentlanguage":
40 return node.contentLanguage ? { qname: prop, valueXml: escapeXml(node.contentLanguage) } : null;
41 case "getcontentlength":
42 return node.kind === "file" ? { qname: prop, valueXml: String(node.size) } : null;
43 case "getcontenttype":
44 return node.kind === "file" && node.contentType ? { qname: prop, valueXml: escapeXml(node.contentType) } : null;
45 case "getetag":
46 return node.kind === "file" && node.etag ? { qname: prop, valueXml: escapeXml(node.etag) } : null;
47 case "getlastmodified":
48 return { qname: prop, valueXml: escapeXml(httpDate(node.modifiedAtMs)) };
49 case "lockdiscovery":
50 return { qname: prop, valueXml: locks.map(lockXml).join("") };
51 case "resourcetype":
52 return { qname: prop, valueXml: node.kind === "collection" ? "<D:collection/>" : "" };
53 case "supportedlock":
54 return { qname: prop, valueXml: supportedLockXml() };
55 default:
56 return null;
57 }
58}
59 
60function deadProp(deadProps: FileDeadPropRow[], prop: QName): PropValue | null {
61 const row = deadProps.find((candidate) => candidate.nsUri === prop.nsUri && candidate.localName === prop.localName);
62 return row ? { qname: prop, valueXml: row.xmlValue } : null;
63}
64 
65function defaultPropsForNode(node: FileNodeRow): QName[] {
66 if (node.kind === "file") return defaultAllpropNames();
67 return [
68 davProp("creationdate"),
69 davProp("displayname"),
70 davProp("getlastmodified"),
71 davProp("lockdiscovery"),
72 davProp("resourcetype"),
73 davProp("supportedlock"),
74 ];
75}
76 
77export function nodePropstats(node: FileDavNodeView, request: PropfindRequest) {
78 if (request.kind === "propname") {
79 const names = [
80 ...defaultPropsForNode(node.node).map(emptyProp),
81 ...node.deadProps.map((prop) => emptyProp({ nsUri: prop.nsUri, localName: prop.localName })),
82 ];
83 return [{ status: 200, props: names }];
84 }
85 
86 const requested =
87 request.kind === "allprop"
88 ? [
89 ...defaultPropsForNode(node.node),
90 ...request.includeProps,
91 ...node.deadProps.map((prop) => ({ nsUri: prop.nsUri, localName: prop.localName })),
92 ]
93 : request.props;
94 const ok: PropValue[] = [];
95 const missing: PropValue[] = [];
96 for (const prop of requested) {
97 const value = liveProp(node.node, node.href, node.locks, prop) ?? deadProp(node.deadProps, prop);
98 if (value) ok.push(value);
99 else missing.push(emptyProp(prop));
100 }
101 return [
102 { status: 200, props: ok },
103 { status: 404, props: missing },
104 ];
105}
106 
107export function multistatusXml(responses: Parameters<typeof buildMultistatus>[0]): string {
108 return buildMultistatus(responses);
109}