Skip to content
File

Blob: src/worker/dav/collection-report.ts

typescript117 lines
1import { davError, davResponse, numberOfMatchesWithinLimitsError } from "@/worker/dav/http";
2import { buildMultistatus, type Propstat } from "@/worker/dav/multistatus";
3import { readReportDepth } from "@/worker/dav/readers";
4import { davResultError, multigetMultistatus, syncCollectionMultistatus } from "@/worker/dav/responses";
5import type { DavRequestContext } from "@/worker/dav/runtime";
6import type { DavOperationResult } from "@/worker/dav/collection-object";
7 
8type CollectionReportOperation = "sync" | "multiget" | "query";
9type ReportDepth = "0" | "1" | "infinity";
10 
11export 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 
43export 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 
82export 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 
94export 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 
101export 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}