import { findLockConflict, findLockConflictOnResource, lockAppliesToHref } from "@/worker/dav/locks"; import { resourceHref } from "@/worker/dav/paths"; import type { FileDavDoDb } from "@/worker/db/file-dav-do/client"; import { activeLocks, createCollectionNode, deadPropsForNodes, ensureRoot, getBlob, insertChange, nodeAtPath, parentForPath, removeDeadProp, setDeadProp, touchNode, treeFromLimited, } from "@/worker/db/file-dav-do/repository"; import type { FileNodeRow } from "@/worker/db/file-dav-do/schema"; import { fileDavError } from "@/worker/objects/file-dav/helpers"; import { fileIfHeaderMatches } from "@/worker/objects/file-dav/helpers"; import type { EnsureInitializedInput, FileDavError, FileDavNodeView, MkcolInput, PropfindInput, ProppatchInput, ResolveFileInput, ResolveFileResult, } from "@/worker/objects/file-dav/types"; import { ifNoneMatchBlocks, strongEtagMatches } from "@/worker/dav/etag"; export function ensureInitialized(db: FileDavDoDb, input: EnsureInitializedInput): { ok: true } { ensureRoot(db, input.subjectId, input.nowMs); return { ok: true }; } export function propfind(db: FileDavDoDb, input: PropfindInput): { ok: true; nodes: FileDavNodeView[] } | FileDavError { const target = nodeAtPath(db, input.path, input.nowMs, input.subjectId); if (!target) return fileDavError(404, "not_found", "Resource not found"); const { rows, exceeded } = treeFromLimited(db, target, input.depth, input.maxNodes); if (input.depth === "infinity" && exceeded) { return fileDavError(403, "forbidden", "Depth infinity result exceeds configured maximum"); } const nodeIds = rows.map((row) => row.node.id); const dead = deadPropsForNodes(db, nodeIds); const locks = activeLocks(db, input.nowMs); return { ok: true, nodes: rows.map((row) => ({ node: row.node, href: row.href, depth: row.depth, deadProps: dead.get(row.node.id) ?? [], locks: locks.filter((lock) => lockAppliesToHref(lock, row.href)), })), }; } export function mkcol(db: FileDavDoDb, input: MkcolInput): { ok: true; node: FileNodeRow } | FileDavError { ensureRoot(db, input.subjectId, input.nowMs); if (input.path.segments.length === 0) { return fileDavError(405, "method_not_allowed", "Root collection already exists"); } if (nodeAtPath(db, input.path, input.nowMs, input.subjectId)) { return fileDavError(405, "method_not_allowed", "Resource already exists"); } const parent = parentForPath(db, input.path, input.nowMs, input.subjectId); if (!parent) return fileDavError(409, "conflict", "Parent collection does not exist"); const href = resourceHref(input.path.segments, true); const locks = activeLocks(db, input.nowMs); if ( !fileIfHeaderMatches({ db, header: input.ifHeader, targetHref: href, targetEtag: null, locks, nowMs: input.nowMs, subjectId: input.subjectId, }) ) { return fileDavError(412, "precondition_failed", "If precondition failed"); } const conflict = findLockConflict(locks, href, input.lockTokens, null) ?? findLockConflictOnResource(locks, parent.parent.href, input.lockTokens, null); if (conflict) return fileDavError(423, "locked", "Resource is locked"); const node = createCollectionNode(db, { parentId: parent.parent.node.id, name: parent.name, nowMs: input.nowMs, }); touchNode(db, parent.parent.node.id, input.nowMs); insertChange(db, { nodeId: node.id, href, changeType: "created", changedAtMs: input.nowMs }); return { ok: true, node }; } export function proppatch(db: FileDavDoDb, input: ProppatchInput): { ok: true } | FileDavError { const target = nodeAtPath(db, input.path, input.nowMs, input.subjectId); if (!target) return fileDavError(404, "not_found", "Resource not found"); if (input.ifMatch && !strongEtagMatches(input.ifMatch, target.node.etag)) { return fileDavError(412, "precondition_failed", "If-Match precondition failed"); } if (input.ifNoneMatch && ifNoneMatchBlocks(input.ifNoneMatch, target.node.etag)) { return fileDavError(412, "precondition_failed", "If-None-Match precondition failed"); } const locks = activeLocks(db, input.nowMs); if ( !fileIfHeaderMatches({ db, header: input.ifHeader, targetHref: target.href, targetEtag: target.node.etag, locks, nowMs: input.nowMs, subjectId: input.subjectId, }) ) { return fileDavError(412, "precondition_failed", "If precondition failed"); } const conflict = findLockConflict(locks, target.href, input.lockTokens, null); if (conflict) return fileDavError(423, "locked", "Resource is locked"); for (const instruction of input.instructions) { if (instruction.kind === "set") setDeadProp(db, target.node.id, instruction); else removeDeadProp(db, target.node.id, instruction); } insertChange(db, { nodeId: target.node.id, href: target.href, changeType: "updated", changedAtMs: input.nowMs, }); return { ok: true }; } export function resolveFile(db: FileDavDoDb, input: ResolveFileInput): ResolveFileResult | FileDavError { const target = nodeAtPath(db, input.path, input.nowMs, input.subjectId); if (!target) return fileDavError(404, "not_found", "Resource not found"); if (target.node.kind !== "file") { return fileDavError(405, "method_not_allowed", "Resource is not a file"); } if (input.ifMatch && !strongEtagMatches(input.ifMatch, target.node.etag)) { return fileDavError(412, "precondition_failed", "If-Match precondition failed"); } if (input.ifNoneMatch && ifNoneMatchBlocks(input.ifNoneMatch, target.node.etag)) { return fileDavError(304, "precondition_failed", "Not modified"); } const blob = target.node.blobId ? (getBlob(db, target.node.blobId) ?? null) : null; if (target.node.blobId && !blob) return fileDavError(404, "not_found", "Blob metadata not found"); return { ok: true, node: target.node, blob, href: target.href }; }