Files
obsidian-livesync/src/serviceFeatures/fileRepair.ts
T

133 lines
5.0 KiB
TypeScript

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<IFileHandler, "deleteRevisionFromDB">;
storageAccess: FileDatabaseInfoCore["storageAccess"] & Pick<StorageAccess, "readHiddenFileBinary">;
};
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<FileRepairInspection> {
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<DiscardUnreadableRevisionResult> {
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<DiscardLiveBranchResult> {
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";
}