File
Blob: src/worker/webdav/lock.ts
| 1 | import { davError, davResponse, emptyResponse } from "@/worker/dav/http"; |
| 2 | import { maxXmlBodyBytes } from "@/worker/dav/limits"; |
| 3 | import type { SubjectRow } from "@/worker/db/d1/schema"; |
| 4 | import type { AppContext } from "@/worker/types"; |
| 5 | import { parseLockInfoXml } from "@/worker/xml/dav-request"; |
| 6 | import { lockXml, parseTimeout } from "@/worker/webdav/lock-xml"; |
| 7 | import { fileDavObject, resultError, type NormalizedFilesRequestPath } from "@/worker/webdav/object"; |
| 8 | import { readFileDavWriteRequest } from "@/worker/webdav/request"; |
| 9 | |
| 10 | export 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 | |
| 64 | export 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 | } |