File
Blob: src/worker/dav/collection-report.ts
| 1 | import { davError, davResponse, numberOfMatchesWithinLimitsError } from "@/worker/dav/http"; |
| 2 | import { buildMultistatus, type Propstat } from "@/worker/dav/multistatus"; |
| 3 | import { readReportDepth } from "@/worker/dav/readers"; |
| 4 | import { davResultError, multigetMultistatus, syncCollectionMultistatus } from "@/worker/dav/responses"; |
| 5 | import type { DavRequestContext } from "@/worker/dav/runtime"; |
| 6 | import type { DavOperationResult } from "@/worker/dav/collection-object"; |
| 7 | |
| 8 | type CollectionReportOperation = "sync" | "multiget" | "query"; |
| 9 | type ReportDepth = "0" | "1" | "infinity"; |
| 10 | |
| 11 | export interface CollectionReportAdapter<TPath, TReport, TResource extends { href: string }> { |
| 12 | invalidBodyMessage: string; |
| 13 | validatePath(context: DavRequestContext, path: TPath): Response | null; |
| 14 | validateReportPath?(context: DavRequestContext, path: TPath, report: TReport): Response | null; |
| 15 | parseReport(xml: string, maxBytes: number): TReport; |
| 16 | operationForReport(report: TReport): CollectionReportOperation; |
| 17 | depthFallback(report: TReport): ReportDepth; |
| 18 | sync( |
| 19 | context: DavRequestContext, |
| 20 | path: TPath, |
| 21 | report: TReport, |
| 22 | ): Promise< |
| 23 | DavOperationResult<{ |
| 24 | resources: TResource[]; |
| 25 | changes: { href: string; changeType: string }[]; |
| 26 | syncToken: string; |
| 27 | truncated: boolean; |
| 28 | }> |
| 29 | >; |
| 30 | multiget( |
| 31 | context: DavRequestContext, |
| 32 | path: TPath, |
| 33 | report: TReport, |
| 34 | ): Promise<DavOperationResult<{ requested: { href: string; resource: TResource | null }[] }>>; |
| 35 | query( |
| 36 | context: DavRequestContext, |
| 37 | path: TPath, |
| 38 | report: TReport, |
| 39 | ): Promise<DavOperationResult<{ resources: TResource[]; truncated: boolean }>>; |
| 40 | propstatsForResource(context: DavRequestContext, path: TPath, report: TReport, resource: TResource): Propstat[]; |
| 41 | } |
| 42 | |
| 43 | export async function handleCollectionReport<TPath, TReport, TResource extends { href: string }>( |
| 44 | context: DavRequestContext, |
| 45 | path: TPath, |
| 46 | maxXmlBytes: number, |
| 47 | adapter: CollectionReportAdapter<TPath, TReport, TResource>, |
| 48 | ): Promise<Response> { |
| 49 | const pathResponse = adapter.validatePath(context, path); |
| 50 | if (pathResponse) return pathResponse; |
| 51 | |
| 52 | let report: TReport; |
| 53 | try { |
| 54 | report = adapter.parseReport(await context.c.req.text(), maxXmlBytes); |
| 55 | } catch (cause) { |
| 56 | return davError(400, cause instanceof Error ? cause.message : adapter.invalidBodyMessage); |
| 57 | } |
| 58 | |
| 59 | const depth = readReportDepth(context.c, adapter.depthFallback(report)); |
| 60 | if (!depth.ok) return depth.response; |
| 61 | |
| 62 | const reportPathResponse = adapter.validateReportPath?.(context, path, report); |
| 63 | if (reportPathResponse) return reportPathResponse; |
| 64 | |
| 65 | const operation = adapter.operationForReport(report); |
| 66 | if (operation === "sync") { |
| 67 | return syncCollectionReport(await adapter.sync(context, path, report), (resource) => |
| 68 | adapter.propstatsForResource(context, path, report, resource), |
| 69 | ); |
| 70 | } |
| 71 | if (operation === "multiget") { |
| 72 | return multigetReport(await adapter.multiget(context, path, report), (resource) => |
| 73 | adapter.propstatsForResource(context, path, report, resource), |
| 74 | ); |
| 75 | } |
| 76 | if (depth.value === "0") return davResponse(buildMultistatus([]), 207); |
| 77 | return queryReport(await adapter.query(context, path, report), (resource) => |
| 78 | adapter.propstatsForResource(context, path, report, resource), |
| 79 | ); |
| 80 | } |
| 81 | |
| 82 | export function syncCollectionReport<TResource extends { href: string }>( |
| 83 | result: DavOperationResult<{ |
| 84 | resources: TResource[]; |
| 85 | changes: { href: string; changeType: string }[]; |
| 86 | syncToken: string; |
| 87 | truncated: boolean; |
| 88 | }>, |
| 89 | propstatsForResource: (resource: TResource) => Propstat[], |
| 90 | ): Response { |
| 91 | return result.ok ? syncCollectionMultistatus(result, propstatsForResource) : davResultError(result); |
| 92 | } |
| 93 | |
| 94 | export function multigetReport<TResource extends { href: string }>( |
| 95 | result: DavOperationResult<{ requested: { href: string; resource: TResource | null }[] }>, |
| 96 | propstatsForResource: (resource: TResource) => Propstat[], |
| 97 | ): Response { |
| 98 | return result.ok ? multigetMultistatus(result.requested, propstatsForResource) : davResultError(result); |
| 99 | } |
| 100 | |
| 101 | export function queryReport<TResource extends { href: string }>( |
| 102 | result: DavOperationResult<{ resources: TResource[]; truncated: boolean }>, |
| 103 | propstatsForResource: (resource: TResource) => Propstat[], |
| 104 | ): Response { |
| 105 | if (!result.ok) return davResultError(result); |
| 106 | if (result.truncated) return numberOfMatchesWithinLimitsError(); |
| 107 | return davResponse( |
| 108 | buildMultistatus( |
| 109 | result.resources.map((resource) => ({ |
| 110 | href: resource.href, |
| 111 | propstats: propstatsForResource(resource), |
| 112 | })), |
| 113 | ), |
| 114 | 207, |
| 115 | ); |
| 116 | } |