File
Blob: src/worker/webdav/props.ts
| 1 | import { buildMultistatus, emptyProp, escapeXml, type PropValue } from "@/worker/dav/multistatus"; |
| 2 | import { davProp, defaultAllpropNames } from "@/worker/dav/props"; |
| 3 | import type { FileDeadPropRow, FileLockRow, FileNodeRow } from "@/worker/db/file-dav-do/schema"; |
| 4 | import type { FileDavNodeView } from "@/worker/objects/file-dav-object"; |
| 5 | import type { PropfindRequest } from "@/worker/xml/dav-request"; |
| 6 | import type { QName } from "@/worker/xml/namespaces"; |
| 7 | import { DAV_NS } from "@/worker/xml/namespaces"; |
| 8 | import { lockXml } from "@/worker/webdav/lock-xml"; |
| 9 | |
| 10 | export function httpDate(ms: number): string { |
| 11 | return new Date(ms).toUTCString(); |
| 12 | } |
| 13 | |
| 14 | function creationDate(ms: number): string { |
| 15 | return new Date(ms).toISOString(); |
| 16 | } |
| 17 | |
| 18 | function 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 | |
| 25 | function 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 | |
| 32 | function 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 | |
| 60 | function 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 | |
| 65 | function 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 | |
| 77 | export 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 | |
| 107 | export function multistatusXml(responses: Parameters<typeof buildMultistatus>[0]): string { |
| 108 | return buildMultistatus(responses); |
| 109 | } |