Skip to content
File

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

typescript85 lines
1import type { FileDavDoDb } from "@/worker/db/file-dav-do/client";
2import {
3 deleteLocks,
4 ensureRoot,
5 expiredLocks,
6 expiredPendingUploads,
7 getBlob,
8 markPendingUpload,
9 pendingUploadByBlob,
10 rootNode,
11} from "@/worker/db/file-dav-do/repository";
12import type {
13 BlobGcCandidate,
14 CleanupExpiredLocksInput,
15 CleanupExpiredPendingUploadsInput,
16 FileDavRepairInput,
17 VerifyBlobUnreferencedInput,
18} from "@/worker/objects/file-dav/types";
19 
20function storedSubjectId(db: FileDavDoDb): string | null {
21 return db.query.fileMeta.findFirst({ where: (meta, { eq }) => eq(meta.key, "subject_id") }).sync()?.value ?? null;
22}
23 
24export function verifyBlobUnreferenced(
25 db: FileDavDoDb,
26 input: VerifyBlobUnreferencedInput,
27): { ok: true; unreferenced: boolean } {
28 const existingSubjectId = storedSubjectId(db);
29 if (existingSubjectId && existingSubjectId !== input.subjectId) return { ok: true, unreferenced: false };
30 
31 const pending = pendingUploadByBlob(db, input.blobId);
32 if (pending?.state === "pending") return { ok: true, unreferenced: false };
33 
34 const blob = getBlob(db, input.blobId);
35 if (!blob) return { ok: true, unreferenced: true };
36 if (blob.blobKey !== input.blobKey) return { ok: true, unreferenced: false };
37 return { ok: true, unreferenced: blob.refcount <= 0 };
38}
39 
40export function cleanupExpiredPendingUploads(
41 db: FileDavDoDb,
42 input: CleanupExpiredPendingUploadsInput,
43): { ok: true; cleaned: number; blobGc: BlobGcCandidate[] } {
44 ensureRoot(db, input.subjectId, input.nowMs);
45 const expired = expiredPendingUploads(db, input.nowMs, input.limit);
46 for (const upload of expired) markPendingUpload(db, upload.uploadId, "aborted");
47 return {
48 ok: true,
49 cleaned: expired.length,
50 blobGc: expired.map((upload) => ({ blobId: upload.blobId, blobKey: upload.blobKey })),
51 };
52}
53 
54export function cleanupExpiredLocks(db: FileDavDoDb, input: CleanupExpiredLocksInput): { ok: true; cleaned: number } {
55 ensureRoot(db, input.subjectId, input.nowMs);
56 const expired = expiredLocks(db, input.nowMs, input.limit);
57 deleteLocks(
58 db,
59 expired.map((lock) => lock.token),
60 );
61 return { ok: true, cleaned: expired.length };
62}
63 
64export function repairFileDav(db: FileDavDoDb, input: FileDavRepairInput) {
65 if (!rootNode(db)) ensureRoot(db, input.subjectId, input.nowMs);
66 const pendingUploads = cleanupExpiredPendingUploads(db, {
67 subjectId: input.subjectId,
68 storageId: input.storageId,
69 nowMs: input.nowMs,
70 limit: input.limit,
71 });
72 const locks = cleanupExpiredLocks(db, {
73 subjectId: input.subjectId,
74 storageId: input.storageId,
75 nowMs: input.nowMs,
76 limit: input.limit,
77 });
78 return {
79 ok: true as const,
80 cleanedPendingUploads: pendingUploads.cleaned,
81 cleanedLocks: locks.cleaned,
82 blobGc: pendingUploads.blobGc,
83 };
84}