import type { LoadedEntry } from "@vrtmrz/livesync-commonlib/compat/common/types"; import { createBlob, isDocContentSame, readAsBlob } from "@vrtmrz/livesync-commonlib/compat/common/utils"; import type { StorageAccess } from "@vrtmrz/livesync-commonlib/compat/interfaces/StorageAccess"; import type { IFileHandler } from "@vrtmrz/livesync-commonlib/compat/interfaces/FileHandler"; import { inspectFileDatabaseInfo, readFileDatabaseRevisionLocally, type FileDatabaseInfo, type FileDatabaseInfoCore, type RevisionDatabaseInfo, } from "./fileDatabaseInfo"; export type FileRepairCore = FileDatabaseInfoCore & { fileHandler: Pick; storageAccess: FileDatabaseInfoCore["storageAccess"] & Pick; }; export type FileRepairRevision = { role: "winner" | "conflict"; metadata: RevisionDatabaseInfo; contentReadable: boolean; contentMatchesStorage: boolean | null; loadedEntry: LoadedEntry | false; }; export type FileRepairInspection = { information: FileDatabaseInfo; revisions: FileRepairRevision[]; requiresAttention: boolean; }; export type DiscardUnreadableRevisionResult = | "discarded" | "failed" | "no-longer-live" | "revision-is-readable"; export type DiscardLiveBranchResult = "discarded" | "failed" | "no-longer-live" | "only-live-revision"; export async function inspectFileRepair(core: FileRepairCore, path: string): Promise { const information = await inspectFileDatabaseInfo(core, path); const storageContent = information.storage.exists ? createBlob(await core.storageAccess.readHiddenFileBinary(path)) : undefined; const revisions: FileRepairRevision[] = []; for (const metadata of information.database.revisions) { const loadedEntry = metadata.deleted || !metadata.contentAvailableLocally ? false : await readFileDatabaseRevisionLocally(core, path, metadata.revision ?? ""); const contentReadable = metadata.deleted || loadedEntry !== false; const contentMatchesStorage = storageContent && loadedEntry !== false ? await isDocContentSame(storageContent, readAsBlob(loadedEntry)) : null; revisions.push({ role: metadata.current ? "winner" : "conflict", metadata, contentReadable, contentMatchesStorage, loadedEntry, }); } const winner = revisions.find(({ role }) => role === "winner"); const winnerRepresentsStoredFile = winner !== undefined && !winner.metadata.deleted; const databaseAndStorageDiffer = information.storage.exists !== winnerRepresentsStoredFile || (information.storage.exists && winnerRepresentsStoredFile && winner.contentMatchesStorage === false); const unreadableLiveRevision = information.database.unavailableConflictRevisions.length > 0 || revisions.some(({ contentReadable }) => !contentReadable); const requiresAttention = databaseAndStorageDiffer || information.database.conflictCount > 0 || unreadableLiveRevision || (information.database.exists && winner === undefined); return { information, revisions, requiresAttention, }; } export async function discardUnreadableLiveRevision( core: FileRepairCore, path: string, revision: string ): Promise { const latest = await inspectFileDatabaseInfo(core, path); const liveRevisions = [ latest.database.currentRevision, ...latest.database.conflictRevisions, ].filter((candidate): candidate is string => candidate !== null); if (!liveRevisions.includes(revision)) { return "no-longer-live"; } const metadata = latest.database.revisions.find((candidate) => candidate.revision === revision); const metadataUnavailable = latest.database.unavailableConflictRevisions.includes(revision); if (!metadataUnavailable && (metadata?.deleted || metadata?.contentAvailableLocally)) { return "revision-is-readable"; } const deleted = await core.fileHandler.deleteRevisionFromDB(latest.databasePath, revision); return deleted ? "discarded" : "failed"; } export async function discardLiveBranch( core: FileRepairCore, path: string, revision: string ): Promise { const latest = await inspectFileDatabaseInfo(core, path); const liveRevisions = [ latest.database.currentRevision, ...latest.database.conflictRevisions, ].filter((candidate): candidate is string => candidate !== null); if (!liveRevisions.includes(revision)) { return "no-longer-live"; } if (liveRevisions.length < 2) { return "only-live-revision"; } const deleted = await core.fileHandler.deleteRevisionFromDB(latest.databasePath, revision); return deleted ? "discarded" : "failed"; }