Skip to content
File

Blob: src/worker/dav/responses.ts

typescript109 lines
1import { davError, davResponse } from "@/worker/dav/http";
2import { buildMultistatus, emptyProp, type MultistatusResponse, type Propstat } from "@/worker/dav/multistatus";
3import { isProtectedLivePropForResource, type DavResourcePropKind } from "@/worker/dav/props";
4import type { ProppatchInstruction } from "@/worker/xml/dav-request";
5 
6export interface DavResultError {
7 ok: false;
8 status: number;
9 message: string;
10}
11 
12export function davResultError(result: DavResultError): Response {
13 return davError(result.status, result.message);
14}
15 
16export function proppatchMultistatus(href: string, instructions: ProppatchInstruction[]): Response {
17 return davResponse(
18 buildMultistatus([
19 {
20 href,
21 propstats: [{ status: 200, props: instructions.map((item) => emptyProp(item.prop)) }],
22 },
23 ]),
24 207,
25 );
26}
27 
28export function protectedProppatchMultistatus(
29 href: string,
30 instructions: ProppatchInstruction[],
31 resourceKind: DavResourcePropKind,
32): Response | null {
33 const protectedProp = instructions.find((instruction) =>
34 isProtectedLivePropForResource(instruction.prop, resourceKind),
35 );
36 if (!protectedProp) return null;
37 return davResponse(
38 buildMultistatus([
39 {
40 href,
41 propstats: [
42 { status: 403, props: [emptyProp(protectedProp.prop)] },
43 {
44 status: 424,
45 props: instructions.filter((item) => item !== protectedProp).map((item) => emptyProp(item.prop)),
46 },
47 ],
48 },
49 ]),
50 207,
51 );
52}
53 
54export function syncCollectionMultistatus<TResource extends { href: string }>(
55 result: {
56 resources: TResource[];
57 changes: { href: string; changeType: string }[];
58 syncToken: string;
59 truncated: boolean;
60 },
61 propstatsForResource: (resource: TResource) => Propstat[],
62): Response {
63 const responses: MultistatusResponse[] = [
64 ...result.resources.map((resource) => ({
65 href: resource.href,
66 propstats: propstatsForResource(resource),
67 })),
68 ...result.changes
69 .filter((change) => change.changeType === "deleted")
70 .map((change) => ({ href: change.href, status: 404, propstats: [] })),
71 ];
72 // RFC 6578 3.10: when a sync-collection response is truncated, include an
73 // additional response with HTTP status 507 to signal the client to retry.
74 if (result.truncated) responses.push({ href: "/", status: 507, propstats: [] });
75 return davResponse(buildMultistatus(responses, { syncToken: result.syncToken }), 207);
76}
77 
78export function multigetMultistatus<TResource>(
79 requested: { href: string; resource: TResource | null }[],
80 propstatsForResource: (resource: TResource) => Propstat[],
81): Response {
82 return davResponse(
83 buildMultistatus(
84 requested.map((entry) =>
85 entry.resource
86 ? {
87 href: entry.href,
88 propstats: propstatsForResource(entry.resource),
89 }
90 : { href: entry.href, status: 404, propstats: [] },
91 ),
92 ),
93 207,
94 );
95}
96 
97export function davObjectResponse(
98 method: string,
99 object: { body: BodyInit; size: number; etag: string },
100 contentType: string,
101): Response {
102 const headers = new Headers({
103 "Content-Type": contentType,
104 "Content-Length": String(object.size),
105 ETag: object.etag,
106 });
107 return new Response(method === "HEAD" ? null : object.body, { status: 200, headers });
108}