Skip to content
File

Blob: src/worker/objects/file-dav/helpers.ts

typescript112 lines
1import { generateEtag } from "@/worker/dav/etag";
2import { ifHeaderMatches } from "@/worker/dav/locks";
3import { normalizeFilesPath, resourceHref, type NormalizedDavPath } from "@/worker/dav/paths";
4import type { FileDavDoDb } from "@/worker/db/file-dav-do/client";
5import {
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";
16import type { FileNodeRow } from "@/worker/db/file-dav-do/schema";
17import type { BlobGcCandidate, FileDavError, FileDavErrorCode } from "@/worker/objects/file-dav/types";
18import type { ParsedIfHeader } from "@/worker/dav/if-header";
19import type { FileLockRow } from "@/worker/db/file-dav-do/schema";
20 
21export function fileDavError(status: number, code: FileDavErrorCode, message: string): FileDavError {
22 return { ok: false, status, code, message };
23}
24 
25export function fileBlobKey(storageId: string, id: string): string {
26 return `blobs/${storageId}/${id}`;
27}
28 
29export function timeoutMs(nowMs: number, seconds: number): number {
30 return nowMs + Math.max(1, Math.min(seconds, 604_800)) * 1000;
31}
32 
33export function fileHref(path: NormalizedDavPath, node: FileNodeRow): string {
34 return resourceHref(path.segments, node.kind === "collection");
35}
36 
37export function segmentsFromHref(href: string): string[] {
38 return href
39 .slice("/files/".length)
40 .split("/")
41 .filter(Boolean)
42 .map((segment) => decodeURIComponent(segment));
43}
44 
45export function blobGcCandidates(db: FileDavDoDb): BlobGcCandidate[] {
46 return orphanBlobs(db).map((blob) => ({ blobId: blob.blobId, blobKey: blob.blobKey }));
47}
48 
49export 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 
55export 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 
73export 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}