File
Blob: src/worker/dav/responses.ts
| 1 | import { davError, davResponse } from "@/worker/dav/http"; |
| 2 | import { buildMultistatus, emptyProp, type MultistatusResponse, type Propstat } from "@/worker/dav/multistatus"; |
| 3 | import { isProtectedLivePropForResource, type DavResourcePropKind } from "@/worker/dav/props"; |
| 4 | import type { ProppatchInstruction } from "@/worker/xml/dav-request"; |
| 5 | |
| 6 | export interface DavResultError { |
| 7 | ok: false; |
| 8 | status: number; |
| 9 | message: string; |
| 10 | } |
| 11 | |
| 12 | export function davResultError(result: DavResultError): Response { |
| 13 | return davError(result.status, result.message); |
| 14 | } |
| 15 | |
| 16 | export 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 | |
| 28 | export 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 | |
| 54 | export 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 | |
| 78 | export 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 | |
| 97 | export 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 | } |