import { davError, davResponse, emptyResponse } from "@/worker/dav/http";
import { maxXmlBodyBytes } from "@/worker/dav/limits";
import type { SubjectRow } from "@/worker/db/d1/schema";
import type { AppContext } from "@/worker/types";
import { parseLockInfoXml } from "@/worker/xml/dav-request";
import { lockXml, parseTimeout } from "@/worker/webdav/lock-xml";
import { fileDavObject, resultError, type NormalizedFilesRequestPath } from "@/worker/webdav/object";
import { readFileDavWriteRequest } from "@/worker/webdav/request";
export async function handleLock(c: AppContext, subject: SubjectRow, path: NormalizedFilesRequestPath) {
const writeRequest = readFileDavWriteRequest(c.req.raw);
if (writeRequest instanceof Response) return writeRequest;
const body = await c.req.text();
const refresh = body.trim() === "";
let lockInfo: { scope: "exclusive" | "shared"; ownerXml: string } | null = null;
if (!refresh) {
try {
lockInfo = parseLockInfoXml(body, maxXmlBodyBytes(c.env));
} catch (cause) {
return davError(400, cause instanceof Error ? cause.message : "Invalid LOCK body");
}
} else if (writeRequest.lockTokens.length === 0) {
return davError(400, "LOCK refresh requires an If header lock token");
}
let depth: "0" | "infinity";
if (refresh) {
// RFC 4918 9.10.2: LOCK refresh ignores Depth; honor whatever the existing lock recorded.
depth = "infinity";
} else {
const depthHeader = c.req.header("depth");
const normalizedDepth = depthHeader?.trim().toLowerCase();
const parsedDepth =
normalizedDepth === undefined
? "infinity"
: normalizedDepth === "0" || normalizedDepth === "infinity"
? normalizedDepth
: null;
if (parsedDepth === null) return davError(400, "LOCK Depth must be 0 or infinity");
depth = parsedDepth;
}
const result = await fileDavObject(c.env, subject.storageId).lock({
subjectId: subject.id,
storageId: subject.storageId,
path: path.path,
ownerXml: lockInfo?.ownerXml ?? null,
scope: lockInfo?.scope ?? "exclusive",
depth,
timeoutSeconds: parseTimeout(c.req.header("timeout") ?? null),
ifHeader: writeRequest.ifHeader,
lockTokens: writeRequest.lockTokens,
nowMs: writeRequest.nowMs,
refresh,
});
if (!result.ok) return resultError(result);
const bodyXml = `${lockXml(
result.lock,
)}`;
// RFC 4918 9.10.1: Lock-Token response header is only for new lock creation, not refresh.
const headers: Record = {};
if (!refresh) headers["Lock-Token"] = `<${result.lock.token}>`;
return davResponse(bodyXml, result.created ? 201 : 200, headers);
}
export async function handleUnlock(c: AppContext, subject: SubjectRow, path: NormalizedFilesRequestPath) {
const token = c.req.header("lock-token");
if (!token) return davError(400, "Missing Lock-Token header");
const result = await fileDavObject(c.env, subject.storageId).unlock({
subjectId: subject.id,
storageId: subject.storageId,
path: path.path,
lockToken: token,
nowMs: Date.now(),
});
if (!result.ok) return resultError(result);
return emptyResponse(204);
}