Skip to content
File

Blob: src/worker/objects/file-dav/properties.ts

typescript154 lines
1import { findLockConflict, findLockConflictOnResource, lockAppliesToHref } from "@/worker/dav/locks";
2import { resourceHref } from "@/worker/dav/paths";
3import type { FileDavDoDb } from "@/worker/db/file-dav-do/client";
4import {
5 activeLocks,
6 createCollectionNode,
7 deadPropsForNodes,
8 ensureRoot,
9 getBlob,
10 insertChange,
11 nodeAtPath,
12 parentForPath,
13 removeDeadProp,
14 setDeadProp,
15 touchNode,
16 treeFromLimited,
17} from "@/worker/db/file-dav-do/repository";
18import type { FileNodeRow } from "@/worker/db/file-dav-do/schema";
19import { fileDavError } from "@/worker/objects/file-dav/helpers";
20import { fileIfHeaderMatches } from "@/worker/objects/file-dav/helpers";
21import type {
22 EnsureInitializedInput,
23 FileDavError,
24 FileDavNodeView,
25 MkcolInput,
26 PropfindInput,
27 ProppatchInput,
28 ResolveFileInput,
29 ResolveFileResult,
30} from "@/worker/objects/file-dav/types";
31import { ifNoneMatchBlocks, strongEtagMatches } from "@/worker/dav/etag";
32 
33export function ensureInitialized(db: FileDavDoDb, input: EnsureInitializedInput): { ok: true } {
34 ensureRoot(db, input.subjectId, input.nowMs);
35 return { ok: true };
36}
37 
38export function propfind(db: FileDavDoDb, input: PropfindInput): { ok: true; nodes: FileDavNodeView[] } | FileDavError {
39 const target = nodeAtPath(db, input.path, input.nowMs, input.subjectId);
40 if (!target) return fileDavError(404, "not_found", "Resource not found");
41 const { rows, exceeded } = treeFromLimited(db, target, input.depth, input.maxNodes);
42 if (input.depth === "infinity" && exceeded) {
43 return fileDavError(403, "forbidden", "Depth infinity result exceeds configured maximum");
44 }
45 
46 const nodeIds = rows.map((row) => row.node.id);
47 const dead = deadPropsForNodes(db, nodeIds);
48 const locks = activeLocks(db, input.nowMs);
49 return {
50 ok: true,
51 nodes: rows.map((row) => ({
52 node: row.node,
53 href: row.href,
54 depth: row.depth,
55 deadProps: dead.get(row.node.id) ?? [],
56 locks: locks.filter((lock) => lockAppliesToHref(lock, row.href)),
57 })),
58 };
59}
60 
61export function mkcol(db: FileDavDoDb, input: MkcolInput): { ok: true; node: FileNodeRow } | FileDavError {
62 ensureRoot(db, input.subjectId, input.nowMs);
63 if (input.path.segments.length === 0) {
64 return fileDavError(405, "method_not_allowed", "Root collection already exists");
65 }
66 if (nodeAtPath(db, input.path, input.nowMs, input.subjectId)) {
67 return fileDavError(405, "method_not_allowed", "Resource already exists");
68 }
69 const parent = parentForPath(db, input.path, input.nowMs, input.subjectId);
70 if (!parent) return fileDavError(409, "conflict", "Parent collection does not exist");
71 const href = resourceHref(input.path.segments, true);
72 const locks = activeLocks(db, input.nowMs);
73 if (
74 !fileIfHeaderMatches({
75 db,
76 header: input.ifHeader,
77 targetHref: href,
78 targetEtag: null,
79 locks,
80 nowMs: input.nowMs,
81 subjectId: input.subjectId,
82 })
83 ) {
84 return fileDavError(412, "precondition_failed", "If precondition failed");
85 }
86 const conflict =
87 findLockConflict(locks, href, input.lockTokens, null) ??
88 findLockConflictOnResource(locks, parent.parent.href, input.lockTokens, null);
89 if (conflict) return fileDavError(423, "locked", "Resource is locked");
90 const node = createCollectionNode(db, {
91 parentId: parent.parent.node.id,
92 name: parent.name,
93 nowMs: input.nowMs,
94 });
95 touchNode(db, parent.parent.node.id, input.nowMs);
96 insertChange(db, { nodeId: node.id, href, changeType: "created", changedAtMs: input.nowMs });
97 return { ok: true, node };
98}
99 
100export function proppatch(db: FileDavDoDb, input: ProppatchInput): { ok: true } | FileDavError {
101 const target = nodeAtPath(db, input.path, input.nowMs, input.subjectId);
102 if (!target) return fileDavError(404, "not_found", "Resource not found");
103 if (input.ifMatch && !strongEtagMatches(input.ifMatch, target.node.etag)) {
104 return fileDavError(412, "precondition_failed", "If-Match precondition failed");
105 }
106 if (input.ifNoneMatch && ifNoneMatchBlocks(input.ifNoneMatch, target.node.etag)) {
107 return fileDavError(412, "precondition_failed", "If-None-Match precondition failed");
108 }
109 const locks = activeLocks(db, input.nowMs);
110 if (
111 !fileIfHeaderMatches({
112 db,
113 header: input.ifHeader,
114 targetHref: target.href,
115 targetEtag: target.node.etag,
116 locks,
117 nowMs: input.nowMs,
118 subjectId: input.subjectId,
119 })
120 ) {
121 return fileDavError(412, "precondition_failed", "If precondition failed");
122 }
123 const conflict = findLockConflict(locks, target.href, input.lockTokens, null);
124 if (conflict) return fileDavError(423, "locked", "Resource is locked");
125 for (const instruction of input.instructions) {
126 if (instruction.kind === "set") setDeadProp(db, target.node.id, instruction);
127 else removeDeadProp(db, target.node.id, instruction);
128 }
129 insertChange(db, {
130 nodeId: target.node.id,
131 href: target.href,
132 changeType: "updated",
133 changedAtMs: input.nowMs,
134 });
135 return { ok: true };
136}
137 
138export function resolveFile(db: FileDavDoDb, input: ResolveFileInput): ResolveFileResult | FileDavError {
139 const target = nodeAtPath(db, input.path, input.nowMs, input.subjectId);
140 if (!target) return fileDavError(404, "not_found", "Resource not found");
141 if (target.node.kind !== "file") {
142 return fileDavError(405, "method_not_allowed", "Resource is not a file");
143 }
144 if (input.ifMatch && !strongEtagMatches(input.ifMatch, target.node.etag)) {
145 return fileDavError(412, "precondition_failed", "If-Match precondition failed");
146 }
147 if (input.ifNoneMatch && ifNoneMatchBlocks(input.ifNoneMatch, target.node.etag)) {
148 return fileDavError(304, "precondition_failed", "Not modified");
149 }
150 const blob = target.node.blobId ? (getBlob(db, target.node.blobId) ?? null) : null;
151 if (target.node.blobId && !blob) return fileDavError(404, "not_found", "Blob metadata not found");
152 return { ok: true, node: target.node, blob, href: target.href };
153}