import { davError, davResponse, emptyResponse } from "@/worker/dav/http"; import { deadPropInstructions, type CollectionDavPath } from "@/worker/dav/collection-handler"; import { davHtmlListingResponse, type DavListingEntry } from "@/worker/dav/html-listing"; import { readCollectionPropfind, readCollectionProppatch, readDavConditionalHeaders, type DavConditionalHeaders, type DavReadResult, } from "@/worker/dav/readers"; import { davObjectResponse, davResultError, protectedProppatchMultistatus, proppatchMultistatus, type DavResultError, } from "@/worker/dav/responses"; import type { DavResourcePropKind } from "@/worker/dav/props"; import type { DavRequestContext } from "@/worker/dav/runtime"; import type { PropfindRequest } from "@/worker/xml/dav-request"; export type DavOperationResult = ({ ok: true } & T) | DavResultError; type CollectionListingPath = | { kind: "home" } | { kind: TCollectionKind; collectionName: string }; type CollectionObjectPath = { kind: "object"; collectionName: string; objectName: string; }; export interface CollectionObjectAdapter< TCollectionKind extends string, TResource extends { href: string }, TObject extends { body: BodyInit; size: number; etag: string }, TPutExtra extends object = Record, > { objectContentType: string; objectName: string; objectPathName: string; putObjectPathName: string; deleteObjectPathName: string; hrefForPath(path: CollectionDavPath): string; resourceKindForPath(path: CollectionDavPath): DavResourcePropKind; maxObjectBytes(context: DavRequestContext): number; preparePut(context: DavRequestContext): DavReadResult | Promise>; describe( context: DavRequestContext, path: CollectionDavPath, depth: "0" | "1", ): Promise>; propfindMultistatus(context: DavRequestContext, resources: TResource[], request: PropfindRequest): string; proppatch( context: DavRequestContext, path: CollectionDavPath, input: { instructions: ReturnType; conditions: DavConditionalHeaders; nowMs: number; }, ): Promise; getObject( context: DavRequestContext, path: Extract, { kind: "object" }>, ): Promise>; putObject( context: DavRequestContext, path: Extract, { kind: "object" }>, input: { body: ArrayBuffer; conditions: DavConditionalHeaders; maxBytes: number; nowMs: number; } & TPutExtra, ): Promise>; deleteObject( context: DavRequestContext, path: Extract, { kind: "object" }>, input: { conditions: DavConditionalHeaders; nowMs: number; }, ): Promise; } export interface CollectionHtmlListingAdapter { title(resource: TResource): string; parentHref(path: CollectionListingPath): string | null; entry(resource: TResource): DavListingEntry; } function propfindResourceKind(path: CollectionDavPath): string { return path.kind === "object" ? "object" : path.kind; } function isCollectionObjectPath( path: CollectionDavPath, ): path is CollectionObjectPath { return path.kind === "object"; } export async function handleCollectionPropfind< TCollectionKind extends string, TResource extends { href: string }, TObject extends { body: BodyInit; size: number; etag: string }, TPutExtra extends object, >( context: DavRequestContext, path: CollectionDavPath, adapter: CollectionObjectAdapter, ): Promise { const propfind = await readCollectionPropfind(context.c, propfindResourceKind(path)); if (!propfind.ok) return propfind.response; const result = await adapter.describe(context, path, propfind.value.depth); if (!result.ok) return davResultError(result); return davResponse(adapter.propfindMultistatus(context, result.resources, propfind.value.request), 207); } export async function handleCollectionProppatch< TCollectionKind extends string, TResource extends { href: string }, TObject extends { body: BodyInit; size: number; etag: string }, TPutExtra extends object, >( context: DavRequestContext, path: CollectionDavPath, adapter: CollectionObjectAdapter, ): Promise { const resourceKind = adapter.resourceKindForPath(path); const proppatch = await readCollectionProppatch(context.c, resourceKind); if (!proppatch.ok) return proppatch.response; const href = adapter.hrefForPath(path); const protectedResponse = protectedProppatchMultistatus(href, proppatch.value, resourceKind); if (protectedResponse) return protectedResponse; const conditions = readDavConditionalHeaders(context.c); if (!conditions.ok) return conditions.response; const result = await adapter.proppatch(context, path, { instructions: deadPropInstructions(proppatch.value), conditions: conditions.value, nowMs: Date.now(), }); return result.ok ? proppatchMultistatus(href, proppatch.value) : davResultError(result); } export async function handleCollectionObjectGetHead< TCollectionKind extends string, TResource extends { href: string }, TObject extends { body: BodyInit; size: number; etag: string }, TPutExtra extends object, >( context: DavRequestContext, path: CollectionDavPath, adapter: CollectionObjectAdapter, ): Promise { if (!isCollectionObjectPath(path)) return davError(405, `GET requires ${adapter.objectName}`); const result = await adapter.getObject(context, path); if (!result.ok) return davResultError(result); return davObjectResponse(context.c.req.method, result.object, adapter.objectContentType); } export async function handleCollectionGetHead< TCollectionKind extends string, TResource extends { href: string }, TObject extends { body: BodyInit; size: number; etag: string }, TPutExtra extends object, >( context: DavRequestContext, path: CollectionDavPath, adapter: CollectionObjectAdapter, listing: CollectionHtmlListingAdapter, ): Promise { if (isCollectionObjectPath(path)) return await handleCollectionObjectGetHead(context, path, adapter); const result = await adapter.describe(context, path, "1"); if (!result.ok) return davResultError(result); const target = result.resources[0]; if (!target) return davError(404, "Resource not found"); return await davHtmlListingResponse( context.c, { title: listing.title(target), href: target.href, parentHref: listing.parentHref(path), entries: result.resources.slice(1).map(listing.entry), }, context.c.req.method, ); } export async function handleCollectionObjectPut< TCollectionKind extends string, TResource extends { href: string }, TObject extends { body: BodyInit; size: number; etag: string }, TPutExtra extends object, >( context: DavRequestContext, path: CollectionDavPath, adapter: CollectionObjectAdapter, ): Promise { if (!isCollectionObjectPath(path)) return davError(405, `PUT requires ${adapter.putObjectPathName}`); const extra = await adapter.preparePut(context); if (!extra.ok) return extra.response; const conditions = readDavConditionalHeaders(context.c); if (!conditions.ok) return conditions.response; const result = await adapter.putObject(context, path, { ...extra.value, body: await context.c.req.arrayBuffer(), conditions: conditions.value, nowMs: Date.now(), maxBytes: adapter.maxObjectBytes(context), }); return result.ok ? emptyResponse(result.created ? 201 : 204, { ETag: result.object.etag }) : davResultError(result); } export async function handleCollectionObjectDelete< TCollectionKind extends string, TResource extends { href: string }, TObject extends { body: BodyInit; size: number; etag: string }, TPutExtra extends object, >( context: DavRequestContext, path: CollectionDavPath, adapter: CollectionObjectAdapter, ): Promise { if (!isCollectionObjectPath(path)) return davError(405, `DELETE requires ${adapter.deleteObjectPathName}`); const conditions = readDavConditionalHeaders(context.c); if (!conditions.ok) return conditions.response; const result = await adapter.deleteObject(context, path, { conditions: conditions.value, nowMs: Date.now() }); return result.ok ? emptyResponse(204) : davResultError(result); }