File
Blob: src/worker/objects/file-dav/helpers.ts
| 1 | import { generateEtag } from "@/worker/dav/etag"; |
| 2 | import { ifHeaderMatches } from "@/worker/dav/locks"; |
| 3 | import { normalizeFilesPath, resourceHref, type NormalizedDavPath } from "@/worker/dav/paths"; |
| 4 | import type { FileDavDoDb } from "@/worker/db/file-dav-do/client"; |
| 5 | import { |
| 6 | copyDeadProps, |
| 7 | createCollectionNode, |
| 8 | createEmptyFileNode, |
| 9 | nodeAtPath, |
| 10 | orphanBlobs, |
| 11 | treeFrom, |
| 12 | updateBlobRefcount, |
| 13 | upsertFileNode, |
| 14 | type NodeAtPath, |
| 15 | } from "@/worker/db/file-dav-do/repository"; |
| 16 | import type { FileNodeRow } from "@/worker/db/file-dav-do/schema"; |
| 17 | import type { BlobGcCandidate, FileDavError, FileDavErrorCode } from "@/worker/objects/file-dav/types"; |
| 18 | import type { ParsedIfHeader } from "@/worker/dav/if-header"; |
| 19 | import type { FileLockRow } from "@/worker/db/file-dav-do/schema"; |
| 20 | |
| 21 | export function fileDavError(status: number, code: FileDavErrorCode, message: string): FileDavError { |
| 22 | return { ok: false, status, code, message }; |
| 23 | } |
| 24 | |
| 25 | export function fileBlobKey(storageId: string, id: string): string { |
| 26 | return `blobs/${storageId}/${id}`; |
| 27 | } |
| 28 | |
| 29 | export function timeoutMs(nowMs: number, seconds: number): number { |
| 30 | return nowMs + Math.max(1, Math.min(seconds, 604_800)) * 1000; |
| 31 | } |
| 32 | |
| 33 | export function fileHref(path: NormalizedDavPath, node: FileNodeRow): string { |
| 34 | return resourceHref(path.segments, node.kind === "collection"); |
| 35 | } |
| 36 | |
| 37 | export function segmentsFromHref(href: string): string[] { |
| 38 | return href |
| 39 | .slice("/files/".length) |
| 40 | .split("/") |
| 41 | .filter(Boolean) |
| 42 | .map((segment) => decodeURIComponent(segment)); |
| 43 | } |
| 44 | |
| 45 | export function blobGcCandidates(db: FileDavDoDb): BlobGcCandidate[] { |
| 46 | return orphanBlobs(db).map((blob) => ({ blobId: blob.blobId, blobKey: blob.blobKey })); |
| 47 | } |
| 48 | |
| 49 | export function etagForHref(db: FileDavDoDb, href: string, nowMs: number, subjectId: string): string | null { |
| 50 | const path = normalizeFilesPath(href); |
| 51 | if (!path.ok) return null; |
| 52 | return nodeAtPath(db, path.path, nowMs, subjectId)?.node.etag ?? null; |
| 53 | } |
| 54 | |
| 55 | export function fileIfHeaderMatches(input: { |
| 56 | db: FileDavDoDb; |
| 57 | header: ParsedIfHeader; |
| 58 | targetHref: string; |
| 59 | targetEtag: string | null; |
| 60 | locks: FileLockRow[]; |
| 61 | nowMs: number; |
| 62 | subjectId: string; |
| 63 | }): boolean { |
| 64 | return ifHeaderMatches({ |
| 65 | header: input.header, |
| 66 | targetHref: input.targetHref, |
| 67 | targetEtag: input.targetEtag, |
| 68 | locks: input.locks, |
| 69 | getEtag: (href) => etagForHref(input.db, href, input.nowMs, input.subjectId), |
| 70 | }); |
| 71 | } |
| 72 | |
| 73 | export function copyTree( |
| 74 | db: FileDavDoDb, |
| 75 | source: NodeAtPath, |
| 76 | parentId: string, |
| 77 | name: string, |
| 78 | depth: "0" | "infinity", |
| 79 | nowMs: number, |
| 80 | maxNodes: number, |
| 81 | ): FileNodeRow { |
| 82 | const newNode = |
| 83 | source.node.kind === "collection" |
| 84 | ? createCollectionNode(db, { parentId, name, nowMs }) |
| 85 | : source.node.blobId |
| 86 | ? upsertFileNode(db, { |
| 87 | existing: undefined, |
| 88 | parentId, |
| 89 | name, |
| 90 | blobId: source.node.blobId, |
| 91 | size: source.node.size, |
| 92 | contentType: source.node.contentType, |
| 93 | contentLanguage: source.node.contentLanguage, |
| 94 | etag: generateEtag(), |
| 95 | nowMs, |
| 96 | }).node |
| 97 | : createEmptyFileNode(db, { |
| 98 | parentId, |
| 99 | name, |
| 100 | contentType: source.node.contentType, |
| 101 | etag: generateEtag(), |
| 102 | nowMs, |
| 103 | }); |
| 104 | if (source.node.blobId) updateBlobRefcount(db, source.node.blobId, 1); |
| 105 | copyDeadProps(db, source.node.id, newNode.id); |
| 106 | if (source.node.kind === "collection" && depth === "infinity") { |
| 107 | const children = treeFrom(db, source, "1", maxNodes + 1).filter((entry) => entry.depth === 1); |
| 108 | for (const child of children) copyTree(db, child, newNode.id, child.node.name, "infinity", nowMs, maxNodes); |
| 109 | } |
| 110 | return newNode; |
| 111 | } |