import { generateEtag } from "@/worker/dav/etag"; import { ifHeaderMatches } from "@/worker/dav/locks"; import { normalizeFilesPath, resourceHref, type NormalizedDavPath } from "@/worker/dav/paths"; import type { FileDavDoDb } from "@/worker/db/file-dav-do/client"; import { copyDeadProps, createCollectionNode, createEmptyFileNode, nodeAtPath, orphanBlobs, treeFrom, updateBlobRefcount, upsertFileNode, type NodeAtPath, } from "@/worker/db/file-dav-do/repository"; import type { FileNodeRow } from "@/worker/db/file-dav-do/schema"; import type { BlobGcCandidate, FileDavError, FileDavErrorCode } from "@/worker/objects/file-dav/types"; import type { ParsedIfHeader } from "@/worker/dav/if-header"; import type { FileLockRow } from "@/worker/db/file-dav-do/schema"; export function fileDavError(status: number, code: FileDavErrorCode, message: string): FileDavError { return { ok: false, status, code, message }; } export function fileBlobKey(storageId: string, id: string): string { return `blobs/${storageId}/${id}`; } export function timeoutMs(nowMs: number, seconds: number): number { return nowMs + Math.max(1, Math.min(seconds, 604_800)) * 1000; } export function fileHref(path: NormalizedDavPath, node: FileNodeRow): string { return resourceHref(path.segments, node.kind === "collection"); } export function segmentsFromHref(href: string): string[] { return href .slice("/files/".length) .split("/") .filter(Boolean) .map((segment) => decodeURIComponent(segment)); } export function blobGcCandidates(db: FileDavDoDb): BlobGcCandidate[] { return orphanBlobs(db).map((blob) => ({ blobId: blob.blobId, blobKey: blob.blobKey })); } export function etagForHref(db: FileDavDoDb, href: string, nowMs: number, subjectId: string): string | null { const path = normalizeFilesPath(href); if (!path.ok) return null; return nodeAtPath(db, path.path, nowMs, subjectId)?.node.etag ?? null; } export function fileIfHeaderMatches(input: { db: FileDavDoDb; header: ParsedIfHeader; targetHref: string; targetEtag: string | null; locks: FileLockRow[]; nowMs: number; subjectId: string; }): boolean { return ifHeaderMatches({ header: input.header, targetHref: input.targetHref, targetEtag: input.targetEtag, locks: input.locks, getEtag: (href) => etagForHref(input.db, href, input.nowMs, input.subjectId), }); } export function copyTree( db: FileDavDoDb, source: NodeAtPath, parentId: string, name: string, depth: "0" | "infinity", nowMs: number, maxNodes: number, ): FileNodeRow { const newNode = source.node.kind === "collection" ? createCollectionNode(db, { parentId, name, nowMs }) : source.node.blobId ? upsertFileNode(db, { existing: undefined, parentId, name, blobId: source.node.blobId, size: source.node.size, contentType: source.node.contentType, contentLanguage: source.node.contentLanguage, etag: generateEtag(), nowMs, }).node : createEmptyFileNode(db, { parentId, name, contentType: source.node.contentType, etag: generateEtag(), nowMs, }); if (source.node.blobId) updateBlobRefcount(db, source.node.blobId, 1); copyDeadProps(db, source.node.id, newNode.id); if (source.node.kind === "collection" && depth === "infinity") { const children = treeFrom(db, source, "1", maxNodes + 1).filter((entry) => entry.depth === 1); for (const child of children) copyTree(db, child, newNode.id, child.node.name, "infinity", nowMs, maxNodes); } return newNode; }