Skip to content
File

Blob: src/worker/webdav/lock.ts

typescript77 lines
1import { davError, davResponse, emptyResponse } from "@/worker/dav/http";
2import { maxXmlBodyBytes } from "@/worker/dav/limits";
3import type { SubjectRow } from "@/worker/db/d1/schema";
4import type { AppContext } from "@/worker/types";
5import { parseLockInfoXml } from "@/worker/xml/dav-request";
6import { lockXml, parseTimeout } from "@/worker/webdav/lock-xml";
7import { fileDavObject, resultError, type NormalizedFilesRequestPath } from "@/worker/webdav/object";
8import { readFileDavWriteRequest } from "@/worker/webdav/request";
9 
10export async function handleLock(c: AppContext, subject: SubjectRow, path: NormalizedFilesRequestPath) {
11 const writeRequest = readFileDavWriteRequest(c.req.raw);
12 if (writeRequest instanceof Response) return writeRequest;
13 const body = await c.req.text();
14 const refresh = body.trim() === "";
15 let lockInfo: { scope: "exclusive" | "shared"; ownerXml: string } | null = null;
16 if (!refresh) {
17 try {
18 lockInfo = parseLockInfoXml(body, maxXmlBodyBytes(c.env));
19 } catch (cause) {
20 return davError(400, cause instanceof Error ? cause.message : "Invalid LOCK body");
21 }
22 } else if (writeRequest.lockTokens.length === 0) {
23 return davError(400, "LOCK refresh requires an If header lock token");
24 }
25 let depth: "0" | "infinity";
26 if (refresh) {
27 // RFC 4918 9.10.2: LOCK refresh ignores Depth; honor whatever the existing lock recorded.
28 depth = "infinity";
29 } else {
30 const depthHeader = c.req.header("depth");
31 const normalizedDepth = depthHeader?.trim().toLowerCase();
32 const parsedDepth =
33 normalizedDepth === undefined
34 ? "infinity"
35 : normalizedDepth === "0" || normalizedDepth === "infinity"
36 ? normalizedDepth
37 : null;
38 if (parsedDepth === null) return davError(400, "LOCK Depth must be 0 or infinity");
39 depth = parsedDepth;
40 }
41 const result = await fileDavObject(c.env, subject.storageId).lock({
42 subjectId: subject.id,
43 storageId: subject.storageId,
44 path: path.path,
45 ownerXml: lockInfo?.ownerXml ?? null,
46 scope: lockInfo?.scope ?? "exclusive",
47 depth,
48 timeoutSeconds: parseTimeout(c.req.header("timeout") ?? null),
49 ifHeader: writeRequest.ifHeader,
50 lockTokens: writeRequest.lockTokens,
51 nowMs: writeRequest.nowMs,
52 refresh,
53 });
54 if (!result.ok) return resultError(result);
55 const bodyXml = `<?xml version="1.0" encoding="utf-8"?><D:prop xmlns:D="DAV:"><D:lockdiscovery>${lockXml(
56 result.lock,
57 )}</D:lockdiscovery></D:prop>`;
58 // RFC 4918 9.10.1: Lock-Token response header is only for new lock creation, not refresh.
59 const headers: Record<string, string> = {};
60 if (!refresh) headers["Lock-Token"] = `<${result.lock.token}>`;
61 return davResponse(bodyXml, result.created ? 201 : 200, headers);
62}
63 
64export async function handleUnlock(c: AppContext, subject: SubjectRow, path: NormalizedFilesRequestPath) {
65 const token = c.req.header("lock-token");
66 if (!token) return davError(400, "Missing Lock-Token header");
67 const result = await fileDavObject(c.env, subject.storageId).unlock({
68 subjectId: subject.id,
69 storageId: subject.storageId,
70 path: path.path,
71 lockToken: token,
72 nowMs: Date.now(),
73 });
74 if (!result.ok) return resultError(result);
75 return emptyResponse(204);
76}